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