Skip to content

Commit c2d0f16

Browse files
bugfix: binary file panic
1 parent d5f760f commit c2d0f16

2 files changed

Lines changed: 161 additions & 3 deletions

File tree

internal/git/diff.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,37 @@ type changesSinceContext struct {
112112
}
113113

114114
func diffToFilename(d *diff.FileDiff) string {
115-
if d.NewName == "/dev/null" {
116-
// For deleted files, NewName is "/dev/null", so use OrigName instead
115+
// For regular diffs, NewName/OrigName come from the "--- a/<path>" /
116+
// "+++ b/<path>" headers and carry an "a/"/"b/" prefix to strip. For
117+
// deleted files NewName is "/dev/null". For binary files git emits no
118+
// "---"/"+++" lines (only "Binary files a/<path> and b/<path> differ"),
119+
// leaving both fields empty — recover the path from the "diff --git"
120+
// extended header instead.
121+
if len(d.NewName) > 2 && d.NewName != "/dev/null" {
122+
return d.NewName[2:]
123+
}
124+
if len(d.OrigName) > 2 && d.OrigName != "/dev/null" {
117125
return d.OrigName[2:]
118126
}
119-
return d.NewName[2:]
127+
return filenameFromExtendedHeader(d.Extended)
128+
}
129+
130+
func filenameFromExtendedHeader(headers []string) string {
131+
for _, h := range headers {
132+
rest, ok := strings.CutPrefix(h, "diff --git ")
133+
if !ok {
134+
continue
135+
}
136+
// Quoted form: "a/<path>" "b/<path>" (paths needing escaping).
137+
if idx := strings.LastIndex(rest, ` "b/`); idx >= 0 {
138+
return strings.TrimSuffix(rest[idx+len(` "b/`):], `"`)
139+
}
140+
// Unquoted form: a/<path> b/<path>
141+
if idx := strings.LastIndex(rest, " b/"); idx >= 0 {
142+
return rest[idx+len(" b/"):]
143+
}
144+
}
145+
return ""
120146
}
121147

122148
// Parse the diff output to get the file names and hunks
@@ -172,6 +198,8 @@ func changesSince(context changesSinceContext) ([]codeowners.DiffFile, error) {
172198
newDiffFile.Hunks = append(newDiffFile.Hunks, newHunkRange)
173199
}
174200
}
201+
// Binary files have no hunks; staleness is intentionally not tracked
202+
// for them (there is no hunk content to hash against the older diff).
175203
if len(newDiffFile.Hunks) > 0 {
176204
diffFiles = append(diffFiles, newDiffFile)
177205
}

internal/git/diff_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,82 @@ func TestNewDiff(t *testing.T) {
110110
"file2.go": 1,
111111
},
112112
},
113+
{
114+
name: "binary file change does not panic",
115+
context: DiffContext{
116+
Base: "main",
117+
Head: "feature",
118+
Dir: ".",
119+
},
120+
mockOutput: `diff --git a/file1.go b/file1.go
121+
index abc..def 100644
122+
--- a/file1.go
123+
+++ b/file1.go
124+
@@ -10,0 +11 @@ func Example() {
125+
+ fmt.Println("New line")
126+
diff --git a/assets/img/offline.png b/assets/img/offline.png
127+
index 1111111..2222222 100644
128+
Binary files a/assets/img/offline.png and b/assets/img/offline.png differ`,
129+
expectedErr: false,
130+
expectedFiles: 2,
131+
expectedHunks: map[string]int{
132+
"file1.go": 1,
133+
"assets/img/offline.png": 0,
134+
},
135+
},
136+
{
137+
// Regression for PE-887: a "mode change + binary patch" entry
138+
// is parsed by go-diff into a FileDiff with empty OrigName/NewName
139+
// (its handleEmpty logic does not cover this 5-extended-header
140+
// shape). The pre-fix code did NewName[2:] and panicked.
141+
name: "mode change + binary patch (PE-887)",
142+
context: DiffContext{
143+
Base: "main",
144+
Head: "feature",
145+
Dir: ".",
146+
},
147+
mockOutput: `diff --git a/assets/img.png b/assets/img.png
148+
old mode 100755
149+
new mode 100644
150+
index 8ed9b15..0bfca45
151+
Binary files a/assets/img.png and b/assets/img.png differ
152+
diff --git a/notes.txt b/notes.txt
153+
index abc..def 100644
154+
--- a/notes.txt
155+
+++ b/notes.txt
156+
@@ -1,0 +2 @@ trailing entry forces parser to flush the mode-change+binary above
157+
+ok
158+
`,
159+
expectedErr: false,
160+
expectedFiles: 2,
161+
expectedHunks: map[string]int{
162+
"assets/img.png": 0,
163+
"notes.txt": 1,
164+
},
165+
},
166+
{
167+
name: "binary file in ignored directory is filtered",
168+
context: DiffContext{
169+
Base: "main",
170+
Head: "feature",
171+
Dir: ".",
172+
IgnoreDirs: []string{"assets/"},
173+
},
174+
mockOutput: `diff --git a/file1.go b/file1.go
175+
index abc..def 100644
176+
--- a/file1.go
177+
+++ b/file1.go
178+
@@ -10,0 +11 @@ func Example() {
179+
+ fmt.Println("New line")
180+
diff --git a/assets/img/offline.png b/assets/img/offline.png
181+
index 1111111..2222222 100644
182+
Binary files a/assets/img/offline.png and b/assets/img/offline.png differ`,
183+
expectedErr: false,
184+
expectedFiles: 1,
185+
expectedHunks: map[string]int{
186+
"file1.go": 1,
187+
},
188+
},
113189
{
114190
name: "ignore deleted files in ignored directories",
115191
context: DiffContext{
@@ -442,6 +518,60 @@ func TestToDiffFiles(t *testing.T) {
442518
},
443519
},
444520
},
521+
{
522+
name: "binary file (no --- / +++ headers, filename in extended)",
523+
fileDiffs: []*diff.FileDiff{
524+
{
525+
Extended: []string{
526+
"diff --git a/assets/img/offline.png b/assets/img/offline.png",
527+
"index abc123..def456 100644",
528+
"Binary files a/assets/img/offline.png and b/assets/img/offline.png differ",
529+
},
530+
},
531+
},
532+
expected: []codeowners.DiffFile{
533+
{
534+
FileName: "assets/img/offline.png",
535+
Hunks: []codeowners.HunkRange{},
536+
},
537+
},
538+
},
539+
{
540+
name: "binary file with quoted path",
541+
fileDiffs: []*diff.FileDiff{
542+
{
543+
Extended: []string{
544+
`diff --git "a/assets/some image.png" "b/assets/some image.png"`,
545+
"Binary files differ",
546+
},
547+
},
548+
},
549+
expected: []codeowners.DiffFile{
550+
{
551+
FileName: "assets/some image.png",
552+
Hunks: []codeowners.HunkRange{},
553+
},
554+
},
555+
},
556+
{
557+
name: "binary rename uses new path",
558+
fileDiffs: []*diff.FileDiff{
559+
{
560+
Extended: []string{
561+
"diff --git a/old/foo.png b/new/foo.png",
562+
"similarity index 100%",
563+
"rename from old/foo.png",
564+
"rename to new/foo.png",
565+
},
566+
},
567+
},
568+
expected: []codeowners.DiffFile{
569+
{
570+
FileName: "new/foo.png",
571+
Hunks: []codeowners.HunkRange{},
572+
},
573+
},
574+
},
445575
{
446576
name: "mixed: added, modified, and deleted files",
447577
fileDiffs: []*diff.FileDiff{

0 commit comments

Comments
 (0)