Skip to content
C.W.K.
Lesson 03 of 06 · published

Build the Commit in a Scratch Index

~13 min · version-control, plumbing, concurrency, implementation

Level 0Wet Clay
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

There Is One Index Per Repository

This is the fact everybody knows and nobody applies. The staging area is repository state, not session state. When three sessions work in one checkout, they share it — and the polite-looking move of scoping a commit to your own paths protects less than it looks like it does. Be precise about which half works, because getting this backwards is common: a commit given paths does disregard whatever else is staged, and that half is documented and reliable. What it cannot do is protect you within a file you named, where it commits the working tree — a neighbor's half-finished edit included. And building the commit the ordinary way still runs git add against the shared index, so your staging rewrites theirs.

The measured version: a session scoped its commit to its own paths while a neighbor had ninety-two files staged. The neighbor's set stayed out of the commit, exactly as documented — and the session still could not ignore it, because every add it made was written into the index that neighbor was working in.

Two Reasons to Reach for Plumbing

The fix is to build the commit without touching the shared index at all, using the low-level operations that let you point at a scratch index file instead.

Isolation. Start from a chosen base tree, add exactly your paths, write a tree, create a commit object. The shared index is neither read nor written while the commit is built, so a neighbor's staged work cannot travel with you and your commit cannot disturb theirs.

No hooks. Plumbing does not fire the pre-commit hook. When that hook does genuine but expensive work — regenerating assets, syncing to an external service — every commit becomes a multi-minute operation holding the lock file everyone else needs. Routing the automated landing around it took landings from minutes to seconds, while ordinary commits by a person kept their hooks and the work the hooks were doing.

Moving the Branch Safely

Creating the commit object does not move anything; the branch reference still has to be updated. Do that with a compare-and-swap: update the reference only if it still points where it did when you started. If somebody else moved it in between, the update fails cleanly, and the correct response is to rebuild on the new tip rather than to force.

One more check belongs before the reference moves: verify that the commit's file list is a subset of the paths this operation was allowed to touch. It costs one tree comparison, it catches the case where a path pattern matched more than intended, and it is the difference between a landing that quietly swept in an unrelated file and one that refused.

When a tool's convenient interface reads shared state, the low-level interface is not a micro-optimization — it is the isolation. The porcelain commands are built for one human in one checkout, which is a reasonable assumption that stops holding the moment automation shares the working copy.

Code

A landing commit built without the shared index·bash
# Steps 1-5 run against a THROWAWAY index file. The repository's
# own index - which a neighbor session may have 92 files staged
# in - is neither read nor written while the commit is built.
# Step 6 touches it once, for our paths only, after the landing
# exists.

export GIT_INDEX_FILE="$(mktemp -u)"

# 1. start from the base we deliberately chose (next lesson)
git read-tree "$BASE"

# 2. add exactly our paths, from the working tree
git add -- content/target/            # OUR material only; the shared
                                      # wiring is a separate pass (L05)

# 3. write a tree object and a commit object. NO HOOKS FIRE.
TREE=$(git write-tree)
NEW=$(git commit-tree "$TREE" -p "$BASE" -m "$MESSAGE")

# 4. leak check BEFORE moving anything: the commit must touch
#    nothing outside the paths this operation is allowed to.
git diff-tree --no-commit-id --name-only -r "$NEW" \
  | grep -qvE '^content/target/' \
  && { echo "refusing: commit touches paths outside land_paths"; exit 1; }

# 5. move the branch by COMPARE-AND-SWAP against the BASE we built
#    on - the checkout's own tip, once the next lesson's rule has
#    run. Anything that landed after the base was chosen makes the
#    swap fail instead of being dropped from history. Then STOP:
#    rebuild, never force - and rebuild WITH the checkout: step
#    back, fast-forward the worktree, commit again (last lesson).
git update-ref refs/heads/main "$NEW" "$BASE" \
  || { echo "refusing: the branch moved since the base was chosen"; exit 1; }

# 6. re-point the SHARED index at the new commit for OUR paths only,
#    so the people working in this checkout do not see our own
#    landing as a staged reversion. Their staged work is untouched.
unset GIT_INDEX_FILE                  # back to the SHARED index first
git reset -q -- content/target/

# The ordinary human `git commit` in this same checkout still runs
# its hooks and still does the work they do. Only the automated
# landing - which gained nothing from them - routes around.

External links

Exercise

In a scratch clone, stage a file, then use the plumbing sequence above to build and land a commit containing a different file. Confirm afterward that your staged file is still staged and was not included. That is the isolation property in one experiment, and having done it once is what makes you reach for it the day three processes share a checkout.
Hint
Set the temporary index path in the environment for just those commands rather than exporting it for your shell session — an exported value that outlives the operation will confuse the next ordinary command you run and produce a very puzzling few minutes.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.