Skip to content

Commit 7d777c7

Browse files
authored
Merge pull request #150 from erizocosmico/feature/commit-contains-udf
internal/function: implement commit_contains function
2 parents a942dff + d03591e commit 7d777c7

File tree

4 files changed

+229
-2
lines changed

4 files changed

+229
-2
lines changed

internal/function/commit_contains.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package function
2+
3+
import (
4+
"io"
5+
6+
"github.com/src-d/gitquery"
7+
"gopkg.in/src-d/go-git.v4/plumbing"
8+
"gopkg.in/src-d/go-git.v4/plumbing/object"
9+
"gopkg.in/src-d/go-mysql-server.v0/sql"
10+
)
11+
12+
// CommitContains is a function that checks whether a blob is in a commit.
13+
type CommitContains struct {
14+
commitHash sql.Expression
15+
blob sql.Expression
16+
}
17+
18+
// NewCommitContains creates a new commit_contains function.
19+
func NewCommitContains(commitHash, blob sql.Expression) sql.Expression {
20+
return &CommitContains{
21+
commitHash: commitHash,
22+
blob: blob,
23+
}
24+
}
25+
26+
// Type implements the Expression interface.
27+
func (CommitContains) Type() sql.Type {
28+
return sql.Boolean
29+
}
30+
31+
// Eval implements the Expression interface.
32+
func (f *CommitContains) Eval(session sql.Session, row sql.Row) (interface{}, error) {
33+
s, ok := session.(*gitquery.Session)
34+
if !ok {
35+
return nil, gitquery.ErrInvalidGitQuerySession.New(session)
36+
}
37+
38+
commitHash, err := f.commitHash.Eval(s, row)
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
if commitHash == nil {
44+
return nil, err
45+
}
46+
47+
commitHash, err = sql.Text.Convert(commitHash)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
blob, err := f.blob.Eval(s, row)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
if blob == nil {
58+
return nil, err
59+
}
60+
61+
blob, err = sql.Text.Convert(blob)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
return f.commitContains(
67+
s.Pool,
68+
plumbing.NewHash(commitHash.(string)),
69+
plumbing.NewHash(blob.(string)),
70+
)
71+
}
72+
73+
func (f *CommitContains) commitContains(
74+
pool *gitquery.RepositoryPool,
75+
commitHash, blob plumbing.Hash,
76+
) (bool, error) {
77+
iter, err := pool.RepoIter()
78+
if err != nil {
79+
return false, err
80+
}
81+
82+
for {
83+
repository, err := iter.Next()
84+
if err == io.EOF {
85+
break
86+
}
87+
88+
if err != nil {
89+
return false, err
90+
}
91+
92+
repo := repository.Repo
93+
commit, err := repo.CommitObject(commitHash)
94+
if err == plumbing.ErrObjectNotFound {
95+
continue
96+
}
97+
98+
if err != nil {
99+
return false, err
100+
}
101+
102+
tree, err := commit.Tree()
103+
if err != nil {
104+
return false, err
105+
}
106+
107+
contained, err := hashInTree(blob, tree)
108+
if err != nil {
109+
return false, err
110+
}
111+
112+
if contained {
113+
return true, nil
114+
}
115+
}
116+
117+
return false, nil
118+
}
119+
120+
func hashInTree(hash plumbing.Hash, tree *object.Tree) (bool, error) {
121+
var contained bool
122+
err := tree.Files().ForEach(func(f *object.File) error {
123+
if f.Blob.Hash == hash {
124+
contained = true
125+
return io.EOF
126+
}
127+
return nil
128+
})
129+
130+
if err != nil && err != io.EOF {
131+
return false, err
132+
}
133+
134+
return contained, nil
135+
}
136+
137+
// Name implements the Expression interface.
138+
func (CommitContains) Name() string {
139+
return "commit_contains"
140+
}
141+
142+
// IsNullable implements the Expression interface.
143+
func (f CommitContains) IsNullable() bool {
144+
return f.commitHash.IsNullable() || f.blob.IsNullable()
145+
}
146+
147+
// Resolved implements the Expression interface.
148+
func (f CommitContains) Resolved() bool {
149+
return f.commitHash.Resolved() && f.blob.Resolved()
150+
}
151+
152+
// TransformUp implements the Expression interface.
153+
func (f CommitContains) TransformUp(fn func(sql.Expression) (sql.Expression, error)) (sql.Expression, error) {
154+
commitHash, err := f.commitHash.TransformUp(fn)
155+
if err != nil {
156+
return nil, err
157+
}
158+
159+
blob, err := f.blob.TransformUp(fn)
160+
if err != nil {
161+
return nil, err
162+
}
163+
164+
return fn(NewCommitContains(commitHash, blob))
165+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package function
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/src-d/gitquery"
8+
"github.com/stretchr/testify/require"
9+
fixtures "gopkg.in/src-d/go-git-fixtures.v3"
10+
"gopkg.in/src-d/go-mysql-server.v0/sql"
11+
"gopkg.in/src-d/go-mysql-server.v0/sql/expression"
12+
)
13+
14+
func TestCommitContains(t *testing.T) {
15+
require.NoError(t, fixtures.Init())
16+
defer func() {
17+
require.NoError(t, fixtures.Clean())
18+
}()
19+
20+
f := NewCommitContains(
21+
expression.NewGetField(0, sql.Text, "commit_hash", true),
22+
expression.NewGetField(1, sql.Text, "blob", true),
23+
)
24+
25+
pool := gitquery.NewRepositoryPool()
26+
for _, f := range fixtures.ByTag("worktree") {
27+
pool.AddGit(f.Worktree().Root())
28+
}
29+
30+
session := gitquery.NewSession(context.TODO(), &pool)
31+
32+
testCases := []struct {
33+
name string
34+
row sql.Row
35+
expected interface{}
36+
err bool
37+
}{
38+
{"commit hash is null", sql.NewRow(nil, "foo"), nil, false},
39+
{"blob hash is null", sql.NewRow("foo", nil), nil, false},
40+
{"blob is not on commit", sql.NewRow("35e85108805c84807bc66a02d91535e1e24b38b9", "9dea2395f5403188298c1dabe8bdafe562c491e3"), false, false},
41+
{"blob is on commit", sql.NewRow("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "9dea2395f5403188298c1dabe8bdafe562c491e3"), true, false},
42+
}
43+
44+
for _, tt := range testCases {
45+
t.Run(tt.name, func(t *testing.T) {
46+
require := require.New(t)
47+
val, err := f.Eval(session, tt.row)
48+
if tt.err {
49+
require.Error(err)
50+
} else {
51+
require.NoError(err)
52+
require.Equal(tt.expected, val)
53+
}
54+
})
55+
}
56+
}

internal/function/registry.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ package function
33
import "gopkg.in/src-d/go-mysql-server.v0/sql"
44

55
var functions = map[string]sql.Function{
6-
"is_tag": sql.Function1(NewIsTag),
7-
"is_remote": sql.Function1(NewIsRemote),
6+
"is_tag": sql.Function1(NewIsTag),
7+
"is_remote": sql.Function1(NewIsRemote),
8+
"commit_contains": sql.Function2(NewCommitContains),
89
}
910

1011
// Register all the gitquery functions in the SQL catalog.

session.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package gitquery
33
import (
44
"context"
55

6+
errors "gopkg.in/src-d/go-errors.v0"
67
"gopkg.in/src-d/go-mysql-server.v0/server"
78
"gopkg.in/src-d/go-mysql-server.v0/sql"
89
"gopkg.in/src-d/go-vitess.v0/mysql"
@@ -28,3 +29,7 @@ func NewSessionBuilder(pool *RepositoryPool) server.SessionBuilder {
2829
return NewSession(ctx, pool)
2930
}
3031
}
32+
33+
// ErrInvalidGitQuerySession is returned when some node expected a GitQuery
34+
// session but received something else.
35+
var ErrInvalidGitQuerySession = errors.NewKind("expecting gitquery session, but received: %T")

0 commit comments

Comments
 (0)