Skip to content

Commit daa9bb4

Browse files
authored
Merge pull request #151 from erizocosmico/feature/history_idx-udf
Feature/history idx udf
2 parents bfb6c82 + abe8d25 commit daa9bb4

File tree

4 files changed

+243
-1
lines changed

4 files changed

+243
-1
lines changed

internal/function/history_idx.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package function
2+
3+
import (
4+
"io"
5+
6+
"github.com/src-d/gitquery"
7+
git "gopkg.in/src-d/go-git.v4"
8+
"gopkg.in/src-d/go-git.v4/plumbing"
9+
"gopkg.in/src-d/go-mysql-server.v0/sql"
10+
"gopkg.in/src-d/go-mysql-server.v0/sql/expression"
11+
)
12+
13+
// HistoryIdx is a function that returns the index of a commit in the history
14+
// of another commit.
15+
type HistoryIdx struct {
16+
expression.BinaryExpression
17+
}
18+
19+
// NewHistoryIdx creates a new HistoryIdx udf.
20+
func NewHistoryIdx(start, target sql.Expression) sql.Expression {
21+
return &HistoryIdx{expression.BinaryExpression{Left: start, Right: target}}
22+
}
23+
24+
// Name implements the Expression interface.
25+
func (HistoryIdx) Name() string { return "history_idx" }
26+
27+
// Eval implements the Expression interface.
28+
func (f *HistoryIdx) Eval(session sql.Session, row sql.Row) (interface{}, error) {
29+
s, ok := session.(*gitquery.Session)
30+
if !ok {
31+
return nil, gitquery.ErrInvalidGitQuerySession.New(session)
32+
}
33+
34+
left, err := f.Left.Eval(session, row)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
if left == nil {
40+
return nil, nil
41+
}
42+
43+
left, err = sql.Text.Convert(left)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
right, err := f.Right.Eval(session, row)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
if right == nil {
54+
return nil, nil
55+
}
56+
57+
right, err = sql.Text.Convert(right)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
start := plumbing.NewHash(left.(string))
63+
target := plumbing.NewHash(right.(string))
64+
65+
// fast path for equal hashes
66+
if start == target {
67+
return int64(0), nil
68+
}
69+
70+
return f.historyIdx(s.Pool, start, target)
71+
}
72+
73+
func (f *HistoryIdx) historyIdx(pool *gitquery.RepositoryPool, start, target plumbing.Hash) (int64, error) {
74+
iter, err := pool.RepoIter()
75+
if err != nil {
76+
return 0, err
77+
}
78+
79+
for {
80+
repo, err := iter.Next()
81+
if err == io.EOF {
82+
return -1, nil
83+
}
84+
85+
if err != nil {
86+
return 0, err
87+
}
88+
89+
idx, err := f.repoHistoryIdx(repo.Repo, start, target)
90+
if err != nil {
91+
return 0, err
92+
}
93+
94+
if idx > -1 {
95+
return idx, nil
96+
}
97+
}
98+
}
99+
100+
type stackFrame struct {
101+
// idx from the start commit
102+
idx int64
103+
// pos in the hashes slice
104+
pos int
105+
hashes []plumbing.Hash
106+
}
107+
108+
func (f *HistoryIdx) repoHistoryIdx(repo *git.Repository, start, target plumbing.Hash) (int64, error) {
109+
// If the target is not on the repo we can avoid starting to traverse the
110+
// tree completely.
111+
_, err := repo.CommitObject(target)
112+
if err == plumbing.ErrObjectNotFound {
113+
return -1, nil
114+
}
115+
116+
if err != nil {
117+
return 0, err
118+
}
119+
120+
// Since commits can have multiple parents we cannot just do a repo.Log and
121+
// keep counting with an index how far it is, because it might go back in
122+
// the history and try another branch.
123+
// Because of that, the traversal of the history is done manually using a
124+
// stack with frames with N commit hashes, representing each level in the
125+
// history. Because the frame keeps track of which was its index, we can
126+
// return accurate indexes even if there are multiple branches.
127+
stack := []*stackFrame{{0, 0, []plumbing.Hash{start}}}
128+
129+
for {
130+
if len(stack) == 0 {
131+
return -1, nil
132+
}
133+
134+
frame := stack[len(stack)-1]
135+
136+
c, err := repo.CommitObject(frame.hashes[frame.pos])
137+
if err == plumbing.ErrObjectNotFound {
138+
return -1, nil
139+
}
140+
141+
if err != nil {
142+
return 0, err
143+
}
144+
145+
frame.pos++
146+
147+
if c.Hash == target {
148+
return frame.idx, nil
149+
}
150+
151+
if frame.pos >= len(frame.hashes) {
152+
stack = stack[:len(stack)-1]
153+
}
154+
155+
if c.NumParents() > 0 {
156+
stack = append(stack, &stackFrame{frame.idx + 1, 0, c.ParentHashes})
157+
}
158+
}
159+
}
160+
161+
// Type implements the Expression interface.
162+
func (HistoryIdx) Type() sql.Type { return sql.Int64 }
163+
164+
// TransformUp implements the Expression interface.
165+
func (f *HistoryIdx) TransformUp(fn func(sql.Expression) (sql.Expression, error)) (sql.Expression, error) {
166+
left, err := f.Left.TransformUp(fn)
167+
if err != nil {
168+
return nil, err
169+
}
170+
171+
right, err := f.Right.TransformUp(fn)
172+
if err != nil {
173+
return nil, err
174+
}
175+
176+
return fn(NewHistoryIdx(left, right))
177+
}

