Merge pull request #854 from cli/close-a-pull-request
Add `gh pr close <numberOrURL>`
This commit is contained in:
commit
e172f31c80
3 changed files with 258 additions and 3 deletions
|
|
@ -1,11 +1,13 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
type PullRequestsPayload struct {
|
||||
|
|
@ -20,9 +22,11 @@ type PullRequestAndTotalCount struct {
|
|||
}
|
||||
|
||||
type PullRequest struct {
|
||||
ID string
|
||||
Number int
|
||||
Title string
|
||||
State string
|
||||
Closed bool
|
||||
URL string
|
||||
BaseRefName string
|
||||
HeadRefName string
|
||||
|
|
@ -344,10 +348,12 @@ func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*Pu
|
|||
query($owner: String!, $repo: String!, $pr_number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $pr_number) {
|
||||
id
|
||||
url
|
||||
number
|
||||
title
|
||||
state
|
||||
closed
|
||||
body
|
||||
author {
|
||||
login
|
||||
|
|
@ -755,6 +761,44 @@ loop:
|
|||
return &res, nil
|
||||
}
|
||||
|
||||
func PullRequestClose(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
|
||||
var mutation struct {
|
||||
ClosePullRequest struct {
|
||||
PullRequest struct {
|
||||
ID githubv4.ID
|
||||
}
|
||||
} `graphql:"closePullRequest(input: $input)"`
|
||||
}
|
||||
|
||||
input := githubv4.ClosePullRequestInput{
|
||||
PullRequestID: pr.ID,
|
||||
}
|
||||
|
||||
v4 := githubv4.NewClient(client.http)
|
||||
err := v4.Mutate(context.Background(), &mutation, input, nil)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
|
||||
var mutation struct {
|
||||
ReopenPullRequest struct {
|
||||
PullRequest struct {
|
||||
ID githubv4.ID
|
||||
}
|
||||
} `graphql:"reopenPullRequest(input: $input)"`
|
||||
}
|
||||
|
||||
input := githubv4.ReopenPullRequestInput{
|
||||
PullRequestID: pr.ID,
|
||||
}
|
||||
|
||||
v4 := githubv4.NewClient(client.http)
|
||||
err := v4.Mutate(context.Background(), &mutation, input, nil)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
|
|
|
|||
|
|
@ -21,16 +21,18 @@ func init() {
|
|||
RootCmd.AddCommand(prCmd)
|
||||
prCmd.AddCommand(prCheckoutCmd)
|
||||
prCmd.AddCommand(prCreateCmd)
|
||||
prCmd.AddCommand(prListCmd)
|
||||
prCmd.AddCommand(prStatusCmd)
|
||||
prCmd.AddCommand(prViewCmd)
|
||||
prCmd.AddCommand(prCloseCmd)
|
||||
prCmd.AddCommand(prReopenCmd)
|
||||
|
||||
prCmd.AddCommand(prListCmd)
|
||||
prListCmd.Flags().IntP("limit", "L", 30, "Maximum number of items to fetch")
|
||||
prListCmd.Flags().StringP("state", "s", "open", "Filter by state: {open|closed|merged|all}")
|
||||
prListCmd.Flags().StringP("base", "B", "", "Filter by base branch")
|
||||
prListCmd.Flags().StringSliceP("label", "l", nil, "Filter by label")
|
||||
prListCmd.Flags().StringP("assignee", "a", "", "Filter by assignee")
|
||||
|
||||
prCmd.AddCommand(prViewCmd)
|
||||
prViewCmd.Flags().BoolP("web", "w", false, "Open a pull request in the browser")
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +67,18 @@ is displayed.
|
|||
With '--web', open the pull request in a web browser instead.`,
|
||||
RunE: prView,
|
||||
}
|
||||
var prCloseCmd = &cobra.Command{
|
||||
Use: "close <number | url>",
|
||||
Short: "Close a pull request",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: prClose,
|
||||
}
|
||||
var prReopenCmd = &cobra.Command{
|
||||
Use: "reopen <number | url>",
|
||||
Short: "Reopen a pull request",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: prReopen,
|
||||
}
|
||||
|
||||
func prStatus(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
|
|
@ -328,6 +342,78 @@ func prView(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
func prClose(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRepo, err := determineBaseRepo(cmd, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr, err := prFromArg(apiClient, baseRepo, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pr.State == "MERGED" {
|
||||
err := fmt.Errorf("%s Pull request #%d can't be closed because it was already merged", utils.Red("!"), pr.Number)
|
||||
return err
|
||||
} else if pr.Closed {
|
||||
fmt.Fprintf(colorableErr(cmd), "%s Pull request #%d is already closed\n", utils.Yellow("!"), pr.Number)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = api.PullRequestClose(apiClient, baseRepo, pr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("API call failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(colorableErr(cmd), "%s Closed pull request #%d\n", utils.Red("✔"), pr.Number)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prReopen(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRepo, err := determineBaseRepo(cmd, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr, err := prFromArg(apiClient, baseRepo, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pr.State == "MERGED" {
|
||||
err := fmt.Errorf("%s Pull request #%d can't be reopened because it was already merged", utils.Red("!"), pr.Number)
|
||||
return err
|
||||
}
|
||||
|
||||
if !pr.Closed {
|
||||
fmt.Fprintf(colorableErr(cmd), "%s Pull request #%d is already open\n", utils.Yellow("!"), pr.Number)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = api.PullRequestReopen(apiClient, baseRepo, pr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("API call failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(colorableErr(cmd), "%s Reopened pull request #%d\n", utils.Green("✔"), pr.Number)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printPrPreview(out io.Writer, pr *api.PullRequest) error {
|
||||
// Header (Title and State)
|
||||
fmt.Fprintln(out, utils.Bold(pr.Title))
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ func RunCommand(cmd *cobra.Command, args string) (*cmdOut, error) {
|
|||
cmd.SetOut(&outBuf)
|
||||
errBuf := bytes.Buffer{}
|
||||
cmd.SetErr(&errBuf)
|
||||
|
||||
// Reset flag values so they don't leak between tests
|
||||
// FIXME: change how we initialize Cobra commands to render this hack unnecessary
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
|
|
@ -800,3 +799,129 @@ func TestPrStateTitleWithColor(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrClose(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 96 }
|
||||
} } }
|
||||
`))
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
|
||||
|
||||
output, err := RunCommand(prCloseCmd, "pr close 96")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr close`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Closed pull request #96`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrClose_alreadyClosed(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 101, "closed": true }
|
||||
} } }
|
||||
`))
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
|
||||
|
||||
output, err := RunCommand(prCloseCmd, "pr close 101")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr close`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Pull request #101 is already closed`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRReopen(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 666, "closed": true}
|
||||
} } }
|
||||
`))
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
|
||||
|
||||
output, err := RunCommand(prReopenCmd, "pr reopen 666")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr reopen`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Reopened pull request #666`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRReopen_alreadyOpen(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 666, "closed": false}
|
||||
} } }
|
||||
`))
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
|
||||
|
||||
output, err := RunCommand(prReopenCmd, "pr reopen 666")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr reopen`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Pull request #666 is already open`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRReopen_alreadyMerged(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 666, "closed": true, "state": "MERGED"}
|
||||
} } }
|
||||
`))
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
|
||||
|
||||
output, err := RunCommand(prReopenCmd, "pr reopen 666")
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error running command `pr reopen`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Pull request #666 can't be reopened because it was already merged`)
|
||||
|
||||
if !r.MatchString(err.Error()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue