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