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
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 // 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 bug was never imported
97 if err == bug.ErrBugNotExist {
98 cleanText, err := text.Cleanup(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 b, 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 errResolve != 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 // TODO: Check only one time and ignore next 'description change' within one issue
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 case NOTE_UNKNOWN:
265 //TODO: send warning via channel
266 return nil
267
268 default:
269 panic("unhandled note type")
270 }
271
272 return nil
273}
274
275func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
276 _, err := b.ResolveOperationWithMetadata(keyGitlabId, parseID(labelEvent.ID))
277 if err != cache.ErrNoMatchingOp {
278 return err
279 }
280
281 // ensure issue author
282 author, err := gi.ensurePerson(repo, labelEvent.User.ID)
283 if err != nil {
284 return err
285 }
286
287 switch labelEvent.Action {
288 case "add":
289 _, err = b.ForceChangeLabelsRaw(
290 author,
291 labelEvent.CreatedAt.Unix(),
292 []string{labelEvent.Label.Name},
293 nil,
294 map[string]string{
295 keyGitlabId: parseID(labelEvent.ID),
296 },
297 )
298
299 case "remove":
300 _, err = b.ForceChangeLabelsRaw(
301 author,
302 labelEvent.CreatedAt.Unix(),
303 nil,
304 []string{labelEvent.Label.Name},
305 map[string]string{
306 keyGitlabId: parseID(labelEvent.ID),
307 },
308 )
309
310 default:
311 panic("unexpected label event action")
312 }
313
314 return err
315}
316
317func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
318 // Look first in the cache
319 i, err := repo.ResolveIdentityImmutableMetadata(keyGitlabId, strconv.Itoa(id))
320 if err == nil {
321 return i, nil
322 }
323 if _, ok := err.(identity.ErrMultipleMatch); ok {
324 return nil, err
325 }
326
327 client := buildClient(gi.conf["token"])
328
329 user, _, err := client.Users.GetUser(id)
330 if err != nil {
331 return nil, err
332 }
333
334 // importing a new identity
335 gi.importedIdentities++
336
337 return repo.NewIdentityRaw(
338 user.Name,
339 user.PublicEmail,
340 user.Username,
341 user.AvatarURL,
342 map[string]string{
343 keyGitlabId: strconv.Itoa(id),
344 keyGitlabLogin: user.Username,
345 },
346 )
347}
348
349func parseID(id int) string {
350 return fmt.Sprintf("%d", id)
351}