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 for _, e := range SortedEvents(IssueEvents(ctx, gi.client, issue)) {
75 if e, ok := e.(ErrorEvent); ok {
76 out <- core.NewImportError(e.Err, "")
77 continue
78 }
79 if err := gi.ensureIssueEvent(repo, b, issue, e); err != nil {
80 err := fmt.Errorf("issue event creation: %v", err)
81 out <- core.NewImportError(err, entity.Id(e.ID()))
82 }
83 }
84
85 if !b.NeedCommit() {
86 out <- core.NewImportNothing(b.Id(), "no imported operation")
87 } else if err := b.Commit(); err != nil {
88 // commit bug state
89 err := fmt.Errorf("bug commit: %v", err)
90 out <- core.NewImportError(err, "")
91 return
92 }
93 }
94 }()
95
96 return out, nil
97}
98
99func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
100 // ensure issue author
101 author, err := gi.ensurePerson(repo, issue.Author.ID)
102 if err != nil {
103 return nil, err
104 }
105
106 // resolve bug
107 b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool {
108 return excerpt.CreateMetadata[core.MetaKeyOrigin] == target &&
109 excerpt.CreateMetadata[metaKeyGitlabId] == fmt.Sprintf("%d", issue.IID) &&
110 excerpt.CreateMetadata[metaKeyGitlabBaseUrl] == gi.conf[confKeyGitlabBaseUrl] &&
111 excerpt.CreateMetadata[metaKeyGitlabProject] == gi.conf[confKeyProjectID]
112 })
113 if err == nil {
114 return b, nil
115 }
116 if err != bug.ErrBugNotExist {
117 return nil, err
118 }
119
120 // if bug was never imported, create bug
121 b, _, err = repo.NewBugRaw(
122 author,
123 issue.CreatedAt.Unix(),
124 text.CleanupOneLine(issue.Title),
125 text.Cleanup(issue.Description),
126 nil,
127 map[string]string{
128 core.MetaKeyOrigin: target,
129 metaKeyGitlabId: fmt.Sprintf("%d", issue.IID),
130 metaKeyGitlabUrl: issue.WebURL,
131 metaKeyGitlabProject: gi.conf[confKeyProjectID],
132 metaKeyGitlabBaseUrl: gi.conf[confKeyGitlabBaseUrl],
133 },
134 )
135
136 if err != nil {
137 return nil, err
138 }
139
140 // importing a new bug
141 gi.out <- core.NewImportBug(b.Id())
142
143 return b, nil
144}
145
146func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCache, issue *gitlab.Issue, event Event) error {
147
148 id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, event.ID())
149 if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
150 return errResolve
151 }
152
153 // ensure issue author
154 author, err := gi.ensurePerson(repo, event.UserID())
155 if err != nil {
156 return err
157 }
158
159 switch event.Kind() {
160 case EventClosed:
161 if errResolve == nil {
162 return nil
163 }
164
165 op, err := b.CloseRaw(
166 author,
167 event.CreatedAt().Unix(),
168 map[string]string{
169 metaKeyGitlabId: event.ID(),
170 },
171 )
172
173 if err != nil {
174 return err
175 }
176
177 gi.out <- core.NewImportStatusChange(op.Id())
178
179 case EventReopened:
180 if errResolve == nil {
181 return nil
182 }
183
184 op, err := b.OpenRaw(
185 author,
186 event.CreatedAt().Unix(),
187 map[string]string{
188 metaKeyGitlabId: event.ID(),
189 },
190 )
191 if err != nil {
192 return err
193 }
194
195 gi.out <- core.NewImportStatusChange(op.Id())
196
197 case EventDescriptionChanged:
198 firstComment := b.Snapshot().Comments[0]
199 // since gitlab doesn't provide the issue history
200 // we should check for "changed the description" notes and compare issue texts
201 // TODO: Check only one time and ignore next 'description change' within one issue
202 if errResolve == cache.ErrNoMatchingOp && issue.Description != firstComment.Message {
203 // comment edition
204 op, err := b.EditCommentRaw(
205 author,
206 event.(NoteEvent).UpdatedAt.Unix(),
207 firstComment.Id(),
208 text.Cleanup(issue.Description),
209 map[string]string{
210 metaKeyGitlabId: event.ID(),
211 },
212 )
213 if err != nil {
214 return err
215 }
216
217 gi.out <- core.NewImportTitleEdition(op.Id())
218 }
219
220 case EventComment:
221 cleanText := text.Cleanup(event.(NoteEvent).Body)
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 event.CreatedAt().Unix(),
230 cleanText,
231 nil,
232 map[string]string{
233 metaKeyGitlabId: event.ID(),
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 event body
252 if comment.Message != cleanText {
253 // comment edition
254 op, err := b.EditCommentRaw(
255 author,
256 event.(NoteEvent).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 EventTitleChanged:
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 event.CreatedAt().Unix(),
279 event.(NoteEvent).Title(),
280 map[string]string{
281 metaKeyGitlabId: event.ID(),
282 },
283 )
284 if err != nil {
285 return err
286 }
287
288 gi.out <- core.NewImportTitleEdition(op.Id())
289
290 case EventAddLabel:
291 _, err = b.ForceChangeLabelsRaw(
292 author,
293 event.CreatedAt().Unix(),
294 []string{event.(LabelEvent).Label.Name},
295 nil,
296 map[string]string{
297 metaKeyGitlabId: event.ID(),
298 },
299 )
300 return err
301
302 case EventRemoveLabel:
303 _, err = b.ForceChangeLabelsRaw(
304 author,
305 event.CreatedAt().Unix(),
306 nil,
307 []string{event.(LabelEvent).Label.Name},
308 map[string]string{
309 metaKeyGitlabId: event.ID(),
310 },
311 )
312 return err
313
314 case EventAssigned,
315 EventUnassigned,
316 EventChangedMilestone,
317 EventRemovedMilestone,
318 EventChangedDuedate,
319 EventRemovedDuedate,
320 EventLocked,
321 EventUnlocked,
322 EventMentionedInIssue,
323 EventMentionedInMergeRequest:
324
325 return nil
326
327 default:
328 return fmt.Errorf("unexpected event")
329 }
330
331 return nil
332}
333
334func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
335 // Look first in the cache
336 i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id))
337 if err == nil {
338 return i, nil
339 }
340 if entity.IsErrMultipleMatch(err) {
341 return nil, err
342 }
343
344 user, _, err := gi.client.Users.GetUser(id)
345 if err != nil {
346 return nil, err
347 }
348
349 i, err = repo.NewIdentityRaw(
350 user.Name,
351 user.PublicEmail,
352 user.Username,
353 user.AvatarURL,
354 nil,
355 map[string]string{
356 // because Gitlab
357 metaKeyGitlabId: strconv.Itoa(id),
358 metaKeyGitlabLogin: user.Username,
359 },
360 )
361 if err != nil {
362 return nil, err
363 }
364
365 gi.out <- core.NewImportIdentity(i.Id())
366 return i, nil
367}