git displaced every centralized VCS incumbent with one structural bet: the repository is a persistent, content-addressed object database β immutable blobs, trees, commits, and tags keyed by the hash of their bytes β with a minuscule mutable layer (refs: named pointers into the DAG) on top. Branching, merging, history rewriting, dedup, verification, and offline work are all derived operations on that model. This deep-dive teaches what choosing the right core data model buys you, and what it costs (garbage, packs, and protocol complexity).
π§ Explain like I'm 10
A photo album where nothing is ever erased. Every time you save your drawing, git takes a brand-new photo of each changed file (a blob), a new photo of each folder's list of contents (a tree), and a new photo of the whole moment (a commit) β and names each photo by a fingerprint of its exact pixels (the SHA hash). Identical photos get identical fingerprints, so they are stored only once, and if anyone sneakily changes one pixel, the fingerprint changes and the trick is caught. A branch is just a sticky note (a ref) pointing at a favorite photo; "merging" means comparing two photo collections and adding only the photos you're missing β you never need to phone a central office, because every computer that cloned the album owns every photo.
Why the grown-ups did it this way: never overwriting anything means history can always be verified, duplicates disappear for free, and two people (or two laptops, offline) can always compare and combine safely β without anyone having to be in charge.
1. Architecture overview
flowchart TD
PORC["porcelain β git commit, git log, git merge...<br/>libgit.a + builtin (130 builtins)"]
PLUMB["plumbing β hash-object, cat-file, rev-parse,<br/>update-ref (the data model as commands)"]
subgraph core["the object database β odb.c + odb/*.c"]
ODB["sources walked in order:<br/>in-memory β loose files β packs<br/>key = hash of type + bytes; value = the bytes"]
end
REFS["refs β refs.c<br/>files / packed / reftable backends<br/>name β oid: the only mutable state"]
IDX["index β read-cache.c<br/>staging snapshot + stat cache<br/>(derived, always rebuildable)"]
WT["working tree β just files"]
PORC --> PLUMB --> ODB
REFS -->|"point into"| ODB
IDX -->|"built from"| ODB
WT -->|"git add"| IDX
IDX -->|"git commit"| ODB
Diagram source β enable JavaScript to render the diagram.- Four object types (
srcof truth:object.c:29object_type_strings[]): blob (file bytes), tree (directory listing: mode+name+child oid), commit (tree oid + parent oids + author/message), tag (signed pointer). Everything else β branches, HEAD, tags-as-ref, remotes β is a ref, a mutable file/mappingname β oid. - The spine β "git add + git commit" β is: working file β blob
(
odb_write_object) β new tree (bottom-up) β new commit (points at tree + parents) β move one ref. Note the asymmetry: writes create immutable objects; the only mutation is the ref update.
Walkthrough: git add + git commit, fingerprint to sticky note
One save, end to end β step through it on the site or read it here. The sequence matters: objects are written first, the pointer moves last.
sequenceDiagram
autonumber
participant W as working tree
participant I as index (read-cache.c)
participant O as object database
participant R as refs
W->>I: git add f β hash file, stage blob oid + stat
I->>O: write blob: deflate, temp file, rename into objects/xx/38hex
I->>O: git commit β write tree objects bottom-up from the index
O->>O: write commit: tree oid + parents + message
O->>R: update-ref moves the branch sticky note (atomic rename)
R->>R: reflog appends the move (the undo journal)
Note over O,R: a crash anywhere before the ref move = orphan objects, never corruption
Diagram source β enable JavaScript to render the diagram.Fingerprint the file
git add f runs the write path:
builtin/hash-object.c:hash_object (38) β odb_write_object β note that
by default "write" only computes the id; persistence is a flag. The
content is prefixed <type> <size>\0, hashed, and deflated
(object-file.c:write_object_file_prepare, 329).
Store the blob immutably
write_loose_object_common (704)
writes a temp file and renames it into
.git/objects/<2-hex>/<38-hex> β the fanout directory. The rename is
the atomicity: an object either exists whole or not at all.
Stage it
The index (read-cache.c:do_read_index, 2198)
gains a cache_entry: path, oid, stat data. The stat cache is why
git status is O(changed files) β unchanged files are never re-hashed.
Write the trees
git commit turns the index into tree
objects bottom-up: one flat sorted list per directory
(tree.c:parse_tree_buffer, 175), each named by its own hash, parents
referenced by fingerprint β decode_tree_entry (17) shows the
<mode> <name>\0<raw oid> format.
Write the commit
commit.c:parse_commit_buffer (516) is
the format in prose: tree <oid>, parent <oid>..., author, message.
A new commit points at the new tree and at the old HEAD as parent β
the DAG grows by append, never by edit.
Move the sticky note
update-ref writes the new oid under
.git/refs/heads/<branch> (temp file + rename) and the reflog appends a
line. This one-line file update is the entire mutation git performed
for your commit.
History you can prove
Everything else β log, diff, merge,
push β is derived by walking fingerprints. If the process died at any
earlier step, you'd have unreferenced objects (garbage for git gc),
never a broken repository.
2. Core data structures β and why these
The object database with swappable sources (odb.c, odb/)
odb_read_object (odb.c:744) β odb_read_object_info_extended
(odb.c:656) walks a list of sources: in-memory (odb/source-inmemory.c),
loose files (odb/source-loose.c), packs (the classic packfile.c
machinery). Writes go through odb_write_object_ext (odb.c:988).
Why layered sources: correctness first (loose objects are trivially
inspectable), performance later (packs), plus testing and partial-clone
backends β all without touching the model. Note
odb/source-loose.c:odb_source_loose_read_object_info (line 206) and
the vtable registration at line 712.
Content addressing: the object ID is the checksum
An object's key is the hash of <type> <size>\0<bytes> β computed while
writing (object-file.c:write_object_file_prepare, line 329, hashing
through the deflate in write_loose_object_common, line 704). Verifying
an object (object-file.c:check_object_signature, line 101) is re-hashing
it. Storage path = .git/objects/<2-hex>/<38-hex> (fanout directory).
Why: integrity, dedup, and addressing collapse into one mechanism.
Identical content stored once; corruption detectable by construction;
no registry to maintain.
The index (read-cache.c)
A flat, sorted array of cache_entry (path, oid, stat data) β the
staging area between working tree and tree objects, loaded by
do_read_index (read-cache.c:2198). Why a derived cache: because the
truth is the object DAG, the index can be deleted and rebuilt; its stat
cache makes git status O(changed files), not O(files).
Refs (refs.c + backends)
A ref is name β oid. The files backend keeps loose files under
.git/refs/ with a packed-refs fallback (refs/files-backend.c:files_read_raw_ref,
line 596; refs/packed-backend.c:packed_refs_lock, line 1217); a
reftable backend (refs/reftable-backend.c) offers a log-structured
alternative. Reflogs record every movement. Why: the entire mutable state
of a distributed system reduced to ~a hundred tiny files with atomic
rename semantics β cheap to lock, cheap to audit.
Packs + deltas (builtin/pack-objects.c, diff-delta.c)
Loose objects are wasteful, so packs concatenate objects with per-object
headers and a fanout index for binary search (packfile.c:bsearch_pack,
line 2002); similar objects are stored as deltas against bases
(builtin/pack-objects.c:try_delta, line 2789; diff-delta.c:create_delta,
line 320), with pack-revindex.c mapping offsetsβorder to resolve chains.
Why: the immutable model makes repacking a pure function β pack any
subset, delete the rest, nothing breaks.
3. Concurrency and failure model
- Concurrency is file-lock granularity:
.git/index.lockserializes index writes; ref updates take per-ref locks (refs/files-backend.c); packed-refs takes one global lock (packed_refs_lock:1217). No daemon, no shared memory β the filesystem is the coordinator. - Crash safety via atomicity of the tiny mutable surface: object
writes complete into a temp file then rename; a crashed commit leaves
unreferenced objects (garbage, not corruption) because refs moved last.
git fsckre-verifies reachability and signatures. - The reflog is the undo journal β every ref move appends a history line, making "destructive" operations recoverable.
- Failure of trust (bitrot, hostile mirrors) is caught by the model
itself:
check_object_signaturere-hashes content; transport (index-pack) verifies every received object's hash before install.
flowchart LR
subgraph locks["the filesystem is the coordinator β no daemon"]
IL["index.lock β serializes index writes"]
RL["per-ref locks β refs/files-backend.c"]
PL["packed-refs lock β one global<br/>(packed_refs_lock 1217)"]
end
W1["git process 1"]
W2["git process 2"]
OBJ["object writes: temp file β rename<br/>crash = orphan object, not corruption"]
REF["ref move: last write wins per ref,<br/>reflog records every move"]
FSCK["git fsck β re-verifies reachability<br/>and every object signature (101)"]
W1 --> IL
W2 --> IL
W1 --> OBJ
W2 --> OBJ
OBJ --> REF
REF --> FSCK
RL -.-> REF
PL -.-> REF
Diagram source β enable JavaScript to render the diagram.4. API and extension design
- Plumbing before porcelain: the data model is directly exposed as
commands β
hash-object(builtin/hash-object.c:64),cat-file,update-ref,rev-list. Porcelain is shell/UI over plumbing; users and scripts share the same substrate (the "scriptable core" decision that made CI possible). - Swappable backends at every layer: odb sources (incl. alternates β
borrow objects from other repos on disk), ref backends (files vs
reftable), hash algorithm polymorphism (
hash.h:struct git_hash_algo, line 275): v2.55 writes compatibility oids during SHA-1βSHA-256 interop (compat_oidparameter ofodb_write_object_ext) β a live lesson in migrating a hash-addressed ecosystem without a flag day. - The wart: config/UI surface sprawl, and the wire protocol grew negotiation features (partial clone, promisor remotes) that strain the "everything is local objects" simplicity.
5. The defining trade-offs
Trade-off 1: immutable content-addressed core over mutable state
- Tension: version history wants cheap edits; distributed use wants verifiable, deduplicated, mergeable state.
- Choice: all history immutable and hash-addressed; mutation confined to ref pointers.
- Evidence:
odb_write_objectnever overwrites (new bytes β new oid); a commit that changes one character writes a new blob, new trees up the path, new commit (commit.c:parse_commit_buffer:516shows the parent links that build the DAG); ref move is a one-line file update. - Given up: in-place anything. Every save produces garbage; every "edit" is add; storage and GC become core concerns (β trade-off 3).
- Verdict: right when history integrity, distribution, and branch/merge are the product. Wrong for hot mutable data β the lesson generalizes: pick immutability where derivation (diffs, merges, sync) dominates.
Trade-off 2: full local replica over central server
- Tension: collaboration needs shared state; networks fail.
- Choice: every clone holds the complete DAG; sharing (fetch/push) is object transfer + ref negotiation between peers.
- Evidence:
git clonecopies the odb;alternateseven lets repos share object stores locally;git log/git diffnever touch the network. - Given up: global ordering/permissions, cheap single-source backups; "server" products (GitHub) became the trust and auth layer outside the model.
- Verdict: right for development workflows (offline speed, forking culture); the price is that identity of humans/repos is metadata, not model β patch-ids, mailmaps, signing keys all bolt on.
Trade-off 3: loose objects + lazy packing over one canonical format
- Tension: write path wants cheap appends; disk/network want density; reads want random access.
- Choice: write loose (one zlib file per object), periodically
repack with deltas (
try_delta), keep packs immutable once written. - Evidence:
odb/source-loose.cfor the write/read-loose path;builtin/pack-objects.cfor the compaction;pack-revindex.cexists purely to reverse-map pack offsets so delta chains can be walked. - Given up: read locality (a 3-byte blob may need its base's base), and a real GC story (unreachable-but-locked objects can accumulate).
- Verdict: right because both formats serve different phases of the object lifecycle β hot (fresh, inspected) vs cold (packed, shipped). Systems that merge the two pay on both ends.
6. Reading path
object.c(whole file, ~250 lines) β the four types as an enum and a type-name table (line 29); the in-memorystruct objectheader all parsed objects share.- Hands-on: in a scratch repo run
git init; echo hi > f; git add f, thenfind .git/objects -type fandgit cat-file -p <oid>β you have just exercised everything step 3 describes. builtin/hash-object.c:hash_object(line 38) βodb.h:odb_write_object(inline, line 557; noteODB_WRITE_OBJECT_PERSISTβ by default "write" only computes the id!) βobject-file.c:write_object_file_prepare(329) +write_loose_object_common(704): header<type> <len>\0, hash, deflate, temp-file-then-rename into the fanout path.tree.c:parse_tree_buffer(175) +tree-walk.c:decode_tree_entry(17) β the tree entry format<mode> <name>\0<raw oid>; directories are flat sorted lists, recursively addressed.commit.c:parse_commit_buffer(516) β the commit format is prose:tree <oid>,parent <oid>..., author/committer lines; notice the graft/shallow hooks that let history be spliced.odb.c:odb_read_object(744) βodb_read_object_info_extended(699) β the source walk (in-memory β loose β packed); thenodb_read_object_peeled(762) for tagβcommitβtree dereferencing.object-file.c:check_object_signature(101) β the model's free integrity check; pair withgit fsckin your scratch repo.refs.c:refs_read_ref(464) βrefs/files-backend.c:files_read_raw_ref(596) with the packed-refs fallback, thenrefs/packed-backend.c:packed_refs_lock(1217) β the entire mutable state of the system.read-cache.c:do_read_index(2198) +set_index_entry(134) β the index as derived cache;git ls-files --debugmakes it visible.builtin/pack-objects.c:try_delta(2789) +packfile.c:bsearch_pack(2002) +diff-delta.c:create_delta_index(135) β how density is bought with copy-window heuristics and binary-searchable fanouts.revision.c:prepare_revision_walk(3974) andmerge-ort.c:merge_ort_internal(5303) β the payoff: log/blame/merge are pure graph algorithms over the DAG, no special cases for "the server's copy".
You have now traced content β object β tree β commit β ref β pack, and seen every mutation the system allows.
7. What to steal
- Make the core immutable and content-addressed; confine mutation to a pointer layer β verification, dedup, and distribution fall out of the addressing scheme (CAS avant la lettre: S3, Nix, OCI images all rediscovered this).
- Derive caches aggressively (index, commit-graph, packs): when truth is append-only and hash-named, caches are always rebuildable β you can add performance structures for years without migrations.
- Expose the data model directly in the API (cat-file/hash-object): scriptability and debuggability come from letting users touch the substrate.
- Migrate breaking changes with dual-write interop
(
compat_oid) β a hash-format ecosystem can be moved across algorithms without a flag day if new writes speak both. - Atomic-rename + append-only reflogs as your only "transactions" β filesystem primitives, correctly sequenced, replace a database.
8. Difficulty and prerequisites
- Difficulty: moderate β the model is simple; the C is plain; the challenge is holding the DAG in your head while reading merge/repack.
- Budget: ~5β7 hours for the reading path with a scratch repo at hand.
- Prerequisites: DAGs and topological ordering; zlib at a "it's
compression" level; comfort with
.gitas a directory you canls.
9. Further reading
Documentation/gitformat-*.txtin the repo (object, index, pack, and reftable formats β the specs the code implements).Documentation/technical/β especiallyprotocol-capabilities.txt,partial-design.txt(partial clone/promisors).- Linus Torvalds' original Google talk on git (2007) β the design argument in the author's own words.