From 29b6f2a86fa61b4f05ec72d568749b8a7202b245 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 7 Apr 2026 11:40:59 +0700 Subject: [PATCH] fix(server): prefer origin/{branch} over local branch for worktrees When creating a worktree, resolve the base branch by checking origin/{branch} first, falling back to the local branch only when the remote ref doesn't exist. This ensures worktrees start from the latest upstream state rather than a potentially stale local branch. --- packages/server/src/utils/worktree.test.ts | 65 ++++++++++++++++++++++ packages/server/src/utils/worktree.ts | 9 ++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/packages/server/src/utils/worktree.test.ts b/packages/server/src/utils/worktree.test.ts index de6df46b1..e8ebb57dc 100644 --- a/packages/server/src/utils/worktree.test.ts +++ b/packages/server/src/utils/worktree.test.ts @@ -149,6 +149,71 @@ describe("createWorktree", () => { expect(metadata).toMatchObject({ version: 1, baseRefName: "main" }); }); + it("prefers origin/{branch} over local {branch} when both exist", async () => { + const remoteDir = join(tempDir, "remote.git"); + const remoteCloneDir = join(tempDir, "remote-clone"); + execSync(`git init --bare ${remoteDir}`); + execSync(`git remote add origin ${remoteDir}`, { cwd: repoDir }); + execSync("git push -u origin main", { cwd: repoDir }); + + execSync(`git clone ${remoteDir} ${remoteCloneDir}`); + execSync("git config user.email 'test@test.com'", { cwd: remoteCloneDir }); + execSync("git config user.name 'Test'", { cwd: remoteCloneDir }); + writeFileSync(join(remoteCloneDir, "file.txt"), "from-origin\n"); + execSync("git add file.txt", { cwd: remoteCloneDir }); + execSync("git -c commit.gpgsign=false commit -m 'advance origin main'", { + cwd: remoteCloneDir, + }); + execSync("git push origin main", { cwd: remoteCloneDir }); + + writeFileSync(join(repoDir, "file.txt"), "from-local\n"); + execSync("git add file.txt", { cwd: repoDir }); + execSync("git -c commit.gpgsign=false commit -m 'advance local main'", { cwd: repoDir }); + + execSync("git fetch origin", { cwd: repoDir }); + + const result = await createWorktree({ + branchName: "prefer-origin-feature", + cwd: repoDir, + baseBranch: "main", + worktreeSlug: "prefer-origin-feature", + runSetup: false, + paseoHome, + }); + + expect(readFileSync(join(result.worktreePath, "file.txt"), "utf8")).toBe("from-origin\n"); + }); + + it("falls back to local {branch} when origin/{branch} does not exist", async () => { + writeFileSync(join(repoDir, "file.txt"), "from-local-only\n"); + execSync("git add file.txt", { cwd: repoDir }); + execSync("git -c commit.gpgsign=false commit -m 'advance local main only'", { cwd: repoDir }); + + const result = await createWorktree({ + branchName: "prefer-local-fallback-feature", + cwd: repoDir, + baseBranch: "main", + worktreeSlug: "prefer-local-fallback-feature", + runSetup: false, + paseoHome, + }); + + expect(readFileSync(join(result.worktreePath, "file.txt"), "utf8")).toBe("from-local-only\n"); + }); + + it("throws when neither origin/{branch} nor local {branch} exists", async () => { + await expect( + createWorktree({ + branchName: "missing-base-feature", + cwd: repoDir, + baseBranch: "does-not-exist", + worktreeSlug: "missing-base-feature", + runSetup: false, + paseoHome, + }), + ).rejects.toThrow("Base branch not found: does-not-exist"); + }); + it("fails with invalid branch name", async () => { await expect( createWorktree({ diff --git a/packages/server/src/utils/worktree.ts b/packages/server/src/utils/worktree.ts index 193901553..35ce3671a 100644 --- a/packages/server/src/utils/worktree.ts +++ b/packages/server/src/utils/worktree.ts @@ -924,15 +924,14 @@ export async function createWorktree({ throw new Error("Base branch cannot be HEAD when creating a Paseo worktree"); } - // Resolve the base branch - try local first, then remote + // Resolve the base branch - prefer origin/{branch}, then fall back to local let resolvedBaseBranch = normalizedBaseBranch; try { - await execAsync(`git rev-parse --verify ${normalizedBaseBranch}`, { cwd }); + await execAsync(`git rev-parse --verify origin/${normalizedBaseBranch}`, { cwd }); + resolvedBaseBranch = `origin/${normalizedBaseBranch}`; } catch { - // Local branch doesn't exist, try remote (origin/{branch}) try { - await execAsync(`git rev-parse --verify origin/${normalizedBaseBranch}`, { cwd }); - resolvedBaseBranch = `origin/${normalizedBaseBranch}`; + await execAsync(`git rev-parse --verify ${normalizedBaseBranch}`, { cwd }); } catch { throw new Error(`Base branch not found: ${normalizedBaseBranch}`); }