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/text"
15)
16
17const (
18 keyGitlabLogin = "gitlab-login"
19)
20
21type gitlabImporter struct {
22 conf core.Configuration
23
24 // iterator
25 iterator *iterator
26
27 // number of imported issues
28 importedIssues int
29
30 // number of imported identities
31 importedIdentities int
32}
33
34func (gi *gitlabImporter) Init(conf core.Configuration) error {
35 gi.conf = conf
36 return nil
37}
38
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
70 // commit bug state
71 if err := b.CommitAsNeeded(); err != nil {
72 return fmt.Errorf("bug commit: %v", err)
73 }
74 }
75
76 if err := gi.iterator.Error(); err != nil {
77 fmt.Printf("import error: %v\n", err)
78 return err
79 }
80
81 fmt.Printf("Successfully imported %d issues and %d identities from Gitlab\n", gi.importedIssues, gi.importedIdentities)
82 return nil
83}
84
85func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
86 // ensure issue author
87 author, err := gi.ensurePerson(repo, issue.Author.ID)
88 if err != nil {
89 return nil, err
90 }
91
92 // resolve bug
93 b, err := repo.ResolveBugCreateMetadata(keyGitlabUrl, issue.WebURL)
94 if err != nil && err != bug.ErrBugNotExist {
95 return nil, err
96 }
97
98 if err == bug.ErrBugNotExist {
99 cleanText, err := text.Cleanup(string(issue.Description))
100 if err != nil {
101 return nil, err
102 }
103
104 // create bug
105 b, _, err = repo.NewBugRaw(
106 author,
107 issue.CreatedAt.Unix(),
108 issue.Title,
109 cleanText,
110 nil,
111 map[string]string{
112 keyOrigin: target,
113 keyGitlabId: parseID(issue.ID),
114 keyGitlabUrl: issue.WebURL,
115 },
116 )
117
118 if err != nil {
119 return nil, err
120 }
121
122 // importing a new bug
123 gi.importedIssues++
124
125 return b, nil
126 }
127
128 return nil, nil
129}
130
131func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
132 id := parseID(note.ID)
133
134 hash, err := b.ResolveOperationWithMetadata(keyGitlabId, id)
135 if err != cache.ErrNoMatchingOp {
136 return err
137 }
138
139 // ensure issue author
140 author, err := gi.ensurePerson(repo, note.Author.ID)
141 if err != nil {
142 return err
143 }
144
145 noteType, body := GetNoteType(note)
146 switch noteType {
147 case NOTE_CLOSED:
148 _, err = b.CloseRaw(
149 author,
150 note.CreatedAt.Unix(),
151 map[string]string{
152 keyGitlabId: id,
153 },
154 )
155 return err
156
157 case NOTE_REOPENED:
158 _, err = b.OpenRaw(
159 author,
160 note.CreatedAt.Unix(),
161 map[string]string{
162 keyGitlabId: id,
163 },
164 )
165 return err
166
167 case NOTE_DESCRIPTION_CHANGED:
168 issue := gi.iterator.IssueValue()
169
170 // since gitlab doesn't provide the issue history
171 // we should check for "changed the description" notes and compare issue texts
172
173 if issue.Description != b.Snapshot().Comments[0].Message {
174 // comment edition
175 _, err = b.EditCommentRaw(
176 author,
177 note.UpdatedAt.Unix(),
178 target,
179 issue.Description,
180 map[string]string{
181 keyGitlabId: id,
182 keyGitlabUrl: "",
183 },
184 )
185
186 return err
187
188 }
189
190 case NOTE_COMMENT:
191
192 cleanText, err := text.Cleanup(body)
193 if err != nil {
194 return err
195 }
196
197 // if we didn't import the comment
198 if err == cache.ErrNoMatchingOp {
199
200 // add comment operation
201 _, err = b.AddCommentRaw(
202 author,
203 note.CreatedAt.Unix(),
204 cleanText,
205 nil,
206 map[string]string{
207 keyGitlabId: id,
208 keyGitlabUrl: "",
209 },
210 )
211
212 return err
213 }
214
215 // if comment was already exported
216
217 // if note wasn't updated
218 if note.UpdatedAt.Equal(*note.CreatedAt) {
219 return nil
220 }
221
222 // search for last comment update
223 timeline, err := b.Snapshot().SearchTimelineItem(hash)
224 if err != nil {
225 return err
226 }
227
228 item, ok := timeline.(*bug.AddCommentTimelineItem)
229 if !ok {
230 return fmt.Errorf("expected add comment time line")
231 }
232
233 // compare local bug comment with the new note body
234 if item.Message != cleanText {
235 // comment edition
236 _, err = b.EditCommentRaw(
237 author,
238 note.UpdatedAt.Unix(),
239 target,
240 cleanText,
241 map[string]string{
242 // no metadata unique metadata to store
243 keyGitlabId: "",
244 keyGitlabUrl: "",
245 },
246 )
247
248 return err
249 }
250
251 return nil
252
253 case NOTE_TITLE_CHANGED:
254
255 _, err = b.SetTitleRaw(
256 author,
257 note.CreatedAt.Unix(),
258 body,
259 map[string]string{
260 keyGitlabId: id,
261 keyGitlabUrl: "",
262 },
263 )
264
265 return err
266
267 default:
268 // non handled note types
269
270 return nil
271 }
272
273 return nil
274}
275
276func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
277 _, err := b.ResolveOperationWithMetadata(keyGitlabId, parseID(labelEvent.ID))
278 if err != cache.ErrNoMatchingOp {
279 return err
280 }
281
282 // ensure issue author
283 author, err := gi.ensurePerson(repo, labelEvent.User.ID)
284 if err != nil {
285 return err
286 }
287
288 switch labelEvent.Action {
289 case "add":
290 _, err = b.ForceChangeLabelsRaw(
291 author,
292 labelEvent.CreatedAt.Unix(),
293 []string{labelEvent.Label.Name},
294 nil,
295 map[string]string{
296 keyGitlabId: parseID(labelEvent.ID),
297 },
298 )
299
300 case "remove":
301 _, err = b.ForceChangeLabelsRaw(
302 author,
303 labelEvent.CreatedAt.Unix(),
304 nil,
305 []string{labelEvent.Label.Name},
306 map[string]string{
307 keyGitlabId: parseID(labelEvent.ID),
308 },
309 )
310
311 default:
312 panic("unexpected label event action")
313 }
314
315 return err
316}
317
318func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
319 // Look first in the cache
320 i, err := repo.ResolveIdentityImmutableMetadata(keyGitlabId, strconv.Itoa(id))
321 if err == nil {
322 return i, nil
323 }
324 if _, ok := err.(identity.ErrMultipleMatch); ok {
325 return nil, err
326 }
327
328 // importing a new identity
329 gi.importedIdentities++
330
331 client := buildClient(gi.conf["token"])
332
333 user, _, err := client.Users.GetUser(id)
334 if err != nil {
335 return nil, err
336 }
337
338 return repo.NewIdentityRaw(
339 user.Name,
340 user.PublicEmail,
341 user.Username,
342 user.AvatarURL,
343 map[string]string{
344 keyGitlabId: strconv.Itoa(id),
345 keyGitlabLogin: user.Username,
346 },
347 )
348}
349
350func parseID(id int) string {
351 return fmt.Sprintf("%d", id)
352}