iterator.go

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