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