fix(git): handle unborn branch in nativeBranchExists to prevent dispatch deadlock (#1815)

After show-ref fails, fall back to checking if the branch is the current
unborn branch via git branch --show-current. This resolves the asymmetry
where git branch --show-current succeeds on zero-commit repos but
show-ref --verify does not, preventing integration branch verification
from creating an unfixable deadlock.

Fixes #1771

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-21 14:37:37 -04:00 committed by GitHub
parent b741090870
commit dd9e66f089
2 changed files with 94 additions and 2 deletions

View file

@ -207,7 +207,9 @@ export function nativeDetectMainBranch(basePath: string): string {
/**
* Check if a local branch exists.
* Native: checks refs/heads/<name> via libgit2.
* Fallback: `git show-ref --verify`.
* Fallback: `git show-ref --verify`, with unborn-branch detection
* so that the current branch in a zero-commit repo is treated as
* existing (fixes #1771).
*/
export function nativeBranchExists(basePath: string, branch: string): boolean {
const native = loadNative();
@ -215,7 +217,12 @@ export function nativeBranchExists(basePath: string, branch: string): boolean {
return native.gitBranchExists(basePath, branch);
}
const result = gitExec(basePath, ["show-ref", "--verify", `refs/heads/${branch}`], true);
return result !== "";
if (result !== "") return true;
// show-ref fails for unborn branches (zero commits). Fall back to checking
// whether the requested branch is the current (unborn) branch.
const current = gitExec(basePath, ["branch", "--show-current"], true);
return current === branch;
}
/**

View file

@ -0,0 +1,85 @@
/**
* unborn-branch.test.ts Regression test for #1771.
*
* Verifies that nativeBranchExists returns true for the current branch
* in a repo with zero commits (unborn branch). Previously, show-ref
* would fail for unborn branches, causing a dispatch deadlock when
* the branch was recorded as integration branch but could never be
* verified.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, realpathSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { execFileSync } from "node:child_process";
import { nativeBranchExists } from "../native-git-bridge.ts";
function git(args: string[], cwd: string): string {
return execFileSync("git", args, {
cwd,
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf-8",
}).trim();
}
test("nativeBranchExists: returns true for unborn branch (zero commits)", () => {
const dir = realpathSync(mkdtempSync(join(tmpdir(), "unborn-branch-test-")));
try {
git(["init"], dir);
git(["config", "user.email", "test@test.com"], dir);
git(["config", "user.name", "Test"], dir);
// Repo has zero commits — HEAD exists but points to refs/heads/main
// which does not yet exist in the ref store.
const currentBranch = git(["branch", "--show-current"], dir);
assert.ok(currentBranch, "git branch --show-current should return a branch name");
// This is the bug: nativeBranchExists would return false because
// show-ref --verify fails on an unborn branch.
const exists = nativeBranchExists(dir, currentBranch);
assert.strictEqual(exists, true, "unborn current branch should be treated as existing");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("nativeBranchExists: returns false for non-existent branch in unborn repo", () => {
const dir = realpathSync(mkdtempSync(join(tmpdir(), "unborn-branch-test-")));
try {
git(["init"], dir);
git(["config", "user.email", "test@test.com"], dir);
git(["config", "user.name", "Test"], dir);
// A branch that is NOT the current unborn branch should still return false.
const exists = nativeBranchExists(dir, "nonexistent-branch");
assert.strictEqual(exists, false, "non-current branch should not exist in unborn repo");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("nativeBranchExists: still works for real branches with commits", () => {
const dir = realpathSync(mkdtempSync(join(tmpdir(), "unborn-branch-test-")));
try {
git(["init"], dir);
git(["config", "user.email", "test@test.com"], dir);
git(["config", "user.name", "Test"], dir);
writeFileSync(join(dir, "file.txt"), "test\n");
git(["add", "."], dir);
git(["commit", "-m", "init"], dir);
// After a commit, the branch exists in refs and should return true.
const currentBranch = git(["branch", "--show-current"], dir);
const exists = nativeBranchExists(dir, currentBranch);
assert.strictEqual(exists, true, "branch with commits should exist");
// Non-existent branch should still return false.
const noExists = nativeBranchExists(dir, "no-such-branch");
assert.strictEqual(noExists, false, "non-existent branch should not exist");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});