Skip to content

Commit 1c4ac94

Browse files
authored
Add "git blame" parser (#62)
1 parent 0bb10af commit 1c4ac94

File tree

2 files changed

+252
-0
lines changed

2 files changed

+252
-0
lines changed

blame.go

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
"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
81+
}
82+
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+
}
144+
}
145+
return t, nil
146+
}

blame_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
var oneRowBlame = `2c49687c5b06776a44e2a8c0635428f647909472 3 3 4
15+
author ᴜɴᴋɴᴡᴏɴ
16+
author-mail <[email protected]>
17+
author-time 1585383299
18+
author-tz +0800
19+
committer GitHub
20+
committer-mail <[email protected]>
21+
committer-time 1585383299
22+
committer-tz +0800
23+
summary ci: migrate from Travis to GitHub Actions (#50)
24+
previous 0d17b78404b7432905a58a235d875e9d28969ee3 README.md
25+
filename README.md
26+
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/gogs/git-module/Go?logo=github&style=for-the-badge)](https://github.com/gogs/git-module/actions?query=workflow%3AGo)
27+
`
28+
29+
var twoRowsBlame = `f29bce1e3a666c02175d080892be185405dd3af4 1 1 2
30+
author Unknwon
31+
author-mail <[email protected]>
32+
author-time 1573967409
33+
author-tz -0800
34+
committer Unknwon
35+
committer-mail <[email protected]>
36+
committer-time 1573967409
37+
committer-tz -0800
38+
summary README: update badges
39+
previous 065699e51f42559ab0c3ad22c1f2c789b2def8fb README.md
40+
filename README.md
41+
# Git Module
42+
f29bce1e3a666c02175d080892be185405dd3af4 2 2
43+
44+
`
45+
46+
var commit1 = &Commit{
47+
ID: MustIDFromString("f29bce1e3a666c02175d080892be185405dd3af4"),
48+
Message: "README: update badges",
49+
Author: &Signature{
50+
Name: "Unknwon",
51+
Email: "<[email protected]>",
52+
When: time.Unix(1573967409, 0).In(time.FixedZone("Fixed", -8*60*60)),
53+
},
54+
Committer: &Signature{
55+
Name: "Unknwon",
56+
Email: "<[email protected]>",
57+
When: time.Unix(1573967409, 0).In(time.FixedZone("Fixed", -8*60*60)),
58+
},
59+
parents: []*SHA1{MustIDFromString("065699e51f42559ab0c3ad22c1f2c789b2def8fb")},
60+
}
61+
62+
var commit2 = &Commit{
63+
ID: MustIDFromString("2c49687c5b06776a44e2a8c0635428f647909472"),
64+
Message: "ci: migrate from Travis to GitHub Actions (#50)",
65+
Author: &Signature{
66+
Name: "ᴜɴᴋɴᴡᴏɴ",
67+
Email: "<[email protected]>",
68+
When: time.Unix(1585383299, 0).In(time.FixedZone("Fixed", 8*60*60)),
69+
},
70+
Committer: &Signature{
71+
Name: "GitHub",
72+
Email: "<[email protected]>",
73+
When: time.Unix(1585383299, 0).In(time.FixedZone("Fixed", 8*60*60)),
74+
},
75+
parents: []*SHA1{MustIDFromString("0d17b78404b7432905a58a235d875e9d28969ee3")},
76+
}
77+
78+
func TestOneRowBlame(t *testing.T) {
79+
blame, _ := BlameContent([]byte(oneRowBlame))
80+
var expect = createBlame()
81+
82+
expect.commits[3] = commit2
83+
84+
assert.Equal(t, expect, blame)
85+
}
86+
87+
func TestMultipleRowsBlame(t *testing.T) {
88+
blame, _ := BlameContent([]byte(twoRowsBlame + oneRowBlame))
89+
var expect = createBlame()
90+
91+
expect.commits[1] = commit1
92+
expect.commits[2] = commit1
93+
expect.commits[3] = commit2
94+
95+
assert.Equal(t, expect, blame)
96+
}
97+
98+
func TestRepository_BlameFile(t *testing.T) {
99+
blame, _ := testrepo.BlameFile("master", "pom.xml")
100+
assert.Greater(t, len(blame.commits), 0)
101+
}
102+
103+
func TestRepository_BlameNotExistFile(t *testing.T) {
104+
_, err := testrepo.BlameFile("master", "0")
105+
assert.Error(t, err)
106+
}

0 commit comments

Comments
 (0)