Reading — step 1 of 5
Read
Tree Objects: Directory Snapshots
Blobs are anonymous content — no names, no permissions. Trees are where names live: a tree object is a directory listing, each entry pointing at a blob (a file) or another tree (a subdirectory) by hash. One tree hash therefore names an entire directory snapshot, recursively, forever. This is the object that turns a content store into a filesystem snapshot machine.
The entry format (binary — bring last lesson's discipline)
A tree's payload is a sequence of entries, each:
<mode> <name>\0<20-byte-binary-sha>
Concretely, a directory with README.md and src/:
100644 README.md\0⟨20 raw bytes⟩40000 src\0⟨20 raw bytes⟩
Three format facts that decide whether your hashes match real Git's:
- The SHA is 20 raw bytes, not 40 hex characters. The single most common build-your-own-git bug. Convert hex → binary before writing (
bytes.fromhex/hex.DecodeString). - Modes are ASCII octal, no leading zero on directories:
100644regular file,100755executable,120000symlink,40000directory — written as40000, not040000(Git is quirky here; match the quirk or mismatch every tree hash). - No separators between entries — the fixed 20-byte SHA is what tells the parser where the next entry begins. (And now you see why parsing on strings dies: those 20 bytes can contain
\0, newlines, anything.)
The sort rule — the other famous gotcha
Entries must be sorted by name, byte-wise — but directories sort as if their name ended with /. So foo.txt sorts before directory foo-bar but after directory foo … work an example:
foo (dir → sorts as "foo/")
foo.txt (file → sorts as "foo.txt")
"foo.txt" < "foo/" byte-wise (. is 0x2E, / is 0x2F) — so the file comes first. Get the sort wrong and your tree's bytes differ → its hash differs → nothing you build agrees with Git ever again. Canonical form is the price of content addressing: same directory must always produce the same bytes.
Hash it like everything else
A tree is an object like any other: wrap the payload in tree <size>\0, SHA-1, store via your round-trip code. Nesting composes naturally — to hash a directory:
- Recursively hash subdirectories (getting their tree hashes) and files (blob hashes).
- Build this directory's entry list, sorted per the rule.
- Hash the resulting tree payload.
Bottom-up, leaves first. And notice what one top-level tree hash now means: it cryptographically pins the entire project state — every filename, every mode, every byte of every file, recursively. Two trees with the same hash are the same project state, guaranteed. A commit (next lesson) needs only to point at one tree hash to snapshot everything.
Your exercise: Hash a Tree Object
Given a directory description, produce its Git tree hash: build entries (mode, name, NUL, raw-20-byte sha), sort with the directory-slash rule, wrap in the header, SHA-1. The graders probe exactly the classic mistakes: hex-instead-of-binary SHAs, 040000, and the file-vs-directory sort order — all three now labeled and defused. When it passes, run git ls-tree <hash> mentally: you can reconstruct the listing from bytes you know cold.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…