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 issueEditIter struct {
27 iterVars
28 query issueEditQuery_A
29}
30
31type iterVars struct {
32 index int
33 capacity int
34 variables varmap
35}
36
37type varmap map[string]interface{}
38
39
40func NewIterator_A(ctx context.Context, client *githubv4.Client, capacity int, owner, project string, since time.Time) *iterator_A {
41 i := &iterator_A{
42 gc: client,
43 since: since,
44 ctx: ctx,
45 issueIter: issueIter{
46 iterVars: newIterVars(capacity),
47 },
48 }
49 i.issueIter.variables.setOwnerProject(owner, project)
50 return i
51}
52
53func newIterVars(capacity int) iterVars {
54 return iterVars{
55 index: -1,
56 capacity: capacity,
57 variables: varmap{},
58 }
59}
60
61func (v *varmap) setOwnerProject(owner, project string) {
62 (*v)["owner"] = githubv4.String(owner)
63 (*v)["name"] = githubv4.String(project)
64}
65
66func (i *iterator_A) currIssueItem() *issue {
67 return &i.issueIter.query.Repository.Issues.Nodes[i.issueIter.index]
68}
69
70func (i *iterator_A) currIssueEditIter() *issueEditIter {
71 return &i.issueIter.issueEditIter[i.issueIter.index]
72}
73
74func (i *iterator_A) currIssueGqlNodeId() githubv4.ID {
75 return i.currIssueItem().Id
76}
77
78// Error return last encountered error
79func (i *iterator_A) Error() error {
80 if i.err != nil {
81 return i.err
82 }
83 return i.ctx.Err() // might return nil
84}
85
86func (i *iterator_A) HasError() bool {
87 return i.err != nil || i.ctx.Err() != nil
88}
89
90func (i *iterator_A) NextIssue() bool {
91 if i.HasError() {
92 return false
93 }
94 index := &i.issueIter.index
95 issues := &i.issueIter.query.Repository.Issues
96 issueItems := &issues.Nodes
97 if 0 <= *index && *index < len(*issueItems)-1 {
98 *index += 1
99 return true
100 }
101
102 if !issues.PageInfo.HasNextPage {
103 return false
104 }
105 nextIssue := i.queryIssue()
106 return nextIssue
107}
108
109func (i *iterator_A) IssueValue() issue {
110 return *i.currIssueItem()
111}
112
113func (i *iterator_A) queryIssue() bool {
114 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
115 defer cancel()
116 if endCursor := i.issueIter.query.Repository.Issues.PageInfo.EndCursor; endCursor != "" {
117 i.issueIter.variables["issueAfter"] = endCursor
118 }
119 if err := i.gc.Query(ctx, &i.issueIter.query, i.issueIter.variables); err != nil {
120 i.err = err
121 return false
122 }
123 // i.resetIssueEditVars()
124 // i.resetTimelineVars()
125 issueItems := &i.issueIter.query.Repository.Issues.Nodes
126 if len(*issueItems) <= 0 {
127 i.issueIter.index = -1
128 return false
129 }
130 i.issueIter.index = 0
131 return true
132}
133
134func (i *iterator_A) NextIssueEdit() bool {
135 if i.HasError() {
136 return false
137 }
138 ieIter := i.currIssueEditIter()
139 ieIdx := &ieIter.index
140 ieItems := ieIter.query.Node.Issue.UserContentEdits
141 if 0 <= *ieIdx && *ieIdx < len(ieItems.Nodes)-1 {
142 *ieIdx += 1
143 return i.nextValidIssueEdit()
144 }
145 if !ieItems.PageInfo.HasNextPage {
146 return false
147 }
148 querySucc := i.queryIssueEdit()
149 if !querySucc {
150 return false
151 }
152 return i.nextValidIssueEdit()
153}
154
155func (i *iterator_A) nextValidIssueEdit() bool {
156 // issueEdit.Diff == nil happen if the event is older than early 2018, Github doesn't have the data before that.
157 // Best we can do is to ignore the event.
158 if issueEdit := i.IssueEditValue(); issueEdit.Diff == nil || string(*issueEdit.Diff) == "" {
159 return i.NextIssueEdit()
160 }
161 return true
162}
163
164func (i *iterator_A) IssueEditValue() userContentEdit {
165 iei := i.currIssueEditIter()
166 return iei.query.Node.Issue.UserContentEdits.Nodes[iei.index]
167}
168
169func (i *iterator_A) queryIssueEdit() bool {
170 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
171 defer cancel()
172 iei := i.currIssueEditIter()
173 if endCursor := iei.query.Node.Issue.UserContentEdits.PageInfo.EndCursor; endCursor != "" {
174 iei.variables["issueEditBefore"] = endCursor
175 }
176 iei.variables["gqlNodeId"] = i.currIssueGqlNodeId()
177 if err := i.gc.Query(ctx, &iei.query, iei.variables); err != nil {
178 i.err = err
179 return false
180 }
181 issueEditItems := iei.query.Node.Issue.UserContentEdits.Nodes
182 if len(issueEditItems) <= 0 {
183 iei.index = -1
184 return false
185 }
186 // The UserContentEditConnection in the Github API serves its elements in reverse chronological
187 // order. For our purpose we have to reverse the edits.
188 reverseEdits(issueEditItems)
189 iei.index = 0
190 return true
191}
192
193
194
195type indexer struct{ index int }
196
197type issueEditIterator struct {
198 index int
199 query issueEditQuery
200 variables map[string]interface{}
201}
202
203type commentEditIterator struct {
204 index int
205 query commentEditQuery
206 variables map[string]interface{}
207}
208
209type timelineIterator struct {
210 index int
211 query issueTimelineQuery
212 variables map[string]interface{}
213
214 issueEdit indexer
215 commentEdit indexer
216
217 // Alex: It would be really help clearity to get rid of this variable.
218 // lastEndCursor cache the timeline end cursor for one iteration
219 lastEndCursor githubv4.String
220}
221
222type iterator struct {
223 // github graphql client
224 gc *githubv4.Client
225
226 // if since is given the iterator will query only the updated
227 // and created issues after this date
228 since time.Time
229
230 // number of timelines/userEditcontent/issueEdit to query
231 // at a time, more capacity = more used memory = less queries
232 // to make
233 capacity int
234
235 // shared context used for all graphql queries
236 ctx context.Context
237
238 // sticky error
239 err error
240
241 // timeline iterator
242 timeline timelineIterator
243
244 // issue edit iterator
245 issueEdit issueEditIterator
246
247 // comment edit iterator
248 commentEdit commentEditIterator
249}
250
251// NewIterator create and initialize a new iterator
252func NewIterator(ctx context.Context, client *githubv4.Client, capacity int, owner, project string, since time.Time) *iterator {
253 i := &iterator{
254 gc: client,
255 since: since,
256 capacity: capacity,
257 ctx: ctx,
258 timeline: timelineIterator{
259 index: -1,
260 issueEdit: indexer{-1},
261 commentEdit: indexer{-1},
262 variables: map[string]interface{}{
263 "owner": githubv4.String(owner),
264 "name": githubv4.String(project),
265 },
266 },
267 commentEdit: commentEditIterator{
268 index: -1,
269 variables: map[string]interface{}{
270 "owner": githubv4.String(owner),
271 "name": githubv4.String(project),
272 },
273 },
274 issueEdit: issueEditIterator{
275 index: -1,
276 variables: map[string]interface{}{
277 "owner": githubv4.String(owner),
278 "name": githubv4.String(project),
279 },
280 },
281 }
282
283 i.initTimelineQueryVariables()
284 return i
285}
286
287// init issue timeline variables
288func (i *iterator) initTimelineQueryVariables() {
289 i.timeline.variables["issueFirst"] = githubv4.Int(1) // each query one single issue only
290 i.timeline.variables["issueAfter"] = (*githubv4.String)(nil)
291 i.timeline.variables["issueSince"] = githubv4.DateTime{Time: i.since}
292 i.timeline.variables["timelineFirst"] = githubv4.Int(i.capacity)
293 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
294 // Fun fact, github provide the comment edition in reverse chronological
295 // order, because haha. Look at me, I'm dying of laughter.
296 i.timeline.variables["issueEditLast"] = githubv4.Int(i.capacity)
297 i.timeline.variables["issueEditBefore"] = (*githubv4.String)(nil)
298 i.timeline.variables["commentEditLast"] = githubv4.Int(i.capacity)
299 i.timeline.variables["commentEditBefore"] = (*githubv4.String)(nil)
300}
301
302// init issue edit variables
303func (i *iterator) initIssueEditQueryVariables() {
304 i.issueEdit.variables["issueFirst"] = githubv4.Int(1)
305 i.issueEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
306 i.issueEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
307 i.issueEdit.variables["issueEditLast"] = githubv4.Int(i.capacity)
308 i.issueEdit.variables["issueEditBefore"] = (*githubv4.String)(nil)
309}
310
311// init issue comment variables
312func (i *iterator) initCommentEditQueryVariables() {
313 i.commentEdit.variables["issueFirst"] = githubv4.Int(1)
314 i.commentEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
315 i.commentEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
316 i.commentEdit.variables["timelineFirst"] = githubv4.Int(1)
317 i.commentEdit.variables["timelineAfter"] = (*githubv4.String)(nil)
318 i.commentEdit.variables["commentEditLast"] = githubv4.Int(i.capacity)
319 i.commentEdit.variables["commentEditBefore"] = (*githubv4.String)(nil)
320}
321
322// reverse UserContentEdits arrays in both of the issue and
323// comment timelines
324func (i *iterator) reverseTimelineEditNodes() {
325 node := i.timeline.query.Repository.Issues.Nodes[0]
326 reverseEdits(node.UserContentEdits.Nodes)
327 for index, ce := range node.TimelineItems.Edges {
328 if ce.Node.Typename == "IssueComment" && len(node.TimelineItems.Edges) != 0 {
329 reverseEdits(node.TimelineItems.Edges[index].Node.IssueComment.UserContentEdits.Nodes)
330 }
331 }
332}
333
334// Error return last encountered error
335func (i *iterator) Error() error {
336 return i.err
337}
338
339func (i *iterator) queryIssue() bool {
340 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
341 defer cancel()
342
343 if err := i.gc.Query(ctx, &i.timeline.query, i.timeline.variables); err != nil {
344 i.err = err
345 return false
346 }
347
348 issues := i.timeline.query.Repository.Issues.Nodes
349 if len(issues) == 0 {
350 return false
351 }
352
353 i.reverseTimelineEditNodes()
354 return true
355}
356
357// NextIssue try to query the next issue and return true. Only one issue is
358// queried at each call.
359func (i *iterator) NextIssue() bool {
360 if i.err != nil {
361 return false
362 }
363
364 if i.ctx.Err() != nil {
365 return false
366 }
367
368 // if $issueAfter variable is nil we can directly make the first query
369 if i.timeline.variables["issueAfter"] == (*githubv4.String)(nil) {
370 nextIssue := i.queryIssue()
371 // prevent from infinite loop by setting a non nil cursor
372 issues := i.timeline.query.Repository.Issues
373 i.timeline.variables["issueAfter"] = issues.PageInfo.EndCursor
374 return nextIssue
375 }
376
377 issues := i.timeline.query.Repository.Issues
378 if !issues.PageInfo.HasNextPage {
379 return false
380 }
381
382 // if we have more issues, query them
383 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
384 i.timeline.index = -1
385
386 timelineEndCursor := issues.Nodes[0].TimelineItems.PageInfo.EndCursor
387 // store cursor for future use
388 i.timeline.lastEndCursor = timelineEndCursor
389
390 // query issue block
391 nextIssue := i.queryIssue()
392 i.timeline.variables["issueAfter"] = issues.PageInfo.EndCursor
393
394 return nextIssue
395}
396
397// IssueValue return the actual issue value
398func (i *iterator) IssueValue() issueTimeline {
399 issues := i.timeline.query.Repository.Issues
400 return issues.Nodes[0]
401}
402
403// NextTimelineItem return true if there is a next timeline item and increments the index by one.
404// It is used iterates over all the timeline items. Extra queries are made if it is necessary.
405func (i *iterator) NextTimelineItem() bool {
406 if i.err != nil {
407 return false
408 }
409
410 if i.ctx.Err() != nil {
411 return false
412 }
413
414 timelineItems := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems
415 // after NextIssue call it's good to check wether we have some timelineItems items or not
416 // Alex: Correct?
417 if len(timelineItems.Edges) == 0 {
418 return false
419 }
420
421 if i.timeline.index < len(timelineItems.Edges)-1 {
422 i.timeline.index++
423 return true
424 }
425
426 if !timelineItems.PageInfo.HasNextPage {
427 return false
428 }
429
430 i.timeline.lastEndCursor = timelineItems.PageInfo.EndCursor
431
432 // more timelines, query them
433 i.timeline.variables["timelineAfter"] = timelineItems.PageInfo.EndCursor
434 // HACK
435 var query timelineItemsQuery
436 // var variables map[string]interface{}
437 variables := make(map[string]interface{})
438 variables["owner"] = i.timeline.variables["owner"]
439 variables["name"] = i.timeline.variables["name"]
440 variables["issueNumber"] = i.timeline.query.Repository.Issues.Nodes[0].Number
441 fmt.Println("### Alex using issue number ", i.timeline.query.Repository.Issues.Nodes[0].Number)
442 variables["timelineFirst"] = i.timeline.variables["timelineFirst"]
443 variables["timelineAfter"] = i.timeline.variables["timelineAfter"]
444 variables["commentEditLast"] = i.timeline.variables["commentEditLast"]
445 variables["commentEditBefore"] = i.timeline.variables["commentEditBefore"]
446
447 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
448 defer cancel()
449
450 // if err := i.gc.Query(ctx, &i.timeline.query, i.timeline.variables); err != nil {
451 if err := i.gc.Query(ctx, &query, variables); err != nil {
452 i.err = err
453 return false
454 }
455 // HACK
456 fmt.Println("### Alex after the query")
457 i.timeline.variables["timelineFirst"] = variables["timelineFirst"]
458 i.timeline.variables["timelineAfter"] = variables["timelineAfter"]
459 i.timeline.variables["commentEditLast"] = variables["commentEditLast"]
460 i.timeline.variables["commentEditBefore"] = variables["commentEditBefore"]
461 i.timeline.query.Repository.Issues.Nodes[0].TimelineItems = query.Repository.Issue.TimelineItems
462
463 timelineItems = i.timeline.query.Repository.Issues.Nodes[0].TimelineItems
464 // (in case github returns something weird) just for safety: better return a false than a panic
465 if len(timelineItems.Edges) == 0 {
466 return false
467 }
468
469 i.reverseTimelineEditNodes()
470 i.timeline.index = 0
471 return true
472}
473
474// TimelineItemValue return the actual timeline item value
475func (i *iterator) TimelineItemValue() timelineItem {
476 timelineItems := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems
477 return timelineItems.Edges[i.timeline.index].Node
478}
479
480func (i *iterator) queryIssueEdit() bool {
481 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
482 defer cancel()
483
484 if err := i.gc.Query(ctx, &i.issueEdit.query, i.issueEdit.variables); err != nil {
485 i.err = err
486 //i.timeline.issueEdit.index = -1
487 return false
488 }
489
490 issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits
491 // reverse issue edits because github
492 reverseEdits(issueEdits.Nodes)
493
494 // this is not supposed to happen
495 if len(issueEdits.Nodes) == 0 {
496 i.timeline.issueEdit.index = -1
497 return false
498 }
499
500 i.issueEdit.index = 0
501 i.timeline.issueEdit.index = -2
502 return i.nextValidIssueEdit()
503}
504
505func (i *iterator) nextValidIssueEdit() bool {
506 // issueEdit.Diff == nil happen if the event is older than early 2018, Github doesn't have the data before that.
507 // Best we can do is to ignore the event.
508 if issueEdit := i.IssueEditValue(); issueEdit.Diff == nil || string(*issueEdit.Diff) == "" {
509 return i.NextIssueEdit()
510 }
511 return true
512}
513
514// NextIssueEdit return true if there is a next issue edit and increments the index by one.
515// It is used iterates over all the issue edits. Extra queries are made if it is necessary.
516func (i *iterator) NextIssueEdit() bool {
517 if i.err != nil {
518 return false
519 }
520
521 if i.ctx.Err() != nil {
522 return false
523 }
524
525 // this mean we looped over all available issue edits in the timeline.
526 // now we have to use i.issueEditQuery
527 if i.timeline.issueEdit.index == -2 {
528 issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits
529 if i.issueEdit.index < len(issueEdits.Nodes)-1 {
530 i.issueEdit.index++
531 return i.nextValidIssueEdit()
532 }
533
534 if !issueEdits.PageInfo.HasPreviousPage {
535 i.timeline.issueEdit.index = -1
536 i.issueEdit.index = -1
537 return false
538 }
539
540 // if there is more edits, query them
541 i.issueEdit.variables["issueEditBefore"] = issueEdits.PageInfo.StartCursor
542 return i.queryIssueEdit()
543 }
544
545 issueEdits := i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits
546 // if there is no edit, the UserContentEdits given by github is empty. That
547 // means that the original message is given by the issue message.
548 //
549 // if there is edits, the UserContentEdits given by github contains both the
550 // original message and the following edits. The issue message give the last
551 // version so we don't care about that.
552 //
553 // the tricky part: for an issue older than the UserContentEdits API, github
554 // doesn't have the previous message version anymore and give an edition
555 // with .Diff == nil. We have to filter them.
556 if len(issueEdits.Nodes) == 0 {
557 return false
558 }
559
560 // loop over them timeline comment edits
561 if i.timeline.issueEdit.index < len(issueEdits.Nodes)-1 {
562 i.timeline.issueEdit.index++
563 return i.nextValidIssueEdit()
564 }
565
566 if !issueEdits.PageInfo.HasPreviousPage {
567 i.timeline.issueEdit.index = -1
568 return false
569 }
570
571 // if there is more edits, query them
572 i.initIssueEditQueryVariables()
573 i.issueEdit.variables["issueEditBefore"] = issueEdits.PageInfo.StartCursor
574 return i.queryIssueEdit()
575}
576
577// IssueEditValue return the actual issue edit value
578func (i *iterator) IssueEditValue() userContentEdit {
579 // if we are using issue edit query
580 if i.timeline.issueEdit.index == -2 {
581 issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits
582 return issueEdits.Nodes[i.issueEdit.index]
583 }
584
585 issueEdits := i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits
586 // else get it from timeline issue edit query
587 return issueEdits.Nodes[i.timeline.issueEdit.index]
588}
589
590func (i *iterator) queryCommentEdit() bool {
591 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
592 defer cancel()
593
594 if err := i.gc.Query(ctx, &i.commentEdit.query, i.commentEdit.variables); err != nil {
595 i.err = err
596 return false
597 }
598
599 commentEdits := i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits
600 // this is not supposed to happen
601 if len(commentEdits.Nodes) == 0 {
602 i.timeline.commentEdit.index = -1
603 return false
604 }
605
606 reverseEdits(commentEdits.Nodes)
607
608 i.commentEdit.index = 0
609 i.timeline.commentEdit.index = -2
610 return i.nextValidCommentEdit()
611}
612
613func (i *iterator) nextValidCommentEdit() bool {
614 // if comment edit diff is a nil pointer or points to an empty string look for next value
615 if commentEdit := i.CommentEditValue(); commentEdit.Diff == nil || string(*commentEdit.Diff) == "" {
616 return i.NextCommentEdit()
617 }
618 return true
619}
620
621// NextCommentEdit return true if there is a next comment edit and increments the index by one.
622// It is used iterates over all the comment edits. Extra queries are made if it is necessary.
623func (i *iterator) NextCommentEdit() bool {
624 if i.err != nil {
625 return false
626 }
627
628 if i.ctx.Err() != nil {
629 return false
630 }
631
632 // same as NextIssueEdit
633 if i.timeline.commentEdit.index == -2 {
634 commentEdits := i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits
635 if i.commentEdit.index < len(commentEdits.Nodes)-1 {
636 i.commentEdit.index++
637 return i.nextValidCommentEdit()
638 }
639
640 if !commentEdits.PageInfo.HasPreviousPage {
641 i.timeline.commentEdit.index = -1
642 i.commentEdit.index = -1
643 return false
644 }
645
646 // if there is more comment edits, query them
647 i.commentEdit.variables["commentEditBefore"] = commentEdits.PageInfo.StartCursor
648 return i.queryCommentEdit()
649 }
650
651 commentEdits := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index].Node.IssueComment
652 // if there is no comment edits
653 if len(commentEdits.UserContentEdits.Nodes) == 0 {
654 return false
655 }
656
657 // loop over them timeline comment edits
658 if i.timeline.commentEdit.index < len(commentEdits.UserContentEdits.Nodes)-1 {
659 i.timeline.commentEdit.index++
660 return i.nextValidCommentEdit()
661 }
662
663 if !commentEdits.UserContentEdits.PageInfo.HasPreviousPage {
664 i.timeline.commentEdit.index = -1
665 return false
666 }
667
668 i.initCommentEditQueryVariables()
669 if i.timeline.index == 0 {
670 i.commentEdit.variables["timelineAfter"] = i.timeline.lastEndCursor
671 } else {
672 i.commentEdit.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index-1].Cursor
673 }
674
675 i.commentEdit.variables["commentEditBefore"] = commentEdits.UserContentEdits.PageInfo.StartCursor
676
677 return i.queryCommentEdit()
678}
679
680// CommentEditValue return the actual comment edit value
681func (i *iterator) CommentEditValue() userContentEdit {
682 if i.timeline.commentEdit.index == -2 {
683 return i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes[i.commentEdit.index]
684 }
685
686 return i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes[i.timeline.commentEdit.index]
687}
688
689func reverseEdits(edits []userContentEdit) {
690 for i, j := 0, len(edits)-1; i < j; i, j = i+1, j-1 {
691 edits[i], edits[j] = edits[j], edits[i]
692 }
693}