Git Internals: Objects, Index, and References
When you type `git commit`, a cascade of operations occurs beneath the surface, transforming your staged changes into an immutable snapshot in the repository's history. To the uninitiated, Git appears as a magical black box where files are versioned and branches are created with simple commands. However, beneath this simplicity lies an elegantly designed content-addressable storage system built on a handful of primitive object types. This chapter peels back the curtain to reveal Git's internal machinery: the object model (blobs, trees, commits, and tags), the pivotal index (staging area), and the reference system (refs and HEAD). Understanding these internals is not merely an academic exercise; it is the foundation for mastering advanced operations such as data recovery, history rewriting, and performance tuning. We will explore how Git stores data, the lifecycle of a commit, and why branches are nothing more than lightweight pointers. By the end of this chapter, you will see Git not as a set of commands, but as a system you can reason about, debug, and manipulate at its core.
The Object Model: Git's Immutable Building Blocks
At the heart of Git lies a deceptively simple yet powerful concept: everything is stored as an object. These objects are immutable, content-addressable, and form a directed acyclic graph (DAG). There are four primary object types: blobs, trees, commits, and tags. All objects are stored in the `.git/objects` directory, compressed using zlib, and named by the SHA-1 hash of their contents. This design ensures data integrity—any change to an object's content results in a different hash, making corruption immediately detectable.
Blobs: Storing File Contents
A blob (binary large object) is the simplest object type. It stores the raw contents of a file, but not the filename, file permissions, or any other metadata. Its sole purpose is to hold data. When you run `git add file.txt`, Git performs the following steps: 1. Reads the contents of `file.txt`. 2. Computes the SHA-1 hash of those contents. 3. Compresses the contents with zlib. 4. Writes the compressed data to `.git/objects/ab/cdef...` (where `ab` are the first two characters of the hash, and `cdef...` is the rest).
Because blobs are content-addressable, identical files across the repository (or even in different commits) are stored only once. This deduplication is a key space-saving feature.
```bash
Create a blob manually and inspect it
echo "Hello, World!" > hello.txt git hash-object -w hello.txt
Output: 3b18e510d4c5e... (the SHA-1 hash)
View the raw compressed object (not for human consumption)
cat .git/objects/3b/18e510d4c5e... | zlib-flate -uncompress
Output: blob 13\0Hello, World!
```
Notice the blob object format: the string `blob `, followed by a space, the file size in bytes, a NUL byte (`\0`), then the raw file content. Git uses this header when calculating the hash, ensuring the hash includes the object type and size.
Practical Insight: Because blobs store only content, renaming a file does not create a new blob; Git detects renames during diff operations by comparing blob hashes, not by tracking filenames explicitly.
Trees: Organizing the Filesystem Hierarchy
A tree object represents a directory snapshot. It contains a list of tree entries, each pointing to either a blob (a file) or another tree (a subdirectory). Each entry records:
- **File mode** (e.g., `100644` for a regular file, `40000` for a directory, `120000` for a symlink, `160000` for a gitlink/submodule).
- **SHA-1 hash** of the object (blob or tree).
- **Filename**.
Trees are recursive: a tree can contain other trees, building the full directory structure. Consider a simple repository with two files:
```bash mkdir project && cd project git init echo "Version 1" > README.md echo "console.log('Hello');" > app.js git add . ```
After `git add .`, Git has created blobs for `README.md` and `app.js`. Then `git write-tree` (called implicitly by `git commit`) creates a tree object:
```bash git write-tree
Output: 7a6b8c9d0e1f... (tree hash)
Inspect the tree object
git cat-file -p 7a6b8c9d0e1f
Output:
100644 blob a8f5c3d... README.md
100644 blob b2e9d1a... app.js
```
The tree object format is: one line per entry, with mode, type (`blob` or `tree`), hash, and filename, separated by spaces. Git stores the tree in `.git/objects/7a/6b8c9d0e1f...`.
If we add a subdirectory:
```bash mkdir src && echo "function() {}" > src/util.js git add src git write-tree
New tree hash, say 9c1d2e3f4a5b...
git cat-file -p 9c1d2e3f4a5b
Output:
100644 blob a8f5c3d... README.md
100644 blob b2e9d1a... app.js
40000 tree c3d4e5f6a7b8 src
```
Now the root tree contains an entry pointing to another tree (`c3d4e5f6a7b8`) for the `src` directory. That subtree contains the blob for `src/util.js`. This hierarchical structure mirrors the filesystem.
Key Takeaway: Trees are the glue that binds blobs into a coherent project snapshot. A commit points to a single root tree, which recursively references all files and subdirectories.
Commits: Snapshots with Metadata
A commit object represents a snapshot of the entire project at a point in time. It contains:
- **Tree hash**: The root tree object for this snapshot.
- **Parent commit hash(es)**: One or more SHA-1s of parent commits (for merge commits, there are multiple parents).
- **Author**: Name, email, and timestamp of when the change was originally written.
- **Committer**: Name, email, and timestamp of when the change was actually committed (often the same as author, but can differ in rebasing or applying patches).
- **Commit message**: A human-readable description.
Commit objects are created by `git commit`, which essentially runs `git write-tree` to get the current index's tree hash, then `git commit-tree` to wrap it in a commit object.
```bash
After staging changes
git commit -m "Initial commit"
Output: [master (root-commit) abc1234] Initial commit
Inspect the commit
git cat-file -p abc1234
Output:
tree 7a6b8c9d0e1f...
author Jane Doe <jane@example.com> 1625097600 +0200
committer Jane Doe <jane@example.com> 1625097600 +0200
#
Initial commit
```
The commit object format is straightforward: header lines (`tree`, `parent`, `author`, `committer`) followed by a blank line, then the commit message. The SHA-1 hash is computed over this entire content.
Important Nuance: The `author` and `committer` fields serve distinct purposes. The author is the original creator of the change. The committer is the person who actually integrated the change into the repository. In a typical linear history, they are identical. However, when you `git cherry-pick` a patch, you become the committer while retaining the original author. In `git rebase`, the commits are rewritten: the original author is preserved, but the committer becomes you (the rebaser), and the parent hashes change, resulting in new commit hashes.
Tags: Human-Readable References to Specific Points
A tag is a named reference to a specific object, usually a commit. There are two types: 1. Lightweight tag: Simply a ref file under `.git/refs/tags/` that contains a commit hash. It has no separate object; it's just a pointer. 2. Annotated tag: A full tag object (type `tag`) that stores additional metadata: the tagged object (commit, tree, or blob), the tagger (name, email, date), a tag message, and an optional GPG signature.
Annotated tags are recommended for releases because they are verifiable and carry more information.
```bash
Create an annotated tag
git tag -a v1.0 -m "Release version 1.0"
Inspect the tag object
git cat-file -p $(git rev-parse v1.0)
Output:
object abc1234... # the commit hash
type commit
tag v1.0
tagger Jane Doe <jane@example.com> 1625184000 +0200
#
Release version 1.0
```
The tag object explicitly references a commit (or other object), providing a stable, na