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}