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