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