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