The Git Object Model: A Deep Dive into Content-Addressable Storage
The speed, integrity, and distributed nature of Git are not magical properties bestowed by a complex network protocol or a centralized server architecture. Instead, they are the direct, inevitable consequences of a single, profoundly elegant design decision: Git treats its entire history as an immutable, content-addressable data store. This chapter peels back the layers of abstraction that higher-level commands like `git add` and `git commit` provide, revealing the raw, foundational building blocks—blobs, trees, commits, and tags—not as abstract concepts but as practical, concrete objects stored in a simple key-value database. We will analyze the algorithms that make this database efficient at scale, including packfile compression and delta encoding, and examine the garbage collection processes that maintain its health. Finally, we will solidify this understanding by manually reconstructing core Git operations using low-level plumbing commands, demonstrating that the entire version control system can be built from just a handful of primitives operating on this immutable object model.
The Primordial Soup: Git's Object Database
At its heart, a Git repository is a content-addressable store. This means that every piece of data—a file's contents, a directory structure, a commit—is stored as an object whose name is a cryptographic hash of its own content. This name, typically a 40-character SHA-1 hexadecimal string (though Git is migrating to SHA-256), serves as the object's unique, globally consistent identifier. The storage mechanism is deceptively simple: objects are written to files within the `.git/objects/` directory, either as loose objects (one file per object) or packed together into efficient packfiles. The implications of this design are far-reaching:
1. Immutability: Once an object is created and named, its content can never change. If you modify a file and commit again, you are not editing the old blob; you are creating a new blob with a different hash. This immutability is the cornerstone of Git's integrity. A corrupted object will have a different hash and will be detected immediately. 2. Content-Addressability: To retrieve an object, you don't need to know its location; you only need its hash. The storage system translates that hash into a file path (e.g., `ab/cdef...` becomes `.git/objects/ab/cdef...`). This makes lookups incredibly fast and deterministic. 3. Deduplication: The same content will always produce the same hash. If two commits in different branches point to the exact same file content, they point to the exact same blob object on disk. Storage is automatically deduplicated at the object level.
This model transforms version control from a linear list of changes into a directed acyclic graph (DAG) of immutable snapshots, where each snapshot is a complete, self-contained representation of the project state at a point in time.
The Four Fundamental Object Types
Git's universe is composed of four primary object types. Understanding their structure and relationships is fundamental to mastering Git's internals.
1. Blob: The Atomic Unit of File Content
A blob object stores the raw, uncompressed contents of a file. It has no metadata—no filename, no permissions, no timestamp. It is purely the byte stream of the file's data. This separation of content from metadata is crucial. The same blob can be referenced by multiple files (e.g., a README copied to different locations) or by multiple commits.
Internal Format: A blob object's storage format is: ``` type size\0content ``` Where:
- `type` is the ASCII string `"blob"`.
- `size` is the decimal length of the `content` in bytes.
- `\0` is a null separator byte.
- `content` is the raw file data.
The SHA-1 hash is computed over this entire byte sequence. For example, creating a blob from the string "Hello World" involves constructing the header `blob 11\0` and then hashing the concatenation of header and content.
Practical Exploration: You can manually create and inspect a blob using Git's plumbing commands: ```bash
Create a file with some content
echo "Hello, Git Internals!" > hello.txt
Compute the SHA-1 of the file's content as a blob object
The --stdin flag reads from stdin, -w writes the object to the database
git hash-object -w --stdin < hello.txt
Output: 5aef3b... (a 40-character SHA-1)
Inspect the raw object stored on disk (loose object format)
The object is stored compressed with zlib; cat-file decompresses it.
git cat-file -p 5aef3b...
Output: Hello, Git Internals!
``` Note that `git cat-file -p` presents the *decompressed* content. To see the raw, compressed bytes stored on disk, you would need to read the file `.git/objects/5a/ef3b...` directly, which begins with a zlib compression header.
2. Tree: The Directory Structure
A tree object represents a snapshot of a single directory. It contains a list of entries, each entry mapping a filename (or subdirectory name) to a *mode*, a *type*, and a *SHA-1 hash* that points to another object (either a blob for a file or another tree for a subdirectory). A tree is essentially a serialized hash table of its directory's contents.
Internal Format: ``` [entries] ``` Each entry is encoded as: ``` mode SP filename NUL SHA-1 ``` Where:
- `mode` is an octal string representing the file type and permissions (e.g., `100644` for a normal file, `100755` for an executable, `40000` for a subdirectory).
- `SP` is a single space.
- `filename` is the null-terminated filename (no path separators; each entry is just the basename).
- `NUL` is a null terminator for the filename.
- `SHA-1` is the 20-byte binary identifier for the object (blob or tree) that this entry references.
The entries within a tree object are sorted by their filename (using byte-value comparison, not locale-specific sorting) to ensure deterministic hashing. The hash of the tree object is computed over the entire concatenated entry list, prefixed by the header `tree <size>\0`.
Example: Consider a directory `project/` containing:
- `README.md` (blob hash: `a1b2c3...`)
- `src/` (subdirectory, tree hash: `d4e5f6...`)
The corresponding tree object's raw content (before hashing/compression) would look like: ``` 100644 README.md\0a1b2c3... (20 bytes) 40000 src\0d4e5f6... (20 bytes) ``` After prepending the header `tree <size>\0` and compressing, this becomes a single object whose SHA-1 is the tree's identifier.
Practical Exploration: ```bash
Stage the hello.txt file (creates an index entry)
git add hello.txt
Write the current index state as a tree object to the database
git write-tree
Output: 3b18e5... (the SHA-1 of the root tree)
Inspect the tree object
git cat-file -p 3b18e5...
Output:
100644 blob 5aef3b... hello.txt
``` The output shows the mode, object type, object hash, and filename for each entry. This tree object is a complete, immutable description of the `project/` directory at the moment `git write-tree` was run.
3. Commit: The Immutable Snapshot with Metadata
A commit object is the wrapper that turns a tree (a directory snapshot) into a historical revision. It contains metadata about *who*, *when*, and *why* a snapshot was taken, and it links to its parent(s) to form the project history.
Internal Format: ``` tree <tree-sha1> parent <parent-sha1> (zero or more lines; omitted for root commit) author <name> <email> <timestamp> <timezone> committer <name> <email> <timestamp> <timezone>
<commit message> ``` Each line (except the final message) ends with a newline `\n`. The blank line between the `committer` line and the commit message is mandatory. The hash is computed over this entire text block, preceded by the header `commit <size>\0`.
Crucially, the `tree` line points to a *tree object*, which recursively contains all the blobs for the entire project. A commit does *not* store a delta or a list of changed files; it stores a complete reference to a full snapshot. This is