Skip to content

Commit 9d1c37b

Browse files
mcuadrosajnavarro
authored andcommitted
utils: CommitStatsCalculator
Signed-off-by: Máximo Cuadros <[email protected]>
1 parent 10a5e76 commit 9d1c37b

File tree

4 files changed

+386
-0
lines changed

4 files changed

+386
-0
lines changed

internal/function/commit_stats.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package function
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"time"
7+
8+
"github.com/src-d/gitbase"
9+
"gopkg.in/src-d/go-git.v4/plumbing"
10+
"gopkg.in/src-d/go-mysql-server.v0/sql"
11+
"gopkg.in/src-d/go-mysql-server.v0/sql/expression"
12+
)
13+
14+
type CommitStats struct {
15+
expression.UnaryExpression
16+
}
17+
18+
func NewCommitStats(e sql.Expression) sql.Expression {
19+
return &CommitStats{expression.UnaryExpression{Child: e}}
20+
}
21+
22+
func (f *CommitStats) String() string {
23+
return fmt.Sprintf("commit_stats(%s)", f.Child)
24+
}
25+
26+
func (CommitStats) Type() sql.Type {
27+
return sql.JSON
28+
}
29+
30+
// TransformUp implements the Expression interface.
31+
func (f CommitStats) TransformUp(fn sql.TransformExprFunc) (sql.Expression, error) {
32+
child, err := f.Child.TransformUp(fn)
33+
if err != nil {
34+
return nil, err
35+
}
36+
return fn(NewCommitStats(child))
37+
}
38+
39+
// Eval implements the Expression interface.
40+
func (f *CommitStats) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
41+
span, ctx := ctx.Span("gitbase.CommitStats")
42+
defer span.Finish()
43+
44+
s := time.Now()
45+
46+
val, err := f.Child.Eval(ctx, row)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
if val == nil {
52+
return false, nil
53+
}
54+
55+
hash, ok := val.(string)
56+
if !ok {
57+
return nil, sql.ErrInvalidType.New(reflect.TypeOf(val).String())
58+
}
59+
60+
r, err := resolveRepo(ctx, row)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
c, err := r.CommitObject(plumbing.NewHash(hash))
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
csc := NewCommitStatsCalculator(r, c)
71+
fmt.Println(csc.Do())
72+
73+
return csc, nil
74+
}
75+
76+
func resolveRepo(ctx *sql.Context, r sql.Row) (*gitbase.Repository, error) {
77+
s, ok := ctx.Session.(*gitbase.Session)
78+
if !ok {
79+
return nil, gitbase.ErrInvalidGitbaseSession.New(ctx.Session)
80+
}
81+
82+
return s.Pool.GetRepo(r[0].(string))
83+
}

internal/function/registry.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "gopkg.in/src-d/go-mysql-server.v0/sql"
44

