mirror of
https://github.com/bluekeyes/go-gitdiff
synced 2026-06-08 13:18:30 +00:00
ccf0c58db8
Call Next() to advance the parser state until it returns a non-nil error, then check if the error is io.EOF. This makes EOF handling easier and also means that Line() and PeekLine() can be called multiple times without changing state. The next step is to update parse functions to return an internal marker error if they are called on an invalid line. This should improve correctness and remove the duplication of testing a condition and then calling a parse function, which checks the same condition.
224 lines
5.7 KiB
Go
224 lines
5.7 KiB
Go
package gitdiff
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Parse parses a patch with changes for one or more files. Any content
|
|
// preceding the first file header is ignored. If an error occurs while
|
|
// parsing, files will contain all files parsed before the error.
|
|
func Parse(r io.Reader) (files []*File, err error) {
|
|
p := &parser{r: bufio.NewReader(r)}
|
|
|
|
var file *File
|
|
for {
|
|
file, err = p.ParseNextFileHeader()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if file == nil {
|
|
break
|
|
}
|
|
|
|
err = p.ParseFileChanges(file)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
files = append(files, file)
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
|
|
// TODO(bkeyes): consider exporting the parser type with configuration
|
|
// this would enable OID validation, p-value guessing, and prefix stripping
|
|
// by allowing users to set or override defaults
|
|
|
|
type parser struct {
|
|
r *bufio.Reader
|
|
|
|
eof bool
|
|
lineno int64
|
|
lines [2]string
|
|
}
|
|
|
|
const (
|
|
fragmentHeaderPrefix = "@@ -"
|
|
|
|
fileHeaderPrefix = "diff --git "
|
|
oldFilePrefix = "--- "
|
|
newFilePrefix = "+++ "
|
|
|
|
devNull = "/dev/null"
|
|
)
|
|
|
|
// ParseNextFileHeader finds and parses the next file header in the stream. It
|
|
// returns nil if no headers are found before the end of the stream.
|
|
func (p *parser) ParseNextFileHeader() (file *File, err error) {
|
|
// based on find_header() in git/apply.c
|
|
|
|
for err = p.Next(); err == nil; err = p.Next() {
|
|
line := p.Line()
|
|
|
|
// check for disconnected fragment headers (corrupt patch)
|
|
if isMaybeFragmentHeader(line) {
|
|
var frag Fragment
|
|
if err := parseFragmentHeader(&frag, line); err != nil {
|
|
// not a valid header, nothing to worry about
|
|
continue
|
|
}
|
|
return nil, p.Errorf(0, "patch fragment without header: %s", line)
|
|
}
|
|
|
|
// check for a git-generated patch
|
|
if strings.HasPrefix(line, fileHeaderPrefix) {
|
|
file = new(File)
|
|
if err := p.ParseGitFileHeader(file, line); err != nil {
|
|
return nil, err
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
// check for a "traditional" patch
|
|
if strings.HasPrefix(line, oldFilePrefix) && strings.HasPrefix(p.PeekLine(), newFilePrefix) {
|
|
if err = p.Next(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
oldFileLine := line
|
|
newFileLine := p.Line()
|
|
|
|
// only a file header if followed by a (probable) unified fragment header
|
|
if !isMaybeFragmentHeader(p.PeekLine()) {
|
|
continue
|
|
}
|
|
|
|
file = new(File)
|
|
if err := p.ParseTraditionalFileHeader(file, oldFileLine, newFileLine); err != nil {
|
|
return nil, err
|
|
}
|
|
return file, nil
|
|
}
|
|
}
|
|
|
|
if err != nil && err != io.EOF {
|
|
return nil, err
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
// ParseFileChanges parses file changes until the next file header or the end
|
|
// of the stream and attaches them to the given file.
|
|
func (p *parser) ParseFileChanges(f *File) error {
|
|
panic("TODO(bkeyes): unimplemented")
|
|
}
|
|
|
|
// Next advances the parser by one line. It returns any error encountered while
|
|
// reading the line, including io.EOF when the end of stream is reached.
|
|
func (p *parser) Next() error {
|
|
if p.eof {
|
|
p.lines[0] = ""
|
|
return io.EOF
|
|
}
|
|
|
|
if p.lineno == 0 {
|
|
// on the first call, need extra shift to initialize both slots
|
|
if err := p.shiftLines(); err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err := p.shiftLines()
|
|
if err == io.EOF {
|
|
p.eof = p.lines[1] == ""
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.lineno++
|
|
return nil
|
|
}
|
|
|
|
func (p *parser) shiftLines() (err error) {
|
|
p.lines[0] = p.lines[1]
|
|
p.lines[1], err = p.r.ReadString('\n')
|
|
return
|
|
}
|
|
|
|
// Line returns the current line or an empty string if Next has returned io.EOF.
|
|
func (p *parser) Line() string {
|
|
return p.lines[0]
|
|
}
|
|
|
|
// PeekLine returns the line following the current line or an empty string if
|
|
// the current line is the final line. If PeekLine returns an empty string,
|
|
// Next will return io.EOF on the next call.
|
|
func (p *parser) PeekLine() string {
|
|
return p.lines[1]
|
|
}
|
|
|
|
// Errorf generates an error and appends the current line information.
|
|
func (p *parser) Errorf(delta int64, msg string, args ...interface{}) error {
|
|
return fmt.Errorf("gitdiff: line %d: %s", p.lineno+delta, fmt.Sprintf(msg, args...))
|
|
}
|
|
|
|
func isMaybeFragmentHeader(line string) bool {
|
|
const shortestValidHeader = "@@ -0,0 +1 @@\n"
|
|
return len(line) >= len(shortestValidHeader) && strings.HasPrefix(line, fragmentHeaderPrefix)
|
|
}
|
|
|
|
// TODO(bkeyes): fix duplication with isMaybeFragmentHeader
|
|
func parseFragmentHeader(f *Fragment, header string) (err error) {
|
|
const startMark = "@@ "
|
|
const endMark = " @@"
|
|
|
|
parts := strings.SplitAfterN(header, endMark, 2)
|
|
if len(parts) < 2 || !strings.HasPrefix(parts[0], startMark) || !strings.HasSuffix(parts[0], endMark) {
|
|
return fmt.Errorf("invalid fragment header")
|
|
}
|
|
|
|
header = parts[0][len(startMark) : len(parts[0])-len(endMark)]
|
|
f.Comment = strings.TrimSpace(parts[1])
|
|
|
|
ranges := strings.Split(header, " ")
|
|
if len(ranges) != 2 {
|
|
return fmt.Errorf("invalid fragment header")
|
|
}
|
|
|
|
if !strings.HasPrefix(ranges[0], "-") || !strings.HasPrefix(ranges[1], "+") {
|
|
return fmt.Errorf("invalid fragment header: bad range marker")
|
|
}
|
|
if f.OldPosition, f.OldLines, err = parseRange(ranges[0][1:]); err != nil {
|
|
return fmt.Errorf("invalid fragment header: %v", err)
|
|
}
|
|
if f.NewPosition, f.NewLines, err = parseRange(ranges[1][1:]); err != nil {
|
|
return fmt.Errorf("invalid fragment header: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseRange(s string) (start int64, end int64, err error) {
|
|
parts := strings.SplitN(s, ",", 2)
|
|
|
|
if start, err = strconv.ParseInt(parts[0], 10, 64); err != nil {
|
|
nerr := err.(*strconv.NumError)
|
|
return 0, 0, fmt.Errorf("bad start of range: %s: %v", parts[0], nerr.Err)
|
|
}
|
|
|
|
if len(parts) > 1 {
|
|
if end, err = strconv.ParseInt(parts[1], 10, 64); err != nil {
|
|
nerr := err.(*strconv.NumError)
|
|
return 0, 0, fmt.Errorf("bad end of range: %s: %v", parts[1], nerr.Err)
|
|
}
|
|
} else {
|
|
end = 1
|
|
}
|
|
|
|
return
|
|
}
|