git-module

git clone git://git.lin.moe/fork/git-module.git

  1package git
  2
  3import (
  4	"bufio"
  5	"bytes"
  6	"errors"
  7	"fmt"
  8	"io"
  9	"strconv"
 10	"strings"
 11)
 12
 13// DiffLineType is the line type in diff.
 14type DiffLineType uint8
 15
 16// A list of different line types.
 17const (
 18	DiffLinePlain DiffLineType = iota + 1
 19	DiffLineAdd
 20	DiffLineDelete
 21	DiffLineSection
 22)
 23
 24// DiffFileType is the file status in diff.
 25type DiffFileType uint8
 26
 27// A list of different file statuses.
 28const (
 29	DiffFileAdd DiffFileType = iota + 1
 30	DiffFileChange
 31	DiffFileDelete
 32	DiffFileRename
 33)
 34
 35// DiffLine represents a line in diff.
 36type DiffLine struct {
 37	Type      DiffLineType // The type of the line
 38	Content   string       // The content of the line
 39	LeftLine  int          // The left line number
 40	RightLine int          // The right line number
 41}
 42
 43// DiffSection represents a section in diff.
 44type DiffSection struct {
 45	Lines []*DiffLine // lines in the section
 46
 47	numAdditions int
 48	numDeletions int
 49}
 50
 51// NumLines returns the number of lines in the section.
 52func (s *DiffSection) NumLines() int {
 53	return len(s.Lines)
 54}
 55
 56// Line returns a specific line by given type and line number in a section.
 57func (s *DiffSection) Line(typ DiffLineType, line int) *DiffLine {
 58	var (
 59		difference      = 0
 60		addCount        = 0
 61		delCount        = 0
 62		matchedDiffLine *DiffLine
 63	)
 64
 65loop:
 66	for _, diffLine := range s.Lines {
 67		switch diffLine.Type {
 68		case DiffLineAdd:
 69			addCount++
 70		case DiffLineDelete:
 71			delCount++
 72		default:
 73			if matchedDiffLine != nil {
 74				break loop
 75			}
 76			difference = diffLine.RightLine - diffLine.LeftLine
 77			addCount = 0
 78			delCount = 0
 79		}
 80
 81		switch typ {
 82		case DiffLineDelete:
 83			if diffLine.RightLine == 0 && diffLine.LeftLine == line-difference {
 84				matchedDiffLine = diffLine
 85			}
 86		case DiffLineAdd:
 87			if diffLine.LeftLine == 0 && diffLine.RightLine == line+difference {
 88				matchedDiffLine = diffLine
 89			}
 90		}
 91	}
 92
 93	if addCount == delCount {
 94		return matchedDiffLine
 95	}
 96	return nil
 97}
 98
 99// DiffFile represents a file in diff.