55
// Functions for gitbase queries.
66
var Functions = []sql.Function{
7+
sql.Function1{Name: "commit_stats", Fn: NewCommitStats},
78
sql.Function1{Name: "is_tag", Fn: NewIsTag},
89
sql.Function1{Name: "is_remote", Fn: NewIsRemote},
910
sql.FunctionN{Name: "language", Fn: NewLanguage},

internal/utils/commit_stats.go

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
package utils
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
7+
"github.com/hhatto/gocloc"
8+
"github.com/src-d/enry"
9+
10+
"gopkg.in/src-d/go-git.v4"
11+
"gopkg.in/src-d/go-git.v4/plumbing/object"
12+
"gopkg.in/src-d/go-git.v4/utils/binary"
13+
"gopkg.in/src-d/go-git.v4/utils/ioutil"
14+
"gopkg.in/src-d/go-git.v4/utils/merkletrie"
15+
)
16+
17+
type LineKind int
18+
19+
const (
20+
Code LineKind = iota + 1
21+
Comment
22+
Blank
23+
)
24+
25+
type CommitStatsCalculator struct {
26+
r *git.Repository
27+
c *object.Commit
28+
p *object.Commit
29+
30+
src map[string]LineKind
31+
dst map[string]LineKind
32+
}
33+
34+
func NewCommitStatsCalculator(r *git.Repository, c *object.Commit) *CommitStatsCalculator {
35+
return &CommitStatsCalculator{r: r, c: c}
36+
}
37+
38+
func (c *CommitStatsCalculator) Do() (*Stats, error) {
39+
var err error
40+
c.p, err = c.c.Parent(0)
41+
if err != nil {
42+
return nil, nil
43+
}
44+
45+
return c.doLines(c.c, c.dst)
46+
}
47+
48+
func (c *CommitStatsCalculator) doLines(commit *object.Commit, m map[string]LineKind) (*Stats, error) {
49+
src, _ := c.c.Tree()
50+
dst, _ := c.p.Tree()
51+
52+
ch, err := object.DiffTree(dst, src)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
stats := &Stats{}
58+
for _, change := range ch {
59+
s, err := c.doChange(change)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
stats.Sum(s)
65+
stats.Files++
66+
67+
}
68+
69+
return stats, nil
70+
}
71+
72+
func (c *CommitStatsCalculator) doChange(ch *object.Change) (*Stats, error) {
73+
a, err := ch.Action()
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
var fi FileInfo
79+
80+
switch a {
81+
case merkletrie.Delete:
82+
fi, err = c.doChangeEntry(&ch.From)
83+
if err != nil {
84+
return nil, err
85+
}
86+
case merkletrie.Insert:
87+
fi, err = c.doChangeEntry(&ch.To)
88+
if err != nil {
89+
return nil, err
90+
}
91+
case merkletrie.Modify:
92+
src, err := c.doChangeEntry(&ch.From)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
dst, err := c.doChangeEntry(&ch.To)
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
dst.Sub(src)
103+
fi = dst
104+
}
105+
106+
return fi.Stats(), nil
107+
}
108+
109+
func (c *CommitStatsCalculator) doChangeEntry(ch *object.ChangeEntry) (FileInfo, error) {
110+
if enry.IsVendor(string(ch.Name)) {
111+
return nil, nil
112+
}
113+
114+
blob, err := c.r.BlobObject(ch.TreeEntry.Hash)
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
isBinary, err := IsBinary(blob)
120+
if err != nil {
121+
return nil, err
122+
}
123+
124+
if isBinary {
125+
return nil, nil
126+
}
127+
128+
return NewFileInfo(blob)
129+
}
130+
131+
var languages = gocloc.NewDefinedLanguages()
132+
133+
type FileInfo map[string]*LineInfo
134+
type LineInfo struct {
135+
Kind LineKind
136+
Count int
137+
}
138+
139+
func NewFileInfo(f *object.Blob) (FileInfo, error) {
140+
ff := make(FileInfo, 50)
141+
142+
r, err := f.Reader()
143+
if err != nil {
144+
return ff, err
145+
}
146+
gocloc.AnalyzeReader("", languages.Langs["Go"], r, &gocloc.ClocOptions{
147+
OnBlank: ff.AddBlank,
148+
OnCode: ff.AddCode,
149+
OnComment: ff.AddComment,
150+
})
151+
152+
return ff, nil
153+
}
154+
155+
func (fi FileInfo) AddCode(line string) { fi.Add(line, Code) }
156+
func (fi FileInfo) AddComment(line string) { fi.Add(line, Comment) }
157+
func (fi FileInfo) AddBlank(line string) { fi.Add(line, Blank) }
158+
func (fi FileInfo) Add(line string, k LineKind) {
159+
if fi[line] == nil {
160+
fi[line] = &LineInfo{}
161+
}
162+
163+
fi[line].Count++
164+
fi[line].Kind = k
165+
}
166+
167+
func (fi FileInfo) Remove(line string, k LineKind, count int) {
168+
if fi[line] == nil {
169+
return
170+
}
171+
172+
fi[line].Count -= count
173+
fi[line].Kind = k
174+
}
175+
176+
type KindStats struct {
177+
Additions int
178+
Deletions int
179+
}
180+
181+
type Stats struct {
182+
Files int
183+
Code KindStats
184+
Comment KindStats
185+
Blank KindStats
186+
Total KindStats
187+
}
188+
189+
func (s *Stats) Sum(stats *Stats) {
190+
sumKindStats(&s.Code, &stats.Code)
191+
sumKindStats(&s.Comment, &stats.Comment)
192+
sumKindStats(&s.Blank, &stats.Blank)
193+
sumKindStats(&s.Total, &stats.Total)
194+
}
195+
196+
func sumKindStats(a, b *KindStats) {
197+
a.Additions += b.Additions
198+
a.Deletions += b.Deletions
199+
}
200+
201+
func (s *Stats) String() string {
202+
return fmt.Sprintf("Code (+%d/-%d)\nComment (+%d/-%d)\nBlank (+%d/-%d)\nTotal (+%d/-%d)\nFiles (%d)\n",
203+
s.Code.Additions, s.Code.Deletions,
204+
s.Comment.Additions, s.Comment.Deletions,
205+
s.Blank.Additions, s.Blank.Deletions,
206+
s.Total.Additions, s.Total.Deletions,
207+
s.Files,
208+
)
209+
}
210+
211+
func (fi FileInfo) Sub(to FileInfo) {
212+
for line, i := range to {
213+
if _, ok := fi[line]; ok {
214+
fi[line].Count -= i.Count
215+
} else {
216+
fi[line] = i
217+
fi[line].Count *= -1
218+
}
219+
}
220+
}
221+
222+
func (fi FileInfo) Stats() *Stats {
223+
stats := &Stats{}
224+
for _, info := range fi {
225+
fillKindStats(&stats.Total, info)
226+
switch info.Kind {
227+
case Code:
228+
fillKindStats(&stats.Code, info)
229+
case Comment:
230+
fillKindStats(&stats.Comment, info)
231+
case Blank:
232+
fillKindStats(&stats.Blank, info)
233+
}
234+
}
235+
236+
return stats
237+
}
238+
239+
func fillKindStats(ks *KindStats, info *LineInfo) {
240+
if info.Count > 0 {
241+
ks.Additions += info.Count
242+
}
243+
if info.Count < 0 {
244+
ks.Deletions += (info.Count * -1)
245+
}
246+
}
247+
248+
func (fi FileInfo) String() string {
249+
buf := bytes.NewBuffer(nil)
250+
for line, i := range fi {
251+
sign := ' '
252+
switch {
253+
case i.Count > 0:
254+
sign = '+'
255+
case i.Count < 0:
256+
sign = '-'
257+
}
258+
259+
fmt.Fprintf(buf, "%c [%3dx] %s\n", sign, i.Count, line)
260+
}
261+
262+
return buf.String()
263+
}
264+
265+
func IsBinary(b *object.Blob) (bin bool, err error) {
266+
reader, err := b.Reader()
267+
if err != nil {
268+
return false, err
269+
}
270+
271+
defer ioutil.CheckClose(reader, &err)
272+
return binary.IsBinary(reader)
273+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package utils
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
"gopkg.in/src-d/go-git-fixtures.v3"
9+
"gopkg.in/src-d/go-git.v4"
10+
"gopkg.in/src-d/go-git.v4/plumbing"
11+
"gopkg.in/src-d/go-git.v4/plumbing/cache"
12+
"gopkg.in/src-d/go-git.v4/storage/filesystem"
13+
)
14+
15+
func TestLanguage(t *testing.T) {
16+
fixtures.Init()
17+
require := require.New(t)
18+
19+
f := fixtures.ByURL("https://github.com/src-d/go-git.git").One()
20+
21+
r, err := git.Open(filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault()), nil)
22+
require.NoError(err)
23+
24+
c, err := r.CommitObject(plumbing.NewHash("d2d68d3413353bd4bf20891ac1daa82cd6e00fb9"))
25+
require.NoError(err)
26+
27+
csc := NewCommitStatsCalculator(r, c)
28+
fmt.Println(csc.Do())
29+
}

0 commit comments

Comments
 (0)