Skip to content

Commit ee05c9e

Browse files
committed
fix(review): detect remote default branch via ls-remote when origin/HEAD is unset
getDefaultBranch relied on `symbolic-ref refs/remotes/origin/HEAD` to prefer the upstream ref over a potentially stale local copy. But origin/HEAD is commonly unset — worktrees, manual remote additions, and certain git operations can leave it missing. When it was absent, the code skipped straight to local `main`/`master`, silently losing the upstream-preference behavior for any user without origin/HEAD. Adds a second-order fallback: `git ls-remote --symref origin HEAD` queries the remote directly for its default branch. Works for any branch name (develop, trunk, etc.) — no hardcoded guesses. Uses a 3-second timeout so offline users aren't blocked; on failure it falls through to the existing local main/master path. To support the timeout, `ReviewGitRuntime.runGit` gains an optional `timeoutMs` param. Bun's implementation kills the process via setTimeout + proc.kill(). Pi's uses spawnSync's native `timeout` option. Both treat timeout as a non-zero exit (graceful fallback). For provenance purposes, this commit was AI assisted.
1 parent d102c5f commit ee05c9e

3 files changed

Lines changed: 36 additions & 3 deletions

File tree

apps/pi-extension/server/serverReview.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,12 @@ export interface ReviewServerResult {
106106
export const reviewRuntime: ReviewGitRuntime = {
107107
async runGit(
108108
args: string[],
109-
options?: { cwd?: string },
109+
options?: { cwd?: string; timeoutMs?: number },
110110
): Promise<GitCommandResult> {
111111
const result = spawnSync("git", args, {
112112
cwd: options?.cwd,
113113
encoding: "utf-8",
114+
...(options?.timeoutMs ? { timeout: options.timeoutMs } : {}),
114115
});
115116
return {
116117
stdout: result.stdout ?? "",

packages/server/git.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,27 @@ export type {
3636

3737
async function runGit(
3838
args: string[],
39-
options?: { cwd?: string },
39+
options?: { cwd?: string; timeoutMs?: number },
4040
): Promise<GitCommandResult> {
4141
const proc = Bun.spawn(["git", ...args], {
4242
cwd: options?.cwd,
4343
stdout: "pipe",
4444
stderr: "pipe",
4545
});
4646

47+
let timer: ReturnType<typeof setTimeout> | undefined;
48+
if (options?.timeoutMs) {
49+
timer = setTimeout(() => proc.kill(), options.timeoutMs);
50+
}
51+
4752
const [stdout, stderr, exitCode] = await Promise.all([
4853
new Response(proc.stdout).text(),
4954
new Response(proc.stderr).text(),
5055
proc.exited,
5156
]);
5257

58+
if (timer) clearTimeout(timer);
59+
5360
return { stdout, stderr, exitCode };
5461
}
5562

packages/shared/review-core.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export interface GitCommandResult {
5959
export interface ReviewGitRuntime {
6060
runGit: (
6161
args: string[],
62-
options?: { cwd?: string },
62+
options?: { cwd?: string; timeoutMs?: number },
6363
) => Promise<GitCommandResult>;
6464
readTextFile: (path: string) => Promise<string | null>;
6565
}
@@ -102,6 +102,31 @@ export async function getDefaultBranch(
102102
}
103103
}
104104

105+
// origin/HEAD wasn't set or pointed at an unfetched ref. Query the remote
106+
// directly for its default branch — works for any branch name (develop,
107+
// trunk, etc.), not just main/master. Best-effort with a 3s timeout so
108+
// offline users aren't blocked. Parses the `ref: refs/heads/<name>` line
109+
// from `git ls-remote --symref origin HEAD`.
110+
try {
111+
const lsRemote = await runtime.runGit(
112+
["ls-remote", "--symref", "origin", "HEAD"],
113+
{ cwd, timeoutMs: 5000 },
114+
);
115+
if (lsRemote.exitCode === 0) {
116+
const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/(\S+)\s+HEAD/m);
117+
if (match) {
118+
const remoteBranch = `origin/${match[1]}`;
119+
const refExists = await runtime.runGit(
120+
["show-ref", "--verify", "--quiet", `refs/remotes/${remoteBranch}`],
121+
{ cwd },
122+
);
123+
if (refExists.exitCode === 0) return remoteBranch;
124+
}
125+
}
126+
} catch {
127+
// Network unavailable, auth failure, or timeout — fall through.
128+
}
129+
105130
const mainBranch = await runtime.runGit(
106131
["show-ref", "--verify", "refs/heads/main"],
107132
{ cwd },

0 commit comments

Comments
 (0)