100type DiffFile struct {
101	// The name of the file.
102	Name string
103	// The type of the file.
104	Type DiffFileType
105	// The index (SHA1 or SHA256 hash) of the file. For a changed/new file, it is the new SHA,
106	// and for a deleted file it becomes "000000".
107	Index string
108	// OldIndex is the old index (SHA1 or SHA256 hash) of the file.
109	OldIndex string
110	// The sections in the file.
111	Sections []*DiffSection
112
113	numAdditions int
114	numDeletions int
115
116	oldName string
117
118	mode    EntryMode
119	oldMode EntryMode
120
121	isBinary     bool
122	isSubmodule  bool
123	isIncomplete bool
124}
125
126// NumSections returns the number of sections in the file.
127func (f *DiffFile) NumSections() int {
128	return len(f.Sections)
129}
130
131// NumAdditions returns the number of additions in the file.
132func (f *DiffFile) NumAdditions() int {
133	return f.numAdditions
134}
135
136// NumDeletions returns the number of deletions in the file.
137func (f *DiffFile) NumDeletions() int {
138	return f.numDeletions
139}
140
141// IsCreated returns true if the file is newly created.
142func (f *DiffFile) IsCreated() bool {
143	return f.Type == DiffFileAdd
144}
145
146// IsDeleted returns true if the file has been deleted.
147func (f *DiffFile) IsDeleted() bool {
148	return f.Type == DiffFileDelete
149}
150
151// IsRenamed returns true if the file has been renamed.
152func (f *DiffFile) IsRenamed() bool {
153	return f.Type == DiffFileRename
154}
155
156// OldName returns previous name before renaming.
157func (f *DiffFile) OldName() string {
158	return f.oldName
159}
160
161// Mode returns the mode of the file.
162func (f *DiffFile) Mode() EntryMode {
163	return f.mode
164}
165
166// OldMode returns the old mode of the file if it's changed.
167func (f *DiffFile) OldMode() EntryMode {
168	return f.oldMode
169}
170
171// IsBinary returns true if the file is in binary format.
172func (f *DiffFile) IsBinary() bool {
173	return f.isBinary
174}
175
176// IsSubmodule returns true if the file contains information of a submodule.
177func (f *DiffFile) IsSubmodule() bool {
178	return f.isSubmodule
179}
180
181// IsIncomplete returns true if the file is incomplete to the file diff.
182func (f *DiffFile) IsIncomplete() bool {
183	return f.isIncomplete
184}
185
186// Diff represents a Git diff.
187type Diff struct {
188	Files []*DiffFile // The files in the diff
189
190	totalAdditions int
191	totalDeletions int
192
193	isIncomplete bool
194}
195
196// NumFiles returns the number of files in the diff.
197func (d *Diff) NumFiles() int {
198	return len(d.Files)
199}
200
201// TotalAdditions returns the total additions in the diff.
202func (d *Diff) TotalAdditions() int {
203	return d.totalAdditions
204}
205
206// TotalDeletions returns the total deletions in the diff.
207func (d *Diff) TotalDeletions() int {
208	return d.totalDeletions
209}
210
211// IsIncomplete returns true if the file is incomplete to the entire diff.
212func (d *Diff) IsIncomplete() bool {
213	return d.isIncomplete
214}
215
216// SteamParseDiffResult contains results of streaming parsing a diff.
217type SteamParseDiffResult struct {
218	Diff *Diff
219	Err  error
220}
221
222type diffParser struct {
223	*bufio.Reader
224	maxFiles     int
225	maxFileLines int
226	maxLineChars int
227
228	// The next line that hasn't been processed. It is used to determine what kind
229	// of process should go in.
230	buffer []byte
231	isEOF  bool
232}
233
234func (p *diffParser) readLine() error {
235	if p.buffer != nil {
236		return nil
237	}
238
239	var err error
240	p.buffer, err = p.ReadBytes('\n')
241	if err != nil {
242		if err != io.EOF {
243			return fmt.Errorf("read string: %v", err)
244		}
245
246		p.isEOF = true
247	}
248
249	// Remove line break
250	if len(p.buffer) > 0 && p.buffer[len(p.buffer)-1] == '\n' {
251		p.buffer = p.buffer[:len(p.buffer)-1]
252	}
253	return nil
254}
255
256var diffHead = []byte("diff --git ")
257
258func (p *diffParser) parseFileHeader() (*DiffFile, error) {
259	line := string(p.buffer)
260	p.buffer = nil
261
262	// NOTE: In case file name is surrounded by double quotes (it happens only in
263	// git-shell). e.g. diff --git "a/xxx" "b/xxx"
264	var middle int
265	hasQuote := line[len(diffHead)] == '"'
266	if hasQuote {
267		middle = strings.Index(line, ` "b/`)
268	} else {
269		middle = strings.Index(line, ` b/`)
270	}
271
272	beg := len(diffHead)
273	a := line[beg+2 : middle]
274	b := line[middle+3:]
275	if hasQuote {
276		a = string(UnescapeChars([]byte(a[1 : len(a)-1])))
277		b = string(UnescapeChars([]byte(b[1 : len(b)-1])))
278	}
279
280	file := &DiffFile{
281		Name:    a,
282		oldName: b,
283		Type:    DiffFileChange,
284	}
285
286	// Check file diff type and submodule
287	var err error
288checkType:
289	for !p.isEOF {
290		if err = p.readLine(); err != nil {
291			return nil, err
292		}
293
294		line := string(p.buffer)
295		p.buffer = nil
296
297		if len(line) == 0 {
298			continue
299		}
300
301		switch {
302		case strings.HasPrefix(line, "new file"):
303			file.Type = DiffFileAdd
304			file.isSubmodule = strings.HasSuffix(line, " 160000")
305			fields := strings.Fields(line)
306			if len(fields) > 0 {
307				mode, _ := strconv.ParseUint(fields[len(fields)-1], 8, 64)
308				file.mode = EntryMode(mode)
309				if file.oldMode == 0 {
310					file.oldMode = file.mode
311				}
312			}
313		case strings.HasPrefix(line, "deleted"):
314			file.Type = DiffFileDelete
315			file.isSubmodule = strings.HasSuffix(line, " 160000")
316			fields := strings.Fields(line)
317			if len(fields) > 0 {
318				mode, _ := strconv.ParseUint(fields[len(fields)-1], 8, 64)
319				file.mode = EntryMode(mode)
320				if file.oldMode == 0 {
321					file.oldMode = file.mode
322				}
323			}
324		case strings.HasPrefix(line, "index"): // e.g. index ee791be..9997571 100644
325			fields := strings.Fields(line[6:])
326			shas := strings.Split(fields[0], "..")
327			if len(shas) != 2 {
328				return nil, errors.New("malformed index: expect two SHAs in the form of <old>..<new>")
329			}
330
331			file.OldIndex = shas[0]
332			file.Index = shas[1]
333			if len(fields) > 1 {
334				mode, _ := strconv.ParseUint(fields[1], 8, 64)
335				file.mode = EntryMode(mode)
336				file.oldMode = EntryMode(mode)
337			}
338			break checkType
339		case strings.HasPrefix(line, "similarity index "):
340			file.Type = DiffFileRename
341			file.oldName = a
342			file.Name = b
343
344			// No need to look for index if it's a pure rename
345			if strings.HasSuffix(line, "100%") {
346				break checkType
347			}
348		case strings.HasPrefix(line, "new mode"):
349			fields := strings.Fields(line)
350			if len(fields) > 0 {
351				mode, _ := strconv.ParseUint(fields[len(fields)-1], 8, 64)
352				file.mode = EntryMode(mode)
353			}
354		case strings.HasPrefix(line, "old mode"):
355			fields := strings.Fields(line)
356			if len(fields) > 0 {
357				mode, _ := strconv.ParseUint(fields[len(fields)-1], 8, 64)
358				file.oldMode = EntryMode(mode)
359			}
360		}
361	}
362
363	return file, nil
364}
365
366func (p *diffParser) parseSection() (_ *DiffSection, isIncomplete bool, _ error) {
367	line := string(p.buffer)
368	p.buffer = nil
369
370	section := &DiffSection{
371		Lines: []*DiffLine{
372			{
373				Type:    DiffLineSection,
374				Content: line,
375			},
376		},
377	}
378
379	// Parse line number, e.g. @@ -0,0 +1,3 @@
380	var leftLine, rightLine int
381	ss := strings.Split(line, "@@")
382	ranges := strings.Split(ss[1][1:], " ")
383	leftLine, _ = strconv.Atoi(strings.Split(ranges[0], ",")[0][1:])
384	if len(ranges) > 1 {
385		rightLine, _ = strconv.Atoi(strings.Split(ranges[1], ",")[0])
386	} else {
387		rightLine = leftLine
388	}
389
390	var err error
391	for !p.isEOF {
392		if err = p.readLine(); err != nil {
393			return nil, false, err
394		}
395
396		if len(p.buffer) == 0 {
397			p.buffer = nil
398			continue
399		}
400
401		// Make sure we're still in the section. If not, we're done with this section.
402		if p.buffer[0] != ' ' &&
403			p.buffer[0] != '+' &&
404			p.buffer[0] != '-' {
405
406			// No new line indicator
407			if p.buffer[0] == '\\' &&
408				bytes.HasPrefix(p.buffer, []byte(`\ No newline at end of file`)) {
409				p.buffer = nil
410				continue
411			}
412			return section, false, nil
413		}
414
415		line := string(p.buffer)
416		p.buffer = nil
417
418		// Too many characters in a single diff line
419		if p.maxLineChars > 0 && len(line) > p.maxLineChars {
420			return section, true, nil
421		}
422
423		switch line[0] {
424		case ' ':
425			section.Lines = append(section.Lines, &DiffLine{
426				Type:      DiffLinePlain,
427				Content:   line,
428				LeftLine:  leftLine,
429				RightLine: rightLine,
430			})
431			leftLine++
432			rightLine++
433		case '+':
434			section.Lines = append(section.Lines, &DiffLine{
435				Type:      DiffLineAdd,
436				Content:   line,
437				RightLine: rightLine,
438			})
439			section.numAdditions++
440			rightLine++
441		case '-':
442			section.Lines = append(section.Lines, &DiffLine{
443				Type:     DiffLineDelete,
444				Content:  line,
445				LeftLine: leftLine,
446			})
447			section.numDeletions++
448			if leftLine > 0 {
449				leftLine++
450			}
451		}
452	}
453
454	return section, false, nil
455}
456
457func (p *diffParser) parse() (*Diff, error) {
458	diff := new(Diff)
459	file := new(DiffFile)
460	currentFileLines := 0
461
462	var err error
463	for !p.isEOF {
464		if err = p.readLine(); err != nil {
465			return nil, err
466		}
467
468		if len(p.buffer) == 0 ||
469			bytes.HasPrefix(p.buffer, []byte("+++ ")) ||
470			bytes.HasPrefix(p.buffer, []byte("--- ")) {
471			p.buffer = nil
472			continue
473		}
474
475		// Found new file
476		if bytes.HasPrefix(p.buffer, diffHead) {
477			// Check if reached maximum number of files
478			if p.maxFiles > 0 && len(diff.Files) >= p.maxFiles {
479				diff.isIncomplete = true
480				_, _ = io.Copy(io.Discard, p)
481				break
482			}
483
484			file, err = p.parseFileHeader()
485			if err != nil {
486				return nil, err
487			}
488			diff.Files = append(diff.Files, file)
489
490			currentFileLines = 0
491			continue
492		}
493
494		if file == nil || file.isIncomplete {
495			p.buffer = nil
496			continue
497		}
498
499		if bytes.HasPrefix(p.buffer, []byte("Binary")) {
500			p.buffer = nil
501			file.isBinary = true
502			continue
503		}
504
505		// Loop until we found section header
506		if p.buffer[0] != '@' {
507			p.buffer = nil
508			continue
509		}
510
511		// Too many diff lines for the file
512		if p.maxFileLines > 0 && currentFileLines > p.maxFileLines {
513			file.isIncomplete = true
514			diff.isIncomplete = true
515			continue
516		}
517
518		section, isIncomplete, err := p.parseSection()
519		if err != nil {
520			return nil, err
521		}
522		file.Sections = append(file.Sections, section)
523		file.numAdditions += section.numAdditions
524		file.numDeletions += section.numDeletions
525		diff.totalAdditions += section.numAdditions
526		diff.totalDeletions += section.numDeletions
527		currentFileLines += section.NumLines()
528		if isIncomplete {
529			file.isIncomplete = true
530			diff.isIncomplete = true
531		}
532	}
533
534	return diff, nil
535}
536
537// StreamParseDiff parses the diff read from the given io.Reader. It does
538// parse-on-read to minimize the time spent on huge diffs. It accepts a channel
539// to notify and send error (if any) to the caller when the process is done.
540// Therefore, this method should be called in a goroutine asynchronously.
541func StreamParseDiff(r io.Reader, done chan<- SteamParseDiffResult, maxFiles, maxFileLines, maxLineChars int) {
542	p := &diffParser{
543		Reader:       bufio.NewReader(r),
544		maxFiles:     maxFiles,
545		maxFileLines: maxFileLines,
546		maxLineChars: maxLineChars,
547	}
548	diff, err := p.parse()
549	done <- SteamParseDiffResult{
550		Diff: diff,
551		Err:  err,
552	}
553}