Skip to content

Stream large -z git output instead of buffering it against the cap#2045

Draft
tyrielv wants to merge 1 commit into
microsoft:masterfrom
tyrielv:tyrielv/fix-invokegit-stderr-oom
Draft

Stream large -z git output instead of buffering it against the cap#2045
tyrielv wants to merge 1 commit into
microsoft:masterfrom
tyrielv:tyrielv/fix-invokegit-stderr-oom

Conversation

@tyrielv

@tyrielv tyrielv commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #2048. That PR bounded the previously-unbounded git stdout/stderr capture and, for the two commands whose full result is correctness-critical, added a large stdout cap plus fail-safe valves if the cap was ever exceeded. This PR removes that truncation exposure entirely by streaming those commands' output instead of buffering it.

  • DiffCachedNameStatus (diff --cached --name-status -z) feeds every staged path into ModifiedPaths.
  • StatusPorcelain (status --porcelain -z) drives the sparse dirty check.

Both use -z (NUL-delimited), so line-based streaming can't chunk them — git delivers the whole blob as a single "line."

Change

Add a NUL-delimited streaming mode to InvokeGitImpl: a new parseStdOutToken callback reads stdout synchronously and splits on \0, invoking the callback once per record as it arrives. stderr stays async (BeginErrorReadLine), so the synchronous stdout read is deadlock-safe ("one sync, one async"). Only one record is held in memory, so an arbitrarily large result streams with no buffering, truncation, or OOM. It keeps git's robust -z format (no path-unquoting) and is guarded to timeoutMs == -1 and mutual exclusion with parseStdOutLine.

DiffCachedNameStatus and StatusPorcelain now stream tokens; their callers drive small status/path state machines. This removes the interim fail-safe valves from #2048 (there's no longer a partial result to guard against) and the dead -z string parsers (GetPathsNotCoveredBySparseFolders, GetNextGitPath). Result.OutputTruncated/ErrorsTruncated remain for the still-buffered commands (stderr is still bounded), but the two converted commands can no longer trip OutputTruncated.

Tests

  • ReadStdOutTokens: NUL splitting, empty input, embedded empty records, a trailing record without a NUL, and a record spanning the 8KB read buffer.
  • DiffCachedNameStatus/StatusPorcelain stream records through the mock.
  • Replaces the removed GetNextGitPath test; PathCoveredBySparseFolders tests unchanged.

Full unit suite: 889 passed, 0 failed (11 pre-existing native-hook skips).

@tyrielv tyrielv force-pushed the tyrielv/fix-invokegit-stderr-oom branch 2 times, most recently from c0cca88 to e4198f0 Compare July 9, 2026 21:37
@tyrielv tyrielv changed the title Fix GVFS.Mount OOM from unbounded git output + auto-recover corrupt packfile Stream large -z git output instead of buffering it against the cap Jul 9, 2026
@tyrielv tyrielv force-pushed the tyrielv/fix-invokegit-stderr-oom branch from e4198f0 to 4b8cc4a Compare July 9, 2026 21:46
Building on the git-output bounding change, this removes the truncation exposure
for the two commands whose full result is correctness-critical by streaming
their output instead of buffering it.

DiffCachedNameStatus (diff --cached --name-status -z) feeds every staged path
into ModifiedPaths; StatusPorcelain (status --porcelain -z) drives the sparse
dirty check. Both use -z (NUL-delimited), so line-based streaming cannot chunk
them - git delivers the whole blob as a single line. The prior change bounded
these at a large stdout cap and made the callers fail safe if the cap was ever
exceeded; this change makes truncation impossible instead.

Add a NUL-delimited streaming mode to InvokeGitImpl: a new parseStdOutToken
callback reads stdout synchronously and splits on NUL, invoking the callback
once per record as it arrives. stderr stays async (BeginErrorReadLine), so the
synchronous stdout read cannot deadlock. Only one record is held in memory, so
an arbitrarily large result streams without buffering, truncation, or OOM. The
mode keeps git's robust -z format (no path-unquoting logic) and is guarded to an
infinite timeout (a synchronous read cannot honor a finite one) and is mutually
exclusive with line streaming.

DiffCachedNameStatus and StatusPorcelain now stream tokens; their callers drive
small status/path state machines. This removes the interim fail-safe valves the
callers had (there is no longer a partial result to guard against) and the dead
-z string parsers (GetPathsNotCoveredBySparseFolders, GetNextGitPath).

Result.OutputTruncated/ErrorsTruncated remain for the still-buffered commands
(stderr is still bounded), but the two converted commands can no longer trip
OutputTruncated.

Tests:
  - ReadStdOutTokens: NUL splitting, empty input, embedded empty records, a
    trailing record without a NUL, and a record spanning the 8KB read buffer.
  - DiffCachedNameStatus/StatusPorcelain stream records through the mock.
  - Replaces the removed GetNextGitPath test; PathCoveredBySparseFolders tests
    are unchanged.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
@tyrielv tyrielv force-pushed the tyrielv/fix-invokegit-stderr-oom branch from 4b8cc4a to 1a2dc13 Compare July 10, 2026 17:32
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.

1 participant