1package github
2
3import (
4 "context"
5 "time"
6
7 "github.com/MichaelMure/git-bug/bridge/core"
8 "github.com/shurcooL/githubv4"
9)
10
11/**
12type iterator interface {
13 Count() int
14 Error() error
15
16 NextIssue() bool
17 NextIssueEdit() bool
18 NextTimeline() bool
19 NextCommentEdit() bool
20
21 IssueValue() issueTimeline
22 IssueEditValue() userContentEdit
23 TimelineValue() timelineItem
24 CommentEditValue() userContentEdit
25}
26*/
27
28type indexer struct{ index int }
29
30type issueEditIterator struct {
31 index int
32 query issueEditQuery
33 variables map[string]interface{}
34}
35
36type commentEditIterator struct {
37 index int
38 query commentEditQuery
39 variables map[string]interface{}
40}
41
42type timelineIterator struct {
43 index int
44 query issueTimelineQuery
45 variables map[string]interface{}
46
47 issueEdit indexer
48 commentEdit indexer
49
50 lastEndCursor githubv4.String // storing timeline end cursor for future use
51}
52
53type iterator struct {
54 // github graphql client
55 gc *githubv4.Client
56
57 // if since is given the iterator will query only the updated
58 // and created issues after this date
59 since time.Time
60
61 // number of timelines/userEditcontent/issueEdit to query
62 // at a time more capacity = more used memory = less queries
63 // to make
64 capacity int
65
66 // sticky error
67 err error
68
69 // count to keep track of the number of imported issues
70 count int
71
72 // timeline iterator
73 timeline timelineIterator
74
75 // issue edit iterator
76 issueEdit issueEditIterator
77
78 // comment edit iterator
79 commentEdit commentEditIterator
80}
81
82func newIterator(conf core.Configuration, since time.Time) *iterator {
83 return &iterator{
84 since: since,
85 gc: buildClient(conf),
86 capacity: 10,
87 count: 0,
88 timeline: timelineIterator{
89 index: -1,
90 issueEdit: indexer{-1},
91 commentEdit: indexer{-1},
92 variables: map[string]interface{}{
93 "owner": githubv4.String(conf["user"]),
94 "name": githubv4.String(conf["project"]),
95 },
96 },
97 commentEdit: commentEditIterator{
98 index: -1,
99 variables: map[string]interface{}{
100 "owner": githubv4.String(conf["user"]),
101 "name": githubv4.String(conf["project"]),
102 },
103 },
104 issueEdit: issueEditIterator{
105 index: -1,
106 variables: map[string]interface{}{
107 "owner": githubv4.String(conf["user"]),
108 "name": githubv4.String(conf["project"]),
109 },
110 },
111 }
112}
113
114// init issue timeline variables
115func (i *iterator) initTimelineQueryVariables() {
116 i.timeline.variables["issueFirst"] = githubv4.Int(1)
117 i.timeline.variables["issueAfter"] = (*githubv4.String)(nil)
118 i.timeline.variables["issueSince"] = githubv4.DateTime{Time: i.since}
119 i.timeline.variables["timelineFirst"] = githubv4.Int(i.capacity)
120 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
121 i.timeline.variables["issueEditLast"] = githubv4.Int(i.capacity)
122 i.timeline.variables["issueEditBefore"] = (*githubv4.String)(nil)
123 i.timeline.variables["commentEditLast"] = githubv4.Int(i.capacity)
124 i.timeline.variables["commentEditBefore"] = (*githubv4.String)(nil)
125}
126
127// init issue edit variables
128func (i *iterator) initIssueEditQueryVariables() {
129 i.issueEdit.variables["issueFirst"] = githubv4.Int(1)
130 i.issueEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
131 i.issueEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
132 i.issueEdit.variables["issueEditLast"] = githubv4.Int(i.capacity)
133 i.issueEdit.variables["issueEditBefore"] = (*githubv4.String)(nil)
134}
135
136// init issue comment variables
137func (i *iterator) initCommentEditQueryVariables() {
138 i.commentEdit.variables["issueFirst"] = githubv4.Int(1)
139 i.commentEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"]
140 i.commentEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since}
141 i.commentEdit.variables["timelineFirst"] = githubv4.Int(1)
142 i.commentEdit.variables["timelineAfter"] = (*githubv4.String)(nil)
143 i.commentEdit.variables["commentEditLast"] = githubv4.Int(i.capacity)
144 i.commentEdit.variables["commentEditBefore"] = (*githubv4.String)(nil)
145}
146
147// reverse UserContentEdits arrays in both of the issue and
148// comment timelines
149func (i *iterator) reverseTimelineEditNodes() {
150 reverseEdits(i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes)
151 for index, ce := range i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges {
152 if ce.Node.Typename == "IssueComment" && len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges) != 0 {
153 reverseEdits(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[index].Node.IssueComment.UserContentEdits.Nodes)
154 }
155 }
156}
157
158// Error return last encountered error
159func (i *iterator) Error() error {
160 return i.err
161}
162
163// Count return number of issues we iterated over
164func (i *iterator) Count() int {
165 return i.count
166}
167
168// Next issue
169func (i *iterator) NextIssue() bool {
170 // we make the first move
171 if i.count == 0 {
172
173 // init variables and goto queryIssue block
174 i.initTimelineQueryVariables()
175 goto queryIssue
176 }
177
178 if i.err != nil {
179 return false
180 }
181
182 if !i.timeline.query.Repository.Issues.PageInfo.HasNextPage {
183 return false
184 }
185
186 // if we have more issues, query them
187 i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil)
188 i.timeline.variables["issueAfter"] = i.timeline.query.Repository.Issues.PageInfo.EndCursor
189 i.timeline.index = -1
190
191 // store cursor for future use
192 i.timeline.lastEndCursor = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
193
194 // query issue block
195queryIssue:
196 if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil {
197 i.err = err
198 return false
199 }
200
201 if len(i.timeline.query.Repository.Issues.Nodes) == 0 {
202 return false
203 }
204
205 i.reverseTimelineEditNodes()
206 i.count++
207 return true
208}
209
210func (i *iterator) IssueValue() issueTimeline {
211 return i.timeline.query.Repository.Issues.Nodes[0]
212}
213
214func (i *iterator) NextTimeline() bool {
215 if i.err != nil {
216 return false
217 }
218
219 if len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges) == 0 {
220 return false
221 }
222
223 if i.timeline.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges))-1 {
224 i.timeline.index++
225 return true
226 }
227
228 if !i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.HasNextPage {
229 return false
230 }
231
232 i.timeline.lastEndCursor = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
233
234 // more timelines, query them
235 i.timeline.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor
236 if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil {
237 i.err = err
238 return false
239 }
240
241 i.reverseTimelineEditNodes()
242 i.timeline.index = 0
243 return true
244}
245
246func (i *iterator) TimelineValue() timelineItem {
247 return i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node
248}
249
250func (i *iterator) NextIssueEdit() bool {
251 if i.err != nil {
252 return false
253 }
254
255 // this mean we looped over all available issue edits in the timeline.
256 // now we have to use i.issueEditQuery
257 if i.timeline.issueEdit.index == -2 {
258 if i.issueEdit.index < min(i.capacity, len(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes))-1 {
259 i.issueEdit.index++
260 return true
261 }
262
263 if !i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.HasPreviousPage {
264 i.timeline.issueEdit.index = -1
265 i.issueEdit.index = -1
266 return false
267 }
268
269 // if there is more edits, query them
270 i.issueEdit.variables["issueEditBefore"] = i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.StartCursor
271 goto queryIssueEdit
272 }
273
274 // if there is no edits
275 if len(i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes) == 0 {
276 return false
277 }
278
279 // loop over them timeline comment edits
280 if i.timeline.issueEdit.index < min(i.capacity, len(i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes))-1 {
281 i.timeline.issueEdit.index++
282 return true
283 }
284
285 if !i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.HasPreviousPage {
286 i.timeline.issueEdit.index = -1
287 return false
288 }
289
290 // if there is more edits, query them
291 i.initIssueEditQueryVariables()
292 i.issueEdit.variables["issueEditBefore"] = i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.PageInfo.StartCursor
293
294queryIssueEdit:
295 if err := i.gc.Query(context.TODO(), &i.issueEdit.query, i.issueEdit.variables); err != nil {
296 i.err = err
297 //i.timeline.issueEdit.index = -1
298 return false
299 }
300
301 // reverse issue edits because github
302 reverseEdits(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes)
303
304 // this is not supposed to happen
305 if len(i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes) == 0 {
306 i.timeline.issueEdit.index = -1
307 return false
308 }
309
310 i.issueEdit.index = 0
311 i.timeline.issueEdit.index = -2
312 return true
313}
314
315func (i *iterator) IssueEditValue() userContentEdit {
316 // if we are using issue edit query
317 if i.timeline.issueEdit.index == -2 {
318 return i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes[i.issueEdit.index]
319 }
320
321 // else get it from timeline issue edit query
322 return i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits.Nodes[i.timeline.issueEdit.index]
323}
324
325func (i *iterator) NextCommentEdit() bool {
326 if i.err != nil {
327 return false
328 }
329
330 // same as NextIssueEdit
331 if i.timeline.commentEdit.index == -2 {
332
333 if i.commentEdit.index < min(i.capacity, len(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes))-1 {
334 i.commentEdit.index++
335 return true
336 }
337
338 if !i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.PageInfo.HasPreviousPage {
339 i.timeline.commentEdit.index = -1
340 i.commentEdit.index = -1
341 return false
342 }
343
344 // if there is more comment edits, query them
345 i.commentEdit.variables["commentEditBefore"] = i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.PageInfo.StartCursor
346 goto queryCommentEdit
347 }
348
349 // if there is no comment edits
350 if len(i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes) == 0 {
351 return false
352 }
353
354 // loop over them timeline comment edits
355 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 {
356 i.timeline.commentEdit.index++
357 return true
358 }
359
360 if !i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.PageInfo.HasPreviousPage {
361 i.timeline.commentEdit.index = -1
362 return false
363 }
364
365 i.initCommentEditQueryVariables()
366 if i.timeline.index == 0 {
367 i.commentEdit.variables["timelineAfter"] = i.timeline.lastEndCursor
368 } else {
369 i.commentEdit.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index-1].Cursor
370 }
371
372 i.commentEdit.variables["commentEditBefore"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.PageInfo.StartCursor
373
374queryCommentEdit:
375 if err := i.gc.Query(context.TODO(), &i.commentEdit.query, i.commentEdit.variables); err != nil {
376 i.err = err
377 return false
378 }
379
380 // this is not supposed to happen
381 if len(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes) == 0 {
382 i.timeline.commentEdit.index = -1
383 return false
384 }
385
386 reverseEdits(i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes)
387
388 i.commentEdit.index = 0
389 i.timeline.commentEdit.index = -2
390 return true
391}
392
393func (i *iterator) CommentEditValue() userContentEdit {
394 if i.timeline.commentEdit.index == -2 {
395 return i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes[i.commentEdit.index]
396 }
397
398 return i.timeline.query.Repository.Issues.Nodes[0].Timeline.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes[i.timeline.commentEdit.index]
399}
400
401func min(a, b int) int {
402 if a > b {
403 return b
404 }
405
406 return a
407}