The Git Object Model: A Foundational Deep Dive into Content-Addressable Storage
At the heart of Git's revolutionary approach to version control lies a deceptively simple yet profoundly powerful concept: content-addressable storage. Unlike traditional version control systems that track *differences* between file versions or maintain a linear history of changes, Git models the entire project history as a directed acyclic graph (DAG) of immutable snapshots, each identified by a cryptographic hash of its contents. This chapter deconstructs the foundational data structures—blobs, trees, commits, and tags—that collectively form this graph. We will explore the mechanics of cryptographic hashing (SHA-1 and the emerging SHA-256), the implications of content-addressability on data integrity, storage efficiency, and operational speed, and finally, examine Git's internal optimization mechanisms: packfiles and garbage collection. A rigorous understanding of this object model is not merely academic; it is a prerequisite for mastering advanced branching strategies, diagnosing complex merge conflicts, and designing robust collaborative workflows in enterprise environments.
The Primacy of Content-Addressable Storage
Content-addressable storage (CAS) is a storage paradigm where data is retrieved not by its location or an arbitrary identifier, but by its content itself. In Git's implementation, the content's cryptographic hash—computed using the SHA-1 (or SHA-256) algorithm—serves as the unique key, or object name. This design yields several immediate and far-reaching consequences:
1. Immutable Integrity: Any alteration to the content, even a single bit, results in a completely different hash. Therefore, an object's name is a cryptographic fingerprint guaranteeing its integrity. If the hash matches, the content is identical to what was originally stored; if not, the data is corrupted or tampered with. 2. Automatic Deduplication: Identical content, regardless of its origin, path, or commit, is stored only once. A file added in two different branches, or a file that reappears after being deleted and re-added, will reference the same blob object if its byte-for-byte content is unchanged. This dramatically reduces repository size. 3. Efficient Lookup and Comparison: Comparing two versions of a file reduces to a constant-time hash comparison, rather than a costly byte-by-byte diff. Determining if a file has changed since the last commit is a simple hash lookup. 4. Foundation for the DAG: Because objects are immutable and identified by their content, they can be safely referenced by other objects (e.g., a commit references a tree, which references blobs) without fear of the reference breaking due to content mutation. This creates a permanent, verifiable, and branching history.
The trade-off is a conceptual shift: Git does not store *changes* (deltas) as first-class citizens at the user level; it stores *states* (snapshots). Deltas are an *implementation detail* used internally for storage optimization (packfiles), not the logical model. This snapshot-centric view is what enables cheap branching and merging—creating a new branch is merely creating a new pointer (a ref) to an existing commit, an operation with negligible cost.
The Four Fundamental Object Types
Git's object model is composed of four atomic, immutable types. All repository data—every file, every directory structure, every commit, every tag—is encoded as one of these objects and stored in the `.git/objects` directory, typically compressed using zlib.
Blob: The Atomic Unit of File Content
A blob (binary large object) is the simplest object type. It stores the raw, uncompressed content of a file—its exact byte sequence. A blob contains no metadata: no filename, no permissions, no timestamp. It is purely the data.
Structure and Storage: The blob's content is prefixed with the header `blob <size>\0` before being hashed and compressed. The resulting SHA-1 (e.g., `e69de29bb2d1d6438b8b29ae775ad8c2e48c5391`) is its object name. The compressed data is stored in `.git/objects/ab/cdef...` (where `ab` are the first two hex digits of the hash, and `cdef...` is the remainder). This two-level directory structure prevents filesystem issues with too many files in a single directory.
Example and Implications: Consider an empty file. Its blob object hash is famously `e69de29bb2d1d6438b8b29ae775ad8c2e48c5391`. Any empty file, regardless of its name (`README.md`, `empty.txt`, `.gitkeep`), will have this exact blob if its content is zero bytes. This demonstrates deduplication: the repository stores one copy of the empty file's content, referenced by every commit that includes an empty file at any path.
```bash
Create an empty file and get its blob hash
$ touch empty.txt $ git hash-object empty.txt e69de29bb2d1d6438b8b29ae775ad8c2e48c5391 ```
The blob's lack of filename is crucial. Filenames and directory hierarchies are the responsibility of the next object type: the tree.
Tree: The Directory Structure and File Metadata
A tree object represents a snapshot of a directory. It maps filenames (and subdirectory names) to other objects (blobs for files, trees for subdirectories), while also storing file mode (permissions and file type).
Structure: A tree object is a sorted list of entries. Each entry consists of: 1. Mode: A 6-digit octal string representing the object type and permissions.
- `100644`: Normal non-executable file (blob).
- `100755`: Normal executable file (blob).
- `120000`: Symbolic link (the blob content is the link target).
- `160000`: Submodule (a gitlink, pointing to another commit in a different repository).
- `040000`: Subdirectory (tree object).
2. Name: The filename or directory name (null-terminated). 3. SHA-1: The 20-byte object name of the referenced object (blob, tree, or commit for submodules).
Example: A tree for a directory containing `script.sh` (executable) and `README.md` might look like this when inspected with `git cat-file -p`:
```bash $ git cat-file -p <tree-sha> 100644 blob a8c8e58b8c75b1c2670a8e0d076a3d5a1d4b8e1c README.md 100755 blob 3b18e510d7d5a5e2e8b8a9c7d6f5e4a3b2c1d0e9 script.sh ```
Here, `script.sh` is marked executable (`100755`), while `README.md` is not (`100644`). The tree itself has its own SHA-1, computed from this sorted list of entries. A root tree represents the entire project's top-level directory structure.
Key Insight: A tree is a deterministic function of its entries' names, modes, and the content hashes of the files/subdirectories. Any change to a file's content (changing its blob) or to the directory structure (adding/removing/renaming files) results in a new tree object with a new hash. This is why committing a modified file creates a new blob *and* a new tree (and a new commit).
Commit: The Immutable Snapshot with Metadata
A commit object is the anchor point of history. It represents a coherent, named snapshot of the entire project at a point in time. It is the object that users interact with most directly (`git log`, `git checkout`).
Structure: A commit object contains: 1. Tree: The SHA-1 of the root tree object for this snapshot. 2. Parent(s): One or more SHA-1s of parent commit(s). A normal commit has one parent. A merge commit has two or more parents, creating the branching structure in the DAG. The initial commit has zero parents. 3. Author: Name, email, and timestamp (with timezone) of the person who originally authored the change. 4. Committer: Name, email, and timestamp of the person who last committed the change (may differ from author in case of cherry-picks or rebases). 5. Message: The commit message, a free-form string.
Example: Inspecting a commit reveals its full structure:
```bash $ git cat-file -p HEAD tree 3a5c5e8b8c75b1c2670a8e0d076a3d5a1d4b8e1c parent 9fceb02d215ae8d5b0a5a2e8d8e7c6d5f4a3b2c