#63 Git stats libgit2

Merged Vialeon

No flags found

Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.

e.g., #unittest #integration

#production #enterprise

#frontend #backend

Learn more about Codecov Flags here.


@@ -30,9 +30,7 @@
Loading
30 30
			committer_when DATETIME, 
31 31
			parent_id TEXT,
32 32
			parent_count INT,
33 -
			tree_id TEXT,
34 -
			additions INT,
35 -
			deletions INT
33 +
			tree_id TEXT
36 34
		)`, args[0]))
37 35
	if err != nil {
38 36
		return nil, err
@@ -154,11 +152,6 @@
Loading
154 152
	case 11:
155 153
		//tree_id
156 154
		c.ResultText(current.TreeID)
157 -
	case 12:
158 -
		c.ResultInt(current.Additions)
159 -
	case 13:
160 -
		c.ResultInt(current.Deletions)
161 -
162 155
	}
163 156
	return nil
164 157
}

@@ -5,8 +5,11 @@
Loading
5 5
	"database/sql"
6 6
	"fmt"
7 7
	"os/exec"
8 +
	"os/user"
9 +
	"path"
8 10
	"strings"
9 11
12 +
	"github.com/gitsight/go-vcsurl"
10 13
	git "github.com/libgit2/git2go/v30"
11 14
	"github.com/mattn/go-sqlite3"
12 15
)
@@ -47,6 +50,10 @@
Loading
47 50
			if err != nil {
48 51
				return err
49 52
			}
53 +
			err = conn.CreateModule("git_stats", &gitStatsModule{})
54 +
			if err != nil {
55 +
				return err
56 +
			}
50 57
51 58
			err = loadHelperFuncs(conn)
52 59
			if err != nil {
@@ -91,13 +98,18 @@
Loading
91 98
		if err != nil {
92 99
			return err
93 100
		}
101 +
94 102
	} else {
95 103
		_, err := g.DB.Exec(fmt.Sprintf("CREATE VIRTUAL TABLE IF NOT EXISTS commits USING git_log_cli('%s');", g.RepoPath))
96 104
		if err != nil {
97 105
			return err
98 106
		}
99 107
100 108
	}
109 +
	_, err = g.DB.Exec(fmt.Sprintf("CREATE VIRTUAL TABLE IF NOT EXISTS stats USING git_stats('%s');", g.RepoPath))
110 +
	if err != nil {
111 +
		return err
112 +
	}
101 113
102 114
	_, err = g.DB.Exec(fmt.Sprintf("CREATE VIRTUAL TABLE IF NOT EXISTS files USING git_tree('%s');", g.RepoPath))
103 115
	if err != nil {
@@ -131,3 +143,26 @@
Loading
131 143
132 144
	return nil
133 145
}
146 +
func CreateAuthenticationCallback(remote *vcsurl.VCS) *git.CloneOptions {
147 +
	cloneOptions := &git.CloneOptions{}
148 +
149 +
	if _, err := remote.Remote(vcsurl.SSH); err == nil { // if SSH, use "default" credentials
150 +
		// use FetchOptions instead of directly RemoteCallbacks
151 +
		// https://github.com/libgit2/git2go/commit/36e0a256fe79f87447bb730fda53e5cbc90eb47c
152 +
		cloneOptions.FetchOptions = &git.FetchOptions{
153 +
			RemoteCallbacks: git.RemoteCallbacks{
154 +
				CredentialsCallback: func(url string, username string, allowedTypes git.CredType) (*git.Cred, error) {
155 +
					usr, _ := user.Current()
156 +
					publicSSH := path.Join(usr.HomeDir, ".ssh/id_rsa.pub")
157 +
					privateSSH := path.Join(usr.HomeDir, ".ssh/id_rsa")
158 +
159 +
					cred, ret := git.NewCredSshKey("git", publicSSH, privateSSH, "")
160 +
					return cred, ret
161 +
				},
162 +
				CertificateCheckCallback: func(cert *git.Certificate, valid bool, hostname string) git.ErrorCode {
163 +
					return git.ErrOk
164 +
				},
165 +
			}}
166 +
	}
167 +
	return cloneOptions
168 +
}

@@ -0,0 +1,196 @@
Loading
1 +
package gitqlite
2 +
3 +
import (
4 +
	"io"
5 +
6 +
	git "github.com/libgit2/git2go/v30"
7 +
)
8 +
9 +
type commitStat struct {
10 +
	commitID  string
11 +
	file      string
12 +
	additions int
13 +
	deletions int
14 +
}
15 +
16 +
type commitStatsIter struct {
17 +
	repo                   *git.Repository
18 +
	commitIter             *git.RevWalk
19 +
	currentCommit          *git.Commit
20 +
	commitStats            []*commitStat
21 +
	currentCommitStatIndex int
22 +
}
23 +
24 +
type commitStatsIterOptions struct {
25 +
	commitID string
26 +
}
27 +
28 +
func stats(commit *git.Commit) ([]*commitStat, error) {
29 +
30 +
	stats := make([]*commitStat, 0)
31 +
32 +
	repo := commit.Owner()
33 +
	tree, err := commit.Tree()
34 +
	if err != nil {
35 +
		return nil, err
36 +
	}
37 +
	defer tree.Free()
38 +
39 +
	var parentTree *git.Tree
40 +
	parent := commit.Parent(0)
41 +
	if parent == nil {
42 +
		parentTree = &git.Tree{}
43 +
	} else {
44 +
		parentTree, err = parent.Tree()
45 +
		if err != nil {
46 +
			return nil, err
47 +
		}
48 +
		defer parentTree.Free()
49 +
	}
50 +
51 +
	diffOpts, err := git.DefaultDiffOptions()
52 +
	if err != nil {
53 +
		return nil, err
54 +
	}
55 +
	diff, err := repo.DiffTreeToTree(parentTree, tree, &diffOpts)
56 +
	if err != nil {
57 +
		return nil, err
58 +
	}
59 +
	diffFindOpts, err := git.DefaultDiffFindOptions()
60 +
	if err != nil {
61 +
		return nil, err
62 +
	}
63 +
	err = diff.FindSimilar(&diffFindOpts)
64 +
	if err != nil {
65 +
		return nil, err
66 +
	}
67 +
68 +
	err = diff.ForEach(func(delta git.DiffDelta, progress float64) (git.DiffForEachHunkCallback, error) {
69 +
		stat := &commitStat{
70 +
			commitID: commit.Id().String(),
71 +
			file:     delta.NewFile.Path,
72 +
		}
73 +
		stats = append(stats, stat)
74 +
		return func(hunk git.DiffHunk) (git.DiffForEachLineCallback, error) {
75 +
			return func(line git.DiffLine) error {
76 +
				switch line.Origin {
77 +
				case git.DiffLineAddition:
78 +
					stat.additions++
79 +
				case git.DiffLineDeletion:
80 +
					stat.deletions++
81 +
				}
82 +
				return nil
83 +
			}, nil
84 +
		}, nil
85 +
	}, git.DiffDetailLines)
86 +
87 +
	if err != nil {
88 +
		return nil, err
89 +
	}
90 +
	return stats, nil
91 +
}
92 +
93 +
func NewCommitStatsIter(repo *git.Repository, opt *commitStatsIterOptions) (*commitStatsIter, error) {
94 +
	if opt.commitID == "" {
95 +
		revWalk, err := repo.Walk()
96 +
		if err != nil {
97 +
			return nil, err
98 +
		}
99 +
100 +
		err = revWalk.PushHead()
101 +
		if err != nil {
102 +
			return nil, err
103 +
		}
104 +
105 +
		revWalk.Sorting(git.SortNone)
106 +
107 +
		return &commitStatsIter{
108 +
			repo:                   repo,
109 +
			commitIter:             revWalk,
110 +
			currentCommit:          nil,
111 +
			commitStats:            make([]*commitStat, 0),
112 +
			currentCommitStatIndex: 100, // init with an index greater than above array, so that the first call to Next() sets up the first commit, rather than trying to return a current Blob
113 +
		}, nil
114 +
115 +
	} else {
116 +
		commitID, err := git.NewOid(opt.commitID)
117 +
		if err != nil {
118 +
			return nil, err
119 +
		}
120 +
121 +
		commit, err := repo.LookupCommit(commitID)
122 +
		if err != nil {
123 +
			return nil, err
124 +
		}
125 +
126 +
		commitStats, err := stats(commit)
127 +
		if err != nil {
128 +
			return nil, err
129 +
		}
130 +
131 +
		return &commitStatsIter{
132 +
			repo:                   repo,
133 +
			commitIter:             nil,
134 +
			currentCommit:          commit,
135 +
			commitStats:            commitStats,
136 +
			currentCommitStatIndex: 0,
137 +
		}, nil
138 +
	}
139 +
}
140 +
141 +
func (iter *commitStatsIter) Next() (*commitStat, error) {
142 +
	defer func() {
143 +
		iter.currentCommitStatIndex++
144 +
	}()
145 +
146 +
	if iter.currentCommitStatIndex < len(iter.commitStats) {
147 +
		return iter.commitStats[iter.currentCommitStatIndex], nil
148 +
	}
149 +
150 +
	// if the commitIter is nil, there are no commits to iterate over, end
151 +
	// this assumes that a currentCommit was set when this was first called, with commitStats already populated
152 +
	if iter.commitIter == nil {
153 +
		return nil, io.EOF
154 +
	}
155 +
156 +
	id := new(git.Oid)
157 +
	err := iter.commitIter.Next(id)
158 +
	if err != nil {
159 +
		if id.IsZero() {
160 +
			return nil, io.EOF
161 +
		}
162 +
163 +
		return nil, err
164 +
	}
165 +
166 +
	commit, err := iter.repo.LookupCommit(id)
167 +
	if err != nil {
168 +
		return nil, err
169 +
	}
170 +
171 +
	iter.currentCommit = commit
172 +
173 +
	commitStats, err := stats(commit)
174 +
	if err != nil {
175 +
		return nil, err
176 +
	}
177 +
178 +
	iter.commitStats = commitStats
179 +
	iter.currentCommitStatIndex = 0
180 +
181 +
	if len(commitStats) == 0 {
182 +
		return iter.Next()
183 +
	}
184 +
185 +
	return commitStats[iter.currentCommitStatIndex], nil
186 +
}
187 +
188 +
func (iter *commitStatsIter) Close() {
189 +
	if iter == nil {
190 +
		return
191 +
	}
192 +
	iter.currentCommit.Free()
193 +
	if iter.commitIter != nil {
194 +
		iter.commitIter.Free()
195 +
	}
196 +
}

@@ -0,0 +1,150 @@
Loading
1 +
package gitqlite
2 +
3 +
import (
4 +
	"fmt"
5 +
	"io"
6 +
7 +
	git "github.com/libgit2/git2go/v30"
8 +
	"github.com/mattn/go-sqlite3"
9 +
)
10 +
11 +
type gitStatsModule struct{}
12 +
13 +
type gitStatsTable struct {
14 +
	repoPath string
15 +
	repo     *git.Repository
16 +
}
17 +
18 +
func (m *gitStatsModule) Create(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) {
19 +
	err := c.DeclareVTab(fmt.Sprintf(`
