diff --git a/api/queries_pr.go b/api/queries_pr.go index 905bd20661d..0b2fd378a83 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -3,6 +3,7 @@ package api import ( "fmt" "strings" + "time" "github.com/cli/cli/internal/ghrepo" ) @@ -61,6 +62,51 @@ type PullRequest struct { } } } + ReviewRequests struct { + Nodes []struct { + RequestedReviewer struct { + TypeName string `json:"__typename"` + Login string + } + } + TotalCount int + } + Reviews struct { + Nodes []struct { + Author struct { + Login string + } + State string + CreatedAt time.Time + PublishedAt time.Time + } + } + Assignees struct { + Nodes []struct { + Login string + } + TotalCount int + } + Labels struct { + Nodes []struct { + Name string + } + TotalCount int + } + ProjectCards struct { + Nodes []struct { + Project struct { + Name string + } + Column struct { + Name string + } + } + TotalCount int + } + Milestone struct { + Title string + } } type NotFoundError struct { @@ -323,6 +369,54 @@ func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*Pu isCrossRepository isDraft maintainerCanModify + reviewRequests(first: 100) { + nodes { + requestedReviewer { + __typename + ...on User { + login + } + } + } + totalCount + } + reviews(last: 100) { + nodes { + author { + login + } + state + createdAt + publishedAt + } + totalCount + } + assignees(first: 100) { + nodes { + login + } + totalCount + } + labels(first: 100) { + nodes { + name + } + totalCount + } + projectCards(first: 100) { + nodes { + project { + name + } + column { + name + } + } + totalCount + } + milestone{ + title + } } } }` @@ -374,6 +468,54 @@ func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, hea } isCrossRepository isDraft + reviewRequests(first: 100) { + nodes { + requestedReviewer { + __typename + ...on User { + login + } + } + } + totalCount + } + reviews(last: 100) { + nodes { + author { + login + } + state + createdAt + publishedAt + } + totalCount + } + assignees(first: 100) { + nodes { + login + } + totalCount + } + labels(first: 100) { + nodes { + name + } + totalCount + } + projectCards(first: 100) { + nodes { + project { + name + } + column { + name + } + } + totalCount + } + milestone{ + title + } } } } diff --git a/command/pr.go b/command/pr.go index e494d88b5a9..797ce9606e4 100644 --- a/command/pr.go +++ b/command/pr.go @@ -327,6 +327,7 @@ func prView(cmd *cobra.Command, args []string) error { } func printPrPreview(out io.Writer, pr *api.PullRequest) error { + // Header (Title and State) fmt.Fprintln(out, utils.Bold(pr.Title)) fmt.Fprintf(out, "%s", prStateTitleWithColor(*pr)) fmt.Fprintln(out, utils.Gray(fmt.Sprintf( @@ -336,6 +337,28 @@ func printPrPreview(out io.Writer, pr *api.PullRequest) error { pr.BaseRefName, pr.HeadRefName, ))) + + // Metadata + // TODO: Reviewers + fmt.Fprintln(out) + if assignees := prAssigneeList(*pr); assignees != "" { + fmt.Fprint(out, utils.Bold("Assignees: ")) + fmt.Fprintln(out, assignees) + } + if labels := prLabelList(*pr); labels != "" { + fmt.Fprint(out, utils.Bold("Labels: ")) + fmt.Fprintln(out, labels) + } + if projects := prProjectList(*pr); projects != "" { + fmt.Fprint(out, utils.Bold("Projects: ")) + fmt.Fprintln(out, projects) + } + if pr.Milestone.Title != "" { + fmt.Fprint(out, utils.Bold("Milestone: ")) + fmt.Fprintln(out, pr.Milestone.Title) + } + + // Body if pr.Body != "" { fmt.Fprintln(out) md, err := utils.RenderMarkdown(pr.Body) @@ -343,13 +366,65 @@ func printPrPreview(out io.Writer, pr *api.PullRequest) error { return err } fmt.Fprintln(out, md) - fmt.Fprintln(out) } + fmt.Fprintln(out) + // Footer fmt.Fprintf(out, utils.Gray("View this pull request on GitHub: %s\n"), pr.URL) return nil } +func prAssigneeList(pr api.PullRequest) string { + if len(pr.Assignees.Nodes) == 0 { + return "" + } + + AssigneeNames := make([]string, 0, len(pr.Assignees.Nodes)) + for _, assignee := range pr.Assignees.Nodes { + AssigneeNames = append(AssigneeNames, assignee.Login) + } + + list := strings.Join(AssigneeNames, ", ") + if pr.Assignees.TotalCount > len(pr.Assignees.Nodes) { + list += ", …" + } + return list +} + +func prLabelList(pr api.PullRequest) string { + if len(pr.Labels.Nodes) == 0 { + return "" + } + + labelNames := make([]string, 0, len(pr.Labels.Nodes)) + for _, label := range pr.Labels.Nodes { + labelNames = append(labelNames, label.Name) + } + + list := strings.Join(labelNames, ", ") + if pr.Labels.TotalCount > len(pr.Labels.Nodes) { + list += ", …" + } + return list +} + +func prProjectList(pr api.PullRequest) string { + if len(pr.ProjectCards.Nodes) == 0 { + return "" + } + + projectNames := make([]string, 0, len(pr.ProjectCards.Nodes)) + for _, project := range pr.ProjectCards.Nodes { + projectNames = append(projectNames, fmt.Sprintf("%s (%s)", project.Project.Name, project.Column.Name)) + } + + list := strings.Join(projectNames, ", ") + if pr.ProjectCards.TotalCount > len(pr.ProjectCards.Nodes) { + list += ", …" + } + return list +} + var prURLRE = regexp.MustCompile(`^https://github\.com/([^/]+)/([^/]+)/pull/(\d+)`) func prFromURL(arg string) (string, ghrepo.Interface) { diff --git a/command/pr_test.go b/command/pr_test.go index db9fd5fd029..00cabcbb95f 100644 --- a/command/pr_test.go +++ b/command/pr_test.go @@ -409,15 +409,45 @@ func TestPRView_Preview(t *testing.T) { fixture string expectedOutputs []string }{ - "Open PR": { + "Open PR without metadata": { ownerRepo: "master", args: "pr view 12", fixture: "../test/fixtures/prViewPreview.json", expectedOutputs: []string{ - "Blueberries are from a fork", - "Open • nobody wants to merge 12 commits into master from blueberries", - "blueberries taste good", - "View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12", + `Blueberries are from a fork`, + `Open • nobody wants to merge 12 commits into master from blueberries`, + `blueberries taste good`, + `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, + }, + }, + "Open PR with metadata by number": { + ownerRepo: "master", + args: "pr view 12", + fixture: "../test/fixtures/prViewPreviewWithMetadataByNumber.json", + expectedOutputs: []string{ + `Blueberries are from a fork`, + `Open • nobody wants to merge 12 commits into master from blueberries`, + `Assignees: marseilles, monaco\n`, + `Labels: one, two, three, four, five\n`, + `Projects: Project 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\)\n`, + `Milestone: uluru\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", + fixture: "../test/fixtures/prViewPreviewWithMetadataByBranch.json", + expectedOutputs: []string{ + `Blueberries are a good fruit`, + `Open • nobody wants to merge 8 commits into master from blueberries`, + `Assignees: marseilles, monaco\n`, + `Labels: one, two, three, four, five\n`, + `Projects: Project 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\)\n`, + `Milestone: uluru\n`, + `blueberries taste good`, + `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10\n`, }, }, "Open PR for the current branch": { @@ -425,10 +455,10 @@ func TestPRView_Preview(t *testing.T) { args: "pr view", fixture: "../test/fixtures/prView.json", expectedOutputs: []string{ - "Blueberries are a good fruit", - "Open • nobody wants to merge 8 commits into master from blueberries", - "blueberries taste good", - "View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10", + `Blueberries are a good fruit`, + `Open • nobody wants to merge 8 commits into master from blueberries`, + `blueberries taste good`, + `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10`, }, }, "Open PR wth empty body for the current branch": { @@ -436,9 +466,9 @@ func TestPRView_Preview(t *testing.T) { args: "pr view", fixture: "../test/fixtures/prView_EmptyBody.json", expectedOutputs: []string{ - "Blueberries are a good fruit", - "Open • nobody wants to merge 8 commits into master from blueberries", - "View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10", + `Blueberries are a good fruit`, + `Open • nobody wants to merge 8 commits into master from blueberries`, + `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10`, }, }, "Closed PR": { @@ -446,10 +476,10 @@ func TestPRView_Preview(t *testing.T) { args: "pr view 12", fixture: "../test/fixtures/prViewPreviewClosedState.json", expectedOutputs: []string{ - "Blueberries are from a fork", - "Closed • nobody wants to merge 12 commits into master from blueberries", - "blueberries taste good", - "View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12", + `Blueberries are from a fork`, + `Closed • nobody wants to merge 12 commits into master from blueberries`, + `blueberries taste good`, + `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, }, }, "Merged PR": { @@ -457,10 +487,10 @@ func TestPRView_Preview(t *testing.T) { args: "pr view 12", fixture: "../test/fixtures/prViewPreviewMergedState.json", expectedOutputs: []string{ - "Blueberries are from a fork", - "Merged • nobody wants to merge 12 commits into master from blueberries", - "blueberries taste good", - "View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12", + `Blueberries are from a fork`, + `Merged • nobody wants to merge 12 commits into master from blueberries`, + `blueberries taste good`, + `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, }, }, "Draft PR": { @@ -468,10 +498,10 @@ func TestPRView_Preview(t *testing.T) { args: "pr view 12", fixture: "../test/fixtures/prViewPreviewDraftState.json", expectedOutputs: []string{ - "Blueberries are from a fork", - "Draft • nobody wants to merge 12 commits into master from blueberries", - "blueberries taste good", - "View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12", + `Blueberries are from a fork`, + `Draft • nobody wants to merge 12 commits into master from blueberries`, + `blueberries taste good`, + `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, }, }, "Draft PR by branch": { @@ -479,10 +509,10 @@ func TestPRView_Preview(t *testing.T) { args: "pr view blueberries", fixture: "../test/fixtures/prViewPreviewDraftStatebyBranch.json", expectedOutputs: []string{ - "Blueberries are a good fruit", - "Draft • nobody wants to merge 8 commits into master from blueberries", - "blueberries taste good", - "View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10", + `Blueberries are a good fruit`, + `Draft • nobody wants to merge 8 commits into master from blueberries`, + `blueberries taste good`, + `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10`, }, }, } diff --git a/test/fixtures/prViewPreview.json b/test/fixtures/prViewPreview.json index 379fd253b2f..f660eaa6c7e 100644 --- a/test/fixtures/prViewPreview.json +++ b/test/fixtures/prViewPreview.json @@ -10,6 +10,21 @@ "author": { "login": "nobody" }, + "assignees": { + "nodes": [], + "totalcount": 0 + }, + "labels": { + "nodes": [], + "totalcount": 0 + }, + "projectcards": { + "nodes": [], + "totalcount": 0 + }, + "milestone": { + "title": "" + }, "commits": { "totalCount": 12 }, diff --git a/test/fixtures/prViewPreviewWithMetadataByBranch.json b/test/fixtures/prViewPreviewWithMetadataByBranch.json new file mode 100644 index 00000000000..aaf9c6dfa9a --- /dev/null +++ b/test/fixtures/prViewPreviewWithMetadataByBranch.json @@ -0,0 +1,126 @@ +{ + "data": { + "repository": { + "pullRequests": { + "nodes": [ + { + "number": 12, + "title": "Blueberries are from a fork", + "state": "OPEN", + "body": "yeah", + "url": "https://github.com/OWNER/REPO/pull/12", + "headRefName": "blueberries", + "baseRefName": "master", + "headRepositoryOwner": { + "login": "hubot" + }, + "assignees": { + "nodes": [], + "totalcount": 0 + }, + "labels": { + "nodes": [], + "totalcount": 0 + }, + "projectcards": { + "nodes": [], + "totalcount": 0 + }, + "milestone": {}, + "commits": { + "totalCount": 12 + }, + "author": { + "login": "nobody" + }, + "isCrossRepository": true, + "isDraft": false + }, + { + "number": 10, + "title": "Blueberries are a good fruit", + "state": "OPEN", + "body": "**blueberries taste good**", + "url": "https://github.com/OWNER/REPO/pull/10", + "baseRefName": "master", + "headRefName": "blueberries", + "author": { + "login": "nobody" + }, + "assignees": { + "nodes": [ + { + "login": "marseilles" + }, + { + "login": "monaco" + } + ], + "totalcount": 2 + }, + "labels": { + "nodes": [ + { + "name": "one" + }, + { + "name": "two" + }, + { + "name": "three" + }, + { + "name": "four" + }, + { + "name": "five" + } + ], + "totalcount": 5 + }, + "projectcards": { + "nodes": [ + { + "project": { + "name": "Project 1" + }, + "column": { + "name": "column A" + } + }, + { + "project": { + "name": "Project 2" + }, + "column": { + "name": "column B" + } + }, + { + "project": { + "name": "Project 3" + }, + "column": { + "name": "column C" + } + } + ], + "totalcount": 3 + }, + "milestone": { + "title": "uluru" + }, + "headRepositoryOwner": { + "login": "OWNER" + }, + "commits": { + "totalCount": 8 + }, + "isCrossRepository": false, + "isDraft": false + } + ] + } + } + } +} diff --git a/test/fixtures/prViewPreviewWithMetadataByNumber.json b/test/fixtures/prViewPreviewWithMetadataByNumber.json new file mode 100644 index 00000000000..de0da816ae7 --- /dev/null +++ b/test/fixtures/prViewPreviewWithMetadataByNumber.json @@ -0,0 +1,89 @@ +{ + "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" + }, + "assignees": { + "nodes": [ + { + "login": "marseilles" + }, + { + "login": "monaco" + } + ], + "totalcount": 2 + }, + "labels": { + "nodes": [ + { + "name": "one" + }, + { + "name": "two" + }, + { + "name": "three" + }, + { + "name": "four" + }, + { + "name": "five" + } + ], + "totalcount": 5 + }, + "projectcards": { + "nodes": [ + { + "project": { + "name": "Project 1" + }, + "column": { + "name": "column A" + } + }, + { + "project": { + "name": "Project 2" + }, + "column": { + "name": "column B" + } + }, + { + "project": { + "name": "Project 3" + }, + "column": { + "name": "column C" + } + } + ], + "totalcount": 3 + }, + "milestone": { + "title": "uluru" + }, + "commits": { + "totalCount": 12 + }, + "baseRefName": "master", + "headRefName": "blueberries", + "headRepositoryOwner": { + "login": "hubot" + }, + "isCrossRepository": true, + "isDraft": false + } + } + } +}