From 236d6d9e0446df06e9dadd6ebdd512a32d6df045 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:15:49 +1000 Subject: [PATCH 1/8] fix: preserve fatal git errors in hasCommits --- src/git/diff.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/src/git/diff.ts b/src/git/diff.ts index ee533a9..02a17eb 100644 --- a/src/git/diff.ts +++ b/src/git/diff.ts @@ -1,5 +1,14 @@ import { execFile, execFileSync, spawnSync } from 'node:child_process'; -import { accessSync, copyFileSync, existsSync, constants, mkdtempSync, rmSync, statSync } from 'node:fs'; +import { + accessSync, + copyFileSync, + existsSync, + constants, + mkdtempSync, + readFileSync, + rmSync, + statSync, +} from 'node:fs'; import { tmpdir } from 'node:os'; import { delimiter, isAbsolute, join, normalize, resolve } from 'node:path'; import { promisify } from 'node:util'; @@ -170,6 +179,42 @@ export async function getStagedDiffWithSignal( } } +function hasStoredHeadRef(ref: string): boolean { + try { + const refPath = getGitPath(ref, process.cwd()); + if (existsSync(refPath)) { + return true; + } + + const packedRefsPath = getGitPath('packed-refs', process.cwd()); + if (!existsSync(packedRefsPath)) { + return false; + } + + return readFileSync(packedRefsPath, 'utf-8') + .split('\\n') + .some((line) => line.length > 41 && line.slice(41) === ref && /^[0-9a-f]{40} /.test(line)); + } catch { + // If the ref layout cannot be inspected, do not classify the failure as an empty repository. + return true; + } +} + +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; + } + + return headRef.length > 0 && !hasStoredHeadRef(headRef); +} + export function hasCommits(): boolean { try { const count = execFileSync(getGitExecutable(), ['rev-list', '--count', 'HEAD'], { @@ -178,8 +223,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', { cause: err }); } } From 3e362ef6049eabaf2ee6da4df41610ddeff34863 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:15:53 +1000 Subject: [PATCH 2/8] test: cover fatal HEAD errors in hasCommits --- tests/git-diff.test.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/git-diff.test.mjs b/tests/git-diff.test.mjs index a678c2b..0bf1c89 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", headRef.slice("refs/heads/".length)); + writeFileSync(headPath, "deadbeef\\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(); From 1f886b63f563fd10e49c774f48109282593e8f46 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:16:45 +1000 Subject: [PATCH 3/8] style: format hasCommits error handling --- src/git/diff.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/git/diff.ts b/src/git/diff.ts index 02a17eb..1dbf537 100644 --- a/src/git/diff.ts +++ b/src/git/diff.ts @@ -192,8 +192,10 @@ function hasStoredHeadRef(ref: string): boolean { } return readFileSync(packedRefsPath, 'utf-8') - .split('\\n') - .some((line) => line.length > 41 && line.slice(41) === ref && /^[0-9a-f]{40} /.test(line)); + .split('\n') + .some( + (line) => line.length > 41 && line.slice(41) === ref && /^[0-9a-f]{40} /.test(line), + ); } catch { // If the ref layout cannot be inspected, do not classify the failure as an empty repository. return true; From 386416ab7791ffa50a74cb33574f56aef6317849 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:17:41 +1000 Subject: [PATCH 4/8] style: wrap hasCommits error construction --- src/git/diff.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/git/diff.ts b/src/git/diff.ts index 1dbf537..5e7ad72 100644 --- a/src/git/diff.ts +++ b/src/git/diff.ts @@ -232,7 +232,9 @@ export function hasCommits(): boolean { const nodeErr = err as NodeJS.ErrnoException & { stderr?: string }; const stderr = nodeErr.stderr?.trim(); - throw new Error(stderr || nodeErr.message || 'Failed to inspect git history', { cause: err }); + throw new Error(stderr || nodeErr.message || 'Failed to inspect git history', { + cause: err, + }); } } From ee02508b3e2c2217c7667e2ef250009c1f0896a7 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:18:26 +1000 Subject: [PATCH 5/8] style: align diff helper with prettier --- src/git/diff.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/git/diff.ts b/src/git/diff.ts index 5e7ad72..c78d13d 100644 --- a/src/git/diff.ts +++ b/src/git/diff.ts @@ -1,14 +1,5 @@ import { execFile, execFileSync, spawnSync } from 'node:child_process'; -import { - accessSync, - copyFileSync, - existsSync, - constants, - mkdtempSync, - readFileSync, - rmSync, - statSync, -} from 'node:fs'; +import { accessSync, copyFileSync, existsSync, constants, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { delimiter, isAbsolute, join, normalize, resolve } from 'node:path'; import { promisify } from 'node:util'; @@ -193,9 +184,7 @@ function hasStoredHeadRef(ref: string): boolean { return readFileSync(packedRefsPath, 'utf-8') .split('\n') - .some( - (line) => line.length > 41 && line.slice(41) === ref && /^[0-9a-f]{40} /.test(line), - ); + .some((line) => line.length > 41 && line.slice(41) === ref && /^[0-9a-f]{40} /.test(line)); } catch { // If the ref layout cannot be inspected, do not classify the failure as an empty repository. return true; @@ -232,9 +221,7 @@ export function hasCommits(): boolean { const nodeErr = err as NodeJS.ErrnoException & { stderr?: string }; const stderr = nodeErr.stderr?.trim(); - throw new Error(stderr || nodeErr.message || 'Failed to inspect git history', { - cause: err, - }); + throw new Error(stderr || nodeErr.message || 'Failed to inspect git history'); } } From 37342578b9bf85b6d26b85b4c17d155fe0cdf887 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:24:12 +1000 Subject: [PATCH 6/8] fix: query git refs when detecting unborn HEAD --- src/git/diff.ts | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/git/diff.ts b/src/git/diff.ts index c78d13d..abd99d8 100644 --- a/src/git/diff.ts +++ b/src/git/diff.ts @@ -1,5 +1,5 @@ import { execFile, execFileSync, spawnSync } from 'node:child_process'; -import { accessSync, copyFileSync, existsSync, constants, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'; +import { accessSync, copyFileSync, existsSync, constants, mkdtempSync, rmSync, statSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { delimiter, isAbsolute, join, normalize, resolve } from 'node:path'; import { promisify } from 'node:util'; @@ -170,27 +170,6 @@ export async function getStagedDiffWithSignal( } } -function hasStoredHeadRef(ref: string): boolean { - try { - const refPath = getGitPath(ref, process.cwd()); - if (existsSync(refPath)) { - return true; - } - - const packedRefsPath = getGitPath('packed-refs', process.cwd()); - if (!existsSync(packedRefsPath)) { - return false; - } - - return readFileSync(packedRefsPath, 'utf-8') - .split('\n') - .some((line) => line.length > 41 && line.slice(41) === ref && /^[0-9a-f]{40} /.test(line)); - } catch { - // If the ref layout cannot be inspected, do not classify the failure as an empty repository. - return true; - } -} - function isUnbornHead(): boolean { let headRef: string; try { @@ -203,7 +182,20 @@ function isUnbornHead(): boolean { return false; } - return headRef.length > 0 && !hasStoredHeadRef(headRef); + try { + execFileSync(getGitExecutable(), ['show-ref', '--verify', '--quiet', headRef], { + encoding: 'utf-8', + stdio: 'pipe', + }); + return false; + } catch (err) { + const nodeErr = err as NodeJS.ErrnoException & { status?: number }; + if (nodeErr.status === 1) { + return headRef.length > 0; + } + + throw err; + } } export function hasCommits(): boolean { From b5981fe73e979bc1a4cb8b12c07dc73e57824cb8 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:24:16 +1000 Subject: [PATCH 7/8] test: corrupt the actual HEAD branch ref --- tests/git-diff.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/git-diff.test.mjs b/tests/git-diff.test.mjs index 0bf1c89..c4cbbd1 100644 --- a/tests/git-diff.test.mjs +++ b/tests/git-diff.test.mjs @@ -118,8 +118,8 @@ test("hasCommits propagates a fatal HEAD error", () => { try { git(["commit", "--allow-empty", "-m", "initial commit"], repoDir); const headRef = git(["symbolic-ref", "--quiet", "HEAD"], repoDir).trim(); - const headPath = join(repoDir, ".git", headRef.slice("refs/heads/".length)); - writeFileSync(headPath, "deadbeef\\n", "utf-8"); + 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); From 881298fa58588745cbc0baabecf3714c6638374b Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:24:52 +1000 Subject: [PATCH 8/8] fix(git): query refs through Git for unborn HEAD detection --- src/git/diff.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/git/diff.ts b/src/git/diff.ts index abd99d8..92fe761 100644 --- a/src/git/diff.ts +++ b/src/git/diff.ts @@ -183,18 +183,17 @@ function isUnbornHead(): boolean { } try { - execFileSync(getGitExecutable(), ['show-ref', '--verify', '--quiet', headRef], { + const refs = execFileSync(getGitExecutable(), ['for-each-ref', '--format=%(refname)', headRef], { encoding: 'utf-8', stdio: 'pipe', - }); - return false; - } catch (err) { - const nodeErr = err as NodeJS.ErrnoException & { status?: number }; - if (nodeErr.status === 1) { - return headRef.length > 0; - } + }) + .split(/\r?\n/) + .filter(Boolean); - throw err; + return headRef.length > 0 && !refs.includes(headRef); + } catch { + // If the ref database cannot be queried, keep the original fatal Git error. + return false; } }