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