From 00438b2bb4142432b177b8096c95c54482aaa282 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Mon, 16 Mar 2026 20:24:24 -0500 Subject: [PATCH] fix: skip redundant checkout in worktree merge when main already current (#757) When mergeMilestoneToMain runs from a worktree context, main is already checked out at the project root. The unconditional git checkout main fails with "already used by worktree" because git refuses to checkout a branch that is active in another worktree. Skip the checkout when the integration branch is already current at the project root, which is always the case in worktree-mode merges. --- src/resources/extensions/gsd/auto-worktree.ts | 8 +++-- .../auto-worktree-milestone-merge.test.ts | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index 672b6bb93..f411e50e1 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -429,8 +429,12 @@ export function mergeMilestoneToMain( const integrationBranch = readIntegrationBranch(originalBasePath_, milestoneId); const mainBranch = integrationBranch ?? prefs.main_branch ?? "main"; - // 5. Checkout integration branch - nativeCheckoutBranch(originalBasePath_, mainBranch); + // 5. Checkout integration branch (skip if already current — avoids git error + // when main is already checked out in the project-root worktree, #757) + const currentBranchAtBase = nativeGetCurrentBranch(originalBasePath_); + if (currentBranchAtBase !== mainBranch) { + nativeCheckoutBranch(originalBasePath_, mainBranch); + } // 6. Build rich commit message const milestoneTitle = roadmap.title.replace(/^M\d+:\s*/, "").trim() || milestoneId; diff --git a/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts b/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts index df78b49d8..806f56097 100644 --- a/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +++ b/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts @@ -290,6 +290,40 @@ async function main(): Promise { assertTrue(existsSync(join(repo, "feature.ts")), "feature.ts merged to main"); } + // ─── Test 6: Skip checkout when main already current (#757) ─────── + console.log("\n=== skip checkout when main already current (#757) ==="); + { + const repo = freshRepo(); + const wtPath = createAutoWorktree(repo, "M060"); + + addSliceToMilestone(repo, wtPath, "M060", "S01", "Skip checkout test", [ + { file: "skip-checkout.ts", content: "export const skip = true;\n", message: "add skip-checkout" }, + ]); + + const roadmap = makeRoadmap("M060", "Skip checkout verification", [ + { id: "S01", title: "Skip checkout test" }, + ]); + + // Verify main is already checked out at repo root (worktree default) + const branchAtRoot = run("git rev-parse --abbrev-ref HEAD", repo); + assertEq(branchAtRoot, "main", "main is already checked out at project root"); + + // mergeMilestoneToMain should succeed without attempting to checkout main + // (which would fail with "already used by worktree" error) + let threw = false; + try { + const result = mergeMilestoneToMain(repo, "M060", roadmap); + assertTrue(result.commitMessage.includes("feat(M060)"), "merge commit created"); + } catch (err) { + threw = true; + console.error("Unexpected error:", err); + } + assertTrue(!threw, "does not fail when main is already checked out at project root"); + + // Verify the merge actually happened + assertTrue(existsSync(join(repo, "skip-checkout.ts")), "skip-checkout.ts merged to main"); + } + } finally { process.chdir(savedCwd); for (const d of tempDirs) {