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 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}