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