@@ -11,11 +11,11 @@ import ( "strings" "dmitri.shuralyov.com/route/github" "dmitri.shuralyov.com/service/change" githubv3 "github.com/google/go-github/github" "github.com/shurcooL/githubql" "github.com/shurcooL/githubv4" "github.com/shurcooL/issues" "github.com/shurcooL/notifications" "github.com/shurcooL/reactions" "github.com/shurcooL/users" ) @@ -24,11 +24,11 @@ import ( // It uses notifications service, if not nil. At this time it infers the current user // from GitHub clients (their authentication info), and cannot be used to serve multiple users. // Both GitHub clients must use same authentication info. // // If router is nil, github.DotCom router is used, which links to subjects on github.com. func NewService(clientV3 *githubv3.Client, clientV4 *githubql.Client, notifications notifications.ExternalService, router github.Router) change.Service { func NewService(clientV3 *githubv3.Client, clientV4 *githubv4.Client, notifications notifications.ExternalService, router github.Router) change.Service { if router == nil { router = github.DotCom{} } return service{ clV3: clientV3, @@ -38,11 +38,11 @@ func NewService(clientV3 *githubv3.Client, clientV4 *githubql.Client, notificati } } type service struct { clV3 *githubv3.Client // GitHub REST API v3 client. clV4 *githubql.Client // GitHub GraphQL API v4 client. clV4 *githubv4.Client // GitHub GraphQL API v4 client. rtr github.Router // notifications may be nil if there's no notifications service. notifications notifications.ExternalService } @@ -54,16 +54,16 @@ func (s service) List(ctx context.Context, rs string, opt change.ListOptions) ([ repo, err := ghRepoSpec(rs) if err != nil { // TODO: Map to 400 Bad Request HTTP error. return nil, err } var states []githubql.PullRequestState var states []githubv4.PullRequestState switch opt.Filter { case change.FilterOpen: states = []githubql.PullRequestState{githubql.PullRequestStateOpen} states = []githubv4.PullRequestState{githubv4.PullRequestStateOpen} case change.FilterClosedMerged: states = []githubql.PullRequestState{githubql.PullRequestStateClosed, githubql.PullRequestStateMerged} states = []githubv4.PullRequestState{githubv4.PullRequestStateClosed, githubv4.PullRequestStateMerged} case change.FilterAll: states = nil // No states to filter the PRs by. default: // TODO: Map to 400 Bad Request HTTP error. return nil, fmt.Errorf("invalid change.ListOptions.Filter value: %q", opt.Filter) @@ -71,30 +71,30 @@ func (s service) List(ctx context.Context, rs string, opt change.ListOptions) ([ var q struct { Repository struct { PullRequests struct { Nodes []struct { Number uint64 State githubql.PullRequestState State githubv4.PullRequestState Title string Labels struct { Nodes []struct { Name string Color string } } `graphql:"labels(first:100)"` Author *githubqlActor CreatedAt githubql.DateTime Author *githubV4Actor CreatedAt githubv4.DateTime Comments struct { TotalCount int } } } `graphql:"pullRequests(first:30,orderBy:{field:CREATED_AT,direction:DESC},states:$prStates)"` } `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"` } variables := map[string]interface{}{ "repositoryOwner": githubql.String(repo.Owner), "repositoryName": githubql.String(repo.Repo), "repositoryOwner": githubv4.String(repo.Owner), "repositoryName": githubv4.String(repo.Repo), "prStates": states, } err = s.clV4.Query(ctx, &q, variables) if err != nil { return nil, err @@ -125,16 +125,16 @@ func (s service) Count(ctx context.Context, rs string, opt change.ListOptions) ( repo, err := ghRepoSpec(rs) if err != nil { // TODO: Map to 400 Bad Request HTTP error. return 0, err } var states []githubql.PullRequestState var states []githubv4.PullRequestState switch opt.Filter { case change.FilterOpen: states = []githubql.PullRequestState{githubql.PullRequestStateOpen} states = []githubv4.PullRequestState{githubv4.PullRequestStateOpen} case change.FilterClosedMerged: states = []githubql.PullRequestState{githubql.PullRequestStateClosed, githubql.PullRequestStateMerged} states = []githubv4.PullRequestState{githubv4.PullRequestStateClosed, githubv4.PullRequestStateMerged} case change.FilterAll: states = nil // No states to filter the PRs by. default: // TODO: Map to 400 Bad Request HTTP error. return 0, fmt.Errorf("invalid change.ListOptions.Filter value: %q", opt.Filter) @@ -145,12 +145,12 @@ func (s service) Count(ctx context.Context, rs string, opt change.ListOptions) ( TotalCount uint64 } `graphql:"pullRequests(states:$prStates)"` } `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"` } variables := map[string]interface{}{ "repositoryOwner": githubql.String(repo.Owner), "repositoryName": githubql.String(repo.Repo), "repositoryOwner": githubv4.String(repo.Owner), "repositoryName": githubv4.String(repo.Repo), "prStates": states, } err = s.clV4.Query(ctx, &q, variables) return q.Repository.PullRequests.TotalCount, err } @@ -163,14 +163,14 @@ func (s service) Get(ctx context.Context, rs string, id uint64) (change.Change, } var q struct { Repository struct { PullRequest struct { Number uint64 State githubql.PullRequestState State githubv4.PullRequestState Title string Author *githubqlActor CreatedAt githubql.DateTime Author *githubV4Actor CreatedAt githubv4.DateTime Comments struct { TotalCount int } Commits struct { TotalCount int @@ -178,13 +178,13 @@ func (s service) Get(ctx context.Context, rs string, id uint64) (change.Change, ChangedFiles int } `graphql:"pullRequest(number:$prNumber)"` } `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"` } variables := map[string]interface{}{ "repositoryOwner": githubql.String(repo.Owner), "repositoryName": githubql.String(repo.Repo), "prNumber": githubql.Int(id), "repositoryOwner": githubv4.String(repo.Owner), "repositoryName": githubv4.String(repo.Repo), "prNumber": githubv4.Int(id), } err = s.clV4.Query(ctx, &q, variables) if err != nil { return change.Change{}, err } @@ -259,43 +259,43 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch // TODO: Map to 400 Bad Request HTTP error. return nil, err } type event struct { // Common fields for all events. Actor *githubqlActor CreatedAt githubql.DateTime Actor *githubV4Actor CreatedAt githubv4.DateTime } var q struct { Repository struct { PullRequest struct { Author *githubqlActor PublishedAt githubql.DateTime LastEditedAt *githubql.DateTime Editor *githubqlActor Body githubql.String Author *githubV4Actor PublishedAt githubv4.DateTime LastEditedAt *githubv4.DateTime Editor *githubV4Actor Body githubv4.String ReactionGroups reactionGroups ViewerCanUpdate bool Timeline struct { Nodes []struct { Typename string `graphql:"__typename"` IssueComment struct { DatabaseID uint64 Author *githubqlActor PublishedAt githubql.DateTime LastEditedAt *githubql.DateTime Editor *githubqlActor Author *githubV4Actor PublishedAt githubv4.DateTime LastEditedAt *githubv4.DateTime Editor *githubV4Actor Body string ReactionGroups reactionGroups ViewerCanUpdate bool } `graphql:"...on IssueComment"` ClosedEvent struct { event Closer struct { Typename string `graphql:"__typename"` PullRequest struct { State githubql.PullRequestState State githubv4.PullRequestState Title string Repository struct { Owner struct{ Login string } Name string } @@ -334,17 +334,17 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch } } `graphql:"...on UnlabeledEvent"` ReviewRequestedEvent struct { event RequestedReviewer struct { User *githubqlUser `graphql:"...on User"` User *githubV4User `graphql:"...on User"` } } `graphql:"...on ReviewRequestedEvent"` ReviewRequestRemovedEvent struct { event RequestedReviewer struct { User *githubqlUser `graphql:"...on User"` User *githubV4User `graphql:"...on User"` } } `graphql:"...on ReviewRequestRemovedEvent"` MergedEvent struct { event Commit struct { @@ -367,15 +367,15 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch // Need to use PullRequest.Reviews rather than PullRequest.Timeline.PullRequestReview, // because the latter is missing single-inline-reply reviews (as of 2018-02-08). Reviews struct { Nodes []struct { DatabaseID uint64 Author *githubqlActor PublishedAt githubql.DateTime LastEditedAt *githubql.DateTime Editor *githubqlActor State githubql.PullRequestReviewState Author *githubV4Actor PublishedAt githubv4.DateTime LastEditedAt *githubv4.DateTime Editor *githubV4Actor State githubv4.PullRequestReviewState Body string ViewerCanUpdate bool Comments struct { Nodes []struct { DatabaseID uint64 @@ -387,16 +387,16 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch } `graphql:"comments(first:100)"` // TODO: Pagination... Figure out how to make pagination across 2 resource types work... } } `graphql:"reviews(first:100)"` // TODO: Pagination... Figure out how to make pagination across 2 resource types work... } `graphql:"pullRequest(number:$prNumber)"` } `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"` Viewer githubqlUser Viewer githubV4User } variables := map[string]interface{}{ "repositoryOwner": githubql.String(repo.Owner), "repositoryName": githubql.String(repo.Repo), "prNumber": githubql.Int(id), "repositoryOwner": githubv4.String(repo.Owner), "repositoryName": githubv4.String(repo.Repo), "prNumber": githubv4.Int(id), } err = s.clV4.Query(ctx, &q, variables) if err != nil { return nil, err } @@ -609,31 +609,31 @@ func (s service) EditComment(ctx context.Context, rs string, id uint64, cr chang return change.Comment{}, err } // See if user has already reacted with that reaction. // If not, add it. Otherwise, remove it. var ( subjectID githubql.ID subjectID githubv4.ID viewerHasReacted bool viewer users.User ) switch { case cr.ID == prDescriptionCommentID: var q struct { Repository struct { PullRequest struct { ID githubql.ID ID githubv4.ID Reactions struct { ViewerHasReacted bool } `graphql:"reactions(content:$reactionContent)"` } `graphql:"pullRequest(number:$prNumber)"` } `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"` Viewer githubqlUser Viewer githubV4User } variables := map[string]interface{}{ "repositoryOwner": githubql.String(repo.Owner), "repositoryName": githubql.String(repo.Repo), "prNumber": githubql.Int(id), "repositoryOwner": githubv4.String(repo.Owner), "repositoryName": githubv4.String(repo.Repo), "prNumber": githubv4.Int(id), "reactionContent": reactionContent, } err = s.clV4.Query(ctx, &q, variables) if err != nil { return change.Comment{}, err @@ -644,20 +644,20 @@ func (s service) EditComment(ctx context.Context, rs string, id uint64, cr chang case strings.HasPrefix(cr.ID, "c"): commentID := "012:IssueComment" + cr.ID[len("c"):] var q struct { Node struct { IssueComment struct { ID githubql.ID ID githubv4.ID Reactions struct { ViewerHasReacted bool } `graphql:"reactions(content:$reactionContent)"` } `graphql:"...on IssueComment"` } `graphql:"node(id:$commentID)"` Viewer githubqlUser Viewer githubV4User } variables := map[string]interface{}{ "commentID": githubql.ID(base64.StdEncoding.EncodeToString([]byte(commentID))), // HACK, TODO: Confirm StdEncoding vs URLEncoding. "commentID": githubv4.ID(base64.StdEncoding.EncodeToString([]byte(commentID))), // HACK, TODO: Confirm StdEncoding vs URLEncoding. "reactionContent": reactionContent, } err = s.clV4.Query(ctx, &q, variables) if err != nil { return change.Comment{}, err @@ -668,20 +668,20 @@ func (s service) EditComment(ctx context.Context, rs string, id uint64, cr chang case strings.HasPrefix(cr.ID, "rc"): commentID := "024:PullRequestReviewComment" + cr.ID[len("rc"):] var q struct { Node struct { PullRequestReviewComment struct { ID githubql.ID ID githubv4.ID Reactions struct { ViewerHasReacted bool } `graphql:"reactions(content:$reactionContent)"` } `graphql:"...on PullRequestReviewComment"` } `graphql:"node(id:$commentID)"` Viewer githubqlUser Viewer githubV4User } variables := map[string]interface{}{ "commentID": githubql.ID(base64.StdEncoding.EncodeToString([]byte(commentID))), // HACK, TODO: Confirm StdEncoding vs URLEncoding. "commentID": githubv4.ID(base64.StdEncoding.EncodeToString([]byte(commentID))), // HACK, TODO: Confirm StdEncoding vs URLEncoding. "reactionContent": reactionContent, } err = s.clV4.Query(ctx, &q, variables) if err != nil { return change.Comment{}, err @@ -701,11 +701,11 @@ func (s service) EditComment(ctx context.Context, rs string, id uint64, cr chang Subject struct { ReactionGroups reactionGroups } } `graphql:"addReaction(input:$input)"` } input := githubql.AddReactionInput{ input := githubv4.AddReactionInput{ SubjectID: subjectID, Content: reactionContent, } err := s.clV4.Mutate(ctx, &m, input, nil) if err != nil { @@ -719,11 +719,11 @@ func (s service) EditComment(ctx context.Context, rs string, id uint64, cr chang Subject struct { ReactionGroups reactionGroups } } `graphql:"removeReaction(input:$input)"` } input := githubql.RemoveReactionInput{ input := githubv4.RemoveReactionInput{ SubjectID: subjectID, Content: reactionContent, } err := s.clV4.Mutate(ctx, &m, input, nil) if err != nil { @@ -754,11 +754,11 @@ func ghRepoSpec(rs string) (repoSpec, error) { Owner: ghOwnerRepo[1], Repo: ghOwnerRepo[2], }, nil } type githubqlActor struct { type githubV4Actor struct { User struct { DatabaseID uint64 } `graphql:"...on User"` Bot struct { DatabaseID uint64 @@ -766,11 +766,11 @@ type githubqlActor struct { Login string AvatarURL string `graphql:"avatarUrl(size:96)"` URL string } func ghActor(actor *githubqlActor) users.User { func ghActor(actor *githubV4Actor) users.User { if actor == nil { return ghost // Deleted user, replace with https://github.com/ghost. } return users.User{ UserSpec: users.UserSpec{ @@ -781,18 +781,18 @@ func ghActor(actor *githubqlActor) users.User { AvatarURL: actor.AvatarURL, HTMLURL: actor.URL, } } type githubqlUser struct { type githubV4User struct { DatabaseID uint64 Login string AvatarURL string `graphql:"avatarUrl(size:96)"` URL string } func ghUser(user *githubqlUser) users.User { func ghUser(user *githubV4User) users.User { if user == nil { return ghost // Deleted user, replace with https://github.com/ghost. } return users.User{ UserSpec: users.UserSpec{ @@ -837,38 +837,38 @@ var ghost = users.User{ AvatarURL: "https://avatars3.githubusercontent.com/u/10137?v=4", HTMLURL: "https://github.com/ghost", } // ghPRState converts a GitHub PullRequestState to change.State. func ghPRState(state githubql.PullRequestState) change.State { func ghPRState(state githubv4.PullRequestState) change.State { switch state { case githubql.PullRequestStateOpen: case githubv4.PullRequestStateOpen: return change.OpenState case githubql.PullRequestStateClosed: case githubv4.PullRequestStateClosed: return change.ClosedState case githubql.PullRequestStateMerged: case githubv4.PullRequestStateMerged: return change.MergedState default: panic("unreachable") } } // ghPRReviewState converts a GitHub PullRequestReviewState to change.ReviewState, if it's supported. func ghPRReviewState(state githubql.PullRequestReviewState) (_ change.ReviewState, ok bool) { func ghPRReviewState(state githubv4.PullRequestReviewState) (_ change.ReviewState, ok bool) { switch state { case githubql.PullRequestReviewStateApproved: case githubv4.PullRequestReviewStateApproved: return change.Approved, true case githubql.PullRequestReviewStateCommented: case githubv4.PullRequestReviewStateCommented: return change.Commented, true case githubql.PullRequestReviewStateChangesRequested: case githubv4.PullRequestReviewStateChangesRequested: return change.ChangesRequested, true case githubql.PullRequestReviewStateDismissed: case githubv4.PullRequestReviewStateDismissed: // PullRequestReviewStateDismissed are reviews that have been retroactively dismissed. // Display them as a regular comment review for now (we can't know the original state). // THINK: Consider displaying these more distinctly. return change.Commented, true case githubql.PullRequestReviewStatePending: case githubv4.PullRequestReviewStatePending: // PullRequestReviewStatePending are reviews that are pending (haven't been posted yet). // TODO: Consider displaying pending review comments. Figure this out // when adding ability to leave reviews. return 0, false default: @@ -883,19 +883,19 @@ func ghColor(hex string) issues.RGB { fmt.Sscanf(hex, "%02x%02x%02x", &c.R, &c.G, &c.B) return c } type reactionGroups []struct { Content githubql.ReactionContent Content githubv4.ReactionContent Users struct { Nodes []*githubqlUser Nodes []*githubV4User TotalCount int } `graphql:"users(first:10)"` ViewerHasReacted bool } // ghReactions converts []githubql.ReactionGroup to []reactions.Reaction. // ghReactions converts []githubv4.ReactionGroup to []reactions.Reaction. func ghReactions(rgs reactionGroups, viewer users.User) []reactions.Reaction { var rs []reactions.Reaction for _, rg := range rgs { if rg.Users.TotalCount == 0 { continue @@ -929,45 +929,45 @@ func ghReactions(rgs reactionGroups, viewer users.User) []reactions.Reaction { }) } return rs } // internalizeReaction converts githubql.ReactionContent to reactions.EmojiID. func internalizeReaction(reaction githubql.ReactionContent) reactions.EmojiID { // internalizeReaction converts githubv4.ReactionContent to reactions.EmojiID. func internalizeReaction(reaction githubv4.ReactionContent) reactions.EmojiID { switch reaction { case githubql.ReactionContentThumbsUp: case githubv4.ReactionContentThumbsUp: return "+1" case githubql.ReactionContentThumbsDown: case githubv4.ReactionContentThumbsDown: return "-1" case githubql.ReactionContentLaugh: case githubv4.ReactionContentLaugh: return "smile" case githubql.ReactionContentHooray: case githubv4.ReactionContentHooray: return "tada" case githubql.ReactionContentConfused: case githubv4.ReactionContentConfused: return "confused" case githubql.ReactionContentHeart: case githubv4.ReactionContentHeart: return "heart" default: panic("unreachable") } } // externalizeReaction converts reactions.EmojiID to githubql.ReactionContent. func externalizeReaction(reaction reactions.EmojiID) (githubql.ReactionContent, error) { // externalizeReaction converts reactions.EmojiID to githubv4.ReactionContent. func externalizeReaction(reaction reactions.EmojiID) (githubv4.ReactionContent, error) { switch reaction { case "+1": return githubql.ReactionContentThumbsUp, nil return githubv4.ReactionContentThumbsUp, nil case "-1": return githubql.ReactionContentThumbsDown, nil return githubv4.ReactionContentThumbsDown, nil case "smile": return githubql.ReactionContentLaugh, nil return githubv4.ReactionContentLaugh, nil case "tada": return githubql.ReactionContentHooray, nil return githubv4.ReactionContentHooray, nil case "confused": return githubql.ReactionContentConfused, nil return githubv4.ReactionContentConfused, nil case "heart": return githubql.ReactionContentHeart, nil return githubv4.ReactionContentHeart, nil default: return "", fmt.Errorf("%q is an unsupported reaction", reaction) } }