1package gitlab
2
3import (
4 "fmt"
5 "strconv"
6 "time"
7
8 "github.com/pkg/errors"
9 "github.com/xanzy/go-gitlab"
10
11 "github.com/MichaelMure/git-bug/bridge/core"
12 "github.com/MichaelMure/git-bug/bug"
13 "github.com/MichaelMure/git-bug/cache"
14 "github.com/MichaelMure/git-bug/util/git"
15)
16
17var (
18 ErrMissingIdentityToken = errors.New("missing identity token")
19)
20
21const (
22 keyGitlabId = "gitlab-id"
23 keyGitlabUrl = "gitlab-url"
24 keyOrigin = "origin"
25)
26
27// gitlabExporter implement the Exporter interface
28type gitlabExporter struct {
29 conf core.Configuration
30
31 // cache identities clients
32 identityClient map[string]*gitlab.Client
33
34 // map identities with their tokens
35 identityToken map[string]string
36
37 // gitlab repository ID
38 repositoryID string
39
40 // cache identifiers used to speed up exporting operations
41 // cleared for each bug
42 cachedOperationIDs map[string]string
43
44 // cache labels used to speed up exporting labels events
45 cachedLabels map[string]string
46}
47
48// Init .
49func (ge *gitlabExporter) Init(conf core.Configuration) error {
50 ge.conf = conf
51 //TODO: initialize with multiple tokens
52 ge.identityToken = make(map[string]string)
53 ge.identityClient = make(map[string]*gitlab.Client)
54 ge.cachedOperationIDs = make(map[string]string)
55 ge.cachedLabels = make(map[string]string)
56 return nil
57}
58
59// getIdentityClient return a gitlab v4 API client configured with the access token of the given identity.
60// if no client were found it will initialize it from the known tokens map and cache it for next use
61func (ge *gitlabExporter) getIdentityClient(id string) (*gitlab.Client, error) {
62 client, ok := ge.identityClient[id]
63 if ok {
64 return client, nil
65 }
66
67 // get token
68 token, ok := ge.identityToken[id]
69 if !ok {
70 return nil, ErrMissingIdentityToken
71 }
72
73 // create client
74 client = buildClient(token)
75 // cache client
76 ge.identityClient[id] = client
77
78 return client, nil
79}
80
81// ExportAll export all event made by the current user to Gitlab
82func (ge *gitlabExporter) ExportAll(repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) {
83 out := make(chan core.ExportResult)
84
85 user, err := repo.GetUserIdentity()
86 if err != nil {
87 return nil, err
88 }
89
90 ge.identityToken[user.Id()] = ge.conf[keyToken]
91
92 // get repository node id
93 ge.repositoryID = ge.conf[keyProjectID]
94
95 if err != nil {
96 return nil, err
97 }
98
99 go func() {
100 defer close(out)
101
102 var allIdentitiesIds []string
103 for id := range ge.identityToken {
104 allIdentitiesIds = append(allIdentitiesIds, id)
105 }
106
107 allBugsIds := repo.AllBugsIds()
108
109 for _, id := range allBugsIds {
110 b, err := repo.ResolveBug(id)
111 if err != nil {
112 out <- core.NewExportError(err, id)
113 return
114 }
115
116 snapshot := b.Snapshot()
117
118 // ignore issues created before since date
119 // TODO: compare the Lamport time instead of using the unix time
120 if snapshot.CreatedAt.Before(since) {
121 out <- core.NewExportNothing(b.Id(), "bug created before the since date")
122 continue
123 }
124
125 if snapshot.HasAnyActor(allIdentitiesIds...) {
126 // try to export the bug and it associated events
127 ge.exportBug(b, since, out)
128 } else {
129 out <- core.NewExportNothing(id, "not an actor")
130 }
131 }
132 }()
133
134 return out, nil
135}
136
137// exportBug publish bugs and related events
138func (ge *gitlabExporter) exportBug(b *cache.BugCache, since time.Time, out chan<- core.ExportResult) {
139 snapshot := b.Snapshot()
140
141 var bugGitlabID string
142 var bugGitlabURL string
143 var bugCreationHash string
144
145 // Special case:
146 // if a user try to export a bug that is not already exported to Gitlab (or imported
147 // from Gitlab) and we do not have the token of the bug author, there is nothing we can do.
148
149 // first operation is always createOp
150 createOp := snapshot.Operations[0].(*bug.CreateOperation)
151 author := snapshot.Author
152
153 // skip bug if origin is not allowed
154 origin, ok := snapshot.GetCreateMetadata(keyOrigin)
155 if ok && origin != target {
156 out <- core.NewExportNothing(b.Id(), fmt.Sprintf("issue tagged with origin: %s", origin))
157 return
158 }
159
160 // get gitlab bug ID
161 gitlabID, ok := snapshot.GetCreateMetadata(keyGitlabId)
162 if ok {
163 gitlabURL, ok := snapshot.GetCreateMetadata(keyGitlabUrl)
164 if !ok {
165 // if we find gitlab ID, gitlab URL must be found too
166 err := fmt.Errorf("expected to find gitlab issue URL")
167 out <- core.NewExportError(err, b.Id())
168 }
169
170 //FIXME:
171 // ignore issue comming from other repositories
172
173 out <- core.NewExportNothing(b.Id(), "bug already exported")
174 // will be used to mark operation related to a bug as exported
175 bugGitlabID = gitlabID
176 bugGitlabURL = gitlabURL
177
178 } else {
179 // check that we have a token for operation author
180 client, err := ge.getIdentityClient(author.Id())
181 if err != nil {
182 // if bug is still not exported and we do not have the author stop the execution
183 out <- core.NewExportNothing(b.Id(), fmt.Sprintf("missing author token"))
184 return
185 }
186
187 // create bug
188 id, url, err := createGitlabIssue(client, ge.repositoryID, createOp.Title, createOp.Message)
189 if err != nil {
190 err := errors.Wrap(err, "exporting gitlab issue")
191 out <- core.NewExportError(err, b.Id())
192 return
193 }
194
195 out <- core.NewExportBug(b.Id())
196
197 hash, err := createOp.Hash()
198 if err != nil {
199 err := errors.Wrap(err, "comment hash")
200 out <- core.NewExportError(err, b.Id())
201 return
202 }
203
204 // mark bug creation operation as exported
205 if err := markOperationAsExported(b, hash, id, url); err != nil {
206 err := errors.Wrap(err, "marking operation as exported")
207 out <- core.NewExportError(err, b.Id())
208 return
209 }
210
211 // commit operation to avoid creating multiple issues with multiple pushes
212 if err := b.CommitAsNeeded(); err != nil {
213 err := errors.Wrap(err, "bug commit")
214 out <- core.NewExportError(err, b.Id())
215 return
216 }
217
218 // cache bug gitlab ID and URL
219 bugGitlabID = id
220 bugGitlabURL = url
221 }
222
223 // get createOp hash
224 hash, err := createOp.Hash()
225 if err != nil {
226 out <- core.NewExportError(err, b.Id())
227 return
228 }
229
230 bugCreationHash = hash.String()
231
232 // cache operation gitlab id
233 ge.cachedOperationIDs[bugCreationHash] = bugGitlabID
234
235 for _, op := range snapshot.Operations[1:] {
236 // ignore SetMetadata operations
237 if _, ok := op.(*bug.SetMetadataOperation); ok {
238 continue
239 }
240
241 // get operation hash
242 hash, err := op.Hash()
243 if err != nil {
244 err := errors.Wrap(err, "operation hash")
245 out <- core.NewExportError(err, b.Id())
246 return
247 }
248
249 // ignore operations already existing in gitlab (due to import or export)
250 // cache the ID of already exported or imported issues and events from Gitlab
251 if id, ok := op.GetMetadata(keyGitlabId); ok {
252 ge.cachedOperationIDs[hash.String()] = id
253 out <- core.NewExportNothing(hash.String(), "already exported operation")
254 continue
255 }
256
257 opAuthor := op.GetAuthor()
258 client, err := ge.getIdentityClient(opAuthor.Id())
259 if err != nil {
260 out <- core.NewExportNothing(hash.String(), "missing operation author token")
261 continue
262 }
263
264 var id, url string
265 switch op.(type) {
266 case *bug.AddCommentOperation:
267 opr := op.(*bug.AddCommentOperation)
268
269 // send operation to gitlab
270 id, url, err = addCommentGitlabIssue(client, bugGitlabID, opr.Message)
271 if err != nil {
272 err := errors.Wrap(err, "adding comment")
273 out <- core.NewExportError(err, b.Id())
274 return
275 }
276
277 out <- core.NewExportComment(hash.String())
278
279 // cache comment id
280 ge.cachedOperationIDs[hash.String()] = id
281
282 case *bug.EditCommentOperation:
283
284 opr := op.(*bug.EditCommentOperation)
285 targetHash := opr.Target.String()
286
287 // Since gitlab doesn't consider the issue body as a comment
288 if targetHash == bugCreationHash {
289
290 // case bug creation operation: we need to edit the Gitlab issue
291 if err := updateGitlabIssueBody(client, bugGitlabID, opr.Message); err != nil {
292 err := errors.Wrap(err, "editing issue")
293 out <- core.NewExportError(err, b.Id())
294 return
295 }
296
297 out <- core.NewExportCommentEdition(hash.String())
298
299 id = bugGitlabID
300 url = bugGitlabURL
301
302 } else {
303
304 // case comment edition operation: we need to edit the Gitlab comment
305 commentID, ok := ge.cachedOperationIDs[targetHash]
306 if !ok {
307 panic("unexpected error: comment id not found")
308 }
309
310 eid, eurl, err := editCommentGitlabIssue(client, commentID, opr.Message)
311 if err != nil {
312 err := errors.Wrap(err, "editing comment")
313 out <- core.NewExportError(err, b.Id())
314 return
315 }
316
317 out <- core.NewExportCommentEdition(hash.String())
318
319 // use comment id/url instead of issue id/url
320 id = eid
321 url = eurl
322 }
323
324 case *bug.SetStatusOperation:
325 opr := op.(*bug.SetStatusOperation)
326 if err := updateGitlabIssueStatus(client, bugGitlabID, opr.Status); err != nil {
327 err := errors.Wrap(err, "editing status")
328 out <- core.NewExportError(err, b.Id())
329 return
330 }
331
332 out <- core.NewExportStatusChange(hash.String())
333
334 id = bugGitlabID
335 url = bugGitlabURL
336
337 case *bug.SetTitleOperation:
338 opr := op.(*bug.SetTitleOperation)
339 if err := updateGitlabIssueTitle(client, bugGitlabID, opr.Title); err != nil {
340 err := errors.Wrap(err, "editing title")
341 out <- core.NewExportError(err, b.Id())
342 return
343 }
344
345 out <- core.NewExportTitleEdition(hash.String())
346
347 id = bugGitlabID
348 url = bugGitlabURL
349
350 case *bug.LabelChangeOperation:
351 opr := op.(*bug.LabelChangeOperation)
352 if err := ge.updateGitlabIssueLabels(client, bugGitlabID, opr.Added, opr.Removed); err != nil {
353 err := errors.Wrap(err, "updating labels")
354 out <- core.NewExportError(err, b.Id())
355 return
356 }
357
358 out <- core.NewExportLabelChange(hash.String())
359
360 id = bugGitlabID
361 url = bugGitlabURL
362
363 default:
364 panic("unhandled operation type case")
365 }
366
367 // mark operation as exported
368 if err := markOperationAsExported(b, hash, id, url); err != nil {
369 err := errors.Wrap(err, "marking operation as exported")
370 out <- core.NewExportError(err, b.Id())
371 return
372 }
373
374 // commit at each operation export to avoid exporting same events multiple times
375 if err := b.CommitAsNeeded(); err != nil {
376 err := errors.Wrap(err, "bug commit")
377 out <- core.NewExportError(err, b.Id())
378 return
379 }
380 }
381}
382
383func markOperationAsExported(b *cache.BugCache, target git.Hash, gitlabID, gitlabURL string) error {
384 _, err := b.SetMetadata(
385 target,
386 map[string]string{
387 keyGitlabId: gitlabID,
388 keyGitlabUrl: gitlabURL,
389 },
390 )
391
392 return err
393}
394
395func (ge *gitlabExporter) getGitlabLabelID(label string) (string, error) {
396 id, ok := ge.cachedLabels[label]
397 if !ok {
398 return "", fmt.Errorf("non cached label")
399 }
400
401 return id, nil
402}
403
404// get label from gitlab
405func (ge *gitlabExporter) loadLabelsFromGitlab(client *gitlab.Client) error {
406
407 labels, _, err := client.Labels.ListLabels(
408 ge.repositoryID,
409 &gitlab.ListLabelsOptions{
410 Page: 0,
411 },
412 )
413
414 if err != nil {
415 return err
416 }
417
418 for _, label := range labels {
419 ge.cachedLabels[label.Name] = strconv.Itoa(label.ID)
420 }
421
422 for page := 2; len(labels) != 0; page++ {
423
424 labels, _, err = client.Labels.ListLabels(
425 ge.repositoryID,
426 &gitlab.ListLabelsOptions{
427 Page: page,
428 },
429 )
430
431 if err != nil {
432 return err
433 }
434
435 for _, label := range labels {
436 ge.cachedLabels[label.Name] = strconv.Itoa(label.ID)
437 }
438 }
439
440 return nil
441}
442
443func (ge *gitlabExporter) createGitlabLabel(gc *gitlab.Client, label bug.Label) (string, error) {
444 client := buildClient(ge.conf[keyToken])
445
446 // RGBA to hex color
447 rgba := label.RGBA()
448 hexColor := fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
449 name := label.String()
450
451 _, _, err := client.Labels.CreateLabel(ge.repositoryID, &gitlab.CreateLabelOptions{
452 Name: &name,
453 Color: &hexColor,
454 })
455
456 if err != nil {
457 return "", err
458 }
459
460 return "", nil
461}
462
463func (ge *gitlabExporter) getLabelsIDs(gc *gitlab.Client, repositoryID string, labels []bug.Label) ([]string, error) {
464 return []string{}, nil
465}
466
467// create a gitlab. issue and return it ID
468func createGitlabIssue(gc *gitlab.Client, repositoryID, title, body string) (string, string, error) {
469 return "", "", nil
470
471}
472
473// add a comment to an issue and return it ID
474func addCommentGitlabIssue(gc *gitlab.Client, subjectID string, body string) (string, string, error) {
475 return "", "", nil
476}
477
478func editCommentGitlabIssue(gc *gitlab.Client, commentID, body string) (string, string, error) {
479 return "", "", nil
480}
481
482func updateGitlabIssueStatus(gc *gitlab.Client, id string, status bug.Status) error {
483 return nil
484}
485
486func updateGitlabIssueBody(gc *gitlab.Client, id string, body string) error {
487 return nil
488}
489
490func updateGitlabIssueTitle(gc *gitlab.Client, id, title string) error {
491 return nil
492}
493
494// update gitlab. issue labels
495func (ge *gitlabExporter) updateGitlabIssueLabels(gc *gitlab.Client, labelableID string, added, removed []bug.Label) error {
496 return nil
497}