Skip to content

Commit 25bee8c

Browse files
committed
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.
1 parent d471b7e commit 25bee8c

File tree

2 files changed

+227
-0
lines changed

2 files changed

+227
-0
lines changed

gitdiff/gitdiff.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,72 @@ func (f *TextFragment) Header() string {
6161
return fmt.Sprintf("@@ -%d,%d +%d,%d @@ %s", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines, f.Comment)
6262
}
6363

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+
64130
// Line is a line in a text fragment.
65131
type Line struct {
66132
Op LineOp

gitdiff/gitdiff_test.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
}

0 commit comments

Comments
 (0)