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

Build the Commit in a Scratch Index

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

Level 0Wet Clay
0 XP0/36 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 never read and never written, 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 is cheap, 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 that never touches the shared index·bash
# Everything below runs 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.

export GIT_INDEX_FILE="$(mktemp -u)"

# the CAS's expected old value: where the branch is RIGHT NOW.
# Not the base - see step 5.
LOCAL=$(git rev-parse refs/heads/main)

# 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. The expected old value is
#    the branch's CURRENT tip, NOT the base we built on. When the
#    base came from the remote the two differ, and a swap against
#    the base would reject every time - in precisely the behind
#    case this is for. Fails cleanly if somebody moved it while we
#    were working; rebuild, never force.
git update-ref refs/heads/main "$NEW" "$LOCAL"

# 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 inPlease sign in to comment.

No comments yet — be the first.