fork of go-gitdiff with jj support
1package gitdiff
2
3import (
4 "io"
5 "os"
6 "reflect"
7 "testing"
8)
9
10func TestParseGitFileHeader(t *testing.T) {
11 tests := map[string]struct {
12 Input string
13 Output *File
14 Err bool
15 }{
16 "fileContentChange": {
17 Input: `diff --git a/dir/file.txt b/dir/file.txt
18index 1c23fcc..40a1b33 100644
19--- a/dir/file.txt
20+++ b/dir/file.txt
21@@ -2,3 +4,5 @@
22`,
23 Output: &File{
24 OldName: "dir/file.txt",
25 NewName: "dir/file.txt",
26 OldMode: os.FileMode(0100644),
27 OldOIDPrefix: "1c23fcc",
28 NewOIDPrefix: "40a1b33",
29 },
30 },
31 "newFile": {
32 Input: `diff --git a/dir/file.txt b/dir/file.txt
33new file mode 100644
34index 0000000..f5711e4
35--- /dev/null
36+++ b/dir/file.txt
37`,
38 Output: &File{
39 NewName: "dir/file.txt",
40 NewMode: os.FileMode(0100644),
41 OldOIDPrefix: "0000000",
42 NewOIDPrefix: "f5711e4",
43 IsNew: true,
44 },
45 },
46 "newEmptyFile": {
47 Input: `diff --git a/empty.txt b/empty.txt
48new file mode 100644
49index 0000000..e69de29
50`,
51 Output: &File{
52 NewName: "empty.txt",
53 NewMode: os.FileMode(0100644),
54 OldOIDPrefix: "0000000",
55 NewOIDPrefix: "e69de29",
56 IsNew: true,
57 },
58 },
59 "deleteFile": {
60 Input: `diff --git a/dir/file.txt b/dir/file.txt
61deleted file mode 100644
62index 44cc321..0000000
63--- a/dir/file.txt
64+++ /dev/null
65`,
66 Output: &File{
67 OldName: "dir/file.txt",
68 OldMode: os.FileMode(0100644),
69 OldOIDPrefix: "44cc321",
70 NewOIDPrefix: "0000000",
71 IsDelete: true,
72 },
73 },
74 "changeMode": {
75 Input: `diff --git a/file.sh b/file.sh
76old mode 100644
77new mode 100755
78`,
79 Output: &File{
80 OldName: "file.sh",
81 NewName: "file.sh",
82 OldMode: os.FileMode(0100644),
83 NewMode: os.FileMode(0100755),
84 },
85 },
86 "rename": {
87 Input: `diff --git a/foo.txt b/bar.txt
88similarity index 100%
89rename from foo.txt
90rename to bar.txt
91`,
92 Output: &File{
93 OldName: "foo.txt",
94 NewName: "bar.txt",
95 Score: 100,
96 IsRename: true,
97 },
98 },
99 "copy": {
100 Input: `diff --git a/file.txt b/copy.txt
101similarity index 100%
102copy from file.txt
103copy to copy.txt
104`,
105 Output: &File{
106 OldName: "file.txt",
107 NewName: "copy.txt",
108 Score: 100,
109 IsCopy: true,
110 },
111 },
112 "missingDefaultFilename": {
113 Input: `diff --git a/foo.sh b/bar.sh
114old mode 100644
115new mode 100755
116`,
117 Err: true,
118 },
119 "missingNewFilename": {
120 Input: `diff --git a/file.txt b/file.txt
121index 1c23fcc..40a1b33 100644
122--- a/file.txt
123`,
124 Err: true,
125 },
126 "missingOldFilename": {
127 Input: `diff --git a/file.txt b/file.txt
128index 1c23fcc..40a1b33 100644
129+++ b/file.txt
130`,
131 Err: true,
132 },
133 "invalidHeaderLine": {
134 Input: `diff --git a/file.txt b/file.txt
135index deadbeef
136--- a/file.txt
137+++ b/file.txt
138`,
139 Err: true,
140 },
141 "notGitHeader": {
142 Input: `--- file.txt
143+++ file.txt
144@@ -0,0 +1 @@
145`,
146 Output: nil,
147 },
148 }
149
150 for name, test := range tests {
151 t.Run(name, func(t *testing.T) {
152 p := newTestParser(test.Input, true)
153
154 f, err := p.ParseGitFileHeader()
155 if test.Err {
156 if err == nil || err == io.EOF {
157 t.Fatalf("expected error parsing git file header, got %v", err)
158 }
159 return
160 }
161 if err != nil {
162 t.Fatalf("unexpected error parsing git file header: %v", err)
163 }
164
165 if !reflect.DeepEqual(test.Output, f) {
166 t.Errorf("incorrect file\nexpected: %+v\n actual: %+v", test.Output, f)
167 }
168 })
169 }
170}
171
172func TestParseTraditionalFileHeader(t *testing.T) {
173 tests := map[string]struct {
174 Input string
175 Output *File
176 Err bool
177 }{
178 "fileContentChange": {
179 Input: `--- dir/file_old.txt 2019-03-21 23:00:00.0 -0700
180+++ dir/file_new.txt 2019-03-21 23:30:00.0 -0700
181@@ -0,0 +1 @@
182`,
183 Output: &File{
184 OldName: "dir/file_new.txt",
185 NewName: "dir/file_new.txt",
186 },
187 },
188 "newFile": {
189 Input: `--- /dev/null 1969-12-31 17:00:00.0 -0700
190+++ dir/file.txt 2019-03-21 23:30:00.0 -0700
191@@ -0,0 +1 @@
192`,
193 Output: &File{
194 NewName: "dir/file.txt",
195 IsNew: true,
196 },
197 },
198 "newFileTimestamp": {
199 Input: `--- dir/file.txt 1969-12-31 17:00:00.0 -0700
200+++ dir/file.txt 2019-03-21 23:30:00.0 -0700
201@@ -0,0 +1 @@
202`,
203 Output: &File{
204 NewName: "dir/file.txt",
205 IsNew: true,
206 },
207 },
208 "deleteFile": {
209 Input: `--- dir/file.txt 2019-03-21 23:30:00.0 -0700
210+++ /dev/null 1969-12-31 17:00:00.0 -0700
211@@ -0,0 +1 @@
212`,
213 Output: &File{
214 OldName: "dir/file.txt",
215 IsDelete: true,
216 },
217 },
218 "deleteFileTimestamp": {
219 Input: `--- dir/file.txt 2019-03-21 23:30:00.0 -0700
220+++ dir/file.txt 1969-12-31 17:00:00.0 -0700
221@@ -0,0 +1 @@
222`,
223 Output: &File{
224 OldName: "dir/file.txt",
225 IsDelete: true,
226 },
227 },
228 "useShortestPrefixName": {
229 Input: `--- dir/file.txt 2019-03-21 23:00:00.0 -0700
230+++ dir/file.txt~ 2019-03-21 23:30:00.0 -0700
231@@ -0,0 +1 @@
232`,
233 Output: &File{
234 OldName: "dir/file.txt",
235 NewName: "dir/file.txt",
236 },
237 },
238 "notTraditionalHeader": {
239 Input: `diff --git a/dir/file.txt b/dir/file.txt
240--- a/dir/file.txt
241+++ b/dir/file.txt
242`,
243 Output: nil,
244 },
245 "noUnifiedFragment": {
246 Input: `--- dir/file_old.txt 2019-03-21 23:00:00.0 -0700
247+++ dir/file_new.txt 2019-03-21 23:30:00.0 -0700
248context line
249+added line
250`,
251 Output: nil,
252 },
253 }
254
255 for name, test := range tests {
256 t.Run(name, func(t *testing.T) {
257 p := newTestParser(test.Input, true)
258
259 f, err := p.ParseTraditionalFileHeader()
260 if test.Err {
261 if err == nil || err == io.EOF {
262 t.Fatalf("expected error parsing traditional file header, got %v", err)
263 }
264 return
265 }
266 if err != nil {
267 t.Fatalf("unexpected error parsing traditional file header: %v", err)
268 }
269
270 if !reflect.DeepEqual(test.Output, f) {
271 t.Errorf("incorrect file\nexpected: %+v\n actual: %+v", test.Output, f)
272 }
273 })
274 }
275}
276
277func TestCleanName(t *testing.T) {
278 tests := map[string]struct {
279 Input string
280 Drop int
281 Output string
282 }{
283 "alreadyClean": {
284 Input: "a/b/c.txt", Output: "a/b/c.txt",
285 },
286 "doubleSlashes": {
287 Input: "a//b/c.txt", Output: "a/b/c.txt",
288 },
289 "tripleSlashes": {
290 Input: "a///b/c.txt", Output: "a/b/c.txt",
291 },
292 "dropPrefix": {
293 Input: "a/b/c.txt", Drop: 2, Output: "c.txt",
294 },
295 "removeDoublesBeforeDrop": {
296 Input: "a//b/c.txt", Drop: 1, Output: "b/c.txt",
297 },
298 }
299
300 for name, test := range tests {
301 t.Run(name, func(t *testing.T) {
302 output := cleanName(test.Input, test.Drop)
303 if output != test.Output {
304 t.Fatalf("incorrect output: expected %q, actual %q", test.Output, output)
305 }
306 })
307 }
308}
309
310func TestParseName(t *testing.T) {
311 tests := map[string]struct {
312 Input string
313 Term rune
314 Drop int
315 Output string
316 N int
317 Err bool
318 }{
319 "singleUnquoted": {
320 Input: "dir/file.txt", Output: "dir/file.txt", N: 12,
321 },
322 "singleQuoted": {
323 Input: `"dir/file.txt"`, Output: "dir/file.txt", N: 14,
324 },
325 "quotedWithEscape": {
326 Input: `"dir/\"quotes\".txt"`, Output: `dir/"quotes".txt`, N: 20,
327 },
328 "quotedWithSpaces": {
329 Input: `"dir/space file.txt"`, Output: "dir/space file.txt", N: 20,
330 },
331 "tabTerminator": {
332 Input: "dir/space file.txt\tfile2.txt", Term: '\t', Output: "dir/space file.txt", N: 18,
333 },
334 "dropPrefix": {
335 Input: "a/dir/file.txt", Drop: 1, Output: "dir/file.txt", N: 14,
336 },
337 "multipleNames": {
338 Input: "dir/a.txt dir/b.txt", Term: -1, Output: "dir/a.txt", N: 9,
339 },
340 "devNull": {
341 Input: "/dev/null", Term: '\t', Drop: 1, Output: "/dev/null", N: 9,
342 },
343 "newlineAlwaysSeparates": {
344 Input: "dir/file.txt\n", Term: 0, Output: "dir/file.txt", N: 12,
345 },
346 "emptyString": {
347 Input: "", Err: true,
348 },
349 "emptyQuotedString": {
350 Input: `""`, Err: true,
351 },
352 "unterminatedQuotes": {
353 Input: `"dir/file.txt`, Err: true,
354 },
355 }
356
357 for name, test := range tests {
358 t.Run(name, func(t *testing.T) {
359 output, n, err := parseName(test.Input, test.Term, test.Drop)
360 if test.Err {
361 if err == nil || err == io.EOF {
362 t.Fatalf("expected error parsing name, but got %v", err)
363 }
364 return
365 }
366 if err != nil {
367 t.Fatalf("unexpected error parsing name: %v", err)
368 }
369
370 if output != test.Output {
371 t.Errorf("incorrect output: expected %q, actual: %q", test.Output, output)
372 }
373 if n != test.N {
374 t.Errorf("incorrect next position: expected %d, actual %d", test.N, n)
375 }
376 })
377 }
378}
379
380func TestParseGitHeaderData(t *testing.T) {
381 tests := map[string]struct {
382 InputFile *File
383 Line string
384 DefaultName string
385
386 OutputFile *File
387 End bool
388 Err bool
389 }{
390 "fragementEndsParsing": {
391 Line: "@@ -12,3 +12,2 @@\n",
392 End: true,
393 },
394 "unknownEndsParsing": {
395 Line: "GIT binary file\n",
396 End: true,
397 },
398 "oldFileName": {
399 Line: "--- a/dir/file.txt\n",
400 OutputFile: &File{
401 OldName: "dir/file.txt",
402 },
403 },
404 "oldFileNameDevNull": {
405 InputFile: &File{
406 IsNew: true,
407 },
408 Line: "--- /dev/null\n",
409 OutputFile: &File{
410 IsNew: true,
411 },
412 },
413 "oldFileNameInconsistent": {
414 InputFile: &File{
415 OldName: "dir/foo.txt",
416 },
417 Line: "--- a/dir/bar.txt\n",
418 Err: true,
419 },
420 "oldFileNameExistingCreateMismatch": {
421 InputFile: &File{
422 OldName: "dir/foo.txt",
423 IsNew: true,
424 },
425 Line: "--- /dev/null\n",
426 Err: true,
427 },
428 "oldFileNameParsedCreateMismatch": {
429 InputFile: &File{
430 IsNew: true,
431 },
432 Line: "--- a/dir/file.txt\n",
433 Err: true,
434 },
435 "oldFileNameMissing": {
436 Line: "--- \n",
437 Err: true,
438 },
439 "newFileName": {
440 Line: "+++ b/dir/file.txt\n",
441 OutputFile: &File{
442 NewName: "dir/file.txt",
443 },
444 },
445 "newFileNameDevNull": {
446 InputFile: &File{
447 IsDelete: true,
448 },
449 Line: "+++ /dev/null\n",
450 OutputFile: &File{
451 IsDelete: true,
452 },
453 },
454 "newFileNameInconsistent": {
455 InputFile: &File{
456 NewName: "dir/foo.txt",
457 },
458 Line: "+++ b/dir/bar.txt\n",
459 Err: true,
460 },
461 "newFileNameExistingDeleteMismatch": {
462 InputFile: &File{
463 NewName: "dir/foo.txt",
464 IsDelete: true,
465 },
466 Line: "+++ /dev/null\n",
467 Err: true,
468 },
469 "newFileNameParsedDeleteMismatch": {
470 InputFile: &File{
471 IsDelete: true,
472 },
473 Line: "+++ b/dir/file.txt\n",
474 Err: true,
475 },
476 "newFileNameMissing": {
477 Line: "+++ \n",
478 Err: true,
479 },
480 "oldMode": {
481 Line: "old mode 100644\n",
482 OutputFile: &File{
483 OldMode: os.FileMode(0100644),
484 },
485 },
486 "invalidOldMode": {
487 Line: "old mode rw\n",
488 Err: true,
489 },
490 "newMode": {
491 Line: "new mode 100755\n",
492 OutputFile: &File{
493 NewMode: os.FileMode(0100755),
494 },
495 },
496 "invalidNewMode": {
497 Line: "new mode rwx\n",
498 Err: true,
499 },
500 "deletedFileMode": {
501 Line: "deleted file mode 100644\n",
502 DefaultName: "dir/file.txt",
503 OutputFile: &File{
504 OldName: "dir/file.txt",
505 OldMode: os.FileMode(0100644),
506 IsDelete: true,
507 },
508 },
509 "newFileMode": {
510 Line: "new file mode 100755\n",
511 DefaultName: "dir/file.txt",
512 OutputFile: &File{
513 NewName: "dir/file.txt",
514 NewMode: os.FileMode(0100755),
515 IsNew: true,
516 },
517 },
518 "copyFrom": {
519 Line: "copy from dir/file.txt\n",
520 OutputFile: &File{
521 OldName: "dir/file.txt",
522 IsCopy: true,
523 },
524 },
525 "copyTo": {
526 Line: "copy to dir/file.txt\n",
527 OutputFile: &File{
528 NewName: "dir/file.txt",
529 IsCopy: true,
530 },
531 },
532 "renameFrom": {
533 Line: "rename from dir/file.txt\n",
534 OutputFile: &File{
535 OldName: "dir/file.txt",
536 IsRename: true,
537 },
538 },
539 "renameTo": {
540 Line: "rename to dir/file.txt\n",
541 OutputFile: &File{
542 NewName: "dir/file.txt",
543 IsRename: true,
544 },
545 },
546 "similarityIndex": {
547 Line: "similarity index 88%\n",
548 OutputFile: &File{
549 Score: 88,
550 },
551 },
552 "similarityIndexTooBig": {
553 Line: "similarity index 9001%\n",
554 OutputFile: &File{
555 Score: 0,
556 },
557 },
558 "similarityIndexInvalid": {
559 Line: "similarity index 12ab%\n",
560 Err: true,
561 },
562 "indexFullSHA1AndMode": {
563 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14..04fab916d8f938173cbb8b93469855f0e838f098 100644\n",
564 OutputFile: &File{
565 OldOIDPrefix: "79c6d7f7b7e76c75b3d238f12fb1323f2333ba14",
566 NewOIDPrefix: "04fab916d8f938173cbb8b93469855f0e838f098",
567 OldMode: os.FileMode(0100644),
568 },
569 },
570 "indexFullSHA1NoMode": {
571 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14..04fab916d8f938173cbb8b93469855f0e838f098\n",
572 OutputFile: &File{
573 OldOIDPrefix: "79c6d7f7b7e76c75b3d238f12fb1323f2333ba14",
574 NewOIDPrefix: "04fab916d8f938173cbb8b93469855f0e838f098",
575 },
576 },
577 "indexAbbrevSHA1AndMode": {
578 Line: "index 79c6d7..04fab9 100644\n",
579 OutputFile: &File{
580 OldOIDPrefix: "79c6d7",
581 NewOIDPrefix: "04fab9",
582 OldMode: os.FileMode(0100644),
583 },
584 },
585 "indexInvalid": {
586 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14\n",
587 Err: true,
588 },
589 }
590
591 for name, test := range tests {
592 t.Run(name, func(t *testing.T) {
593 var f File
594 if test.InputFile != nil {
595 f = *test.InputFile
596 }
597
598 end, err := parseGitHeaderData(&f, test.Line, test.DefaultName)
599 if test.Err {
600 if err == nil || err == io.EOF {
601 t.Fatalf("expected error parsing header data, but got %v", err)
602 }
603 return
604 }
605 if err != nil {
606 t.Fatalf("unexpected error parsing header data: %v", err)
607 }
608
609 if test.OutputFile != nil && !reflect.DeepEqual(test.OutputFile, &f) {
610 t.Errorf("incorrect output:\nexpected: %+v\nactual: %+v", test.OutputFile, &f)
611 }
612 if end != test.End {
613 t.Errorf("incorrect end state, expected %t, actual %t", test.End, end)
614 }
615 })
616 }
617}
618
619func TestParseGitHeaderName(t *testing.T) {
620 tests := map[string]struct {
621 Input string
622 Output string
623 Err bool
624 }{
625 "twoMatchingNames": {
626 Input: "a/dir/file.txt b/dir/file.txt",
627 Output: "dir/file.txt",
628 },
629 "twoDifferentNames": {
630 Input: "a/dir/foo.txt b/dir/bar.txt",
631 Output: "",
632 },
633 "missingSecondName": {
634 Input: "a/dir/foo.txt",
635 Err: true,
636 },
637 "invalidName": {
638 Input: `"a/dir/file.txt b/dir/file.txt`,
639 Err: true,
640 },
641 }
642
643 for name, test := range tests {
644 t.Run(name, func(t *testing.T) {
645 output, err := parseGitHeaderName(test.Input)
646 if test.Err {
647 if err == nil {
648 t.Fatalf("expected error parsing header name, but got nil")
649 }
650 return
651 }
652 if err != nil {
653 t.Fatalf("unexpected error parsing header name: %v", err)
654 }
655
656 if output != test.Output {
657 t.Errorf("incorrect output: expected %q, actual %q", test.Output, output)
658 }
659 })
660 }
661}
662
663func TestHasEpochTimestamp(t *testing.T) {
664 tests := map[string]struct {
665 Input string
666 Output bool
667 }{
668 "utcTimestamp": {
669 Input: "+++ file.txt\t1970-01-01 00:00:00 +0000\n",
670 Output: true,
671 },
672 "utcZoneWithColon": {
673 Input: "+++ file.txt\t1970-01-01 00:00:00 +00:00\n",
674 Output: true,
675 },
676 "utcZoneWithMilliseconds": {
677 Input: "+++ file.txt\t1970-01-01 00:00:00.000000 +00:00\n",
678 Output: true,
679 },
680 "westTimestamp": {
681 Input: "+++ file.txt\t1969-12-31 16:00:00 -0800\n",
682 Output: true,
683 },
684 "eastTimestamp": {
685 Input: "+++ file.txt\t1970-01-01 04:00:00 +0400\n",
686 Output: true,
687 },
688 "noTab": {
689 Input: "+++ file.txt 1970-01-01 00:00:00 +0000\n",
690 Output: false,
691 },
692 "invalidFormat": {
693 Input: "+++ file.txt\t1970-01-01T00:00:00Z\n",
694 Output: false,
695 },
696 "notEpoch": {
697 Input: "+++ file.txt\t2019-03-21 12:34:56.789 -0700\n",
698 Output: false,
699 },
700 }
701
702 for name, test := range tests {
703 t.Run(name, func(t *testing.T) {
704 output := hasEpochTimestamp(test.Input)
705 if output != test.Output {
706 t.Errorf("incorrect output: expected %t, actual %t", test.Output, output)
707 }
708 })
709 }
710}