20 +
			CREATE TABLE %q (
21 +
			commit_id TEXT,
22 +
			file TEXT,
23 +
			additions INT,
24 +
			deletions INT
25 +
			)`, args[0]))
26 +
	if err != nil {
27 +
		return nil, err
28 +
	}
29 +
30 +
	// the repoPath will be enclosed in double quotes "..." since ensureTables uses %q when setting up the table
31 +
	// we need to pop those off when referring to the actual directory in the fs
32 +
	repoPath := args[3][1 : len(args[3])-1]
33 +
	return &gitStatsTable{repoPath: repoPath}, nil
34 +
}
35 +
36 +
func (m *gitStatsModule) Connect(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) {
37 +
	return m.Create(c, args)
38 +
}
39 +
40 +
func (m *gitStatsModule) DestroyModule() {}
41 +
42 +
func (v *gitStatsTable) Open() (sqlite3.VTabCursor, error) {
43 +
	repo, err := git.OpenRepository(v.repoPath)
44 +
	if err != nil {
45 +
		return nil, err
46 +
	}
47 +
	v.repo = repo
48 +
49 +
	return &StatsCursor{repo: v.repo}, nil
50 +
}
51 +
52 +
func (v *gitStatsTable) BestIndex(cst []sqlite3.InfoConstraint, ob []sqlite3.InfoOrderBy) (*sqlite3.IndexResult, error) {
53 +
	used := make([]bool, len(cst))
54 +
	// TODO implement an index for file name glob patterns?
55 +
	// TODO this loop construct won't work well for multiple constraints...
56 +
	for c, constraint := range cst {
57 +
		switch {
58 +
		case constraint.Usable && constraint.Column == 0 && constraint.Op == sqlite3.OpEQ:
59 +
			used[c] = true
60 +
			return &sqlite3.IndexResult{Used: used, IdxNum: 1, IdxStr: "stats-by-commit-id", EstimatedCost: 1.0, EstimatedRows: 1}, nil
61 +
		}
62 +
	}
63 +
64 +
	return &sqlite3.IndexResult{Used: used, EstimatedCost: 100}, nil
65 +
}
66 +
67 +
func (v *gitStatsTable) Disconnect() error {
68 +
	v.repo = nil
69 +
	return nil
70 +
}
71 +
func (v *gitStatsTable) Destroy() error { return nil }
72 +
73 +
type StatsCursor struct {
74 +
	repo     *git.Repository
75 +
	iterator *commitStatsIter
76 +
	current  *commitStat
77 +
}
78 +
79 +
func (vc *StatsCursor) Column(c *sqlite3.SQLiteContext, col int) error {
80 +
	stat := vc.current
81 +
	switch col {
82 +
	case 0:
83 +
		//commit id
84 +
		c.ResultText(stat.commitID)
85 +
	case 1:
86 +
		c.ResultText(stat.file)
87 +
	case 2:
88 +
		c.ResultInt(stat.additions)
89 +
	case 3:
90 +
		c.ResultInt(stat.deletions)
91 +
92 +
	}
93 +
94 +
	return nil
95 +
}
96 +
func (vc *StatsCursor) Filter(idxNum int, idxStr string, vals []interface{}) error {
97 +
	var opt *commitStatsIterOptions
98 +
99 +
	switch idxNum {
100 +
	case 0:
101 +
		opt = &commitStatsIterOptions{}
102 +
	case 1:
103 +
		opt = &commitStatsIterOptions{commitID: vals[0].(string)}
104 +
	}
105 +
106 +
	iter, err := NewCommitStatsIter(vc.repo, opt)
107 +
	if err != nil {
108 +
		return err
109 +
	}
110 +
111 +
	vc.iterator = iter
112 +
113 +
	file, err := vc.iterator.Next()
114 +
	if err != nil {
115 +
		if err == io.EOF {
116 +
			vc.current = nil
117 +
			return nil
118 +
		}
119 +
		return err
120 +
	}
121 +
122 +
	vc.current = file
123 +
	return nil
124 +
}
125 +
126 +
func (vc *StatsCursor) Next() error {
127 +
	file, err := vc.iterator.Next()
128 +
	if err != nil {
129 +
		if err == io.EOF {
130 +
			vc.current = nil
131 +
			return nil
132 +
		}
133 +
		return err
134 +
	}
135 +
	vc.current = file
136 +
	return nil
137 +
}
138 +
139 +
func (vc *StatsCursor) EOF() bool {
140 +
	return vc.current == nil
141 +
}
142 +
143 +
func (vc *StatsCursor) Rowid() (int64, error) {
144 +
	return int64(0), nil
145 +
}
146 +
147 +
func (vc *StatsCursor) Close() error {
148 +
	vc.iterator.Close()
149 +
	return nil
150 +
}

@@ -31,9 +31,7 @@
Loading
31 31
			committer_when DATETIME, 
32 32
			parent_id TEXT,
33 33
			parent_count INT,
34 -
			tree_id TEXT,
35 -
			additions INT,
36 -
			deletions INT
34 +
			tree_id TEXT
37 35
		)`, args[0]))
38 36
	if err != nil {
39 37
		return nil, err

Learn more Showing 3 files with coverage changes found.

Changes in pkg/gitqlite/git_log.go
-33
-13
+46
Loading file...
New file pkg/gitqlite/git_stats.go
New
Loading file...
New file pkg/gitqlite/git_stats_iter.go
New
Loading file...

37 Commits

Hiding 1 contexual commits
Hiding 2 contexual commits
-8
-41
-13
+46
+2
+23
+1
-22
Hiding 2 contexual commits
+1 Files
+61
+90
+17
-46
-1
-1
-30
-16
-3
-11
+49
+16
+3
+30
-2
-2
+1
+1
-74
-74
+14
+7
+1
+6
+1
+1
Hiding 1 contexual commits
+3
+3
Hiding 6 contexual commits Hiding 6 contexual commits
+1 Files
+169
+13
+3
+153
Pull Request Base Commit
Files Coverage
pkg -3.22% 63.86%
Project Totals (11 files) 63.86%
Loading