fix(FN-3842): treat already-merged branch as finalize success
This commit is contained in:
@@ -143,6 +143,7 @@ import {
|
||||
summarizeVerificationOutput,
|
||||
inferDefaultTestCommand,
|
||||
resolveTaskDiffBaseRef,
|
||||
commitOrAmendMergeWithFixes,
|
||||
MergeAbortedError,
|
||||
type ConflictCategory,
|
||||
} from "../merger.js";
|
||||
@@ -7627,3 +7628,45 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
||||
expect(fixAttempts[4][1]).toContain("attempt 3/3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("commitOrAmendMergeWithFixes", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("returns already-merged success when branch tip is ancestor of integration target and finalize has no staged content", async () => {
|
||||
mockedExecSync.mockImplementation((cmd: any) => {
|
||||
const cmdStr = String(cmd);
|
||||
if (cmdStr.includes("git diff --cached --name-only")) return "" as any;
|
||||
if (cmdStr === "git diff --name-only") return "" as any;
|
||||
if (cmdStr.includes("git status -z --porcelain")) return "" as any;
|
||||
if (cmdStr === "git rev-parse HEAD") return "abc123" as any;
|
||||
if (cmdStr === "git rev-parse fusion/fn-9999") return "def456" as any;
|
||||
if (cmdStr === "git merge-base def456 abc123") return "abc123" as any;
|
||||
if (cmdStr === "git diff --stat abc123..fusion/fn-9999") return "" as any;
|
||||
if (cmdStr === "git ls-files --others --exclude-standard") return "" as any;
|
||||
if (cmdStr.includes("git log -1 --pretty=%B HEAD")) return "commit message without trailer" as any;
|
||||
if (cmdStr === "git merge-base --is-ancestor def456 abc123") return "" as any;
|
||||
return "" as any;
|
||||
});
|
||||
|
||||
const result = await commitOrAmendMergeWithFixes(
|
||||
"/tmp/root",
|
||||
"FN-9999",
|
||||
"fusion/fn-9999",
|
||||
"",
|
||||
true,
|
||||
"abc123",
|
||||
"",
|
||||
undefined,
|
||||
DEFAULT_SETTINGS,
|
||||
undefined,
|
||||
null,
|
||||
null,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
expect(result).toEqual({ ok: true, reason: "branch-already-merged" });
|
||||
expect(mockedExecSync.mock.calls.some((call) => String(call[0]) === "git merge-base --is-ancestor def456 abc123")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2733,6 +2733,9 @@ export async function commitOrAmendMergeWithFixes(
|
||||
` diffStat(preAttemptHeadSha..branch)\n${diffStatSummary || " <empty>"}`;
|
||||
mergerLog.warn(diagnostics);
|
||||
|
||||
// FN-3842 ordering: trailer short-circuit first (this task already on
|
||||
// HEAD), then ancestor short-circuit (branch already reachable from
|
||||
// integration target via a different commit path), then squash-restore.
|
||||
if (trailerOnHead) {
|
||||
mergerLog.log(
|
||||
`${taskId}: HEAD already carries Fusion-Task-Id trailer — treating in-merge fix finalize as no-op success`,
|
||||
@@ -2740,6 +2743,23 @@ export async function commitOrAmendMergeWithFixes(
|
||||
return { ok: true, reason: "head-task-trailer" };
|
||||
}
|
||||
|
||||
let branchAlreadyOnIntegrationTarget = false;
|
||||
try {
|
||||
await execAsync(`git merge-base --is-ancestor ${branchTip} ${preAttemptHeadSha}`, {
|
||||
cwd: rootDir,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
branchAlreadyOnIntegrationTarget = true;
|
||||
} catch {
|
||||
branchAlreadyOnIntegrationTarget = false;
|
||||
}
|
||||
if (branchAlreadyOnIntegrationTarget) {
|
||||
mergerLog.log(
|
||||
`${taskId}: branch tip ${branchTip} is already ancestor of integration target ${preAttemptHeadSha} — treating finalize as already-merged success`,
|
||||
);
|
||||
return { ok: true, reason: "branch-already-merged" };
|
||||
}
|
||||
|
||||
// No commit and no staged content can still be recoverable when the
|
||||
// in-merge fix path cleared the previous squash index state. Rebuild the
|
||||
// squash from branch -> preAttemptHeadSha and continue normally.
|
||||
|
||||
Reference in New Issue
Block a user