Browse Source

Enrich the git package

Brendan Abolivier 6 years ago
parent
commit
8603a20016
Signed by: Brendan Abolivier <contact@brendanabolivier.com> GPG key ID: 8EF1500759F70623
2 changed files with 184 additions and 71 deletions
  1. 177
    68
      src/git/git.go
  2. 7
    3
      src/puller/puller.go

+ 177
- 68
src/git/git.go View File

@@ -10,87 +10,227 @@ import (
10 10
 	"github.com/sirupsen/logrus"
11 11
 	"golang.org/x/crypto/ssh"
12 12
 	gogit "gopkg.in/src-d/go-git.v4"
13
+	"gopkg.in/src-d/go-git.v4/plumbing"
14
+	"gopkg.in/src-d/go-git.v4/plumbing/object"
15
+	"gopkg.in/src-d/go-git.v4/plumbing/storer"
13 16
 	"gopkg.in/src-d/go-git.v4/plumbing/transport"
14 17
 	gitssh "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh"
15 18
 )
16 19
 
20
+type Repository struct {
21
+	Repo *gogit.Repository
22
+	cfg  config.GitSettings
23
+	auth *gitssh.PublicKeys
24
+}
25
+
26
+func NewRepository(cfg config.GitSettings) (r *Repository, invalidRepo bool, err error) {
27
+	repo, err := gogit.PlainOpen(cfg.ClonePath)
28
+	if err != nil {
29
+		if err == gogit.ErrRepositoryNotExists {
30
+			invalidRepo = true
31
+		} else {
32
+			return
33
+		}
34
+	}
35
+
36
+	r = &Repository{
37
+		Repo: repo,
38
+		cfg:  cfg,
39
+	}
40
+
41
+	err = r.getAuth()
42
+	return
43
+}
44
+
17 45
 // Sync synchronises a Git repository using a given configuration. "synchronises"
18 46
 // means that, if the repo from the configuration isn't already cloned in the
19
-// directory specified in the configuration, it will clone the repository,
20
-// else it will simply pull it in order to be up to date with the remote.
47
+// directory specified in the configuration, it will clone the repository (unless
48
+// if explicitely told not to), else it will simply pull it in order to be up to
49
+// date with the remote.
21 50
 // Returns the go-git representation of the repository.
22 51
 // Returns an error if there was an issue loading the SSH private key, checking
23 52
 // whether the clone path already exists, or synchronising the repo with the
24 53
 // remote.
