Skip to content

Commit a2722c2

Browse files
Booyaka101claude
andauthored
fix: pass non-unified-diff input through to stdout (closes #115) (#132)
## Summary When diffnav is configured as `pager.diff` (per the README), git pipes the output of every `git diff` invocation through diffnav, including non-unified-diff variants like `git diff --stat`, `--shortstat`, `--name-only`, and `--name-status`. Those produce summary text with no `diff --git ` markers, which causes two latent bugs: 1. **Panic.** The `bluekeyes/go-gitdiff` parser returns zero files, the TUI tries to render a scrollbar over an empty viewport, and the program panics with `runtime error: integer divide by zero` in `RenderScrollbar` ([pkg/ui/common/scrollbar.go:18](https://github.com/dlvhdr/diffnav/blob/main/pkg/ui/common/scrollbar.go#L18)). 2. **Escape-sequence leak.** Before the panic, bubbletea's terminal-capability handshake leaks `[?2026\$p[?2027\$p` query bytes to the user's stdout — the symptom in the bug report. ## Fix Detect non-unified-diff input early in `cmd/root.go` (after stdin is fully read, before opening the TTY) and pass it through verbatim. The caller sees the same output they would have without diffnav as the configured pager — same behaviour as `cat`/the implicit pager-fallthrough that other TUI pagers use for non-diff input. Detection is a `strings.Contains(input, "diff --git ")` check, extracted into a small `isUnifiedDiff` helper for unit testing. Both `git diff` and `git show` always emit at least one `diff --git ` header in the unified form, so the check matches the TUI's actual input contract. ## Test plan - [x] **8 new unit tests** in `cmd/input_test.go` covering: - `git diff` (unified) - `git show` (preamble + unified) - `git diff --stat` - `git diff --shortstat` - `git diff --name-only` - `git diff --name-status` - `git log` (no patch) - empty input - [x] **Empirically reproduced on Windows** with the canonical `--stat` output: - **Pre-fix**: `[?2026\$p[?2027\$p` bytes leak, then `runtime error: integer divide by zero` panic. - **Post-fix**: stat output is printed verbatim and diffnav exits 0. - [x] **Real unified-diff input still routes to the TUI** (verified by piping `gh_dash_pr.diff` test fixture into the post-fix binary — TUI starts, hits the same pre-existing panic the upstream test environment hits, confirming the input is reaching the renderer). ## Notes - The two pre-existing `pkg/ui/panes/filetree` test failures on `main` (`TestNoLastPath`, `TestCollapseTree`) are unrelated lipgloss-styling differences and not touched by this PR. - This is a strict superset: any input that previously rendered now still renders; only inputs the renderer would have crashed on now exit gracefully. - Closes #115. Should also fix the `git log` symptom from #28 (re-emerged after the recent v2 rewrite, since `git log` without `-p` produces no `diff --git ` line and would hit the same path). ## AI usage disclosure (per AI_POLICY.md) Tool: Claude Code (Opus 4.7). Extent: pattern analysis (root cause for the panic + escape leak), code drafting for the `isUnifiedDiff` helper and the early-return path, and the test cases. I reviewed every change manually, ran `go build`, `go test ./cmd/...`, and reproduced the pre-/post-fix behaviour locally on Windows before pushing. Happy to walk through any line on request. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7934a39 commit a2722c2

3 files changed

Lines changed: 105 additions & 0 deletions

File tree

cmd/input.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package cmd
2+
3+
import "strings"
4+
5+
// isUnifiedDiff reports whether the input looks like a git unified-diff stream
6+
// (the format the TUI is built to render).
7+
//
8+
// `git diff` and `git show` produce unified diffs that always contain at least
9+
// one `diff --git ` header line, regardless of which dialect of the diff
10+
// command was invoked. Summary forms (`--stat`, `--shortstat`, `--name-only`,
11+
// `--name-status`) and metadata-only commands (`git log` with no patch) emit
12+
// other shapes that the renderer cannot consume.
13+
func isUnifiedDiff(input string) bool {
14+
return strings.Contains(input, "diff --git ")
15+
}

cmd/input_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
func TestIsUnifiedDiff(t *testing.T) {
6+
cases := []struct {
7+
name string
8+
input string
9+
want bool
10+
}{
11+
{
12+
name: "git diff (unified)",
13+
input: "diff --git a/foo.go b/foo.go\n" +
14+
"index 1234567..89abcde 100644\n" +
15+
"--- a/foo.go\n" +
16+
"+++ b/foo.go\n" +
17+
"@@ -1,3 +1,4 @@\n" +
18+
" package foo\n" +
19+
"+\n" +
20+
" func A() {}\n",
21+
want: true,
22+
},
23+
{
24+
name: "git show (preamble + unified)",
25+
input: "commit abc1234567890\n" +
26+
"Author: Someone <s@example.com>\n" +
27+
"Date: Mon Jan 1 00:00:00 2026 +0000\n" +
28+
"\n" +
29+
" subject\n" +
30+
"\n" +
31+
"diff --git a/foo.go b/foo.go\n" +
32+
"@@ -1 +1,2 @@\n" +
33+
" a\n+b\n",
34+
want: true,
35+
},
36+
{
37+
name: "git diff --stat",
38+
input: " main.go | 5 ++---\n" +
39+
" foo.go | 1 +\n" +
40+
" 2 files changed, 3 insertions(+), 3 deletions(-)\n",
41+
want: false,
42+
},
43+
{
44+
name: "git diff --shortstat",
45+
input: " 2 files changed, 3 insertions(+), 3 deletions(-)\n",
46+
want: false,
47+
},
48+
{
49+
name: "git diff --name-only",
50+
input: "main.go\nfoo.go\n",
51+
want: false,
52+
},
53+
{
54+
name: "git diff --name-status",
55+
input: "M\tmain.go\nA\tfoo.go\n",
56+
want: false,
57+
},
58+
{
59+
name: "git log (no patch)",
60+
input: "commit abc1234567890\n" +
61+
"Author: Someone <s@example.com>\n" +
62+
"Date: Mon Jan 1 00:00:00 2026 +0000\n" +
63+
"\n" +
64+
" subject\n",
65+
want: false,
66+
},
67+
{
68+
name: "empty",
69+
input: "",
70+
want: false,
71+
},
72+
}
73+
74+
for _, tc := range cases {
75+
t.Run(tc.name, func(t *testing.T) {
76+
got := isUnifiedDiff(tc.input)
77+
if got != tc.want {
78+
t.Fatalf("isUnifiedDiff(%q) = %v, want %v", tc.name, got, tc.want)
79+
}
80+
})
81+
}
82+
}

cmd/root.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@ func init() {
202202
fmt.Println("No input provided, exiting")
203203
os.Exit(0)
204204
}
205+
206+
if !isUnifiedDiff(input) {
207+
fmt.Print(input)
208+
if !strings.HasSuffix(input, "\n") {
209+
fmt.Println()
210+
}
211+
os.Exit(0)
212+
}
205213
}
206214

207215
cfg := config.Load()

0 commit comments

Comments
 (0)