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