1package github
2
3import (
4 "context"
5 "fmt"
6 "strings"
7
8 "github.com/MichaelMure/git-bug/bridge/core"
9 "github.com/MichaelMure/git-bug/bug"
10 "github.com/MichaelMure/git-bug/cache"
11 "github.com/shurcooL/githubv4"
12)
13
14const keyGithubId = "github-id"
15const keyGithubUrl = "github-url"
16
17// githubImporter implement the Importer interface
18type githubImporter struct{}
19
20type Actor struct {
21 Login githubv4.String
22 AvatarUrl githubv4.String
23}
24
25type ActorEvent struct {
26 Id githubv4.ID
27 CreatedAt githubv4.DateTime
28 Actor Actor
29}
30
31type AuthorEvent struct {
32 Id githubv4.ID
33 CreatedAt githubv4.DateTime
34 Author Actor
35}
36
37type TimelineItem struct {
38 Typename githubv4.String `graphql:"__typename"`
39
40 // Issue
41 IssueComment struct {
42 AuthorEvent
43 Body githubv4.String
44 Url githubv4.URI
45 // TODO: edition
46 } `graphql:"... on IssueComment"`
47
48 // Label
49 LabeledEvent struct {
50 ActorEvent
51 Label struct {
52 // Color githubv4.String
53 Name githubv4.String
54 }
55 } `graphql:"... on LabeledEvent"`
56 UnlabeledEvent struct {
57 ActorEvent
58 Label struct {
59 // Color githubv4.String
60 Name githubv4.String
61 }
62 } `graphql:"... on UnlabeledEvent"`
63
64 // Status
65 ClosedEvent struct {
66 ActorEvent
67 // Url githubv4.URI
68 } `graphql:"... on ClosedEvent"`
69 ReopenedEvent struct {
70 ActorEvent
71 } `graphql:"... on ReopenedEvent"`
72
73 // Title
74 RenamedTitleEvent struct {
75 ActorEvent
76 CurrentTitle githubv4.String
77 PreviousTitle githubv4.String
78 } `graphql:"... on RenamedTitleEvent"`
79}
80
81type Issue struct {
82 AuthorEvent
83 Title string
84 Body githubv4.String
85 Url githubv4.URI
86
87 Timeline struct {
88 Nodes []TimelineItem
89 PageInfo struct {
90 EndCursor githubv4.String
91 HasNextPage bool
92 }
93 } `graphql:"timeline(first: $timelineFirst, after: $timelineAfter)"`
94}
95
96var q struct {
97 Repository struct {
98 Issues struct {
99 Nodes []Issue
100 PageInfo struct {
101 EndCursor githubv4.String
102 HasNextPage bool
103 }
104 } `graphql:"issues(first: $issueFirst, after: $issueAfter, orderBy: {field: CREATED_AT, direction: ASC})"`
105 } `graphql:"repository(owner: $owner, name: $name)"`
106}
107
108func (*githubImporter) ImportAll(repo *cache.RepoCache, conf core.Configuration) error {
109 client := buildClient(conf)
110
111 variables := map[string]interface{}{
112 "owner": githubv4.String(conf[keyUser]),
113 "name": githubv4.String(conf[keyProject]),
114 "issueFirst": githubv4.Int(1),
115 "issueAfter": (*githubv4.String)(nil),
116 "timelineFirst": githubv4.Int(10),
117 "timelineAfter": (*githubv4.String)(nil),
118 }
119
120 var b *cache.BugCache
121
122 for {
123 err := client.Query(context.TODO(), &q, variables)
124 if err != nil {
125 return err
126 }
127
128 if len(q.Repository.Issues.Nodes) != 1 {
129 return fmt.Errorf("Something went wrong when iterating issues, len is %d", len(q.Repository.Issues.Nodes))
130 }
131
132 issue := q.Repository.Issues.Nodes[0]
133
134 if b == nil {
135 b, err = importIssue(repo, issue)
136 if err != nil {
137 return err
138 }
139 }
140
141 for _, item := range q.Repository.Issues.Nodes[0].Timeline.Nodes {
142 importTimelineItem(b, item)
143 }
144
145 if !issue.Timeline.PageInfo.HasNextPage {
146 err = b.CommitAsNeeded()
147 if err != nil {
148 return err
149 }
150
151 b = nil
152
153 if !q.Repository.Issues.PageInfo.HasNextPage {
154 break
155 }
156
157 variables["issueAfter"] = githubv4.NewString(q.Repository.Issues.PageInfo.EndCursor)
158 variables["timelineAfter"] = (*githubv4.String)(nil)
159 continue
160 }
161
162 variables["timelineAfter"] = githubv4.NewString(issue.Timeline.PageInfo.EndCursor)
163 }
164
165 return nil
166}
167
168func (*githubImporter) Import(repo *cache.RepoCache, conf core.Configuration, id string) error {
169 fmt.Println(conf)
170 fmt.Println("IMPORT")
171
172 return nil
173}
174
175func importIssue(repo *cache.RepoCache, issue Issue) (*cache.BugCache, error) {
176 fmt.Printf("import issue: %s\n", issue.Title)
177
178 // TODO: check + import files
179
180 return repo.NewBugRaw(
181 makePerson(issue.Author),
182 issue.CreatedAt.Unix(),
183 issue.Title,
184 cleanupText(string(issue.Body)),
185 nil,
186 map[string]string{
187 keyGithubId: parseId(issue.Id),
188 keyGithubUrl: issue.Url.String(),
189 },
190 )
191}
192
193func importTimelineItem(b *cache.BugCache, item TimelineItem) error {
194 switch item.Typename {
195 case "IssueComment":
196 // fmt.Printf("import %s: %s\n", item.Typename, item.IssueComment)
197 return b.AddCommentRaw(
198 makePerson(item.IssueComment.Author),
199 item.IssueComment.CreatedAt.Unix(),
200 cleanupText(string(item.IssueComment.Body)),
201 nil,
202 map[string]string{
203 keyGithubId: parseId(item.IssueComment.Id),
204 keyGithubUrl: item.IssueComment.Url.String(),
205 },
206 )
207
208 case "LabeledEvent":
209 // fmt.Printf("import %s: %s\n", item.Typename, item.LabeledEvent)
210 _, err := b.ChangeLabelsRaw(
211 makePerson(item.LabeledEvent.Actor),
212 item.LabeledEvent.CreatedAt.Unix(),
213 []string{
214 string(item.LabeledEvent.Label.Name),
215 },
216 nil,
217 )
218 return err
219
220 case "UnlabeledEvent":
221 // fmt.Printf("import %s: %s\n", item.Typename, item.UnlabeledEvent)
222 _, err := b.ChangeLabelsRaw(
223 makePerson(item.UnlabeledEvent.Actor),
224 item.UnlabeledEvent.CreatedAt.Unix(),
225 nil,
226 []string{
227 string(item.UnlabeledEvent.Label.Name),
228 },
229 )
230 return err
231
232 case "ClosedEvent":
233 // fmt.Printf("import %s: %s\n", item.Typename, item.ClosedEvent)
234 return b.CloseRaw(
235 makePerson(item.ClosedEvent.Actor),
236 item.ClosedEvent.CreatedAt.Unix(),
237 )
238
239 case "ReopenedEvent":
240 // fmt.Printf("import %s: %s\n", item.Typename, item.ReopenedEvent)
241 return b.OpenRaw(
242 makePerson(item.ReopenedEvent.Actor),
243 item.ReopenedEvent.CreatedAt.Unix(),
244 )
245
246 case "RenamedTitleEvent":
247 // fmt.Printf("import %s: %s\n", item.Typename, item.RenamedTitleEvent)
248 return b.SetTitleRaw(
249 makePerson(item.RenamedTitleEvent.Actor),
250 item.RenamedTitleEvent.CreatedAt.Unix(),
251 string(item.RenamedTitleEvent.CurrentTitle),
252 )
253
254 default:
255 fmt.Println("ignore event ", item.Typename)
256 }
257
258 return nil
259}
260
261// makePerson create a bug.Person from the Github data
262func makePerson(actor Actor) bug.Person {
263 return bug.Person{
264 Name: string(actor.Login),
265 AvatarUrl: string(actor.AvatarUrl),
266 }
267}
268
269// parseId convert the unusable githubv4.ID (an interface{}) into a string
270func parseId(id githubv4.ID) string {
271 return fmt.Sprintf("%v", id)
272}
273
274func cleanupText(text string) string {
275 // windows new line, Github, really ?
276 return strings.Replace(text, "\r\n", "\n", -1)
277}