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
17type gitlabImporter struct {
18 conf core.Configuration
19
20 // iterator
21 iterator *iterator
22
23 // number of imported issues
24 importedIssues int
25
26 // number of imported identities
27 importedIdentities int
28}
29
30func (gi *gitlabImporter) Init(conf core.Configuration) error {
31 gi.conf = conf
32 return nil
33}
34
35func (gi *gitlabImporter) ImportAll(repo *cache.RepoCache, since time.Time) error {
36 gi.iterator = NewIterator(gi.conf[keyProjectID], gi.conf[keyToken], since)
37
38 // Loop over all matching issues
39 for gi.iterator.NextIssue() {
40 issue := gi.iterator.IssueValue()
41 fmt.Printf("importing issue: %v\n", issue.Title)
42
43 // create issue
44 b, err := gi.ensureIssue(repo, issue)
45 if err != nil {
46 return fmt.Errorf("issue creation: %v", err)
47 }
48
49 // Loop over all notes
50 for gi.iterator.NextNote() {
51 note := gi.iterator.NoteValue()
52 if err := gi.ensureNote(repo, b, note); err != nil {
53 return fmt.Errorf("note creation: %v", err)
54 }
55 }
56
57 // Loop over all label events
58 for gi.iterator.NextLabelEvent() {
59 labelEvent := gi.iterator.LabelEventValue()
60 if err := gi.ensureLabelEvent(repo, b, labelEvent); err != nil {
61 return fmt.Errorf("label event creation: %v", err)
62 }
63 }
64
65 // commit bug state
66 if err := b.CommitAsNeeded(); err != nil {
67 return fmt.Errorf("bug commit: %v", err)
68 }
69 }
70
71 if err := gi.iterator.Error(); err != nil {
72 fmt.Printf("import error: %v\n", err)
73 return err
74 }
75
76 fmt.Printf("Successfully imported %d issues and %d identities from Gitlab\n", gi.importedIssues, gi.importedIdentities)
77 return nil
78}
79
80func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
81 // ensure issue author
82 author, err := gi.ensurePerson(repo, issue.Author.ID)
83 if err != nil {
84 return nil, err
85 }
86
87 // resolve bug
88 b, err := repo.ResolveBugCreateMetadata(keyGitlabUrl, issue.WebURL)
89 if err != nil && err != bug.ErrBugNotExist {
90 return nil, err
91 }
92
93 if err == bug.ErrBugNotExist {
94 cleanText, err := text.Cleanup(string(issue.Description))
95 if err != nil {
96 return nil, err
97 }
98
99 // create bug
100 b, _, err = repo.NewBugRaw(
101 author,
102 issue.CreatedAt.Unix(),
103 issue.Title,
104 cleanText,
105 nil,
106 map[string]string{
107 keyOrigin: target,
108 keyGitlabId: parseID(issue.ID),
109 keyGitlabUrl: issue.WebURL,
110 },
111 )
112
113 if err != nil {
114 return nil, err
115 }
116
117 // importing a new bug
118 gi.importedIssues++
119
120 return b, nil
121 }
122
123 return b, nil
124}
125
126func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, note *gitlab.Note) error {
127 id := parseID(note.ID)
128
129 // ensure issue author
130 author, err := gi.ensurePerson(repo, note.Author.ID)
131 if err != nil {
132 return err
133 }
134
135 hash, errResolve := b.ResolveOperationWithMetadata(keyGitlabId, id)
136 if errResolve != cache.ErrNoMatchingOp {
137 return err
138 }
139
140 noteType, body := GetNoteType(note)
141 switch noteType {
142 case NOTE_CLOSED:
143 _, err = b.CloseRaw(
144 author,
145 note.CreatedAt.Unix(),
146 map[string]string{
147 keyGitlabId: id,
148 keyGitlabUrl: "",
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 keyGitlabUrl: "",
160 },
161 )
162 return err
163
164 case NOTE_DESCRIPTION_CHANGED:
165 issue := gi.iterator.IssueValue()
166
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 != b.Snapshot().Comments[0].Message {
171
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 errResolve == 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 comment, err := b.Snapshot().SearchComment(hash)
222 if err != nil {
223 return err
224 }
225
226 // compare local bug comment with the new note body
227 if comment.Message != cleanText {
228 // comment edition
229 _, err = b.EditCommentRaw(
230 author,
231 note.UpdatedAt.Unix(),
232 target,
233 cleanText,
234 map[string]string{
235 // no metadata unique metadata to store
236 keyGitlabId: "",
237 keyGitlabUrl: "",
238 },
239 )
240
241 return err
242 }
243
244 return nil
245
246 case NOTE_TITLE_CHANGED:
247 // title change events are given new notes
248 _, err = b.SetTitleRaw(
249 author,
250 note.CreatedAt.Unix(),
251 body,
252 map[string]string{
253 keyGitlabId: id,
254 keyGitlabUrl: "",
255 },
256 )
257
258 return err
259
260 default:
261 // non handled note types, this is not an error
262 //TODO: send warning via channel
263 return nil
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 panic("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 // importing a new identity
322 gi.importedIdentities++
323
324 client := buildClient(gi.conf["token"])
325
326 user, _, err := client.Users.GetUser(id)
327 if err != nil {
328 return nil, err
329 }
330
331 return repo.NewIdentityRaw(
332 user.Name,
333 user.PublicEmail,
334 user.Username,
335 user.AvatarURL,
336 map[string]string{
337 keyGitlabId: strconv.Itoa(id),
338 keyGitlabLogin: user.Username,
339 },
340 )
341}
342
343func parseID(id int) string {
344 return fmt.Sprintf("%d", id)
345}