1package github
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "github.com/shurcooL/githubv4"
9)
10
11type iterator_A struct {
12 gc *githubv4.Client
13 since time.Time
14 ctx context.Context
15 err error
16 issueIter issueIter
17}
18
19type issueIter struct {
20 iterVars
21 query issueQuery
22 // issueEditIter []issueEditIter
23 // timelineIter []timelineIter
24}
25
26type iterVars struct {
27 index int
28 capacity int
29 variables varmap
30}
31
32type varmap map[string]interface{}
33
34type indexer struct{ index int }
35
36type issueEditIterator struct {
37 index int
38 query issueEditQuery
39 variables map[string]interface{}
40}
41
42type commentEditIterator struct {
43 index int
44 query commentEditQuery
45 variables map[string]interface{}
46}
47
48type timelineIterator struct {
49 index int
50 query issueTimelineQuery
51 variables map[string]interface{}
52
53 issueEdit indexer
54 commentEdit indexer
55
56 // Alex: It would be really help clearity to get rid of this variable.
57 // lastEndCursor cache the timeline end cursor for one iteration
58 lastEndCursor githubv4.String
59}
60
61type iterator struct {
62 // github graphql client
63 gc *githubv4.Client
64
65 // if since is given the iterator will query only the updated
66 // and created issues after this date
67 since time.Time
68
69 // number of timelines/userEditcontent/issueEdit to query
70 // at a time, more capacity = more used memory = less queries
71 // to make
72 capacity int
73
74 // shared context used for all graphql queries
75 ctx context.Context
76
77 // sticky error
78 err error
79
80 // timeline iterator
81 timeline timelineIterator
82
83 // issue edit iterator
84 issueEdit issueEditIterator
85
86 // comment edit iterator
87 commentEdit commentEditIterator
88}
89
90// NewIterator create and initialize a new iterator
91func NewIterator(ctx context.Context, client *githubv4.Client, capacity int, owner, project string, since time.Time) *iterator {
92 i := &iterator{
93 gc: client,
94 since: since,
95 capacity: capacity,
96 ctx: ctx,
97 timeline: timelineIterator{
98 index: -1,
99 issueEdit: indexer{-1},
100 commentEdit: indexer{-1},
101 variables: map[string]interface{}{
102 "owner": githubv4.String(owner),
103 "name": githubv4.String(project),
104 },
105 },
106 commentEdit: commentEditIterator{
107 index: -1,
108 variables: map[string]interface{}{
109 "owner": githubv4.String(owner),
110 "name": githubv4.String(project),
111 },
112 },
113 issueEdit: issueEditIterator{
114 index: -1,
115 variables: map[string]interface{}{
116 "owner": githubv4.String(owner),
117 "name": githubv4.String(project),
118 },
119 },
120 }
121
122 i.initTimelineQueryVariables()
123 return i
124}
125
126// init issue timeline variables
127func (i *iterator) initTimelineQueryVariables() {
128 i.timeline.variables["issueFirst"] = githubv4.Int(1) // each query one single issue only
129 i.timeline.variables["issueAfter"] = (*githubv4.String)(nil)
130 i.timeline.variables["issueSince"] = githubv4.DateTime{Time: i.since}
131 i.timeline.variables["timelineFirst"] = githubv4.Int(i.capacity)
132 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
133 // Fun fact, github provide the comment edition in reverse chronological
134 // order, because haha. Look at me, I'm dying of laughter.
135 i.timeline.variables["issueEditLast"] = githubv4.Int(i.capacity)
136 i.timeline.variables["issueEditBefore"] = (*githubv4.String)(nil)
137 i.timeline.variables["commentEditLast"] = githubv4.Int(i.capacity)
138 i.timeline.variables["commentEditBefore"] = (*githubv4.String)(nil)
139}
140
141// init issue edit variables
142func (i *iterator) initIssueEditQueryVariables() {
143 i.issueEdit.variables["issueFirst"] = githubv4.Int(1)
144 i.issueEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
145 i.issueEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
146 i.issueEdit.variables["issueEditLast"] = githubv4.Int(i.capacity)
147 i.issueEdit.variables["issueEditBefore"] = (*githubv4.String)(nil)
148}
149
150// init issue comment variables
151func (i *iterator) initCommentEditQueryVariables() {
152 i.commentEdit.variables["issueFirst"] = githubv4.Int(1)
153 i.commentEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
154 i.commentEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
155 i.commentEdit.variables["timelineFirst"] = githubv4.Int(1)
156 i.commentEdit.variables["timelineAfter"] = (*githubv4.String)(nil)
157 i.commentEdit.variables["commentEditLast"] = githubv4.Int(i.capacity)
158 i.commentEdit.variables["commentEditBefore"] = (*githubv4.String)(nil)
159}
160
161// reverse UserContentEdits arrays in both of the issue and
162// comment timelines
163func (i *iterator) reverseTimelineEditNodes() {
164 node := i.timeline.query.Repository.Issues.Nodes[0]
165 reverseEdits(node.UserContentEdits.Nodes)
166 for index, ce := range node.TimelineItems.Edges {
167 if ce.Node.Typename == "IssueComment" && len(node.TimelineItems.Edges) != 0 {
168 reverseEdits(node.TimelineItems.Edges[index].Node.IssueComment.UserContentEdits.Nodes)
169 }
170 }
171}
172
173// Error return last encountered error
174func (i *iterator) Error() error {
175 return i.err
176}
177
178func (i *iterator) queryIssue() bool {
179 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
180 defer cancel()
181
182 if err := i.gc.Query(ctx, &i.timeline.query, i.timeline.variables); err != nil {
183 i.err = err
184 return false
185 }
186
187 issues := i.timeline.query.Repository.Issues.Nodes
188 if len(issues) == 0 {
189 return false
190 }
191
192 i.reverseTimelineEditNodes()
193 return true
194}
195
196// NextIssue try to query the next issue and return true. Only one issue is
197// queried at each call.
198func (i *iterator) NextIssue() bool {
199 if i.err != nil {
200 return false
201 }
202
203 if i.ctx.Err() != nil {
204 return false
205 }
206
207 // if $issueAfter variable is nil we can directly make the first query
208 if i.timeline.variables["issueAfter"] == (*githubv4.String)(nil) {
209 nextIssue := i.queryIssue()
210 // prevent from infinite loop by setting a non nil cursor
211 issues := i.timeline.query.Repository.Issues
212 i.timeline.variables["issueAfter"] = issues.PageInfo.EndCursor
213 return nextIssue
214 }
215
216 issues := i.timeline.query.Repository.Issues
217 if !issues.PageInfo.HasNextPage {
218 return false
219 }
220
221 // if we have more issues, query them
222 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
223 i.timeline.index = -1
224
225 timelineEndCursor := issues.Nodes[0].TimelineItems.PageInfo.EndCursor
226 // store cursor for future use
227 i.timeline.lastEndCursor = timelineEndCursor
228
229 // query issue block
230 nextIssue := i.queryIssue()
231 i.timeline.variables["issueAfter"] = issues.PageInfo.EndCursor
232
233 return nextIssue
234}
235
236// IssueValue return the actual issue value
237func (i *iterator) IssueValue() issueTimeline {
238 issues := i.timeline.query.Repository.Issues
239 return issues.Nodes[0]
240}
241
242// NextTimelineItem return true if there is a next timeline item and increments the index by one.
243// It is used iterates over all the timeline items. Extra queries are made if it is necessary.
244func (i *iterator) NextTimelineItem() bool {
245 if i.err != nil {
246 return false
247 }
248
249 if i.ctx.Err() != nil {
250 return false
251 }
252
253 timelineItems := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems
254 // after NextIssue call it's good to check wether we have some timelineItems items or not
255 // Alex: Correct?
256 if len(timelineItems.Edges) == 0 {
257 return false
258 }
259
260 if i.timeline.index < len(timelineItems.Edges)-1 {
261 i.timeline.index++
262 return true
263 }
264
265 if !timelineItems.PageInfo.HasNextPage {
266 return false
267 }
268
269 i.timeline.lastEndCursor = timelineItems.PageInfo.EndCursor
270
271 // more timelines, query them
272 i.timeline.variables["timelineAfter"] = timelineItems.PageInfo.EndCursor
273 // HACK
274 var query timelineItemsQuery
275 // var variables map[string]interface{}
276 variables := make(map[string]interface{})
277 variables["owner"] = i.timeline.variables["owner"]
278 variables["name"] = i.timeline.variables["name"]
279 variables["issueNumber"] = i.timeline.query.Repository.Issues.Nodes[0].Number
280 fmt.Println("### Alex using issue number ", i.timeline.query.Repository.Issues.Nodes[0].Number)
281 variables["timelineFirst"] = i.timeline.variables["timelineFirst"]
282 variables["timelineAfter"] = i.timeline.variables["timelineAfter"]
283 variables["commentEditLast"] = i.timeline.variables["commentEditLast"]
284 variables["commentEditBefore"] = i.timeline.variables["commentEditBefore"]
285
286 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
287 defer cancel()
288
289 // if err := i.gc.Query(ctx, &i.timeline.query, i.timeline.variables); err != nil {
290 if err := i.gc.Query(ctx, &query, variables); err != nil {
291 i.err = err
292 return false
293 }
294 // HACK
295 fmt.Println("### Alex after the query")
296 i.timeline.variables["timelineFirst"] = variables["timelineFirst"]
297 i.timeline.variables["timelineAfter"] = variables["timelineAfter"]
298 i.timeline.variables["commentEditLast"] = variables["commentEditLast"]
299 i.timeline.variables["commentEditBefore"] = variables["commentEditBefore"]
300 i.timeline.query.Repository.Issues.Nodes[0].TimelineItems = query.Repository.Issue.TimelineItems
301
302 timelineItems = i.timeline.query.Repository.Issues.Nodes[0].TimelineItems
303 // (in case github returns something weird) just for safety: better return a false than a panic
304 if len(timelineItems.Edges) == 0 {
305 return false
306 }
307
308 i.reverseTimelineEditNodes()
309 i.timeline.index = 0
310 return true
311}
312
313// TimelineItemValue return the actual timeline item value
314func (i *iterator) TimelineItemValue() timelineItem {
315 timelineItems := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems
316 return timelineItems.Edges[i.timeline.index].Node
317}
318
319func (i *iterator) queryIssueEdit() bool {
320 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
321 defer cancel()
322
323 if err := i.gc.Query(ctx, &i.issueEdit.query, i.issueEdit.variables); err != nil {
324 i.err = err
325 //i.timeline.issueEdit.index = -1
326 return false
327 }
328
329 issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits
330 // reverse issue edits because github
331 reverseEdits(issueEdits.Nodes)
332
333 // this is not supposed to happen
334 if len(issueEdits.Nodes) == 0 {
335 i.timeline.issueEdit.index = -1
336 return false
337 }
338
339 i.issueEdit.index = 0
340 i.timeline.issueEdit.index = -2
341 return i.nextValidIssueEdit()
342}
343
344func (i *iterator) nextValidIssueEdit() bool {
345 // issueEdit.Diff == nil happen if the event is older than early 2018, Github doesn't have the data before that.
346 // Best we can do is to ignore the event.
347 if issueEdit := i.IssueEditValue(); issueEdit.Diff == nil || string(*issueEdit.Diff) == "" {
348 return i.NextIssueEdit()
349 }
350 return true
351}
352
353// NextIssueEdit return true if there is a next issue edit and increments the index by one.
354// It is used iterates over all the issue edits. Extra queries are made if it is necessary.
355func (i *iterator) NextIssueEdit() bool {
356 if i.err != nil {
357 return false
358 }
359
360 if i.ctx.Err() != nil {
361 return false
362 }
363
364 // this mean we looped over all available issue edits in the timeline.
365 // now we have to use i.issueEditQuery
366 if i.timeline.issueEdit.index == -2 {
367 issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits
368 if i.issueEdit.index < len(issueEdits.Nodes)-1 {
369 i.issueEdit.index++
370 return i.nextValidIssueEdit()
371 }
372
373 if !issueEdits.PageInfo.HasPreviousPage {
374 i.timeline.issueEdit.index = -1
375 i.issueEdit.index = -1
376 return false
377 }
378
379 // if there is more edits, query them
380 i.issueEdit.variables["issueEditBefore"] = issueEdits.PageInfo.StartCursor
381 return i.queryIssueEdit()
382 }
383
384 issueEdits := i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits
385 // if there is no edit, the UserContentEdits given by github is empty. That
386 // means that the original message is given by the issue message.
387 //
388 // if there is edits, the UserContentEdits given by github contains both the
389 // original message and the following edits. The issue message give the last
390 // version so we don't care about that.
391 //
392 // the tricky part: for an issue older than the UserContentEdits API, github
393 // doesn't have the previous message version anymore and give an edition
394 // with .Diff == nil. We have to filter them.
395 if len(issueEdits.Nodes) == 0 {
396 return false
397 }
398
399 // loop over them timeline comment edits
400 if i.timeline.issueEdit.index < len(issueEdits.Nodes)-1 {
401 i.timeline.issueEdit.index++
402 return i.nextValidIssueEdit()
403 }
404
405 if !issueEdits.PageInfo.HasPreviousPage {
406 i.timeline.issueEdit.index = -1
407 return false
408 }
409
410 // if there is more edits, query them
411 i.initIssueEditQueryVariables()
412 i.issueEdit.variables["issueEditBefore"] = issueEdits.PageInfo.StartCursor
413 return i.queryIssueEdit()
414}
415
416// IssueEditValue return the actual issue edit value
417func (i *iterator) IssueEditValue() userContentEdit {
418 // if we are using issue edit query
419 if i.timeline.issueEdit.index == -2 {
420 issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits
421 return issueEdits.Nodes[i.issueEdit.index]
422 }
423
424 issueEdits := i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits
425 // else get it from timeline issue edit query
426 return issueEdits.Nodes[i.timeline.issueEdit.index]
427}
428
429func (i *iterator) queryCommentEdit() bool {
430 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
431 defer cancel()
432
433 if err := i.gc.Query(ctx, &i.commentEdit.query, i.commentEdit.variables); err != nil {
434 i.err = err
435 return false
436 }
437
438 commentEdits := i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits
439 // this is not supposed to happen
440 if len(commentEdits.Nodes) == 0 {
441 i.timeline.commentEdit.index = -1
442 return false
443 }
444
445 reverseEdits(commentEdits.Nodes)
446
447 i.commentEdit.index = 0
448 i.timeline.commentEdit.index = -2
449 return i.nextValidCommentEdit()
450}
451
452func (i *iterator) nextValidCommentEdit() bool {
453 // if comment edit diff is a nil pointer or points to an empty string look for next value
454 if commentEdit := i.CommentEditValue(); commentEdit.Diff == nil || string(*commentEdit.Diff) == "" {
455 return i.NextCommentEdit()
456 }
457 return true
458}
459
460// NextCommentEdit return true if there is a next comment edit and increments the index by one.
461// It is used iterates over all the comment edits. Extra queries are made if it is necessary.
462func (i *iterator) NextCommentEdit() bool {
463 if i.err != nil {
464 return false
465 }
466
467 if i.ctx.Err() != nil {
468 return false
469 }
470
471 // same as NextIssueEdit
472 if i.timeline.commentEdit.index == -2 {
473 commentEdits := i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits
474 if i.commentEdit.index < len(commentEdits.Nodes)-1 {
475 i.commentEdit.index++
476 return i.nextValidCommentEdit()
477 }
478
479 if !commentEdits.PageInfo.HasPreviousPage {
480 i.timeline.commentEdit.index = -1
481 i.commentEdit.index = -1
482 return false
483 }
484
485 // if there is more comment edits, query them
486 i.commentEdit.variables["commentEditBefore"] = commentEdits.PageInfo.StartCursor
487 return i.queryCommentEdit()
488 }
489
490 commentEdits := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index].Node.IssueComment
491 // if there is no comment edits
492 if len(commentEdits.UserContentEdits.Nodes) == 0 {
493 return false
494 }
495
496 // loop over them timeline comment edits
497 if i.timeline.commentEdit.index < len(commentEdits.UserContentEdits.Nodes)-1 {
498 i.timeline.commentEdit.index++
499 return i.nextValidCommentEdit()
500 }
501
502 if !commentEdits.UserContentEdits.PageInfo.HasPreviousPage {
503 i.timeline.commentEdit.index = -1
504 return false
505 }
506
507 i.initCommentEditQueryVariables()
508 if i.timeline.index == 0 {
509 i.commentEdit.variables["timelineAfter"] = i.timeline.lastEndCursor
510 } else {
511 i.commentEdit.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index-1].Cursor
512 }
513
514 i.commentEdit.variables["commentEditBefore"] = commentEdits.UserContentEdits.PageInfo.StartCursor
515
516 return i.queryCommentEdit()
517}
518
519// CommentEditValue return the actual comment edit value
520func (i *iterator) CommentEditValue() userContentEdit {
521 if i.timeline.commentEdit.index == -2 {
522 return i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes[i.commentEdit.index]
523 }
524
525 return i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes[i.timeline.commentEdit.index]
526}
527
528func reverseEdits(edits []userContentEdit) {
529 for i, j := 0, len(edits)-1; i < j; i, j = i+1, j-1 {
530 edits[i], edits[j] = edits[j], edits[i]
531 }
532}