iterator.go

  1package github
  2
  3import (
  4	"context"
  5	"time"
  6
  7	"github.com/pkg/errors"
  8	"github.com/shurcooL/githubv4"
  9)
 10
 11type iterator struct {
 12	// Github graphql client
 13	gc *githubv4.Client
 14
 15	// The iterator will only query issues updated or created after the date given in
 16	// the variable since.
 17	since time.Time
 18
 19	// Shared context, which is used for all graphql queries.
 20	ctx context.Context
 21
 22	// Sticky error
 23	err error
 24
 25	// Issue iterator
 26	issueIter issueIter
 27}
 28
 29type issueIter struct {
 30	iterVars
 31	query         issueQuery
 32	issueEditIter []issueEditIter
 33	timelineIter  []timelineIter
 34}
 35
 36type issueEditIter struct {
 37	iterVars
 38	query issueEditQuery
 39}
 40
 41type timelineIter struct {
 42	iterVars
 43	query           timelineQuery
 44	commentEditIter []commentEditIter
 45}
 46
 47type commentEditIter struct {
 48	iterVars
 49	query commentEditQuery
 50}
 51
 52type iterVars struct {
 53	// Iterator index
 54	index int
 55
 56	// capacity is the number of elements (issues, issue edits, timeline items, or
 57	// comment edits) to query at a time. More capacity = more used memory =
 58	// less queries to make.
 59	capacity int
 60
 61	// Variable assignments for graphql query
 62	variables varmap
 63}
 64
 65type varmap map[string]interface{}
 66
 67func newIterVars(capacity int) iterVars {
 68	return iterVars{
 69		index:     -1,
 70		capacity:  capacity,
 71		variables: varmap{},
 72	}
 73}
 74
 75// NewIterator creates and initialize a new iterator.
 76func NewIterator(ctx context.Context, client *githubv4.Client, capacity int, owner, project string, since time.Time) *iterator {
 77	i := &iterator{
 78		gc:    client,
 79		since: since,
 80		ctx:   ctx,
 81		issueIter: issueIter{
 82			iterVars:      newIterVars(capacity),
 83			timelineIter:  make([]timelineIter, capacity),
 84			issueEditIter: make([]issueEditIter, capacity),
 85		},
 86	}
 87	i.issueIter.variables.setOwnerProject(owner, project)
 88	for idx := range i.issueIter.issueEditIter {
 89		ie := &i.issueIter.issueEditIter[idx]
 90		ie.iterVars = newIterVars(capacity)
 91	}
 92	for i1 := range i.issueIter.timelineIter {
 93		tli := &i.issueIter.timelineIter[i1]
 94		tli.iterVars = newIterVars(capacity)
 95		tli.commentEditIter = make([]commentEditIter, capacity)
 96		for i2 := range tli.commentEditIter {
 97			cei := &tli.commentEditIter[i2]
 98			cei.iterVars = newIterVars(capacity)
 99		}
100	}
101	i.resetIssueVars()
102	return i
103}
104
105func (v *varmap) setOwnerProject(owner, project string) {
106	(*v)["owner"] = githubv4.String(owner)
107	(*v)["name"] = githubv4.String(project)
108}
109
110func (i *iterator) resetIssueVars() {
111	vars := &i.issueIter.variables
112	(*vars)["issueFirst"] = githubv4.Int(i.issueIter.capacity)
113	(*vars)["issueAfter"] = (*githubv4.String)(nil)
114	(*vars)["issueSince"] = githubv4.DateTime{Time: i.since}
115	i.issueIter.query.Repository.Issues.PageInfo.HasNextPage = true
116	i.issueIter.query.Repository.Issues.PageInfo.EndCursor = ""
117}
118
119func (i *iterator) resetIssueEditVars() {
120	for idx := range i.issueIter.issueEditIter {
121		ie := &i.issueIter.issueEditIter[idx]
122		ie.variables["issueEditLast"] = githubv4.Int(ie.capacity)
123		ie.variables["issueEditBefore"] = (*githubv4.String)(nil)
124		ie.query.Node.Issue.UserContentEdits.PageInfo.HasNextPage = true
125		ie.query.Node.Issue.UserContentEdits.PageInfo.EndCursor = ""
126	}
127}
128
129func (i *iterator) resetTimelineVars() {
130	for idx := range i.issueIter.timelineIter {
131		ip := &i.issueIter.timelineIter[idx]
132		ip.variables["timelineFirst"] = githubv4.Int(ip.capacity)
133		ip.variables["timelineAfter"] = (*githubv4.String)(nil)
134		ip.query.Node.Issue.TimelineItems.PageInfo.HasNextPage = true
135		ip.query.Node.Issue.TimelineItems.PageInfo.EndCursor = ""
136	}
137}
138
139func (i *iterator) resetCommentEditVars() {
140	for i1 := range i.issueIter.timelineIter {
141		for i2 := range i.issueIter.timelineIter[i1].commentEditIter {
142			ce := &i.issueIter.timelineIter[i1].commentEditIter[i2]
143			ce.variables["commentEditLast"] = githubv4.Int(ce.capacity)
144			ce.variables["commentEditBefore"] = (*githubv4.String)(nil)
145			ce.query.Node.IssueComment.UserContentEdits.PageInfo.HasNextPage = true
146			ce.query.Node.IssueComment.UserContentEdits.PageInfo.EndCursor = ""
147		}
148	}
149}
150
151// Error return last encountered error
152func (i *iterator) Error() error {
153	if i.err != nil {
154		return i.err
155	}
156	return i.ctx.Err() // might return nil
157}
158
159func (i *iterator) HasError() bool {
160	return i.err != nil || i.ctx.Err() != nil
161}
162
163func (i *iterator) currIssueItem() *issue {
164	return &i.issueIter.query.Repository.Issues.Nodes[i.issueIter.index]
165}
166
167func (i *iterator) currIssueEditIter() *issueEditIter {
168	return &i.issueIter.issueEditIter[i.issueIter.index]
169}
170
171func (i *iterator) currTimelineIter() *timelineIter {
172	return &i.issueIter.timelineIter[i.issueIter.index]
173}
174
175func (i *iterator) currCommentEditIter() *commentEditIter {
176	timelineIter := i.currTimelineIter()
177	return &timelineIter.commentEditIter[timelineIter.index]
178}
179
180func (i *iterator) currIssueGqlNodeId() githubv4.ID {
181	return i.currIssueItem().Id
182}
183
184// NextIssue returns true if there exists a next issue and advances the iterator by one.
185// It is used to iterate over all issues. Queries to github are made when necessary.
186func (i *iterator) NextIssue() bool {
187	if i.HasError() {
188		return false
189	}
190	index := &i.issueIter.index
191	issues := &i.issueIter.query.Repository.Issues
192	issueItems := &issues.Nodes
193	if 0 <= *index && *index < len(*issueItems)-1 {
194		*index += 1
195		return true
196	}
197
198	if !issues.PageInfo.HasNextPage {
199		return false
200	}
201	nextIssue := i.queryIssue()
202	return nextIssue
203}
204
205// IssueValue returns the actual issue value.
206func (i *iterator) IssueValue() issue {
207	return *i.currIssueItem()
208}
209
210func (i *iterator) queryIssue() bool {
211	ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
212	defer cancel()
213	if endCursor := i.issueIter.query.Repository.Issues.PageInfo.EndCursor; endCursor != "" {
214		i.issueIter.variables["issueAfter"] = endCursor
215	}
216	if err := i.gc.Query(ctx, &i.issueIter.query, i.issueIter.variables); err != nil {
217		i.err = err
218		return false
219	}
220	i.resetIssueEditVars()
221	i.resetTimelineVars()
222	issueItems := &i.issueIter.query.Repository.Issues.Nodes
223	if len(*issueItems) <= 0 {
224		i.issueIter.index = -1
225		return false
226	}
227	i.issueIter.index = 0
228	return true
229}
230
231// NextIssueEdit returns true if there exists a next issue edit and advances the iterator
232// by one. It is used to iterate over all the issue edits. Queries to github are made when
233// necessary.
234func (i *iterator) NextIssueEdit() bool {
235	if i.HasError() {
236		return false
237	}
238	ieIter := i.currIssueEditIter()
239	ieIdx := &ieIter.index
240	ieItems := ieIter.query.Node.Issue.UserContentEdits
241	if 0 <= *ieIdx && *ieIdx < len(ieItems.Nodes)-1 {
242		*ieIdx += 1
243		return i.nextValidIssueEdit()
244	}
245	if !ieItems.PageInfo.HasNextPage {
246		return false
247	}
248	querySucc := i.queryIssueEdit()
249	if !querySucc {
250		return false
251	}
252	return i.nextValidIssueEdit()
253}
254
255func (i *iterator) nextValidIssueEdit() bool {
256	// issueEdit.Diff == nil happen if the event is older than early 2018, Github doesn't have
257	// the data before that. Best we can do is to ignore the event.
258	if issueEdit := i.IssueEditValue(); issueEdit.Diff == nil || string(*issueEdit.Diff) == "" {
259		return i.NextIssueEdit()
260	}
261	return true
262}
263
264// IssueEditValue returns the actual issue edit value.
265func (i *iterator) IssueEditValue() userContentEdit {
266	iei := i.currIssueEditIter()
267	return iei.query.Node.Issue.UserContentEdits.Nodes[iei.index]
268}
269
270func (i *iterator) queryIssueEdit() bool {
271	ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
272	defer cancel()
273	iei := i.currIssueEditIter()
274	if endCursor := iei.query.Node.Issue.UserContentEdits.PageInfo.EndCursor; endCursor != "" {
275		iei.variables["issueEditBefore"] = endCursor
276	}
277	iei.variables["gqlNodeId"] = i.currIssueGqlNodeId()
278	if err := i.gc.Query(ctx, &iei.query, iei.variables); err != nil {
279		i.err = err
280		return false
281	}
282	issueEditItems := iei.query.Node.Issue.UserContentEdits.Nodes
283	if len(issueEditItems) <= 0 {
284		iei.index = -1
285		return false
286	}
287	// The UserContentEditConnection in the Github API serves its elements in reverse chronological
288	// order. For our purpose we have to reverse the edits.
289	reverseEdits(issueEditItems)
290	iei.index = 0
291	return true
292}
293
294// NextTimelineItem returns true if there exists a next timeline item and advances the iterator
295// by one. It is used to iterate over all the timeline items. Queries to github are made when
296// necessary.
297func (i *iterator) NextTimelineItem() bool {
298	if i.HasError() {
299		return false
300	}
301	tlIter := &i.issueIter.timelineIter[i.issueIter.index]
302	tlIdx := &tlIter.index
303	tlItems := tlIter.query.Node.Issue.TimelineItems
304	if 0 <= *tlIdx && *tlIdx < len(tlItems.Nodes)-1 {
305		*tlIdx += 1
306		return true
307	}
308	if !tlItems.PageInfo.HasNextPage {
309		return false
310	}
311	nextTlItem := i.queryTimeline()
312	return nextTlItem
313}
314
315// TimelineItemValue returns the actual timeline item value.
316func (i *iterator) TimelineItemValue() timelineItem {
317	tli := i.currTimelineIter()
318	return tli.query.Node.Issue.TimelineItems.Nodes[tli.index]
319}
320
321func (i *iterator) queryTimeline() bool {
322	ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
323	defer cancel()
324	tli := i.currTimelineIter()
325	if endCursor := tli.query.Node.Issue.TimelineItems.PageInfo.EndCursor; endCursor != "" {
326		tli.variables["timelineAfter"] = endCursor
327	}
328	tli.variables["gqlNodeId"] = i.currIssueGqlNodeId()
329	if err := i.gc.Query(ctx, &tli.query, tli.variables); err != nil {
330		i.err = err
331		return false
332	}
333	i.resetCommentEditVars()
334	timelineItems := &tli.query.Node.Issue.TimelineItems
335	if len(timelineItems.Nodes) <= 0 {
336		tli.index = -1
337		return false
338	}
339	tli.index = 0
340	return true
341}
342
343// NextCommentEdit returns true if there exists a next comment edit and advances the iterator
344// by one. It is used to iterate over all issue edits. Queries to github are made when
345// necessary.
346func (i *iterator) NextCommentEdit() bool {
347	if i.HasError() {
348		return false
349	}
350
351	tmlnVal := i.TimelineItemValue()
352	if tmlnVal.Typename != "IssueComment" {
353		// The timeline iterator does not point to a comment.
354		i.err = errors.New("Call to NextCommentEdit() while timeline item is not a comment")
355		return false
356	}
357
358	cei := i.currCommentEditIter()
359	ceIdx := &cei.index
360	ceItems := &cei.query.Node.IssueComment.UserContentEdits
361	if 0 <= *ceIdx && *ceIdx < len(ceItems.Nodes)-1 {
362		*ceIdx += 1
363		return i.nextValidCommentEdit()
364	}
365	if !ceItems.PageInfo.HasNextPage {
366		return false
367	}
368	querySucc := i.queryCommentEdit()
369	if !querySucc {
370		return false
371	}
372	return i.nextValidCommentEdit()
373}
374
375func (i *iterator) nextValidCommentEdit() bool {
376	// if comment edit diff is a nil pointer or points to an empty string look for next value
377	if commentEdit := i.CommentEditValue(); commentEdit.Diff == nil || string(*commentEdit.Diff) == "" {
378		return i.NextCommentEdit()
379	}
380	return true
381}
382
383// CommentEditValue returns the actual comment edit value.
384func (i *iterator) CommentEditValue() userContentEdit {
385	cei := i.currCommentEditIter()
386	return cei.query.Node.IssueComment.UserContentEdits.Nodes[cei.index]
387}
388
389func (i *iterator) queryCommentEdit() bool {
390	ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
391	defer cancel()
392	cei := i.currCommentEditIter()
393
394	if endCursor := cei.query.Node.IssueComment.UserContentEdits.PageInfo.EndCursor; endCursor != "" {
395		cei.variables["commentEditBefore"] = endCursor
396	}
397	tmlnVal := i.TimelineItemValue()
398	if tmlnVal.Typename != "IssueComment" {
399		i.err = errors.New("Call to queryCommentEdit() while timeline item is not a comment")
400		return false
401	}
402	cei.variables["gqlNodeId"] = tmlnVal.IssueComment.Id
403	if err := i.gc.Query(ctx, &cei.query, cei.variables); err != nil {
404		i.err = err
405		return false
406	}
407	ceItems := cei.query.Node.IssueComment.UserContentEdits.Nodes
408	if len(ceItems) <= 0 {
409		cei.index = -1
410		return false
411	}
412	// The UserContentEditConnection in the Github API serves its elements in reverse chronological
413	// order. For our purpose we have to reverse the edits.
414	reverseEdits(ceItems)
415	cei.index = 0
416	return true
417}
418
419func reverseEdits(edits []userContentEdit) {
420	for i, j := 0, len(edits)-1; i < j; i, j = i+1, j-1 {
421		edits[i], edits[j] = edits[j], edits[i]
422	}
423}