Skip to content

fix(cmds/git/diff): preserve POSIX/git contract for programmatic consumers#1981

Open
YOMXXX wants to merge 1 commit into
rtk-ai:developfrom
YOMXXX:fix/diff-programmatic-contract
Open

fix(cmds/git/diff): preserve POSIX/git contract for programmatic consumers#1981
YOMXXX wants to merge 1 commit into
rtk-ai:developfrom
YOMXXX:fix/diff-programmatic-contract

Conversation

@YOMXXX
Copy link
Copy Markdown
Contributor

@YOMXXX YOMXXX commented May 20, 2026

Summary

`rtk git diff` and `rtk diff` were emitting decorative output that broke `git apply`, `patch`, shell loops over `--name-only`, and exit-code-based predicates. Because the `PreToolUse` hook silently rewrites `git diff` to `rtk git diff` for hooked agents, those decorations silently distorted any patch / script workflow that used to work. Issue #1918 (and its duplicates #1081 / #1869) ask for the POSIX/git contract to be honored.

Two surfaces are fixed:

`rtk git diff` (proxy for `git diff`)

  • Detects non-TTY stdout (`std::io::IsTerminal::is_terminal`) and machine-friendly flags (`--name-only`, `--name-status`, `--check`, `--exit-code`, `--raw`, `-z`, `--no-color`, `--numstat`, `--shortstat`, `--dirstat`, `--patch-with-raw`, `--patch-with-stat`, `--output[=…]`).
  • When either is true: emit raw `git diff` output verbatim, with byte-for-byte fidelity (`print!` not `println!`) so trailing newlines are preserved for `git apply`.
  • Propagate git's actual exit code rather than forcing 0.

`rtk diff` (RTK's GNU-diff-like file compare)

  • Returns `0` when files are identical, `1` when they differ, `2` on missing-file / I/O error — the GNU diff exit-code contract.
  • The `[ok] Files are identical` status now goes to stderr, so `rtk diff a b > patch.diff` produces an empty patch on identical files (which is what scripts expect) rather than text in stdout.

Reproduction

```bash

rtk git diff round-trip

mkdir -p /tmp/g && cd /tmp/g && git init -q
git config user.email t@t.t && git config user.name t
printf '1\n2\n3\n' > f && git add f && git commit -q -m x
printf '1\n2x\n3\n' > f

rtk git diff > rtk.patch
git stash -q
git apply --check rtk.patch && echo "applies"

Before this PR: 'No valid patches in input'

After this PR: 'applies'

rtk git diff --name-only

Before this PR: 'f\n\n--- Changes ---\n f (...) ...'

After this PR: 'f'

rtk diff exit codes

printf 'a\n' > /tmp/a && printf 'b\n' > /tmp/b
rtk diff /tmp/a /tmp/b; echo "exit=$?"

Before this PR: exit=0 (silently wrong)

After this PR: exit=1 (GNU diff convention)

rtk diff /tmp/nonexistent /tmp/a; echo "exit=$?"

Before this PR: exit=1

After this PR: exit=2 (GNU diff convention)

```

Behavior changes

  • Non-TTY `rtk git diff`: no longer prepends a stat summary, no longer wraps in `--- Changes ---`. Output is byte-for-byte `git diff`. The compact path is only taken in interactive (TTY) sessions without machine-readable flags.
  • Machine-friendly flags (`--name-only` etc.): same — clean passthrough.
  • `rtk diff` exit codes now match GNU `diff`. The "Files are identical" message moves from stdout to stderr.

The compact-diff experience for interactive `rtk git diff` users is unchanged.

Test plan

  • `test_has_machine_friendly_diff_flag_detects_name_only`.
  • `test_has_machine_friendly_diff_flag_detects_machine_variants` (covers 12 flags).
  • `test_has_machine_friendly_diff_flag_ignores_normal_args` (negative).
  • `test_has_machine_friendly_diff_flag_detects_output_arg` (covers both `--output=...` and `--output ...`).
  • `test_run_returns_0_on_identical_files`.
  • `test_run_returns_1_on_different_files`.
  • `test_run_returns_2_on_missing_file`.
  • All 51 existing diff tests still pass.
  • `cargo fmt --all` clean.
  • `cargo clippy --all-targets` zero warnings.
  • `cargo test --bin rtk -- --test-threads=8` 1908 passed, 0 failed.

Out of scope (deferred to a follow-up)

  • `[hooks].exclude_commands` subcommand matching ("git diff" must skip the hook) — issue [hooks] exclude_commands does not cover subcommands (e.g. "git diff") #1919. Wider scope; deserves its own PR.
  • TTY detection for `rtk diff` (the GNU-like file compare) is not added — it's a content-transforming command, not a passthrough, so the "non-TTY emits unified diff" semantics don't apply cleanly. Exit-code fix is sufficient for the headline scripting use case.

Fixes #1918, #1869
Refs #1081 (duplicate of #1918)

…umers

Issues rtk-ai#1918 / rtk-ai#1869 / rtk-ai#1081 / rtk-ai#1869: 'rtk git diff' and 'rtk diff' were
emitting decorative output that broke 'git apply', 'patch', shell loops
over '--name-only', and exit-code-based predicates. Because the
PreToolUse hook silently rewrites 'git diff' to 'rtk git diff' for
agents, those decorations silently distorted any patch/script workflow.

Two surfaces are fixed:

1. 'rtk git diff' (proxy for 'git diff')
   - Detect non-TTY stdout AND machine-friendly flags (--name-only,
     --name-status, --check, --exit-code, --raw, -z, --no-color,
     --numstat, --shortstat, --dirstat, --patch-with-raw,
     --patch-with-stat, --output[=...]).
   - When either is true: emit raw 'git diff' output verbatim, with
     byte-for-byte fidelity (print! not println!) so trailing newlines
     are preserved for 'git apply'.
   - Propagate git's actual exit code rather than forcing 0.

2. 'rtk diff' (RTK's GNU-diff-like file compare)
   - Returns 0 when files are identical, 1 when they differ, 2 on
     missing-file / I/O error -- the GNU diff exit-code contract.
   - The '[ok] Files are identical' status now goes to stderr, so
     'rtk diff a b > patch.diff' produces an empty patch on identical
     files (which is what scripts expect) rather than text in stdout.

Eight new tests cover: machine-friendly flag detection (name-only,
name-status, stat variants, raw, -z, exit-code, output[=...]); normal
args bypass passthrough; identical files exit 0; different files exit
1; missing file exits 2.

Fixes rtk-ai#1918, rtk-ai#1869
Refs rtk-ai#1081 (duplicate of rtk-ai#1918)
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.

rtk diff and rtk git diff produce output that breaks programmatic consumers (patch, git apply, --name-only, exit codes)

1 participant