Skip to content

Commit c3b8bb2

Browse files
Add handling added/deleted files in diff (#50)
1 parent f935979 commit c3b8bb2

8 files changed

+204
-2
lines changed

diff/diff.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,12 @@ func (h *Hunk) Stat() Stat {
5454
}
5555

5656
var (
57-
hunkPrefix = []byte("@@ ")
57+
hunkPrefix = []byte("@@ ")
58+
onlyInMessagePrefix = []byte("Only in ")
5859
)
5960

6061
const hunkHeader = "@@ -%d,%d +%d,%d @@"
62+
const onlyInMessage = "Only in %s: %s\n"
6163

6264
// diffTimeParseLayout is the layout used to parse the time in unified diff file
6365
// header timestamps.

diff/diff_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,123 @@ func TestParseMultiFileDiffHeaders(t *testing.T) {
496496
},
497497
},
498498
},
499+
{
500+
filename: "sample_contains_added_deleted_files.diff",
501+
wantDiffs: []*FileDiff{
502+
{
503+
OrigName: "source_a/file_1.txt",
504+
OrigTime: nil,
505+
NewName: "source_b/file_1.txt",
506+
NewTime: nil,
507+
Extended: []string{
508+
"diff -u source_a/file_1.txt source_b/file_1.txt",
509+
},
510+
},
511+
{
512+
OrigName: "source_a/file_2.txt",
513+
OrigTime: nil,
514+
NewName: "",
515+
NewTime: nil,
516+
Extended: nil,
517+
},
518+
{
519+
OrigName: "source_b/file_3.txt",
520+
OrigTime: nil,
521+
NewName: "",
522+
NewTime: nil,
523+
Extended: nil,
524+
},
525+
},
526+
},
527+
{
528+
filename: "sample_contains_only_added_deleted_files.diff",
529+
wantDiffs: []*FileDiff{
530+
{
531+
OrigName: "source_a/file_1.txt",
532+
OrigTime: nil,
533+
NewName: "",
534+
NewTime: nil,
535+
Extended: nil,
536+
},
537+
{
538+
OrigName: "source_a/file_2.txt",
539+
OrigTime: nil,
540+
NewName: "",
541+
NewTime: nil,
542+
Extended: nil,
543+
},
544+
{
545+
OrigName: "source_b/file_3.txt",
546+
OrigTime: nil,
547+
NewName: "",
548+
NewTime: nil,
549+
Extended: nil,
550+
},
551+
},
552+
},
553+
{
554+
filename: "sample_onlyin_line_isnt_a_file_header.diff",
555+
wantDiffs: []*FileDiff{
556+
{
557+
OrigName: "source_a/file_1.txt",
558+
OrigTime: nil,
559+
NewName: "source_b/file_1.txt",
560+
NewTime: nil,
561+
Extended: []string{
562+
"diff -u source_a/file_1.txt source_b/file_1.txt",
563+
},
564+
},
565+
{
566+
OrigName: "source_a/file_2.txt",
567+
OrigTime: nil,
568+
NewName: "",
569+
NewTime: nil,
570+
Extended: []string{
571+
"Only in universe!",
572+
},
573+
},
574+
{
575+
OrigName: "source_b/file_3.txt some unrelated stuff here.",
576+
OrigTime: nil,
577+
NewName: "",
578+
NewTime: nil,
579+
Extended: nil,
580+
},
581+
{
582+
OrigName: "source_b/file_3.txt",
583+
OrigTime: nil,
584+
NewName: "",
585+
NewTime: nil,
586+
Extended: nil,
587+
},
588+
},
589+
},
590+
{
591+
filename: "sample_onlyin_complex_filenames.diff",
592+
wantDiffs: []*FileDiff{
593+
{
594+
OrigName: "internal/trace/foo bar/bam",
595+
OrigTime: nil,
596+
NewName: "",
597+
NewTime: nil,
598+
Extended: nil,
599+
},
600+
{
601+
OrigName: "internal/trace/foo bar/bam: bar",
602+
OrigTime: nil,
603+
NewName: "",
604+
NewTime: nil,
605+
Extended: nil,
606+
},
607+
{
608+
OrigName: "internal/trace/hello/world: bazz",
609+
OrigTime: nil,
610+
NewName: "",
611+
NewTime: nil,
612+
Extended: nil,
613+
},
614+
},
615+
},
499616
}
500617
for _, test := range tests {
501618
diffData, err := ioutil.ReadFile(filepath.Join("testdata", test.filename))
@@ -574,6 +691,10 @@ func TestParseMultiFileDiffAndPrintMultiFileDiff(t *testing.T) {
574691
{filename: "long_line_multi.diff", wantFileDiffs: 3},
575692
{filename: "empty.diff", wantFileDiffs: 0},
576693
{filename: "empty_multi.diff", wantFileDiffs: 2},
694+
{filename: "sample_contains_added_deleted_files.diff", wantFileDiffs: 3},
695+
{filename: "sample_contains_only_added_deleted_files.diff", wantFileDiffs: 3},
696+
{filename: "sample_onlyin_line_isnt_a_file_header.diff", wantFileDiffs: 4},
697+
{filename: "sample_onlyin_complex_filenames.diff", wantFileDiffs: 3},
577698
}
578699
for _, test := range tests {
579700
diffData, err := ioutil.ReadFile(filepath.Join("testdata", test.filename))

diff/parse.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"io"
9+
"path/filepath"
910
"strconv"
1011
"strings"
1112
"time"
@@ -72,6 +73,12 @@ func (r *MultiFileDiffReader) ReadFile() (*FileDiff, error) {
7273
}
7374
}
7475

76+
// FileDiff is added/deleted file
77+
// No further collection of hunks needed
78+
if fd.NewName == "" {
79+
return fd, nil
80+
}
81+
7582
// Before reading hunks, check to see if there are any. If there
7683
// aren't any, and there's another file after this file in the
7784
// diff, then the hunks reader will complain ErrNoHunkHeader. It's
@@ -223,8 +230,16 @@ func (r *FileDiffReader) HunksReader() *HunksReader {
223230

224231
// ReadFileHeaders reads the unified file diff header (the lines that
225232
// start with "---" and "+++" with the orig/new file names and
226-
// timestamps).
233+
// timestamps). Or which starts with "Only in " with dir path and filename.
234+
// "Only in" message is supported in POSIX locale: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/diff.html#tag_20_34_10
227235
func (r *FileDiffReader) ReadFileHeaders() (origName, newName string, origTimestamp, newTimestamp *time.Time, err error) {
236+
if r.fileHeaderLine != nil {
237+
if isOnlyMessage, source, filename := parseOnlyInMessage(r.fileHeaderLine); isOnlyMessage {
238+
return filepath.Join(string(source), string(filename)),
239+
"", nil, nil, nil
240+
}
241+
}
242+
228243
origName, origTimestamp, err = r.readOneFileHeader([]byte("--- "))
229244
if err != nil {
230245
return "", "", nil, nil, err
@@ -330,6 +345,12 @@ func (r *FileDiffReader) ReadExtendedHeaders() ([]string, error) {
330345
return xheaders, nil
331346
}
332347

348+
// Reached message that file is added/deleted
349+
if isOnlyInMessage, _, _ := parseOnlyInMessage(line); isOnlyInMessage {
350+
r.fileHeaderLine = line // pass to readOneFileHeader (see fileHeaderLine field doc)
351+
return xheaders, nil
352+
}
353+
333354
r.line++
334355
r.offset += int64(len(line))
335356
xheaders = append(xheaders, string(line))
@@ -403,6 +424,10 @@ var (
403424

404425
// ErrExtendedHeadersEOF is when an EOF was encountered while reading extended file headers, which means that there were no ---/+++ headers encountered before hunks (if any) began.
405426
ErrExtendedHeadersEOF = errors.New("expected file header while reading extended headers, got EOF")
427+
428+
// ErrBadOnlyInMessage is when a file have a malformed `only in` message
429+
// Should be in format `Only in {source}: {filename}`
430+
ErrBadOnlyInMessage = errors.New("bad 'only in' message")
406431
)
407432

408433
// ParseHunks parses hunks from a unified diff. The diff must consist
@@ -612,6 +637,19 @@ func (r *HunksReader) ReadAllHunks() ([]*Hunk, error) {
612637
}
613638
}
614639

640+
// parseOnlyInMessage checks if line is a "Only in {source}: {filename}" and returns source and filename
641+
func parseOnlyInMessage(line []byte) (bool, []byte, []byte) {
642+
if !bytes.HasPrefix(line, onlyInMessagePrefix) {
643+
return false, nil, nil
644+
}
645+
line = line[len(onlyInMessagePrefix):]
646+
idx := bytes.Index(line, []byte(": "))
647+
if idx < 0 {
648+
return false, nil, nil
649+
}
650+
return true, line[:idx], line[idx+2:]
651+
}
652+
615653
// A ParseError is a description of a unified diff syntax error.
616654
type ParseError struct {
617655
Line int // Line where the error occurred

diff/print.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"fmt"
66
"io"
7+
"path/filepath"
78
"time"
89

910
"sourcegraph.com/sqs/pbtypes"
@@ -36,6 +37,16 @@ func PrintFileDiff(d *FileDiff) ([]byte, error) {
3637
}
3738
}
3839

40+
// FileDiff is added/deleted file
41+
// No further hunks printing needed
42+
if d.NewName == "" {
43+
_, err := fmt.Fprintf(&buf, onlyInMessage, filepath.Dir(d.OrigName), filepath.Base(d.OrigName))
44+
if err != nil {
45+
return nil, err
46+
}
47+
return buf.Bytes(), nil
48+
}
49+
3950
if d.Hunks == nil {
4051
return buf.Bytes(), nil
4152
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
diff -u source_a/file_1.txt source_b/file_1.txt
2+
--- source_a/file_1.txt
3+
+++ source_b/file_1.txt
4+
@@ -2,3 +3,4 @@
5+
To be, or not to be, that is the question:
6+
-Whether 'tis nobler in the mind to suffer
7+
+The slings and arrows of outrageous fortune,
8+
+Or to take arms against a sea of troubles
9+
And by opposing end them. To die—to sleep,
10+
Only in source_a: file_2.txt
11+
Only in source_b: file_3.txt
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Only in source_a: file_1.txt
2+
Only in source_a: file_2.txt
3+
Only in source_b: file_3.txt
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Only in internal/trace/foo bar: bam
2+
Only in internal/trace/foo bar: bam: bar
3+
Only in internal/trace/hello: world: bazz
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
diff -u source_a/file_1.txt source_b/file_1.txt
2+
--- source_a/file_1.txt
3+
+++ source_b/file_1.txt
4+
@@ -2,3 +3,4 @@
5+
To be, or not to be, that is the question:
6+
-Whether 'tis nobler in the mind to suffer
7+
+The slings and arrows of outrageous fortune,
8+
+Or to take arms against a sea of troubles
9+
And by opposing end them. To die—to sleep,
10+
Only in universe!
11+
Only in source_a: file_2.txt
12+
Only in source_b: file_3.txt some unrelated stuff here.
13+
Only in source_b: file_3.txt

0 commit comments

Comments
 (0)