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.
What's wrong
Directory.Packages.props:10pinsktsu.RunCommandto 1.5.0.RunCommandGitProcessRunner(GitIntegration/Execution/RunCommandGitProcessRunner.cs:65-148) appends whatever chunks RunCommand delivers intoStandardOutputand 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.csexplains: "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 a97019cin this repo is 8,769 chars. Run 200 times throughGitTextBuilder, 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):GitParseException: Malformed hunk header: '@@ -1'(JsonRequiredConditionally 01d3e4e)@@header, e.g. this repo's a97019cGitIntegration/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:
GitPatchParser(Parsing/GitPatchParser.cs:253-303) ends the hunk at the truncation point without an error, because it follows line markers and never checksOldCount/NewCount. The result is aGitHunkshorter than its header, and any later files are silently dropped.Apply(file.PatchFor(...))on such a hunk then fails as a corrupt patch.Log/Status/Diffjust returns fewer entries.Suggested fix / acceptance criteria
ktsu.RunCommandto ≥ 1.8.0. 1.9.0 was tested.ktsu.Semantics.Paths/ktsu.Semantics.Stringsfrom 3.0.1 to ≥ 5.8.0, otherwise restore fails with NU1109 downgrade errors.VersionOverrideconsumer-resolution check described in CLAUDE.md.Patch()orLog()) many times and asserts it is byte-identical each time.GitPatchParsercheck each hunk's line tally against its@@counts and throwGitParseExceptionon mismatch, so a short read fails loudly instead of producing a malformed hunk.