package github
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/shurcooL/githubv4"
)
type iterator struct {
// Github graphql client
gc *githubv4.Client
// The iterator will only query issues updated or created after the date given in
// the variable since.
since time.Time
// Shared context, which is used for all graphql queries.
ctx context.Context
// Sticky error
err error
// Issue iterator
issueIter issueIter
}
type issueIter struct {
iterVars
query issueQuery
issueEditIter []issueEditIter
timelineIter []timelineIter
}
type issueEditIter struct {
iterVars
query issueEditQuery
}
type timelineIter struct {
iterVars
query timelineQuery
commentEditIter []commentEditIter
}
type commentEditIter struct {
iterVars
query commentEditQuery
}
type iterVars struct {
// Iterator index
index int
// 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
// Variable assignments for graphql query
variables varmap
}
type varmap map[string]interface{}
func newIterVars(capacity int) iterVars {
return iterVars{
index: -1,
capacity: capacity,
variables: varmap{},
}
}
// 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,
ctx: ctx,
issueIter: issueIter{
iterVars: newIterVars(capacity),
timelineIter: make([]timelineIter, capacity),
issueEditIter: make([]issueEditIter, capacity),
},
}
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
}
func (v *varmap) setOwnerProject(owner, project string) {
(*v)["owner"] = githubv4.String(owner)
(*v)["name"] = githubv4.String(project)
}
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 = ""
}
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 = ""
}
}
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 = ""
}
}
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 = ""
}
}
}
// Error return last encountered error
func (i *iterator) Error() error {
if i.err != nil {
return i.err
}
return i.ctx.Err() // might return nil
}
func (i *iterator) HasError() bool {
return i.err != nil || i.ctx.Err() != nil
}
func (i *iterator) currIssueItem() *issue {
return &i.issueIter.query.Repository.Issues.Nodes[i.issueIter.index]
}
func (i *iterator) currIssueEditIter() *issueEditIter {
return &i.issueIter.issueEditIter[i.issueIter.index]
}
func (i *iterator) currTimelineIter() *timelineIter {
return &i.issueIter.timelineIter[i.issueIter.index]
}
func (i *iterator) currCommentEditIter() *commentEditIter {
timelineIter := i.currTimelineIter()
return &timelineIter.commentEditIter[timelineIter.index]
}
func (i *iterator) currIssueGqlNodeId() githubv4.ID {
return i.currIssueItem().Id
}
// 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
}
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 !issues.PageInfo.HasNextPage {
return false
}
nextIssue := i.queryIssue()
return nextIssue
}
// 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 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
}
i.resetIssueEditVars()
i.resetTimelineVars()
issueItems := &i.issueIter.query.Repository.Issues.Nodes
if len(*issueItems) <= 0 {
i.issueIter.index = -1
return false
}
i.issueIter.index = 0
return true
}
// 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
}
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
}
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.
if issueEdit := i.IssueEditValue(); issueEdit.Diff == nil || string(*issueEdit.Diff) == "" {
return i.NextIssueEdit()
}
return true
}
// 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
}
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
}
// 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
}
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 !tlItems.PageInfo.HasNextPage {
return false
}
nextTlItem := i.queryTimeline()
return nextTlItem
}
// 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) queryTimeline() bool {
ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
defer cancel()
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
}
// 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
}
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
}
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()
}
func (i *iterator) nextValidCommentEdit() bool {
// if comment edit diff is a nil pointer or points to an empty string look for next value
if commentEdit := i.CommentEditValue(); commentEdit.Diff == nil || string(*commentEdit.Diff) == "" {
return i.NextCommentEdit()
}
return true
}
// CommentEditValue returns the actual comment edit value.
func (i *iterator) CommentEditValue() userContentEdit {
cei := i.currCommentEditIter()
return cei.query.Node.IssueComment.UserContentEdits.Nodes[cei.index]
}
func (i *iterator) queryCommentEdit() bool {
ctx, cancel := context.WithTimeout(i.ctx, defaultTimeout)
defer cancel()
cei := i.currCommentEditIter()
if endCursor := cei.query.Node.IssueComment.UserContentEdits.PageInfo.EndCursor; endCursor != "" {
cei.variables["commentEditBefore"] = endCursor
}
tmlnVal := i.TimelineItemValue()
if tmlnVal.Typename != "IssueComment" {
i.err = errors.New("Call to queryCommentEdit() while timeline item is not a comment")
return false
}
cei.variables["gqlNodeId"] = tmlnVal.IssueComment.Id
if err := i.gc.Query(ctx, &cei.query, cei.variables); err != nil {
i.err = err
return false
}
ceItems := cei.query.Node.IssueComment.UserContentEdits.Nodes
if len(ceItems) <= 0 {
cei.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(ceItems)
cei.index = 0
return true
}
func reverseEdits(edits []userContentEdit) {
for i, j := 0, len(edits)-1; i < j; i, j = i+1, j-1 {
edits[i], edits[j] = edits[j], edits[i]
}
}