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 return nil
253
254 default:
255 panic("unhandled note type")
256 }
257
258 return nil
259}
260
261func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCache, labelEvent *gitlab.LabelEvent) error {
262 _, err := b.ResolveOperationWithMetadata(keyGitlabId, parseID(labelEvent.ID))
263 if err != cache.ErrNoMatchingOp {
264 return err
265 }
266
267 // ensure issue author
268 author, err := gi.ensurePerson(repo, labelEvent.User.ID)
269 if err != nil {
270 return err
271 }
272
273 switch labelEvent.Action {
274 case "add":
275 _, err = b.ForceChangeLabelsRaw(
276 author,
277 labelEvent.CreatedAt.Unix(),
278 []string{labelEvent.Label.Name},
279 nil,
280 map[string]string{
281 keyGitlabId: parseID(labelEvent.ID),
282 },
283 )
284
285 case "remove":
286 _, err = b.ForceChangeLabelsRaw(
287 author,
288 labelEvent.CreatedAt.Unix(),
289 nil,
290 []string{labelEvent.Label.Name},
291 map[string]string{
292 keyGitlabId: parseID(labelEvent.ID),
293 },
294 )
295
296 default:
297 err = fmt.Errorf("unexpected label event action")
298 }
299
300 return err
301}
302
303func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
304 // Look first in the cache
305 i, err := repo.ResolveIdentityImmutableMetadata(keyGitlabId, strconv.Itoa(id))
306 if err == nil {
307 return i, nil
308 }
309 if _, ok := err.(identity.ErrMultipleMatch); ok {
310 return nil, err
311 }
312
313 client := buildClient(gi.conf["token"])
314
315 user, _, err := client.Users.GetUser(id)
316 if err != nil {
317 return nil, err
318 }
319
320 // importing a new identity
321 gi.importedIdentities++
322
323 return repo.NewIdentityRaw(
324 user.Name,
325 user.PublicEmail,
326 user.Username,
327 user.AvatarURL,
328 map[string]string{
329 // because Gitlab
330 keyGitlabId: strconv.Itoa(id),
331 keyGitlabLogin: user.Username,
332 },
333 )
334}
335
336func parseID(id int) string {
337 return fmt.Sprintf("%d", id)
338}