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