Merge pull request #762 from doi-t/reviewers-in-pr-view
Add Reviewers to pr view in CLI
This commit is contained in:
commit
5ba3baa3c8
5 changed files with 279 additions and 10 deletions
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shurcooL/githubv4"
|
||||
|
||||
|
|
@ -86,6 +85,7 @@ type PullRequest struct {
|
|||
RequestedReviewer struct {
|
||||
TypeName string `json:"__typename"`
|
||||
Login string
|
||||
Name string
|
||||
}
|
||||
}
|
||||
TotalCount int
|
||||
|
|
@ -95,9 +95,7 @@ type PullRequest struct {
|
|||
Author struct {
|
||||
Login string
|
||||
}
|
||||
State string
|
||||
CreatedAt time.Time
|
||||
PublishedAt time.Time
|
||||
State string
|
||||
}
|
||||
}
|
||||
Assignees struct {
|
||||
|
|
@ -403,6 +401,9 @@ func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*Pu
|
|||
...on User {
|
||||
login
|
||||
}
|
||||
...on Team {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
|
|
@ -413,8 +414,6 @@ func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*Pu
|
|||
login
|
||||
}
|
||||
state
|
||||
createdAt
|
||||
publishedAt
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
|
|
@ -503,6 +502,9 @@ func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, hea
|
|||
...on User {
|
||||
login
|
||||
}
|
||||
...on Team {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
|
|
@ -513,8 +515,6 @@ func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, hea
|
|||
login
|
||||
}
|
||||
state
|
||||
createdAt
|
||||
publishedAt
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
|
|
|
|||
118
command/pr.go
118
command/pr.go
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -429,10 +430,13 @@ func printPrPreview(out io.Writer, pr *api.PullRequest) error {
|
|||
pr.BaseRefName,
|
||||
pr.HeadRefName,
|
||||
)))
|
||||
fmt.Fprintln(out)
|
||||
|
||||
// Metadata
|
||||
// TODO: Reviewers
|
||||
fmt.Fprintln(out)
|
||||
if reviewers := prReviewerList(*pr); reviewers != "" {
|
||||
fmt.Fprint(out, utils.Bold("Reviewers: "))
|
||||
fmt.Fprintln(out, reviewers)
|
||||
}
|
||||
if assignees := prAssigneeList(*pr); assignees != "" {
|
||||
fmt.Fprint(out, utils.Bold("Assignees: "))
|
||||
fmt.Fprintln(out, assignees)
|
||||
|
|
@ -466,6 +470,116 @@ func printPrPreview(out io.Writer, pr *api.PullRequest) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Ref. https://developer.github.com/v4/enum/pullrequestreviewstate/
|
||||
const (
|
||||
requestedReviewState = "REQUESTED" // This is our own state for review request
|
||||
approvedReviewState = "APPROVED"
|
||||
changesRequestedReviewState = "CHANGES_REQUESTED"
|
||||
commentedReviewState = "COMMENTED"
|
||||
)
|
||||
|
||||
type reviewerState struct {
|
||||
Name string
|
||||
State string
|
||||
}
|
||||
|
||||
// colorFuncForReviewerState returns a color function for a reviewer state
|
||||
func colorFuncForReviewerState(state string) func(string) string {
|
||||
switch state {
|
||||
case requestedReviewState:
|
||||
return utils.Yellow
|
||||
case approvedReviewState:
|
||||
return utils.Green
|
||||
case changesRequestedReviewState:
|
||||
return utils.Red
|
||||
case commentedReviewState:
|
||||
return func(str string) string { return str } // Do nothing
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// formattedReviewerState formats a reviewerState with state color
|
||||
func formattedReviewerState(reviewer *reviewerState) string {
|
||||
stateColorFunc := colorFuncForReviewerState(reviewer.State)
|
||||
return fmt.Sprintf("%s (%s)", reviewer.Name, stateColorFunc(strings.ReplaceAll(strings.Title(strings.ToLower(reviewer.State)), "_", " ")))
|
||||
}
|
||||
|
||||
// prReviewerList generates a reviewer list with their last state
|
||||
func prReviewerList(pr api.PullRequest) string {
|
||||
reviewerStates := parseReviewers(pr)
|
||||
reviewers := make([]string, 0, len(reviewerStates))
|
||||
|
||||
sortReviewerStates(reviewerStates)
|
||||
|
||||
for _, reviewer := range reviewerStates {
|
||||
reviewers = append(reviewers, formattedReviewerState(reviewer))
|
||||
}
|
||||
|
||||
reviewerList := strings.Join(reviewers, ", ")
|
||||
|
||||
return reviewerList
|
||||
}
|
||||
|
||||
// Ref. https://developer.github.com/v4/union/requestedreviewer/
|
||||
const teamTypeName = "Team"
|
||||
|
||||
const ghostName = "ghost"
|
||||
|
||||
// parseReviewers parses given Reviews and ReviewRequests
|
||||
func parseReviewers(pr api.PullRequest) []*reviewerState {
|
||||
reviewerStates := make(map[string]*reviewerState)
|
||||
|
||||
for _, review := range pr.Reviews.Nodes {
|
||||
if review.Author.Login != pr.Author.Login {
|
||||
name := review.Author.Login
|
||||
if name == "" {
|
||||
name = ghostName
|
||||
}
|
||||
reviewerStates[name] = &reviewerState{
|
||||
Name: name,
|
||||
State: review.State,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite reviewer's state if a review request for the same reviewer exists.
|
||||
for _, reviewRequest := range pr.ReviewRequests.Nodes {
|
||||
name := reviewRequest.RequestedReviewer.Login
|
||||
if reviewRequest.RequestedReviewer.TypeName == teamTypeName {
|
||||
name = reviewRequest.RequestedReviewer.Name
|
||||
}
|
||||
reviewerStates[name] = &reviewerState{
|
||||
Name: name,
|
||||
State: requestedReviewState,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to slice for ease of sort
|
||||
result := make([]*reviewerState, 0, len(reviewerStates))
|
||||
for _, reviewer := range reviewerStates {
|
||||
result = append(result, reviewer)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// sortReviewerStates puts completed reviews before review requests and sorts names alphabetically
|
||||
func sortReviewerStates(reviewerStates []*reviewerState) {
|
||||
sort.Slice(reviewerStates, func(i, j int) bool {
|
||||
if reviewerStates[i].State == requestedReviewState &&
|
||||
reviewerStates[j].State != requestedReviewState {
|
||||
return false
|
||||
}
|
||||
if reviewerStates[j].State == requestedReviewState &&
|
||||
reviewerStates[i].State != requestedReviewState {
|
||||
return true
|
||||
}
|
||||
|
||||
return reviewerStates[i].Name < reviewerStates[j].Name
|
||||
})
|
||||
}
|
||||
|
||||
func prAssigneeList(pr api.PullRequest) string {
|
||||
if len(pr.Assignees.Nodes) == 0 {
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -461,6 +461,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Open • nobody wants to merge 12 commits into master from blueberries`,
|
||||
`Reviewers: 2 \(Approved\), 3 \(Commented\), 1 \(Requested\)\n`,
|
||||
`Assignees: marseilles, monaco\n`,
|
||||
`Labels: one, two, three, four, five\n`,
|
||||
`Projects: Project 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\), Project 4 \(Awaiting triage\)\n`,
|
||||
|
|
@ -469,6 +470,17 @@ func TestPRView_Preview(t *testing.T) {
|
|||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12\n`,
|
||||
},
|
||||
},
|
||||
"Open PR with reviewers by number": {
|
||||
ownerRepo: "master",
|
||||
args: "pr view 12",
|
||||
fixture: "../test/fixtures/prViewPreviewWithReviewersByNumber.json",
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Reviewers: DEF \(Commented\), def \(Changes requested\), ghost \(Approved\), xyz \(Approved\), 123 \(Requested\), Team 1 \(Requested\), abc \(Requested\)\n`,
|
||||
`blueberries taste good`,
|
||||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12\n`,
|
||||
},
|
||||
},
|
||||
"Open PR with metadata by branch": {
|
||||
ownerRepo: "master",
|
||||
args: "pr view blueberries",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,39 @@
|
|||
"author": {
|
||||
"login": "nobody"
|
||||
},
|
||||
"reviewRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"requestedReviewer": {
|
||||
"__typename": "User",
|
||||
"login": "1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalcount": 1
|
||||
},
|
||||
"reviews": {
|
||||
"nodes": [
|
||||
{
|
||||
"author": {
|
||||
"login": "3"
|
||||
},
|
||||
"state": "COMMENTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "2"
|
||||
},
|
||||
"state": "APPROVED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "1"
|
||||
},
|
||||
"state": "CHANGES_REQUESTED"
|
||||
}
|
||||
]
|
||||
},
|
||||
"assignees": {
|
||||
"nodes": [
|
||||
{
|
||||
|
|
|
|||
110
test/fixtures/prViewPreviewWithReviewersByNumber.json
vendored
Normal file
110
test/fixtures/prViewPreviewWithReviewersByNumber.json
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequest": {
|
||||
"number": 12,
|
||||
"title": "Blueberries are from a fork",
|
||||
"state": "OPEN",
|
||||
"body": "**blueberries taste good**",
|
||||
"url": "https://github.com/OWNER/REPO/pull/12",
|
||||
"author": {
|
||||
"login": "nobody"
|
||||
},
|
||||
"reviewRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"requestedReviewer": {
|
||||
"__typename": "user",
|
||||
"login": "123"
|
||||
}
|
||||
},
|
||||
{
|
||||
"requestedReviewer": {
|
||||
"__typename": "Team",
|
||||
"name": "Team 1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"requestedReviewer": {
|
||||
"__typename": "user",
|
||||
"login": "abc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalcount": 1
|
||||
},
|
||||
"reviews": {
|
||||
"nodes": [
|
||||
{
|
||||
"author": {
|
||||
"login": "123"
|
||||
},
|
||||
"state": "COMMENTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "def"
|
||||
},
|
||||
"state": "CHANGES_REQUESTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "abc"
|
||||
},
|
||||
"state": "APPROVED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "DEF"
|
||||
},
|
||||
"state": "COMMENTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "xyz"
|
||||
},
|
||||
"state": "APPROVED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": ""
|
||||
},
|
||||
"state": "APPROVED"
|
||||
}
|
||||
]
|
||||
},
|
||||
"assignees": {
|
||||
"nodes": [],
|
||||
"totalcount": 0
|
||||
},
|
||||
"labels": {
|
||||
"nodes": [],
|
||||
"totalcount": 0
|
||||
},
|
||||
"projectcards": {
|
||||
"nodes": [],
|
||||
"totalcount": 0
|
||||
},
|
||||
"milestone": {},
|
||||
"participants": {
|
||||
"nodes": [
|
||||
{
|
||||
"login": "marseilles"
|
||||
}
|
||||
],
|
||||
"totalcount": 1
|
||||
},
|
||||
"commits": {
|
||||
"totalCount": 12
|
||||
},
|
||||
"baseRefName": "master",
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"isDraft": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue