Skip to content

Git output over ~8 KiB is intermittently truncated (pinned ktsu.RunCommand 1.5.0), so Patch()/Log()/Diff() silently return partial results or malformed hunks #127

Description

@matt-edmondson

What's wrong

Directory.Packages.props:10 pins ktsu.RunCommand to 1.5.0. RunCommandGitProcessRunner (GitIntegration/Execution/RunCommandGitProcessRunner.cs:65-148) appends whatever chunks RunCommand delivers into StandardOutput and trusts it to have read the pipe to EOF.

RunCommand 1.5.0 stops reading when the process exits and then does a single final read, so anything still in the pipe after that is dropped. This was fixed upstream in ktsu-dev/RunCommand#87 (commit 0993c0e, "read output to EOF"), first released in v1.8.0. The upstream comment in AsyncProcessStreamReader.cs explains: "stopping at exit plus a final read silently dropped everything past the first few kilobytes."

Failure scenario

Reproduced with the real git runner:

  • git diff a97019c~1 a97019c in this repo is 8,769 chars. Run 200 times through GitTextBuilder, 9 of the 200 runs returned exactly 8,192 chars, cut mid-line.

  • Running Patch().Between(sha~1, sha) over the last 12 commits of every ktsu-dev repo (1,801 file patches):

    • one GitParseException: Malformed hunk header: '@@ -1' (JsonRequiredConditionally 01d3e4e)
    • seven hunks whose parsed line count disagrees with their @@ header, e.g. this repo's a97019c GitIntegration/GitRepository.cs (old 1/6, new 1/10)

    Re-running the same commits gives correct results, so the parser is fine and the input was short.

Consequences for callers:

  • When the cut lands mid-hunk, GitPatchParser (Parsing/GitPatchParser.cs:253-303) ends the hunk at the truncation point without an error, because it follows line markers and never checks OldCount/NewCount. The result is a GitHunk shorter than its header, and any later files are silently dropped.
  • Apply(file.PatchFor(...)) on such a hunk then fails as a corrupt patch.
  • A truncated Log/Status/Diff just returns fewer entries.
  • The failure is nondeterministic: about 5% of outputs over 8 KiB in these runs.

Suggested fix / acceptance criteria

  • Bump ktsu.RunCommand to ≥ 1.8.0. 1.9.0 was tested.
    • This requires moving ktsu.Semantics.Paths / ktsu.Semantics.Strings from 3.0.1 to ≥ 5.8.0, otherwise restore fails with NU1109 downgrade errors.
    • In a scratch copy with RunCommand 1.9.0 and Semantics 5.8.0, the repro showed 0/200 truncations and the full suite passed (708/708).
    • The Semantics change is a major version jump, so re-run the KTSU0006 / VersionOverride consumer-resolution check described in CLAUDE.md.
  • Add an integration test that reads output well over 8 KiB (a large Patch() or Log()) many times and asserts it is byte-identical each time.
  • Optional hardening: have GitPatchParser check each hunk's line tally against its @@ counts and throw GitParseException on mismatch, so a short read fails loudly instead of producing a malformed hunk.

Activity

  1. matt-edmondson commented on Sep 27, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    • Category: Bug
    • Priority: High. Every command whose output passes ~8 KiB can come back short at random (about 5% of the time in the repro), with no error raised. Patch(), Log(), Status() and Diff() are all affected. The bug also reaches downstream consumers that go through GitIntegration, such as KtsuTools sync and ProjectDirector.
    • Area / suggested owner: Dependency pin in Directory.Packages.props (ktsu.RunCommand 1.5.0) and Execution/RunCommandGitProcessRunner.cs. The optional hardening belongs in Parsing/GitPatchParser.cs.
    • Duplicates / in progress: Not a duplicate. The root cause was already fixed upstream in Read each output stream to end of stream, not just until exit RunCommand#87 (v1.8.0), and this repo just hasn't taken the fix. No open PR. The bump is also blocked by the ktsu.Semantics.* 3.x → 5.8 jump (NU1109), so it isn't a routine Dependabot bump.
    • Next step: Bump RunCommand to ≥ 1.9.0 and Semantics to ≥ 5.8.0 together, then re-run the KTSU0006 / VersionOverride consumer check. Add the >8 KiB repeat-read integration test. Also add the hunk-count check in GitPatchParser so that any future short read fails loudly instead of returning a malformed hunk. Note that RunCommand#89/GitPullBuilder.ExecuteAsync can drop the real diagnostic for non-conflict failures reported on stdout #90/Add submodule listing to the local layer #92 are open against newer RunCommand versions, so check them before choosing the target version.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingreadyFully specified; implement as written

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions