Return command output verbatim by default#538
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #538 +/- ##
==========================================
+ Coverage 60.98% 61.19% +0.20%
==========================================
Files 40 40
Lines 6531 6563 +32
Branches 1101 1103 +2
==========================================
+ Hits 3983 4016 +33
+ Misses 1951 1950 -1
Partials 597 597 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
c42d2d6 to
2049c04
Compare
Code reviewFound 1 issue:
Lines 2059 to 2063 in 2049c04 The newline-suffixed SHA used as a git pathspec: Lines 638 to 643 in 2049c04 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
2049c04 to
94436ff
Compare
c1ba17a to
788caff
Compare
Code reviewFresh re-review at head
libvcs/docs/project/adr/0001-faithful-subprocess-output-capture.md Lines 88 to 91 in 788caff
libvcs/docs/project/adr/0001-faithful-subprocess-output-capture.md Lines 80 to 82 in 788caff (also Risks: libvcs/docs/project/adr/0001-faithful-subprocess-output-capture.md Lines 141 to 143 in 788caff 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
why: The legacy run() helper post-processes captured output (per-line strip, drops blank lines, no trailing newline), which corrupts whitespace-significant output -- captured diffs no longer apply, blob reads lose content, multi-line errors smash together. The decision and rejected alternatives need a durable record so the behavior isn't "simplified" back into the bug. what: - Record the decision: capture pristine, trim/decode at the edge, phased migration onto SubprocessCommand. - Capture the measured alternatives matrix and prior-art lessons (pip, uv, mise, mercurial, gitoxide, jujutsu, git) as evidence. - Start a docs/project/adr/ section with an index and toctree.
why: The runner trimmed each captured line and dropped blank lines, so whitespace-significant output was corrupted: captured diffs were rejected by `git apply`, `cat-file blob` lost indentation and blank lines, and multi-line stderr was concatenated into one run-on line. Single-token reads like `rev-parse HEAD` hid the defect. what: - Capture stdout/stderr verbatim; decode once; trim only the whole output (str.rstrip), preserving leading indentation, blank lines, and interior structure. - Add `trim` (default True). Pass `trim=False` for byte-faithful output -- diffs that feed `git apply`, exact blob contents. - Rejoin stderr without smashing lines together. - Correct the Svn.blame doctest, which had encoded the stripped output. - Cover trim=False fidelity, the default trim contract, and stderr line preservation with functional tests.
why: Trimming in the capture path corrupted whitespace-significant output. Returning verbatim by default makes the plain `.run(["diff"])` faithful and applyable, fixing the original bug without an opt-in. Bare-value reads opt in with `trim=True`. what: - Flip `trim` default from True to False (verbatim). - Rewrite the docstring around the verbatim default.
why: get_revision() is a scalar API downstream compares to bare SHAs; with verbatim run() it would carry a trailing newline. what: - Strip the rev-parse result so get_revision() stays bare.
why: With run() verbatim by default, command methods return output with a trailing newline. Forwarding a `trim` flag lets callers opt into the bare value, and lets doctests show clean output without raw-string newline noise. what: - Forward a `trim` parameter from git/hg/svn command methods to run(). - Use `trim=True` in command doctests and drop the trailing newline. - Restore plain docstrings where the escaped newline is gone.
why: Tests that read scalars via the low-level run() or thin cmd wrappers relied on the old implicit trim. what: - Strip rev-parse / symbolic-ref / is-shallow / version reads. - Invert the default-behavior test to assert verbatim output.
why: The chosen design flipped to verbatim-by-default during review. what: - Update ADR 0001 Phase 1, alternatives, and consequences to reflect verbatim default with `trim=True` opt-in.
why: Document the verbatim-output breaking change and the related output-fidelity fixes for downstream readers. what: - Add a Breaking changes entry for the verbatim-default `.run()` (with the trim=True migration) and Fixes for applyable diffs, byte-identical blobs, and intact multi-line error output.
why: rev_list's since/after/until/before/max_age/min_age flags were never applied -- the guard tested the imported `datetime` module instead of the loop value, so the condition was always False. what: - Test the loop value (datetime_kwarg), matching the adjacent flag loop, so the date-range flags reach git rev-list.
c267a51 to
0069b20
Compare
Summary
.run()on theGit/Hg/Svncommand classes (and the internal runner) now returns command output verbatim by default — exactly what the VCS printed. It previously stripped each line, dropped blank lines, and removed the trailing newline, so a capturedgit diffwas rejected bygit apply,git cat-file bloblost indentation and blank lines, and multi-line stderr was concatenated into one run-on line.trimparameter (defaultFalse). Passtrim=Truefor the convenient "bare value" output the old default produced — a single whole-outputstr.rstrip, never per-line.trimpassthrough on the namedGit/Hg/Svncommand methods, so the opt-in works everywhere, not only the low-levelrun().GitSync.get_revision()(andrev_parse/rev_listviatrim=True) still return a clean value.libvcs.exc.CommandError.output.Breaking change
The default return of
.run()flips from trimmed to verbatim. Reads that want a bare value opt in withtrim=True.Before — a bare value came back trimmed:
After — output is verbatim; opt into trimming:
High-level helpers are unaffected:
GitSync.get_revision()still returns a bare revision, and downstreamvcspull(which strips defensively) passes unchanged — see the companion PR.The bug, now fixed at the default
Before (on
master) —.run(["diff"])corrupts the patch;git applyrejects it:After (this PR) —
.run(["diff"])is byte-identical togit diffand applies cleanly:Changes by area
src/libvcs/_internal/run.py: capture stdout/stderr verbatim and decode once; newtrimparameter (defaultFalse) applies a single whole-outputrstripwhen set; stderr keeps its line structure; the UTF-8 decode fallback useserrors="backslashreplace".src/libvcs/cmd/git.py,cmd/hg.py,cmd/svn.py: forwardtrimfrom the named command methods torun(); doctests show clean output viatrim=True; theGitRemoteCmd.set_urldoctest is now git-version-agnostic;Svn.blamereflects the faithful output.src/libvcs/sync/git.py:get_revision()andupdate_repo'srev_listreads strip to keep bare SHAs.tests/: functional tests for verbatim diff/blob fidelity +git apply, the default-preserves-newline contract, and stderr line preservation; sync/cmd tests adapted to verbatim output.docs/project/adr/,CHANGES: ADR 0001 (faithful subprocess output capture) wired into the Project docs, plus a changelog entry.Design decisions
trim=Trueis the opt-in for bare values: a single whole-outputrstrip, never per-line; high-level sync APIs apply it internally so downstream comparisons stay bare.CompletedProcessbackend (libvcs._internal.subprocess.SubprocessCommand) and retiringconsole_to_strare Phase 2, deferred to keep this change focused.Companion PR
uv.sources; its full suite passes unchanged, confirming the verbatim default is downstream-safe.Verification
The old per-line strip/filter is gone:
$ rg "line.strip\(\) for line in" src/libvcs/_internal/run.py(returns zero matches)
Test plan
test_run_trim_false_preserves_diff— captured diff is byte-identical togit diffand passesgit apply --checktest_run_trim_false_preserves_blob—cat-file blobround-trips byte-for-bytetest_run_default_preserves_trailing_newline— default.run()returns verbatim output (trailing newline kept);trim=Trueyields the bare valuetest_run_failure_preserves_stderr_lines— failed command keeps stderr line structure inCommandError.outputuv run pytest— full suite greenuv run ruff check . && uv run ruff format --check .uv run mypy