1package github
2
3import (
4 "context"
5 "encoding/hex"
6 "encoding/json"
7 "fmt"
8 "io/ioutil"
9 "math/rand"
10 "net/http"
11 "time"
12
13 "github.com/shurcooL/githubv4"
14
15 "github.com/MichaelMure/git-bug/bridge/core"
16 "github.com/MichaelMure/git-bug/bug"
17 "github.com/MichaelMure/git-bug/cache"
18)
19
20// githubImporter implement the Importer interface
21type githubExporter struct {
22 gc *githubv4.Client
23 conf core.Configuration
24 cachedLabels map[string]githubv4.ID
25}
26
27// Init .
28func (ge *githubExporter) Init(conf core.Configuration) error {
29 ge.gc = buildClient(conf["token"])
30 ge.conf = conf
31 ge.cachedLabels = make(map[string]githubv4.ID)
32 return nil
33}
34
35// ExportAll export all event made by the current user to Github
36func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) error {
37 identity, err := repo.GetUserIdentity()
38 if err != nil {
39 return err
40 }
41
42 allBugsIds := repo.AllBugsIds()
43
44 // collect bugs
45 bugs := make([]*cache.BugCache, 0)
46 for _, id := range allBugsIds {
47 b, err := repo.ResolveBug(id)
48 if err != nil {
49 return err
50 }
51
52 snapshot := b.Snapshot()
53
54 // ignore issues edited before since date
55 if snapshot.LastEditTime().Before(since) {
56 continue
57 }
58
59 // if identity participated in a bug
60 for _, p := range snapshot.Participants {
61 if p.Id() == identity.Id() {
62 bugs = append(bugs, b)
63 }
64 }
65 }
66
67 // get repository node id
68 repositoryID, err := getRepositoryNodeID(
69 ge.conf[keyOwner],
70 ge.conf[keyProject],
71 ge.conf[keyToken],
72 )
73 if err != nil {
74 return err
75 }
76
77 for _, b := range bugs {
78 snapshot := b.Snapshot()
79 bugGithubID := ""
80
81 for _, op := range snapshot.Operations {
82 // treat only operations after since date
83 if op.Time().Before(since) {
84 continue
85 }
86
87 // ignore SetMetadata operations
88 if _, ok := op.(*bug.SetMetadataOperation); ok {
89 continue
90 }
91
92 // ignore imported issues and operations from github
93 if _, ok := op.GetMetadata(keyGithubId); ok {
94 continue
95 }
96
97 // get operation hash
98 hash, err := op.Hash()
99 if err != nil {
100 return fmt.Errorf("reading operation hash: %v", err)
101 }
102
103 // ignore already exported issues and operations
104 if _, err := b.ResolveOperationWithMetadata("github-exported-op", hash.String()); err != nil {
105 continue
106 }
107
108 switch op.(type) {
109 case *bug.CreateOperation:
110 opr := op.(*bug.CreateOperation)
111 //TODO export files
112 bugGithubID, err = ge.createGithubIssue(repositoryID, opr.Title, opr.Message)
113 if err != nil {
114 return fmt.Errorf("exporting bug %v: %v", b.HumanId(), err)
115 }
116
117 case *bug.AddCommentOperation:
118 opr := op.(*bug.AddCommentOperation)
119 bugGithubID, err = ge.addCommentGithubIssue(bugGithubID, opr.Message)
120 if err != nil {
121 return fmt.Errorf("adding comment %v: %v", "", err)
122 }
123
124 case *bug.EditCommentOperation:
125 opr := op.(*bug.EditCommentOperation)
126 if err := ge.editCommentGithubIssue(bugGithubID, opr.Message); err != nil {
127 return fmt.Errorf("editing comment %v: %v", "", err)
128 }
129
130 case *bug.SetStatusOperation:
131 opr := op.(*bug.SetStatusOperation)
132 if err := ge.updateGithubIssueStatus(bugGithubID, opr.Status); err != nil {
133 return fmt.Errorf("updating status %v: %v", bugGithubID, err)
134 }
135
136 case *bug.SetTitleOperation:
137 opr := op.(*bug.SetTitleOperation)
138 if err := ge.updateGithubIssueTitle(bugGithubID, opr.Title); err != nil {
139 return fmt.Errorf("editing comment %v: %v", bugGithubID, err)
140 }
141
142 case *bug.LabelChangeOperation:
143 opr := op.(*bug.LabelChangeOperation)
144 if err := ge.updateGithubIssueLabels(bugGithubID, opr.Added, opr.Removed); err != nil {
145 return fmt.Errorf("updating labels %v: %v", bugGithubID, err)
146 }
147
148 default:
149 // ignore other type of operations
150 }
151
152 }
153
154 if err := b.CommitAsNeeded(); err != nil {
155 return fmt.Errorf("bug commit: %v", err)
156 }
157
158 fmt.Printf("debug: %v", bugGithubID)
159 }
160
161 return nil
162}
163
164// getRepositoryNodeID request github api v3 to get repository node id
165func getRepositoryNodeID(owner, project, token string) (string, error) {
166 url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project)
167
168 client := &http.Client{
169 Timeout: defaultTimeout,
170 }
171
172 req, err := http.NewRequest("GET", url, nil)
173 if err != nil {
174 return "", err
175 }
176
177 // need the token for private repositories
178 req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
179
180 resp, err := client.Do(req)
181 if err != nil {
182 return "", err
183 }
184
185 if resp.StatusCode != http.StatusOK {
186 return "", fmt.Errorf("error retrieving repository node id %v", resp.StatusCode)
187 }
188
189 aux := struct {
190 NodeID string `json:"node_id"`
191 }{}
192
193 data, _ := ioutil.ReadAll(resp.Body)
194 defer resp.Body.Close()
195
196 err = json.Unmarshal(data, &aux)
197 if err != nil {
198 return "", err
199 }
200
201 return aux.NodeID, nil
202}
203
204func (ge *githubExporter) markOperationAsExported(b *cache.BugCache, opHash string) error {
205 return nil
206}
207
208// get label from github
209func (ge *githubExporter) getGithubLabelID(label string) (string, error) {
210 url := fmt.Sprintf("%s/repos/%s/%s/labels/%s", githubV3Url, ge.conf[keyOwner], ge.conf[keyProject], label)
211
212 client := &http.Client{
213 Timeout: defaultTimeout,
214 }
215
216 req, err := http.NewRequest("GET", url, nil)
217 if err != nil {
218 return "", err
219 }
220
221 // need the token for private repositories
222 req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.conf[keyToken]))
223
224 resp, err := client.Do(req)
225 if err != nil {
226 return "", err
227 }
228
229 if resp.StatusCode != http.StatusFound {
230 return "", fmt.Errorf("error getting label: status code: %v", resp.StatusCode)
231 }
232
233 aux := struct {
234 ID string `json:"id"`
235 NodeID string `json:"node_id"`
236 Color string `json:"color"`
237 }{}
238
239 data, _ := ioutil.ReadAll(resp.Body)
240 defer resp.Body.Close()
241
242 err = json.Unmarshal(data, &aux)
243 if err != nil {
244 return "", err
245 }
246
247 return aux.NodeID, nil
248}
249
250// create github label using api v3
251func (ge *githubExporter) createGithubLabel(label, labelColor string) (string, error) {
252 url := fmt.Sprintf("%s/repos/%s/%s/labels", githubV3Url, ge.conf[keyOwner], ge.conf[keyProject])
253
254 client := &http.Client{
255 Timeout: defaultTimeout,
256 }
257
258 req, err := http.NewRequest("POST", url, nil)
259 if err != nil {
260 return "", err
261 }
262
263 // need the token for private repositories
264 req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.conf[keyToken]))
265
266 resp, err := client.Do(req)
267 if err != nil {
268 return "", err
269 }
270
271 if resp.StatusCode != http.StatusCreated {
272 return "", fmt.Errorf("error creating label: response status %v", resp.StatusCode)
273 }
274
275 aux := struct {
276 ID string `json:"id"`
277 NodeID string `json:"node_id"`
278 Color string `json:"color"`
279 }{}
280
281 data, _ := ioutil.ReadAll(resp.Body)
282 defer resp.Body.Close()
283
284 err = json.Unmarshal(data, &aux)
285 if err != nil {
286 return "", err
287 }
288
289 return aux.NodeID, nil
290}
291
292// randomHexColor return a random hex color code
293func randomHexColor() string {
294 bytes := make([]byte, 6)
295 if _, err := rand.Read(bytes); err != nil {
296 return "fffff"
297 }
298
299 return hex.EncodeToString(bytes)
300}
301
302func (ge *githubExporter) getOrCreateGithubLabelID(repositoryID, label string) (string, error) {
303 // try to get label id
304 labelID, err := ge.getGithubLabelID(label)
305 if err == nil {
306 return labelID, nil
307 }
308
309 // random color
310 color := randomHexColor()
311
312 // create label and return id
313 labelID, err = ge.createGithubLabel(label, color)
314 if err != nil {
315 return "", err
316 }
317
318 return labelID, nil
319}
320
321func (ge *githubExporter) getLabelsIDs(repositoryID string, labels []bug.Label) ([]githubv4.ID, error) {
322 ids := make([]githubv4.ID, 0, len(labels))
323 var err error
324
325 // check labels ids
326 for _, l := range labels {
327 label := string(l)
328
329 id, ok := ge.cachedLabels[label]
330 if !ok {
331 // try to query label id
332 id, err = ge.getOrCreateGithubLabelID(repositoryID, label)
333 if err != nil {
334 return nil, fmt.Errorf("get or create github label: %v", err)
335 }
336
337 // cache label id
338 ge.cachedLabels[label] = id
339 }
340
341 ids = append(ids, githubv4.ID(id))
342 }
343
344 return ids, nil
345}
346
347// create a github issue and return it ID
348func (ge *githubExporter) createGithubIssue(repositoryID, title, body string) (string, error) {
349 m := &createIssueMutation{}
350 input := &githubv4.CreateIssueInput{
351 RepositoryID: repositoryID,
352 Title: githubv4.String(title),
353 Body: (*githubv4.String)(&body),
354 }
355
356 if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil {
357 return "", err
358 }
359
360 return m.CreateIssue.Issue.ID, nil
361}
362
363// add a comment to an issue and return it ID
364func (ge *githubExporter) addCommentGithubIssue(subjectID string, body string) (string, error) {
365 m := &addCommentToIssueMutation{}
366 input := &githubv4.AddCommentInput{
367 SubjectID: subjectID,
368 Body: githubv4.String(body),
369 }
370
371 if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil {
372 return "", err
373 }
374
375 return m.AddComment.CommentEdge.Node.ID, nil
376}
377
378func (ge *githubExporter) editCommentGithubIssue(commentID, body string) error {
379 m := &updateIssueCommentMutation{}
380 input := &githubv4.UpdateIssueCommentInput{
381 ID: commentID,
382 Body: githubv4.String(body),
383 }
384
385 if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil {
386 return err
387 }
388
389 return nil
390}
391
392func (ge *githubExporter) updateGithubIssueStatus(id string, status bug.Status) error {
393 m := &updateIssueMutation{}
394
395 // set state
396 state := githubv4.IssueStateClosed
397 if status == bug.OpenStatus {
398 state = githubv4.IssueStateOpen
399 }
400
401 input := &githubv4.UpdateIssueInput{
402 ID: id,
403 State: &state,
404 }
405
406 if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil {
407 return err
408 }
409
410 return nil
411}
412
413func (ge *githubExporter) updateGithubIssueBody(id string, body string) error {
414 m := &updateIssueMutation{}
415 input := &githubv4.UpdateIssueInput{
416 ID: id,
417 Body: (*githubv4.String)(&body),
418 }
419
420 if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil {
421 return err
422 }
423
424 return nil
425}
426
427func (ge *githubExporter) updateGithubIssueTitle(id, title string) error {
428 m := &updateIssueMutation{}
429 input := &githubv4.UpdateIssueInput{
430 ID: id,
431 Title: (*githubv4.String)(&title),
432 }
433
434 if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil {
435 return err
436 }
437
438 return nil
439}
440
441// update github issue labels
442func (ge *githubExporter) updateGithubIssueLabels(labelableID string, added, removed []bug.Label) error {
443 addedIDs, err := ge.getLabelsIDs(labelableID, added)
444 if err != nil {
445 return fmt.Errorf("getting added labels ids: %v", err)
446 }
447
448 m := &addLabelsToLabelableMutation{}
449 inputAdd := &githubv4.AddLabelsToLabelableInput{
450 LabelableID: labelableID,
451 LabelIDs: addedIDs,
452 }
453
454 // add labels
455 if err := ge.gc.Mutate(context.TODO(), m, inputAdd, nil); err != nil {
456 return err
457 }
458
459 removedIDs, err := ge.getLabelsIDs(labelableID, added)
460 if err != nil {
461 return fmt.Errorf("getting added labels ids: %v", err)
462 }
463
464 m2 := &removeLabelsFromLabelableMutation{}
465 inputRemove := &githubv4.RemoveLabelsFromLabelableInput{
466 LabelableID: labelableID,
467 LabelIDs: removedIDs,
468 }
469
470 // remove label labels
471 if err := ge.gc.Mutate(context.TODO(), m2, inputRemove, nil); err != nil {
472 return err
473 }
474
475 return nil
476}