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 // send only channel
27 out chan<- core.ImportResult
28}
29
30func (gi *gitlabImporter) Init(_ context.Context, repo *cache.RepoCache, conf core.Configuration) error {
31 gi.conf = conf
32
33 creds, err := auth.List(repo,
34 auth.WithTarget(target),
35 auth.WithKind(auth.KindToken),
36 auth.WithMeta(auth.MetaKeyBaseURL, conf[confKeyGitlabBaseUrl]),
37 auth.WithMeta(auth.MetaKeyLogin, conf[confKeyDefaultLogin]),
38 )
39 if err != nil {
40 return err
41 }
42
43 if len(creds) == 0 {
44 return ErrMissingIdentityToken
45 }
46
47 gi.client, err = buildClient(conf[confKeyGitlabBaseUrl], creds[0].(*auth.Token))
48 if err != nil {
49 return err
50 }
51
52 return nil
53}
54
55// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
56// of the missing issues / comments / label events / title changes ...
57func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
58
59 out := make(chan core.ImportResult)
60 gi.out = out
61
62 go func() {
63 defer close(out)
64
65 for issue := range Issues(ctx, gi.client, gi.conf[confKeyProjectID], since) {
66
67 b, err := gi.ensureIssue(repo, issue)
68 if err != nil {
69 err := fmt.Errorf("issue creation: %v", err)
70 out <- core.NewImportError(err, "")
71 return
72 }
73
74 issueEvents := SortedEvents(
75 Notes(ctx, gi.client, issue),
76 LabelEvents(ctx, gi.client, issue),
77 StateEvents(ctx, gi.client, issue),
78 )
79
80 for e := range issueEvents {
81 if e, ok := e.(ErrorEvent); ok {
82 out <- core.NewImportError(e.Err, "")
83 continue
84 }
85 if err := gi.ensureIssueEvent(repo, b, issue, e); err != nil {
86 err := fmt.Errorf("issue event creation: %v", err)
87 out <- core.NewImportError(err, entity.Id(e.ID()))
88 }
89 }
90
91 if !b.NeedCommit() {
92 out <- core.NewImportNothing(b.Id(), "no imported operation")
93 } else if err := b.Commit(); err != nil {
94 // commit bug state
95 err := fmt.Errorf("bug commit: %v", err)
96 out <- core.NewImportError(err, "")
97 return
98 }
99 }
100 }()
101
102 return out, nil
103}
104
105func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
106 // ensure issue author
107 author, err := gi.ensurePerson(repo, issue.Author.ID)
108 if err != nil {
109 return nil, err
110 }
111
112 // resolve bug
113 b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool {
114 return excerpt.CreateMetadata[core.MetaKeyOrigin] == target &&
115 excerpt.CreateMetadata[metaKeyGitlabId] == fmt.Sprintf("%d", issue.IID) &&
116 excerpt.CreateMetadata[metaKeyGitlabBaseUrl] == gi.conf[confKeyGitlabBaseUrl] &&
117 excerpt.CreateMetadata[metaKeyGitlabProject] == gi.conf[confKeyProjectID]
118 })
119 if err == nil {
120 return b, nil
121 }
122 if err != bug.ErrBugNotExist {
123 return nil, err
124 }
125
126 // if bug was never imported, create bug
127 b, _, err = repo.NewBugRaw(
128 author,
129 issue.CreatedAt.Unix(),
130 text.CleanupOneLine(issue.Title),
131 text.Cleanup(issue.Description),
132 nil,
133 map[string]string{
134 core.MetaKeyOrigin: target,
135 metaKeyGitlabId: fmt.Sprintf("%d", issue.IID),
136 metaKeyGitlabUrl: issue.WebURL,
137 metaKeyGitlabProject: gi.conf[confKeyProjectID],
138 metaKeyGitlabBaseUrl: gi.conf[confKeyGitlabBaseUrl],
139 },
140 )
141
142 if err != nil {
143 return nil, err
144 }
145
146 // importing a new bug
147 gi.out <- core.NewImportBug(b.Id())
148
149 return b, nil
150}
151
152func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCache, issue *gitlab.Issue, event Event) error {
153
154 id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, event.ID())
155 if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
156 return errResolve
157 }
158
159 // ensure issue author
160 author, err := gi.ensurePerson(repo, event.UserID())
161 if err != nil {
162 return err
163 }
164
165 switch event.Kind() {
166 case EventClosed:
167 if errResolve == nil {
168 return nil
169 }
170
171 op, err := b.CloseRaw(
172 author,
173 event.CreatedAt().Unix(),
174 map[string]string{
175 metaKeyGitlabId: event.ID(),
176 },
177 )
178
179 if err != nil {
180 return err
181 }
182
183 gi.out <- core.NewImportStatusChange(op.Id())
184
185 case EventReopened:
186 if errResolve == nil {
187 return nil
188 }
189
190 op, err := b.OpenRaw(
191 author,
192 event.CreatedAt().Unix(),
193 map[string]string{
194 metaKeyGitlabId: event.ID(),
195 },
196 )
197 if err != nil {
198 return err
199 }
200
201 gi.out <- core.NewImportStatusChange(op.Id())
202
203 case EventDescriptionChanged:
204 firstComment := b.Snapshot().Comments[0]
205 // since gitlab doesn't provide the issue history
206 // we should check for "changed the description" notes and compare issue texts
207 // TODO: Check only one time and ignore next 'description change' within one issue
208 if errResolve == cache.ErrNoMatchingOp && issue.Description != firstComment.Message {
209 // comment edition
210 op, err := b.EditCommentRaw(
211 author,
212 event.(NoteEvent).UpdatedAt.Unix(),
213 firstComment.Id(),
214 text.Cleanup(issue.Description),
215 map[string]string{
216 metaKeyGitlabId: event.ID(),
217 },
218 )
219 if err != nil {
220 return err
221 }
222
223 gi.out <- core.NewImportTitleEdition(op.Id())
224 }
225
226 case EventComment:
227 cleanText := text.Cleanup(event.(NoteEvent).Body)
228
229 // if we didn't import the comment
230 if errResolve == cache.ErrNoMatchingOp {
231
232 // add comment operation
233 op, err := b.AddCommentRaw(
234 author,
235 event.CreatedAt().Unix(),
236 cleanText,
237 nil,
238 map[string]string{
239 metaKeyGitlabId: event.ID(),
240 },
241 )
242 if err != nil {
243 return err
244 }
245 gi.out <- core.NewImportComment(op.Id())
246 return nil
247 }
248
249 // if comment was already exported
250
251 // search for last comment update
252 comment, err := b.Snapshot().SearchComment(id)
253 if err != nil {
254 return err
255 }
256
257 // compare local bug comment with the new event body
258 if comment.Message != cleanText {
259 // comment edition
260 op, err := b.EditCommentRaw(
261 author,
262 event.(NoteEvent).UpdatedAt.Unix(),
263 comment.Id(),
264 cleanText,
265 nil,
266 )
267
268 if err != nil {
269 return err
270 }
271 gi.out <- core.NewImportCommentEdition(op.Id())
272 }
273
274 return nil
275
276 case EventTitleChanged:
277 // title change events are given new notes
278 if errResolve == nil {
279 return nil
280 }
281
282 op, err := b.SetTitleRaw(
283 author,
284 event.CreatedAt().Unix(),
285 event.(NoteEvent).Title(),
286 map[string]string{
287 metaKeyGitlabId: event.ID(),
288 },
289 )
290 if err != nil {
291 return err
292 }
293
294 gi.out <- core.NewImportTitleEdition(op.Id())
295
296 case EventAddLabel:
297 _, err = b.ForceChangeLabelsRaw(
298 author,
299 event.CreatedAt().Unix(),
300 []string{event.(LabelEvent).Label.Name},
301 nil,
302 map[string]string{
303 metaKeyGitlabId: event.ID(),
304 },
305 )
306 return err
307
308 case EventRemoveLabel:
309 _, err = b.ForceChangeLabelsRaw(
310 author,
311 event.CreatedAt().Unix(),
312 nil,
313 []string{event.(LabelEvent).Label.Name},
314 map[string]string{
315 metaKeyGitlabId: event.ID(),
316 },
317 )
318 return err
319
320 case EventAssigned,
321 EventUnassigned,
322 EventChangedMilestone,
323 EventRemovedMilestone,
324 EventChangedDuedate,
325 EventRemovedDuedate,
326 EventLocked,
327 EventUnlocked,
328 EventMentionedInIssue,
329 EventMentionedInMergeRequest:
330
331 return nil
332
333 default:
334 return fmt.Errorf("unexpected event")
335 }
336
337 return nil
338}
339
340func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
341 // Look first in the cache
342 i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id))
343 if err == nil {
344 return i, nil
345 }
346 if entity.IsErrMultipleMatch(err) {
347 return nil, err
348 }
349
350 user, _, err := gi.client.Users.GetUser(id)
351 if err != nil {
352 return nil, err
353 }
354
355 i, err = repo.NewIdentityRaw(
356 user.Name,
357 user.PublicEmail,
358 user.Username,
359 user.AvatarURL,
360 nil,
361 map[string]string{
362 // because Gitlab
363 metaKeyGitlabId: strconv.Itoa(id),
364 metaKeyGitlabLogin: user.Username,
365 },
366 )
367 if err != nil {
368 return nil, err
369 }
370
371 gi.out <- core.NewImportIdentity(i.Id())
372 return i, nil
373}