1package gitlab
2
3import (
4 "context"
5 "fmt"
6 "strconv"
7 "time"
8
9 "github.com/xanzy/go-gitlab"
10
11 "github.com/MichaelMure/git-bug/bridge/core"
12 "github.com/MichaelMure/git-bug/bridge/core/auth"
13 "github.com/MichaelMure/git-bug/bridge/gitlab/iterator"
14 "github.com/MichaelMure/git-bug/bug"
15 "github.com/MichaelMure/git-bug/cache"
16 "github.com/MichaelMure/git-bug/entity"
17 "github.com/MichaelMure/git-bug/util/text"
18)
19
20// gitlabImporter implement the Importer interface
21type gitlabImporter struct {
22 conf core.Configuration
23
24 // default client
25 client *gitlab.Client
26
27 // iterator
28 iterator *iterator.Iterator
29
30 // send only channel
31 out chan<- core.ImportResult
32}
33
34func (gi *gitlabImporter) Init(_ context.Context, repo *cache.RepoCache, conf core.Configuration) error {
35 gi.conf = conf
36
37 creds, err := auth.List(repo,
38 auth.WithTarget(target),
39 auth.WithKind(auth.KindToken),
40 auth.WithMeta(auth.MetaKeyBaseURL, conf[confKeyGitlabBaseUrl]),
41 auth.WithMeta(auth.MetaKeyLogin, conf[confKeyDefaultLogin]),
42 )
43 if err != nil {
44 return err
45 }
46
47 if len(creds) == 0 {
48 return ErrMissingIdentityToken
49 }
50
51 gi.client, err = buildClient(conf[confKeyGitlabBaseUrl], creds[0].(*auth.Token))
52 if err != nil {
53 return err
54 }
55
56 return nil
57}
58
59// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
60// of the missing issues / comments / label events / title changes ...
61func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
62 gi.iterator = iterator.NewIterator(ctx, gi.client, 10, gi.conf[confKeyProjectID], since)
63 out := make(chan core.ImportResult)
64 gi.out = out
65
66 go func() {
67 defer close(gi.out)
68
69 // Loop over all matching issues
70 for gi.iterator.NextIssue() {
71 issue := gi.iterator.IssueValue()
72
73 // create issue
74 b, err := gi.ensureIssue(repo, issue)
75 if err != nil {
76 err := fmt.Errorf("issue creation: %v", err)
77 out <- core.NewImportError(err, "")
78 return
79 }
80
81 // Loop over all notes
82 for gi.iterator.NextNote() {
83 note := gi.iterator.NoteValue()
84 if err := gi.ensureNote(repo, b, note); err != nil {
85 err := fmt.Errorf("note creation: %v", err)
86 out <- core.NewImportError(err, entity.Id(strconv.Itoa(note.ID)))
87 return
88 }
89 }
90
91 // Loop over all label events
92 for gi.iterator.NextLabelEvent() {
93 labelEvent := gi.iterator.LabelEventValue()
94 if err := gi.ensureLabelEvent(repo, b, labelEvent); err != nil {
95 err := fmt.Errorf("label event creation: %v", err)
96 out <- core.NewImportError(err, entity.Id(strconv.Itoa(labelEvent.ID)))
97 return
98 }
99 }
100
101 if !b.NeedCommit() {
102 out <- core.NewImportNothing(b.Id(), "no imported operation")
103 } else if err := b.Commit(); err != nil {
104 // commit bug state
105 err := fmt.Errorf("bug commit: %v", err)
106 out <- core.NewImportError(err, "")
107 return
108 }
109 }
110
111 if err := gi.iterator.Error(); err != nil {
112 out <- core.NewImportError(err, "")
113 }
114 }()
115
116 return out, nil
117}
118
119func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
120 // ensure issue author
121 author, err := gi.ensurePerson(repo, issue.Author.ID)
122 if err != nil {
123 return nil, err
124 }
125
126 // resolve bug
127 b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool {
128 return excerpt.CreateMetadata[core.MetaKeyOrigin] == target &&
129 excerpt.CreateMetadata[metaKeyGitlabId] == parseID(issue.IID) &&
130 excerpt.CreateMetadata[metaKeyGitlabBaseUrl] == gi.conf[confKeyGitlabBaseUrl] &&
131 excerpt.CreateMetadata[metaKeyGitlabProject] == gi.conf[confKeyProjectID]
132 })
133 if err == nil {
134 return b, nil
135 }
136 if err != bug.ErrBugNotExist {
137 return nil, err
138 }
139
140 // if bug was never imported
141 cleanTitle, err := text.Cleanup(issue.Title)
142 if err != nil {
143 return nil, err
144 }
145 cleanDesc, err := text.Cleanup(issue.Description)
146 if err != nil {
147 return nil, err
148 }
149
150 // create bug
151 b, _, err = repo.NewBugRaw(
152 author,
153 issue.CreatedAt.Unix(),
154 cleanTitle,
155 cleanDesc,
156 nil,
157 map[string]string{
158 core.MetaKeyOrigin: target,
159 metaKeyGitlabId: parseID(issue.IID),
160 metaKeyGitlabUrl: issue.WebURL,
161 metaKeyGitlabProject: gi.conf[confKeyProjectID],
162 metaKeyGitlabBaseUrl: gi.conf[confKeyGitlabBaseUrl],
163 },
164 )
165
166 if err != nil {
167 return nil, err
168 }
169
170 // importing a new bug
171 gi.out <- core.NewImportBug(b.Id())
172
173 return b, nil
174}
175
176func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
177 gitlabID := parseID(note.ID)
178
179 id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, gitlabID)
180 if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
181 return errResolve
182 }
183
184 // ensure issue author
185 author, err := gi.ensurePerson(repo, note.Author.ID)
186 if err != nil {
187 return err
188 }
189
190 noteType, body := GetNoteType(note)
191 switch noteType {
192 case NOTE_CLOSED:
193 if errResolve == nil {
194 return nil
195 }
196
197 op, err := b.CloseRaw(
198 author,
199 note.CreatedAt.Unix(),
200 map[string]string{
201 metaKeyGitlabId: gitlabID,
202 },
203 )
204 if err != nil {
205 return err
206 }
207
208 gi.out <- core.NewImportStatusChange(op.Id())
209
210 case NOTE_REOPENED:
211 if errResolve == nil {
212 return nil
213 }
214
215 op, err := b.OpenRaw(
216 author,
217 note.CreatedAt.Unix(),
218 map[string]string{
219 metaKeyGitlabId: gitlabID,
220 },
221 )
222 if err != nil {
223 return err
224 }
225
226 gi.out <- core.NewImportStatusChange(op.Id())
227
228 case NOTE_DESCRIPTION_CHANGED:
229 issue := gi.iterator.IssueValue()
230
231 firstComment := b.Snapshot().Comments[0]
232 // since gitlab doesn't provide the issue history
233 // we should check for "changed the description" notes and compare issue texts
234 // TODO: Check only one time and ignore next 'description change' within one issue
235 if errResolve == cache.ErrNoMatchingOp && issue.Description != firstComment.Message {
236 // comment edition
237 op, err := b.EditCommentRaw(
238 author,
239 note.UpdatedAt.Unix(),
240 firstComment.Id(),
241 issue.Description,
242 map[string]string{
243 metaKeyGitlabId: gitlabID,
244 },
245 )
246 if err != nil {
247 return err
248 }
249
250 gi.out <- core.NewImportTitleEdition(op.Id())
251 }
252
253 case NOTE_COMMENT:
254 cleanText, err := text.Cleanup(body)
255 if err != nil {
256 return err
257 }
258
259 // if we didn't import the comment
260 if errResolve == cache.ErrNoMatchingOp {
261
262 // add comment operation
263 op, err := b.AddCommentRaw(
264 author,
265 note.CreatedAt.Unix(),
266 cleanText,
267 nil,
268 map[string]string{
269 metaKeyGitlabId: gitlabID,
270 },
271 )
272 if err != nil {
273 return err
274 }
275 gi.out <- core.NewImportComment(op.Id())
276 return nil
277 }
278
279 // if comment was already exported
280
281 // search for last comment update
282 comment, err := b.Snapshot().SearchComment(id)
283 if err != nil {
284 return err
285 }
286
287 // compare local bug comment with the new note body
288 if comment.Message != cleanText {
289 // comment edition
290 op, err := b.EditCommentRaw(
291 author,
292 note.UpdatedAt.Unix(),
293 comment.Id(),
294 cleanText,
295 nil,
296 )
297
298 if err != nil {
299 return err
300 }
301 gi.out <- core.NewImportCommentEdition(op.Id())
302 }
303
304 return nil
305
306 case NOTE_TITLE_CHANGED:
307 // title change events are given new notes
308 if errResolve == nil {
309 return nil
310 }
311
312 op, err := b.SetTitleRaw(
313 author,
314 note.CreatedAt.Unix(),
315 body,
316 map[string]string{
317 metaKeyGitlabId: gitlabID,
318 },
319 )
320 if err != nil {
321 return err
322 }
323
324 gi.out <- core.NewImportTitleEdition(op.Id())
325
326 case NOTE_UNKNOWN,
327 NOTE_ASSIGNED,
328 NOTE_UNASSIGNED,
329 NOTE_CHANGED_MILESTONE,
330 NOTE_REMOVED_MILESTONE,
331 NOTE_CHANGED_DUEDATE,
332 NOTE_REMOVED_DUEDATE,
333 NOTE_LOCKED,
334 NOTE_UNLOCKED,
335 NOTE_MENTIONED_IN_ISSUE,
336 NOTE_MENTIONED_IN_MERGE_REQUEST:
337
338 return nil
339
340 default:
341 panic("unhandled note type")
342 }
343
344 return nil
345}
346
347func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
348 _, err := b.ResolveOperationWithMetadata(metaKeyGitlabId, parseID(labelEvent.ID))
349 if err != cache.ErrNoMatchingOp {
350 return err
351 }
352
353 // ensure issue author
354 author, err := gi.ensurePerson(repo, labelEvent.User.ID)
355 if err != nil {
356 return err
357 }
358
359 switch labelEvent.Action {
360 case "add":
361 _, err = b.ForceChangeLabelsRaw(
362 author,
363 labelEvent.CreatedAt.Unix(),
364 []string{labelEvent.Label.Name},
365 nil,
366 map[string]string{
367 metaKeyGitlabId: parseID(labelEvent.ID),
368 },
369 )
370
371 case "remove":
372 _, err = b.ForceChangeLabelsRaw(
373 author,
374 labelEvent.CreatedAt.Unix(),
375 nil,
376 []string{labelEvent.Label.Name},
377 map[string]string{
378 metaKeyGitlabId: parseID(labelEvent.ID),
379 },
380 )
381
382 default:
383 err = fmt.Errorf("unexpected label event action")
384 }
385
386 return err
387}
388
389func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
390 // Look first in the cache
391 i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id))
392 if err == nil {
393 return i, nil
394 }
395 if entity.IsErrMultipleMatch(err) {
396 return nil, err
397 }
398
399 user, _, err := gi.client.Users.GetUser(id)
400 if err != nil {
401 return nil, err
402 }
403
404 i, err = repo.NewIdentityRaw(
405 user.Name,
406 user.PublicEmail,
407 user.Username,
408 user.AvatarURL,
409 map[string]string{
410 // because Gitlab
411 metaKeyGitlabId: strconv.Itoa(id),
412 metaKeyGitlabLogin: user.Username,
413 },
414 )
415 if err != nil {
416 return nil, err
417 }
418
419 gi.out <- core.NewImportIdentity(i.Id())
420 return i, nil
421}
422
423func parseID(id int) string {
424 return fmt.Sprintf("%d", id)
425}