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	// number of imported issues
 53	importedIssues int
 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	i.timeline.variables["issueEditLast"] = githubv4.Int(i.capacity)
104	i.timeline.variables["issueEditBefore"] = (*githubv4.String)(nil)
105	i.timeline.variables["commentEditLast"] = githubv4.Int(i.capacity)
106	i.timeline.variables["commentEditBefore"] = (*githubv4.String)(nil)
107}
108
109// init issue edit variables
110func (i *iterator) initIssueEditQueryVariables() {
111	i.issueEdit.variables["issueFirst"] = githubv4.Int(1)
112	i.issueEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
113	i.issueEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
114	i.issueEdit.variables["issueEditLast"] = githubv4.Int(i.capacity)
115	i.issueEdit.variables["issueEditBefore"] = (*githubv4.String)(nil)
116}
117
118// init issue comment variables
119func (i *iterator) initCommentEditQueryVariables() {
120	i.commentEdit.variables["issueFirst"] = githubv4.Int(1)
121	i.commentEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
122	i.commentEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
123	i.commentEdit.variables["timelineFirst"] = githubv4.Int(1)
124	i.commentEdit.variables["timelineAfter"] = (*githubv4.String)(nil)
125	i.commentEdit.variables["commentEditLast"] = githubv4.Int(i.capacity)
126	i.commentEdit.variables["commentEditBefore"] = (*githubv4.String)(nil)
127}
128
129// reverse UserContentEdits arrays in both of the issue and
130// comment timelines
131func (i *iterator) reverseTimelineEditNodes() {
132	node := i.timeline.query.Repository.Issues.Nodes[0]
133	reverseEdits(node.UserContentEdits.Nodes)
134	for index, ce := range node.Timeline.Edges {
135		if ce.Node.Typename == "IssueComment" && len(node.Timeline.Edges) != 0 {
136			reverseEdits(node.Timeline.Edges[index].Node.IssueComment.UserContentEdits.Nodes)
137		}
138	}
139}
140
141// Error return last encountered error
142func (i *iterator) Error() error {
143	return i.err
144}
145
146// ImportedIssues return the number of issues we iterated over
147func (i *iterator) ImportedIssues() int {
148	return i.importedIssues
149}
150
151func (i *iterator) queryIssue() bool {
152	if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil {
153		i.err = err
154		return false
155	}
156
157	if len(i.timeline.query.Repository.Issues.Nodes) == 0 {
158		return false
159	}
160
161	i.reverseTimelineEditNodes()
162	i.importedIssues++
163	return true
164}
165
166// Next issue
167func (i *iterator) NextIssue() bool {
168	// we make the first move
169	if i.importedIssues == 0 {
170
171		// init variables and goto queryIssue block
172		i.initTimelineQueryVariables()
173		return i.queryIssue()
174	}
175
176	if i.err != nil {
177		return false
178	}
179
180	if !i.timeline.query.Repository.Issues.PageInfo.HasNextPage {
181		return false
182	}
183
184	// if we have more issues, query them
185	i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
186	i.timeline.variables["issueAfter"] = i.timeline.query.Repository.Issues.PageInfo.EndCursor
187	i.timeline.index = -1
188
189	// store cursor for future use
190	i.timeline.lastEndCursor = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
191
192	// query issue block
193	return i.queryIssue()
194}
195
196func (i *iterator) IssueValue() issueTimeline {
197	return i.timeline.query.Repository.Issues.Nodes[0]
198}
199
200func (i *iterator) NextTimeline() bool {
201	if i.err != nil {
202		return false
203	}
204
205	if len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges) == 0 {
206		return false
207	}
208
209	if i.timeline.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges))-1 {
210		i.timeline.index++
211		return true
212	}
213
214	if !i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.HasNextPage {
215		return false
216	}
217
218	i.timeline.lastEndCursor = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
219
220	// more timelines, query them
221	i.timeline.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
222	if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil {
223		i.err = err
224		return false
225	}
226
227	i.reverseTimelineEditNodes()
228	i.timeline.index = 0
229	return true
230}
231
232func (i *iterator) TimelineValue() timelineItem {
233	return i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node
234}
235
236func (i *iterator) queryIssueEdit() bool {
237	if err := i.gc.Query(context.TODO(), &i.issueEdit.query, i.issueEdit.variables); err != nil {
238		i.err = err
239		//i.timeline.issueEdit.index = -1
240		return false
241	}
242
243	// reverse issue edits because github
244	reverseEdits(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes)
245
246	// this is not supposed to happen
247	if len(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes) == 0 {
248		i.timeline.issueEdit.index = -1
249		return false
250	}
251
252	i.issueEdit.index = 0
253	i.timeline.issueEdit.index = -2
254	return true
255}
256
257func (i *iterator) NextIssueEdit() bool {
258	if i.err != nil {
259		return false
260	}
261
262	// this mean we looped over all available issue edits in the timeline.
263	// now we have to use i.issueEditQuery
264	if i.timeline.issueEdit.index == -2 {
265		if i.issueEdit.index < min(i.capacity, len(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes))-1 {
266			i.issueEdit.index++
267			return true
268		}
269
270		if !i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.HasPreviousPage {
271			i.timeline.issueEdit.index = -1
272			i.issueEdit.index = -1
273			return false
274		}
275
276		// if there is more edits, query them
277		i.issueEdit.variables["issueEditBefore"] = i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.StartCursor
278		return i.queryIssueEdit()
279	}
280
281	// if there is no edits
282	if len(i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes) == 0 {
283		return false
284	}
285
286	// loop over them timeline comment edits
287	if i.timeline.issueEdit.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes))-1 {
288		i.timeline.issueEdit.index++
289		return true
290	}
291
292	if !i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.HasPreviousPage {
293		i.timeline.issueEdit.index = -1
294		return false
295	}
296
297	// if there is more edits, query them
298	i.initIssueEditQueryVariables()
299	i.issueEdit.variables["issueEditBefore"] = i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.StartCursor
300	return i.queryIssueEdit()
301}
302
303func (i *iterator) IssueEditValue() userContentEdit {
304	// if we are using issue edit query
305	if i.timeline.issueEdit.index == -2 {
306		return i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes[i.issueEdit.index]
307	}
308
309	// else get it from timeline issue edit query
310	return i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes[i.timeline.issueEdit.index]
311}
312
313func (i *iterator) queryCommentEdit() bool {
314	if err := i.gc.Query(context.TODO(), &i.commentEdit.query, i.commentEdit.variables); err != nil {
315		i.err = err
316		return false
317	}
318
319	// this is not supposed to happen
320	if len(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes) == 0 {
321		i.timeline.commentEdit.index = -1
322		return false
323	}
324
325	reverseEdits(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes)
326
327	i.commentEdit.index = 0
328	i.timeline.commentEdit.index = -2
329	return true
330}
331
332func (i *iterator) NextCommentEdit() bool {
333	if i.err != nil {
334		return false
335	}
336
337	// same as NextIssueEdit
338	if i.timeline.commentEdit.index == -2 {
339
340		if i.commentEdit.index < min(i.capacity, len(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes))-1 {
341			i.commentEdit.index++
342			return true
343		}
344
345		if !i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.PageInfo.HasPreviousPage {
346			i.timeline.commentEdit.index = -1
347			i.commentEdit.index = -1
348			return false
349		}
350
351		// if there is more comment edits, query them
352		i.commentEdit.variables["commentEditBefore"] = i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.PageInfo.StartCursor
353		return i.queryCommentEdit()
354	}
355
356	// if there is no comment edits
357	if len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes) == 0 {
358		return false
359	}
360
361	// loop over them timeline comment edits
362	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 {
363		i.timeline.commentEdit.index++
364		return true
365	}
366
367	if !i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.PageInfo.HasPreviousPage {
368		i.timeline.commentEdit.index = -1
369		return false
370	}
371
372	i.initCommentEditQueryVariables()
373	if i.timeline.index == 0 {
374		i.commentEdit.variables["timelineAfter"] = i.timeline.lastEndCursor
375	} else {
376		i.commentEdit.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index-1].Cursor
377	}
378
379	i.commentEdit.variables["commentEditBefore"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.PageInfo.StartCursor
380
381	return i.queryCommentEdit()
382}
383
384func (i *iterator) CommentEditValue() userContentEdit {
385	if i.timeline.commentEdit.index == -2 {
386		return i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes[i.commentEdit.index]
387	}
388
389	return i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes[i.timeline.commentEdit.index]
390}
391
392func min(a, b int) int {
393	if a > b {
394		return b
395	}
396
397	return a
398}