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