1package github
2
3import (
4 "context"
5 "time"
6
7 "github.com/shurcooL/githubv4"
8)
9
10type indexer struct{ index int }
11
12type issueEditIterator struct {
13 index int
14 query issueEditQuery
15 variables map[string]interface{}
16}
17
18type commentEditIterator struct {
19 index int
20 query commentEditQuery
21 variables map[string]interface{}
22}
23
24type timelineIterator struct {
25 index int
26 query issueTimelineQuery
27 variables map[string]interface{}
28
29 issueEdit indexer
30 commentEdit indexer
31
32 // lastEndCursor cache the timeline end cursor for one iteration
33 lastEndCursor githubv4.String
34}
35
36type iterator struct {
37 // github graphql client
38 gc *githubv4.Client
39
40 // if since is given the iterator will query only the updated
41 // and created issues after this date
42 since time.Time
43
44 // number of timelines/userEditcontent/issueEdit to query
45 // at a time, more capacity = more used memory = less queries
46 // to make
47 capacity int
48
49 // sticky error
50 err error
51
52 // number of imported issues
53 importedIssues int
54
55 // timeline iterator
56 timeline timelineIterator
57
58 // issue edit iterator
59 issueEdit issueEditIterator
60
61 // comment edit iterator
62 commentEdit commentEditIterator
63}
64
65func NewIterator(user, project, token string, since time.Time) *iterator {
66 return &iterator{
67 gc: buildClient(token),
68 since: since,
69 capacity: 10,
70 timeline: timelineIterator{
71 index: -1,
72 issueEdit: indexer{-1},
73 commentEdit: indexer{-1},
74 variables: map[string]interface{}{
75 "owner": githubv4.String(user),
76 "name": githubv4.String(project),
77 },
78 },
79 commentEdit: commentEditIterator{
80 index: -1,
81 variables: map[string]interface{}{
82 "owner": githubv4.String(user),
83 "name": githubv4.String(project),
84 },
85 },
86 issueEdit: issueEditIterator{
87 index: -1,
88 variables: map[string]interface{}{
89 "owner": githubv4.String(user),
90 "name": githubv4.String(project),
91 },
92 },
93 }
94}
95
96// init issue timeline variables
97func (i *iterator) initTimelineQueryVariables() {
98 i.timeline.variables["issueFirst"] = githubv4.Int(1)
99 i.timeline.variables["issueAfter"] = (*githubv4.String)(nil)
100 i.timeline.variables["issueSince"] = githubv4.DateTime{Time: i.since}
101 i.timeline.variables["timelineFirst"] = githubv4.Int(i.capacity)
102 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
103 // Fun fact, github provide the comment edition in reverse chronological
104 // order, because haha. Look at me, I'm dying of laughter.
105 i.timeline.variables["issueEditLast"] = githubv4.Int(i.capacity)
106 i.timeline.variables["issueEditBefore"] = (*githubv4.String)(nil)
107 i.timeline.variables["commentEditLast"] = githubv4.Int(i.capacity)
108 i.timeline.variables["commentEditBefore"] = (*githubv4.String)(nil)
109}
110
111// init issue edit variables
112func (i *iterator) initIssueEditQueryVariables() {
113 i.issueEdit.variables["issueFirst"] = githubv4.Int(1)
114 i.issueEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
115 i.issueEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
116 i.issueEdit.variables["issueEditLast"] = githubv4.Int(i.capacity)
117 i.issueEdit.variables["issueEditBefore"] = (*githubv4.String)(nil)
118}
119
120// init issue comment variables
121func (i *iterator) initCommentEditQueryVariables() {
122 i.commentEdit.variables["issueFirst"] = githubv4.Int(1)
123 i.commentEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
124 i.commentEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
125 i.commentEdit.variables["timelineFirst"] = githubv4.Int(1)
126 i.commentEdit.variables["timelineAfter"] = (*githubv4.String)(nil)
127 i.commentEdit.variables["commentEditLast"] = githubv4.Int(i.capacity)
128 i.commentEdit.variables["commentEditBefore"] = (*githubv4.String)(nil)
129}
130
131// reverse UserContentEdits arrays in both of the issue and
132// comment timelines
133func (i *iterator) reverseTimelineEditNodes() {
134 node := i.timeline.query.Repository.Issues.Nodes[0]
135 reverseEdits(node.UserContentEdits.Nodes)
136 for index, ce := range node.Timeline.Edges {
137 if ce.Node.Typename == "IssueComment" && len(node.Timeline.Edges) != 0 {
138 reverseEdits(node.Timeline.Edges[index].Node.IssueComment.UserContentEdits.Nodes)
139 }
140 }
141}
142
143// Error return last encountered error
144func (i *iterator) Error() error {
145 return i.err
146}
147
148// ImportedIssues return the number of issues we iterated over
149func (i *iterator) ImportedIssues() int {
150 return i.importedIssues
151}
152
153func (i *iterator) queryIssue() bool {
154 if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil {
155 i.err = err
156 return false
157 }
158
159 if len(i.timeline.query.Repository.Issues.Nodes) == 0 {
160 return false
161 }
162
163 i.reverseTimelineEditNodes()
164 i.importedIssues++
165 return true
166}
167
168// Next issue
169func (i *iterator) NextIssue() bool {
170 // we make the first move
171 if i.importedIssues == 0 {
172
173 // init variables and goto queryIssue block
174 i.initTimelineQueryVariables()
175 return i.queryIssue()
176 }
177
178 if i.err != nil {
179 return false
180 }
181
182 if !i.timeline.query.Repository.Issues.PageInfo.HasNextPage {
183 return false
184 }
185
186 // if we have more issues, query them
187 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
188 i.timeline.variables["issueAfter"] = i.timeline.query.Repository.Issues.PageInfo.EndCursor
189 i.timeline.index = -1
190
191 // store cursor for future use
192 i.timeline.lastEndCursor = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
193
194 // query issue block
195 return i.queryIssue()
196}
197
198func (i *iterator) IssueValue() issueTimeline {
199 return i.timeline.query.Repository.Issues.Nodes[0]
200}
201
202func (i *iterator) NextTimeline() bool {
203 if i.err != nil {
204 return false
205 }
206
207 if len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges) == 0 {
208 return false
209 }
210
211 if i.timeline.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges))-1 {
212 i.timeline.index++
213 return true
214 }
215
216 if !i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.HasNextPage {
217 return false
218 }
219
220 i.timeline.lastEndCursor = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
221
222 // more timelines, query them
223 i.timeline.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
224 if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil {
225 i.err = err
226 return false
227 }
228
229 i.reverseTimelineEditNodes()
230 i.timeline.index = 0
231 return true
232}
233
234func (i *iterator) TimelineValue() timelineItem {
235 return i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node
236}
237
238func (i *iterator) queryIssueEdit() bool {
239 if err := i.gc.Query(context.TODO(), &i.issueEdit.query, i.issueEdit.variables); err != nil {
240 i.err = err
241 //i.timeline.issueEdit.index = -1
242 return false
243 }
244
245 // reverse issue edits because github
246 reverseEdits(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes)
247
248 // this is not supposed to happen
249 if len(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes) == 0 {
250 i.timeline.issueEdit.index = -1
251 return false
252 }
253
254 i.issueEdit.index = 0
255 i.timeline.issueEdit.index = -2
256 return true
257}
258
259func (i *iterator) NextIssueEdit() bool {
260 if i.err != nil {
261 return false
262 }
263
264 // this mean we looped over all available issue edits in the timeline.
265 // now we have to use i.issueEditQuery
266 if i.timeline.issueEdit.index == -2 {
267 if i.issueEdit.index < min(i.capacity, len(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes))-1 {
268 i.issueEdit.index++
269 return true
270 }
271
272 if !i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.HasPreviousPage {
273 i.timeline.issueEdit.index = -1
274 i.issueEdit.index = -1
275 return false
276 }
277
278 // if there is more edits, query them
279 i.issueEdit.variables["issueEditBefore"] = i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.StartCursor
280 return i.queryIssueEdit()
281 }
282
283 // if there is no edit, the UserContentEdits given by github is empty. That
284 // means that the original message is given by the issue message.
285 //
286 // if there is edits, the UserContentEdits given by github contains both the
287 // original message and the following edits. The issue message give the last
288 // version so we don't care about that.
289 //
290 // the tricky part: for an issue older than the UserContentEdits API, github
291 // doesn't have the previous message version anymore and give an edition
292 // with .Diff == nil. We have to filter them.
293 if len(i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes) == 0 {
294 return false
295 }
296
297 // loop over them timeline comment edits
298 if i.timeline.issueEdit.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes))-1 {
299 i.timeline.issueEdit.index++
300 return true
301 }
302
303 if !i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.HasPreviousPage {
304 i.timeline.issueEdit.index = -1
305 return false
306 }
307
308 // if there is more edits, query them
309 i.initIssueEditQueryVariables()
310 i.issueEdit.variables["issueEditBefore"] = i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.StartCursor
311 return i.queryIssueEdit()
312}
313
314func (i *iterator) IssueEditValue() userContentEdit {
315 // if we are using issue edit query
316 if i.timeline.issueEdit.index == -2 {
317 return i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes[i.issueEdit.index]
318 }
319
320 // else get it from timeline issue edit query
321 return i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes[i.timeline.issueEdit.index]
322}
323
324func (i *iterator) queryCommentEdit() bool {
325 if err := i.gc.Query(context.TODO(), &i.commentEdit.query, i.commentEdit.variables); err != nil {
326 i.err = err
327 return false
328 }
329
330 // this is not supposed to happen
331 if len(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes) == 0 {
332 i.timeline.commentEdit.index = -1
333 return false
334 }
335
336 reverseEdits(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes)
337
338 i.commentEdit.index = 0
339 i.timeline.commentEdit.index = -2
340 return true
341}
342
343func (i *iterator) NextCommentEdit() bool {
344 if i.err != nil {
345 return false
346 }
347
348 // same as NextIssueEdit
349 if i.timeline.commentEdit.index == -2 {
350
351 if i.commentEdit.index < min(i.capacity, len(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes))-1 {
352 i.commentEdit.index++
353 return true
354 }
355
356 if !i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.PageInfo.HasPreviousPage {
357 i.timeline.commentEdit.index = -1
358 i.commentEdit.index = -1
359 return false
360 }
361
362 // if there is more comment edits, query them
363 i.commentEdit.variables["commentEditBefore"] = i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.PageInfo.StartCursor
364 return i.queryCommentEdit()
365 }
366
367 // if there is no comment edits
368 if len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes) == 0 {
369 return false
370 }
371
372 // loop over them timeline comment edits
373 if i.timeline.commentEdit.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes))-1 {
374 i.timeline.commentEdit.index++
375 return true
376 }
377
378 if !i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.PageInfo.HasPreviousPage {
379 i.timeline.commentEdit.index = -1
380 return false
381 }
382
383 i.initCommentEditQueryVariables()
384 if i.timeline.index == 0 {
385 i.commentEdit.variables["timelineAfter"] = i.timeline.lastEndCursor
386 } else {
387 i.commentEdit.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index-1].Cursor
388 }
389
390 i.commentEdit.variables["commentEditBefore"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.PageInfo.StartCursor
391
392 return i.queryCommentEdit()
393}
394
395func (i *iterator) CommentEditValue() userContentEdit {
396 if i.timeline.commentEdit.index == -2 {
397 return i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes[i.commentEdit.index]
398 }
399
400 return i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes[i.timeline.commentEdit.index]
401}
402
403func min(a, b int) int {
404 if a > b {
405 return b
406 }
407
408 return a
409}