Stop refusing an amend as a duplicate of the commit it replaces - #668
Conversation
Work in progress: the decision logic is measured and its regressions pass, but the marker is not being found at read time. Pushed unfinished because it lives in a temporary worktree and losing it would cost the whole investigation, not just the code. `prepare-commit-msg` is the only hook git tells whether this is an amend — it gets `commit` as the source and HEAD as the sha, where `commit-msg` gets nothing that differs. It records that; `commit-msg` reads it, consumes it, and only then does `duplicate-id` ignore HEAD. Three conditions, all required, and anything unrecognised is not an amend: `rebase -i` reword produces arguments identical to an amend and is separated only by a rebase being in progress. The two mistakes are not symmetric — taking a non-amend for an amend drops HEAD from the duplicate check and lets a real identity collision through, while the reverse is only the inconvenience #638 describes. So the default is the safe direction rather than a list of exclusions, which would break the day git adds an operation. test/amend-marker.test.ts pins what git actually passes for six operations, because the reword case is the kind of fact that stops being true without anyone noticing. Blast: module Undo: easy Certainty: tentative Ruled-out: option A, not counting HEAD at all | it reverses what #430 built — its two assertions fail directly, and the loss is not narrow because HEAD is the position every commit passes through Ruled-out: a heuristic on whether the new message extends the old | it would decide from a guess whether a record directs an agent, and no real case for it exists Limit: the amend path does not pass yet; `--git-path` returns a relative path, so the hook and validate may resolve the marker against different working directories — that is the next measurement, not a guess to code around Provenance: authored Record-Id: r-amendmarker638 Verified: four cases in test/amend-marker.test.ts covering ordinary commit, amend, rebase reword, cherry-pick, revert and merge; the three conditions measured true against a real amend; tsc --noEmit clean
`git commit --amend -m` and `--amend -F` supply the message on the command line, so git reports the message's origin rather than the commit's: the arguments reaching prepare-commit-msg are byte-identical to an ordinary commit. No marker can be written for them and the amend stays refused. That is a limit of the signal git offers, not of this implementation, and it is worth a test rather than a sentence: measured against a `-m` fixture the working code reads as broken, which is exactly what happened here and cost hours of debugging the wrong side. Verified on the path git does mark: `--amend --no-edit` now passes where it was refused as a duplicate. Limit: git reports the message's origin, not the commit's Blast: local Undo: easy Certainty: firm Provenance: authored Record-Id: r-638bnd
8698976 to
2607bae
Compare
The negative control, and what this PR does not proveA negative control was written for this work: an ordinary commit reusing the The fixture was asserted valid before the result was read — the seed commit So the control passes on What that costs this PR is worth stating plainly: the safe direction — that an RebaseRebased onto
The amend that produced this push was itself performed through the built CLI |
CommitLore — record lintTrailers: clean — 2 commits in Trailer violations fail this check. Active constraints are informational — they are what the repository already decided, not a verdict on this PR. |
|
Correction: the comment above saying #667 blocks demonstrating this end to end is wrong. #667 was not a defect — the reproduction used byte-identical trailer payloads, which SPEC §3.2 treats as one record re-declared. A same-id collision with a different payload is refused ( |
Closes #638.
git commit --amendwas refused withduplicate-id, because the commit itreplaces is still HEAD when the record is checked and carries the same
Record-Id.How it decides
commit-msgcannot tell an amend from an ordinary commit — git hands itnothing that differs.
prepare-commit-msgcan, so it writes a marker andcommit-msgconsumes it:Anything unrecognised is not an amend. The two mistakes are not symmetric:
calling a non-amend an amend drops HEAD from the duplicate check and lets a
real identity collision through, while the reverse is only the inconvenience
this issue describes. A list of operations to exclude would break the day
git adds one, so the default has to be the safe direction.
Only
duplicate-idignores that commit.Follows:andSupersedes:stillresolve against it — removing it from the prior stream outright turned every
reference to a record declared there into a dangling one, which two existing
tests caught.
What it does not reach
--amend -mand--amend -Fsupply the message on the command line, so gitreports the message's origin rather than the commit's and the arguments are
byte-identical to an ordinary commit. Those stay refused. This is pinned as a
test rather than left as a comment, because measured against a
-mfixturethe working code reads as broken.
Regressions
test/amend-marker.test.tspins what git passes for each operation — ordinarycommit, amend, rebase reword, cherry-pick, revert, merge, and the
-m/-Fboundary above. Reword is the case that matters: its arguments are identical
to an amend's, and only the rebase directory separates them.
Verified end to end:
--amend --no-editnow reportsreferences okand exits0 where it previously reported
duplicate-id.Not caused by this branch
A negative control written for this work — an ordinary commit reusing HEAD's
Record-Id— is accepted here and on main. That is #667, filed separately.