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