Git's Core Architecture: Under the Hood
When developers encounter unexpected Git behavior—whether it's a mysterious merge conflict, an "object not found" error, or confusion about why rebasing rewrites history—their ability to diagnose and resolve the issue depends almost entirely on their understanding of Git's internal architecture. Git's command-line interface presents itself as a simple version control tool with commands like `add`, `commit`, and `push`, but beneath this seemingly straightforward surface lies an elegant and remarkably powerful system built on a handful of fundamental concepts. Mastering these concepts transforms Git from a black box that occasionally does inexplicable things into a predictable, comprehensible tool whose behavior you can reason about with confidence.
This chapter takes you under the hood of Git, exploring the architectural decisions that make it work. We'll examine the content-addressable storage system that gives Git its legendary reliability, dissect the four fundamental object types that form the building blocks of every repository, understand the index or staging area mechanism that mediates between your working directory and the repository, and explore advanced topics like pack files, delta compression, and the reflog system. By the end of this chapter, you'll possess the mental models necessary to understand Git's behavior in any situation, debug complex issues with precision, and make informed decisions about branching strategies, merge workflows, and repository maintenance.
The Content-Addressable Storage Foundation
At the heart of Git's architecture lies a deceptively simple concept: content-addressable storage. Unlike traditional file systems that locate files by name, Git locates data by its cryptographic hash. When you add content to a Git repository, Git computes the SHA-1 hash of that content and uses the hash as the address where the content is stored. This seemingly straightforward design decision has profound implications for everything Git does.
Consider what happens when you create a new file in a Git repository. Suppose you have a file named `config.txt` containing the text `database=production`. When you run `git add config.txt`, Git doesn't simply record that you want to track this file with this name. Instead, Git reads the file's contents, computes the SHA-1 hash of those contents, and stores the contents in the object database under that hash. The actual filename `config.txt` is irrelevant at this stage—Git stores raw content and retrieves it by hash.
The SHA-1 hash of `database=production\n` (assuming a trailing newline) is `8ab686eafeb1f44702738c8b0f24f2567c36da6d`. When Git stores this blob object, it creates a file at `.git/objects/8a/b686eafeb1f44702738c8b0f24f2567c36da6d`. The two-character directory prefix comes from the first two characters of the hash, a standard technique for distributing files across directories to avoid filesystem performance issues with too many files in a single directory. Inside this file, Git stores a compressed representation of the blob, including a header that identifies it as a blob object and indicates the original content length.
This content-addressable approach provides Git's core guarantee: given the same input, Git will always produce the same hash, and given the same hash, Git will always retrieve the same content. This immutability is foundational to Git's reliability. Once an object is stored, it cannot be modified—any change to the content would result in a different hash, which would point to a different storage location. This property means that Git objects are inherently tamper-evident: if someone could somehow alter the contents of a stored object, the hash would no longer match, and Git would detect the corruption.
The implications extend far beyond simple storage reliability. Because identical content always produces identical hashes, Git naturally eliminates duplicate storage. If you have two files with the same contents, or if you add the same file in multiple commits, Git stores only one copy of the content. This deduplication happens automatically and transparently, making repositories surprisingly efficient despite their apparent complexity.
Git's use of SHA-1 for content addressing is worth examining more closely. SHA-1 produces a 160-bit hash, typically represented as 40 hexadecimal characters. The probability of a hash collision—two different pieces of content producing the same hash—is astronomically small. With approximately $2^{80}$ possible hash values, the likelihood of accidentally encountering a collision is roughly comparable to winning the lottery multiple times in succession. While researchers have demonstrated theoretical weaknesses in SHA-1 and Git has begun transitioning to SHA-256, the practical security implications for version control purposes remain minimal.
```bash
Demonstrating content-addressable storage
$ echo "database=production" | git hash-object --stdin 8ab686eafeb1f44702738c8b0f24f2567c36da6d
$ echo "database=production" | git hash-object --stdin -w
Creates .git/objects/8a/b686eafeb1f44702738c8b0f24f2567c36da6d
$ git cat-file -t 8ab686eafeb1f44702738c8b0f24f2567c36da6d blob
$ git cat-file -p 8ab686eafeb1f44702738c8b0f24f2567c36da6d database=production ```
This direct interaction with Git's object database reveals the underlying mechanism. The `hash-object` command computes the SHA-1 hash of content and optionally writes the object to the database. The `cat-file` command retrieves information about objects: `-t` shows the object type, while `-p` pretty-prints the content. Notice that `cat-file` retrieves the content solely based on its hash—there's no filename involved in this lookup.
The Four Fundamental Object Types
Git's entire data model consists of only four object types, each serving a distinct purpose in the version control system. Understanding these objects and their relationships is essential for comprehending how Git represents your project's history.
Blob Objects: Raw Content Storage
Blob objects represent the simplest form of Git storage—they contain the raw contents of a file without any metadata, no filename, no path, no information about when the file was created or modified. This minimalism is a deliberate design choice with several important implications.
The absence of filename information in blobs might seem surprising at first. If Git doesn't store filenames in blobs, how does it know which files exist in your project? The answer lies in tree objects, which we'll examine shortly. For now, understand that this separation of content from metadata provides significant benefits. If you rename a file, Git doesn't need to modify any blob objects—the content remains identical, so the hash remains identical. Git simply updates the tree structure that references the blob.
Blob objects also demonstrate Git's content-based deduplication elegantly. Consider a scenario where you add 100 files to a repository, then discover that 50 of those files are duplicates with identical content. Git stores only one blob for each unique content, regardless of how many files share that content. The trees referencing these blobs will each have different names, but they'll all point to the same blob object.
```bash
Creating and examining blob objects
$ echo "console.log('Hello');" > app.js $ git add app.js
Git stores the blob and we can examine it directly
$ git ls-files --stage 100644 8a472e7b1d72d9e1ef3f1a7a2c3f8e9d4b5a6c7d 0 app.js
The hash here is the blob's hash
$ git cat-file -p 8a472e7b1d72d9e1ef3f1a7a2c3f8e9d4b5a6c7d console.log('Hello'); ```
Tree Objects: Directory Structure
If blob objects store file contents, tree objects store directory structure—mapping names to blobs (files) and to other trees (subdirectories). A tree object consists of multiple entries, each containing a mode (file type and permissions), a name (the filename or directory name), and a reference to either a blob objec