The object store — blobs, trees, commits, tags
Git stores everything as four content-addressed object types: blob (file bytes), tree (a directory), commit (one tree plus parents and metadata), and tag. An object's name is the SHA of its content, so identical content is stored once. Inspect with git cat-file.
You have used git commit a thousand times. But what is a commit on disk? Not a diff, not a row in a database — it is a tiny file whose name is a hash of its own contents, pointing at other files whose names are also hashes of their contents. Once you see this, branches, merges, and reset stop being magic and become pointer arithmetic over an append-only object store.
By the end of this lesson you will be able to name git’s four object types, explain why their names are content hashes, and walk a commit down to its file bytes with git cat-file.
After this lesson you can describe the four git object types — blob, tree, commit, tag — explain content-addressing and the deduplication it gives you, and use git cat-file and git hash-object to inspect the real objects behind a commit.
Everything git stores is one of four object types. Under .git/objects git keeps a key-value store of immutable objects, each one of exactly four kinds:
- blob — the raw bytes of one file’s contents. A blob has no filename and no permissions; it is just content.
- tree — one directory. A tree is a list of entries, each
mode + type + hash + name, pointing at blobs (files) and other trees (subdirectories). The names live here, not in the blobs. - commit — a snapshot. It points at exactly one top-level tree, lists zero or more parent commits, and carries author, committer, timestamps, and your message.
- tag — an annotated tag object: a name that points at another object (usually a commit) plus a tagger and message. (Lightweight tags are just refs and create no object.)
An object’s name is the hash of its content — this is content-addressing. Git takes the object’s type, length, and bytes, hashes them, and uses that hash as the object’s address. Historically the hash is SHA-1 (40 hex chars); git is migrating to SHA-256. The consequence is profound: the name is derived from the content, so you cannot change content without changing the name.
# Hash some content the way git does, without storing it
echo 'hello' | git hash-object --stdin
# -> ce013625030ba8dba906f756967f9e9ca394464aIdentical content is stored exactly once. Because the address is the content hash, two files with the same bytes — or the same file unchanged across a hundred commits — resolve to the same blob hash and occupy one object. A commit that touches one file in a 10,000-file tree does not copy the other 9,999 blobs; its new trees simply re-point at the unchanged hashes. Snapshots are cheap precisely because shared content is shared by address.
git rev-parse HEAD # the commit object's hash
git cat-file -t HEAD # -> commit (ask the type of an object)
git cat-file -p HEAD # -> pretty-print: tree, parent, author, message▸Why this works
Content-addressing is also git’s integrity check. If a byte of an object is corrupted on disk, its content no longer matches its name, and git fsck (or any read that re-hashes it) detects the mismatch. The whole history is a Merkle DAG: a commit hash commits to its tree hash, which commits to every blob hash beneath it, which means a single SHA at the tip cryptographically fixes the entire reachable history. Change anything deep in the past and every descendant hash changes.
You can walk a commit down to file bytes by hand. Each cat-file -p peels one layer:
git cat-file -p HEAD # commit -> shows "tree <hash>"
git cat-file -p HEAD^{tree} # tree -> rows of: mode type hash name
git cat-file -p <blob-hash> # blob -> the file's actual contentsA tree row like 100644 blob a1b2c3… README.md says: a normal file (100644), stored as the blob a1b2c3…, named README.md. Executable files use mode 100755; subdirectories appear as 040000 tree …. That is the entire filesystem model.
Loose objects get packed for efficiency. A freshly written object is a loose object: one zlib-compressed file at .git/objects/ab/cdef… (first two hex chars are the directory). When these pile up, git gc rewrites many of them into a packfile (.git/objects/pack/*.pack) that stores objects together and delta-compresses similar ones against each other. Same content-addressed identities, denser storage — which is why a repo with deep history is far smaller than the sum of its snapshots.
Proving dedup with two commits.
Initialise a repo and commit a file:
git init demo && cd demo
printf 'hello\n' > a.txt
git add a.txt && git commit -m "add a"
git rev-parse HEAD:a.txt # -> ce013625… (the blob hash for a.txt)Now copy the same bytes into a second file and commit:
cp a.txt b.txt
git add b.txt && git commit -m "add b (same bytes)"
git rev-parse HEAD:b.txt # -> ce013625… (IDENTICAL hash)Both files name the same blob — git stored the content once and pointed two tree entries at it. The two commits differ only in their trees (which now list two names) and their parent links. Change one byte of b.txt and its blob hash changes completely; the old blob stays, still reachable from the first commit. Nothing is ever edited in place — the store only grows.
▸Common mistake
A common misreading: “blobs store filenames and permissions.” They do not. A blob is only content — that is exactly why an identical file under two names dedups to one blob. Names and modes live in the tree entry that points at the blob. This split is also why renaming a file with unchanged contents adds no new blob at all; only the tree changes.
Two different files in the same commit contain byte-for-byte identical contents. How many blob objects does git store for them?
Git is a content-addressed object store with four types: blobs hold file bytes, trees are directories that map names and modes to blob/tree hashes, commits point at one tree plus parent commits and metadata, and tags annotate another object. Every object’s name is the SHA of its content, which gives free deduplication (identical content stored once) and integrity (corruption changes the name). You can walk any commit down to bytes with git cat-file -p, and git gc packs loose objects into delta-compressed packfiles. Next you will see how human-friendly refs — branches, tags, and HEAD — are just names pointing into this store.
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.