From 1bc3f85d6a24699f049f86a84dba21e4b5dbbde4 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Fri, 6 Mar 2026 10:54:42 +0100 Subject: [PATCH] fix: Merge worktree conflict when target branch already checked out Use a temp branch + update-ref to avoid "already checked out" error when merging into the default branch. Also show actual branch name in the Merge & Push button instead of "Default". --- apps/server/git/simple-git-branch-manager.ts | 18 +++++++++++++++--- .../src/components/review/InitiativeReview.tsx | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/server/git/simple-git-branch-manager.ts b/apps/server/git/simple-git-branch-manager.ts index 0c73ce7..a7678e7 100644 --- a/apps/server/git/simple-git-branch-manager.ts +++ b/apps/server/git/simple-git-branch-manager.ts @@ -31,19 +31,27 @@ export class SimpleGitBranchManager implements BranchManager { } async mergeBranch(repoPath: string, sourceBranch: string, targetBranch: string): Promise { - // Use an ephemeral worktree for merge safety + // Use an ephemeral worktree with a temp branch for merge safety. + // We can't check out targetBranch directly — it may already be checked out + // in the clone's main working tree or an agent worktree. const tmpPath = mkdtempSync(join(tmpdir(), 'cw-merge-')); const repoGit = simpleGit(repoPath); + const tempBranch = `cw-merge-${Date.now()}`; try { - // Create ephemeral worktree on target branch - await repoGit.raw(['worktree', 'add', tmpPath, targetBranch]); + // Create worktree with a temp branch starting at targetBranch's commit + await repoGit.raw(['worktree', 'add', '-b', tempBranch, tmpPath, targetBranch]); const wtGit = simpleGit(tmpPath); try { await wtGit.merge([sourceBranch, '--no-edit']); + // Update the real target branch ref to the merge result. + // update-ref bypasses the "branch is checked out" guard. + const mergeCommit = (await wtGit.revparse(['HEAD'])).trim(); + await repoGit.raw(['update-ref', `refs/heads/${targetBranch}`, mergeCommit]); + log.info({ repoPath, sourceBranch, targetBranch }, 'merge completed cleanly'); return { success: true, message: `Merged ${sourceBranch} into ${targetBranch}` }; } catch (mergeErr) { @@ -73,6 +81,10 @@ export class SimpleGitBranchManager implements BranchManager { try { rmSync(tmpPath, { recursive: true, force: true }); } catch { /* ignore */ } try { await repoGit.raw(['worktree', 'prune']); } catch { /* ignore */ } } + // Delete the temp branch + try { + await repoGit.raw(['branch', '-D', tempBranch]); + } catch { /* ignore — may already be cleaned up */ } } } diff --git a/apps/web/src/components/review/InitiativeReview.tsx b/apps/web/src/components/review/InitiativeReview.tsx index fc0cd7a..a23a83a 100644 --- a/apps/web/src/components/review/InitiativeReview.tsx +++ b/apps/web/src/components/review/InitiativeReview.tsx @@ -214,7 +214,7 @@ export function InitiativeReview({ initiativeId, onCompleted }: InitiativeReview ) : ( )} - Merge & Push to Default + Merge & Push to {targetBranch || "default"}