1package github
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "github.com/shurcooL/githubv4"
9)
10
11type indexer struct{ index int }
12
13type issueEditIterator struct {
14 index int
15 query issueEditQuery
16 variables map[string]interface{}
17}
18
19type commentEditIterator struct {
20 index int
21 query commentEditQuery
22 variables map[string]interface{}
23}
24
25type timelineIterator struct {
26 index int
27 query issueTimelineQuery
28 variables map[string]interface{}
29
30 issueEdit indexer
31 commentEdit indexer
32
33 // Alex: It would be really help clearity to get rid of this variable.
34 // lastEndCursor cache the timeline end cursor for one iteration
35 lastEndCursor githubv4.String
36}
37
38type iterator struct {
39 // github graphql client
40 gc *githubv4.Client
41
42 // if since is given the iterator will query only the updated
43 // and created issues after this date
44 since time.Time
45
46 // number of timelines/userEditcontent/issueEdit to query
47 // at a time, more capacity = more used memory = less queries
48 // to make
49 capacity int
50
51 // shared context used for all graphql queries
52 ctx context.Context
53
54 // sticky error
55 err error
56
57 // timeline iterator
58 timeline timelineIterator
59
60 // issue edit iterator
61 issueEdit issueEditIterator
62
63 // comment edit iterator
64 commentEdit commentEditIterator
65}
66
67// NewIterator create and initialize a new iterator
68func NewIterator(ctx context.Context, client *githubv4.Client, capacity int, owner, project string, since time.Time) *iterator {
69 i := &iterator{
70 gc: client,
71 since: since,
72 capacity: capacity,
73 ctx: ctx,
74 timeline: timelineIterator{
75 index: -1,
76 issueEdit: indexer{-1},
77 commentEdit: indexer{-1},
78 variables: map[string]interface{}{
79 "owner": githubv4.String(owner),
80 "name": githubv4.String(project),
81 },
82 },
83 commentEdit: commentEditIterator{
84 index: -1,
85 variables: map[string]interface{}{
86 "owner": githubv4.String(owner),
87 "name": githubv4.String(project),
88 },
89 },
90 issueEdit: issueEditIterator{
91 index: -1,
92 variables: map[string]interface{}{
93 "owner": githubv4.String(owner),
94 "name": githubv4.String(project),
95 },
96 },
97 }
98
99 i.initTimelineQueryVariables()
100 return i
101}
102
103// init issue timeline variables
104func (i *iterator) initTimelineQueryVariables() {
105 i.timeline.variables["issueFirst"] = githubv4.Int(1) // each query one single issue only
106 i.timeline.variables["issueAfter"] = (*githubv4.String)(nil)
107 i.timeline.variables["issueSince"] = githubv4.DateTime{Time: i.since}
108 i.timeline.variables["timelineFirst"] = githubv4.Int(i.capacity)
109 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
110 // Fun fact, github provide the comment edition in reverse chronological
111 // order, because haha. Look at me, I'm dying of laughter.
112 i.timeline.variables["issueEditLast"] = githubv4.Int(i.capacity)
113 i.timeline.variables["issueEditBefore"] = (*githubv4.String)(nil)
114 i.timeline.variables["commentEditLast"] = githubv4.Int(i.capacity)
115 i.timeline.variables["commentEditBefore"] = (*githubv4.String)(nil)
116}
117
118// init issue edit variables
119func (i *iterator) initIssueEditQueryVariables() {
120 i.issueEdit.variables["issueFirst"] = githubv4.Int(1)
121 i.issueEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
122 i.issueEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
123 i.issueEdit.variables["issueEditLast"] = githubv4.Int(i.capacity)
124 i.issueEdit.variables["issueEditBefore"] = (*githubv4.String)(nil)
125}
126
127// init issue comment variables
128func (i *iterator) initCommentEditQueryVariables() {
129 i.commentEdit.variables["issueFirst"] = githubv4.Int(1)
130 i.commentEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
131 i.commentEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
132 i.commentEdit.variables["timelineFirst"] = githubv4.Int(1)
133 i.commentEdit.variables["timelineAfter"] = (*githubv4.String)(nil)
134 i.commentEdit.variables["commentEditLast"] = githubv4.Int(i.capacity)
135 i.commentEdit.variables["commentEditBefore"] = (*githubv4.String)(nil)
136}
137
138// reverse UserContentEdits arrays in both of the issue and
139// comment timelines
140func (i *iterator) reverseTimelineEditNodes() {
141 node := i.timeline.query.Repository.Issues.Nodes[0]
142 reverseEdits(node.UserContentEdits.Nodes)
143 for index, ce := range node.TimelineItems.Edges {
144 if ce.Node.Typename == "IssueComment" && len(node.TimelineItems.Edges) != 0 {
145 reverseEdits(node.TimelineItems.Edges[index].Node.IssueComment.UserContentEdits.Nodes)
146 }
147 }
148}
149
150// Error return last encountered error
151func (i *iterator) Error() error {
152 return i.err
153}
154
155func (i *iterator) queryIssue() bool {
156 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
157 defer cancel()
158
159 if err := i.gc.Query(ctx, &i.timeline.query, i.timeline.variables); err != nil {
160 i.err = err
161 return false
162 }
163
164 issues := i.timeline.query.Repository.Issues.Nodes
165 if len(issues) == 0 {
166 return false
167 }
168
169 i.reverseTimelineEditNodes()
170 return true
171}
172
173// NextIssue try to query the next issue and return true. Only one issue is
174// queried at each call.
175func (i *iterator) NextIssue() bool {
176 if i.err != nil {
177 return false
178 }
179
180 if i.ctx.Err() != nil {
181 return false
182 }
183
184 // if $issueAfter variable is nil we can directly make the first query
185 if i.timeline.variables["issueAfter"] == (*githubv4.String)(nil) {
186 nextIssue := i.queryIssue()
187 // prevent from infinite loop by setting a non nil cursor
188 issues := i.timeline.query.Repository.Issues
189 i.timeline.variables["issueAfter"] = issues.PageInfo.EndCursor
190 return nextIssue
191 }
192
193 issues := i.timeline.query.Repository.Issues
194 if !issues.PageInfo.HasNextPage {
195 return false
196 }
197
198 // if we have more issues, query them
199 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
200 i.timeline.index = -1
201
202 timelineEndCursor := issues.Nodes[0].TimelineItems.PageInfo.EndCursor
203 // store cursor for future use
204 i.timeline.lastEndCursor = timelineEndCursor
205
206 // query issue block
207 nextIssue := i.queryIssue()
208 i.timeline.variables["issueAfter"] = issues.PageInfo.EndCursor
209
210 return nextIssue
211}
212
213// IssueValue return the actual issue value
214func (i *iterator) IssueValue() issueTimeline {
215 issues := i.timeline.query.Repository.Issues
216 return issues.Nodes[0]
217}
218
219// NextTimelineItem return true if there is a next timeline item and increments the index by one.
220// It is used iterates over all the timeline items. Extra queries are made if it is necessary.
221func (i *iterator) NextTimelineItem() bool {
222 if i.err != nil {
223 return false
224 }
225
226 if i.ctx.Err() != nil {
227 return false
228 }
229
230 timelineItems := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems
231 // after NextIssue call it's good to check wether we have some timelineItems items or not
232 // Alex: Correct?
233 if len(timelineItems.Edges) == 0 {
234 return false
235 }
236
237 if i.timeline.index < len(timelineItems.Edges)-1 {
238 i.timeline.index++
239 return true
240 }
241
242 if !timelineItems.PageInfo.HasNextPage {
243 return false
244 }
245
246 i.timeline.lastEndCursor = timelineItems.PageInfo.EndCursor
247
248 // more timelines, query them
249 i.timeline.variables["timelineAfter"] = timelineItems.PageInfo.EndCursor
250 // HACK
251 var query timelineItemsQuery
252 // var variables map[string]interface{}
253 variables := make(map[string]interface{})
254 variables["owner"] = i.timeline.variables["owner"]
255 variables["name"] = i.timeline.variables["name"]
256 variables["issueNumber"] = i.timeline.query.Repository.Issues.Nodes[0].Number
257 fmt.Println("### Alex using issue number ", i.timeline.query.Repository.Issues.Nodes[0].Number)
258 variables["timelineFirst"] = i.timeline.variables["timelineFirst"]
259 variables["timelineAfter"] = i.timeline.variables["timelineAfter"]
260 variables["commentEditLast"] = i.timeline.variables["commentEditLast"]
261 variables["commentEditBefore"] = i.timeline.variables["commentEditBefore"]
262
263 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
264 defer cancel()
265
266 // if err := i.gc.Query(ctx, &i.timeline.query, i.timeline.variables); err != nil {
267 if err := i.gc.Query(ctx, &query, variables); err != nil {
268 i.err = err
269 return false
270 }
271 // HACK
272 fmt.Println("### Alex after the query")
273 i.timeline.variables["timelineFirst"] = variables["timelineFirst"]
274 i.timeline.variables["timelineAfter"] = variables["timelineAfter"]
275 i.timeline.variables["commentEditLast"] = variables["commentEditLast"]
276 i.timeline.variables["commentEditBefore"] = variables["commentEditBefore"]
277 i.timeline.query.Repository.Issues.Nodes[0].TimelineItems = query.Repository.Issue.TimelineItems
278
279 timelineItems = i.timeline.query.Repository.Issues.Nodes[0].TimelineItems
280 // (in case github returns something weird) just for safety: better return a false than a panic
281 if len(timelineItems.Edges) == 0 {
282 return false
283 }
284
285 i.reverseTimelineEditNodes()
286 i.timeline.index = 0
287 return true
288}
289
290// TimelineItemValue return the actual timeline item value
291func (i *iterator) TimelineItemValue() timelineItem {
292 timelineItems := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems
293 return timelineItems.Edges[i.timeline.index].Node
294}
295
296func (i *iterator) queryIssueEdit() bool {
297 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
298 defer cancel()
299
300 if err := i.gc.Query(ctx, &i.issueEdit.query, i.issueEdit.variables); err != nil {
301 i.err = err
302 //i.timeline.issueEdit.index = -1
303 return false
304 }
305
306 issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits
307 // reverse issue edits because github
308 reverseEdits(issueEdits.Nodes)
309
310 // this is not supposed to happen
311 if len(issueEdits.Nodes) == 0 {
312 i.timeline.issueEdit.index = -1
313 return false
314 }
315
316 i.issueEdit.index = 0
317 i.timeline.issueEdit.index = -2
318 return i.nextValidIssueEdit()
319}
320
321func (i *iterator) nextValidIssueEdit() bool {
322 // issueEdit.Diff == nil happen if the event is older than early 2018, Github doesn't have the data before that.
323 // Best we can do is to ignore the event.
324 if issueEdit := i.IssueEditValue(); issueEdit.Diff == nil || string(*issueEdit.Diff) == "" {
325 return i.NextIssueEdit()
326 }
327 return true
328}
329
330// NextIssueEdit return true if there is a next issue edit and increments the index by one.
331// It is used iterates over all the issue edits. Extra queries are made if it is necessary.
332func (i *iterator) NextIssueEdit() bool {
333 if i.err != nil {
334 return false
335 }
336
337 if i.ctx.Err() != nil {
338 return false
339 }
340
341 // this mean we looped over all available issue edits in the timeline.
342 // now we have to use i.issueEditQuery
343 if i.timeline.issueEdit.index == -2 {
344 issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits
345 if i.issueEdit.index < len(issueEdits.Nodes)-1 {
346 i.issueEdit.index++
347 return i.nextValidIssueEdit()
348 }
349
350 if !issueEdits.PageInfo.HasPreviousPage {
351 i.timeline.issueEdit.index = -1
352 i.issueEdit.index = -1
353 return false
354 }
355
356 // if there is more edits, query them
357 i.issueEdit.variables["issueEditBefore"] = issueEdits.PageInfo.StartCursor
358 return i.queryIssueEdit()
359 }
360
361 issueEdits := i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits
362 // if there is no edit, the UserContentEdits given by github is empty. That
363 // means that the original message is given by the issue message.
364 //
365 // if there is edits, the UserContentEdits given by github contains both the
366 // original message and the following edits. The issue message give the last
367 // version so we don't care about that.
368 //
369 // the tricky part: for an issue older than the UserContentEdits API, github
370 // doesn't have the previous message version anymore and give an edition
371 // with .Diff == nil. We have to filter them.
372 if len(issueEdits.Nodes) == 0 {
373 return false
374 }
375
376 // loop over them timeline comment edits
377 if i.timeline.issueEdit.index < len(issueEdits.Nodes)-1 {
378 i.timeline.issueEdit.index++
379 return i.nextValidIssueEdit()
380 }
381
382 if !issueEdits.PageInfo.HasPreviousPage {
383 i.timeline.issueEdit.index = -1
384 return false
385 }
386
387 // if there is more edits, query them
388 i.initIssueEditQueryVariables()
389 i.issueEdit.variables["issueEditBefore"] = issueEdits.PageInfo.StartCursor
390 return i.queryIssueEdit()
391}
392
393// IssueEditValue return the actual issue edit value
394func (i *iterator) IssueEditValue() userContentEdit {
395 // if we are using issue edit query
396 if i.timeline.issueEdit.index == -2 {
397 issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits
398 return issueEdits.Nodes[i.issueEdit.index]
399 }
400
401 issueEdits := i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits
402 // else get it from timeline issue edit query
403 return issueEdits.Nodes[i.timeline.issueEdit.index]
404}
405
406func (i *iterator) queryCommentEdit() bool {
407 ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
408 defer cancel()
409
410 if err := i.gc.Query(ctx, &i.commentEdit.query, i.commentEdit.variables); err != nil {
411 i.err = err
412 return false
413 }
414
415 commentEdits := i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits
416 // this is not supposed to happen
417 if len(commentEdits.Nodes) == 0 {
418 i.timeline.commentEdit.index = -1
419 return false
420 }
421
422 reverseEdits(commentEdits.Nodes)
423
424 i.commentEdit.index = 0
425 i.timeline.commentEdit.index = -2
426 return i.nextValidCommentEdit()
427}
428
429func (i *iterator) nextValidCommentEdit() bool {
430 // if comment edit diff is a nil pointer or points to an empty string look for next value
431 if commentEdit := i.CommentEditValue(); commentEdit.Diff == nil || string(*commentEdit.Diff) == "" {
432 return i.NextCommentEdit()
433 }
434 return true
435}
436
437// NextCommentEdit return true if there is a next comment edit and increments the index by one.
438// It is used iterates over all the comment edits. Extra queries are made if it is necessary.
439func (i *iterator) NextCommentEdit() bool {
440 if i.err != nil {
441 return false
442 }
443
444 if i.ctx.Err() != nil {
445 return false
446 }
447
448 // same as NextIssueEdit
449 if i.timeline.commentEdit.index == -2 {
450 commentEdits := i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits
451 if i.commentEdit.index < len(commentEdits.Nodes)-1 {
452 i.commentEdit.index++
453 return i.nextValidCommentEdit()
454 }
455
456 if !commentEdits.PageInfo.HasPreviousPage {
457 i.timeline.commentEdit.index = -1
458 i.commentEdit.index = -1
459 return false
460 }
461
462 // if there is more comment edits, query them
463 i.commentEdit.variables["commentEditBefore"] = commentEdits.PageInfo.StartCursor
464 return i.queryCommentEdit()
465 }
466
467 commentEdits := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index].Node.IssueComment
468 // if there is no comment edits
469 if len(commentEdits.UserContentEdits.Nodes) == 0 {
470 return false
471 }
472
473 // loop over them timeline comment edits
474 if i.timeline.commentEdit.index < len(commentEdits.UserContentEdits.Nodes)-1 {
475 i.timeline.commentEdit.index++
476 return i.nextValidCommentEdit()
477 }
478
479 if !commentEdits.UserContentEdits.PageInfo.HasPreviousPage {
480 i.timeline.commentEdit.index = -1
481 return false
482 }
483
484 i.initCommentEditQueryVariables()
485 if i.timeline.index == 0 {
486 i.commentEdit.variables["timelineAfter"] = i.timeline.lastEndCursor
487 } else {
488 i.commentEdit.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index-1].Cursor
489 }
490
491 i.commentEdit.variables["commentEditBefore"] = commentEdits.UserContentEdits.PageInfo.StartCursor
492
493 return i.queryCommentEdit()
494}
495
496// CommentEditValue return the actual comment edit value
497func (i *iterator) CommentEditValue() userContentEdit {
498 if i.timeline.commentEdit.index == -2 {
499 return i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes[i.commentEdit.index]
500 }
501
502 return i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes[i.timeline.commentEdit.index]
503}
504
505func reverseEdits(edits []userContentEdit) {
506 for i, j := 0, len(edits)-1; i < j; i, j = i+1, j-1 {
507 edits[i], edits[j] = edits[j], edits[i]
508 }
509}