Skip to content

Commit 2f8c37c

Browse files
author
Juanjo Alvarez
committed
Blame func prototype skeleton
Signed-off-by: Juanjo Alvarez <[email protected]>
1 parent 97d9667 commit 2f8c37c

File tree

2 files changed

+209
-0
lines changed

2 files changed

+209
-0
lines changed

internal/function/blame.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package function
2+
3+
import (
4+
"fmt"
5+
"github.com/src-d/gitbase"
6+
"gopkg.in/src-d/go-git.v4"
7+
"gopkg.in/src-d/go-mysql-server.v0/sql"
8+
9+
"gopkg.in/src-d/go-git.v4/plumbing"
10+
"gopkg.in/src-d/go-git.v4/plumbing/object"
11+
)
12+
13+
type (
14+
// Blame implements git-blame function as UDF
15+
Blame struct {
16+
repo sql.Expression
17+
commit sql.Expression
18+
}
19+
20+
// BlameLine represents each line of git blame's output
21+
BlameLine struct {
22+
Commit string `json:"commit"`
23+
File string `json:"file"`
24+
LineNum int `json:"linenum"`
25+
Author string `json:"author"`
26+
Text string `json:"text"`
27+
}
28+
)
29+
30+
// NewBlame constructor
31+
func NewBlame(repo, commit sql.Expression) sql.Expression {
32+
return &Blame{repo, commit}
33+
}
34+
35+
func (b *Blame) String() string {
36+
return fmt.Sprintf("blame(%s, %s)", b.repo, b.commit)
37+
}
38+
39+
// Type implements the sql.Expression interface
40+
func (*Blame) Type() sql.Type {
41+
return sql.Array(sql.JSON)
42+
}
43+
44+
// TransformUp implements the sql.Expression interface
45+
func (b *Blame) TransformUp(fn sql.TransformExprFunc) (sql.Expression, error) {
46+
repo, err := b.repo.TransformUp(fn)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
commit, err := b.commit.TransformUp(fn)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
return fn(&Blame{repo, commit})
57+
}
58+
59+
// Children implements the Expression interface.
60+
func (b *Blame) Children() []sql.Expression {
61+
return []sql.Expression{b.commit}
62+
}
63+
64+
// IsNullable implements the Expression interface.
65+
func (*Blame) IsNullable() bool {
66+
return false
67+
}
68+
69+
// Resolved implements the Expression interface.
70+
func (b *Blame) Resolved() bool {
71+
return b.commit.Resolved()
72+
}
73+
74+
// Eval implements the sql.Expression interface.
75+
func (b *Blame) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
76+
span, ctx := ctx.Span("gitbase.Blame")
77+
defer span.Finish()
78+
79+
repo, err := b.resolveRepo(ctx, row)
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
commit, err := b.resolveCommit(ctx, repo, row)
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
fIter, err := commit.Files()
90+
if err != nil {
91+
return nil, err
92+
}
93+
defer fIter.Close()
94+
95+
var lines []BlameLine
96+
for f, err := fIter.Next(); err == nil; f, err = fIter.Next() {
97+
result, err := git.Blame(commit, f.Name)
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
for i, l := range result.Lines {
103+
lines = append(lines, BlameLine{
104+
Commit: commit.Hash.String(),
105+
File: f.Name,
106+
LineNum: i,
107+
Author: l.Author,
108+
Text: l.Text,
109+
})
110+
}
111+
}
112+
113+
return lines, nil
114+
}
115+
116+
func (b *Blame) resolveCommit(ctx *sql.Context, repo *gitbase.Repository, row sql.Row) (*object.Commit, error) {
117+
str, err := exprToString(ctx, b.commit, row)
118+
if err != nil {
119+
return nil, err
120+
}
121+
122+
commitHash, err := repo.ResolveRevision(plumbing.Revision(str))
123+
if err != nil {
124+
h := plumbing.NewHash(str)
125+
commitHash = &h
126+
}
127+
to, err := repo.CommitObject(*commitHash)
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
return to, nil
133+
}
134+
135+
func (b *Blame) resolveRepo(ctx *sql.Context, r sql.Row) (*gitbase.Repository, error) {
136+
repoID, err := exprToString(ctx, b.repo, r)
137+
if err != nil {
138+
return nil, err
139+
}
140+
s, ok := ctx.Session.(*gitbase.Session)
141+
if !ok {
142+
return nil, gitbase.ErrInvalidGitbaseSession.New(ctx.Session)
143+
}
144+
return s.Pool.GetRepo(repoID)
145+
}

internal/function/blame_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package function
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"gopkg.in/src-d/go-mysql-server.v0/sql/expression"
7+
8+
"github.com/src-d/gitbase"
9+
"github.com/stretchr/testify/require"
10+
fixtures "gopkg.in/src-d/go-git-fixtures.v3"
11+
"gopkg.in/src-d/go-git.v4/plumbing/cache"
12+
"gopkg.in/src-d/go-mysql-server.v0/sql"
13+
"testing"
14+
)
15+
16+
func TestBlameEval(t *testing.T) {
17+
require.NoError(t, fixtures.Init())
18+
19+
defer func() {
20+
require.NoError(t, fixtures.Clean())
21+
}()
22+
23+
path := fixtures.ByTag("worktree").One().Worktree().Root()
24+
25+
pool := gitbase.NewRepositoryPool(cache.DefaultMaxSize)
26+
require.NoError(t, pool.AddGitWithID("worktree", path))
27+
28+
session := gitbase.NewSession(pool)
29+
ctx := sql.NewContext(context.TODO(), sql.WithSession(session))
30+
31+
testCases := []struct {
32+
name string
33+
repo sql.Expression
34+
commit sql.Expression
35+
row sql.Row
36+
expected BlameLine
37+
}{
38+
{
39+
name: "init commit",
40+
repo: expression.NewGetField(0, sql.Text, "repository_id", false),
41+
commit: expression.NewGetField(1, sql.Text, "commit_hash", false),
42+
row: sql.NewRow("worktree", "b029517f6300c2da0f4b651b8642506cd6aaf45d"),
43+
expected: BlameLine{
44+
"b029517f6300c2da0f4b651b8642506cd6aaf45d",
45+
".gitignore",
46+
0,
47+
48+
"*.class",
49+
},
50+
}}
51+
52+
for _, tc := range testCases {
53+
t.Run(tc.name, func(t *testing.T) {
54+
blame := NewBlame(tc.repo, tc.commit)
55+
results, err := blame.Eval(ctx, tc.row)
56+
require.NoError(t, err)
57+
for _, r := range results.([]BlameLine) {
58+
fmt.Println(r)
59+
require.EqualValues(t, tc.expected, r)
60+
break
61+
}
62+
})
63+
}
64+
}

0 commit comments

Comments
 (0)