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