Conversation
Two changes, one because the tool was awkward to use and one because it had a hole. `--apply` is gone; merging is what it does. `--dry-run` prints a real unified diff (`git diff --no-index`) instead of the custom before/after format the old default emitted, so the output is a diff you can read, grade, pipe to `patch`, or hand to another tool. Nothing here needs a bespoke rendering of a diff. Almost every call is an agent finishing a pull; making the common path the default and the preview the flag matches that. The hole: the old tool was driven entirely by conflicts, and there is a way to lose an upstream change with no conflict at all. When upstream DELETES a file we had moved out of the prefix, both sides deleted that path -- git raises nothing, `git status` says nothing, the pull succeeds, and we go on carrying a file upstream removed. Nothing conflict-driven can see it, because there is no conflict to see. So the tool now also audits the merge itself: it reads upstream's own diff against the previous split point and reports deletions whose file we still have. `-M` is load-bearing there -- without it every upstream rename reads as a delete and each one is a false positive. That audit has to run when nothing conflicted, which is precisely when the old tool did not run at all. So the mode is detected rather than flagged: MERGE_HEAD means a conflicted pull in progress, a merge commit at HEAD means one that just finished. Either way, run it; hence the name, `finish-subtree-pull.sh`, and hence the section heading in AGENTS.md being an instruction rather than a condition. Exit is non-zero if anything was left for a human OR anything was flagged by the audit. Verified on synthetic subtree fixtures, both modes: - upstream modifies a file we moved out -> merged to its new home; conflicted result gets diff3 markers in the file and stages 1/2/3 at the new path, so `git status` shows UU and `git add` resolves it - upstream deletes a file we moved out -> flagged STILL HERE, exit 1, in both the conflicted-pull and completed-pull modes - upstream RENAMES a file we moved out -> mapped back through MERGE_HEAD and merged cleanly, and NOT flagged as a deletion (the -M case) - upstream deletes a file we never moved -> git deletes it, nothing flagged, exit 0 - --dry-run leaves the working tree and index untouched Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGAGAib517Ae1kg8SCdcEt
…uses finish-subtree-pull.sh handles files we moved OUT of a prefix, which git's rename detection cannot follow. That is the rare case. The common one is the conflict this repository creates by existing: every service's imports were rewritten to github.com/fil-forge/forge/<svc>, so a pull conflicts on every file where upstream touched that import block -- fourteen of them in the resync of eight prefixes, each the same non-decision. resolve-rewrite-conflicts.sh takes upstream's file and re-applies the rewrite, but only where that is provably safe, and THE CHECK IS THE POINT. It verifies per file that rewrite(base) is exactly ours -- meaning our side carries nothing upstream could disagree with -- and refuses with a diff when it is not. gofmt normalises both sides for .go files, which is load-bearing rather than tidy: the rewrite makes a path longer, which can move it within its import group, and gofmt sorts groups. Without it, a file whose only difference IS the rewrite compares unequal byte for byte and gets refused as a hand merge. Verified against the real thing rather than a fixture. Replaying the sprue pull of the resync branch, from origin/main, with up-sprue at 506f5f6: - 3 resolved, 8 left, matching the hand resolution exactly - all three resolved files are BYTE-IDENTICAL to what was committed after resolving them by hand, which is the strongest available evidence that those hand resolutions were right - go.mod and go.sum refused, with the sibling pins and the libforge difference printed as the reason - the six modify/delete workflow conflicts reported as not its business And proved to fail on the defect: appending one unrelated comment line to our side of ucan_conclude_http_put.go makes it refuse that file and print the added line, while still taking its sibling in the same run.
|
[From Claude:] Added a second script,
The check is the point, not the fixTaking upstream's file and re-applying the rewrite is safe only if our side carries nothing upstream could disagree with. So it verifies per file that
Verified against the real thing, not a fixtureReplaying #14's
That byte-identical result is the strongest evidence available that #14's 14 mechanical resolutions were right: the tool and the hand agree exactly, having been done independently. And proved to fail on the defectAppending one unrelated comment line to our side of It refuses that file, names exactly what the extra content is, and still takes its sibling in the same run. Neither tool sees the third classWorth stating plainly, since two tools can read like full coverage. A hunk can merge cleanly and still carry a polyrepo import path, because it never touched a line we had rewritten — true of a file upstream added and of an existing file that merely gained an import. Nothing conflicts, so nothing reports it. Generated by Claude Code |
…r all The guppy digest re-pin was the right diagnosis -- e2e passed on 9b3a0b0. And a reversal worth recording rather than quietly making: the resync commit said the indexing-service poller fix would arrive with a later pull and that not porting it ahead of upstream was deliberate. Then it went red on #14 with the identical signature. Waiting on my own open PR to merge is still waiting, so it is ported -- byte for byte, no import lines touched, no-ops on the next pull. The upstream PR stays open; that is still where the fix belongs.
[From Claude:]
Follow-up to #10. Two changes: one because the tool was awkward to call, one because it had a hole.
--applyis gone; merging is what it does--dry-runnow prints a real unified diff (git diff --no-index) rather than the custom before/after format the old default emitted. There is no reason to have our own rendering of a diff — the output is now something you can read, grade, pipe topatch, or hand to another tool.Almost every call is an agent finishing a pull, so the common path is the default and the preview is the flag.
The hole: a lost upstream change that raises no conflict
The old tool was driven entirely by conflicts. There is a way to lose an upstream change with no conflict at all:
Nothing conflict-driven can see that, because there is no conflict to see. So the tool now also audits the merge itself: it reads upstream's own diff against the previous split point and reports deletions whose file we still have.
-Mis load-bearing there — without it every upstream rename reads as a delete, and each one is a false positive.Why the name changed
That audit has to run when nothing conflicted — which is precisely when the old tool did not run at all. So the mode is detected rather than flagged:
MERGE_HEADexistsHEADis a merge commitHence
finish-subtree-pull.sh, and hence theAGENTS.mdheading now being an instruction ("Run this with everygit subtree pull") rather than a condition ("When agit subtree pullconflicts…"). Either way, run it.Exit is non-zero if anything was left for a human or anything was flagged by the audit.
Verification
Synthetic subtree fixtures, both modes. Each case built from a real
git subtree add+git subtree pull, not simulated.merged svc/pkg/a.go -> shared/a.go (1 conflict(s); markers in the file, UU in git status);git ls-files -ushows stages 1/2/3 atshared/a.go,git addresolves itSTILL HERE upstream deleted pkg/doomed.go; we moved it to shared/doomed.go and still have it— in both modes, including the completed-pull audit where nothing else would have mentioned itMERGE_HEADtosvc/pkg/renamed.go,merged … (clean, staged),0 upstream deletion(s), exit 0 — the-McaseD svc/pkg/keep.gostaged by git, 0 flagged, exit 0--dry-rungit status --porcelainstillDU svc/pkg/a.goafter the runPreviously measured on a real
spruepull (#10): 6 skips, 0 false positives.What it still refuses to guess
Unchanged from #10, and worth restating: it applies only where a rename was recorded and the destination verified to exist in the index. It refuses — reports and skips, never guesses — a same-basename match, a true delete, a destination with uncommitted changes, and a binary file (where
git merge-fileproduces plausible garbage rather than failing).Generated by Claude Code