Skip to content

fix(git): suppress stderr leak in getCurrentCommit and getGitRoot (#1172)#1341

Merged
magyargergo merged 2 commits into
abhigyanpatwari:mainfrom
azizur100389:fix/suppress-git-stderr
May 5, 2026
Merged

fix(git): suppress stderr leak in getCurrentCommit and getGitRoot (#1172)#1341
magyargergo merged 2 commits into
abhigyanpatwari:mainfrom
azizur100389:fix/suppress-git-stderr

Conversation

@azizur100389

Copy link
Copy Markdown
Contributor

Summary

  • Root cause: Node's execSync forwards child stderr to the parent process when the stdio option is not explicitly set (documented Node.js behaviour). getCurrentCommit and getGitRoot caught the resulting error but never suppressed the stderr output, so git rev-parse failures printed fatal: not a git repository to the user's terminal.
  • Fix: Add stdio: ['ignore', 'pipe', 'ignore'] to both functions, matching the pattern already used by getRemoteUrl, getRemoteOriginUrl, and getCanonicalRepoRoot in the same file.
  • Regression test: New test that git init-s a directory without commits (forces git rev-parse HEAD to fail), then asserts process.stderr.write is never called with a fatal message.

Closes #1172

Test plan

  • npx vitest run test/unit/git-utils.test.ts — 15/18 pass (3 pre-existing failures unrelated to this change)
  • New does not leak git stderr to process.stderr (#1172) test passes
  • tsc --noEmit — no new type errors
  • prettier --check — clean

…higyanpatwari#1172)

Node's execSync forwards the child's stderr to the parent process when
the stdio option is not explicitly set. getCurrentCommit and getGitRoot
both caught the resulting error but did not suppress the stderr output,
causing "fatal: not a git repository" messages to leak to the terminal
whenever they were called on a path outside a git worktree.

Add stdio: ['ignore', 'pipe', 'ignore'] to both functions, matching the
pattern already used by getRemoteUrl, getRemoteOriginUrl, and
getCanonicalRepoRoot in the same file.
@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

@azizur100389 is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
8022 8021 0 1 371s

✅ All 8021 tests passed

1 test(s) skipped — expand for details
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 77.62% 24457/31508 77.02% 📈 +0.6 🟢 ███████████████░░░░░
Branches 66.21% 15519/23437 65.97% 📈 +0.2 🟢 █████████████░░░░░░░
Functions 82.86% 2414/2913 81.86% 📈 +1.0 🟢 ████████████████░░░░
Lines 80.65% 22078/27375 79.89% 📈 +0.8 🟢 ████████████████░░░░

📋 View full run · Generated by CI

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 3m 51s —— View job


PR #1341 Review — fix(git): suppress stderr leak in getCurrentCommit and getGitRoot

Tasks

  • Post initial comment
  • Read context files (DoD.md, AGENTS.md, GUARDRAILS.md, CONTRIBUTING.md, TESTING.md, ARCHITECTURE.md)
  • Inspect changed files (git.ts, git-utils.test.ts)
  • Inspect caller files (group.ts, status.ts, analyze.ts, run-analyze.ts, repo-manager.ts, git-staleness.ts)
  • Search for relevant patterns (execSync, stdio, stderr, getCurrentCommit, getGitRoot)
  • Run hidden Unicode checks
  • Check CI status and git log
  • Compile and post comprehensive review

Review bar for this PR

  • getCurrentCommit() and getGitRoot() no longer leak git stderr on expected failure paths.
  • Return semantics preserved: getCurrentCommit()'', getGitRoot()null on failure.
  • Successful commands still parse stdout correctly; Windows path normalization in getGitRoot() unchanged.
  • stdio pattern is now consistent with getRemoteUrl, getRemoteOriginUrl, getCanonicalRepoRoot.
  • Regression test proves no fatal stderr is written when getCurrentCommit() is called on a commit-less git repo.
  • No hidden bidi Unicode controls in executable code; scope limited to two files only.
  • CI/typecheck green on the head SHA.

Current PR state

Attribute Value
Head SHA c21d318
Status Open, not draft
Commits 1
Changed files 2 (gitnexus/src/storage/git.ts, gitnexus/test/unit/git-utils.test.ts)
CI ✅ All checks passed — typecheck, tests (3 platforms), E2E
Test results 8020/8020 passed, 1 skipped (documented limitation unrelated to this PR)
Typecheck tsc --noEmit clean
Issue #1172 Linked in PR body and in test comment at git-utils.test.ts:103
Package-lock / deps Unchanged
Hidden Unicode Em dashes (, U+2014) present in PR-added comments only (lines 20, 101 of git.ts). Not bidi controls; not executable code. Pre-existing box-drawing decorators in test file unchanged.
Vercel Irrelevant to this storage/git helper fix
Local vs CI test discrepancy PR body reports 15/18 locally; CI shows 8020/8020 pass — local 3 failures are pre-existing environment issues, not caused by this PR. CI is authoritative.

Understanding of the change

Node's execSync inherits stdio from the parent process when no stdio option is passed. This means that when git rev-parse HEAD or git rev-parse --show-toplevel fails (e.g., called from a non-git directory), Git writes fatal: not a git repository directly to the parent process's stderr file descriptor — not through Node's error handling chain. The try/catch block catches the ENOENT/non-zero exit but cannot retroactively suppress what was already written to the terminal.

stdio: ['ignore', 'pipe', 'ignore'] fixes this by:

  • 'ignore' on stdin/stderr: closes those fds at the child level before git starts, so no bytes flow to the parent's terminal even on failure.
  • 'pipe' on stdout: routes stdout into a Buffer that execSync returns, so successful commands still parse the sha/path correctly.

This mirrors the pattern already established by getRemoteUrl, getRemoteOriginUrl, and getCanonicalRepoRoot in the same file. It is a CLI/UX fix only — it does not change what git command is run, what cwd is used, or what the function returns.


Findings

[MINOR] getGitRoot() quiet-failure not regression-tested for stderr

  • Category: Test coverage gap
  • Files: gitnexus/test/unit/git-utils.test.ts
  • Issue: The PR changes both getCurrentCommit() and getGitRoot(), but the new regression test (lines 104–119) only spies on process.stderr.write for getCurrentCommit(). The existing getGitRoot test (lines 125–133) asserts null return value but has no stderr spy. This means if getGitRoot's stdio suppression were accidentally reverted, no test would fail loudly for the stderr regression.
  • Why it matters: Half the fix is unprotected by a behavioural assertion. DoD §2.7 requires tests that would fail if the changed behaviour were broken.
  • Recommended fix: Add a process.stderr.write spy to the existing getGitRoot plain-temp-dir test, assert getGitRoot(tmpDir) === null, and assert no fatal appears in captured output. A 4-line addition to the existing test block is sufficient — no new describe needed. Fix this →
  • Blocks merge: No — the implementation is demonstrably correct by inspection; this is a coverage gap only.

[LOW] Em dashes in PR-added comments (non-ASCII, non-bidi, comment-only)

  • Category: File hygiene / Unicode
  • Files: gitnexus/src/storage/git.ts:20, gitnexus/src/storage/git.ts:101
  • Issue: The new comments added by this PR contain (U+2014 EM DASH), which is non-ASCII and likely what triggers GitHub's Unicode warning banner on the diff. These are not bidi controls (bidi range is U+202A–U+202E, U+2066–U+2069). They appear only in comments, not in string literals or executable code. The pre-existing box-drawing decorators in git-utils.test.ts are unchanged.
  • Why it matters: Non-ASCII in source comments is a minor repo hygiene concern. It does not affect runtime behaviour. Replacing with -- or : in comments would silence the GitHub warning.
  • Recommended fix: Normalize to ASCII punctuation in comments (e.g. // Suppress stderr: without an explicit stdio option ...). Optional but keeps the file uniformly ASCII.
  • Blocks merge: No.

Git helper behaviour assessment

getCurrentCommit() (git.ts:16–32):

  • Command: git rev-parse HEADunchanged
  • cwd: repoPathunchanged
  • stdio: ['ignore', 'pipe', 'ignore']correctly added
  • .toString().trim()unchanged
  • catch returns ''unchanged
  • No new logging, no command interpolation ✅

getGitRoot() (git.ts:97–111):

  • Command: git rev-parse --show-toplevelunchanged
  • cwd: fromPathunchanged
  • stdio: ['ignore', 'pipe', 'ignore']correctly added
  • .toString().trim()unchanged
  • path.resolve(raw) for Windows /d/Projects/FooD:\Projects\Foo normalization — unchanged
  • catch returns nullunchanged

Other helpers (isGitRepo, getRemoteUrl, getRemoteOriginUrl, getCanonicalRepoRoot): unmodified. isGitRepo correctly uses stdio: 'ignore' (no stdout needed). All stdout-capturing helpers now consistently use ['ignore', 'pipe', 'ignore']. The file is now fully consistent.


Caller impact assessment

Caller Usage Impact
status.ts:21 getGitRoot(cwd) ?? cwd No longer leaks stderr when cwd is outside a git repo
status.ts:33 getCurrentCommit(repo.repoPath) Quiet on failure; '' fallback unchanged
analyze.ts:225 getGitRoot(process.cwd()) Quiet on failure; null check at line 226 triggers user-friendly console.log message (not affected)
index-repo.ts:50 getGitRoot(process.cwd()) Same; null handled cleanly
wiki.ts:101 getGitRoot(process.cwd()) Same
git-staleness.ts:118 getCurrentCommit(cwdGitRoot) Quiet on failure; `
run-analyze.ts:168 getCurrentCommit(repoPath) behind repoHasGit guard Unchanged path; guard still controls whether it's called

No caller relied on git stderr being visible. All callers use null/'' fallbacks. No legitimate GitNexus-owned diagnostics are suppressed — those go through console.error or console.log, not through child-process stderr. Group status command no longer prints fatal: not a git repository when probing repos from a non-git parent directory. ✅


Test assessment

New test (does not leak git stderr to process.stderr (#1172), lines 104–119):

  • Uses real git (consistent with suite) ✅
  • Isolated mkdtemp with finally cleanup ✅
  • git init -q without commits reliably fails git rev-parse HEAD — this is the exact failure class reported in group status prints "fatal: not a git repository" for valid repos when run from non-git parent directory #1172
  • process.stderr.write spy restored in finally
  • Asserts getCurrentCommit(tmpDir) === ''
  • Asserts no 'fatal' in captured stderr ✅
  • Would fail on the pre-fix code (stderr was inherited, not suppressed) ✅
  • Cross-platform stable: no hardcoded paths, no timing dependencies, no network ✅
  • No git state leaked: rmSync in finally

Gap: No equivalent stderr regression test for getGitRoot(). The existing test (returns null for a plain temp directory, lines 125–133) only asserts the return value. This is the main follow-up item.

Pre-existing worktree test (lines 246–288): calls getGitRoot(worktreeDir) in the success path — validates correct behaviour but not quiet-failure. Unrelated to this gap.


Hidden Unicode / hygiene assessment

File Finding Severity
git.ts:20 (U+2014 EM DASH) in new comment Low — comment only, not bidi
git.ts:101 (U+2014 EM DASH) in new comment Low — comment only, not bidi
git-utils.test.ts (U+2500 BOX DRAWINGS) in section headers Pre-existing, not introduced by this PR

No bidirectional control characters (U+202A–U+202E, U+2066–U+2069) found in either file. No invisible zero-width characters. No non-ASCII in string literals or stdio arrays. The GitHub Unicode warning is triggered by the em dashes in comments — this is cosmetic, not a security or correctness concern.

Scope hygiene: exactly 2 files changed, no package-lock/dependency/generated-file churn, no unrelated formatting, no AGENTS.md/CLAUDE.md edits. ✅


Elegance assessment

This is the correct size of fix. Two stdio option additions and one forwarding comment bring the two previously-inconsistent helpers into line with the rest of the file. The comment on getCurrentCommit (5 lines) explains the Node.js execSync stdio inheritance behaviour clearly — appropriate detail for a non-obvious platform behaviour. The getGitRoot comment (// Suppress stderr — see getCurrentCommit comment and #1172.) avoids duplication by cross-referencing rather than repeating.

A shared quietExec helper is not warranted for two call sites at this point. Adding one would be premature for a targeted fix. If more helpers are added later that need the same pattern, a helper could be introduced then.


Final verdict

production-ready with minor follow-ups

The core fix is correct, consistent, and minimal. Both getCurrentCommit() and getGitRoot() now suppress child-process stderr without changing return semantics, stdout capture, or Windows path normalization. The stdio pattern is now uniform across all stdout-capturing helpers in the file. CI is fully green, typecheck is clean, and the fix lands exactly on the affected functions with no scope creep.

The only gap is that getGitRoot()'s stderr suppression, while demonstrably correct by code inspection, lacks a matching regression test to guard against future reversion. This is a coverage miss on half the changed surface — worth fixing as a small follow-up to the same PR before merge, but not a correctness or production-safety blocker. The em dashes in new comments are harmless cosmetic noise that could be normalized to silence the GitHub Unicode warning banner.

- Add matching process.stderr.write spy test for getGitRoot (abhigyanpatwari#1172)
- Replace U+2014 em dashes with ASCII -- in new comments
@magyargergo magyargergo merged commit 4048f53 into abhigyanpatwari:main May 5, 2026
24 of 25 checks passed
@magyargergo magyargergo mentioned this pull request May 10, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

group status prints "fatal: not a git repository" for valid repos when run from non-git parent directory

2 participants