Git’s command line is famously confusing. The data model underneath is not. It’s
a small content-addressed key-value store with exactly four object types. Once you
can see the objects, commands like reset, checkout, and rebase stop being
incantations and become obvious manipulations of a graph.
Content Addressing: The Hash Is the Key
Git stores everything by the hash of its content. Hash the bytes, and that hash is the address:
echo 'hello' | git hash-object --stdin
# ce013625030ba8dba906f756967f9e9ca394464aSame content always produces the same hash, so identical files are stored once. Change one byte and you get a completely different address — which is how Git detects any change and why history is tamper-evident.
The Four Objects
Everything in .git/objects is one of four types:
blob → file contents (no name, no metadata, just bytes)
tree → a directory: names + modes → blob/tree hashes
commit → a snapshot: one tree + parent(s) + author + message
tag → an annotated pointer to an objectThey compose into the snapshot of your project:
commit ──▶ tree ──▶ blob (README.md)
└──▶ tree ──▶ blob (src/main.c)
parent
│
▼
commit ──▶ tree ──▶ ...A commit doesn’t store a diff — it points at a tree, a full snapshot of every file at that moment. Unchanged files reuse the same blob hashes across commits, so snapshots are cheap despite being complete.
Commits Are a Linked List
Each commit records its parent(s), forming a directed graph back through history:
A ◀── B ◀── C ◀── D (main)
▲
└── E ◀── F (feature, branched at C)A merge is just a commit with two parents. There’s no special “branch history” structure — branches are derived by walking parent pointers.
Refs: Branches Are Just Pointers
A branch isn’t a copy of anything. It’s a 40-character file containing the hash of one commit:
cat .git/refs/heads/main
# d4f7a... (the tip commit of main)Creating a branch writes one tiny file. That’s why branching is instant. HEAD
points at the current branch; committing just advances the branch’s pointer to the
new commit. Now the commands fall out:
git resetmoves a branch pointer to a different commit.git checkout/switchmovesHEADand updates your working tree to that tree’s blobs.git rebasereplays commits onto a new parent, creating new commit objects.
Takeaways
- Git is a content-addressed store: an object’s address is the hash of its content, so identical content is deduplicated and history is tamper-evident.
- Four object types — blob, tree, commit, tag — compose into full snapshots, not diffs.
- Commits link to parents to form the history graph; a merge is a two-parent commit.
- Branches and
HEADare just pointers to commits, which is why branching and reset are instant.