Skip to content

feat: graft recorded sync point so merges ignore squash-stale base#10

Merged
flipvh merged 1 commit into
mainfrom
feat/sync-base-graft
Jul 7, 2026
Merged

feat: graft recorded sync point so merges ignore squash-stale base#10
flipvh merged 1 commit into
mainfrom
feat/sync-base-graft

Conversation

@flipvh

@flipvh flipvh commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

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 actual git merge in 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 in get-attachments.ts reappeared as a duplicated const readFilter on 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-only git replace --graft that 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:

  • is deleted in a finally — it never leaks into pushed history (replace refs aren't pushed anyway, but this keeps the working repo clean);
  • no-ops when the recorded base is already a native ancestor (fresh merge-commit history, or --hard/--unpinned aggressive mode which intentionally uses the natural base);
  • never clobbers an existing replacement object (e.g. the scaffold-root graft from 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

  • 5 new e2e tests in tests/e2e/sync-base-graft.test.ts reconstruct 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_HEAD intact), cleans up on conflict, and respects both no-op guards.
  • Verified against the live raak repo: with the graft, git merge cella-upstream/main reports "Already up to date" (previously staged the 18-line duplicate); without it, the block returns.
  • pnpm check clean, 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

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]>
@flipvh flipvh merged commit 2196855 into main Jul 7, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant