diff --git a/git/api/blame.go b/git/api/blame.go index c6decb35f..9889461e8 100644 --- a/git/api/blame.go +++ b/git/api/blame.go @@ -27,6 +27,8 @@ import ( "github.com/harness/gitness/errors" "github.com/harness/gitness/git/command" "github.com/harness/gitness/git/sha" + + "github.com/gotidy/ptr" ) var ( @@ -37,8 +39,14 @@ var ( ) type BlamePart struct { - Commit *Commit `json:"commit"` - Lines []string `json:"lines"` + Commit *Commit `json:"commit"` + Lines []string `json:"lines"` + Previous *BlamePartPrevious `json:"previous,omitempty"` +} + +type BlamePartPrevious struct { + CommitSHA sha.SHA `json:"commit_sha"` + FileName string `json:"file_name"` } type BlameNextReader interface { @@ -92,17 +100,22 @@ func (g *Git) Blame( }() return &BlameReader{ - scanner: bufio.NewScanner(pipeRead), - commitCache: make(map[string]*Commit), - errReader: stderr, // Any stderr output will cause the BlameReader to fail. + scanner: bufio.NewScanner(pipeRead), + cache: make(map[string]blameReaderCacheItem), + errReader: stderr, // Any stderr output will cause the BlameReader to fail. } } +type blameReaderCacheItem struct { + commit *Commit + previous *BlamePartPrevious +} + type BlameReader struct { - scanner *bufio.Scanner - lastLine string - commitCache map[string]*Commit - errReader io.Reader + scanner *bufio.Scanner + lastLine string + cache map[string]blameReaderCacheItem + errReader io.Reader } func (r *BlameReader) nextLine() (string, error) { @@ -132,6 +145,7 @@ func (r *BlameReader) unreadLine(line string) { //nolint:complexity,gocognit,nestif // it's ok func (r *BlameReader) NextPart() (*BlamePart, error) { var commit *Commit + var previous *BlamePartPrevious var lines []string var err error @@ -146,8 +160,10 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { commitSHA := sha.Must(matches[1]) if commit == nil { - commit = r.commitCache[commitSHA.String()] - if commit == nil { + if cacheItem, ok := r.cache[commitSHA.String()]; ok { + commit = cacheItem.commit + previous = cacheItem.previous + } else { commit = &Commit{SHA: commitSHA} } @@ -164,11 +180,15 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { if !commit.SHA.Equal(commitSHA) { r.unreadLine(line) - r.commitCache[commit.SHA.String()] = commit + r.cache[commit.SHA.String()] = blameReaderCacheItem{ + commit: commit, + previous: previous, + } return &BlamePart{ - Commit: commit, - Lines: lines, + Commit: commit, + Lines: lines, + Previous: previous, }, nil } @@ -187,7 +207,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { continue } - parseBlameHeaders(line, commit) + parseBlameHeaders(line, commit, &previous) } // Check if there's something in the error buffer... If yes, that's the error! @@ -223,15 +243,16 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { if commit != nil && len(lines) > 0 { part = &BlamePart{ - Commit: commit, - Lines: lines, + Commit: commit, + Lines: lines, + Previous: previous, } } return part, err } -func parseBlameHeaders(line string, commit *Commit) { +func parseBlameHeaders(line string, commit *Commit, previous **BlamePartPrevious) { // This is the list of git blame headers that we process. Other headers we ignore. const ( headerSummary = "summary " @@ -241,6 +262,7 @@ func parseBlameHeaders(line string, commit *Commit) { headerCommitterName = "committer " headerCommitterMail = "committer-mail " headerCommitterTime = "committer-time " + headerPrevious = "previous " ) switch { @@ -258,6 +280,8 @@ func parseBlameHeaders(line string, commit *Commit) { commit.Committer.Identity.Email = extractEmail(line[len(headerCommitterMail):]) case strings.HasPrefix(line, headerCommitterTime): commit.Committer.When = extractTime(line[len(headerCommitterTime):]) + case strings.HasPrefix(line, headerPrevious): + *previous = ptr.Of(extractPrevious(line[len(headerPrevious):])) } } @@ -265,6 +289,19 @@ func extractName(s string) string { return s } +// extractPrevious extracts the sha and filename of the previous commit. +// example: previous 999d2ed306a916423d18e022abe258e92419ab9a README.md +func extractPrevious(s string) BlamePartPrevious { + rawSHA, fileName, _ := strings.Cut(s, " ") + if len(fileName) > 0 && fileName[0] == '"' { + fileName, _ = strconv.Unquote(fileName) + } + return BlamePartPrevious{ + CommitSHA: sha.Must(rawSHA), + FileName: fileName, + } +} + // extractEmail extracts email from git blame output. // The email address is wrapped between "<" and ">" characters. // If "<" or ">" are not in place it returns the string as it. diff --git a/git/api/blame_test.go b/git/api/blame_test.go index 484d572e3..64fc10c1e 100644 --- a/git/api/blame_test.go +++ b/git/api/blame_test.go @@ -41,6 +41,7 @@ committer-mail committer-time 1669812989 committer-tz +0100 summary Pull request 1 +previous ec84ae5018520efdead481c81a31950b82196ec6 "\"\\n\\123\342\210\206'ex' \\\\t.\\ttxt\"" filename file_name_before_rename.go Line 10 16f267ad4f731af1b2e36f42e170ed8921377398 12 11 1 @@ -55,7 +56,7 @@ committer-mail committer-time 1673952128 committer-tz +0100 summary Pull request 2 -previous 6561a7b86e1a5e74ea0e4e73ccdfc18b486a2826 file_name.go +previous 6561a7b86e1a5e74ea0e4e73ccdfc18b486a2826 file_name.go filename file_name.go Line 12 16f267ad4f731af1b2e36f42e170ed8921377398 13 13 2 @@ -86,6 +87,10 @@ filename file_name.go When: time.Unix(1669812989, 0), }, } + previous1 := &BlamePartPrevious{ + CommitSHA: sha.Must("ec84ae5018520efdead481c81a31950b82196ec6"), + FileName: `"\n\123∆'ex' \\t.\ttxt"`, + } commit2 := &Commit{ SHA: sha.Must("dcb4b6b63e86f06ed4e4c52fbc825545dc0b6200"), @@ -100,26 +105,33 @@ filename file_name.go When: time.Unix(1673952128, 0), }, } + previous2 := &BlamePartPrevious{ + CommitSHA: sha.Must("6561a7b86e1a5e74ea0e4e73ccdfc18b486a2826"), + FileName: " file_name.go ", + } want := []*BlamePart{ { - Commit: commit1, - Lines: []string{"Line 10", "Line 11"}, + Commit: commit1, + Lines: []string{"Line 10", "Line 11"}, + Previous: previous1, }, { - Commit: commit2, - Lines: []string{"Line 12"}, + Commit: commit2, + Lines: []string{"Line 12"}, + Previous: previous2, }, { - Commit: commit1, - Lines: []string{"Line 13", "Line 14"}, + Commit: commit1, + Lines: []string{"Line 13", "Line 14"}, + Previous: previous1, }, } reader := BlameReader{ - scanner: bufio.NewScanner(strings.NewReader(blameOut)), - commitCache: make(map[string]*Commit), - errReader: strings.NewReader(""), + scanner: bufio.NewScanner(strings.NewReader(blameOut)), + cache: make(map[string]blameReaderCacheItem), + errReader: strings.NewReader(""), } var got []*BlamePart @@ -144,9 +156,9 @@ filename file_name.go func TestBlameReader_NextPart_UserError(t *testing.T) { reader := BlameReader{ - scanner: bufio.NewScanner(strings.NewReader("")), - commitCache: make(map[string]*Commit), - errReader: strings.NewReader("fatal: no such path\n"), + scanner: bufio.NewScanner(strings.NewReader("")), + cache: make(map[string]blameReaderCacheItem), + errReader: strings.NewReader("fatal: no such path\n"), } _, err := reader.NextPart() @@ -157,9 +169,9 @@ func TestBlameReader_NextPart_UserError(t *testing.T) { func TestBlameReader_NextPart_CmdError(t *testing.T) { reader := BlameReader{ - scanner: bufio.NewScanner(iotest.ErrReader(errors.New("dummy error"))), - commitCache: make(map[string]*Commit), - errReader: strings.NewReader(""), + scanner: bufio.NewScanner(iotest.ErrReader(errors.New("dummy error"))), + cache: make(map[string]blameReaderCacheItem), + errReader: strings.NewReader(""), } _, err := reader.NextPart() diff --git a/git/blame.go b/git/blame.go index c3293144d..59ba377c0 100644 --- a/git/blame.go +++ b/git/blame.go @@ -20,6 +20,7 @@ import ( "io" "github.com/harness/gitness/errors" + "github.com/harness/gitness/git/sha" ) type BlameParams struct { @@ -65,8 +66,14 @@ func (params *BlameParams) Validate() error { } type BlamePart struct { - Commit *Commit `json:"commit"` - Lines []string `json:"lines"` + Commit *Commit `json:"commit"` + Lines []string `json:"lines"` + Previous *BlamePartPrevious `json:"previous,omitempty"` +} + +type BlamePartPrevious struct { + CommitSHA sha.SHA `json:"commit_sha"` + FileName string `json:"file_name"` } // Blame processes and streams the git blame output data. @@ -94,7 +101,6 @@ func (s *Service) Blame(ctx context.Context, params *BlameParams) (<-chan *Blame for { part, errRead := reader.NextPart() - if part == nil { return } @@ -108,7 +114,18 @@ func (s *Service) Blame(ctx context.Context, params *BlameParams) (<-chan *Blame lines := make([]string, len(part.Lines)) copy(lines, part.Lines) - ch <- &BlamePart{Commit: commit, Lines: lines} + next := &BlamePart{ + Commit: commit, + Lines: lines, + } + if part.Previous != nil { + next.Previous = &BlamePartPrevious{ + CommitSHA: part.Previous.CommitSHA, + FileName: part.Previous.FileName, + } + } + + ch <- next if errRead != nil && errors.Is(errRead, io.EOF) { return