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 // ensure issue author
136 author, err := gi.ensurePerson(repo, note.Author.ID)
137 if err != nil {
138 return err
139 }
140
141 noteType, body := GetNoteType(note)
142 switch noteType {
143 case NOTE_CLOSED:
144 _, err = b.CloseRaw(
145 author,
146 note.CreatedAt.Unix(),
147 map[string]string{
148 keyGitlabId: id,
149 },
150 )
151 return err
152
153 case NOTE_REOPENED:
154 _, err = b.OpenRaw(
155 author,
156 note.CreatedAt.Unix(),
157 map[string]string{
158 keyGitlabId: id,
159 },
160 )
161 return err
162
163 case NOTE_DESCRIPTION_CHANGED:
164 issue := gi.iterator.IssueValue()
165
166 firstComment := b.Snapshot().Comments[0]
167 // since gitlab doesn't provide the issue history
168 // we should check for "changed the description" notes and compare issue texts
169 // TODO: Check only one time and ignore next 'description change' within one issue
170 if issue.Description != firstComment.Message {
171
172 // comment edition
173 _, err = b.EditCommentRaw(
174 author,
175 note.UpdatedAt.Unix(),
176 git.Hash(firstComment.Id()),
177 issue.Description,
178 map[string]string{
179 keyGitlabId: id,
180 },
181 )
182
183 return err
184 }
185
186 case NOTE_COMMENT:
187 hash, errResolve := b.ResolveOperationWithMetadata(keyGitlabId, id)
188 if errResolve != cache.ErrNoMatchingOp {
189 return errResolve
190 }
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 errResolve == 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 },
209 )
210
211 return err
212 }
213
214 // if comment was already exported
215
216 // search for last comment update
217 comment, err := b.Snapshot().SearchComment(hash)
218 if err != nil {
219 return err
220 }
221
222 // compare local bug comment with the new note body
223 if comment.Message != cleanText {
224 // comment edition
225 _, err = b.EditCommentRaw(
226 author,
227 note.UpdatedAt.Unix(),
228 git.Hash(comment.Id()),
229 cleanText,
230 nil,
231 )
232
233 return err
234 }
235
236 return nil
237
238 case NOTE_TITLE_CHANGED:
239 // title change events are given new notes
240 _, err = b.SetTitleRaw(
241 author,
242 note.CreatedAt.Unix(),
243 body,
244 map[string]string{
245 keyGitlabId: id,
246 },
247 )
248
249 return err
250
251 case NOTE_UNKNOWN,
252 NOTE_ASSIGNED,
253 NOTE_UNASSIGNED,
254 NOTE_CHANGED_MILESTONE,
255 NOTE_REMOVED_MILESTONE,
256 NOTE_CHANGED_DUEDATE,
257 NOTE_REMOVED_DUEDATE,
258 NOTE_LOCKED,
259 NOTE_UNLOCKED:
260 return nil
261
262 default:
263 panic("unhandled note type")
264 }
265
266 return nil
267}
268
269func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
270 _, err := b.ResolveOperationWithMetadata(keyGitlabId, parseID(labelEvent.ID))
271 if err != cache.ErrNoMatchingOp {
272 return err
273 }
274
275 // ensure issue author
276 author, err := gi.ensurePerson(repo, labelEvent.User.ID)
277 if err != nil {
278 return err
279 }
280
281 switch labelEvent.Action {
282 case "add":
283 _, err = b.ForceChangeLabelsRaw(
284 author,
285 labelEvent.CreatedAt.Unix(),
286 []string{labelEvent.Label.Name},
287 nil,
288 map[string]string{
289 keyGitlabId: parseID(labelEvent.ID),
290 },
291 )
292
293 case "remove":
294 _, err = b.ForceChangeLabelsRaw(
295 author,
296 labelEvent.CreatedAt.Unix(),
297 nil,
298 []string{labelEvent.Label.Name},
299 map[string]string{
300 keyGitlabId: parseID(labelEvent.ID),
301 },
302 )
303
304 default:
305 err = fmt.Errorf("unexpected label event action")
306 }
307
308 return err
309}
310
311func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
312 // Look first in the cache
313 i, err := repo.ResolveIdentityImmutableMetadata(keyGitlabId, strconv.Itoa(id))
314 if err == nil {
315 return i, nil
316 }
317 if _, ok := err.(identity.ErrMultipleMatch); ok {
318 return nil, err
319 }
320
321 client := buildClient(gi.conf["token"])
322
323 user, _, err := client.Users.GetUser(id)
324 if err != nil {
325 return nil, err
326 }
327
328 // importing a new identity
329 gi.importedIdentities++
330
331 return repo.NewIdentityRaw(
332 user.Name,
333 user.PublicEmail,
334 user.Username,
335 user.AvatarURL,
336 map[string]string{
337 // because Gitlab
338 keyGitlabId: strconv.Itoa(id),
339 keyGitlabLogin: user.Username,
340 },
341 )
342}
343
344func parseID(id int) string {
345 return fmt.Sprintf("%d", id)
346}