Skip to content

Commit 1ff5b56

Browse files
authored
Simplify Git blame implementation (#63)
1 parent 1c4ac94 commit 1ff5b56

File tree

7 files changed

+105
-245
lines changed

7 files changed

+105
-245
lines changed

.github/workflows/go.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
name: Test
2020
strategy:
2121
matrix:
22-
go-version: [1.13.x, 1.14.x]
22+
go-version: [1.14.x, 1.15.x]
2323
platform: [ubuntu-latest, macos-latest, windows-latest]
2424
runs-on: ${{ matrix.platform }}
2525
steps:

blame.go

+9-136
Original file line numberDiff line numberDiff line change
@@ -4,143 +4,16 @@
44

55
package git
66

7-
import (
8-
"bufio"
9-
"bytes"
10-
"strconv"
11-
"strings"
12-
"time"
13-
)
14-
15-
type (
16-
Blame struct {
17-
commits map[int]*Commit
18-
}
19-
BlameOptions struct {
20-
}
21-
)
22-
23-
// BlameFile returns map of line number and the Commit changed that line.
24-
func (r *Repository) BlameFile(rev, file string, opts ...BlameOptions) (*Blame, error) {
25-
cmd := NewCommand("blame", "-p", rev, "--", file)
26-
stdout, err := cmd.RunInDir(r.path)
27-
if err != nil {
28-
return nil, err
29-
}
30-
return BlameContent(stdout)
31-
}
32-
33-
// BlameContent parse content of `git blame` in porcelain format
34-
func BlameContent(content []byte) (*Blame, error) {
35-
var commits = make(map[[20]byte]*Commit)
36-
var commit = &Commit{}
37-
var details = make(map[string]string)
38-
var result = createBlame()
39-
scanner := bufio.NewScanner(bytes.NewReader(content))
40-
for scanner.Scan() {
41-
line := scanner.Text()
42-
if string(line[0]) != "\t" {
43-
words := strings.Fields(line)
44-
sha, err := NewIDFromString(words[0])
45-
if err == nil {
46-
// SHA and rows numbers line
47-
commit = getCommit(sha, commits)
48-
commit.fill(details)
49-
details = make(map[string]string) // empty all details
50-
i, err := strconv.Atoi(words[2])
51-
if err != nil {
52-
return nil, err
53-
}
54-
result.commits[i] = commit
55-
} else {
56-
// commit details line
57-
switch words[0] {
58-
case "summary":
59-
commit.Message = line[len(words[0])+1:]
60-
case "previous":
61-
commit.parents = []*SHA1{MustIDFromString(words[1])}
62-
default:
63-
if len(words) > 1 {
64-
details[words[0]] = line[len(words[0])+1:]
65-
}
66-
}
67-
}
68-
} else {
69-
// needed for last line in blame
70-
commit.fill(details)
71-
}
72-
}
73-
74-
return result, nil
75-
}
76-
77-
func createBlame() *Blame {
78-
var blame = Blame{}
79-
blame.commits = make(map[int]*Commit)
80-
return &blame
7+
// Blame contains information of a Git file blame.
8+
type Blame struct {
9+
lines []*Commit
8110
}
8211

83-
// Return commit from map or creates a new one
84-
func getCommit(sha *SHA1, commits map[[20]byte]*Commit) *Commit {
85-
commit, ok := commits[sha.bytes]
86-
if !ok {
87-
commit = &Commit{
88-
ID: sha,
89-
}
90-
commits[sha.bytes] = commit
91-
}
92-
93-
return commit
94-
}
95-
96-
func (c *Commit) fill(data map[string]string) {
97-
author, ok := data["author"]
98-
if ok && c.Author == nil {
99-
t, err := parseBlameTime(data, "author")
100-
if err != nil {
101-
c.Author = &Signature{
102-
Name: author,
103-
Email: data["author-mail"],
104-
}
105-
} else {
106-
c.Author = &Signature{
107-
Name: author,
108-
Email: data["author-mail"],
109-
When: t,
110-
}
111-
}
112-
}
113-
committer, ok := data["committer"]
114-
if ok && c.Committer == nil {
115-
t, err := parseBlameTime(data, "committer")
116-
if err != nil {
117-
c.Committer = &Signature{
118-
Name: committer,
119-
Email: data["committer-mail"],
120-
}
121-
} else {
122-
c.Committer = &Signature{
123-
Name: committer,
124-
Email: data["committer-mail"],
125-
When: t,
126-
}
127-
}
128-
}
129-
}
130-
131-
func parseBlameTime(data map[string]string, prefix string) (time.Time, error) {
132-
atoi, err := strconv.ParseInt(data[prefix+"-time"], 10, 64)
133-
if err != nil {
134-
return time.Time{}, err
135-
}
136-
t := time.Unix(atoi, 0)
137-
138-
if len(data["author-tz"]) == 5 {
139-
hours, ok1 := strconv.ParseInt(data[prefix+"-tz"][:3], 10, 0)
140-
mins, ok2 := strconv.ParseInt(data[prefix+"-tz"][3:5], 10, 0)
141-
if ok1 == nil && ok2 == nil {
142-
t = t.In(time.FixedZone("Fixed", int((hours*60+mins)*60)))
143-
}
12+
// Line returns the commit by given line number (1-based).
13+
// It returns nil when no such line.
14+
func (b *Blame) Line(i int) *Commit {
15+
if i <= 0 || len(b.lines) < i {
16+
return nil
14417
}
145-
return t, nil
18+
return b.lines[i-1]
14619
}

blame_test.go

-106
This file was deleted.

repo_blame.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2020 The Gogs Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package git
6+
7+
import (
8+
"bytes"
9+
"time"
10+
)
11+
12+
// BlameOptions contains optional arguments for blaming a file.
13+
// Docs: https://git-scm.com/docs/git-blame
14+
type BlameOptions struct {
15+
// The timeout duration before giving up for each shell command execution.
16+
// The default timeout duration will be used when not supplied.
17+
Timeout time.Duration
18+
}
19+
20+
// BlameFile returns blame results of the file with the given revision of the repository.
21+
func (r *Repository) BlameFile(rev, file string, opts ...BlameOptions) (*Blame, error) {
22+
var opt BlameOptions
23+
if len(opts) > 0 {
24+
opt = opts[0]
25+
}
26+
27+
stdout, err := NewCommand("blame", "-l", "-s", rev, "--", file).RunInDirWithTimeout(opt.Timeout, r.path)
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
lines := bytes.Split(stdout, []byte{'\n'})
33+
blame := &Blame{
34+
lines: make([]*Commit, 0, len(lines)),
35+
}
36+
for _, line := range lines {
37+
if len(line) < 40 {
38+
break
39+
}
40+
id := line[:40]
41+
42+
// Earliest commit is indicated by a leading "^"
43+
if id[0] == '^' {
44+
id = id[1:]
45+
}
46+
commit, err := r.CatFileCommit(string(id), CatFileCommitOptions{Timeout: opt.Timeout}) //nolint
47+
if err != nil {
48+
return nil, err
49+
}
50+
blame.lines = append(blame.lines, commit)
51+
}
52+
return blame, nil
53+
}

repo_blame_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2020 The Gogs Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package git
6+
7+
import (
8+
"fmt"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestRepository_BlameFile(t *testing.T) {
15+
t.Run("bad file", func(t *testing.T) {
16+
_, err := testrepo.BlameFile("", "404.txt")
17+
assert.Error(t, err)
18+
})
19+
20+
blame, err := testrepo.BlameFile("cfc3b2993f74726356887a5ec093de50486dc617", "README.txt")
21+
assert.Nil(t, err)
22+
23+
// Assert representative commits
24+
// https://github.com/gogs/git-module-testrepo/blame/master/README.txt
25+
tests := []struct {
26+
line int
27+
expID string
28+
}{
29+
{line: 1, expID: "755fd577edcfd9209d0ac072eed3b022cbe4d39b"},
30+
{line: 3, expID: "a13dba1e469944772490909daa58c53ac8fa4b0d"},
31+
{line: 5, expID: "755fd577edcfd9209d0ac072eed3b022cbe4d39b"},
32+
{line: 13, expID: "8d2636da55da593c421e1cb09eea502a05556a69"},
33+
}
34+
for _, test := range tests {
35+
t.Run(fmt.Sprintf("Line %d", test.line), func(t *testing.T) {
36+
line := blame.Line(test.line)
37+
assert.Equal(t, test.expID, line.ID.String())
38+
})
39+
}
40+
}

repo_pull.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
// MergeBaseOptions contains optional arguments for getting merge base.
13-
// // Docs: https://git-scm.com/docs/git-merge-base
13+
// Docs: https://git-scm.com/docs/git-merge-base
1414
type MergeBaseOptions struct {
1515
// The timeout duration before giving up for each shell command execution.
1616
// The default timeout duration will be used when not supplied.

utils.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"sync"
1111
)
1212

13-
// objectCache provides thread-safe cache opeations.
13+
// objectCache provides thread-safe cache operations.
1414
// TODO(@unknwon): Use sync.Map once requires Go 1.13.
1515
type objectCache struct {
1616
lock sync.RWMutex

0 commit comments

Comments
 (0)