Skip to content

Commit ead93a3

Browse files
committed
commitstats: Calculate function
Signed-off-by: Máximo Cuadros <[email protected]>
1 parent 5e2a081 commit ead93a3

File tree

2 files changed

+500
-0
lines changed

2 files changed

+500
-0
lines changed

internal/commitstats/commit_stats.go

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
package commitstats
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"io"
8+
9+
"github.com/hhatto/gocloc"
10+
"gopkg.in/src-d/enry.v1"
11+
"gopkg.in/src-d/go-git.v4"
12+
"gopkg.in/src-d/go-git.v4/plumbing/object"
13+
"gopkg.in/src-d/go-git.v4/utils/binary"
14+
"gopkg.in/src-d/go-git.v4/utils/ioutil"
15+
"gopkg.in/src-d/go-git.v4/utils/merkletrie"
16+
)
17+
18+
// LineKind defines the kind of a line in a file.
19+
type LineKind int
20+
21+
const (
22+
// Code represents a line of code.
23+
Code LineKind = iota + 1
24+
// Comment represents a line of comment.
25+
Comment
26+
// Blank represents an empty line.
27+
Blank
28+
// Other represents a line from any other kind.
29+
Other
30+
)
31+
32+
// Calculate calculates the CommitStats for from commit to another.
33+
// if from is nil the first parent is used, if the commit is orphan the stats
34+
// are compared against a empty commit.
35+
func Calculate(r *git.Repository, from, to *object.Commit) (*CommitStats, error) {
36+
cc := &commitStatsCalculator{}
37+
38+
var err error
39+
if to.NumParents() != 0 && from == nil {
40+
from, err = to.Parent(0)
41+
if err != nil {
42+
return nil, err
43+
}
44+
}
45+
46+
if from == nil {
47+
return cc.doCommit(to)
48+
}
49+
50+
return cc.doDiff(r, from, to)
51+
}
52+
53+
type commitStatsCalculator struct{}
54+
55+
func (cc *commitStatsCalculator) doCommit(c *object.Commit) (*CommitStats, error) {
56+
files, err := c.Files()
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
stats := &CommitStats{}
62+
return stats, files.ForEach(func(f *object.File) error {
63+
fi, err := cc.doBlob(&f.Blob, f.Name)
64+
if err != nil {
65+
return err
66+
}
67+
68+
stats.Add(fi.stats())
69+
stats.Files++
70+
return nil
71+
})
72+
}
73+
74+
func (cc *commitStatsCalculator) doDiff(r *git.Repository, from, to *object.Commit) (*CommitStats, error) {
75+
ch, err := cc.computeDiff(from, to)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
stats := &CommitStats{}
81+
for _, change := range ch {
82+
s, err := cc.doChange(r, change)
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
stats.Add(s)
88+
stats.Files++
89+
90+
}
91+
92+
return stats, nil
93+
}
94+
95+
func (cc *commitStatsCalculator) computeDiff(from, to *object.Commit) (object.Changes, error) {
96+
src, err := to.Tree()
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
dst, err := from.Tree()
102+
if err != nil {
103+
return nil, err
104+
}
105+
106+
return object.DiffTree(dst, src)
107+
}
108+
109+
func (cc *commitStatsCalculator) doChange(r *git.Repository, ch *object.Change) (*CommitStats, error) {
110+
a, err := ch.Action()
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
var fi fileStats
116+
117+
switch a {
118+
case merkletrie.Delete:
119+
fi, err = cc.doChangeEntry(r, &ch.From)
120+
if err != nil {
121+
return nil, err
122+
}
123+
case merkletrie.Insert:
124+
fi, err = cc.doChangeEntry(r, &ch.To)
125+
if err != nil {
126+
return nil, err
127+
}
128+
case merkletrie.Modify:
129+
src, err := cc.doChangeEntry(r, &ch.From)
130+
if err != nil {
131+
return nil, err
132+
}
133+
134+
dst, err := cc.doChangeEntry(r, &ch.To)
135+
if err != nil {
136+
return nil, err
137+
}
138+
139+
if src == nil {
140+
src = make(fileStats)
141+
}
142+
143+
if dst == nil {
144+
dst = make(fileStats)
145+
}
146+
147+
dst.sub(src)
148+
fi = dst
149+
}
150+
151+
return fi.stats(), nil
152+
}
153+
154+
func (cc *commitStatsCalculator) doChangeEntry(r *git.Repository, ch *object.ChangeEntry) (fileStats, error) {
155+
blob, err := r.BlobObject(ch.TreeEntry.Hash)
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
return cc.doBlob(blob, ch.Name)
161+
}
162+
163+
func (cc *commitStatsCalculator) doBlob(blob *object.Blob, filename string) (fileStats, error) {
164+
if enry.IsVendor(filename) {
165+
return nil, nil
166+
}
167+
168+
isBinary, err := isBinary(blob)
169+
if err != nil {
170+
return nil, err
171+
}
172+
173+
if isBinary {
174+
return nil, nil
175+
}
176+
177+
lang := cc.getLanguage(filename)
178+
179+
return newFileStats(blob, lang)
180+
}
181+
182+
func (*commitStatsCalculator) getLanguage(filename string) string {
183+
if lang, ok := enry.GetLanguageByFilename(filename); ok {
184+
return lang
185+
}
186+
187+
if lang, ok := enry.GetLanguageByExtension(filename); ok {
188+
return lang
189+
}
190+
191+
return ""
192+
}
193+
194+
// KindStats represents the stats for a kind of lines in a file.
195+
type KindStats struct {
196+
// Additions number of lines added.
197+
Additions int
198+
// Deletions number of lines deleted.
199+
Deletions int
200+
}
201+
202+
// Add adds the given stats to this stats.
203+
func (k *KindStats) Add(add KindStats) {
204+
k.Additions += add.Additions
205+
k.Deletions += add.Deletions
206+
}
207+
208+
// CommitStats represents the stats for a commit.
209+
type CommitStats struct {
210+
// Files add/modified/removed by this commit.
211+
Files int
212+
// Code stats of the code lines.
213+
Code KindStats
214+
// Comment stats of the comment lines.
215+
Comment KindStats
216+
// Blank stats of the blank lines.
217+
Blank KindStats
218+
// Other stats of files that are not from a recognized or format language.
219+
Other KindStats
220+
// Total the sum of the previous stats.
221+
Total KindStats
222+
}
223+
224+
// Add adds the given stats to this stats.
225+
func (s *CommitStats) Add(stats *CommitStats) {
226+
s.Code.Add(stats.Code)
227+
s.Comment.Add(stats.Comment)
228+
s.Blank.Add(stats.Blank)
229+
s.Other.Add(stats.Other)
230+
s.Total.Add(stats.Total)
231+
}
232+
233+
func (s *CommitStats) String() string {
234+
return fmt.Sprintf("Code (+%d/-%d)\nComment (+%d/-%d)\nBlank (+%d/-%d)\nOther (+%d/-%d)\nTotal (+%d/-%d)\nFiles (%d)\n",
235+
s.Code.Additions, s.Code.Deletions,
236+
s.Comment.Additions, s.Comment.Deletions,
237+
s.Blank.Additions, s.Blank.Deletions,
238+
s.Other.Additions, s.Other.Deletions,
239+
s.Total.Additions, s.Total.Deletions,
240+
s.Files,
241+
)
242+
}
243+
244+
var languages = gocloc.NewDefinedLanguages()
245+
246+
type fileStats map[string]*LineInfo
247+
248+
// LineInfo represents the information about a sigle line.
249+
type LineInfo struct {
250+
Kind LineKind
251+
Count int
252+
}
253+
254+
func newFileStats(f *object.Blob, lang string) (fileStats, error) {
255+
ff := make(fileStats, 50)
256+
257+
r, err := f.Reader()
258+
if err != nil {
259+
return ff, err
260+
}
261+
262+
defer ioutil.CheckClose(r, &err)
263+
264+
l, ok := languages.Langs[lang]
265+
if ok {
266+
doNewFileStatsGoCloc(r, l, &ff)
267+
return ff, nil
268+
}
269+
270+
return ff, doNewFileStatsPlain(r, &ff)
271+
}
272+
273+
func doNewFileStatsGoCloc(r io.Reader, l *gocloc.Language, ff *fileStats) {
274+
gocloc.AnalyzeReader("", l, r, &gocloc.ClocOptions{
275+
OnBlank: ff.addBlank,
276+
OnCode: ff.addCode,
277+
OnComment: ff.addComment,
278+
})
279+
}
280+
281+
func doNewFileStatsPlain(r io.Reader, ff *fileStats) error {
282+
s := bufio.NewScanner(r)
283+
for s.Scan() {
284+
ff.addOther(s.Text())
285+
286+
}
287+
288+
return s.Err()
289+
}
290+
291+
func (fi fileStats) addCode(line string) { fi.add(line, Code) }
292+
func (fi fileStats) addComment(line string) { fi.add(line, Comment) }
293+
func (fi fileStats) addBlank(line string) { fi.add(line, Blank) }
294+
func (fi fileStats) addOther(line string) { fi.add(line, Other) }
295+
func (fi fileStats) add(line string, k LineKind) {
296+
if fi[line] == nil {
297+
fi[line] = &LineInfo{}
298+
}
299+
300+
fi[line].Count++
301+
fi[line].Kind = k
302+
}
303+
304+
func (fi fileStats) sub(to fileStats) {
305+
for line, i := range to {
306+
if _, ok := fi[line]; ok {
307+
fi[line].Count -= i.Count
308+
} else {
309+
fi[line] = i
310+
fi[line].Count *= -1
311+
}
312+
}
313+
}
314+
315+
func (fi fileStats) stats() *CommitStats {
316+
stats := &CommitStats{}
317+
for _, info := range fi {
318+
fillKindStats(&stats.Total, info)
319+
switch info.Kind {
320+
case Code:
321+
fillKindStats(&stats.Code, info)
322+
case Comment:
323+
fillKindStats(&stats.Comment, info)
324+
case Blank:
325+
fillKindStats(&stats.Blank, info)
326+
case Other:
327+
fillKindStats(&stats.Other, info)
328+
}
329+
}
330+
331+
return stats
332+
}
333+
334+
func fillKindStats(ks *KindStats, info *LineInfo) {
335+
if info.Count > 0 {
336+
ks.Additions += info.Count
337+
}
338+
if info.Count < 0 {
339+
ks.Deletions += (info.Count * -1)
340+
}
341+
}
342+
343+
func (fi fileStats) String() string {
344+
buf := bytes.NewBuffer(nil)
345+
for line, i := range fi {
346+
sign := ' '
347+
switch {
348+
case i.Count > 0:
349+
sign = '+'
350+
case i.Count < 0:
351+
sign = '-'
352+
}
353+
354+
fmt.Fprintf(buf, "%c [%3dx] %s\n", sign, i.Count, line)
355+
}
356+
357+
return buf.String()
358+
}
359+
360+
func isBinary(b *object.Blob) (bin bool, err error) {
361+
reader, err := b.Reader()
362+
if err != nil {
363+
return false, err
364+
}
365+
366+
defer ioutil.CheckClose(reader, &err)
367+
return binary.IsBinary(reader)
368+
}

0 commit comments

Comments
 (0)