1package git23import (4 "bufio"5 "bytes"6 "errors"7 "fmt"8 "io"9 "strconv"10 "strings"11)1213// DiffLineType is the line type in diff.14type DiffLineType uint81516// A list of different line types.17const (18 DiffLinePlain DiffLineType = iota + 119 DiffLineAdd20 DiffLineDelete21 DiffLineSection22)2324// DiffFileType is the file status in diff.25type DiffFileType uint82627// A list of different file statuses.28const (29 DiffFileAdd DiffFileType = iota + 130 DiffFileChange31 DiffFileDelete32 DiffFileRename33)3435// DiffLine represents a line in diff.36type DiffLine struct {37 Type DiffLineType // The type of the line38 Content string // The content of the line39 LeftLine int // The left line number40 RightLine int // The right line number41}4243// DiffSection represents a section in diff.44type DiffSection struct {45 Lines []*DiffLine // lines in the section4647 numAdditions int48 numDeletions int49}5051// NumLines returns the number of lines in the section.52func (s *DiffSection) NumLines() int {53 return len(s.Lines)54}5556// 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 = 060 addCount = 061 delCount = 062 matchedDiffLine *DiffLine63 )6465loop: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 loop75 }76 difference = diffLine.RightLine - diffLine.LeftLine77 addCount = 078 delCount = 079 }8081 switch typ {82 case DiffLineDelete:83 if diffLine.RightLine == 0 && diffLine.LeftLine == line-difference {84 matchedDiffLine = diffLine85 }86 case DiffLineAdd:87 if diffLine.LeftLine == 0 && diffLine.RightLine == line+difference {88 matchedDiffLine = diffLine89 }90 }91 }9293 if addCount == delCount {94 return matchedDiffLine95 }96 return nil97}9899// DiffFile represents a file in diff.100type DiffFile struct {101 // The name of the file.102 Name string103 // The type of the file.104 Type DiffFileType105 // 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 string108 // OldIndex is the old index (SHA1 or SHA256 hash) of the file.109 OldIndex string110 // The sections in the file.111 Sections []*DiffSection112113 numAdditions int114 numDeletions int115116 oldName string117118 mode EntryMode119 oldMode EntryMode120121 isBinary bool122 isSubmodule bool123 isIncomplete bool124}125126// NumSections returns the number of sections in the file.127func (f *DiffFile) NumSections() int {128 return len(f.Sections)129}130131// NumAdditions returns the number of additions in the file.132func (f *DiffFile) NumAdditions() int {133 return f.numAdditions134}135136// NumDeletions returns the number of deletions in the file.137func (f *DiffFile) NumDeletions() int {138 return f.numDeletions139}140141// IsCreated returns true if the file is newly created.142func (f *DiffFile) IsCreated() bool {143 return f.Type == DiffFileAdd144}145146// IsDeleted returns true if the file has been deleted.147func (f *DiffFile) IsDeleted() bool {148 return f.Type == DiffFileDelete149}150151// IsRenamed returns true if the file has been renamed.152func (f *DiffFile) IsRenamed() bool {153 return f.Type == DiffFileRename154}155156// OldName returns previous name before renaming.157func (f *DiffFile) OldName() string {158 return f.oldName159}160161// Mode returns the mode of the file.162func (f *DiffFile) Mode() EntryMode {163 return f.mode164}165166// OldMode returns the old mode of the file if it's changed.167func (f *DiffFile) OldMode() EntryMode {168 return f.oldMode169}170171// IsBinary returns true if the file is in binary format.172func (f *DiffFile) IsBinary() bool {173 return f.isBinary174}175176// IsSubmodule returns true if the file contains information of a submodule.177func (f *DiffFile) IsSubmodule() bool {178 return f.isSubmodule179}180181// IsIncomplete returns true if the file is incomplete to the file diff.182func (f *DiffFile) IsIncomplete() bool {183 return f.isIncomplete184}185186// Diff represents a Git diff.187type Diff struct {188 Files []*DiffFile // The files in the diff189190 totalAdditions int191 totalDeletions int192193 isIncomplete bool194}195196// NumFiles returns the number of files in the diff.197func (d *Diff) NumFiles() int {198 return len(d.Files)199}200201// TotalAdditions returns the total additions in the diff.202func (d *Diff) TotalAdditions() int {203 return d.totalAdditions204}205206// TotalDeletions returns the total deletions in the diff.207func (d *Diff) TotalDeletions() int {208 return d.totalDeletions209}210211// IsIncomplete returns true if the file is incomplete to the entire diff.212func (d *Diff) IsIncomplete() bool {213 return d.isIncomplete214}215216// SteamParseDiffResult contains results of streaming parsing a diff.217type SteamParseDiffResult struct {218 Diff *Diff219 Err error220}221222type diffParser struct {223 *bufio.Reader224 maxFiles int225 maxFileLines int226 maxLineChars int227228 // The next line that hasn't been processed. It is used to determine what kind229 // of process should go in.230 buffer []byte231 isEOF bool232}233234func (p *diffParser) readLine() error {235 if p.buffer != nil {236 return nil237 }238239 var err error240 p.buffer, err = p.ReadBytes('\n')241 if err != nil {242 if err != io.EOF {243 return fmt.Errorf("read string: %v", err)244 }245246 p.isEOF = true247 }248249 // Remove line break250 if len(p.buffer) > 0 && p.buffer[len(p.buffer)-1] == '\n' {251 p.buffer = p.buffer[:len(p.buffer)-1]252 }253 return nil254}255256var diffHead = []byte("diff --git ")257258func (p *diffParser) parseFileHeader() (*DiffFile, error) {259 line := string(p.buffer)260 p.buffer = nil261262 // NOTE: In case file name is surrounded by double quotes (it happens only in263 // git-shell). e.g. diff --git "a/xxx" "b/xxx"264 var middle int265 hasQuote := line[len(diffHead)] == '"'266 if hasQuote {267 middle = strings.Index(line, ` "b/`)268 } else {269 middle = strings.Index(line, ` b/`)270 }271272 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 }279280 file := &DiffFile{281 Name: a,282 oldName: b,283 Type: DiffFileChange,284 }285286 // Check file diff type and submodule287 var err error288checkType:289 for !p.isEOF {290 if err = p.readLine(); err != nil {291 return nil, err292 }293294 line := string(p.buffer)295 p.buffer = nil296297 if len(line) == 0 {298 continue299 }300301 switch {302 case strings.HasPrefix(line, "new file"):303 file.Type = DiffFileAdd304 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.mode311 }312 }313 case strings.HasPrefix(line, "deleted"):314 file.Type = DiffFileDelete315 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.mode322 }323 }324 case strings.HasPrefix(line, "index"): // e.g. index ee791be..9997571 100644325 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 }330331 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 checkType339 case strings.HasPrefix(line, "similarity index "):340 file.Type = DiffFileRename341 file.oldName = a342 file.Name = b343344 // No need to look for index if it's a pure rename345 if strings.HasSuffix(line, "100%") {346 break checkType347 }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 }362363 return file, nil364}365366func (p *diffParser) parseSection() (_ *DiffSection, isIncomplete bool, _ error) {367 line := string(p.buffer)368 p.buffer = nil369370 section := &DiffSection{371 Lines: []*DiffLine{372 {373 Type: DiffLineSection,374 Content: line,375 },376 },377 }378379 // Parse line number, e.g. @@ -0,0 +1,3 @@380 var leftLine, rightLine int381 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 = leftLine388 }389390 var err error391 for !p.isEOF {392 if err = p.readLine(); err != nil {393 return nil, false, err394 }395396 if len(p.buffer) == 0 {397 p.buffer = nil398 continue399 }400401 // 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] != '-' {405406 // No new line indicator407 if p.buffer[0] == '\\' &&408 bytes.HasPrefix(p.buffer, []byte(`\ No newline at end of file`)) {409 p.buffer = nil410 continue411 }412 return section, false, nil413 }414415 line := string(p.buffer)416 p.buffer = nil417418 // Too many characters in a single diff line419 if p.maxLineChars > 0 && len(line) > p.maxLineChars {420 return section, true, nil421 }422423 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 }453454 return section, false, nil455}456457func (p *diffParser) parse() (*Diff, error) {458 diff := new(Diff)459 file := new(DiffFile)460 currentFileLines := 0461462 var err error463 for !p.isEOF {464 if err = p.readLine(); err != nil {465 return nil, err466 }467468 if len(p.buffer) == 0 ||469 bytes.HasPrefix(p.buffer, []byte("+++ ")) ||470 bytes.HasPrefix(p.buffer, []byte("--- ")) {471 p.buffer = nil472 continue473 }474475 // Found new file476 if bytes.HasPrefix(p.buffer, diffHead) {477 // Check if reached maximum number of files478 if p.maxFiles > 0 && len(diff.Files) >= p.maxFiles {479 diff.isIncomplete = true480 _, _ = io.Copy(io.Discard, p)481 break482 }483484 file, err = p.parseFileHeader()485 if err != nil {486 return nil, err487 }488 diff.Files = append(diff.Files, file)489490 currentFileLines = 0491 continue492 }493494 if file == nil || file.isIncomplete {495 p.buffer = nil496 continue497 }498499 if bytes.HasPrefix(p.buffer, []byte("Binary")) {500 p.buffer = nil501 file.isBinary = true502 continue503 }504505 // Loop until we found section header506 if p.buffer[0] != '@' {507 p.buffer = nil508 continue509 }510511 // Too many diff lines for the file512 if p.maxFileLines > 0 && currentFileLines > p.maxFileLines {513 file.isIncomplete = true514 diff.isIncomplete = true515 continue516 }517518 section, isIncomplete, err := p.parseSection()519 if err != nil {520 return nil, err521 }522 file.Sections = append(file.Sections, section)523 file.numAdditions += section.numAdditions524 file.numDeletions += section.numDeletions525 diff.totalAdditions += section.numAdditions526 diff.totalDeletions += section.numDeletions527 currentFileLines += section.NumLines()528 if isIncomplete {529 file.isIncomplete = true530 diff.isIncomplete = true531 }532 }533534 return diff, nil535}536537// StreamParseDiff parses the diff read from the given io.Reader. It does538// parse-on-read to minimize the time spent on huge diffs. It accepts a channel539// 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}