Snapshots, not diffs
Git stores each commit as a full snapshot of the project tree, not as a chain of diffs. A commit points to a tree, the tree points to blobs, and unchanged files reuse the same content-addressed blob — so snapshots are cheap. A diff is computed on demand, never stored.
Most people assume git stores history the obvious way: keep the first version, then for every change save a little patch — “line 7 changed from X to Y” — and stack those patches up. That is how older systems like Subversion and CVS actually worked, and the mental model leaks into how people reason about git. It is also wrong, and the wrong model makes git’s speed look like magic instead of an obvious consequence of its design.
By the end of this lesson you will be able to say what git really stores for each commit — a full snapshot of your project — and explain why that makes branching, checkout, and merge fast rather than wasteful.
After this lesson you can explain that a git commit stores a complete snapshot of the project tree (not a diff), describe how a commit points to a tree that points to content-addressed blobs, say why identical content is stored only once, and explain that git diff is computed on demand by comparing two snapshots.
A commit is a snapshot of your whole project, not a record of what changed. When you commit, git does not write down “these three lines changed.” It records what every tracked file looks like at that moment — the entire tree, top to bottom. Conceptually, each commit is a photograph of the project. Older systems (CVS, Subversion) store a base version plus a list of per-file deltas and reconstruct any version by replaying patches. Git took the opposite bet: store the states, derive the differences later.
The snapshot is built from three object types: commit, tree, and blob. Git’s storage is a small graph of objects:
- A blob holds the raw bytes of one file’s contents. Just the content — no filename, no path.
- A tree is a directory listing: it maps names (
README.md,src/) to the blobs and sub-trees they contain. - A commit points to one top-level tree (the project’s root snapshot) plus metadata: author, message, and a link to its parent commit.
So a commit points to a tree, the tree points to blobs and other trees, and together they reconstruct the exact project state — no patch replay required.
Every object is named by the SHA-1 hash of its content — this is content-addressing. Git runs a hash function over an object’s bytes and uses the resulting 40-character hex string as the object’s name and address.
# git computes the same hash you would get with hash-object
echo 'hello' | git hash-object --stdin
# -> ce013625030ba8dba906f756967f9e9ca394464aThe crucial property: the name is a fingerprint of the content. Identical content always produces the identical hash. Change one byte and the hash changes completely. The address is not a location on disk — it is derived from what is inside.
▸Why this works
Content-addressing is why “store a full snapshot every commit” is not wasteful. If a file did not change between two commits, its bytes are identical, so its blob hash is identical, so both commits’ trees point to the same blob object — git stores that content exactly once. A commit that touches one file in a 10,000-file repo creates one new blob, a handful of new trees along that file’s path, and one new commit object. The other 9,999 files are shared by reference, not copied. Snapshots are cheap precisely because identical content is deduplicated by its hash.
Because git holds whole snapshots, reading any version is a direct lookup, not a replay. To check out an old commit, git reads that commit’s tree and writes out the blobs it names — done. It never walks a chain of patches. This is why git checkout, branching, and switching history points feel instant: there is no diff to apply, just a snapshot to materialize. Branching is cheap for the same reason — a branch is only a pointer to one commit (a 40-byte hash), not a copy of files.
A diff is an output computed on demand, never the storage format. When you run git diff or git log -p, git takes two snapshots and calculates the differences between them right then:
git diff HEAD~1 HEAD # compute changes between two snapshotsThe patch you see is generated for your eyes, not loaded from disk. This inverts the old systems: they stored diffs and computed snapshots; git stores snapshots and computes diffs. The diff is a view, not the source of truth.
One edit in a two-file project.
Your repo has README.md and app.js. Commit A snapshots both: its tree names blob A (README content) and blob B (app.js content). Now you fix a typo in app.js and make commit B.
What git actually writes for commit B: a new blob C (the new app.js bytes — its hash differs because one byte changed), a new tree (README → blob A, app.js → blob C), and a new commit object pointing at that tree. Notice README.md was untouched, so its content and hash are unchanged — commit B’s tree reuses blob A, the very same object commit A points to. Git did not copy the README and did not store a “typo fixed” patch. It stored a fresh snapshot whose only new content is the one file that changed; everything else is shared by hash. When you later run git diff A B, git compares the two trees and computes the one-line change for you on the spot.
▸Common mistake
A common misconception is “full snapshots every commit must make the repo enormous.” It does not, because of deduplication: unchanged files are pointers to existing blobs, not copies. A repo with a thousand commits does not hold a thousand copies of an unchanged license file — it holds one blob, referenced a thousand times. (Git also later packs objects with delta compression in storage, but that is an optimization layer underneath; the logical model the commands work with is always whole snapshots.)
In git, what does a single commit fundamentally store?
Git stores each commit as a full snapshot of the project, not as a chain of diffs. A commit points to a tree, the tree points to blobs (file contents) and sub-trees, and every object is named by the SHA hash of its content — content-addressing. Because identical content yields an identical hash, unchanged files across commits reuse the same blob and are stored only once, so snapshots stay cheap. Reading any version is a direct lookup with no patch replay, which is why checkout, branching, and merge are fast. A diff is computed on demand by comparing two snapshots — it is an output, never the storage format. Next you will see where your files actually live as you work: the three areas git moves changes through.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.