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.
This commit is contained in:
@@ -149,6 +149,71 @@ describe("createWorktree", () => {
|
|||||||
expect(metadata).toMatchObject({ version: 1, baseRefName: "main" });
|
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 () => {
|
it("fails with invalid branch name", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
createWorktree({
|
createWorktree({
|
||||||
|
|||||||
@@ -924,15 +924,14 @@ export async function createWorktree({
|
|||||||
throw new Error("Base branch cannot be HEAD when creating a Paseo worktree");
|
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;
|
let resolvedBaseBranch = normalizedBaseBranch;
|
||||||
try {
|
try {
|
||||||
await execAsync(`git rev-parse --verify ${normalizedBaseBranch}`, { cwd });
|
await execAsync(`git rev-parse --verify origin/${normalizedBaseBranch}`, { cwd });
|
||||||
|
resolvedBaseBranch = `origin/${normalizedBaseBranch}`;
|
||||||
} catch {
|
} catch {
|
||||||
// Local branch doesn't exist, try remote (origin/{branch})
|
|
||||||
try {
|
try {
|
||||||
await execAsync(`git rev-parse --verify origin/${normalizedBaseBranch}`, { cwd });
|
await execAsync(`git rev-parse --verify ${normalizedBaseBranch}`, { cwd });
|
||||||
resolvedBaseBranch = `origin/${normalizedBaseBranch}`;
|
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(`Base branch not found: ${normalizedBaseBranch}`);
|
throw new Error(`Base branch not found: ${normalizedBaseBranch}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user