Chapter 1: The Atomic Core - Git's Object Model
On this page 14
- 1.1 The Need for Atomic Storage: Why Git Thinks in Objects
- 1.2 Deconstructing History: Git’s Four Core Object Types
- The Blob Object: Raw File Content
- The Tree Object: Directory Structure
- The Commit Object: A Snapshot in Time
- The Tag Object: Human-Readable Milestones
- 1.3 Peering into the .git Directory: Practical Object Inspection
- Creating and Inspecting Blob Objects
- Constructing and Inspecting Tree Objects
- 1.4 Common Misconceptions and Pitfalls with Git Objects
- Object Mutability
- The Nature of Git Hashes
- Plumbing vs. Porcelain Commands
- 1.5 Building a Mini-Repository: A Hands-On Object Model Challenge
1.1 The Need for Atomic Storage: Why Git Thinks in Objects
Version control systems exist to manage changes to a collection of files over time. At its heart, this task involves two primary challenges: storing data efficiently and ensuring its integrity. Early approaches to version control often focused on storing full copies of files at each revision, or recording a series of differences (deltas) between consecutive versions. While functional, these methods can lead to inefficiencies, particularly when large files are modified frequently, or when multiple branches diverge and merge.
Consider a simple text file. If you modify it, save it, and then modify it again, how should a version control system store these three states? Storing three complete copies is wasteful if only a few lines changed. Storing only the differences can become complex, requiring the system to reconstruct any version by applying a chain of deltas, which can be fragile if an intermediate delta or the original base is lost or corrupted.
Git takes a fundamentally different approach. Instead of primarily thinking about “files” or “versions” as its atomic units, Git decomposes all data into immutable, self-contained pieces. It doesn’t track files by their names or paths initially; it tracks them by their content.
This leads to Git’s core innovation: content-addressable storage. When any piece of data—be it a file’s content, a directory listing, or a commit message—is added to Git, the system computes a cryptographic hash of that data. Historically, Git has used the SHA-1 (Secure Hash Algorithm 1) algorithm, which produces a 40-character hexadecimal string (e.g., da39a3ee5e6b4b0d3255bfef95601890afd80709). Future versions of Git are transitioning to SHA-256 for enhanced security.
This hash serves as the unique identifier, or “address,” for that specific piece of content. If the content is identical, its SHA-1 hash will be identical. If even a single byte of the content changes, the resulting hash will be completely different. This property provides a powerful guarantee: the ID is the content. You cannot have two different pieces of content with the same hash, and you cannot accidentally corrupt content without its hash no longer matching, immediately signaling a problem.
Every single piece of information Git manages—from the text of a source code file to the metadata about who made a change and when—is stored as one of these content-addressed “objects.” This atomic, immutable storage mechanism forms the bedrock of Git’s efficiency, integrity, and distributed capabilities. It simplifies the problem of tracking changes by ensuring that any piece of data, once stored, is uniquely identified and verifiable, regardless of its role within a larger project structure.
1.2 Deconstructing History: Git’s Four Core Object Types
Git’s power stems from its content-addressable filesystem, where every piece of data is stored as an “object” identified by its SHA-1 hash. Understanding these objects is fundamental to grasping how Git constructs and manages project history. There are four primary object types: blob, tree, commit, and tag. Together, they form a directed acyclic graph (DAG) that represents every state and transition in your repository.
The Blob Object: Raw File Content
At its simplest, a blob (binary large object) stores the exact content of a file. Git does not store filenames or directory paths within a blob; it only cares about the raw data. If two files in different directories have identical content, Git stores only one blob object for both.
To illustrate, let’s create a blob:
echo "Hello, Git objects!" | git hash-object -w --stdin
This command prints a 40-character SHA-1 hash, for example, f7831f479bb50393693245053716a5757788484a. The -w flag writes the object to Git’s object database, and --stdin reads content from standard input. We can inspect its content:
git cat-file -p f7831f479bb50393693245053716a5757788484a
Hello, Git objects!
The Tree Object: Directory Structure
While blobs store file content, tree objects are responsible for representing directory structures. A tree object lists other tree objects (subdirectories) and blob objects (files) along with their names, file modes (e.g., executable, symbolic link), and corresponding SHA-1 hashes. This provides the link between a filename and its content.
Consider a simple directory structure:
my_project/
├── file1.txt
└── subdir/
└── file2.txt
A tree object for my_project would contain an entry for file1.txt (pointing to a blob) and an entry for subdir (pointing to another tree object). We can inspect the root tree of our current commit:
git cat-file -p HEAD^{tree}
100644 blob 83870327f42d63426e2540984a92c019904d9943 .gitignore
040000 tree 7291244301a57d38ae8597379207008f5d070b4a src
100644 blob 3e51a666e4a2e584d4ae61c6b65349e548231c5f README.md
Each line shows the file mode, object type (blob or tree), the object’s SHA-1 hash, and its name.
The Commit Object: A Snapshot in Time
A commit object represents a complete snapshot of your project at a specific point in time. It doesn’t store file differences; instead, it points to a single root tree object, which in turn references all the blobs and sub-trees that make up the project’s state. Crucially, a commit also records metadata: the author, committer, timestamp, a commit message, and references to its parent commit(s). These parent references are what build the project’s history.
To view a commit object:
git cat-file -p HEAD
tree 7291244301a57d38ae8597379207008f5d070b4a
parent 6b8b049d1e342898f869a239b35b6d91f2c25350
author Jane Doe <[email protected]> 1678886400 +0100
committer Jane Doe <[email protected]> 1678886400 +0100
Initial project setup
Here, the tree line points to the root directory’s tree object, and the parent line links to the previous commit, forming the historical chain.
The Tag Object: Human-Readable Milestones
A tag object serves as a human-readable name for a specific point in history, typically a commit object. While lightweight tags are just pointers to a commit, annotated tags are full Git objects themselves. They contain their own metadata, such as the tagger’s name, email, date, and a tag message, and point to another Git object (most commonly a commit). Annotated tags are immutable and cryptographically secure, making them ideal for marking release versions.
To inspect an annotated tag:
git tag -a v1.0 -m "Initial stable release" HEAD
git cat-file -p v1.0
object 6b8b049d1e342898f869a239b35b6d91f2c25350
type commit
tag v1.0
tagger Jane Doe <[email protected]> 1678886400 +0100
Initial stable release
The object line in the tag object clearly indicates which commit it points to, making it easy to reference significant milestones in your project’s history. These four object types, linked by their SHA-1 hashes, form the immutable foundation of Git’s data model.
1.3 Peering into the .git Directory: Practical Object Inspection
Git’s core functionality relies on a simple, yet robust, object database stored within the .git directory. Understanding this structure directly provides a clearer picture of how Git tracks content. While daily Git operations use high-level “porcelain” commands like git add or git commit, we will now explore Git’s low-level “plumbing” commands. These tools interact directly with the object database, allowing us to create and inspect objects manually.
Let’s begin by initializing a new Git repository for our experimentation:
mkdir git_object_lab
cd git_object_lab
git init
Creating and Inspecting Blob Objects
The most fundamental object type is the blob, which stores the exact content of a file. To create a blob, we first need some content.
echo "This is the content of my first file." > file1.txt
Now, we can use git hash-object to create a blob object from file1.txt. The -w flag instructs Git to write the object into the database, and the --stdin flag is not needed here as we are providing a filename.
git hash-object -w file1.txt
This command will output a 40-character SHA-1 hash, for example, d670460b4b4aece5915caf5c68d12f5651f19fe1. This hash is the object’s unique identifier. Git stores this object in .git/objects/d6/70460b4b4aece5915caf5c68d12f5651f19fe1, where the first two characters of the hash form the directory name.
To confirm the object’s type and content, we use git cat-file.
git cat-file -t d670460b4b4aece5915caf5c68d12f5651f19fe1
The output will be blob, confirming its type. To view its content:
git cat-file -p d670460b4b4aece5915caf5c68d12f5651f19fe1
This will print This is the content of my first file., demonstrating that the blob faithfully stores the file’s content.
Constructing and Inspecting Tree Objects
While blobs store file content, tree objects represent directories, containing pointers to other blobs and trees. They capture the directory structure and file permissions.
To create a tree, Git first needs to know what files and directories it should contain. This information is typically held in the Git index (staging area). We can manually add files to the index using git update-index.
echo "Content for a second file." > file2.txt
mkdir subdir
echo "Content for a file in a subdirectory." > subdir/file3.txt
git update-index --add file1.txt file2.txt subdir/file3.txt
Now, with the index populated, we can instruct Git to create a tree object from its current state using git write-tree.
git write-tree
This will again output a SHA-1 hash, for example, b40f1a4e526487e471d87e1451a941e97491761d. This is the hash of our new tree object.
We can inspect this tree object using git cat-file:
git cat-file -t b40f1a4e526487e471d87e1451a941e97491761d
# Output: tree
git cat-file -p b40f1a4e526487e471d87e1451a941e97491761d
The content of the tree object will list its entries, each showing a mode (e.g., 100644 for a regular file, 040000 for a directory), object type (blob or tree), the object’s SHA-1 hash, and its filename. You will see entries for file1.txt, file2.txt, and subdir. The subdir entry itself points to another tree object, representing the contents of that subdirectory. This recursive structure allows Git to represent arbitrary directory hierarchies.
1.4 Common Misconceptions and Pitfalls with Git Objects
Understanding Git’s object model is fundamental, yet several common misunderstandings can hinder a clear grasp of its power and integrity. Let us clarify some of these points.
Object Mutability
A frequent misconception is that Git objects can be directly “edited” or altered after creation. This is incorrect. Once a blob, tree, commit, or tag object is written to the Git object database, it is immutable. Its content and its corresponding SHA-1 hash are permanently fixed.
When you modify a file in your working directory and then stage it, Git does not change the existing blob object. Instead, it computes a new SHA-1 hash for the modified content and creates an entirely new blob object in the database. This new blob replaces the reference to the old one in the index and subsequent tree objects. The original, unmodified blob object remains in the database until Git’s garbage collection eventually removes unreferenced objects. This immutability is a cornerstone of Git’s data integrity.
The Nature of Git Hashes
Another point of confusion revolves around the SHA-1 hashes Git uses. These are not merely arbitrary identifiers. Each 40-character hexadecimal string is a cryptographic hash of the object’s exact content, prefixed with its type (e.g., “blob”, “tree”) and size.
The hash is deterministic: the same content, type, and size will always produce the exact same SHA-1 hash. This property means that if two objects have the same hash, their content is guaranteed to be identical. This strong link between content and hash is what allows Git to efficiently detect changes, ensure data integrity, and identify objects uniquely across distributed repositories. Git primarily uses SHA-1 for object identification as of version 2.43.0, though work is ongoing to support SHA-256 for future security enhancements.
Plumbing vs. Porcelain Commands
Git commands are often categorized into two groups: “plumbing” and “porcelain.” Misunderstanding this distinction can obscure Git’s underlying mechanics.
Plumbing commands are low-level tools that directly interact with Git’s object model and internal data structures. They are the building blocks, often designed for scripting or for inspecting Git’s internals. Their output is typically raw and machine-readable. Examples include git hash-object (to compute an object’s hash and optionally store it), git cat-file (to display object content), and git write-tree (to create a tree object).
# Plumbing: Computes hash and stores content of a file
git hash-object -w myfile.txt
Porcelain commands, in contrast, are the high-level, user-friendly commands designed for common development workflows. They abstract away the complexity of the plumbing commands, often executing multiple plumbing commands behind the scenes to achieve a user-facing operation. Examples include git add, git commit, git push, and git pull.
# Porcelain: Stages changes, which involves plumbing commands like git hash-object
git add myfile.txt
Understanding this distinction helps clarify how Git operates. Porcelain commands provide convenience, while plumbing commands offer direct access to Git’s “atomic core.”
1.5 Building a Mini-Repository: A Hands-On Object Model Challenge
Understanding Git’s object model shifts from abstract knowledge to practical insight when you interact with it directly. This section challenges you to construct a small repository history using only Git’s “plumbing” commands. This exercise will solidify your grasp of blobs, trees, and commit objects, and how they link together.
We will build a two-commit history. The first commit will introduce a single file. The second will modify that file.
-
Prepare Your Workspace First, create an empty directory for this exercise and initialize a Git repository within it. While
git initis a high-level command, it is necessary here to establish the.gitdirectory structure where our plumbing commands will store objects. We will not use other high-level commands likegit addorgit commit.mkdir manual_repo_challenge cd manual_repo_challenge git init -
The First Blob Object Create your first file. Then, use
git hash-objectto create a blob object from its content. The-wflag writes the object to the object database, and the output is its SHA-1 hash.echo "Hello, Git object model!" > file1.txt FIRST_BLOB_HASH=$(git hash-object -w file1.txt) echo "First blob hash: $FIRST_BLOB_HASH" -
The First Tree Object A tree object represents a directory snapshot. To build it, we first need to stage our blob using
git update-index. This command modifies Git’s staging area (the index). The--cacheinfooption allows us to add an object directly by its mode, hash, and path. Then,git write-treecreates the tree object from the current index.git update-index --add --cacheinfo 100644 "$FIRST_BLOB_HASH" file1.txt FIRST_TREE_HASH=$(git write-tree) echo "First tree hash: $FIRST_TREE_HASH"The
100644mode indicates a normal file. -
The First Commit Object Now, create a commit object that points to our first tree. The
git commit-treecommand takes a tree hash and, optionally, parent commit hashes and authorship information. For the very first commit, there are no parents. We pipe the commit message directly.FIRST_COMMIT_HASH=$(echo "Initial commit: Add file1.txt" | git commit-tree "$FIRST_TREE_HASH") echo "First commit hash: $FIRST_COMMIT_HASH" -
Update a Branch Reference To make this commit easily discoverable by commands like
git log, we should update a branch reference. This is how Git knows where the “head” of a branch is.git update-ref refs/heads/main "$FIRST_COMMIT_HASH" -
The Second Blob Object (File Modification) Modify
file1.txtand create a new blob object for its updated content.echo "Hello again, Git object model!" > file1.txt SECOND_BLOB_HASH=$(git hash-object -w file1.txt) echo "Second blob hash: $SECOND_BLOB_HASH" -
The Second Tree Object Update the index with the new blob for
file1.txt. Then, write a new tree object.git update-index --cacheinfo 100644 "$SECOND_BLOB_HASH" file1.txt SECOND_TREE_HASH=$(git write-tree) echo "Second tree hash: $SECOND_TREE_HASH" -
The Second Commit Object Create the second commit, referencing the new tree and, crucially, specifying the
FIRST_COMMIT_HASHas its parent using the-pflag. This establishes the history link.SECOND_COMMIT_HASH=$(echo "Second commit: Update file1.txt" | git commit-tree "$SECOND_TREE_HASH" -p "$FIRST_COMMIT_HASH") echo "Second commit hash: $SECOND_COMMIT_HASH"Again, update the
mainbranch to point to this new commit.git update-ref refs/heads/main "$SECOND_COMMIT_HASH" -
Verification You can now inspect your manually constructed history.
git log main git cat-file -p "$FIRST_COMMIT_HASH" git cat-file -p "$SECOND_COMMIT_HASH" git cat-file -p "$FIRST_TREE_HASH" git cat-file -p "$FIRST_BLOB_HASH"
This exercise demonstrates the fundamental building blocks of Git’s history: content stored as blobs, directory structures as trees, and historical snapshots as commits, all linked by their SHA-1 hashes. Every high-level Git operation ultimately boils down to creating and linking these objects.
Spotted an error? Tell us via the corrections process — verified reports get fixed and credited.