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