diff options
Diffstat (limited to 'bridge/github')
-rw-r--r-- | bridge/github/import.go | 2 | ||||
-rw-r--r-- | bridge/github/import_query.go | 129 | ||||
-rw-r--r-- | bridge/github/iterator.go | 633 |
3 files changed, 345 insertions, 419 deletions
diff --git a/bridge/github/import.go b/bridge/github/import.go index 78e93436..e8a4d3cb 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -100,7 +100,7 @@ func (gi *githubImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, return out, nil } -func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline) (*cache.BugCache, error) { +func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issue) (*cache.BugCache, error) { // ensure issue author author, err := gi.ensurePerson(repo, issue.Author) if err != nil { diff --git a/bridge/github/import_query.go b/bridge/github/import_query.go index 58f6d95e..228d204a 100644 --- a/bridge/github/import_query.go +++ b/bridge/github/import_query.go @@ -47,14 +47,9 @@ type userContentEdit struct { } type issueComment struct { - authorEvent - Body githubv4.String - Url githubv4.URI - - UserContentEdits struct { - Nodes []userContentEdit - PageInfo pageInfo - } `graphql:"userContentEdits(last: $commentEditLast, before: $commentEditBefore)"` + authorEvent // NOTE: contains Id + Body githubv4.String + Url githubv4.URI } type timelineItem struct { @@ -96,70 +91,6 @@ type timelineItem struct { } `graphql:"... on RenamedTitleEvent"` } -type issueTimeline struct { - authorEvent - Title string - Body githubv4.String - Url githubv4.URI - - TimelineItems struct { - Edges []struct { - Cursor githubv4.String - Node timelineItem - } - PageInfo pageInfo - } `graphql:"timelineItems(first: $timelineFirst, after: $timelineAfter)"` - - UserContentEdits struct { - Nodes []userContentEdit - PageInfo pageInfo - } `graphql:"userContentEdits(last: $issueEditLast, before: $issueEditBefore)"` -} - -type issueEdit struct { - UserContentEdits struct { - Nodes []userContentEdit - PageInfo pageInfo - } `graphql:"userContentEdits(last: $issueEditLast, before: $issueEditBefore)"` -} - -type issueTimelineQuery struct { - Repository struct { - Issues struct { - Nodes []issueTimeline - PageInfo pageInfo - } `graphql:"issues(first: $issueFirst, after: $issueAfter, orderBy: {field: CREATED_AT, direction: ASC}, filterBy: {since: $issueSince})"` - } `graphql:"repository(owner: $owner, name: $name)"` -} - -type issueEditQuery struct { - Repository struct { - Issues struct { - Nodes []issueEdit - PageInfo pageInfo - } `graphql:"issues(first: $issueFirst, after: $issueAfter, orderBy: {field: CREATED_AT, direction: ASC}, filterBy: {since: $issueSince})"` - } `graphql:"repository(owner: $owner, name: $name)"` -} - -type commentEditQuery struct { - Repository struct { - Issues struct { - Nodes []struct { - Timeline struct { - Nodes []struct { - IssueComment struct { - UserContentEdits struct { - Nodes []userContentEdit - PageInfo pageInfo - } `graphql:"userContentEdits(last: $commentEditLast, before: $commentEditBefore)"` - } `graphql:"... on IssueComment"` - } - } `graphql:"timeline(first: $timelineFirst, after: $timelineAfter)"` - } - } `graphql:"issues(first: $issueFirst, after: $issueAfter, orderBy: {field: CREATED_AT, direction: ASC}, filterBy: {since: $issueSince})"` - } `graphql:"repository(owner: $owner, name: $name)"` -} - type ghostQuery struct { User struct { Login githubv4.String @@ -187,3 +118,57 @@ type loginQuery struct { Login string `graphql:"login"` } `graphql:"viewer"` } + +type issueQuery struct { + Repository struct { + Issues struct { + Nodes []issue + PageInfo pageInfo + } `graphql:"issues(first: $issueFirst, after: $issueAfter, orderBy: {field: CREATED_AT, direction: ASC}, filterBy: {since: $issueSince})"` + } `graphql:"repository(owner: $owner, name: $name)"` +} + +type issue struct { + authorEvent + Title string + Number githubv4.Int + Body githubv4.String + Url githubv4.URI +} + +type issueEditQuery struct { + Node struct { + Typename githubv4.String `graphql:"__typename"` + Issue struct { + UserContentEdits struct { + Nodes []userContentEdit + TotalCount githubv4.Int + PageInfo pageInfo + } `graphql:"userContentEdits(last: $issueEditLast, before: $issueEditBefore)"` + } `graphql:"... on Issue"` + } `graphql:"node(id: $gqlNodeId)"` +} + +type timelineQuery struct { + Node struct { + Typename githubv4.String `graphql:"__typename"` + Issue struct { + TimelineItems struct { + Nodes []timelineItem + PageInfo pageInfo + } `graphql:"timelineItems(first: $timelineFirst, after: $timelineAfter)"` + } `graphql:"... on Issue"` + } `graphql:"node(id: $gqlNodeId)"` +} + +type commentEditQuery struct { + Node struct { + Typename githubv4.String `graphql:"__typename"` + IssueComment struct { + UserContentEdits struct { + Nodes []userContentEdit + PageInfo pageInfo + } `graphql:"userContentEdits(last: $commentEditLast, before: $commentEditBefore)"` + } `graphql:"... on IssueComment"` + } `graphql:"node(id: $gqlNodeId)"` +} diff --git a/bridge/github/iterator.go b/bridge/github/iterator.go index 40b00292..d21faae8 100644 --- a/bridge/github/iterator.go +++ b/bridge/github/iterator.go @@ -4,398 +4,371 @@ import ( "context" "time" + "github.com/pkg/errors" "github.com/shurcooL/githubv4" ) -type indexer struct{ index int } +type iterator struct { + // Github graphql client + gc *githubv4.Client -type issueEditIterator struct { - index int - query issueEditQuery - variables map[string]interface{} -} + // The iterator will only query issues updated or created after the date given in + // the variable since. + since time.Time -type commentEditIterator struct { - index int - query commentEditQuery - variables map[string]interface{} -} + // Shared context, which is used for all graphql queries. + ctx context.Context -type timelineIterator struct { - index int - query issueTimelineQuery - variables map[string]interface{} + // Sticky error + err error - issueEdit indexer - commentEdit indexer + // Issue iterator + issueIter issueIter +} - // lastEndCursor cache the timeline end cursor for one iteration - lastEndCursor githubv4.String +type issueIter struct { + iterVars + query issueQuery + issueEditIter []issueEditIter + timelineIter []timelineIter } -type iterator struct { - // github graphql client - gc *githubv4.Client +type issueEditIter struct { + iterVars + query issueEditQuery +} - // if since is given the iterator will query only the updated - // and created issues after this date - since time.Time +type timelineIter struct { + iterVars + query timelineQuery + commentEditIter []commentEditIter +} - // number of timelines/userEditcontent/issueEdit to query - // at a time, more capacity = more used memory = less queries - // to make - capacity int +type commentEditIter struct { + iterVars + query commentEditQuery +} - // shared context used for all graphql queries - ctx context.Context +type iterVars struct { + // Iterator index + index int - // sticky error - err error + // capacity is the number of elements (issues, issue edits, timeline items, or + // comment edits) to query at a time. More capacity = more used memory = + // less queries to make. + capacity int - // timeline iterator - timeline timelineIterator + // Variable assignments for graphql query + variables varmap +} - // issue edit iterator - issueEdit issueEditIterator +type varmap map[string]interface{} - // comment edit iterator - commentEdit commentEditIterator +func newIterVars(capacity int) iterVars { + return iterVars{ + index: -1, + capacity: capacity, + variables: varmap{}, + } } -// NewIterator create and initialize a new iterator +// NewIterator creates and initialize a new iterator. func NewIterator(ctx context.Context, client *githubv4.Client, capacity int, owner, project string, since time.Time) *iterator { i := &iterator{ - gc: client, - since: since, - capacity: capacity, - ctx: ctx, - timeline: timelineIterator{ - index: -1, - issueEdit: indexer{-1}, - commentEdit: indexer{-1}, - variables: map[string]interface{}{ - "owner": githubv4.String(owner), - "name": githubv4.String(project), - }, - }, - commentEdit: commentEditIterator{ - index: -1, - variables: map[string]interface{}{ - "owner": githubv4.String(owner), - "name": githubv4.String(project), - }, - }, - issueEdit: issueEditIterator{ - index: -1, - variables: map[string]interface{}{ - "owner": githubv4.String(owner), - "name": githubv4.String(project), - }, + gc: client, + since: since, + ctx: ctx, + issueIter: issueIter{ + iterVars: newIterVars(capacity), + timelineIter: make([]timelineIter, capacity), + issueEditIter: make([]issueEditIter, capacity), }, } - - i.initTimelineQueryVariables() - return i -} - -// init issue timeline variables -func (i *iterator) initTimelineQueryVariables() { - i.timeline.variables["issueFirst"] = githubv4.Int(1) - i.timeline.variables["issueAfter"] = (*githubv4.String)(nil) - i.timeline.variables["issueSince"] = githubv4.DateTime{Time: i.since} - i.timeline.variables["timelineFirst"] = githubv4.Int(i.capacity) - i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil) - // Fun fact, github provide the comment edition in reverse chronological - // order, because haha. Look at me, I'm dying of laughter. - i.timeline.variables["issueEditLast"] = githubv4.Int(i.capacity) - i.timeline.variables["issueEditBefore"] = (*githubv4.String)(nil) - i.timeline.variables["commentEditLast"] = githubv4.Int(i.capacity) - i.timeline.variables["commentEditBefore"] = (*githubv4.String)(nil) -} - -// init issue edit variables -func (i *iterator) initIssueEditQueryVariables() { - i.issueEdit.variables["issueFirst"] = githubv4.Int(1) - i.issueEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"] - i.issueEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since} - i.issueEdit.variables["issueEditLast"] = githubv4.Int(i.capacity) - i.issueEdit.variables["issueEditBefore"] = (*githubv4.String)(nil) -} - -// init issue comment variables -func (i *iterator) initCommentEditQueryVariables() { - i.commentEdit.variables["issueFirst"] = githubv4.Int(1) - i.commentEdit.variables["issueAfter"] = i.timeline.variables["issueAfter"] - i.commentEdit.variables["issueSince"] = githubv4.DateTime{Time: i.since} - i.commentEdit.variables["timelineFirst"] = githubv4.Int(1) - i.commentEdit.variables["timelineAfter"] = (*githubv4.String)(nil) - i.commentEdit.variables["commentEditLast"] = githubv4.Int(i.capacity) - i.commentEdit.variables["commentEditBefore"] = (*githubv4.String)(nil) -} - -// reverse UserContentEdits arrays in both of the issue and -// comment timelines -func (i *iterator) reverseTimelineEditNodes() { - node := i.timeline.query.Repository.Issues.Nodes[0] - reverseEdits(node.UserContentEdits.Nodes) - for index, ce := range node.TimelineItems.Edges { - if ce.Node.Typename == "IssueComment" && len(node.TimelineItems.Edges) != 0 { - reverseEdits(node.TimelineItems.Edges[index].Node.IssueComment.UserContentEdits.Nodes) + i.issueIter.variables.setOwnerProject(owner, project) + for idx := range i.issueIter.issueEditIter { + ie := &i.issueIter.issueEditIter[idx] + ie.iterVars = newIterVars(capacity) + } + for i1 := range i.issueIter.timelineIter { + tli := &i.issueIter.timelineIter[i1] + tli.iterVars = newIterVars(capacity) + tli.commentEditIter = make([]commentEditIter, capacity) + for i2 := range tli.commentEditIter { + cei := &tli.commentEditIter[i2] + cei.iterVars = newIterVars(capacity) } } + i.resetIssueVars() + return i } -// Error return last encountered error -func (i *iterator) Error() error { - return i.err +func (v *varmap) setOwnerProject(owner, project string) { + (*v)["owner"] = githubv4.String(owner) + (*v)["name"] = githubv4.String(project) } -func (i *iterator) queryIssue() bool { - ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout) - defer cancel() - - if err := i.gc.Query(ctx, &i.timeline.query, i.timeline.variables); err != nil { - i.err = err - return false - } +func (i *iterator) resetIssueVars() { + vars := &i.issueIter.variables + (*vars)["issueFirst"] = githubv4.Int(i.issueIter.capacity) + (*vars)["issueAfter"] = (*githubv4.String)(nil) + (*vars)["issueSince"] = githubv4.DateTime{Time: i.since} + i.issueIter.query.Repository.Issues.PageInfo.HasNextPage = true + i.issueIter.query.Repository.Issues.PageInfo.EndCursor = "" +} - issues := i.timeline.query.Repository.Issues.Nodes - if len(issues) == 0 { - return false +func (i *iterator) resetIssueEditVars() { + for idx := range i.issueIter.issueEditIter { + ie := &i.issueIter.issueEditIter[idx] + ie.variables["issueEditLast"] = githubv4.Int(ie.capacity) + ie.variables["issueEditBefore"] = (*githubv4.String)(nil) + ie.query.Node.Issue.UserContentEdits.PageInfo.HasNextPage = true + ie.query.Node.Issue.UserContentEdits.PageInfo.EndCursor = "" } - - i.reverseTimelineEditNodes() - return true } -// NextIssue try to query the next issue and return true. Only one issue is -// queried at each call. -func (i *iterator) NextIssue() bool { - if i.err != nil { - return false +func (i *iterator) resetTimelineVars() { + for idx := range i.issueIter.timelineIter { + ip := &i.issueIter.timelineIter[idx] + ip.variables["timelineFirst"] = githubv4.Int(ip.capacity) + ip.variables["timelineAfter"] = (*githubv4.String)(nil) + ip.query.Node.Issue.TimelineItems.PageInfo.HasNextPage = true + ip.query.Node.Issue.TimelineItems.PageInfo.EndCursor = "" } +} - // if $issueAfter variable is nil we can directly make the first query - if i.timeline.variables["issueAfter"] == (*githubv4.String)(nil) { - nextIssue := i.queryIssue() - // prevent from infinite loop by setting a non nil cursor - issues := i.timeline.query.Repository.Issues - i.timeline.variables["issueAfter"] = issues.PageInfo.EndCursor - return nextIssue +func (i *iterator) resetCommentEditVars() { + for i1 := range i.issueIter.timelineIter { + for i2 := range i.issueIter.timelineIter[i1].commentEditIter { + ce := &i.issueIter.timelineIter[i1].commentEditIter[i2] + ce.variables["commentEditLast"] = githubv4.Int(ce.capacity) + ce.variables["commentEditBefore"] = (*githubv4.String)(nil) + ce.query.Node.IssueComment.UserContentEdits.PageInfo.HasNextPage = true + ce.query.Node.IssueComment.UserContentEdits.PageInfo.EndCursor = "" + } } +} - issues := i.timeline.query.Repository.Issues - if !issues.PageInfo.HasNextPage { - return false +// Error return last encountered error +func (i *iterator) Error() error { + if i.err != nil { + return i.err } + return i.ctx.Err() // might return nil +} - // if we have more issues, query them - i.timeline.variables["timelineAfter"] = (*githubv4.String)(nil) - i.timeline.index = -1 - - timelineEndCursor := issues.Nodes[0].TimelineItems.PageInfo.EndCursor - // store cursor for future use - i.timeline.lastEndCursor = timelineEndCursor +func (i *iterator) HasError() bool { + return i.err != nil || i.ctx.Err() != nil +} - // query issue block - nextIssue := i.queryIssue() - i.timeline.variables["issueAfter"] = issues.PageInfo.EndCursor +func (i *iterator) currIssueItem() *issue { + return &i.issueIter.query.Repository.Issues.Nodes[i.issueIter.index] +} - return nextIssue +func (i *iterator) currIssueEditIter() *issueEditIter { + return &i.issueIter.issueEditIter[i.issueIter.index] } -// IssueValue return the actual issue value -func (i *iterator) IssueValue() issueTimeline { - issues := i.timeline.query.Repository.Issues - return issues.Nodes[0] +func (i *iterator) currTimelineIter() *timelineIter { + return &i.issueIter.timelineIter[i.issueIter.index] } -// NextTimelineItem return true if there is a next timeline item and increments the index by one. -// It is used iterates over all the timeline items. Extra queries are made if it is necessary. -func (i *iterator) NextTimelineItem() bool { - if i.err != nil { - return false - } +func (i *iterator) currCommentEditIter() *commentEditIter { + timelineIter := i.currTimelineIter() + return &timelineIter.commentEditIter[timelineIter.index] +} - if i.ctx.Err() != nil { - return false - } +func (i *iterator) currIssueGqlNodeId() githubv4.ID { + return i.currIssueItem().Id +} - timelineItems := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems - // after NextIssue call it's good to check wether we have some timelineItems items or not - if len(timelineItems.Edges) == 0 { +// NextIssue returns true if there exists a next issue and advances the iterator by one. +// It is used to iterate over all issues. Queries to github are made when necessary. +func (i *iterator) NextIssue() bool { + if i.HasError() { return false } - - if i.timeline.index < len(timelineItems.Edges)-1 { - i.timeline.index++ + index := &i.issueIter.index + issues := &i.issueIter.query.Repository.Issues + issueItems := &issues.Nodes + if 0 <= *index && *index < len(*issueItems)-1 { + *index += 1 return true } - if !timelineItems.PageInfo.HasNextPage { + if !issues.PageInfo.HasNextPage { return false } + nextIssue := i.queryIssue() + return nextIssue +} - i.timeline.lastEndCursor = timelineItems.PageInfo.EndCursor - - // more timelines, query them - i.timeline.variables["timelineAfter"] = timelineItems.PageInfo.EndCursor +// IssueValue returns the actual issue value. +func (i *iterator) IssueValue() issue { + return *i.currIssueItem() +} +func (i *iterator) queryIssue() bool { ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout) defer cancel() - - if err := i.gc.Query(ctx, &i.timeline.query, i.timeline.variables); err != nil { + if endCursor := i.issueIter.query.Repository.Issues.PageInfo.EndCursor; endCursor != "" { + i.issueIter.variables["issueAfter"] = endCursor + } + if err := i.gc.Query(ctx, &i.issueIter.query, i.issueIter.variables); err != nil { i.err = err return false } - - timelineItems = i.timeline.query.Repository.Issues.Nodes[0].TimelineItems - // (in case github returns something weird) just for safety: better return a false than a panic - if len(timelineItems.Edges) == 0 { + i.resetIssueEditVars() + i.resetTimelineVars() + issueItems := &i.issueIter.query.Repository.Issues.Nodes + if len(*issueItems) <= 0 { + i.issueIter.index = -1 return false } - - i.reverseTimelineEditNodes() - i.timeline.index = 0 + i.issueIter.index = 0 return true } -// TimelineItemValue return the actual timeline item value -func (i *iterator) TimelineItemValue() timelineItem { - timelineItems := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems - return timelineItems.Edges[i.timeline.index].Node -} - -func (i *iterator) queryIssueEdit() bool { - ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout) - defer cancel() - - if err := i.gc.Query(ctx, &i.issueEdit.query, i.issueEdit.variables); err != nil { - i.err = err - //i.timeline.issueEdit.index = -1 +// NextIssueEdit returns true if there exists a next issue edit and advances the iterator +// by one. It is used to iterate over all the issue edits. Queries to github are made when +// necessary. +func (i *iterator) NextIssueEdit() bool { + if i.HasError() { return false } - - issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits - // reverse issue edits because github - reverseEdits(issueEdits.Nodes) - - // this is not supposed to happen - if len(issueEdits.Nodes) == 0 { - i.timeline.issueEdit.index = -1 + ieIter := i.currIssueEditIter() + ieIdx := &ieIter.index + ieItems := ieIter.query.Node.Issue.UserContentEdits + if 0 <= *ieIdx && *ieIdx < len(ieItems.Nodes)-1 { + *ieIdx += 1 + return i.nextValidIssueEdit() + } + if !ieItems.PageInfo.HasNextPage { + return false + } + querySucc := i.queryIssueEdit() + if !querySucc { return false } - - i.issueEdit.index = 0 - i.timeline.issueEdit.index = -2 return i.nextValidIssueEdit() } func (i *iterator) nextValidIssueEdit() bool { - // issueEdit.Diff == nil happen if the event is older than early 2018, Github doesn't have the data before that. - // Best we can do is to ignore the event. + // issueEdit.Diff == nil happen if the event is older than early 2018, Github doesn't have + // the data before that. Best we can do is to ignore the event. if issueEdit := i.IssueEditValue(); issueEdit.Diff == nil || string(*issueEdit.Diff) == "" { return i.NextIssueEdit() } return true } -// NextIssueEdit return true if there is a next issue edit and increments the index by one. -// It is used iterates over all the issue edits. Extra queries are made if it is necessary. -func (i *iterator) NextIssueEdit() bool { - if i.err != nil { +// IssueEditValue returns the actual issue edit value. +func (i *iterator) IssueEditValue() userContentEdit { + iei := i.currIssueEditIter() + return iei.query.Node.Issue.UserContentEdits.Nodes[iei.index] +} + +func (i *iterator) queryIssueEdit() bool { + ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout) + defer cancel() + iei := i.currIssueEditIter() + if endCursor := iei.query.Node.Issue.UserContentEdits.PageInfo.EndCursor; endCursor != "" { + iei.variables["issueEditBefore"] = endCursor + } + iei.variables["gqlNodeId"] = i.currIssueGqlNodeId() + if err := i.gc.Query(ctx, &iei.query, iei.variables); err != nil { + i.err = err return false } - - if i.ctx.Err() != nil { + issueEditItems := iei.query.Node.Issue.UserContentEdits.Nodes + if len(issueEditItems) <= 0 { + iei.index = -1 return false } + // The UserContentEditConnection in the Github API serves its elements in reverse chronological + // order. For our purpose we have to reverse the edits. + reverseEdits(issueEditItems) + iei.index = 0 + return true +} - // this mean we looped over all available issue edits in the timeline. - // now we have to use i.issueEditQuery - if i.timeline.issueEdit.index == -2 { - issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits - if i.issueEdit.index < len(issueEdits.Nodes)-1 { - i.issueEdit.index++ - return i.nextValidIssueEdit() - } - - if !issueEdits.PageInfo.HasPreviousPage { - i.timeline.issueEdit.index = -1 - i.issueEdit.index = -1 - return false - } - - // if there is more edits, query them - i.issueEdit.variables["issueEditBefore"] = issueEdits.PageInfo.StartCursor - return i.queryIssueEdit() - } - - issueEdits := i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits - // if there is no edit, the UserContentEdits given by github is empty. That - // means that the original message is given by the issue message. - // - // if there is edits, the UserContentEdits given by github contains both the - // original message and the following edits. The issue message give the last - // version so we don't care about that. - // - // the tricky part: for an issue older than the UserContentEdits API, github - // doesn't have the previous message version anymore and give an edition - // with .Diff == nil. We have to filter them. - if len(issueEdits.Nodes) == 0 { +// NextTimelineItem returns true if there exists a next timeline item and advances the iterator +// by one. It is used to iterate over all the timeline items. Queries to github are made when +// necessary. +func (i *iterator) NextTimelineItem() bool { + if i.HasError() { return false } - - // loop over them timeline comment edits - if i.timeline.issueEdit.index < len(issueEdits.Nodes)-1 { - i.timeline.issueEdit.index++ - return i.nextValidIssueEdit() + tlIter := &i.issueIter.timelineIter[i.issueIter.index] + tlIdx := &tlIter.index + tlItems := tlIter.query.Node.Issue.TimelineItems + if 0 <= *tlIdx && *tlIdx < len(tlItems.Nodes)-1 { + *tlIdx += 1 + return true } - - if !issueEdits.PageInfo.HasPreviousPage { - i.timeline.issueEdit.index = -1 + if !tlItems.PageInfo.HasNextPage { return false } - - // if there is more edits, query them - i.initIssueEditQueryVariables() - i.issueEdit.variables["issueEditBefore"] = issueEdits.PageInfo.StartCursor - return i.queryIssueEdit() + nextTlItem := i.queryTimeline() + return nextTlItem } -// IssueEditValue return the actual issue edit value -func (i *iterator) IssueEditValue() userContentEdit { - // if we are using issue edit query - if i.timeline.issueEdit.index == -2 { - issueEdits := i.issueEdit.query.Repository.Issues.Nodes[0].UserContentEdits - return issueEdits.Nodes[i.issueEdit.index] - } - - issueEdits := i.timeline.query.Repository.Issues.Nodes[0].UserContentEdits - // else get it from timeline issue edit query - return issueEdits.Nodes[i.timeline.issueEdit.index] +// TimelineItemValue returns the actual timeline item value. +func (i *iterator) TimelineItemValue() timelineItem { + tli := i.currTimelineIter() + return tli.query.Node.Issue.TimelineItems.Nodes[tli.index] } -func (i *iterator) queryCommentEdit() bool { +func (i *iterator) queryTimeline() bool { ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout) defer cancel() - - if err := i.gc.Query(ctx, &i.commentEdit.query, i.commentEdit.variables); err != nil { + tli := i.currTimelineIter() + if endCursor := tli.query.Node.Issue.TimelineItems.PageInfo.EndCursor; endCursor != "" { + tli.variables["timelineAfter"] = endCursor + } + tli.variables["gqlNodeId"] = i.currIssueGqlNodeId() + if err := i.gc.Query(ctx, &tli.query, tli.variables); err != nil { i.err = err return false } + i.resetCommentEditVars() + timelineItems := &tli.query.Node.Issue.TimelineItems + if len(timelineItems.Nodes) <= 0 { + tli.index = -1 + return false + } + tli.index = 0 + return true +} - commentEdits := i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits - // this is not supposed to happen - if len(commentEdits.Nodes) == 0 { - i.timeline.commentEdit.index = -1 +// NextCommentEdit returns true if there exists a next comment edit and advances the iterator +// by one. It is used to iterate over all issue edits. Queries to github are made when +// necessary. +func (i *iterator) NextCommentEdit() bool { + if i.HasError() { return false } - reverseEdits(commentEdits.Nodes) + tmlnVal := i.TimelineItemValue() + if tmlnVal.Typename != "IssueComment" { + // The timeline iterator does not point to a comment. + i.err = errors.New("Call to NextCommentEdit() while timeline item is not a comment") + return false + } - i.commentEdit.index = 0 - i.timeline.commentEdit.index = -2 + cei := i.currCommentEditIter() + ceIdx := &cei.index + ceItems := &cei.query.Node.IssueComment.UserContentEdits + if 0 <= *ceIdx && *ceIdx < len(ceItems.Nodes)-1 { + *ceIdx += 1 + return i.nextValidCommentEdit() + } + if !ceItems.PageInfo.HasNextPage { + return false + } + querySucc := i.queryCommentEdit() + if !querySucc { + return false + } return i.nextValidCommentEdit() } @@ -407,72 +380,40 @@ func (i *iterator) nextValidCommentEdit() bool { return true } -// NextCommentEdit return true if there is a next comment edit and increments the index by one. -// It is used iterates over all the comment edits. Extra queries are made if it is necessary. -func (i *iterator) NextCommentEdit() bool { - if i.err != nil { - return false - } - - if i.ctx.Err() != nil { - return false - } - - // same as NextIssueEdit - if i.timeline.commentEdit.index == -2 { - commentEdits := i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits - if i.commentEdit.index < len(commentEdits.Nodes)-1 { - i.commentEdit.index++ - return i.nextValidCommentEdit() - } +// CommentEditValue returns the actual comment edit value. +func (i *iterator) CommentEditValue() userContentEdit { + cei := i.currCommentEditIter() + return cei.query.Node.IssueComment.UserContentEdits.Nodes[cei.index] +} - if !commentEdits.PageInfo.HasPreviousPage { - i.timeline.commentEdit.index = -1 - i.commentEdit.index = -1 - return false - } +func (i *iterator) queryCommentEdit() bool { + ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout) + defer cancel() + cei := i.currCommentEditIter() - // if there is more comment edits, query them - i.commentEdit.variables["commentEditBefore"] = commentEdits.PageInfo.StartCursor - return i.queryCommentEdit() + if endCursor := cei.query.Node.IssueComment.UserContentEdits.PageInfo.EndCursor; endCursor != "" { + cei.variables["commentEditBefore"] = endCursor } - - commentEdits := i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index].Node.IssueComment - // if there is no comment edits - if len(commentEdits.UserContentEdits.Nodes) == 0 { + tmlnVal := i.TimelineItemValue() + if tmlnVal.Typename != "IssueComment" { + i.err = errors.New("Call to queryCommentEdit() while timeline item is not a comment") return false } - - // loop over them timeline comment edits - if i.timeline.commentEdit.index < len(commentEdits.UserContentEdits.Nodes)-1 { - i.timeline.commentEdit.index++ - return i.nextValidCommentEdit() - } - - if !commentEdits.UserContentEdits.PageInfo.HasPreviousPage { - i.timeline.commentEdit.index = -1 + cei.variables["gqlNodeId"] = tmlnVal.IssueComment.Id + if err := i.gc.Query(ctx, &cei.query, cei.variables); err != nil { + i.err = err return false } - - i.initCommentEditQueryVariables() - if i.timeline.index == 0 { - i.commentEdit.variables["timelineAfter"] = i.timeline.lastEndCursor - } else { - i.commentEdit.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index-1].Cursor - } - - i.commentEdit.variables["commentEditBefore"] = commentEdits.UserContentEdits.PageInfo.StartCursor - - return i.queryCommentEdit() -} - -// CommentEditValue return the actual comment edit value -func (i *iterator) CommentEditValue() userContentEdit { - if i.timeline.commentEdit.index == -2 { - return i.commentEdit.query.Repository.Issues.Nodes[0].Timeline.Nodes[0].IssueComment.UserContentEdits.Nodes[i.commentEdit.index] + ceItems := cei.query.Node.IssueComment.UserContentEdits.Nodes + if len(ceItems) <= 0 { + cei.index = -1 + return false } - - return i.timeline.query.Repository.Issues.Nodes[0].TimelineItems.Edges[i.timeline.index].Node.IssueComment.UserContentEdits.Nodes[i.timeline.commentEdit.index] + // The UserContentEditConnection in the Github API serves its elements in reverse chronological + // order. For our purpose we have to reverse the edits. + reverseEdits(ceItems) + cei.index = 0 + return true } func reverseEdits(edits []userContentEdit) { |