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}