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/bridge/gitlab/iterator"
14 "github.com/MichaelMure/git-bug/bug"
15 "github.com/MichaelMure/git-bug/cache"
16 "github.com/MichaelMure/git-bug/entity"
17 "github.com/MichaelMure/git-bug/util/text"
18)
19
20// gitlabImporter implement the Importer interface
21type gitlabImporter struct {
22 conf core.Configuration
23
24 // default client
25 client *gitlab.Client
26
27 // iterator
28 iterator *iterator.Iterator
29
30 // send only channel
31 out chan<- core.ImportResult
32}
33
34func (gi *gitlabImporter) Init(_ context.Context, repo *cache.RepoCache, conf core.Configuration) error {
35 gi.conf = conf
36
37 creds, err := auth.List(repo,
38 auth.WithTarget(target),
39 auth.WithKind(auth.KindToken),
40 auth.WithMeta(auth.MetaKeyBaseURL, conf[confKeyGitlabBaseUrl]),
41 auth.WithMeta(auth.MetaKeyLogin, conf[confKeyDefaultLogin]),
42 )
43 if err != nil {
44 return err
45 }
46
47 if len(creds) == 0 {
48 return ErrMissingIdentityToken
49 }
50
51 gi.client, err = buildClient(conf[confKeyGitlabBaseUrl], creds[0].(*auth.Token))
52 if err != nil {
53 return err
54 }
55
56 return nil
57}
58
59// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
60// of the missing issues / comments / label events / title changes ...
61func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
62 gi.iterator = iterator.NewIterator(ctx, gi.client, 10, gi.conf[confKeyProjectID], since)
63 out := make(chan core.ImportResult)
64 gi.out = out
65
66 go func() {
67 defer close(gi.out)
68
69 // Loop over all matching issues
70 for gi.iterator.NextIssue() {
71 issue := gi.iterator.IssueValue()
72
73 // create issue
74 b, err := gi.ensureIssue(repo, issue)
75 if err != nil {
76 err := fmt.Errorf("issue creation: %v", err)
77 out <- core.NewImportError(err, "")
78 return
79 }
80
81 // Loop over all notes
82 for gi.iterator.NextNote() {
83 note := gi.iterator.NoteValue()
84 if err := gi.ensureNote(repo, b, note); err != nil {
85 err := fmt.Errorf("note creation: %v", err)
86 out <- core.NewImportError(err, entity.Id(strconv.Itoa(note.ID)))
87 return
88 }
89 }
90
91 // Loop over all label events
92 for gi.iterator.NextLabelEvent() {
93 labelEvent := gi.iterator.LabelEventValue()
94 if err := gi.ensureLabelEvent(repo, b, labelEvent); err != nil {
95 err := fmt.Errorf("label event creation: %v", err)
96 out <- core.NewImportError(err, entity.Id(strconv.Itoa(labelEvent.ID)))
97 return
98 }
99 }
100
101 if !b.NeedCommit() {
102 out <- core.NewImportNothing(b.Id(), "no imported operation")
103 } else if err := b.Commit(); err != nil {
104 // commit bug state
105 err := fmt.Errorf("bug commit: %v", err)
106 out <- core.NewImportError(err, "")
107 return
108 }
109 }
110
111 if err := gi.iterator.Error(); err != nil {
112 out <- core.NewImportError(err, "")
113 }
114 }()
115
116 return out, nil
117}
118
119func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
120 // ensure issue author
121 author, err := gi.ensurePerson(repo, issue.Author.ID)
122 if err != nil {
123 return nil, err
124 }
125
126 // resolve bug
127 b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool {
128 return excerpt.CreateMetadata[core.MetaKeyOrigin] == target &&
129 excerpt.CreateMetadata[metaKeyGitlabId] == parseID(issue.IID) &&
130 excerpt.CreateMetadata[metaKeyGitlabBaseUrl] == gi.conf[confKeyGitlabBaseUrl] &&
131 excerpt.CreateMetadata[metaKeyGitlabProject] == gi.conf[confKeyProjectID]
132 })
133 if err == nil {
134 return b, nil
135 }
136 if err != bug.ErrBugNotExist {
137 return nil, err
138 }
139
140 // if bug was never imported, create bug
141 b, _, err = repo.NewBugRaw(
142 author,
143 issue.CreatedAt.Unix(),
144 text.CleanupOneLine(issue.Title),
145 text.Cleanup(issue.Description),
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 text.Cleanup(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 := text.Cleanup(body)
245
246 // if we didn't import the comment
247 if errResolve == cache.ErrNoMatchingOp {
248
249 // add comment operation
250 op, err := b.AddCommentRaw(
251 author,
252 note.CreatedAt.Unix(),
253 cleanText,
254 nil,
255 map[string]string{
256 metaKeyGitlabId: gitlabID,
257 },
258 )
259 if err != nil {
260 return err
261 }
262 gi.out <- core.NewImportComment(op.Id())
263 return nil
264 }
265
266 // if comment was already exported
267
268 // search for last comment update
269 comment, err := b.Snapshot().SearchComment(id)
270 if err != nil {
271 return err
272 }
273
274 // compare local bug comment with the new note body
275 if comment.Message != cleanText {
276 // comment edition
277 op, err := b.EditCommentRaw(
278 author,
279 note.UpdatedAt.Unix(),
280 comment.Id(),
281 cleanText,
282 nil,
283 )
284
285 if err != nil {
286 return err
287 }
288 gi.out <- core.NewImportCommentEdition(op.Id())
289 }
290
291 return nil
292
293 case NOTE_TITLE_CHANGED:
294 // title change events are given new notes
295 if errResolve == nil {
296 return nil
297 }
298
299 op, err := b.SetTitleRaw(
300 author,
301 note.CreatedAt.Unix(),
302 text.CleanupOneLine(body),
303 map[string]string{
304 metaKeyGitlabId: gitlabID,
305 },
306 )
307 if err != nil {
308 return err
309 }
310
311 gi.out <- core.NewImportTitleEdition(op.Id())
312
313 case NOTE_UNKNOWN,
314 NOTE_ASSIGNED,
315 NOTE_UNASSIGNED,
316 NOTE_CHANGED_MILESTONE,
317 NOTE_REMOVED_MILESTONE,
318 NOTE_CHANGED_DUEDATE,
319 NOTE_REMOVED_DUEDATE,
320 NOTE_LOCKED,
321 NOTE_UNLOCKED,
322 NOTE_MENTIONED_IN_ISSUE,
323 NOTE_MENTIONED_IN_MERGE_REQUEST:
324
325 return nil
326
327 default:
328 panic("unhandled note type")
329 }
330
331 return nil
332}
333
334func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
335 _, err := b.ResolveOperationWithMetadata(metaKeyGitlabId, parseID(labelEvent.ID))
336 if err != cache.ErrNoMatchingOp {
337 return err
338 }
339
340 // ensure issue author
341 author, err := gi.ensurePerson(repo, labelEvent.User.ID)
342 if err != nil {
343 return err
344 }
345
346 switch labelEvent.Action {
347 case "add":
348 _, err = b.ForceChangeLabelsRaw(
349 author,
350 labelEvent.CreatedAt.Unix(),
351 []string{text.CleanupOneLine(labelEvent.Label.Name)},
352 nil,
353 map[string]string{
354 metaKeyGitlabId: parseID(labelEvent.ID),
355 },
356 )
357
358 case "remove":
359 _, err = b.ForceChangeLabelsRaw(
360 author,
361 labelEvent.CreatedAt.Unix(),
362 nil,
363 []string{text.CleanupOneLine(labelEvent.Label.Name)},
364 map[string]string{
365 metaKeyGitlabId: parseID(labelEvent.ID),
366 },
367 )
368
369 default:
370 err = fmt.Errorf("unexpected label event action")
371 }
372
373 return err
374}
375
376func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
377 // Look first in the cache
378 i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id))
379 if err == nil {
380 return i, nil
381 }
382 if entity.IsErrMultipleMatch(err) {
383 return nil, err
384 }
385
386 user, _, err := gi.client.Users.GetUser(id)
387 if err != nil {
388 return nil, err
389 }
390
391 i, err = repo.NewIdentityRaw(
392 user.Name,
393 user.PublicEmail,
394 user.Username,
395 user.AvatarURL,
396 nil,
397 map[string]string{
398 // because Gitlab
399 metaKeyGitlabId: strconv.Itoa(id),
400 metaKeyGitlabLogin: user.Username,
401 },
402 )
403 if err != nil {
404 return nil, err
405 }
406
407 gi.out <- core.NewImportIdentity(i.Id())
408 return i, nil
409}
410
411func parseID(id int) string {
412 return fmt.Sprintf("%d", id)
413}