Git's Mental Model: Understanding What's Really Happening
Understanding Git's Mental Model: What's Really Happening Under the Hood
Every developer uses Git. Most developers think they understand Git. But here's a uncomfortable truth that becomes apparent the moment something goes wrong: most developers have only scratched the surface of what Git actually is. They know the commands—`commit`, `push`, `merge`, `rebase`—but they don't understand what's happening when those commands execute. They've built a mental model based on analogies and surface-level behavior, and that model fails them precisely when they need it most: during conflicts, when rebasing goes sideways, or when they accidentally lose work they thought was safely committed.
The problem isn't intelligence or experience. It's that Git is genuinely counterintuitive if you approach it with the wrong mental model. Most people think of Git as a file backup system with some versioning features bolted on. You save your code, Git saves a snapshot, you can go back to old snapshots. Simple, right?
Wrong. Git is nothing like a file backup system. Git is, at its core, a content-addressable filesystem with a version control system built on top. Once you truly internalize what that means—once you can see your repository not as a collection of files and folders, but as a directed acyclic graph of objects connected by references—the entire system clicks into place. Commands that seemed mysterious become obvious. Problems that seemed frightening become tractable. You'll stop treating Git like a magic box and start treating it like the elegant, predictable tool it actually is.
This chapter is all about rebuilding your mental model from the ground up. We're going to dig into the `.git` directory structure, explore every type of Git object (blobs, trees, commits, and tags), and understand how refs work as pointers to commits. This foundation will make everything else in this book—every branching strategy, every merge technique, every recovery operation—make intuitive sense.
The Mental Model Shift: From Files to Objects
Let's start with the fundamental concept that changes everything: Git is not a file storage system.
In a traditional file system, you store files by their location. `/home/sarah/projects/webapp/src/main.js` is meaningful—it's the path that tells the system where to find your file. If you move the file to `/home/sarah/backup/old/main.js`, the system loses track of it unless you update all the references.
Git works completely differently. In Git, content is stored by its cryptographic hash. This is called content-addressable storage, and it's the single most important concept in understanding Git.
Every piece of content in Git—every file, every directory, every commit—gets a unique identifier based on its content. This identifier is a SHA-1 hash, a 40-character hexadecimal string that looks something like this: `a1b2c3d4e5f6789012345678901234567890abcd`. The hash isn't random; it's deterministic. The same content will always produce the same hash. Change even a single character in a file, and the hash changes completely.
```bash $ echo "Hello, World!" | git hash-object --stdin 8ab686eafeb1f44702738c8b0f24f2567c36da6d
$ echo "Hello, World" | git hash-object --stdin [Different hash - missing the exclamation mark] ed1a3c57a8b0f16d6e2c8f4a1b7e9d3c5f6a8b1d ```
This means something profound: if you have the hash, you can retrieve the content. The hash IS the address. There are no paths, no locations, no moving files around. Content with a given hash either exists in its entirety or it doesn't exist at all.
Think about what this implies for version control. If I commit code today and it gets hash `abc123`, that hash uniquely identifies exactly that content. Tomorrow, if I make changes and commit again, I get a different hash: `def456`. The old content (`abc123`) still exists—Git hasn't overwritten it or lost it. It's still addressable, still retrievable, still part of the repository's history. Git never truly deletes anything; it just stops referencing it.
This is why Git is so fast at computing diffs and checking history. The content doesn't need to be "archived" somewhere separate from the current state. Everything is stored as immutable objects, and the history is just a web of references connecting these objects.
Inside the .git Directory
Now that you understand content-addressable storage, let's look at how Git actually organizes this on disk. When you run `git init` in a new directory, Git creates a hidden `.git` folder. This folder is your entire repository—the complete database of objects, references, configuration, and metadata that represents your project's history.
Let's explore the structure:
```bash $ tree -L 2 .git .git ├── HEAD # Points to current branch/commit ├── config # Repository-specific settings ├── description # Repository description (for GitWeb) ├── hooks/ # Client/server-side scripts ├── info/ # Additional repository info ├── objects/ # THE OBJECT DATABASE ├── packed-refs # Packed references (optimization) └── refs/ # Branch and tag references ```
Most of these directories are administrative overhead, but three are critical to understanding Git's internals: `objects/`, `refs/`, and `HEAD`. Let's examine each.
The Object Database: Where Everything Lives
The `objects/` directory is where Git stores all your content. Every piece of data in a Git repository ultimately lives here. When you commit code, Git creates objects representing that code and stores them in this directory.
The objects directory uses a clever storage strategy. Objects are stored in subdirectories named after the first two characters of their hash. So an object with hash `a1b2c3d4...` would be stored at `.git/objects/a1/b2c3d4...`. This isn't just for organization; it's a filesystem optimization. Most filesystems slow down when a single directory contains hundreds of thousands of files. By splitting objects across 256 subdirectories (00 through ff), Git keeps each directory manageable.
```bash $ find .git/objects -type f | head -20 .git/objects/5a/bc1234567890abcdef1234567890abcdef1234 .git/objects/a1/b2c3d4e5f6789012345678901234567890abcd ... ```
When you're working with a fresh repository, the objects directory will be mostly empty. It fills up as you create commits, add files, and perform operations that create new content.
References: The Pointers to Your History
While objects are the immutable content, references (refs) are the mutable pointers that give that content structure. If objects are the atoms of Git, refs are the molecules—they give meaning and organization to the raw data.
References live in the `refs/` directory, and they come in several flavors:
``` .git/refs/ ├── heads/ # Local branches (refs/heads/main, refs/heads/feature-x) ├── tags/ # Tags (refs/tags/v1.0.0, refs/tags/beta) └── remotes/ # Remote tracking branches ├── origin/ ├── upstream/ └── ... ```
Each ref is a simple text file containing a single line: a SHA-1 hash. When you're on the `main` branch and you commit, Git doesn't modify the branch name itself (that would be confusing). Instead, Git updates the file at `refs/heads/main` to point to your new commit.
```bash $ cat .git/refs/heads/main a1b2c3d4e5f6789012345678901234567890abcd
$ cat .git/refs/heads/feature-login 98f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f ```
This simplicity is elegant. A branch in Git is just a file containing a hash. Creating a branch is creating a file. Deleting a branch is deleting a file. There are no copies of code, no branching overhead, no complicated machinery. You're just creating a new pointer.
HEAD: Where You Are Right Now
The `HEAD` file is special. It points to the current reference—whichever branch or commit you're currently on. Git consults HEAD to know where to attach new commits.
```bash $ cat .git/HEAD ref