Files
fusion/.changeset/merger-ff-only-ref-advance.md
gsxdsm bf4428c00c fix(merger): require fast-forward ref advances and read integration tip from refs/heads/<branch>
Closes a "non-fast-forward ref overwrite" path where a subsequent merger
could orphan a previously-merged squash by advancing the integration
branch to a sibling commit.

Symptom (observed on fusion/fn-5419): main reflog shows
  385b6e93 -> f6358ce4 (FN-5551 squash) -> 63ec7098 (FN-5552 squash)
with f6358ce4 and 63ec7098 both parented at 385b6e93. The FN-5551 squash
was correctly committed to main, then the FN-5552 merger built its own
squash off the stale 385b6e93 base and the CAS update-ref blindly moved
main sideways, orphaning f6358ce4 onto whichever feature branch had
already branched from it.

Two coupled fixes uphold the missing invariant — local <integrationBranch>
only advances via fast-forward, and the merger never builds a squash off
a stale base sha:

1. advanceIntegrationBranchRef: add a `merge-base --is-ancestor` check
   before update-ref. Non-FF attempts now return
   reason: "non-fast-forward-advance" instead of overwriting the ref.
   The existing concurrent-advance CAS guard is retained.

2. runMerge: resolve the integration-branch tip via
   `git rev-parse --verify refs/heads/<integrationBranch>` instead of
   `git rev-parse HEAD` in rootDir. In reuse-task-worktree mode rootDir's
   HEAD can lag behind the shared ref after a sibling merger advanced it
   via update-ref without re-checking-out.

Adds regression coverage in merger-ref-update-advance.test.ts: a
sibling-commit advance with a matching expectedCurrentSha is now refused
with the new reason, and multi-commit fast-forwards still succeed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:49:34 -07:00

1.3 KiB

@fusion/engine
@fusion/engine
patch

fix(merger): require fast-forward ref advances and read integration tip from refs/heads/<branch>

Closes a class of "orphaned merge" bug where a subsequent merger could overwrite the integration branch tip with a sibling commit, leaving the previous squash reachable only from a feature branch.

Two coupled fixes:

  1. advanceIntegrationBranchRef now refuses non-fast-forward advances. The CAS check still guards against concurrent ref movement, but the new merge-base --is-ancestor check additionally requires the new sha to descend from the expected current sha. Non-FF attempts return reason: "non-fast-forward-advance" instead of silently orphaning the prior tip.

  2. runMerge resolves the integration-branch tip via git rev-parse --verify refs/heads/<integrationBranch> instead of git rev-parse HEAD in rootDir. In reuse-task-worktree mode, rootDir's HEAD can lag behind the shared ref after a sibling merger advanced it via update-ref without re-checking-out — using HEAD there caused the eventual squash commit to parent off an earlier sha and orphan the previously-merged tip.

Together these uphold the invariant: local <integrationBranch> only advances via fast-forward, and the merger never builds a squash off a stale base sha.