25
-func Sync(cfg config.GitSettings) (r *gogit.Repository, err error) {
26
-	// Generate an authentication structure instance from the user and private
27
-	// key
28
-	auth, err := getAuth(cfg.User, cfg.PrivateKeyPath)
29
-	if err != nil {
30
-		return
31
-	}
32
-
54
+func (r *Repository) Sync(dontClone bool) (err error) {
33 55
 	// Check whether the clone path already exists
34
-	exists, err := dirExists(cfg.ClonePath)
56
+	exists, err := dirExists(r.cfg.ClonePath)
35 57
 	if err != nil {
36 58
 		return
37 59
 	}
38 60
 
39 61
 	// Check whether the clone path is a Git repository
40 62
 	var isRepo bool
41
-	if isRepo, err = dirExists(cfg.ClonePath + "/.git"); err != nil {
63
+	if isRepo, err = dirExists(r.cfg.ClonePath + "/.git"); err != nil {
42 64
 		return
43 65
 	} else if exists && !isRepo {
44 66
 		err = fmt.Errorf(
45 67
 			"%s already exists but is not a Git repository",
46
-			cfg.ClonePath,
68
+			r.cfg.ClonePath,
47 69
 		)
48 70
 
49 71
 		return
50 72
 	}
51 73
 
52 74
 	logrus.WithFields(logrus.Fields{
53
-		"repo":       cfg.User + "@" + cfg.URL,
54
-		"clone_path": cfg.ClonePath,
75
+		"repo":       r.cfg.User + "@" + r.cfg.URL,
76
+		"clone_path": r.cfg.ClonePath,
55 77
 		"pull":       exists,
56 78
 	}).Info("Synchronising the Git repository with the remote")
57 79
 
58 80
 	// If the clone path already exists, pull from the remote, else clone it.
59 81
 	if exists {
60
-		r, err = pull(cfg.ClonePath, auth)
61
-	} else {
62
-		r, err = clone(cfg.URL, cfg.ClonePath, auth)
82
+		err = r.pull()
83
+	} else if !dontClone {
84
+		err = r.clone()
63 85
 	}
64 86
 
65 87
 	return
66 88
 }
67 89
 
90
+// Push uses a given repository and configuration to push the local history of
91
+// the said repository to the remote, using an authentication structure instance
92
+// created from the configuration to authenticate on the remote.
93
+// Returns with an error if there was an issue creating the authentication
94
+// structure instance or pushing to the remote. In the latter case, if the error
95
+// is a known non-error, doesn't return any error.
96
+func (r *Repository) Push() (err error) {
97
+	logrus.WithFields(logrus.Fields{
98
+		"repo":       r.cfg.User + "@" + r.cfg.URL,
99
+		"clone_path": r.cfg.ClonePath,
100
+	}).Info("Pushing to the remote")
101
+
102
+	// Push to remote
103
+	if err = r.Repo.Push(&gogit.PushOptions{
104
+		Auth: r.auth,
105
+	}); err != nil {
106
+		// Check error against known non-errors
107
+		err = checkRemoteErrors(err, logrus.Fields{
108
+			"repo":       r.cfg.User + "@" + r.cfg.URL,
109
+			"clone_path": r.cfg.ClonePath,
110
+			"error":      err,
111
+		})
112
+	}
113
+
114
+	return err
115
+}
116
+
117
+func (r *Repository) GetLatestCommit() (*object.Commit, error) {
118
+	// Retrieve latest hash
119
+	refs, err := r.Repo.References()
120
+	if err != nil {
121
+		return nil, err
122
+	}
123
+
124
+	ref, err := refs.Next()
125
+	if err != nil {
126
+		return nil, err
127
+	}
128
+
129
+	hash := ref.Hash()
130
+	return r.Repo.CommitObject(hash)
131
+}
132
+
133
+func (r *Repository) Log(fromHash string) (object.CommitIter, error) {
134
+	hash := plumbing.NewHash(fromHash)
135
+
136
+	return r.Repo.Log(&gogit.LogOptions{
137
+		From: hash,
138
+	})
139
+}
140
+
141
+func (r *Repository) LineCountsDeltasIgnoreManagerCommits(
142
+	from *object.Commit, to *object.Commit,
143
+) (lineCountsDeltas map[string]int, err error) {
144
+	// We expect "from" to be the oldest commit, and "to" to be the most recent
145
+	// one. Because Log() works the other way (in anti-chronological order),
146
+	// we call it with "to" and not "from" because, that way, we'll go from "to"
147
+	// to "from".
148
+	iter, err := r.Log(to.Hash.String())
149
+	if err != nil {
150
+		return
151
+	}
152
+
153
+	lineCountsDeltas = make(map[string]int)
154
+	err = iter.ForEach(func(commit *object.Commit) error {
155
+		if commit.Author.Email == r.cfg.CommitsAuthor.Email {
156
+			return nil
157
+		}
158
+
159
+		if commit.Hash.String() == from.Hash.String() {
160
+			return storer.ErrStop
161
+		}
162
+
163
+		stats, err := commit.Stats()
164
+		if err != nil {
165
+			return err
166
+		}
167
+
168
+		for _, stat := range stats {
169
+			// We're getting recent -> old additions and deletions. Because we
170
+			// want the opposite (old -> recent), we must invert the sign of both.
171
+			lineCountsDeltas[stat.Name] = stat.Deletion - stat.Addition
172
+		}
173
+
174
+		return nil
175
+	})
176
+
177
+	return
178
+}
179
+
180
+func GetFilesLineCountsAtCommit(commit *object.Commit) (map[string]int, error) {
181
+	tree, err := commit.Tree()
182
+	if err != nil {
183
+		return nil, err
184
+	}
185
+
186
+	lineCounts := make(map[string]int)
187
+
188
+	files := tree.Files()
189
+
190
+	var lines []string
191
+	err = files.ForEach(func(file *object.File) error {
192
+		lines, err = file.Lines()
193
+		if err != nil {
194
+			return err
195
+		}
196
+
197
+		lineCounts[file.Name] = len(lines)
198
+
199
+		return nil
200
+	})
201
+
202
+	return lineCounts, err
203
+}
204
+
68 205
 // getAuth returns the authentication structure instance needed to authenticate
69 206
 // on the remote, using a given user and private key path.
70 207
 // Returns an error if there was an issue reading the private key file or
71 208
 // parsing it.
72
-func getAuth(user string, privateKeyPath string) (*gitssh.PublicKeys, error) {
73
-	privateKey, err := ioutil.ReadFile(privateKeyPath)
209
+func (r *Repository) getAuth() error {
210
+	privateKey, err := ioutil.ReadFile(r.cfg.PrivateKeyPath)
74 211
 	if err != nil {
75
-		return nil, err
212
+		return err
76 213
 	}
77 214
 
78 215
 	signer, err := ssh.ParsePrivateKey(privateKey)
79 216
 	if err != nil {
80
-		return nil, err
217
+		return err
81 218
 	}
82 219
 
83
-	return &gitssh.PublicKeys{User: user, Signer: signer}, nil
220
+	r.auth = &gitssh.PublicKeys{User: r.cfg.User, Signer: signer}
221
+	return nil
84 222
 }
85 223
 
86 224
 // clone clones a Git repository into a given path, using a given auth.
87 225
 // Returns the go-git representation of the Git repository.
88 226
 // Returns an error if there was an issue cloning the repository.
89
-func clone(repo string, clonePath string, auth *gitssh.PublicKeys) (*gogit.Repository, error) {
90
-	return gogit.PlainClone(clonePath, false, &gogit.CloneOptions{
91
-		URL:  repo,
92
-		Auth: auth,
227
+func (r *Repository) clone() (err error) {
228
+	r.Repo, err = gogit.PlainClone(r.cfg.ClonePath, false, &gogit.CloneOptions{
229
+		URL:  r.cfg.URL,
230
+		Auth: r.auth,
93 231
 	})
232
+
233
+	return err
94 234
 }
95 235
 
96 236
 // pull opens the repository located at a given path, and pulls it from the
@@ -99,32 +239,34 @@ func clone(repo string, clonePath string, auth *gitssh.PublicKeys) (*gogit.Repos
99 239
 // Returns an error if there was an issue opening the repo, getting its work
100 240
 // tree or pulling from the remote. In the latter case, if the error is a known
101 241
 // non-error, doesn't return any error.
102
-func pull(clonePath string, auth *gitssh.PublicKeys) (*gogit.Repository, error) {
242
+func (r *Repository) pull() error {
103 243
 	// Open the repository
104
-	r, err := gogit.PlainOpen(clonePath)
244
+	repo, err := gogit.PlainOpen(r.cfg.ClonePath)
105 245
 	if err != nil {
106
-		return nil, err
246
+		return err
107 247
 	}
108 248
 
109 249
 	// Get its worktree
110
-	w, err := r.Worktree()
250
+	w, err := repo.Worktree()
111 251
 	if err != nil {
112
-		return nil, err
252
+		return err
113 253
 	}
114 254
 
115 255
 	// Pull from remote
116 256
 	if err = w.Pull(&gogit.PullOptions{
117 257
 		RemoteName: "origin",
118
-		Auth:       auth,
258
+		Auth:       r.auth,
119 259
 	}); err != nil {
120 260
 		// Check error against known non-errors
121 261
 		err = checkRemoteErrors(err, logrus.Fields{
122
-			"clone_path": clonePath,
262
+			"clone_path": r.cfg.ClonePath,
123 263
 			"error":      err,
124 264
 		})
125 265
 	}
126 266
 
127
-	return r, err
267
+	r.Repo = repo
268
+
269
+	return err
128 270
 }
129 271
 
130 272
 // dirExists is a snippet checking if a directory exists on the disk.
@@ -141,39 +283,6 @@ func dirExists(path string) (bool, error) {
141 283
 	return true, err
142 284
 }
143 285
 
144
-// Push uses a given repository and configuration to push the local history of
145
-// the said repository to the remote, using an authentication structure instance
146
-// created from the configuration to authenticate on the remote.
147
-// Returns with an error if there was an issue creating the authentication
148
-// structure instance or pushing to the remote. In the latter case, if the error
149
-// is a known non-error, doesn't return any error.
150
-func Push(r *gogit.Repository, cfg config.GitSettings) error {
151
-	// Get the authentication structure instance
152
-	auth, err := getAuth(cfg.User, cfg.PrivateKeyPath)
153
-	if err != nil {
154
-		return err
155
-	}
156
-
157
-	logrus.WithFields(logrus.Fields{
158
-		"repo":       cfg.User + "@" + cfg.URL,
159
-		"clone_path": cfg.ClonePath,
160
-	}).Info("Pushing to the remote")
161
-
162
-	// Push to remote
163
-	if err = r.Push(&gogit.PushOptions{
164
-		Auth: auth,
165
-	}); err != nil {
166
-		// Check error against known non-errors
167
-		err = checkRemoteErrors(err, logrus.Fields{
168
-			"repo":       cfg.User + "@" + cfg.URL,
169
-			"clone_path": cfg.ClonePath,
170
-			"error":      err,
171
-		})
172
-	}
173
-
174
-	return err
175
-}
176
-
177 286
 // processRemoteErrors checks an error against known non-errors returned when
178 287
 // communicating with the remote. If the error is a non-error, returns nil and
179 288
 // logs it with the provided fields. If not, returns the error.

+ 7
- 3
src/puller/puller.go View File

@@ -27,12 +27,16 @@ type diffVersion struct {
27 27
 // repo.
28 28
 func PullGrafanaAndCommit(client *grafana.Client, cfg *config.Config) error {
29 29
 	// Clone or pull the repo
30
-	repo, err := git.Sync(cfg.Git)
30
+	repo, _, err := git.NewRepository(cfg.Git)
31 31
 	if err != nil {
32 32
 		return err
33 33
 	}
34 34
 
35
-	w, err := repo.Worktree()
35
+	if err = repo.Sync(false); err != nil {
36
+		return err
37
+	}
38
+
39
+	w, err := repo.Repo.Worktree()
36 40
 	if err != nil {
37 41
 		return err
38 42
 	}
@@ -124,7 +128,7 @@ func PullGrafanaAndCommit(client *grafana.Client, cfg *config.Config) error {
124 128
 
125 129
 	// Push the changes (we don't do it in the if clause above in case there are
126 130
 	// pending commits in the local repo that haven't been pushed yet).
127
-	if err = git.Push(repo, cfg.Git); err != nil {
131
+	if err = repo.Push(); err != nil {
128 132
 		return err
129 133
 	}
130 134