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/entities/bug"
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 out := make(chan core.ImportResult)
59 gi.out = out
60
61 go func() {
62 defer close(out)
63
64 for issue := range Issues(ctx, gi.client, gi.conf[confKeyProjectID], since) {
65
66 b, err := gi.ensureIssue(repo, issue)
67 if err != nil {
68 err := fmt.Errorf("issue creation: %v", err)
69 out <- core.NewImportError(err, "")
70 return
71 }
72
73 issueEvents := SortedEvents(
74 Notes(ctx, gi.client, issue),
75 LabelEvents(ctx, gi.client, issue),
76 StateEvents(ctx, gi.client, issue),
77 )
78
79 for e := range issueEvents {
80 if e, ok := e.(ErrorEvent); ok {
81 out <- core.NewImportError(e.Err, "")
82 continue
83 }
84 if err := gi.ensureIssueEvent(repo, b, issue, e); err != nil {
85 err := fmt.Errorf("issue event creation: %v", err)
86 out <- core.NewImportError(err, entity.Id(e.ID()))
87 }
88 }
89
90 if !b.NeedCommit() {
91 out <- core.NewImportNothing(b.Id(), "no imported operation")
92 } else if err := b.Commit(); err != nil {
93 // commit bug state
94 err := fmt.Errorf("bug commit: %v", err)
95 out <- core.NewImportError(err, "")
96 return
97 }
98 }
99 }()
100
101 return out, nil
102}
103
104func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
105 // ensure issue author
106 author, err := gi.ensurePerson(repo, issue.Author.ID)
107 if err != nil {
108 return nil, err
109 }
110
111 // resolve bug
112 b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool {
113 return excerpt.CreateMetadata[core.MetaKeyOrigin] == target &&
114 excerpt.CreateMetadata[metaKeyGitlabId] == fmt.Sprintf("%d", issue.IID) &&
115 excerpt.CreateMetadata[metaKeyGitlabBaseUrl] == gi.conf[confKeyGitlabBaseUrl] &&
116 excerpt.CreateMetadata[metaKeyGitlabProject] == gi.conf[confKeyProjectID]
117 })
118 if err == nil {
119 return b, nil
120 }
121 if !entity.IsErrNotFound(err) {
122 return nil, err
123 }
124
125 // if bug was never imported, create bug
126 b, _, err = repo.NewBugRaw(
127 author,
128 issue.CreatedAt.Unix(),
129 text.CleanupOneLine(issue.Title),
130 text.Cleanup(issue.Description),
131 nil,
132 map[string]string{
133 core.MetaKeyOrigin: target,
134 metaKeyGitlabId: fmt.Sprintf("%d", issue.IID),
135 metaKeyGitlabUrl: issue.WebURL,
136 metaKeyGitlabProject: gi.conf[confKeyProjectID],
137 metaKeyGitlabBaseUrl: gi.conf[confKeyGitlabBaseUrl],
138 },
139 )
140
141 if err != nil {
142 return nil, err
143 }
144
145 // importing a new bug
146 gi.out <- core.NewImportBug(b.Id())
147
148 return b, nil
149}
150
151func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCache, issue *gitlab.Issue, event Event) error {
152 id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, event.ID())
153 if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
154 return errResolve
155 }
156
157 // ensure issue author
158 author, err := gi.ensurePerson(repo, event.UserID())
159 if err != nil {
160 return err
161 }
162
163 switch event.Kind() {
164 case EventClosed:
165 if errResolve == nil {
166 return nil
167 }
168
169 op, err := b.CloseRaw(
170 author,
171 event.CreatedAt().Unix(),
172 map[string]string{
173 metaKeyGitlabId: event.ID(),
174 },
175 )
176
177 if err != nil {
178 return err
179 }
180
181 gi.out <- core.NewImportStatusChange(b.Id(), op.Id())
182
183 case EventReopened:
184 if errResolve == nil {
185 return nil
186 }
187
188 op, err := b.OpenRaw(
189 author,
190 event.CreatedAt().Unix(),
191 map[string]string{
192 metaKeyGitlabId: event.ID(),
193 },
194 )
195 if err != nil {
196 return err
197 }
198
199 gi.out <- core.NewImportStatusChange(b.Id(), op.Id())
200
201 case EventDescriptionChanged:
202 firstComment := b.Snapshot().Comments[0]
203 // since gitlab doesn't provide the issue history
204 // we should check for "changed the description" notes and compare issue texts
205 // TODO: Check only one time and ignore next 'description change' within one issue
206 cleanedDesc := text.Cleanup(issue.Description)
207 if errResolve == cache.ErrNoMatchingOp && cleanedDesc != firstComment.Message {
208 // comment edition
209 op, err := b.EditCommentRaw(
210 author,
211 event.(NoteEvent).UpdatedAt.Unix(),
212 firstComment.CombinedId(),
213 cleanedDesc,
214 map[string]string{
215 metaKeyGitlabId: event.ID(),
216 },
217 )
218 if err != nil {
219 return err
220 }
221
222 gi.out <- core.NewImportTitleEdition(b.Id(), op.Id())
223 }
224
225 case EventComment:
226 cleanText := text.Cleanup(event.(NoteEvent).Body)
227
228 // if we didn't import the comment
229 if errResolve == cache.ErrNoMatchingOp {
230
231 // add comment operation
232 commentId, _, err := b.AddCommentRaw(
233 author,
234 event.CreatedAt().Unix(),
235 cleanText,
236 nil,
237 map[string]string{
238 metaKeyGitlabId: event.ID(),
239 },
240 )
241 if err != nil {
242 return err
243 }
244 gi.out <- core.NewImportComment(b.Id(), commentId)
245 return nil
246 }
247
248 // if comment was already exported
249
250 // search for last comment update
251 comment, err := b.Snapshot().SearchCommentByOpId(id)
252 if err != nil {
253 return err
254 }
255
256 // compare local bug comment with the new event body
257 if comment.Message != cleanText {
258 // comment edition
259 _, err := b.EditCommentRaw(
260 author,
261 event.(NoteEvent).UpdatedAt.Unix(),
262 comment.CombinedId(),
263 cleanText,
264 nil,
265 )
266
267 if err != nil {
268 return err
269 }
270 gi.out <- core.NewImportCommentEdition(b.Id(), comment.CombinedId())
271 }
272
273 return nil
274
275 case EventTitleChanged:
276 // title change events are given new notes
277 if errResolve == nil {
278 return nil
279 }
280
281 op, err := b.SetTitleRaw(
282 author,
283 event.CreatedAt().Unix(),
284 event.(NoteEvent).Title(),
285 map[string]string{
286 metaKeyGitlabId: event.ID(),
287 },
288 )
289 if err != nil {
290 return err
291 }
292
293 gi.out <- core.NewImportTitleEdition(b.Id(), op.Id())
294
295 case EventAddLabel:
296 _, err = b.ForceChangeLabelsRaw(
297 author,
298 event.CreatedAt().Unix(),
299 []string{event.(LabelEvent).Label.Name},
300 nil,
301 map[string]string{
302 metaKeyGitlabId: event.ID(),
303 },
304 )
305 return err
306
307 case EventRemoveLabel:
308 _, err = b.ForceChangeLabelsRaw(
309 author,
310 event.CreatedAt().Unix(),
311 nil,
312 []string{event.(LabelEvent).Label.Name},
313 map[string]string{
314 metaKeyGitlabId: event.ID(),
315 },
316 )
317 return err
318
319 case EventAssigned,
320 EventUnassigned,
321 EventChangedMilestone,
322 EventRemovedMilestone,
323 EventChangedDuedate,
324 EventRemovedDuedate,
325 EventLocked,
326 EventUnlocked,
327 EventMentionedInIssue,
328 EventMentionedInMergeRequest:
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.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.NewIdentityRaw(
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}