Skip to content

fix(pm): git-history's touch leg reads the graft, not just the parent's presence - #18398

Merged
os-elon-musk merged 3 commits into
mainfrom
claude/issue-18355-git-history-touch-boundary-flag
Sep 16, 2026
Merged

os-elon-musk merged 3 commits into
mainfrom
claude/issue-18355-git-history-touch-boundary-flag

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #18355

touchIsProvable proved leg 1 by asking whether every parent the commit OBJECT names is present locally. A later fetch — of another ref, or of one sha — brings that parent into the object store without moving the shallow boundary, so the graft stays registered and git's traversal goes on treating the commit as parentless. Leg 1 then passed, diff-tree diffed the commit against its EMPTY grafted parent list and printed nothing, and the verdict came back:

{ provable: false, boundary: false, reason: "SHA has its parent(s) locally but its diff does not touch PATH" }

The refusal is correct (no caller is handed the sha) and both the boundary flag and the sentence beside it were wrong: an operator was told the file was simply untouched, when a grafted walk is what answered. Which of the two sentences came out depended on when the last fetch ran.

The discriminator, and why it is not the shallow file

Leg 1 now reads the graft where git applies it, per sha. git rev-parse SHA^@ lists the parents the TRAVERSAL uses — the --max-parents=0 reading asked of ONE commit — and an object naming a parent that list does not carry is grafted, whether or not that parent is in the store. New export isGraftBoundary(cwd, sha, parents?) compares the two lists; touchIsProvable fails leg 1 on it and returns boundary: true with a reason that names the outlived registration.

The card named two candidate discriminators (shallow-file membership, or rev-list --max-parents=0). Both were measured; SHA^@ is the same traversal fact, asked per sha:

reading cost (20 runs, this checkout) notes
git rev-parse SHA^@ 2.6 ms / call answers without walking — same cost on a complete clone
git rev-list --max-parents=0 SHA 15 ms / call walks the ancestry; costs more the more history is present
shallow-file membership needs the common dir resolved (it prints a RELATIVE .git from a repo root) and the sha normalised to 40 hex; misses a graft from any other source

A real root still passes leg 1 (its object names no parent, so the two lists agree) and an ordinary commit still answers — the case a --max-parents=0 count on its own cannot tell apart, which is why this compares two lists instead of counting one.

No behaviour change beyond the flag and the sentence

The refusal itself, the --deepen doubling to --unshallow, exit 2 and the empty stdout are untouched. A grafted commit's diff-tree prints nothing in every case, so no reading that used to be provable: true can become a refusal here.

Tests

  • node scripts/pm/git-history.mjs --self-test62 cases, exit 0 (was 54). Eight new cases in the touch battery, floor 17 to 25.
  • node scripts/pm/check-harness-current.mjs --self-test — the consumer, 14 cases, exit 0, unedited. It prints boundary where it used to print unprovable for this state, which is the point of the card.
  • Fixture, constructed not described: clone --depth=5, then a plain git fetch origin PARENT_SHA (no --deepen, so nothing rewrites the shallow list) with uploadpack.allowAnySHA1InWant on the upstream. The case READS THE STATE FIRST — shallow list still names the boundary, parent object present, walk still roots there, clone still shallow — because a case asserting on a state that did not form proves nothing.
  • Ablation (leg reverted to parent-presence only, on the committed tree, restored with git checkout HEAD -- under a trap, blob hash compared both ways): 3 cases red, then byte-identical restore and green again.
PRE : hash-object=1239050079c6d4507a65e8cf3af16f3f2cdfc5d7 == HEAD:scripts/pm/git-history.mjs
POST: removed-marker=0 removed-leg=0 injected=1 hash=188314847512759d24fa11310b86c5d3d4bdb815
ABLATED SELF-TEST EXIT=1 · git-history --self-test: 3 FAILED.
RESTORED: hash=1239050079c6d4507a65e8cf3af16f3f2cdfc5d7 (HEAD=1239050079c6d4507a65e8cf3af16f3f2cdfc5d7) git-diff-HEAD-empty=yes
RESTORED SELF-TEST EXIT=0 · all cases passed.

Acceptance notes

  • The first spelling of the CLI-refusal case tested stderr for graft boundary and stayed GREEN under ablation: the refusal boilerplate prints that phrase on every refusal, so the case was pinning a constant. The ablation is what found it; the case now pins the reason clause and the absence of the misleading sentence, and its own history is written into the case name.
  • One wording-only second commit: a neighbouring case title still called the predicate "the parent's presence". The assertion is unchanged.
  • skip-changeset: scripts/pm/** is not in any package's files[] — nothing publishes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt


Generated by Claude Code

…'s presence

`touchIsProvable` proved leg 1 by asking whether every parent the commit object
names is present locally. A later fetch — of another ref, or of a single sha —
brings that parent into the object store without moving the shallow boundary,
so the graft stays registered and git's traversal goes on treating the commit
as parentless. Leg 1 then passed, `diff-tree` diffed the commit against its
EMPTY grafted parent list and printed nothing, and the verdict came back
`{provable: false, boundary: false, reason: "<sha> has its parent(s) locally
but its diff does not touch <path>"}`: the refusal correct, the flag a false
negative, and the sentence telling an operator the file was untouched when a
grafted walk is what answered. Which sentence came out depended on when the
last fetch ran.

Leg 1 now reads the graft where git applies it, per sha: `git rev-parse
<sha>^@` lists the parents the TRAVERSAL uses — the `--max-parents=0` reading
asked of one commit — and an object naming a parent that list does not carry is
grafted whether or not the parent is in the store. Measured at 2.6 ms per call
against 15 ms for `rev-list --max-parents=0 <sha>` (20 runs each): the ancestry
walk costs more the more history it can see, `^@` costs the same everywhere. It
also needs no shallow-file path to resolve through a linked worktree's common
dir, and it catches a graft from any source.

A real root still passes (its object names no parent, so the two lists agree)
and an ordinary commit still answers — both pinned, beside a FIRING CONTROL on
the same grafted clone, so the new leg cannot become "refuse everything".

Eight self-test cases in the `touch` battery, floor 17 -> 25, on a fixture that
constructs the state rather than describing it: clone `--depth=5`, then a plain
fetch of the boundary's own parent sha with no `--deepen`, so nothing rewrites
the shallow list. The fixture's state is READ FIRST (shallow list still names
the boundary, parent object present, walk still roots there) — a case asserting
on a state that did not form proves nothing.

Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
Co-authored-by: Claude <noreply@anthropic.com>
…ate it now tests

The case asserts that a still-shallow clone answers once the named commit is
usable; its title still said the predicate was "the parent's presence", which
is the half this branch just replaced. Wording only — the assertion is byte-for
-byte the same.

Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
Co-authored-by: Claude <noreply@anthropic.com>
@os-elon-musk os-elon-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 16, 2026 — with Claude
…printed for

The refusal for the 「parent fetched, graft still registered」 state prints
`fetch --deepen=N`. Measured on the fixture: with fetching allowed the same
clone deepens once and answers the TRUE touch at exit 0, so the refusal is a
step rather than a dead end. Pinned last on that clone, because it deepens it.

`touch` battery 25 -> 26 cases, self-test 63.

Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants