Git Architecture: Under the Hood
When a developer runs `git status` or `git commit`, they interact with a sophisticated content-addressable storage system that has been refined over fifteen years of distributed development. Yet most practitioners treat Git as a black box—a collection of commands that somehow track changes to files. This chapter dismantles that abstraction entirely. By understanding Git's internal architecture, you gain the ability to diagnose subtle bugs, recover from catastrophic mistakes, optimize repository performance, and truly comprehend why Git behaves as it does in complex branching scenarios. The investment in understanding these internals pays dividends across every project where version control matters.
The Content-Addressable Object Model
At Git's core lies an elegant simplicity: everything stored in a Git repository is an object. These objects are immutable—once created, they cannot be modified—and they are addressed by their content, not by arbitrary identifiers. This design decision, made by Linus Torvalds in the original implementation, explains Git's remarkable reliability and its ability to handle complex merge scenarios without data loss.
The Four Fundamental Object Types
Git's object model comprises four types, each serving a distinct purpose in the version control lifecycle. Understanding these types and their relationships reveals how Git constructs the complete history of a project.
Blob Objects represent file content. A blob contains raw binary data—nothing more, nothing less. It stores no filename, no permissions, no metadata about where the file exists in a directory structure. The blob's identity is determined entirely by its content through SHA-1 hashing. This means if two files in different locations contain identical content, Git stores only one blob object, deduplicating automatically. Similarly, if a file changes but later reverts to its original content, Git recognizes this and reuses the original blob. To inspect a blob directly, use the `git cat-file -p` command with a blob's SHA-1 hash:
```bash $ echo "Hello, World" | git hash-object --stdin 8ab686eafeb1f44702738c8b0f24f2567c36da6d $ git cat-file -p 8ab686ea Hello, World ```
The `hash-object` command computes the SHA-1 hash of given content, optionally storing it in the object database. The `-p` flag to `cat-file` pretty-prints the object's content. Notice that the blob contains only the text—no trailing newline from the `echo` command affects the hash because `echo` adds one, but piping through `hash-object --stdin` captures exactly what we send.
Tree Objects represent directory structures. A tree contains references to blobs (files) and other trees (subdirectories), along with metadata including file names, permissions, and the mode indicating whether something is a file or a directory. Each entry in a tree maps a name and mode to a specific object hash:
```bash $ git cat-file -t 3c4e9cd789d88d8d89c1073707c3585e41b5e986 tree $ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b5e986 100644 blob a8c3cd11e33e3e4e2e4e4e4e4e4e4e4e4e4e4e4 .gitignore 100644 blob b7c3cd11e33e3e4e2e4e4e4e4e4e4e4e4e4e4e5 README.md 040000 tree d5e9cd11e33e3e4e2e4e4e4e4e4e4e4e4e4e4e6 src 100755 blob c6f4cd11e33e3e4e2e4e4e4e4e4e4e4e4e4e4e7 build.sh ```
The mode `100644` indicates a regular file with default permissions, while `100755` indicates an executable file. The `040000` mode marks entries as subtrees (directories). This flat representation of directory structure is crucial—trees don't nest recursively in the storage format; instead, each tree object represents exactly one directory level.
Commit Objects provide context and history. A commit contains exactly one tree object (the root of the directory structure at that point in time), zero or more parent commit hashes (zero for initial commits, one for normal commits, multiple for merge commits), author and committer metadata with timestamps, and a commit message. The commit object is what humans interact with most directly, yet it's the thinnest layer in the object model:
```bash $ git cat-file -p HEAD tree 3c4e9cd789d88d8d89c1073707c3585e41b5e986 parent 7b2d8ef1234567890abcdef1234567890abcdef12 author Jane Smith <jane@example.com> 1699574400 -0800 committer Jane Smith <jane@example.com> 1699574400 -0800
Add user authentication module
Implements OAuth2 flow with support for GitHub and Google providers. Includes session management and secure token storage. ```
The timestamps are Unix epoch seconds, making them timezone-independent and easily comparable. The distinction between author and committer supports workflows where someone prepares patches for another to commit—the original author is credited separately from the person who incorporated the change.
Tag Objects create named references to specific commits, typically used for release versioning. Annotated tags contain additional metadata beyond lightweight tag references:
```bash $ git cat-file -t 92b8b694ffb1675e5975148e0791812a5a1f7a7d tag $ git cat-file -p 92b8b694ffb1675e5975148e0791812a5a1f7a7d object 7b2d8ef1234567890abcdef1234567890abcdef12 type commit tag v2.1.0 tagger Jane Smith <jane@example.com> 1699651200 -0800
Release version 2.1.0
Features:
- Multi-factor authentication
- API rate limiting
- Performance improvements for large repositories
```
The tag object references another object (in this case, a commit) by hash, declares the type of that object, provides a tag name, metadata about who and when created it, and an optional message.
Object Storage and Hash Calculation
Git computes SHA-1 hashes using the formula: `SHA1("blob {content_length}\0{content}")`. The prefix includes the object type, byte length, and a null byte before the content. This means identical content in different object types (which cannot happen since type is part of the hash) or content with different lengths produces different hashes. The null byte separator prevents collision attacks where someone might craft content starting with a length number.
Consider the implications: because hashes are content-derived, any modification to a file's content changes its blob hash. This propagates through the tree (since trees reference blobs by hash), which propagates through commits (since commits reference trees), and ultimately through any tags referencing those commits. Git's integrity protection is fundamental to the object model—not an afterthought or optional feature.
The probability of a SHA-1 collision in a repository with one million objects is approximately $10^{-18}$, vanishingly small. However, the theoretical possibility led to Git's planned migration to SHA-256, demonstrating how deeply the hash algorithm is integrated. The transition involves maintaining compatibility with existing SHA-1 objects while gradually introducing SHA-256 support.
The .git Directory Structure
Every Git repository contains a `.git` directory at its root. This directory holds everything Git needs to track your project—the object database, references, configuration, and hooks. Understanding its structure illuminates Git's operation and enables advanced manipulation when necessary.
The Objects Directory
The `objects` directory contains the object database, organized in a SHA-prefixed subdirectory structure. Git uses the first two characters of each object's hash to create subdirectories, distributing objects across 256 directories and avoiding filesystem limitations on directories with excessive files:
```bash .git/objects/ ├── 8a/ │ └── b686eafeb1f44702738c8b0f24f2567c36da6d ├── info/ │ └── .gitignore │ └── .gitattributes │ └── .gitignore ├── pack/ │ └── pack-abc123.def456.pack │ └── pack-abc123.def456.idx └── [226 additional two-character directories] ```
Loose objects—individual files stored directly in the subdirectory structure—appear as files named by their hash (the remaining 38 characters after the directory prefix). The `info` and `pack` sub