iterator.go

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