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