|
@@ -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.
|