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 // timeline iterator
53 timeline timelineIterator
54
55 // issue edit iterator
56 issueEdit issueEditIterator
57
58 // comment edit iterator
59 commentEdit commentEditIterator
60}
61
62// NewIterator create and initalize a new iterator
63func NewIterator(owner, project, token string, since time.Time) *iterator {
64 i := &iterator{
65 gc: buildClient(token),
66 since: since,
67 capacity: 10,
68 timeline: timelineIterator{
69 index: -1,
70 issueEdit: indexer{-1},
71 commentEdit: indexer{-1},
72 variables: map[string]interface{}{
73 keyOwner: githubv4.String(owner),
74 "name": githubv4.String(project),
75 },
76 },
77 commentEdit: commentEditIterator{
78 index: -1,
79 variables: map[string]interface{}{
80 keyOwner: githubv4.String(owner),
81 "name": githubv4.String(project),
82 },
83 },
84 issueEdit: issueEditIterator{
85 index: -1,
86 variables: map[string]interface{}{
87 keyOwner: githubv4.String(owner),
88 "name": githubv4.String(project),
89 },
90 },
91 }
92
93 i.initTimelineQueryVariables()
94 return i
95}
96
97// init issue timeline variables
98func (i *iterator) initTimelineQueryVariables() {
99 i.timeline.variables["issueFirst"] = githubv4.Int(1)
100 i.timeline.variables["issueAfter"] = (*githubv4.String)(nil)
101 i.timeline.variables["issueSince"] = githubv4.DateTime{Time: i.since}
102 i.timeline.variables["timelineFirst"] = githubv4.Int(i.capacity)
103 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
104 // Fun fact, github provide the comment edition in reverse chronological
105 // order, because haha. Look at me, I'm dying of laughter.
106 i.timeline.variables["issueEditLast"] = githubv4.Int(i.capacity)
107 i.timeline.variables["issueEditBefore"] = (*githubv4.String)(nil)
108 i.timeline.variables["commentEditLast"] = githubv4.Int(i.capacity)
109 i.timeline.variables["commentEditBefore"] = (*githubv4.String)(nil)
110}
111
112// init issue edit variables
113func (i *iterator) initIssueEditQueryVariables() {
114 i.issueEdit.variables["issueFirst"] = githubv4.Int(1)
115 i.issueEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
116 i.issueEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
117 i.issueEdit.variables["issueEditLast"] = githubv4.Int(i.capacity)
118 i.issueEdit.variables["issueEditBefore"] = (*githubv4.String)(nil)
119}
120
121// init issue comment variables
122func (i *iterator) initCommentEditQueryVariables() {
123 i.commentEdit.variables["issueFirst"] = githubv4.Int(1)
124 i.commentEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
125 i.commentEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
126 i.commentEdit.variables["timelineFirst"] = githubv4.Int(1)
127 i.commentEdit.variables["timelineAfter"] = (*githubv4.String)(nil)
128 i.commentEdit.variables["commentEditLast"] = githubv4.Int(i.capacity)
129 i.commentEdit.variables["commentEditBefore"] = (*githubv4.String)(nil)
130}
131
132// reverse UserContentEdits arrays in both of the issue and
133// comment timelines
134func (i *iterator) reverseTimelineEditNodes() {
135 node := i.timeline.query.Repository.Issues.Nodes[0]
136 reverseEdits(node.UserContentEdits.Nodes)
137 for index, ce := range node.Timeline.Edges {
138 if ce.Node.Typename == "IssueComment" && len(node.Timeline.Edges) != 0 {
139 reverseEdits(node.Timeline.Edges[index].Node.IssueComment.UserContentEdits.Nodes)
140 }
141 }
142}
143
144// Error return last encountered error
145func (i *iterator) Error() error {
146 return i.err
147}
148
149func (i *iterator) queryIssue() bool {
150 if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil {
151 i.err = err
152 return false
153 }
154
155 if len(i.timeline.query.Repository.Issues.Nodes) == 0 {
156 return false
157 }
158
159 i.reverseTimelineEditNodes()
160 return true
161}
162
163// NextIssue try to query the next issue and return true. Only one issue is
164// queried at each call.
165func (i *iterator) NextIssue() bool {
166 // if $issueAfter variable is nil we can directly make the first query
167 if i.timeline.variables["issueAfter"] == (*githubv4.String)(nil) {
168 nextIssue := i.queryIssue()
169 // prevent from infinite loop by setting a non nil cursor
170 i.timeline.variables["issueAfter"] = i.timeline.query.Repository.Issues.PageInfo.EndCursor
171 return nextIssue
172 }
173
174 if i.err != nil {
175 return false
176 }
177
178 if !i.timeline.query.Repository.Issues.PageInfo.HasNextPage {
179 return false
180 }
181
182 // if we have more issues, query them
183 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
184 i.timeline.variables["issueAfter"] = i.timeline.query.Repository.Issues.PageInfo.EndCursor
185 i.timeline.index = -1
186
187 // store cursor for future use
188 i.timeline.lastEndCursor = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
189
190 // query issue block
191 return i.queryIssue()
192}
193
194// IssueValue return the actual issue value
195func (i *iterator) IssueValue() issueTimeline {
196 return i.timeline.query.Repository.Issues.Nodes[0]
197}
198
199// NextTimelineItem return true if there is a next timeline item and increments the index by one.
200// It is used iterates over all the timeline items. Extra queries are made if it is necessary.
201func (i *iterator) NextTimelineItem() bool {
202 if i.err != nil {
203 return false
204 }
205
206 if len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges) == 0 {
207 return false
208 }
209
210 if i.timeline.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges))-1 {
211 i.timeline.index++
212 return true
213 }
214
215 if !i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.HasNextPage {
216 return false
217 }
218
219 i.timeline.lastEndCursor = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
220
221 // more timelines, query them
222 i.timeline.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
223 if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil {
224 i.err = err
225 return false
226 }
227
228 i.reverseTimelineEditNodes()
229 i.timeline.index = 0
230 return true
231}
232
233// TimelineItemValue return the actual timeline item value
234func (i *iterator) TimelineItemValue() 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 i.nextValidIssueEdit()
257}
258
259func (i *iterator) nextValidIssueEdit() bool {
260 // issueEdit.Diff == nil happen if the event is older than early 2018, Github doesn't have the data before that.
261 // Best we can do is to ignore the event.
262 if issueEdit := i.IssueEditValue(); issueEdit.Diff == nil || string(*issueEdit.Diff) == "" {
263 return i.NextIssueEdit()
264 }
265 return true
266}
267
268// NextIssueEdit return true if there is a next issue edit and increments the index by one.
269// It is used iterates over all the issue edits. Extra queries are made if it is necessary.
270func (i *iterator) NextIssueEdit() bool {
271 if i.err != nil {
272 return false
273 }
274
275 // this mean we looped over all available issue edits in the timeline.
276 // now we have to use i.issueEditQuery
277 if i.timeline.issueEdit.index == -2 {
278 if i.issueEdit.index < min(i.capacity, len(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes))-1 {
279 i.issueEdit.index++
280 return i.nextValidIssueEdit()
281 }
282
283 if !i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.HasPreviousPage {
284 i.timeline.issueEdit.index = -1
285 i.issueEdit.index = -1
286 return false
287 }
288
289 // if there is more edits, query them
290 i.issueEdit.variables["issueEditBefore"] = i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.StartCursor
291 return i.queryIssueEdit()
292 }
293
294 // if there is no edit, the UserContentEdits given by github is empty. That
295 // means that the original message is given by the issue message.
296 //
297 // if there is edits, the UserContentEdits given by github contains both the
298 // original message and the following edits. The issue message give the last
299 // version so we don't care about that.
300 //
301 // the tricky part: for an issue older than the UserContentEdits API, github
302 // doesn't have the previous message version anymore and give an edition
303 // with .Diff == nil. We have to filter them.
304 if len(i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes) == 0 {
305 return false
306 }
307
308 // loop over them timeline comment edits
309 if i.timeline.issueEdit.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes))-1 {
310 i.timeline.issueEdit.index++
311 return i.nextValidIssueEdit()
312 }
313
314 if !i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.HasPreviousPage {
315 i.timeline.issueEdit.index = -1
316 return false
317 }
318
319 // if there is more edits, query them
320 i.initIssueEditQueryVariables()
321 i.issueEdit.variables["issueEditBefore"] = i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.StartCursor
322 return i.queryIssueEdit()
323}
324
325// IssueEditValue return the actual issue edit value
326func (i *iterator) IssueEditValue() userContentEdit {
327 // if we are using issue edit query
328 if i.timeline.issueEdit.index == -2 {
329 return i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes[i.issueEdit.index]
330 }
331
332 // else get it from timeline issue edit query
333 return i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes[i.timeline.issueEdit.index]
334}
335
336func (i *iterator) queryCommentEdit() bool {
337 if err := i.gc.Query(context.TODO(), &i.commentEdit.query, i.commentEdit.variables); err != nil {
338 i.err = err
339 return false
340 }
341
342 // this is not supposed to happen
343 if len(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes) == 0 {
344 i.timeline.commentEdit.index = -1
345 return false
346 }
347
348 reverseEdits(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes)
349
350 i.commentEdit.index = 0
351 i.timeline.commentEdit.index = -2
352 return i.nextValidCommentEdit()
353}
354
355func (i *iterator) nextValidCommentEdit() bool {
356 // if comment edit diff is a nil pointer or points to an empty string look for next value
357 if commentEdit := i.CommentEditValue(); commentEdit.Diff == nil || string(*commentEdit.Diff) == "" {
358 return i.NextCommentEdit()
359 }
360 return true
361}
362
363// NextCommentEdit return true if there is a next comment edit and increments the index by one.
364// It is used iterates over all the comment edits. Extra queries are made if it is necessary.
365func (i *iterator) NextCommentEdit() bool {
366 if i.err != nil {
367 return false
368 }
369
370 // same as NextIssueEdit
371 if i.timeline.commentEdit.index == -2 {
372
373 if i.commentEdit.index < min(i.capacity, len(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes))-1 {
374 i.commentEdit.index++
375 return i.nextValidCommentEdit()
376 }
377
378 if !i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.PageInfo.HasPreviousPage {
379 i.timeline.commentEdit.index = -1
380 i.commentEdit.index = -1
381 return false
382 }
383
384 // if there is more comment edits, query them
385 i.commentEdit.variables["commentEditBefore"] = i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.PageInfo.StartCursor
386 return i.queryCommentEdit()
387 }
388
389 // if there is no comment edits
390 if len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes) == 0 {
391 return false
392 }
393
394 // loop over them timeline comment edits
395 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 {
396 i.timeline.commentEdit.index++
397 return i.nextValidCommentEdit()
398 }
399
400 if !i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.PageInfo.HasPreviousPage {
401 i.timeline.commentEdit.index = -1
402 return false
403 }
404
405 i.initCommentEditQueryVariables()
406 if i.timeline.index == 0 {
407 i.commentEdit.variables["timelineAfter"] = i.timeline.lastEndCursor
408 } else {
409 i.commentEdit.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index-1].Cursor
410 }
411
412 i.commentEdit.variables["commentEditBefore"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.PageInfo.StartCursor
413
414 return i.queryCommentEdit()
415}
416
417// CommentEditValue return the actual comment edit value
418func (i *iterator) CommentEditValue() userContentEdit {
419 if i.timeline.commentEdit.index == -2 {
420 return i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes[i.commentEdit.index]
421 }
422
423 return i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes[i.timeline.commentEdit.index]
424}
425
426func min(a, b int) int {
427 if a > b {
428 return b
429 }
430
431 return a
432}
433
434func reverseEdits(edits []userContentEdit) {
435 for i, j := 0, len(edits)-1; i < j; i, j = i+1, j-1 {
436 edits[i], edits[j] = edits[j], edits[i]
437 }
438}