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 isNoNewlineLine := func(s string) bool {
83 // test for "\ No newline at end of file" by prefix because the text
84 // changes by locale (git claims all versions are at least 12 chars)
85 return len(s) >= 12 && s[:2] == "\\ "
86 }
87
88 oldLines, newLines := frag.OldLines, frag.NewLines
89 for {
90 line := p.Line(0)
91 op, data := line[0], line[1:]
92
93 switch op {
94 case '\n':
95 data = "\n"
96 fallthrough // newer GNU diff versions create empty context lines
97 case ' ':
98 oldLines--
99 newLines--
100 if frag.LinesAdded == 0 && frag.LinesDeleted == 0 {
101 frag.LeadingContext++
102 } else {
103 frag.TrailingContext++
104 }
105 frag.Lines = append(frag.Lines, Line{OpContext, data})
106 case '-':
107 oldLines--
108 frag.LinesDeleted++
109 frag.TrailingContext = 0
110 frag.Lines = append(frag.Lines, Line{OpDelete, data})
111 case '+':
112 newLines--
113 frag.LinesAdded++
114 frag.TrailingContext = 0
115 frag.Lines = append(frag.Lines, Line{OpAdd, data})
116 default:
117 // this may appear in middle of fragment if it's for a deleted line
118 if isNoNewlineLine(line) {
119 last := &frag.Lines[len(frag.Lines)-1]
120 last.Line = strings.TrimSuffix(last.Line, "\n")
121 break
122 }
123 // TODO(bkeyes): if this is because we hit the next header, it
124 // would be helpful to return the miscounts line error. We could
125 // either test for the common headers ("@@ -", "diff --git") or
126 // assume any invalid op ends the fragment; git returns the same
127 // generic error in all cases so either is compatible
128 return p.Errorf(0, "invalid line operation: %q", op)
129 }
130
131 next := p.Line(1)
132 if oldLines <= 0 && newLines <= 0 && !isNoNewlineLine(next) {
133 break
134 }
135
136 if err := p.Next(); err != nil {
137 if err == io.EOF {
138 break
139 }
140 return err
141 }
142 }
143
144 if oldLines != 0 || newLines != 0 {
145 hdr := max(frag.OldLines-oldLines, frag.NewLines-newLines) + 1
146 return p.Errorf(-hdr, "fragment header miscounts lines: %+d old, %+d new", -oldLines, -newLines)
147 }
148
149 if err := p.Next(); err != nil && err != io.EOF {
150 return err
151 }
152 return nil
153}
154
155func parseRange(s string) (start int64, end int64, err error) {
156 parts := strings.SplitN(s, ",", 2)
157
158 if start, err = strconv.ParseInt(parts[0], 10, 64); err != nil {
159 nerr := err.(*strconv.NumError)
160 return 0, 0, fmt.Errorf("bad start of range: %s: %v", parts[0], nerr.Err)
161 }
162
163 if len(parts) > 1 {
164 if end, err = strconv.ParseInt(parts[1], 10, 64); err != nil {
165 nerr := err.(*strconv.NumError)
166 return 0, 0, fmt.Errorf("bad end of range: %s: %v", parts[1], nerr.Err)
167 }
168 } else {
169 end = 1
170 }
171
172 return
173}
174
175func max(a, b int64) int64 {
176 if a > b {
177 return a
178 }
179 return b
180}