Skip to content

Commit ffb1156

Browse files
authored
Merge pull request #169 from erizocosmico/feature/commit-has-tree-udf
internal/function: add commit_has_tree UDF
2 parents bac7d17 + aee47ee commit ffb1156

File tree

3 files changed

+229
-0
lines changed

3 files changed

+229
-0
lines changed

internal/function/commit_has_tree.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package function
2+
3+
import (
4+
"io"
5+
6+
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
7+
"gopkg.in/src-d/go-git.v4/plumbing/object"
8+
9+
"github.com/src-d/gitquery"
10+
git "gopkg.in/src-d/go-git.v4"
11+
"gopkg.in/src-d/go-git.v4/plumbing"
12+
"gopkg.in/src-d/go-mysql-server.v0/sql"
13+
"gopkg.in/src-d/go-mysql-server.v0/sql/expression"
14+
)
15+
16+
// CommitHasTree is a function that checks whether a tree is part of a commit
17+
// or not.
18+
type CommitHasTree struct {
19+
expression.BinaryExpression
20+
}
21+
22+
// NewCommitHasTree creates a new CommitHasTree function.
23+
func NewCommitHasTree(commit, tree sql.Expression) sql.Expression {
24+
return &CommitHasTree{expression.BinaryExpression{
25+
Left: commit,
26+
Right: tree,
27+
}}
28+
}
29+
30+
// Name implements the Expression interface.
31+
func (CommitHasTree) Name() string { return "commit_has_tree" }
32+
33+
// Type implements the Expression interface.
34+
func (CommitHasTree) Type() sql.Type { return sql.Boolean }
35+
36+
// Eval implements the Expression interface.
37+
func (f *CommitHasTree) Eval(s sql.Session, row sql.Row) (interface{}, error) {
38+
session, ok := s.(*gitquery.Session)
39+
if !ok {
40+
return nil, gitquery.ErrInvalidGitQuerySession.New(s)
41+
}
42+
43+
left, err := f.Left.Eval(s, row)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
if left == nil {
49+
return nil, nil
50+
}
51+
52+
left, err = sql.Text.Convert(left)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
right, err := f.Right.Eval(s, row)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
if right == nil {
63+
return nil, nil
64+
}
65+
66+
right, err = sql.Text.Convert(right)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
commitHash := plumbing.NewHash(left.(string))
72+
treeHash := plumbing.NewHash(right.(string))
73+
74+
iter, err := session.Pool.RepoIter()
75+
if err != nil {
76+
return nil, err
77+
}
78+
79+
for {
80+
repo, err := iter.Next()
81+
if err == io.EOF {
82+
return false, nil
83+
}
84+
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
ok, err := commitHasTree(repo.Repo, commitHash, treeHash)
90+
if err == plumbing.ErrObjectNotFound {
91+
continue
92+
}
93+
94+
return ok, nil
95+
}
96+
}
97+
98+
func commitHasTree(
99+
repo *git.Repository,
100+
commitHash, treeHash plumbing.Hash,
101+
) (bool, error) {
102+
commit, err := repo.CommitObject(commitHash)
103+
if err != nil {
104+
return false, err
105+
}
106+
107+
if commit.TreeHash == treeHash {
108+
return true, nil
109+
}
110+
111+
tree, err := commit.Tree()
112+
if err != nil {
113+
return false, err
114+
}
115+
116+
return treeInEntries(repo, tree.Entries, treeHash)
117+
}
118+
119+
func treeInEntries(
120+
repo *git.Repository,
121+
entries []object.TreeEntry,
122+
hash plumbing.Hash,
123+
) (bool, error) {
124+
type stackFrame struct {
125+
pos int
126+
entries []object.TreeEntry
127+
}
128+
var stack = []*stackFrame{{0, entries}}
129+
130+
for {
131+
if len(stack) == 0 {
132+
return false, nil
133+
}
134+
135+
frame := stack[len(stack)-1]
136+
if frame.pos >= len(frame.entries) {
137+
stack = stack[:len(stack)-1]
138+
continue
139+
}
140+
141+
entry := frame.entries[frame.pos]
142+
frame.pos++
143+
if entry.Mode == filemode.Dir {
144+
if entry.Hash == hash {
145+
return true, nil
146+
}
147+
148+
tree, err := repo.TreeObject(entry.Hash)
149+
if err != nil {
150+
return false, nil
151+
}
152+
153+
stack = append(stack, &stackFrame{0, tree.Entries})
154+
}
155+
}
156+
}
157+
158+
// TransformUp implements the Expression interface.
159+
func (f *CommitHasTree) TransformUp(fn func(sql.Expression) (sql.Expression, error)) (sql.Expression, error) {
160+
left, err := f.Left.TransformUp(fn)
161+
if err != nil {
162+
return nil, err
163+
}
164+
165+
right, err := f.Right.TransformUp(fn)
166+
if err != nil {
167+
return nil, err
168+
}
169+
170+
return fn(NewCommitHasTree(left, right))
171+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 TestCommitHasTree(t *testing.T) {
15+
require.NoError(t, fixtures.Init())
16+
defer func() {
17+
require.NoError(t, fixtures.Clean())
18+
}()
19+
20+
f := NewCommitHasTree(
21+
expression.NewGetField(0, sql.Text, "commit_hash", true),
22+
expression.NewGetField(1, sql.Text, "tree_hash", 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+
{"tree hash is null", sql.NewRow("foo", nil), nil, false},
40+
{"tree is not on commit", sql.NewRow("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "c2d30fa8ef288618f65f6eed6e168e0d514886f4"), false, false},
41+
{"tree is on commit", sql.NewRow("e8d3ffab552895c19b9fcf7aa264d277cde33881", "dbd3641b371024f44d0e469a9c8f5457b0660de1"), true, false},
42+
{"subtree is on commit", sql.NewRow("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "5a877e6a906a2743ad6e45d99c1793642aaf8eda"), true, false},
43+
}
44+
45+
for _, tt := range testCases {
46+
t.Run(tt.name, func(t *testing.T) {
47+
require := require.New(t)
48+
val, err := f.Eval(session, tt.row)
49+
if tt.err {
50+
require.Error(err)
51+
} else {
52+
require.NoError(err)
53+
require.Equal(tt.expected, val)
54+
}
55+
})
56+
}
57+
}

internal/function/registry.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ var functions = map[string]sql.Function{
77
"is_remote": sql.Function1(NewIsRemote),
88
"commit_has_blob": sql.Function2(NewCommitHasBlob),
99
"history_idx": sql.Function2(NewHistoryIdx),
10+
"commit_has_tree": sql.Function2(NewCommitHasTree),
1011
}
1112

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

0 commit comments

Comments
 (0)