master
  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}