diff --git a/gitdiff/gitdiff.go b/gitdiff/gitdiff.go index e0d0a3c..a63331b 100644 --- a/gitdiff/gitdiff.go +++ b/gitdiff/gitdiff.go @@ -1,8 +1,25 @@ package gitdiff +import ( + "os" +) + // File describes changes to a single file. It can be either a text file or a // binary file. type File struct { + OldName string + NewName string + + IsNew bool + IsDelete bool + IsCopy bool + IsRename bool + + OldMode os.FileMode + NewMode os.FileMode + + Score int + Fragments []*Fragment } diff --git a/gitdiff/parser.go b/gitdiff/parser.go index b08a2b7..e59547c 100644 --- a/gitdiff/parser.go +++ b/gitdiff/parser.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "io" + "os" "regexp" "strconv" "strings" @@ -45,7 +46,7 @@ type parser struct { const ( fragmentHeaderPrefix = "@@ -" - fileHeaderPrefix = "diff --git" + fileHeaderPrefix = "diff --git " oldFilePrefix = "--- " newFilePrefix = "+++ " ) @@ -127,7 +128,26 @@ func (p *parser) ParseFileChanges(f *File) error { } func (p *parser) ParseGitFileHeader(f *File, header string) error { - panic("unimplemented") + // TODO(bkeyes): parse header line for filename + // necessary to get the filename for mode changes or add/rm empty files + + for { + line, err := p.PeekLine() + if err != nil { + return err + } + + more, err := parseGitHeaderLine(f, line) + if err != nil { + return p.Errorf("header: %v", err) + } + if !more { + break + } + p.Line() + } + + return nil } func (p *parser) ParseTraditionalFileHeader(f *File, oldFile, newFile string) error { @@ -203,3 +223,108 @@ func parseFragmentHeader(f *Fragment, header string) error { return nil } + +func parseGitHeaderLine(f *File, line string) (more bool, err error) { + match := func(s string) bool { + if strings.HasPrefix(line, s) { + // TODO(bkeyes): strip final line separator too + line = line[len(s):] + return true + } + return false + } + + switch { + case match(fragmentHeaderPrefix): + // start of a fragment indicates the end of the header + return false, nil + + case match(oldFilePrefix): + + case match(newFilePrefix): + + case match("old mode "): + if f.OldMode, err = parseModeLine(line); err != nil { + return false, err + } + + case match("new mode "): + if f.NewMode, err = parseModeLine(line); err != nil { + return false, err + } + + case match("deleted file mode "): + // TODO(bkeyes): maybe set old name from default? + f.IsDelete = true + if f.OldMode, err = parseModeLine(line); err != nil { + return false, err + } + + case match("new file mode "): + f.IsNew = true + if f.NewMode, err = parseModeLine(line); err != nil { + return false, err + } + + case match("copy from "): + f.IsCopy = true + // TODO(bkeyes): set old name + + case match("copy to "): + f.IsCopy = true + // TODO(bkeyes): set new name + + case match("rename old "): + f.IsRename = true + // TODO(bkeyes): set old name + + case match("rename new "): + f.IsRename = true + // TODO(bkeyes): set new name + + case match("rename from "): + f.IsRename = true + // TODO(bkeyes): set old name + + case match("rename to "): + f.IsRename = true + // TODO(bkeyes): set new name + + case match("similarity index "): + f.Score = parseScoreLine(line) + + case match("dissimilarity index "): + f.Score = parseScoreLine(line) + + case match("index "): + + default: + // unknown line also indicates the end of the header + return false, nil + } + + return true, nil +} + +func parseModeLine(s string) (os.FileMode, error) { + s = strings.TrimSuffix(s, "\n") + + mode, err := strconv.ParseInt(s, 8, 32) + if err != nil { + nerr := err.(*strconv.NumError) + return os.FileMode(0), fmt.Errorf("invalid mode line: %v", nerr.Err) + } + + return os.FileMode(mode), nil +} + +func parseScoreLine(s string) int { + s = strings.TrimSuffix(s, "\n") + + // gitdiff_similarity / gitdiff_dissimilarity ignore invalid scores + score, _ := strconv.ParseInt(s, 10, 32) + if score <= 100 { + return int(score) + } + return 0 +}