Git's Core Architecture: Objects, References, and the Immutable History
Git's power and flexibility stem from a deceptively simple yet profoundly elegant core architecture. Unlike many version control systems that track file differences or rely on a central server's state, Git is fundamentally a content-addressable filesystem with a directed acyclic graph (DAG) at its heart. This chapter deconstructs that architecture—the immutable objects, the references that point to them, and the immutable history they form. Mastering these foundational concepts is not an academic exercise; it is the single most critical step in moving from casual Git user to someone who can confidently execute advanced operations like interactive rebasing, complex cherry-picking, and safe history rewriting. Without this mental model, you are effectively driving a high-performance vehicle while blindfolded, reacting to commands without understanding the mechanics beneath you. We will build this model from the ground up, visualize it with diagrams, and solidify it with hands-on exploration using low-level Git commands.
The Content-Addressable Storage Foundation
At its core, Git does not store files and changes in the way you might intuitively expect. It does not store *deltas* (differences between versions) by default, nor does it store a snapshot of the entire project for every commit in an uncompressed form. Instead, Git stores snapshots of your project's entire state, but it does so in a highly efficient, content-addressable manner.
What is Content-Addressable Storage?
A content-addressable storage system retrieves data not by its location (like a traditional filesystem path) but by its *content*. The content itself is used to generate a unique cryptographic hash, typically SHA-1 in Git's case (though there are plans to migrate to SHA-256). This hash serves as the object's name, its sole identifier, and its address. The formula is conceptually simple:
`object_id = SHA-1(content)`
This means: 1. Immutability: If the content changes even by a single bit, the resulting SHA-1 hash will be completely different. Therefore, once an object is created and stored, it can never be altered. You can only create a new object with new content. 2. Deduplication: The exact same content will always produce the exact same SHA-1 hash. If you have the same file in two different commits, or even in two different repositories, Git stores only one copy of the blob object. This is a primary source of Git's incredible storage efficiency. 3. Integrity Verification: The hash is a cryptographic fingerprint. At any time, you can recompute the SHA-1 of an object's content and compare it to its stored ID. If they differ, the data is corrupted.
This design is why you can safely push your repository to an untrusted server—the server cannot alter your history without breaking all the hashes, which your local Git would immediately detect upon fetching.
The `.git/objects` Directory: The Actual Database
All Git objects are stored, in their compressed and serialized form, inside the `.git/objects` directory. You will see two types of subdirectories there:
1. `objects/??/` (loose objects): Where `??` are the first two characters of the object's SHA-1 hash. The rest of the hash is the filename. For example, the object with hash `a1b2c3d4...` would be stored as `.git/objects/a1/b2c3d4...`. This two-level directory structure prevents filesystem slowdowns when a single directory contains millions of files. 2. `objects/pack/` (packfiles): For efficiency, Git periodically compresses groups of objects into a single `*.pack` file along with an index `*.idx`. This is how repositories stay small even with thousands of commits.
You should never manually edit files in `.git/objects`. All interactions must happen through Git commands to maintain integrity.
The Four Fundamental Object Types
Git's entire history and state are built from just four types of objects. They are all stored in the same content-addressable database but have different internal structures and roles. Understanding each is paramount.
1. Blob: The Raw Data Container
A blob (Binary Large OBject) is the simplest object. It stores the raw, uncompressed contents of a file. A blob has no filename, no timestamp, no permissions—only the raw byte stream of the file's content.
This is a crucial distinction. The filename and directory information are *not* part of the blob. They belong to a higher-level object: the tree. This separation is what allows Git to handle file renames and moves efficiently—it doesn't track them explicitly. If you rename `file.txt` to `file2.txt` but its content remains 90% identical, Git will still store a new blob for the changed parts and a new tree entry, but the unchanged content may be reused from an existing blob.
```bash
Practical Exploration: Creating and Inspecting a Blob
Let's create a simple file and store its content as a Git blob.
echo "Hello, Git World!" > hello.txt
This command computes the SHA-1 of the file's content and creates a blob object.
It outputs the hash to stdout. The object is now stored in .git/objects.
blob_hash=$(git hash-object hello.txt) echo "The blob hash is: $blob_hash"
Now, let's inspect this raw object using `git cat-file`.
-p flag means "pretty print" the object's contents in a human-readable form.
git cat-file -p $blob_hash
Output: Hello, Git World!
```
Key Insight: The blob hash is *solely* a function of the file's content. If you change even one character in `hello.txt` and run `git hash-object` again, you will get a *completely different* hash. The original blob object remains in the database, immortal and immutable.
2. Tree: The Directory Snapshot
A tree object represents a snapshot of a single directory. It maps filenames to object IDs (which can be blob IDs for files or tree IDs for subdirectories). It also stores the file mode (e.g., `100644` for a regular file, `100755` for an executable, `120000` for a symlink).
A tree is essentially a list of entries: ``` [filename] [mode] [object_type] [object_sha1] ```
Let's build a small project structure manually to see how trees and blobs connect.
```bash
Start a new repo to avoid interference
git init demo-repo cd demo-repo
Create two files
echo "Content of file1" > file1.txt echo "Content of file2" > file2.txt
Stage them. This writes blob objects to the database and writes a *tree* object
representing the index (staging area) to the database. We'll inspect that tree.
git add file1.txt file2.txt
The index (staging area) has a tree. We can write that tree to the object database
and get its hash. `git write-tree` creates a tree object from the current index.
tree_hash=$(git write-tree) echo "Tree hash for the index: $tree_hash"
Inspect the tree object. It lists the files in the directory.
git cat-file -p $tree_hash
Output will look like:
100644 blob a1b2c3d4... file1.txt
100644 blob e5f6g7h8... file2.txt
(The blob hashes will be the ones created by `git add`)
Now, let's see the blob for file1.txt directly.
git cat-file -p $(git rev-parse :file1.txt) # :file1.txt is index lookup syntax ```
Key Insight: A commit will eventually point to a single *root tree* object. That root tree contains all the files and directories of your project at that point in time, recursively through sub-tree objects. The tree structure is what gives your project its directory hierarchy.
3. Commit: The Historical Anchor
A commit is the object that ties everything together into a coherent point in history. A commit object contains:
- A **pointer to a root tree object** (the snapshot of the entire project).
- Zero or more **pointers to parent commit objects** (usually one, but more for merge commits). This is what creates the DAG.
- **Author information** (name, email, timestamp).
- **Committer information** (name, email, timestamp—often the same as author, but can differ with ope