iterator.go

  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}