Inside Git …
Inside Git: A Clear Guide to Blobs, Trees, Commits, and References
How Git maps file contents to hash-addressed objects, builds snapshots, and uses lightweight pointers (branches, HEAD, tags) to manage history.
Most developers know the commands; few understand what Git actually stores. This article peels back the layers to explain how Git maps file contents to hash-addressed objects — blobs, trees, and commits — why filenames live in trees, and how branches,
HEAD, and tags point into the object graph. Follow a short hands-on walkthrough and a compact diagram to make Git’s internals clear and usable.
Introduction
Most developers learn a handful of Git commands and use them every day: git add, git commit, git push, git pull. That’s enough to ship features, but it’s easy to miss what Git actually does behind the scenes. Knowing the internals turns Git from a black box into a predictable tool: you’ll understand why branches move, why identical files don’t take extra space, and how Git represents the entire history of a project.
This article explains Git’s core data model — blobs, trees, and commits — and shows how references (branches, HEAD, tags) point at that model.
Core idea: Git is a content-addressable store
At its core, Git maps content to keys. Any content you give Git (a file, a directory snapshot, or a commit object) is turned into a sequence of bytes, hashed (SHA-1 historically; newer Git versions may use SHA-256), compressed, and stored in the object database under a name derived from its hash. If two values are identical, Git computes the same hash and reuses the same object — no duplicate storage.
The three fundamental object types
- Blob: the raw data of a file (the bytes). A blob contains file contents only — no filename or permissions metadata (except mode bits stored by trees).
- Tree: an entry that represents a directory. A tree lists entries: name, mode, object type (blob or tree), and object hash. Trees build a directory hierarchy by pointing to blobs and other trees.
- Commit: a snapshot of the repository state at a moment in time. A commit object records metadata (author, committer, timestamps, message), points to a tree (the snapshot), and may point to one or more parent commits.
All three are stored as hashed objects in the .git/objects database. The naming convention in that folder splits the 40-character hash into a two-character directory and the remaining 38-character filename. That makes object lookup efficient on most filesystems.
A short, concrete walkthrough
- Initialize a repository and add a file:
git init my-tasks
cd my-tasks
echo "Rodrigo" > Members.txt
git add Members.txt
git commit -m "Add Members.txt"After the commit, inspect the objects folder and the new objects Git created. Use git cat-file to explore:
# Show the commit object
git log --oneline
# Suppose the commit id is 54725f...
git cat-file -p 54725f# Show the tree object referenced by the commit
git cat-file -p <tree-hash># Show a blob referenced by the tree
git cat-file -p <blob-hash>
You’ll see that the commit contains a tree: line with a hash, and that tree lists entries that reference blob hashes and file names. The blob itself, when printed, is just the file content.
Why tree owns the filename
Trees, not blobs, store filenames. That design makes it possible for multiple tree entries (different paths) to reference the same blob hash when file contents are identical. If two files contain the exact same bytes, Git stores a single blob object and both trees refer to it. This is efficient and explains why object ids are content-based, not filename-based.
Object reuse example
Create two files in different directories with the same content and commit them. You’ll find that Git uses the same blob hash for both files. You can verify this with git hash-object:
echo -n "Rodrigo" | git hash-object --stdin
# repeat in another file with identical content; the hash will matchThis behaviors is one reason Git is an excellent content tracker — it avoids duplication by design.
Commits and history
Every commit references a tree and zero or more parent commits. The parent reference builds a linked chain (a directed acyclic graph) of commits that represents project history. The commit object itself is stored in the object database and therefore has a hash based on its content (which includes the tree hash and parent references). That means the commit hash encodes not only the snapshot but also its place in history.
Get Suhaib Arshad’s stories in your inbox
Join Medium for free to get updates from this writer.
Friendly names: refs, branches, and HEAD
A raw 40-character hash is not human friendly. Git provides references to make working with history practical:
- Branch: stored under
.git/refs/heads/, a branch is simply a file that contains a commit hash the branch currently points to. Creating a branch writes a new file with a hash string. - Tag: stored under
.git/refs/tags/, a tag points to a specific commit (or other object) and is typically not updated after creation — it’s used to mark releases. - HEAD: the special file
.git/HEADrecords what you currently have checked out. In normal usage,HEADcontains a symbolic reference such asref: refs/heads/main. In other states (detached HEAD) it can contain a direct hash.
Because branches are lightweight pointers (files with a hash), switching branches is a matter of changing HEAD and updating the working tree to match the tree referenced by the pointed commit.
Branches are pointers, not objects
Remember that branches are not objects in the Git object database; they are references that point to commit objects. When you create a new commit while on a branch, Git writes the new commit object into .git/objects and updates the branch file to the new commit hash. HEAD continues to point to that branch, so successive commits move the branch forward.
Tags vs branches
Tags are snapshots: once created they normally remain fixed and point to the same commit. Branches are active pointers you move by committing.
Useful low-level commands for exploration
git cat-file -p <object>: print the contents of a Git object (blob/tree/commit).git hash-object: compute the hash Git would use for a given file or stdin.git ls-tree <tree>: list a tree’s contents.git rev-parse <ref>: resolve a ref name to a raw hash.
These commands are invaluable when you want to peek under the hood and confirm what Git is representing.
Diagram: Git object relationships
Diagram caption : Blobs (file contents) are referenced by trees (directory snapshots); trees are referenced by commits (snapshots). Commits reference parents; branches and HEAD are lightweight pointers to commits.
Why this model matters in practice
- Efficient storage: identical content is shared, so repositories avoid duplication across the project history.
- Fast operations: commits are cheap because a commit stores only a tree hash and parent hashes; Git can create and rearrange these pointers quickly.
- Precise snapshots: each commit is an immutable object identified by its content hash — that makes history reproducible across machines.
- Simple references: branches and tags are small files pointing to commits; this simplicity is what makes branching and merging so fast and lightweight.
A small exercise :
Try this on your machine to confirm these ideas:
git init demo
echo "Alice" > a.txt
git add a.txt
git commit -m "add a"
mkdir dir
echo "Alice" > dir/b.txt
git add dir/b.txt
git commit -m "add b"
# Compare blob hashes
git ls-tree HEAD^{tree}
# Use git hash-object to confirm identical content hashesYou should see two tree objects and one repeated blob hash if both files contain the same bytes. Inspect the .git/objects folder to see how hashes map to files.
Summary
Git is a content tracker built on three object types — blobs, trees, and commits — stored in an object database and identified by content hashes. Trees give names to blobs, commits link snapshots into history, and lightweight references (branches, tags, HEAD) point into this graph. Understanding these pieces explains many everyday behaviours of Git and makes it far easier to reason about merging, rebasing, and low-level recovery.



