diff --git a/src/git/diff.ts b/src/git/diff.ts index ee533a9..92fe761 100644 --- a/src/git/diff.ts +++ b/src/git/diff.ts @@ -170,6 +170,33 @@ export async function getStagedDiffWithSignal( } } +function isUnbornHead(): boolean { + let headRef: string; + try { + headRef = execFileSync(getGitExecutable(), ['symbolic-ref', '--quiet', 'HEAD'], { + encoding: 'utf-8', + stdio: 'pipe', + }).trim(); + } catch { + // A detached or malformed HEAD is not the normal empty-repository state. + return false; + } + + try { + const refs = execFileSync(getGitExecutable(), ['for-each-ref', '--format=%(refname)', headRef], { + encoding: 'utf-8', + stdio: 'pipe', + }) + .split(/\r?\n/) + .filter(Boolean); + + return headRef.length > 0 && !refs.includes(headRef); + } catch { + // If the ref database cannot be queried, keep the original fatal Git error. + return false; + } +} + export function hasCommits(): boolean { try { const count = execFileSync(getGitExecutable(), ['rev-list', '--count', 'HEAD'], { @@ -178,8 +205,14 @@ export function hasCommits(): boolean { }).trim(); return Number.parseInt(count, 10) > 0; - } catch { - return false; + } catch (err) { + if (isUnbornHead()) { + return false; + } + + const nodeErr = err as NodeJS.ErrnoException & { stderr?: string }; + const stderr = nodeErr.stderr?.trim(); + throw new Error(stderr || nodeErr.message || 'Failed to inspect git history'); } } diff --git a/tests/git-diff.test.mjs b/tests/git-diff.test.mjs index a678c2b..c4cbbd1 100644 --- a/tests/git-diff.test.mjs +++ b/tests/git-diff.test.mjs @@ -112,6 +112,23 @@ test("hasCommits returns true after the first commit", () => { } }); +test("hasCommits propagates a fatal HEAD error", () => { + const repoDir = initRepo(); + + try { + git(["commit", "--allow-empty", "-m", "initial commit"], repoDir); + const headRef = git(["symbolic-ref", "--quiet", "HEAD"], repoDir).trim(); + const headPath = join(repoDir, ".git", "refs", "heads", headRef.slice("refs/heads/".length)); + writeFileSync(headPath, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n", "utf-8"); + + withCwd(repoDir, () => { + assert.throws(() => hasCommits(), /unknown revision|bad object|ambiguous argument/i); + }); + } finally { + rmSync(repoDir, { recursive: true, force: true }); + } +}); + test("getStagedDiff returns diff when changes are staged", () => { const repoDir = initRepo();