fork of go-gitdiff with jj support

Add validation function for text fragment

Applying a fragment requires the content to match the stored counts, so
there must be a way to check this. Parsed fragments should always be
valid, but manually created or modified fragments may be invalid.

+227
+66
gitdiff/gitdiff.go
··· 61 61 return fmt.Sprintf("@@ -%d,%d +%d,%d @@ %s", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines, f.Comment) 62 62 } 63 63 64 + // Validate checks that the fragment is self-consistent and appliable. Validate 65 + // returns an error if and only if the fragment is invalid. 66 + func (f *TextFragment) Validate() error { 67 + var ( 68 + oldLines, newLines int64 69 + leadingContext, trailingContext int64 70 + contextLines, addedLines, deletedLines int64 71 + ) 72 + 73 + // count the types of lines in the fragment content 74 + for i, line := range f.Lines { 75 + switch line.Op { 76 + case OpContext: 77 + oldLines++ 78 + newLines++ 79 + contextLines++ 80 + if addedLines == 0 && deletedLines == 0 { 81 + leadingContext++ 82 + } else { 83 + trailingContext++ 84 + } 85 + case OpAdd: 86 + newLines++ 87 + addedLines++ 88 + trailingContext = 0 89 + case OpDelete: 90 + oldLines++ 91 + deletedLines++ 92 + trailingContext = 0 93 + default: 94 + return fmt.Errorf("unknown operator %q on line %d", line.Op, i+1) 95 + } 96 + } 97 + 98 + // check the actual counts against the reported counts 99 + if oldLines != f.OldLines { 100 + return lineCountErr("old", oldLines, f.OldLines) 101 + } 102 + if newLines != f.NewLines { 103 + return lineCountErr("new", newLines, f.NewLines) 104 + } 105 + if leadingContext != f.LeadingContext { 106 + return lineCountErr("leading context", leadingContext, f.LeadingContext) 107 + } 108 + if trailingContext != f.TrailingContext { 109 + return lineCountErr("trailing context", trailingContext, f.TrailingContext) 110 + } 111 + if addedLines != f.LinesAdded { 112 + return lineCountErr("added", addedLines, f.LinesAdded) 113 + } 114 + if deletedLines != f.LinesDeleted { 115 + return lineCountErr("deleted", deletedLines, f.LinesDeleted) 116 + } 117 + 118 + // if a file is being created, it can only contain additions 119 + if f.OldPosition == 0 && f.OldLines != 0 { 120 + return fmt.Errorf("file creation fragment contains context or deletion lines") 121 + } 122 + 123 + return nil 124 + } 125 + 126 + func lineCountErr(kind string, actual, reported int64) error { 127 + return fmt.Errorf("fragment contains %d %s lines but reports %d", actual, kind, reported) 128 + } 129 + 64 130 // Line is a line in a text fragment. 65 131 type Line struct { 66 132 Op LineOp
+161
gitdiff/gitdiff_test.go
··· 1 + package gitdiff 2 + 3 + import ( 4 + "strings" 5 + "testing" 6 + ) 7 + 8 + func TestTextFragmentValidate(t *testing.T) { 9 + tests := map[string]struct { 10 + Fragment TextFragment 11 + Err string 12 + }{ 13 + "oldLines": { 14 + Fragment: TextFragment{ 15 + OldPosition: 1, 16 + OldLines: 3, 17 + NewPosition: 1, 18 + NewLines: 2, 19 + LeadingContext: 1, 20 + TrailingContext: 0, 21 + LinesAdded: 1, 22 + LinesDeleted: 1, 23 + Lines: []Line{ 24 + {Op: OpContext, Line: "line 1\n"}, 25 + {Op: OpDelete, Line: "old line 2\n"}, 26 + {Op: OpAdd, Line: "new line 2\n"}, 27 + }, 28 + }, 29 + Err: "2 old lines", 30 + }, 31 + "newLines": { 32 + Fragment: TextFragment{ 33 + OldPosition: 1, 34 + OldLines: 2, 35 + NewPosition: 1, 36 + NewLines: 3, 37 + LeadingContext: 1, 38 + TrailingContext: 0, 39 + LinesAdded: 1, 40 + LinesDeleted: 1, 41 + Lines: []Line{ 42 + {Op: OpContext, Line: "line 1\n"}, 43 + {Op: OpDelete, Line: "old line 2\n"}, 44 + {Op: OpAdd, Line: "new line 2\n"}, 45 + }, 46 + }, 47 + Err: "2 new lines", 48 + }, 49 + "leadingContext": { 50 + Fragment: TextFragment{ 51 + OldPosition: 1, 52 + OldLines: 2, 53 + NewPosition: 1, 54 + NewLines: 2, 55 + LeadingContext: 0, 56 + TrailingContext: 0, 57 + LinesAdded: 1, 58 + LinesDeleted: 1, 59 + Lines: []Line{ 60 + {Op: OpContext, Line: "line 1\n"}, 61 + {Op: OpDelete, Line: "old line 2\n"}, 62 + {Op: OpAdd, Line: "new line 2\n"}, 63 + }, 64 + }, 65 + Err: "1 leading context lines", 66 + }, 67 + "trailingContext": { 68 + Fragment: TextFragment{ 69 + OldPosition: 1, 70 + OldLines: 4, 71 + NewPosition: 1, 72 + NewLines: 3, 73 + LeadingContext: 1, 74 + TrailingContext: 1, 75 + LinesAdded: 1, 76 + LinesDeleted: 2, 77 + Lines: []Line{ 78 + {Op: OpContext, Line: "line 1\n"}, 79 + {Op: OpDelete, Line: "old line 2\n"}, 80 + {Op: OpAdd, Line: "new line 2\n"}, 81 + {Op: OpContext, Line: "line 3\n"}, 82 + {Op: OpDelete, Line: "old line 4\n"}, 83 + }, 84 + }, 85 + Err: "0 trailing context lines", 86 + }, 87 + "linesAdded": { 88 + Fragment: TextFragment{ 89 + OldPosition: 1, 90 + OldLines: 4, 91 + NewPosition: 1, 92 + NewLines: 3, 93 + LeadingContext: 1, 94 + TrailingContext: 0, 95 + LinesAdded: 2, 96 + LinesDeleted: 2, 97 + Lines: []Line{ 98 + {Op: OpContext, Line: "line 1\n"}, 99 + {Op: OpDelete, Line: "old line 2\n"}, 100 + {Op: OpAdd, Line: "new line 2\n"}, 101 + {Op: OpContext, Line: "line 3\n"}, 102 + {Op: OpDelete, Line: "old line 4\n"}, 103 + }, 104 + }, 105 + Err: "1 added lines", 106 + }, 107 + "linesDeleted": { 108 + Fragment: TextFragment{ 109 + OldPosition: 1, 110 + OldLines: 4, 111 + NewPosition: 1, 112 + NewLines: 3, 113 + LeadingContext: 1, 114 + TrailingContext: 0, 115 + LinesAdded: 1, 116 + LinesDeleted: 1, 117 + Lines: []Line{ 118 + {Op: OpContext, Line: "line 1\n"}, 119 + {Op: OpDelete, Line: "old line 2\n"}, 120 + {Op: OpAdd, Line: "new line 2\n"}, 121 + {Op: OpContext, Line: "line 3\n"}, 122 + {Op: OpDelete, Line: "old line 4\n"}, 123 + }, 124 + }, 125 + Err: "2 deleted lines", 126 + }, 127 + "fileCreation": { 128 + Fragment: TextFragment{ 129 + OldPosition: 0, 130 + OldLines: 2, 131 + NewPosition: 1, 132 + NewLines: 1, 133 + LeadingContext: 0, 134 + TrailingContext: 0, 135 + LinesAdded: 1, 136 + LinesDeleted: 2, 137 + Lines: []Line{ 138 + {Op: OpDelete, Line: "old line 1\n"}, 139 + {Op: OpDelete, Line: "old line 2\n"}, 140 + {Op: OpAdd, Line: "new line\n"}, 141 + }, 142 + }, 143 + Err: "creation fragment", 144 + }, 145 + } 146 + 147 + for name, test := range tests { 148 + t.Run(name, func(t *testing.T) { 149 + err := test.Fragment.Validate() 150 + if test.Err == "" && err != nil { 151 + t.Fatalf("unexpected validation error: %v", err) 152 + } 153 + if test.Err != "" && err == nil { 154 + t.Fatal("expected validation error, but got nil") 155 + } 156 + if !strings.Contains(err.Error(), test.Err) { 157 + t.Fatalf("incorrect validation error: %q is not in %q", test.Err, err.Error()) 158 + } 159 + }) 160 + } 161 + }