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