Systems · Developer Tools

Content, Addressed by Its Hash.

Git feels like it stores diffs — you commit “a change,” after all — but under the hood it does something simpler and stranger: every commit is a full snapshot of your project, and the magic that makes that cheap is content addressing. Git keeps three kinds of objects. A blob is the raw contents of one file. A tree is a directory listing — names pointing at blobs (files) and other trees (subdirectories). A commit points to one top-level tree (the whole snapshot) plus its parent commit(s), with an author and message. The twist: each object’s name is the hash of its own contents. So two files with identical bytes are the same blob, stored once; and when you change one file and commit, only that file’s new blob, the trees on the path to it, and the new commit are created — every unchanged file and subdirectory keeps its old hash and is shared for free. That’s why a repository with thousands of commits isn’t thousands of full copies. Edit files, make commits, and watch the object graph grow — new objects lighting up, unchanged ones quietly reused.

commits are snapshots not diffs · blob / tree / commit · content-addressed by hash · identical content stored once
blob

The raw contents of a single file. No name, no history — just bytes, addressed by their hash.

tree

A directory listing: names mapped to blob hashes (files) and other tree hashes (subdirectories).

commit

A snapshot: a pointer to one top-level tree, plus parent commit(s), author, and message.

content addressing

Every object’s name is the hash of its contents — so identical content collapses to one object, shared everywhere.

objects.js — hash the content, share the rest
Ready
A tiny repo with three files. Edit a file to change its contents, then Commit to snapshot the working tree into Git objects. Watch which objects are new (content changed) and which are reused (same hash as before). Try making two files identical.
0
commits
0
objects stored
0
reused last commit

How it works

Git is, at its core, a content-addressed key–value store with a graph on top, and understanding that one idea demystifies almost everything else. When Git stores any object, it computes a hash of the object’s bytes and uses that hash as the object’s address. This has three consequences that do all the work. First, deduplication is automatic: if two files (or the same file across many commits) have identical content, they hash to the same blob and are physically stored once — so a commit that changes one file in a huge tree adds only the objects on the path from the root tree down to that file, and shares everything else by reference. Second, integrity is built in: because an object’s name is its content hash, you cannot alter a file’s bytes without changing its hash, which changes its tree’s hash, which changes the commit’s hash — so a commit id cryptographically fixes the entire snapshot beneath it, and any corruption or tampering is detectable. Third, history is a graph, not a line: each commit records its parent(s), so branches are just pointers to commits and a merge is simply a commit with two parents. This is why operations that sound expensive are cheap — a branch is a 40-character file, a snapshot shares unchanged subtrees, and checking out an old commit just means reading the tree it already points to.

1

Files become blobs

Each file’s contents are hashed and stored as a blob, addressed by that hash. Two files with identical bytes become the same blob — stored exactly once.

2

Directories become trees

A tree lists names pointing at blob hashes (files) and other tree hashes (subdirectories). The tree’s own hash depends on everything it references.

3

A commit snapshots a tree

A commit points to one top-level tree (the whole project state) plus its parent commit and metadata. Its hash fixes the entire snapshot beneath it.

Change one file → share the rest

Editing one file makes a new blob, new trees on the path to it, and a new commit — but every unchanged file and subtree keeps its hash and is reused. Identical content is never stored twice.

Commit
snapshot, not a diff
Object name
hash of its contents
Identical content
stored once
Unchanged files
shared for free

The code

# Git as a content-addressed store (conceptually) def store(bytes): h = sha1(header + bytes) # the object's NAME is its hash if h not in objects: # identical content already there? objects[h] = bytes # store once; otherwise reuse return h def commit(working_files, parent): tree = store(b"".join( # tree references each file's blob hash name + b" " + store(content) # unchanged files → same hash → reused for name, content in working_files)) return store(b"commit " + tree + b" parent " + parent)

Quick check

1. Is a Git commit a diff (the change) or a snapshot (the whole state)?

2. Why are two files with identical content stored only once?

3. What happens to unchanged files when you change one file and commit?

FAQ

How does Git store data internally?

As a content-addressed object store with three main types. A blob holds a file’s raw contents; a tree is a directory listing mapping names to blob hashes and other tree hashes; a commit points to one top-level tree (a full snapshot) plus its parent(s) and metadata. Every object is named by the hash of its contents, so identical content is stored once and objects form a graph linked by hash. Branches and tags are just pointers to commits.

Does Git store diffs or full snapshots?

Conceptually, full snapshots: each commit captures the whole project state via its tree. It feels like diffs because content addressing lets unchanged files share existing blobs, so a commit adds only changed objects. Separately, for on-disk and network efficiency, Git can pack objects as deltas in packfiles — but that’s a storage optimization beneath the model, not the model itself. Checking out a commit reconstructs the full snapshot from its tree.

What is a Git commit hash and why does it matter?

A commit hash is computed from the commit’s contents — the tree hash, parent hash(es), author, and message. Because it depends on the tree (whose hash depends on its blobs and subtrees), it cryptographically fixes the entire snapshot and the history behind it: a tamper-evident fingerprint you can’t change without changing the hash. It’s also why rewriting history (rebase, amend) yields new commit hashes — the content changed, so the address must too.

How do branches and merges work in Git’s object model?

A branch is just a named pointer to a commit (a small file holding a commit hash), so branching is nearly free and switching just changes which commit you view. Since each commit records its parent(s), history is a directed graph: a normal commit has one parent, a merge commit has two or more, tying lines of history together. The stored snapshots don’t change — a merge is a new commit whose tree is the combined result and whose parents are the merged commits. This graph-of-snapshots model makes branching and merging cheap.

Keep going

Finished this one? 0 / 44 Labs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Labs