fork of go-gitdiff with jj support
1package gitdiff
2
3import (
4 "fmt"
5 "io"
6 "strconv"
7 "strings"
8)
9
10// ParseTextFragments parses text fragments until the next file header or the
11// end of the stream and attaches them to the given file. It returns the number
12// of fragments that were added.
13func (p *parser) ParseTextFragments(f *File) (n int, err error) {
14 for {
15 frag, err := p.ParseTextFragmentHeader()
16 if err != nil {
17 return n, err
18 }
19 if frag == nil {
20 return n, nil
21 }
22
23 if f.IsNew && frag.OldLines > 0 {
24 return n, p.Errorf(-1, "new file depends on old contents")
25 }
26 if f.IsDelete && frag.NewLines > 0 {
27 return n, p.Errorf(-1, "deleted file still has contents")
28 }
29
30 if err := p.ParseTextChunk(frag); err != nil {
31 return n, err
32 }
33
34 f.TextFragments = append(f.TextFragments, frag)
35 n++
36 }
37}
38
39func (p *parser) ParseTextFragmentHeader() (*TextFragment, error) {
40 const (
41 startMark = "@@ -"
42 endMark = " @@"
43 )
44
45 if !strings.HasPrefix(p.Line(0), startMark) {
46 return nil, nil
47 }
48
49 parts := strings.SplitAfterN(p.Line(0), endMark, 2)
50 if len(parts) < 2 {
51 return nil, p.Errorf(0, "invalid fragment header")
52 }
53
54 f := &TextFragment{}
55 f.Comment = strings.TrimSpace(parts[1])
56
57 header := parts[0][len(startMark) : len(parts[0])-len(endMark)]
58 ranges := strings.Split(header, " +")
59 if len(ranges) != 2 {
60 return nil, p.Errorf(0, "invalid fragment header")
61 }
62
63 var err error
64 if f.OldPosition, f.OldLines, err = parseRange(ranges[0]); err != nil {
65 return nil, p.Errorf(0, "invalid fragment header: %v", err)
66 }
67 if f.NewPosition, f.NewLines, err = parseRange(ranges[1]); err != nil {
68 return nil, p.Errorf(0, "invalid fragment header: %v", err)
69 }
70
71 if err := p.Next(); err != nil && err != io.EOF {
72 return nil, err
73 }
74 return f, nil
75}
76
77func (p *parser) ParseTextChunk(frag *TextFragment) error {
78 if p.Line(0) == "" {
79 return p.Errorf(0, "no content following fragment header")
80 }
81
82 oldLines, newLines := frag.OldLines, frag.NewLines
83 for oldLines > 0 || newLines > 0 {
84 line := p.Line(0)
85 op, data := line[0], line[1:]
86
87 switch op {
88 case '\n':
89 data = "\n"
90 fallthrough // newer GNU diff versions create empty context lines
91 case ' ':
92 oldLines--
93 newLines--
94 if frag.LinesAdded == 0 && frag.LinesDeleted == 0 {
95 frag.LeadingContext++
96 } else {
97 frag.TrailingContext++
98 }
99 frag.Lines = append(frag.Lines, Line{OpContext, data})
100 case '-':
101 oldLines--
102 frag.LinesDeleted++
103 frag.TrailingContext = 0
104 frag.Lines = append(frag.Lines, Line{OpDelete, data})
105 case '+':
106 newLines--
107 frag.LinesAdded++
108 frag.TrailingContext = 0
109 frag.Lines = append(frag.Lines, Line{OpAdd, data})
110 case '\\':
111 // this may appear in middle of fragment if it's for a deleted line
112 if isNoNewlineMarker(line) {
113 removeLastNewline(frag)
114 break
115 }
116 fallthrough
117 default:
118 // TODO(bkeyes): if this is because we hit the next header, it
119 // would be helpful to return the miscounts line error. We could
120 // either test for the common headers ("@@ -", "diff --git") or
121 // assume any invalid op ends the fragment; git returns the same
122 // generic error in all cases so either is compatible
123 return p.Errorf(0, "invalid line operation: %q", op)
124 }
125
126 if err := p.Next(); err != nil {
127 if err == io.EOF {
128 break
129 }
130 return err
131 }
132 }
133
134 if oldLines != 0 || newLines != 0 {
135 hdr := max(frag.OldLines-oldLines, frag.NewLines-newLines) + 1
136 return p.Errorf(-hdr, "fragment header miscounts lines: %+d old, %+d new", -oldLines, -newLines)
137 }
138 if frag.LinesAdded == 0 && frag.LinesDeleted == 0 {
139 return p.Errorf(0, "fragment contains no changes")
140 }
141
142 // check for a final "no newline" marker since it is not included in the
143 // counters used to stop the loop above
144 if isNoNewlineMarker(p.Line(0)) {
145 removeLastNewline(frag)
146 if err := p.Next(); err != nil && err != io.EOF {
147 return err
148 }
149 }
150
151 return nil
152}
153
154func isNoNewlineMarker(s string) bool {
155 // test for "\ No newline at end of file" by prefix because the text
156 // changes by locale (git claims all versions are at least 12 chars)
157 return len(s) >= 12 && s[:2] == "\\ "
158}
159
160func removeLastNewline(frag *TextFragment) {
161 if len(frag.Lines) > 0 {
162 last := &frag.Lines[len(frag.Lines)-1]
163 last.Line = strings.TrimSuffix(last.Line, "\n")
164 }
165}
166
167func parseRange(s string) (start int64, end int64, err error) {
168 parts := strings.SplitN(s, ",", 2)
169
170 if start, err = strconv.ParseInt(parts[0], 10, 64); err != nil {
171 nerr := err.(*strconv.NumError)
172 return 0, 0, fmt.Errorf("bad start of range: %s: %v", parts[0], nerr.Err)
173 }
174
175 if len(parts) > 1 {
176 if end, err = strconv.ParseInt(parts[1], 10, 64); err != nil {
177 nerr := err.(*strconv.NumError)
178 return 0, 0, fmt.Errorf("bad end of range: %s: %v", parts[1], nerr.Err)
179 }
180 } else {
181 end = 1
182 }
183
184 return
185}
186
187func max(a, b int64) int64 {
188 if a > b {
189 return a
190 }
191 return b
192}