Reading — step 1 of 5
Read
Checkout: Updating the Working Tree
Checkout has a scary reputation it doesn't deserve. Strip the porcelain and it's a pure function: given the snapshot I have and the snapshot I want, compute the minimal file operations that turn one into the other — then do them. You'll implement the computing half (the plan), which is where all the intelligence lives.
Two snapshots in, operations out
Switching from commit A to commit B means the working tree must stop looking like A's tree and start looking like B's. Load both trees (your read-tree walker!) into path → hash maps and compare, path by path:
| in A | in B | hashes | plan |
|---|---|---|---|
| yes | yes | equal | nothing — the file is already right |
| yes | yes | differ | update — write B's blob content |
| yes | no | — | delete |
| no | yes | — | create — write B's blob |
That table is checkout. Two map lookups per path, four outcomes. Everything else is bookkeeping: after applying the plan, point HEAD at B (branch name → symbolic; raw commit → detached, as you built in Refs), and set the index to B's tree so status starts clean.
Why the "nothing" row is the important one
Naive checkout could just delete everything and re-extract B. It would even be correct — and git would be unusable on real repos: switching branches in a 100k-file repository typically touches a handful of files, and the no-op row is why checkout takes milliseconds instead of minutes. The immutable object store makes the comparison honest: same hash ⇒ same content, guaranteed, no byte-comparison needed. Content addressing (lesson one!) pays out here as pure speed.
The safety rule
Real git refuses to check out over uncommitted changes it would destroy: if your working copy of a file differs from the current snapshot and the plan wants to update or delete that file, checkout aborts ("your local changes would be overwritten"). The rule's logic in one line: a working-tree change lives only in the working tree — no object, no ref, no reflog — so overwriting it is the one data loss git cannot undo. Anything committed is recoverable; checkout guards the one thing that isn't. (Files that are dirty but identical in A and B are safe to leave — the no-op row saves uncommitted work too.)
The same table runs everything
Here's the payoff for building it cleanly: reset --hard is this plan applied without moving branches; merge's working-tree update is this plan from the merge result; clone is this plan from empty; stash pop is this plan in reverse. Git has one working-tree updater, and you're writing it. Even the index update rides along — set every planned path's index entry to B's hash, done.
Your exercise: Compute Checkout Plan
Given two trees, output the create/update/delete operations (in the spec's format and order). The classic mistakes are the asymmetric ones: files only in A (must delete — walkers that iterate only B's paths miss them entirely) and the update-vs-create confusion (both write B's content; the name of the operation depends on presence in A). Iterate the union of both key sets, apply the table, sort the output. Four rows of logic — and the day-to-day speed of every git user on earth.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…