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