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