feat: [PIPE-22535]: Include previous commit in blame (#2812)

pull/3576/head
Johannes Batzill 2024-10-17 18:21:51 +00:00 committed by Harness
parent ac8eb9ff37
commit 5cbd33bd5d
3 changed files with 104 additions and 38 deletions

View File

@ -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.

View File

@ -41,6 +41,7 @@ committer-mail <noreply@harness.io>
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 <noreply@harness.io>
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()

View File

@ -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