feat: graft recorded sync point so merges ignore squash-stale base#10
Merged
Conversation
Squash syncs advance the recorded sync point (manifest + refs/cella/last-sync) without advancing git's commit graph. The 3-way merge, however, ran against git's own merge-base — stale after any squash — so every sync replayed upstream hunks the fork had already integrated or deliberately resolved away, surfacing as clean auto-merges (e.g. a duplicated block reappearing on each sync) or repeat conflicts. getEffectiveMergeBase already computed the correct base for analysis, but the merge itself did not use it. Add withTemporarySyncBaseGraft: it wraps the merge in a local-only 'git replace --graft' that adds the recorded sync point as a parent of the fork HEAD, so native git resolves the merge against the manifest-tracked base. The replace ref is deleted in a finally (never leaks into pushed history), no-ops when the base is already a native ancestor, and never clobbers an existing replacement. Wired into both the direct-mode merge and the analyze-preview worktree merge. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Squash syncs advance the recorded sync point (
cella.manifest.json+refs/cella/last-sync) but not git's commit graph — a squash commit has a single parent, so git's native merge-base stays at the last true merge ancestor.getEffectiveMergeBase()already accounts for this when computing counts and file classification, but the actualgit mergein phase 3 used git's own (stale) merge-base.The result: every sync replays all upstream changes since the last real merge ancestor. Hunks the fork already integrated — or deliberately resolved away in a previous sync — re-apply as "clean" auto-merges into non-overlapping lines, or re-conflict on files already resolved. Observed in
cellajs/raak: a resolved-away scope block inget-attachments.tsreappeared as a duplicatedconst readFilteron three consecutive syncs, and 8 already-resolved files re-conflicted.Concrete: raak's recorded sync point was 5 days / 48 commits ahead of git's native merge-base.
Fix
withTemporarySyncBaseGraft()wraps the merge in a local-onlygit replace --graftthat adds the recorded sync point as an extra parent of the fork's HEAD commit. Native git then resolves the 3-way merge against the manifest-tracked base instead of the squash-stale one. The replace ref:finally— it never leaks into pushed history (replace refs aren't pushed anyway, but this keeps the working repo clean);--hard/--unpinnedaggressive mode which intentionally uses the natural base);ensureSyncBase).Wired into both merge paths in
merge-engine.ts: the direct-mode phase-3 merge and the analyze-preview worktree merge (replace refs are shared across worktrees).Validation
tests/e2e/sync-base-graft.test.tsreconstruct the raak scenario (fork squash-integrates a sync, resolves an upstream block away). One test is a deliberate control documenting the old behavior (plain merge replays the block); the rest prove the graft prevents re-application, still lands genuinely new upstream work, leaves a real in-progress merge (MERGE_HEADintact), cleans up on conflict, and respects both no-op guards.git merge cella-upstream/mainreports "Already up to date" (previously staged the 18-line duplicate); without it, the block returns.pnpm checkclean, full suite 137 passing (132 existing + 5 new).Note
When HEAD is GPG-signed, git prints a "signature will be removed in the replacement commit" warning during the graft — harmless (the replacement is temporary and local), can be suppressed in a follow-up if noise is a concern.
🤖 Generated with Claude Code