diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index 256ad11a3..92cb389c8 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -1566,14 +1566,17 @@ export function mergeMilestoneToMain( // Non-fatal — proceed with merge; untracked files may block it } - // 7c. Clean stale MERGE_HEAD before the squash merge (#2912). - // The native (libgit2) merge path or a prior interrupted merge may leave - // MERGE_HEAD in the git dir. `git merge --squash` refuses to run when - // MERGE_HEAD exists, so remove it preemptively. + // 7b. Clean up stale merge state before attempting squash merge (#2912). + // A leftover MERGE_HEAD (from a previous failed merge, libgit2 native path, + // or interrupted operation) causes `git merge --squash` to refuse with + // "fatal: You have not concluded your merge (MERGE_HEAD exists)". + // Defensively remove merge artifacts before starting. try { - const gitDirPre = resolveGitDir(originalBasePath_); - const mergeHeadPre = join(gitDirPre, "MERGE_HEAD"); - if (existsSync(mergeHeadPre)) unlinkSync(mergeHeadPre); + const gitDir_ = resolveGitDir(originalBasePath_); + for (const f of ["SQUASH_MSG", "MERGE_MSG", "MERGE_HEAD"]) { + const p = join(gitDir_, f); + if (existsSync(p)) unlinkSync(p); + } } catch { /* best-effort */ } // 8. Squash merge — auto-resolve .gsd/ state file conflicts (#530) diff --git a/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts b/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts index 826d65501..48f5897d9 100644 --- a/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +++ b/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts @@ -739,6 +739,39 @@ describe("auto-worktree-milestone-merge", { timeout: 300_000 }, () => { ); }); + test("#2912: stale SQUASH_MSG and MERGE_MSG are cleaned before squash merge", () => { + // Verifies that the pre-merge cleanup (step 7b) removes all three merge + // artifacts — not just MERGE_HEAD — so that `git merge --squash` never + // encounters leftover state from a prior interrupted operation. + const repo = freshRepo(); + const wtPath = createAutoWorktree(repo, "M294"); + + addSliceToMilestone(repo, wtPath, "M294", "S01", "Feature C", [ + { file: "feature-c.ts", content: "export const c = true;\n", message: "add feature c" }, + ]); + + const roadmap = makeRoadmap("M294", "Stale merge artifacts", [ + { id: "S01", title: "Feature C" }, + ]); + + // Plant stale merge artifacts in the git dir to simulate a prior + // interrupted merge. The pre-merge cleanup must remove all of them. + const gitDir = join(repo, ".git"); + writeFileSync(join(gitDir, "SQUASH_MSG"), "stale squash message\n"); + writeFileSync(join(gitDir, "MERGE_MSG"), "stale merge message\n"); + + mergeMilestoneToMain(repo, "M294", roadmap); + + assert.ok( + !existsSync(join(gitDir, "SQUASH_MSG")), + "#2912: stale SQUASH_MSG must be removed by pre-merge cleanup", + ); + assert.ok( + !existsSync(join(gitDir, "MERGE_MSG")), + "#2912: stale MERGE_MSG must be removed by pre-merge cleanup", + ); + }); + test("#1906: codeFilesChanged=true when real code is merged", () => { const repo = freshRepo(); const wtPath = createAutoWorktree(repo, "M190");