fix(server): resolve remote-only branches in switcher checkout

Branch switcher listed remote-only branches as if they were local, then
threw "Branch not found" when clicked because the checkout path only
verified `refs/heads/<name>`. Explicit `origin/<name>` input went the
other way — verify passed, but checkout produced a detached HEAD.

Split the concerns into three layers:

- Enumerate: `listBranchSuggestions` returns provenance (`hasLocal`,
  `hasRemote`) so the wire payload no longer loses origin-only info.
  Fields are optional on `branchDetails` for backward compat.
- Resolve: new `resolveBranchCheckout` normalizes `origin/<name>` →
  `<name>`, verifies explicit `refs/heads/` and `refs/remotes/origin/`
  paths (no tag-DWIM), returns a tagged
  `{ local | remote-only | not-found }` result. Policy (local wins,
  origin fallback) lives here.
- Act: `checkoutExistingBranch` no longer does implicit DWIM. Local →
  `git checkout <name>`. Remote-only → `git checkout -b <name> --track
  origin/<name>`. Not-found → throws the existing
  `Branch not found: <name>` string. Response carries optional
  `source: "local" | "remote"`.

`validateBranch` now routes through the same resolver so both paths
agree on what a branch is.

Also fixes a related stale-base diff bug: worktrees created off
`origin/main` could show phantom "added" commits when the local `main`
lagged. `resolveBestComparisonBaseRef` is now origin-first for
diff/status. Merge still uses the "most-ahead" helper so unpublished
local commits aren't lost. `doesGitRefExist` tightened to only swallow
exit 1 (missing ref), propagating other git failures.
This commit is contained in:
Mohamed Boudra
2026-04-19 14:26:52 +07:00
parent e373c33108
commit 2d9cbb0453
7 changed files with 568 additions and 120 deletions
+274 -6
View File
@@ -14,6 +14,7 @@ import {
getCheckoutShortstat,
getPullRequestStatus,
getCheckoutStatus,
checkoutResolvedBranch,
listBranchSuggestions,
mergeToBase,
mergeFromBase,
@@ -22,6 +23,7 @@ import {
NotGitRepoError,
pullCurrentBranch,
pushCurrentBranch,
resolveBranchCheckout,
resolveRepositoryDefaultBranch,
parseWorktreeList,
isPaseoWorktreePath,
@@ -344,6 +346,96 @@ const x = 1;
expect(shortstat).toEqual({ additions: 1, deletions: 0 });
});
it("does not count origin base commits as feature changes when local main is stale", async () => {
const remoteDir = join(tempDir, "remote.git");
const otherClone = join(tempDir, "other-clone");
execSync(`git init --bare -b main ${remoteDir}`);
execSync(`git remote add origin ${remoteDir}`, { cwd: repoDir });
execSync("git push -u origin main", { cwd: repoDir });
execSync(`git clone ${remoteDir} ${otherClone}`);
execSync("git config user.email 'test@test.com'", { cwd: otherClone });
execSync("git config user.name 'Test'", { cwd: otherClone });
writeFileSync(join(otherClone, "already-on-origin.txt"), "origin\n");
execSync("git add already-on-origin.txt", { cwd: otherClone });
execSync("git -c commit.gpgsign=false commit -m 'origin base commit'", { cwd: otherClone });
execSync("git push", { cwd: otherClone });
writeFileSync(join(repoDir, "local-only-base.txt"), "local\n");
execSync("git add local-only-base.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'local base drift'", { cwd: repoDir });
execSync("git fetch origin", { cwd: repoDir });
execSync("git checkout -b feature origin/main", { cwd: repoDir });
const shortstat = await getCheckoutShortstat(repoDir);
expect(shortstat).toBeNull();
const diff = await getCheckoutDiff(repoDir, {
mode: "base",
baseRef: "main",
includeStructured: true,
});
expect(diff.diff).toBe("");
expect(diff.structured).toEqual([]);
});
it("falls back to the local base branch when origin is absent", async () => {
execSync("git checkout -b feature", { cwd: repoDir });
writeFileSync(join(repoDir, "local-feature.txt"), "feature\n");
execSync("git add local-feature.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'local feature'", { cwd: repoDir });
const shortstat = await getCheckoutShortstat(repoDir);
expect(shortstat).toEqual({ additions: 1, deletions: 0 });
const diff = await getCheckoutDiff(repoDir, { mode: "base", baseRef: "main" });
expect(diff.diff).toContain("local-feature.txt");
});
it("keeps an explicit origin base ref instead of stripping it to a stale local branch", async () => {
const remoteDir = join(tempDir, "remote.git");
const otherClone = join(tempDir, "other-clone");
execSync(`git init --bare -b main ${remoteDir}`);
execSync(`git remote add origin ${remoteDir}`, { cwd: repoDir });
execSync("git push -u origin main", { cwd: repoDir });
execSync(`git clone ${remoteDir} ${otherClone}`);
execSync("git config user.email 'test@test.com'", { cwd: otherClone });
execSync("git config user.name 'Test'", { cwd: otherClone });
writeFileSync(join(otherClone, "origin-base.txt"), "origin\n");
execSync("git add origin-base.txt", { cwd: otherClone });
execSync("git -c commit.gpgsign=false commit -m 'origin base'", { cwd: otherClone });
execSync("git push", { cwd: otherClone });
writeFileSync(join(repoDir, "local-drift.txt"), "local\n");
execSync("git add local-drift.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'local drift'", { cwd: repoDir });
execSync("git fetch origin", { cwd: repoDir });
execSync("git checkout -b feature origin/main", { cwd: repoDir });
const diff = await getCheckoutDiff(repoDir, { mode: "base", baseRef: "origin/main" });
expect(diff.diff).toBe("");
});
it("shows feature commits when the local and origin base branches are up to date", async () => {
const remoteDir = join(tempDir, "remote.git");
execSync(`git init --bare -b main ${remoteDir}`);
execSync(`git remote add origin ${remoteDir}`, { cwd: repoDir });
execSync("git push -u origin main", { cwd: repoDir });
execSync("git fetch origin", { cwd: repoDir });
execSync("git checkout -b feature", { cwd: repoDir });
writeFileSync(join(repoDir, "feature.txt"), "feature\n");
execSync("git add feature.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'feature commit'", { cwd: repoDir });
const shortstat = await getCheckoutShortstat(repoDir);
expect(shortstat).toEqual({ additions: 1, deletions: 0 });
const diff = await getCheckoutDiff(repoDir, { mode: "base", baseRef: "main" });
expect(diff.diff).toContain("feature.txt");
});
it("warms shortstat cache in the background without blocking listing callers", async () => {
expect(getCachedCheckoutShortstat(repoDir)).toBeUndefined();
@@ -790,31 +882,38 @@ const x = 1;
execSync(`git --git-dir ${remoteDir} show-ref --verify refs/heads/feature`);
});
it("lists merged local and remote branch suggestions without duplicates", async () => {
it("lists merged local and remote branch suggestions with provenance", async () => {
const remoteDir = join(tempDir, "remote.git");
execSync(`git init --bare -b main ${remoteDir}`);
execSync(`git remote add origin ${remoteDir}`, { cwd: repoDir });
execSync("git push -u origin main", { cwd: repoDir });
execSync("git checkout -b local-only", { cwd: repoDir });
execSync("git checkout -b feature/local-only", { cwd: repoDir });
execSync("git checkout main", { cwd: repoDir });
execSync("git checkout -b feature/shared", { cwd: repoDir });
writeFileSync(join(repoDir, "shared.txt"), "shared\n");
execSync("git add shared.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'shared branch'", { cwd: repoDir });
execSync("git push -u origin feature/shared", { cwd: repoDir });
execSync("git checkout main", { cwd: repoDir });
const otherClone = join(tempDir, "other-clone");
execSync(`git clone ${remoteDir} ${otherClone}`);
execSync("git config user.email 'test@test.com'", { cwd: otherClone });
execSync("git config user.name 'Test'", { cwd: otherClone });
execSync("git checkout -b remote-only", { cwd: otherClone });
execSync("git checkout -b feature/remote-only", { cwd: otherClone });
writeFileSync(join(otherClone, "remote-only.txt"), "remote-only\n");
execSync("git add remote-only.txt", { cwd: otherClone });
execSync("git -c commit.gpgsign=false commit -m 'remote only branch'", { cwd: otherClone });
execSync("git push -u origin remote-only", { cwd: otherClone });
execSync("git push -u origin feature/remote-only", { cwd: otherClone });
execSync("git fetch origin", { cwd: repoDir });
const branches = await listBranchSuggestions(repoDir, { limit: 50 });
const branchNames = branches.map((branch) => branch.name);
expect(branchNames).toContain("main");
expect(branchNames).toContain("local-only");
expect(branchNames).toContain("remote-only");
expect(branchNames).toContain("feature/local-only");
expect(branchNames).toContain("feature/remote-only");
expect(branchNames).toContain("feature/shared");
expect(branchNames.filter((name) => name === "main")).toHaveLength(1);
expect(branchNames).not.toContain("HEAD");
expect(branchNames.some((name) => name.startsWith("origin/"))).toBe(false);
@@ -826,6 +925,175 @@ const x = 1;
}),
]),
);
expect(branches.find((branch) => branch.name === "feature/local-only")).toMatchObject({
hasLocal: true,
hasRemote: false,
});
expect(branches.find((branch) => branch.name === "feature/remote-only")).toMatchObject({
hasLocal: false,
hasRemote: true,
});
expect(branches.find((branch) => branch.name === "feature/shared")).toMatchObject({
hasLocal: true,
hasRemote: true,
});
});
it("resolves branch checkout targets with local precedence and origin normalization", async () => {
const remoteDir = join(tempDir, "remote.git");
execSync(`git init --bare -b main ${remoteDir}`);
execSync(`git remote add origin ${remoteDir}`, { cwd: repoDir });
execSync("git push -u origin main", { cwd: repoDir });
execSync("git checkout -b feature/local", { cwd: repoDir });
execSync("git checkout main", { cwd: repoDir });
execSync("git checkout -b feature/shared", { cwd: repoDir });
writeFileSync(join(repoDir, "shared.txt"), "shared\n");
execSync("git add shared.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'shared branch'", { cwd: repoDir });
execSync("git push -u origin feature/shared", { cwd: repoDir });
execSync("git checkout main", { cwd: repoDir });
const otherClone = join(tempDir, "other-clone");
execSync(`git clone ${remoteDir} ${otherClone}`);
execSync("git config user.email 'test@test.com'", { cwd: otherClone });
execSync("git config user.name 'Test'", { cwd: otherClone });
execSync("git checkout -b feature/remote-only", { cwd: otherClone });
writeFileSync(join(otherClone, "remote-only.txt"), "remote-only\n");
execSync("git add remote-only.txt", { cwd: otherClone });
execSync("git -c commit.gpgsign=false commit -m 'remote only branch'", { cwd: otherClone });
execSync("git push -u origin feature/remote-only", { cwd: otherClone });
execSync("git fetch origin", { cwd: repoDir });
await expect(resolveBranchCheckout(repoDir, "feature/local")).resolves.toEqual({
kind: "local",
name: "feature/local",
});
await expect(resolveBranchCheckout(repoDir, "feature/remote-only")).resolves.toEqual({
kind: "remote-only",
name: "feature/remote-only",
remoteRef: "origin/feature/remote-only",
});
await expect(resolveBranchCheckout(repoDir, "origin/feature/remote-only")).resolves.toEqual({
kind: "remote-only",
name: "feature/remote-only",
remoteRef: "origin/feature/remote-only",
});
await expect(resolveBranchCheckout(repoDir, "feature/shared")).resolves.toEqual({
kind: "local",
name: "feature/shared",
});
await expect(resolveBranchCheckout(repoDir, "feature/unknown")).resolves.toEqual({
kind: "not-found",
});
});
it("does not resolve tags as branch checkout targets", async () => {
execSync("git checkout -b feature/a", { cwd: repoDir });
execSync("git checkout main", { cwd: repoDir });
execSync("git tag v1", { cwd: repoDir });
await expect(resolveBranchCheckout(repoDir, "v1")).resolves.toEqual({
kind: "not-found",
});
});
it("checks out a remote-only branch as a local tracking branch", async () => {
const remoteDir = join(tempDir, "remote.git");
execSync(`git init --bare -b main ${remoteDir}`);
execSync(`git remote add origin ${remoteDir}`, { cwd: repoDir });
execSync("git push -u origin main", { cwd: repoDir });
const otherClone = join(tempDir, "other-clone");
execSync(`git clone ${remoteDir} ${otherClone}`);
execSync("git config user.email 'test@test.com'", { cwd: otherClone });
execSync("git config user.name 'Test'", { cwd: otherClone });
execSync("git checkout -b feature/remote-only", { cwd: otherClone });
writeFileSync(join(otherClone, "remote-only.txt"), "remote-only\n");
execSync("git add remote-only.txt", { cwd: otherClone });
execSync("git -c commit.gpgsign=false commit -m 'remote only branch'", { cwd: otherClone });
execSync("git push -u origin feature/remote-only", { cwd: otherClone });
execSync("git fetch origin", { cwd: repoDir });
const resolution = await resolveBranchCheckout(repoDir, "feature/remote-only");
await expect(checkoutResolvedBranch({ cwd: repoDir, resolution })).resolves.toEqual({
source: "remote",
});
expect(execSync("git symbolic-ref --short HEAD", { cwd: repoDir }).toString().trim()).toBe(
"feature/remote-only",
);
execSync("git symbolic-ref -q HEAD", { cwd: repoDir });
expect(
execSync("git rev-parse --abbrev-ref --symbolic-full-name @{u}", { cwd: repoDir })
.toString()
.trim(),
).toBe("origin/feature/remote-only");
});
it("normalizes explicit origin input when checking out a remote-only branch", async () => {
const remoteDir = join(tempDir, "remote.git");
execSync(`git init --bare -b main ${remoteDir}`);
execSync(`git remote add origin ${remoteDir}`, { cwd: repoDir });
execSync("git push -u origin main", { cwd: repoDir });
const otherClone = join(tempDir, "other-clone");
execSync(`git clone ${remoteDir} ${otherClone}`);
execSync("git config user.email 'test@test.com'", { cwd: otherClone });
execSync("git config user.name 'Test'", { cwd: otherClone });
execSync("git checkout -b feature/remote-only", { cwd: otherClone });
writeFileSync(join(otherClone, "remote-only.txt"), "remote-only\n");
execSync("git add remote-only.txt", { cwd: otherClone });
execSync("git -c commit.gpgsign=false commit -m 'remote only branch'", { cwd: otherClone });
execSync("git push -u origin feature/remote-only", { cwd: otherClone });
execSync("git fetch origin", { cwd: repoDir });
const resolution = await resolveBranchCheckout(repoDir, "origin/feature/remote-only");
await expect(checkoutResolvedBranch({ cwd: repoDir, resolution })).resolves.toEqual({
source: "remote",
});
expect(execSync("git symbolic-ref --short HEAD", { cwd: repoDir }).toString().trim()).toBe(
"feature/remote-only",
);
execSync("git symbolic-ref -q HEAD", { cwd: repoDir });
expect(
execSync("git rev-parse --abbrev-ref --symbolic-full-name @{u}", { cwd: repoDir })
.toString()
.trim(),
).toBe("origin/feature/remote-only");
});
it("checks out the local branch when local and remote branches share a name", async () => {
const remoteDir = join(tempDir, "remote.git");
execSync(`git init --bare -b main ${remoteDir}`);
execSync(`git remote add origin ${remoteDir}`, { cwd: repoDir });
execSync("git push -u origin main", { cwd: repoDir });
execSync("git checkout -b feature/shared", { cwd: repoDir });
writeFileSync(join(repoDir, "shared.txt"), "shared\n");
execSync("git add shared.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'shared branch'", { cwd: repoDir });
execSync("git push -u origin feature/shared", { cwd: repoDir });
execSync("git checkout main", { cwd: repoDir });
const resolution = await resolveBranchCheckout(repoDir, "feature/shared");
await expect(checkoutResolvedBranch({ cwd: repoDir, resolution })).resolves.toEqual({
source: "local",
});
expect(execSync("git symbolic-ref --short HEAD", { cwd: repoDir }).toString().trim()).toBe(
"feature/shared",
);
});
it("throws the existing branch-not-found message for unknown checkout targets", async () => {
await expect(
checkoutResolvedBranch({
cwd: repoDir,
resolution: { kind: "not-found" },
requestedBranch: "missing-branch",
}),
).rejects.toThrow(/^Branch not found: missing-branch$/);
});
it("filters branch suggestions by query and enforces result limit", async () => {
+186 -44
View File
@@ -130,6 +130,8 @@ interface GitRef {
export interface BranchSuggestion {
name: string;
committerDate: number;
hasLocal: boolean;
hasRemote: boolean;
}
async function listGitRefs(cwd: string, refPrefix: string): Promise<GitRef[]> {
@@ -154,9 +156,15 @@ async function listGitRefs(cwd: string, refPrefix: string): Promise<GitRef[]> {
.filter((ref): ref is GitRef => ref !== null);
}
interface BranchSuggestionMeta {
committerDate: number;
hasLocal: boolean;
hasRemote: boolean;
}
function sortBranchSuggestions(
branchNames: string[],
branchMeta: Map<string, { isLocal: boolean; committerDate: number }>,
branchMeta: Map<string, BranchSuggestionMeta>,
query: string,
): string[] {
const normalizedQuery = query.trim().toLowerCase();
@@ -197,14 +205,15 @@ export async function listBranchSuggestions(
listGitRefs(cwd, "refs/remotes/origin"),
]);
const branchMeta = new Map<string, { isLocal: boolean; committerDate: number }>();
const branchMeta = new Map<string, BranchSuggestionMeta>();
for (const ref of localRefs) {
const normalized = normalizeBranchSuggestionName(ref.name);
if (!normalized) continue;
const existing = branchMeta.get(normalized);
branchMeta.set(normalized, {
isLocal: true,
hasLocal: true,
hasRemote: existing?.hasRemote ?? false,
committerDate: Math.max(ref.committerDate, existing?.committerDate ?? 0),
});
}
@@ -214,10 +223,15 @@ export async function listBranchSuggestions(
if (!normalized) continue;
const existing = branchMeta.get(normalized);
if (!existing) {
branchMeta.set(normalized, { isLocal: false, committerDate: ref.committerDate });
branchMeta.set(normalized, {
hasLocal: false,
hasRemote: true,
committerDate: ref.committerDate,
});
} else {
branchMeta.set(normalized, {
...existing,
hasRemote: true,
committerDate: Math.max(ref.committerDate, existing.committerDate),
});
}
@@ -231,10 +245,110 @@ export async function listBranchSuggestions(
}
const ordered = sortBranchSuggestions(filteredNames, branchMeta, query);
return ordered.slice(0, limit).map((name) => ({
name,
committerDate: branchMeta.get(name)?.committerDate ?? 0,
}));
return ordered.slice(0, limit).map((name) => {
const meta = branchMeta.get(name);
return {
name,
committerDate: meta?.committerDate ?? 0,
hasLocal: meta?.hasLocal ?? false,
hasRemote: meta?.hasRemote ?? false,
};
});
}
export interface LocalBranchCheckoutResolution {
kind: "local";
name: string;
}
export interface RemoteOnlyBranchCheckoutResolution {
kind: "remote-only";
name: string;
remoteRef: string;
}
export interface NotFoundBranchCheckoutResolution {
kind: "not-found";
}
export type BranchCheckoutResolution =
| LocalBranchCheckoutResolution
| RemoteOnlyBranchCheckoutResolution
| NotFoundBranchCheckoutResolution;
export async function resolveBranchCheckout(
cwd: string,
name: string,
): Promise<BranchCheckoutResolution> {
await requireGitRepo(cwd);
const normalized = normalizeBranchSuggestionName(name);
if (!normalized) {
return { kind: "not-found" };
}
const localRef = `refs/heads/${normalized}`;
const localResult = await runGitCommand(["rev-parse", "--verify", "--quiet", localRef], {
cwd,
env: READ_ONLY_GIT_ENV,
acceptExitCodes: [0, 1],
});
const hasLocal = localResult.exitCode === 0;
if (hasLocal) {
return { kind: "local", name: normalized };
}
const remoteRef = `origin/${normalized}`;
const remoteRefPath = `refs/remotes/${remoteRef}`;
const remoteResult = await runGitCommand(["rev-parse", "--verify", "--quiet", remoteRefPath], {
cwd,
env: READ_ONLY_GIT_ENV,
acceptExitCodes: [0, 1],
});
const hasRemote = remoteResult.exitCode === 0;
if (hasRemote) {
return { kind: "remote-only", name: normalized, remoteRef };
}
return { kind: "not-found" };
}
export type BranchCheckoutSource = "local" | "remote";
export interface CheckoutExistingBranchResult {
source: BranchCheckoutSource;
}
export interface CheckoutResolvedBranchInput {
cwd: string;
resolution: BranchCheckoutResolution;
requestedBranch?: string;
}
export async function checkoutResolvedBranch(
input: CheckoutResolvedBranchInput,
): Promise<CheckoutExistingBranchResult> {
const { cwd, resolution } = input;
switch (resolution.kind) {
case "local": {
const { stdout } = await runGitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd });
const current = stdout.trim();
if (current === resolution.name) {
return { source: "local" };
}
await runGitCommand(["checkout", resolution.name], { cwd });
return { source: "local" };
}
case "remote-only":
await runGitCommand(["checkout", "-b", resolution.name, "--track", resolution.remoteRef], {
cwd,
});
return { source: "remote" };
case "not-found":
throw new Error(`Branch not found: ${input.requestedBranch ?? "unknown"}`);
}
}
async function listCheckoutFileChanges(
@@ -847,25 +961,59 @@ async function resolveBaseRef(repoRoot: string): Promise<string | null> {
}
function normalizeLocalBranchRefName(input: string): string {
return input.startsWith("origin/") ? input.slice("origin/".length) : input;
if (input.startsWith("refs/remotes/origin/")) {
return input.slice("refs/remotes/origin/".length);
}
if (input.startsWith("refs/heads/")) {
return input.slice("refs/heads/".length);
}
if (input.startsWith("origin/")) {
return input.slice("origin/".length);
}
return input;
}
interface ComparisonBaseRefName {
localName: string;
originRef: string;
}
function normalizeComparisonBaseRefName(input: string): ComparisonBaseRefName {
const localName = normalizeLocalBranchRefName(input);
return { localName, originRef: `origin/${localName}` };
}
async function doesGitRefExist(cwd: string, fullRef: string): Promise<boolean> {
try {
await runGitCommand(["show-ref", "--verify", "--quiet", fullRef], {
cwd,
env: READ_ONLY_GIT_ENV,
});
return true;
} catch {
return false;
}
const result = await runGitCommand(["show-ref", "--verify", "--quiet", fullRef], {
cwd,
env: READ_ONLY_GIT_ENV,
acceptExitCodes: [0, 1],
});
return result.exitCode === 0;
}
async function resolveBestComparisonBaseRef(
cwd: string,
normalizedBaseRef: string,
): Promise<string> {
async function resolveBestComparisonBaseRef(cwd: string, baseRef: string): Promise<string> {
const normalized = normalizeComparisonBaseRefName(baseRef);
const [hasLocal, hasOrigin] = await Promise.all([
doesGitRefExist(cwd, `refs/heads/${normalized.localName}`),
doesGitRefExist(cwd, `refs/remotes/origin/${normalized.localName}`),
]);
if (hasOrigin) {
return normalized.originRef;
}
if (hasLocal) {
return normalized.localName;
}
const refName =
baseRef.startsWith("origin/") || baseRef.startsWith("refs/remotes/origin/")
? normalized.originRef
: normalized.localName;
throw new Error(`Base branch not found locally or on origin: ${refName}`);
}
async function resolveMostAheadBaseRef(cwd: string, normalizedBaseRef: string): Promise<string> {
const [hasLocal, hasOrigin] = await Promise.all([
doesGitRefExist(cwd, `refs/heads/${normalizedBaseRef}`),
doesGitRefExist(cwd, `refs/remotes/origin/${normalizedBaseRef}`),
@@ -881,20 +1029,18 @@ async function resolveBestComparisonBaseRef(
throw new Error(`Base branch not found locally or on origin: ${normalizedBaseRef}`);
}
// Both exist: choose the ref with more unique commits compared to the other.
try {
const { stdout } = await runGitCommand(
["rev-list", "--left-right", "--count", `${normalizedBaseRef}...origin/${normalizedBaseRef}`],
{ cwd, env: READ_ONLY_GIT_ENV },
);
const [localOnlyRaw, originOnlyRaw] = stdout.trim().split(/\s+/);
const localOnly = Number.parseInt(localOnlyRaw ?? "0", 10);
const originOnly = Number.parseInt(originOnlyRaw ?? "0", 10);
if (!Number.isNaN(localOnly) && !Number.isNaN(originOnly) && originOnly > localOnly) {
return `origin/${normalizedBaseRef}`;
}
} catch {
// ignore and fall back to local
const { stdout } = await runGitCommand(
["rev-list", "--left-right", "--count", `${normalizedBaseRef}...origin/${normalizedBaseRef}`],
{ cwd, env: READ_ONLY_GIT_ENV },
);
const [localOnlyRaw, originOnlyRaw] = stdout.trim().split(/\s+/);
const localOnly = Number.parseInt(localOnlyRaw ?? "0", 10);
const originOnly = Number.parseInt(originOnlyRaw ?? "0", 10);
if (Number.isNaN(localOnly) || Number.isNaN(originOnly)) {
return normalizedBaseRef;
}
if (originOnly > localOnly) {
return `origin/${normalizedBaseRef}`;
}
return normalizedBaseRef;
@@ -909,7 +1055,7 @@ async function getAheadBehind(
if (!normalizedBaseRef || !currentBranch || normalizedBaseRef === currentBranch) {
return null;
}
const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, baseRef);
const { stdout } = await runGitCommand(
["rev-list", "--left-right", "--count", `${comparisonBaseRef}...${currentBranch}`],
{ cwd, env: READ_ONLY_GIT_ENV },
@@ -1189,10 +1335,7 @@ async function getCheckoutShortstatUncached(
if (currentBranch && localBaseRef && currentBranch !== localBaseRef) {
// Feature branch: diff against the merge-base with the base branch
const comparisonBaseRef = await resolveBestComparisonBaseRef(
cwd,
normalizeLocalBranchRefName(localBaseRef),
);
const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, localBaseRef);
try {
const { stdout } = await runGitCommand(["merge-base", "HEAD", comparisonBaseRef], {
@@ -1329,8 +1472,7 @@ export async function getCheckoutDiff(
throw new Error(`Base ref mismatch: expected ${baseRef}, got ${compare.baseRef}`);
}
const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
const bestBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
const bestBaseRef = await resolveBestComparisonBaseRef(cwd, baseRef);
refForDiff = (await tryResolveMergeBase(cwd, bestBaseRef)) ?? bestBaseRef;
}
@@ -1715,7 +1857,7 @@ export async function mergeFromBase(
}
const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
const bestBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
const bestBaseRef = await resolveMostAheadBaseRef(cwd, normalizedBaseRef);
if (bestBaseRef === currentBranch) {
return;
}