1package gitlab
2
3import (
4 "fmt"
5 "strconv"
6 "time"
7
8 "github.com/xanzy/go-gitlab"
9
10 "github.com/MichaelMure/git-bug/bridge/core"
11 "github.com/MichaelMure/git-bug/bug"
12 "github.com/MichaelMure/git-bug/cache"
13 "github.com/MichaelMure/git-bug/identity"
14 "github.com/MichaelMure/git-bug/util/git"
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 // iterator
23 iterator *iterator
24
25 // number of imported issues
26 importedIssues int
27
28 // number of imported identities
29 importedIdentities int
30}
31
32func (gi *gitlabImporter) Init(conf core.Configuration) error {
33 gi.conf = conf
34 return nil
35}
36
37// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
38// of the missing issues / comments / label events / title changes ...
39func (gi *gitlabImporter) ImportAll(repo *cache.RepoCache, since time.Time) error {
40 gi.iterator = NewIterator(gi.conf[keyProjectID], gi.conf[keyToken], since)
41
42 // Loop over all matching issues
43 for gi.iterator.NextIssue() {
44 issue := gi.iterator.IssueValue()
45 fmt.Printf("importing issue: %v\n", issue.Title)
46
47 // create issue
48 b, err := gi.ensureIssue(repo, issue)
49 if err != nil {
50 return fmt.Errorf("issue creation: %v", err)
51 }
52
53 // Loop over all notes
54 for gi.iterator.NextNote() {
55 note := gi.iterator.NoteValue()
56 if err := gi.ensureNote(repo, b, note); err != nil {
57 return fmt.Errorf("note creation: %v", err)
58 }
59 }
60
61 // Loop over all label events
62 for gi.iterator.NextLabelEvent() {
63 labelEvent := gi.iterator.LabelEventValue()
64 if err := gi.ensureLabelEvent(repo, b, labelEvent); err != nil {
65 return fmt.Errorf("label event creation: %v", err)
66 }
67 }
68
69 if err := gi.iterator.Error(); err != nil {
70 fmt.Printf("import error: %v\n", err)
71 return err
72 }
73
74 // commit bug state
75 if err := b.CommitAsNeeded(); err != nil {
76 return fmt.Errorf("bug commit: %v", err)
77 }
78 }
79
80 fmt.Printf("Successfully imported %d issues and %d identities from Gitlab\n", gi.importedIssues, gi.importedIdentities)
81 return nil
82}
83
84func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
85 // ensure issue author
86 author, err := gi.ensurePerson(repo, issue.Author.ID)
87 if err != nil {
88 return nil, err
89 }
90
91 // resolve bug
92 b, err := repo.ResolveBugCreateMetadata(keyGitlabUrl, issue.WebURL)
93 if err != nil && err != bug.ErrBugNotExist {
94 return nil, err
95 }
96
97 if err == nil {
98 return b, nil
99 }
100
101 // if bug was never imported
102 cleanText, err := text.Cleanup(issue.Description)
103 if err != nil {
104 return nil, err
105 }
106
107 // create bug
108 b, _, err = repo.NewBugRaw(
109 author,
110 issue.CreatedAt.Unix(),
111 issue.Title,
112 cleanText,
113 nil,
114 map[string]string{
115 core.KeyOrigin: target,
116 keyGitlabId: parseID(issue.ID),
117 keyGitlabUrl: issue.WebURL,
118 keyGitlabProject: gi.conf[keyProjectID],
119 },
120 )
121
122 if err != nil {
123 return nil, err
124 }
125
126 // importing a new bug
127 gi.importedIssues++
128
129 return b, nil
130}
131
132func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
133 id := parseID(note.ID)
134
135 hash, errResolve := b.ResolveOperationWithMetadata(keyGitlabId, id)
136 if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
137 return errResolve
138 }
139
140 // ensure issue author
141 author, err := gi.ensurePerson(repo, note.Author.ID)
142 if err != nil {
143 return err
144 }
145
146 noteType, body := GetNoteType(note)
147 switch noteType {
148 case NOTE_CLOSED:
149 if errResolve == nil {
150 return nil
151 }
152
153 _, err = b.CloseRaw(
154 author,
155 note.CreatedAt.Unix(),
156 map[string]string{
157 keyGitlabId: id,
158 },
159 )
160 return err
161
162 case NOTE_REOPENED:
163 if errResolve == nil {
164 return nil
165 }
166
167 _, err = b.OpenRaw(
168 author,
169 note.CreatedAt.Unix(),
170 map[string]string{
171 keyGitlabId: id,
172 },
173 )
174 return err
175
176 case NOTE_DESCRIPTION_CHANGED:
177 issue := gi.iterator.IssueValue()
178
179 firstComment := b.Snapshot().Comments[0]
180 // since gitlab doesn't provide the issue history
181 // we should check for "changed the description" notes and compare issue texts
182 // TODO: Check only one time and ignore next 'description change' within one issue
183 if errResolve == cache.ErrNoMatchingOp && issue.Description != firstComment.Message {
184 // comment edition
185 _, err = b.EditCommentRaw(
186 author,
187 note.UpdatedAt.Unix(),
188 git.Hash(firstComment.Id()),
189 issue.Description,
190 map[string]string{
191 keyGitlabId: id,
192 },
193 )
194
195 return err
196 }
197
198 case NOTE_COMMENT:
199 cleanText, err := text.Cleanup(body)
200 if err != nil {
201 return err
202 }
203
204 // if we didn't import the comment
205 if errResolve == cache.ErrNoMatchingOp {
206
207 // add comment operation
208 _, err = b.AddCommentRaw(
209 author,
210 note.CreatedAt.Unix(),
211 cleanText,
212 nil,
213 map[string]string{
214 keyGitlabId: id,
215 },
216 )
217
218 return err
219 }
220
221 // if comment was already exported
222
223 // search for last comment update
224 comment, err := b.Snapshot().SearchComment(hash)
225 if err != nil {
226 return err
227 }
228
229 // compare local bug comment with the new note body
230 if comment.Message != cleanText {
231 // comment edition
232 _, err = b.EditCommentRaw(
233 author,
234 note.UpdatedAt.Unix(),
235 git.Hash(comment.Id()),
236 cleanText,
237 nil,
238 )
239
240 return err
241 }
242
243 return nil
244
245 case NOTE_TITLE_CHANGED:
246 // title change events are given new notes
247 if errResolve == nil {
248 return nil
249 }
250
251 _, err = b.SetTitleRaw(
252 author,
253 note.CreatedAt.Unix(),
254 body,
255 map[string]string{
256 keyGitlabId: id,
257 },
258 )
259
260 return err
261
262 case NOTE_UNKNOWN,
263 NOTE_ASSIGNED,
264 NOTE_UNASSIGNED,
265 NOTE_CHANGED_MILESTONE,
266 NOTE_REMOVED_MILESTONE,
267 NOTE_CHANGED_DUEDATE,
268 NOTE_REMOVED_DUEDATE,
269 NOTE_LOCKED,
270 NOTE_UNLOCKED:
271 return nil
272
273 default:
274 panic("unhandled note type")
275 }
276
277 return nil
278}
279
280func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
281 _, err := b.ResolveOperationWithMetadata(keyGitlabId, parseID(labelEvent.ID))
282 if err != cache.ErrNoMatchingOp {
283 return err
284 }
285
286 // ensure issue author
287 author, err := gi.ensurePerson(repo, labelEvent.User.ID)
288 if err != nil {
289 return err
290 }
291
292 switch labelEvent.Action {
293 case "add":
294 _, err = b.ForceChangeLabelsRaw(
295 author,
296 labelEvent.CreatedAt.Unix(),
297 []string{labelEvent.Label.Name},
298 nil,
299 map[string]string{
300 keyGitlabId: parseID(labelEvent.ID),
301 },
302 )
303
304 case "remove":
305 _, err = b.ForceChangeLabelsRaw(
306 author,
307 labelEvent.CreatedAt.Unix(),
308 nil,
309 []string{labelEvent.Label.Name},
310 map[string]string{
311 keyGitlabId: parseID(labelEvent.ID),
312 },
313 )
314
315 default:
316 err = fmt.Errorf("unexpected label event action")
317 }
318
319 return err
320}
321
322func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
323 // Look first in the cache
324 i, err := repo.ResolveIdentityImmutableMetadata(keyGitlabId, strconv.Itoa(id))
325 if err == nil {
326 return i, nil
327 }
328 if _, ok := err.(identity.ErrMultipleMatch); ok {
329 return nil, err
330 }
331
332 client := buildClient(gi.conf["token"])
333
334 user, _, err := client.Users.GetUser(id)
335 if err != nil {
336 return nil, err
337 }
338
339 // importing a new identity
340 gi.importedIdentities++
341
342 return repo.NewIdentityRaw(
343 user.Name,
344 user.PublicEmail,
345 user.Username,
346 user.AvatarURL,
347 map[string]string{
348 // because Gitlab
349 keyGitlabId: strconv.Itoa(id),
350 keyGitlabLogin: user.Username,
351 },
352 )
353}
354
355func parseID(id int) string {
356 return fmt.Sprintf("%d", id)
357}