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