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