iterator.go

  1package github
  2
  3import (
  4	"context"
  5	"time"
  6
  7	"github.com/shurcooL/githubv4"
  8)
  9
 10type indexer struct{ index int }
 11
 12type issueEditIterator struct {
 13	index     int
 14	query     issueEditQuery
 15	variables map[string]interface{}
 16}
 17
 18type commentEditIterator struct {
 19	index     int
 20	query     commentEditQuery
 21	variables map[string]interface{}
 22}
 23
 24type timelineIterator struct {
 25	index     int
 26	query     issueTimelineQuery
 27	variables map[string]interface{}
 28
 29	issueEdit   indexer
 30	commentEdit indexer
 31
 32	// lastEndCursor cache the timeline end cursor for one iteration
 33	lastEndCursor githubv4.String
 34}
 35
 36type iterator struct {
 37	// github graphql client
 38	gc *githubv4.Client
 39
 40	// if since is given the iterator will query only the updated
 41	// and created issues after this date
 42	since time.Time
 43
 44	// number of timelines/userEditcontent/issueEdit to query
 45	// at a time, more capacity = more used memory = less queries
 46	// to make
 47	capacity int
 48
 49	// sticky error
 50	err error
 51
 52	// started
 53	started bool
 54
 55	// timeline iterator
 56	timeline timelineIterator
 57
 58	// issue edit iterator
 59	issueEdit issueEditIterator
 60
 61	// comment edit iterator
 62	commentEdit commentEditIterator
 63}
 64
 65func NewIterator(user, project, token string, since time.Time) *iterator {
 66	return &iterator{
 67		gc:       buildClient(token),
 68		since:    since,
 69		capacity: 10,
 70		timeline: timelineIterator{
 71			index:       -1,
 72			issueEdit:   indexer{-1},
 73			commentEdit: indexer{-1},
 74			variables: map[string]interface{}{
 75				"owner": githubv4.String(user),
 76				"name":  githubv4.String(project),
 77			},
 78		},
 79		commentEdit: commentEditIterator{
 80			index: -1,
 81			variables: map[string]interface{}{
 82				"owner": githubv4.String(user),
 83				"name":  githubv4.String(project),
 84			},
 85		},
 86		issueEdit: issueEditIterator{
 87			index: -1,
 88			variables: map[string]interface{}{
 89				"owner": githubv4.String(user),
 90				"name":  githubv4.String(project),
 91			},
 92		},
 93	}
 94}
 95
 96// init issue timeline variables
 97func (i *iterator) initTimelineQueryVariables() {
 98	i.timeline.variables["issueFirst"] = githubv4.Int(1)
 99	i.timeline.variables["issueAfter"] = (*githubv4.String)(nil)
100	i.timeline.variables["issueSince"] = githubv4.DateTime{Time: i.since}
101	i.timeline.variables["timelineFirst"] = githubv4.Int(i.capacity)
102	i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
103	// Fun fact, github provide the comment edition in reverse chronological
104	// order, because haha. Look at me, I'm dying of laughter.
105	i.timeline.variables["issueEditLast"] = githubv4.Int(i.capacity)
106	i.timeline.variables["issueEditBefore"] = (*githubv4.String)(nil)
107	i.timeline.variables["commentEditLast"] = githubv4.Int(i.capacity)
108	i.timeline.variables["commentEditBefore"] = (*githubv4.String)(nil)
109}
110
111// init issue edit variables
112func (i *iterator) initIssueEditQueryVariables() {
113	i.issueEdit.variables["issueFirst"] = githubv4.Int(1)
114	i.issueEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
115	i.issueEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
116	i.issueEdit.variables["issueEditLast"] = githubv4.Int(i.capacity)
117	i.issueEdit.variables["issueEditBefore"] = (*githubv4.String)(nil)
118}
119
120// init issue comment variables
121func (i *iterator) initCommentEditQueryVariables() {
122	i.commentEdit.variables["issueFirst"] = githubv4.Int(1)
123	i.commentEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
124	i.commentEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
125	i.commentEdit.variables["timelineFirst"] = githubv4.Int(1)
126	i.commentEdit.variables["timelineAfter"] = (*githubv4.String)(nil)
127	i.commentEdit.variables["commentEditLast"] = githubv4.Int(i.capacity)
128	i.commentEdit.variables["commentEditBefore"] = (*githubv4.String)(nil)
129}
130
131// reverse UserContentEdits arrays in both of the issue and
132// comment timelines
133func (i *iterator) reverseTimelineEditNodes() {
134	node := i.timeline.query.Repository.Issues.Nodes[0]
135	reverseEdits(node.UserContentEdits.Nodes)
136	for index, ce := range node.Timeline.Edges {
137		if ce.Node.Typename == "IssueComment" && len(node.Timeline.Edges) != 0 {
138			reverseEdits(node.Timeline.Edges[index].Node.IssueComment.UserContentEdits.Nodes)
139		}
140	}
141}
142
143// Error return last encountered error
144func (i *iterator) Error() error {
145	return i.err
146}
147
148func (i *iterator) queryIssue() bool {
149	if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil {
150		i.err = err
151		return false
152	}
153
154	if len(i.timeline.query.Repository.Issues.Nodes) == 0 {
155		return false
156	}
157
158	i.reverseTimelineEditNodes()
159	return true
160}
161
162// Next issue
163func (i *iterator) NextIssue() bool {
164	// we make the first move
165	if !i.started {
166		// init variables and goto queryIssue block
167		i.initTimelineQueryVariables()
168		i.started = true
169		return i.queryIssue()
170	}
171
172	if i.err != nil {
173		return false
174	}
175
176	if !i.timeline.query.Repository.Issues.PageInfo.HasNextPage {
177		return false
178	}
179
180	// if we have more issues, query them
181	i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
182	i.timeline.variables["issueAfter"] = i.timeline.query.Repository.Issues.PageInfo.EndCursor
183	i.timeline.index = -1
184
185	// store cursor for future use
186	i.timeline.lastEndCursor = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
187
188	// query issue block
189	return i.queryIssue()
190}
191
192func (i *iterator) IssueValue() issueTimeline {
193	return i.timeline.query.Repository.Issues.Nodes[0]
194}
195
196func (i *iterator) NextTimeline() bool {
197	if i.err != nil {
198		return false
199	}
200
201	if len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges) == 0 {
202		return false
203	}
204
205	if i.timeline.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges))-1 {
206		i.timeline.index++
207		return true
208	}
209
210	if !i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.HasNextPage {
211		return false
212	}
213
214	i.timeline.lastEndCursor = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
215
216	// more timelines, query them
217	i.timeline.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
218	if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil {
219		i.err = err
220		return false
221	}
222
223	i.reverseTimelineEditNodes()
224	i.timeline.index = 0
225	return true
226}
227
228func (i *iterator) TimelineValue() timelineItem {
229	return i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node
230}
231
232func (i *iterator) queryIssueEdit() bool {
233	if err := i.gc.Query(context.TODO(), &i.issueEdit.query, i.issueEdit.variables); err != nil {
234		i.err = err
235		//i.timeline.issueEdit.index = -1
236		return false
237	}
238
239	// reverse issue edits because github
240	reverseEdits(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes)
241
242	// this is not supposed to happen
243	if len(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes) == 0 {
244		i.timeline.issueEdit.index = -1
245		return false
246	}
247
248	i.issueEdit.index = 0
249	i.timeline.issueEdit.index = -2
250	return true
251}
252
253func (i *iterator) NextIssueEdit() bool {
254	if i.err != nil {
255		return false
256	}
257
258	// this mean we looped over all available issue edits in the timeline.
259	// now we have to use i.issueEditQuery
260	if i.timeline.issueEdit.index == -2 {
261		if i.issueEdit.index < min(i.capacity, len(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes))-1 {
262			i.issueEdit.index++
263			if issueEdit := i.IssueEditValue(); issueEdit.Diff == nil || string(*issueEdit.Diff) == "" {
264				return i.NextIssueEdit()
265			}
266			return true
267		}
268
269		if !i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.HasPreviousPage {
270			i.timeline.issueEdit.index = -1
271			i.issueEdit.index = -1
272			return false
273		}
274
275		// if there is more edits, query them
276		i.issueEdit.variables["issueEditBefore"] = i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.StartCursor
277		return i.queryIssueEdit()
278	}
279
280	// if there is no edit, the UserContentEdits given by github is empty. That
281	// means that the original message is given by the issue message.
282	//
283	// if there is edits, the UserContentEdits given by github contains both the
284	// original message and the following edits. The issue message give the last
285	// version so we don't care about that.
286	//
287	// the tricky part: for an issue older than the UserContentEdits API, github
288	// doesn't have the previous message version anymore and give an edition
289	// with .Diff == nil. We have to filter them.
290	if len(i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes) == 0 {
291		return false
292	}
293
294	// loop over them timeline comment edits
295	if i.timeline.issueEdit.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes))-1 {
296		i.timeline.issueEdit.index++
297		// issueEdit.Diff == nil happen if the event is older than early 2018, Github doesn't have the data before that.
298		// Best we can do is to ignore the event.
299		if issueEdit := i.IssueEditValue(); issueEdit.Diff == nil || string(*issueEdit.Diff) == "" {
300			return i.NextIssueEdit()
301		}
302		return true
303	}
304
305	if !i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.HasPreviousPage {
306		i.timeline.issueEdit.index = -1
307		return false
308	}
309
310	// if there is more edits, query them
311	i.initIssueEditQueryVariables()
312	i.issueEdit.variables["issueEditBefore"] = i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.StartCursor
313	return i.queryIssueEdit()
314}
315
316func (i *iterator) IssueEditValue() userContentEdit {
317	// if we are using issue edit query
318	if i.timeline.issueEdit.index == -2 {
319		return i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes[i.issueEdit.index]
320	}
321
322	// else get it from timeline issue edit query
323	return i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes[i.timeline.issueEdit.index]
324}
325
326func (i *iterator) queryCommentEdit() bool {
327	if err := i.gc.Query(context.TODO(), &i.commentEdit.query, i.commentEdit.variables); err != nil {
328		i.err = err
329		return false
330	}
331
332	// this is not supposed to happen
333	if len(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes) == 0 {
334		i.timeline.commentEdit.index = -1
335		return false
336	}
337
338	reverseEdits(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes)
339
340	i.commentEdit.index = 0
341	i.timeline.commentEdit.index = -2
342	return true
343}
344
345func (i *iterator) NextCommentEdit() bool {
346	if i.err != nil {
347		return false
348	}
349
350	// same as NextIssueEdit
351	if i.timeline.commentEdit.index == -2 {
352
353		if i.commentEdit.index < min(i.capacity, len(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes))-1 {
354			i.commentEdit.index++
355			if commentEdit := i.CommentEditValue(); commentEdit.Diff == nil || string(*commentEdit.Diff) == "" {
356				return i.NextCommentEdit()
357			}
358			return true
359		}
360
361		if !i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.PageInfo.HasPreviousPage {
362			i.timeline.commentEdit.index = -1
363			i.commentEdit.index = -1
364			return false
365		}
366
367		// if there is more comment edits, query them
368		i.commentEdit.variables["commentEditBefore"] = i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.PageInfo.StartCursor
369		return i.queryCommentEdit()
370	}
371
372	// if there is no comment edits
373	if len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes) == 0 {
374		return false
375	}
376
377	// loop over them timeline comment edits
378	if i.timeline.commentEdit.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes))-1 {
379		i.timeline.commentEdit.index++
380		// if comment edit diff is nil or point to an empty string look for next value
381		if commentEdit := i.CommentEditValue(); commentEdit.Diff == nil || string(*commentEdit.Diff) == "" {
382			return i.NextCommentEdit()
383		}
384		return true
385	}
386
387	if !i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.PageInfo.HasPreviousPage {
388		i.timeline.commentEdit.index = -1
389		return false
390	}
391
392	i.initCommentEditQueryVariables()
393	if i.timeline.index == 0 {
394		i.commentEdit.variables["timelineAfter"] = i.timeline.lastEndCursor
395	} else {
396		i.commentEdit.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index-1].Cursor
397	}
398
399	i.commentEdit.variables["commentEditBefore"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.PageInfo.StartCursor
400
401	return i.queryCommentEdit()
402}
403
404func (i *iterator) CommentEditValue() userContentEdit {
405	if i.timeline.commentEdit.index == -2 {
406		return i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes[i.commentEdit.index]
407	}
408
409	return i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes[i.timeline.commentEdit.index]
410}
411
412func min(a, b int) int {
413	if a > b {
414		return b
415	}
416
417	return a
418}
419
420func reverseEdits(edits []userContentEdit) {
421	for i, j := 0, len(edits)-1; i < j; i, j = i+1, j-1 {
422		edits[i], edits[j] = edits[j], edits[i]
423	}
424}