internal/function/history_idx_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+
"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 TestHistoryIdx(t *testing.T) {
15+
require.NoError(t, fixtures.Init())
16+
defer func() {
17+
require.NoError(t, fixtures.Clean())
18+
}()
19+
20+
f := NewHistoryIdx(
21+
expression.NewGetField(0, sql.Text, "start", true),
22+
expression.NewGetField(1, sql.Text, "target", 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+
{"start is null", sql.NewRow(nil, "foo"), nil, false},
39+
{"target is null", sql.NewRow("foo", nil), nil, false},
40+
{"target is not on start history", sql.NewRow("b029517f6300c2da0f4b651b8642506cd6aaf45d", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5"), int64(-1), false},
41+
{"commits are equal", sql.NewRow("35e85108805c84807bc66a02d91535e1e24b38b9", "35e85108805c84807bc66a02d91535e1e24b38b9"), int64(0), false},
42+
{"target is on commit history", sql.NewRow("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "b029517f6300c2da0f4b651b8642506cd6aaf45d"), int64(5), false},
43+
{"target is on commit history with a multi parent", sql.NewRow("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "b8e471f58bcbca63b07bda20e428190409c2db47"), int64(5), false},
44+
{
45+
"target is on commit history and is not first in the pool",
46+
sql.NewRow("b685400c1f9316f350965a5993d350bc746b0bf4", "c7431b5bc9d45fb64a87d4a895ce3d1073c898d2"),
47+
int64(3),
48+
false,
49+
},
50+
}
51+
52+
for _, tt := range testCases {
53+
t.Run(tt.name, func(t *testing.T) {
54+
require := require.New(t)
55+
val, err := f.Eval(session, tt.row)
56+
if tt.err {
57+
require.Error(err)
58+
} else {
59+
require.NoError(err)
60+
require.Equal(tt.expected, val)
61+
}
62+
})
63+
}
64+
}

internal/function/registry.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var functions = map[string]sql.Function{
66
"is_tag": sql.Function1(NewIsTag),
77
"is_remote": sql.Function1(NewIsRemote),
88
"commit_contains": sql.Function2(NewCommitContains),
9+
"history_idx": sql.Function2(NewHistoryIdx),
910
}
1011

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

session.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package gitquery
33
import (
44
"context"
55

6-
errors "gopkg.in/src-d/go-errors.v0"
6+
errors "gopkg.in/src-d/go-errors.v1"
77
"gopkg.in/src-d/go-mysql-server.v0/server"
88
"gopkg.in/src-d/go-mysql-server.v0/sql"
99
"gopkg.in/src-d/go-vitess.v0/mysql"

0 commit comments

Comments
 (0)