Merge remote-tracking branch 'origin/master' into issue-metadata-resolve-ids

This commit is contained in:
Mislav Marohnić 2020-05-13 17:20:37 +02:00
commit 57e60ab8a1
9 changed files with 618 additions and 37 deletions

View file

@ -143,6 +143,14 @@ type PullRequestReviewStatus struct {
ReviewRequired bool
}
type PullRequestMergeMethod int
const (
PullRequestMergeMethodMerge PullRequestMergeMethod = iota
PullRequestMergeMethodRebase
PullRequestMergeMethodSquash
)
func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus {
var status PullRequestReviewStatus
switch pr.ReviewDecision {
@ -466,6 +474,7 @@ func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, hea
type response struct {
Repository struct {
PullRequests struct {
ID githubv4.ID
Nodes []PullRequest
}
}
@ -658,6 +667,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
requestReviews(input: $input) { clientMutationId }
}`
reviewParams["pullRequestId"] = pr.ID
reviewParams["union"] = true
variables := map[string]interface{}{
"input": reviewParams,
}
@ -915,6 +925,34 @@ func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) e
return err
}
func PullRequestMerge(client *Client, repo ghrepo.Interface, pr *PullRequest, m PullRequestMergeMethod) error {
mergeMethod := githubv4.PullRequestMergeMethodMerge
switch m {
case PullRequestMergeMethodRebase:
mergeMethod = githubv4.PullRequestMergeMethodRebase
case PullRequestMergeMethodSquash:
mergeMethod = githubv4.PullRequestMergeMethodSquash
}
var mutation struct {
MergePullRequest struct {
PullRequest struct {
ID githubv4.ID
}
} `graphql:"mergePullRequest(input: $input)"`
}
input := githubv4.MergePullRequestInput{
PullRequestID: pr.ID,
MergeMethod: &mergeMethod,
}
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

View file

@ -25,6 +25,10 @@ func init() {
prCmd.AddCommand(prStatusCmd)
prCmd.AddCommand(prCloseCmd)
prCmd.AddCommand(prReopenCmd)
prCmd.AddCommand(prMergeCmd)
prMergeCmd.Flags().BoolP("merge", "m", true, "Merge the commits with the base branch")
prMergeCmd.Flags().BoolP("rebase", "r", false, "Rebase the commits onto the base branch")
prMergeCmd.Flags().BoolP("squash", "s", false, "Squash the commits into one commit and merge it into the base branch")
prCmd.AddCommand(prListCmd)
prListCmd.Flags().IntP("limit", "L", 30, "Maximum number of items to fetch")
@ -58,7 +62,7 @@ var prStatusCmd = &cobra.Command{
RunE: prStatus,
}
var prViewCmd = &cobra.Command{
Use: "view [{<number> | <url> | <branch>}]",
Use: "view [<number> | <url> | <branch>]",
Short: "View a pull request",
Long: `Display the title, body, and other information about a pull request.
@ -81,6 +85,13 @@ var prReopenCmd = &cobra.Command{
RunE: prReopen,
}
var prMergeCmd = &cobra.Command{
Use: "merge [<number> | <url> | <branch>]",
Short: "Merge a pull request",
Args: cobra.MaximumNArgs(1),
RunE: prMerge,
}
func prStatus(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
apiClient, err := apiClientForContext(ctx)
@ -100,6 +111,7 @@ func prStatus(cmd *cobra.Command, args []string) error {
repoOverride, _ := cmd.Flags().GetString("repo")
currentPRNumber, currentPRHeadRef, err := prSelectorForCurrentBranch(ctx, baseRepo)
if err != nil && repoOverride == "" && err.Error() != "git: not on any branch" {
return fmt.Errorf("could not query for pull request for current branch: %w", err)
}
@ -419,6 +431,75 @@ func prReopen(cmd *cobra.Command, args []string) error {
return nil
}
func prMerge(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
}
var pr *api.PullRequest
if len(args) > 0 {
pr, err = prFromArg(apiClient, baseRepo, args[0])
if err != nil {
return err
}
} else {
prNumber, branchWithOwner, err := prSelectorForCurrentBranch(ctx, baseRepo)
if err != nil {
return err
}
if prNumber != 0 {
pr, err = api.PullRequestByNumber(apiClient, baseRepo, prNumber)
} else {
pr, err = api.PullRequestForBranch(apiClient, baseRepo, "", branchWithOwner)
}
if err != nil {
return err
}
}
if pr.State == "MERGED" {
err := fmt.Errorf("%s Pull request #%d was already merged", utils.Red("!"), pr.Number)
return err
}
rebase, err := cmd.Flags().GetBool("rebase")
if err != nil {
return err
}
squash, err := cmd.Flags().GetBool("squash")
if err != nil {
return err
}
var output string
if rebase {
output = fmt.Sprintf("%s Rebased and merged pull request #%d\n", utils.Green("✔"), pr.Number)
err = api.PullRequestMerge(apiClient, baseRepo, pr, api.PullRequestMergeMethodRebase)
} else if squash {
output = fmt.Sprintf("%s Squashed and merged pull request #%d\n", utils.Green("✔"), pr.Number)
err = api.PullRequestMerge(apiClient, baseRepo, pr, api.PullRequestMergeMethodSquash)
} else {
output = fmt.Sprintf("%s Merged pull request #%d\n", utils.Green("✔"), pr.Number)
err = api.PullRequestMerge(apiClient, baseRepo, pr, api.PullRequestMergeMethodMerge)
}
if err != nil {
return fmt.Errorf("API call failed: %w", err)
}
fmt.Fprint(colorableOut(cmd), output)
return nil
}
func printPrPreview(out io.Writer, pr *api.PullRequest) error {
// Header (Title and State)
fmt.Fprintln(out, utils.Bold(pr.Title))

View file

@ -172,6 +172,7 @@ func TestPRCreate_metadata(t *testing.T) {
eq(t, inputs["pullRequestId"], "NEWPULLID")
eq(t, inputs["userIds"], []interface{}{"HUBOTID", "MONAID"})
eq(t, inputs["teamIds"], []interface{}{"COREID", "ROBOTID"})
eq(t, inputs["union"], true)
}))
cs, cmdTeardown := test.InitCmdStubber()

View file

@ -6,13 +6,16 @@ import (
"strconv"
"strings"
"github.com/cli/cli/api"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
"github.com/cli/cli/api"
"github.com/cli/cli/pkg/surveyext"
"github.com/cli/cli/utils"
)
func init() {
// TODO re-register post release
// prCmd.AddCommand(prReviewCmd)
prCmd.AddCommand(prReviewCmd)
prReviewCmd.Flags().BoolP("approve", "a", false, "Approve pull request")
prReviewCmd.Flags().BoolP("request-changes", "r", false, "Request changes on a pull request")
@ -28,6 +31,8 @@ var prReviewCmd = &cobra.Command{
Examples:
gh pr review # add a review for the current branch's pull request
gh pr review 123 # add a review for pull request 123
gh pr review -a # mark the current branch's pull request as approved
gh pr review -c -b "interesting" # comment on the current branch's pull request
gh pr review 123 -r -b "needs more ascii art" # request changes on pull request 123
@ -56,15 +61,19 @@ func processReviewOpt(cmd *cobra.Command) (*api.PullRequestReviewInput, error) {
state = api.ReviewComment
}
if found != 1 {
return nil, errors.New("need exactly one of --approve, --request-changes, or --comment")
}
body, err := cmd.Flags().GetString("body")
if err != nil {
return nil, err
}
if found == 0 && body == "" {
return nil, nil // signal interactive mode
} else if found == 0 && body != "" {
return nil, errors.New("--body unsupported without --approve, --request-changes, or --comment")
} else if found > 1 {
return nil, errors.New("need exactly one of --approve, --request-changes, or --comment")
}
if (flag == "request-changes" || flag == "comment") && body == "" {
return nil, fmt.Errorf("body cannot be blank for %s review", flag)
}
@ -108,7 +117,7 @@ func prReview(cmd *cobra.Command, args []string) error {
}
}
input, err := processReviewOpt(cmd)
reviewData, err := processReviewOpt(cmd)
if err != nil {
return fmt.Errorf("did not understand desired review action: %w", err)
}
@ -124,12 +133,144 @@ func prReview(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("could not find pull request: %w", err)
}
prNum = pr.Number
}
err = api.AddReview(apiClient, pr, input)
out := colorableOut(cmd)
if reviewData == nil {
reviewData, err = reviewSurvey(cmd)
if err != nil {
return err
}
if reviewData == nil && err == nil {
fmt.Fprint(out, "Discarding.\n")
return nil
}
}
err = api.AddReview(apiClient, pr, reviewData)
if err != nil {
return fmt.Errorf("failed to create review: %w", err)
}
switch reviewData.State {
case api.ReviewComment:
fmt.Fprintf(out, "%s Reviewed pull request #%d\n", utils.Gray("-"), prNum)
case api.ReviewApprove:
fmt.Fprintf(out, "%s Approved pull request #%d\n", utils.Green("✓"), prNum)
case api.ReviewRequestChanges:
fmt.Fprintf(out, "%s Requested changes to pull request #%d\n", utils.Red("+"), prNum)
}
return nil
}
func reviewSurvey(cmd *cobra.Command) (*api.PullRequestReviewInput, error) {
editorCommand, err := determineEditor(cmd)
if err != nil {
return nil, err
}
typeAnswers := struct {
ReviewType string
}{}
typeQs := []*survey.Question{
{
Name: "reviewType",
Prompt: &survey.Select{
Message: "What kind of review do you want to give?",
Options: []string{
"Comment",
"Approve",
"Request changes",
},
},
},
}
err = SurveyAsk(typeQs, &typeAnswers)
if err != nil {
return nil, err
}
var reviewState api.PullRequestReviewState
switch typeAnswers.ReviewType {
case "Approve":
reviewState = api.ReviewApprove
case "Request changes":
reviewState = api.ReviewRequestChanges
case "Comment":
reviewState = api.ReviewComment
default:
panic("unreachable state")
}
bodyAnswers := struct {
Body string
}{}
blankAllowed := false
if reviewState == api.ReviewApprove {
blankAllowed = true
}
bodyQs := []*survey.Question{
&survey.Question{
Name: "body",
Prompt: &surveyext.GhEditor{
BlankAllowed: blankAllowed,
EditorCommand: editorCommand,
Editor: &survey.Editor{
Message: "Review body",
FileName: "*.md",
},
},
},
}
err = SurveyAsk(bodyQs, &bodyAnswers)
if err != nil {
return nil, err
}
if bodyAnswers.Body == "" && (reviewState == api.ReviewComment || reviewState == api.ReviewRequestChanges) {
return nil, errors.New("this type of review cannot be blank")
}
if len(bodyAnswers.Body) > 0 {
out := colorableOut(cmd)
renderedBody, err := utils.RenderMarkdown(bodyAnswers.Body)
if err != nil {
return nil, err
}
fmt.Fprintf(out, "Got:\n%s", renderedBody)
}
confirm := false
confirmQs := []*survey.Question{
{
Name: "confirm",
Prompt: &survey.Confirm{
Message: "Submit?",
Default: true,
},
},
}
err = SurveyAsk(confirmQs, &confirm)
if err != nil {
return nil, err
}
if !confirm {
return nil, nil
}
return &api.PullRequestReviewInput{
Body: bodyAnswers.Body,
State: reviewState,
}, nil
}

View file

@ -4,26 +4,40 @@ import (
"bytes"
"encoding/json"
"io/ioutil"
"regexp"
"testing"
"github.com/cli/cli/test"
)
func TestPRReview_validation(t *testing.T) {
t.Skip("skipping until release is done")
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
for _, cmd := range []string{
`pr review 123`,
`pr review --approve --comment 123`,
`pr review --approve --comment -b"hey" 123`,
} {
http.StubRepoResponse("OWNER", "REPO")
_, err := RunCommand(cmd)
if err == nil {
t.Fatal("expected error")
}
eq(t, err.Error(), "did not understand desired review action: need exactly one of --approve, --request-changes, or --comment")
}
}
func TestPRReview_bad_body(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
_, err := RunCommand(`pr review -b "radical"`)
if err == nil {
t.Fatal("expected error")
}
eq(t, err.Error(), "did not understand desired review action: --body unsupported without --approve, --request-changes, or --comment")
}
func TestPRReview_url_arg(t *testing.T) {
t.Skip("skipping until release is done")
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -46,11 +60,13 @@ func TestPRReview_url_arg(t *testing.T) {
} } } } `))
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
_, err := RunCommand("pr review --approve https://github.com/OWNER/REPO/pull/123")
output, err := RunCommand("pr review --approve https://github.com/OWNER/REPO/pull/123")
if err != nil {
t.Fatalf("error running pr review: %s", err)
}
test.ExpectLines(t, output.String(), "Approved pull request #123")
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
reqBody := struct {
Variables struct {
@ -69,7 +85,6 @@ func TestPRReview_url_arg(t *testing.T) {
}
func TestPRReview_number_arg(t *testing.T) {
t.Skip("skipping until release is done")
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -92,11 +107,13 @@ func TestPRReview_number_arg(t *testing.T) {
} } } } `))
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
_, err := RunCommand("pr review --approve 123")
output, err := RunCommand("pr review --approve 123")
if err != nil {
t.Fatalf("error running pr review: %s", err)
}
test.ExpectLines(t, output.String(), "Approved pull request #123")
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
reqBody := struct {
Variables struct {
@ -115,24 +132,26 @@ func TestPRReview_number_arg(t *testing.T) {
}
func TestPRReview_no_arg(t *testing.T) {
t.Skip("skipping until release is done")
initBlankContext("", "OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequests": { "nodes": [
{ "url": "https://github.com/OWNER/REPO/pull/123",
"number": 123,
"id": "foobar123",
"headRefName": "feature",
"baseRefName": "master" }
] } } } }`))
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
_, err := RunCommand(`pr review --comment -b "cool story"`)
output, err := RunCommand(`pr review --comment -b "cool story"`)
if err != nil {
t.Fatalf("error running pr review: %s", err)
}
test.ExpectLines(t, output.String(), "- Reviewed pull request #123")
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
reqBody := struct {
Variables struct {
@ -151,7 +170,6 @@ func TestPRReview_no_arg(t *testing.T) {
}
func TestPRReview_blank_comment(t *testing.T) {
t.Skip("skipping until release is done")
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -161,7 +179,6 @@ func TestPRReview_blank_comment(t *testing.T) {
}
func TestPRReview_blank_request_changes(t *testing.T) {
t.Skip("skipping until release is done")
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -171,7 +188,6 @@ func TestPRReview_blank_request_changes(t *testing.T) {
}
func TestPRReview(t *testing.T) {
t.Skip("skipping until release is done")
type c struct {
Cmd string
ExpectedEvent string
@ -218,3 +234,170 @@ func TestPRReview(t *testing.T) {
eq(t, reqBody.Variables.Input.Body, kase.ExpectedBody)
}
}
func TestPRReview_interactive(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequests": { "nodes": [
{ "url": "https://github.com/OWNER/REPO/pull/123",
"number": 123,
"id": "foobar123",
"headRefName": "feature",
"baseRefName": "master" }
] } } } }
`))
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
as, teardown := initAskStubber()
defer teardown()
as.Stub([]*QuestionStub{
{
Name: "reviewType",
Value: "Approve",
},
})
as.Stub([]*QuestionStub{
{
Name: "body",
Value: "cool story",
},
})
as.Stub([]*QuestionStub{
{
Name: "confirm",
Value: true,
},
})
output, err := RunCommand(`pr review`)
if err != nil {
t.Fatalf("got unexpected error running pr review: %s", err)
}
test.ExpectLines(t, output.String(),
"Approved pull request #123",
"Got:",
"cool.*story") // weird because markdown rendering puts a bunch of junk between works
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
reqBody := struct {
Variables struct {
Input struct {
Event string
Body string
}
}
}{}
_ = json.Unmarshal(bodyBytes, &reqBody)
eq(t, reqBody.Variables.Input.Event, "APPROVE")
eq(t, reqBody.Variables.Input.Body, "cool story")
}
func TestPRReview_interactive_no_body(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequests": { "nodes": [
{ "url": "https://github.com/OWNER/REPO/pull/123",
"id": "foobar123",
"headRefName": "feature",
"baseRefName": "master" }
] } } } }
`))
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
as, teardown := initAskStubber()
defer teardown()
as.Stub([]*QuestionStub{
{
Name: "reviewType",
Value: "Request changes",
},
})
as.Stub([]*QuestionStub{
{
Name: "body",
Default: true,
},
})
as.Stub([]*QuestionStub{
{
Name: "confirm",
Value: true,
},
})
_, err := RunCommand(`pr review`)
if err == nil {
t.Fatal("expected error")
}
eq(t, err.Error(), "this type of review cannot be blank")
}
func TestPRReview_interactive_blank_approve(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequests": { "nodes": [
{ "url": "https://github.com/OWNER/REPO/pull/123",
"number": 123,
"id": "foobar123",
"headRefName": "feature",
"baseRefName": "master" }
] } } } }
`))
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
as, teardown := initAskStubber()
defer teardown()
as.Stub([]*QuestionStub{
{
Name: "reviewType",
Value: "Approve",
},
})
as.Stub([]*QuestionStub{
{
Name: "body",
Default: true,
},
})
as.Stub([]*QuestionStub{
{
Name: "confirm",
Value: true,
},
})
output, err := RunCommand(`pr review`)
if err != nil {
t.Fatalf("got unexpected error running pr review: %s", err)
}
unexpect := regexp.MustCompile("Got:")
if unexpect.MatchString(output.String()) {
t.Errorf("did not expect to see body printed in %s", output.String())
}
test.ExpectLines(t, output.String(), "Approved pull request #123")
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
reqBody := struct {
Variables struct {
Input struct {
Event string
Body string
}
}
}{}
_ = json.Unmarshal(bodyBytes, &reqBody)
eq(t, reqBody.Variables.Input.Event, "APPROVE")
eq(t, reqBody.Variables.Input.Body, "")
}

View file

@ -3,6 +3,7 @@ package command
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"os"
"os/exec"
@ -970,5 +971,125 @@ func TestPRReopen_alreadyMerged(t *testing.T) {
if !r.MatchString(err.Error()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}
type stubResponse struct {
ResponseCode int
ResponseBody io.Reader
}
func initWithStubs(branch string, stubs ...stubResponse) {
initBlankContext("", "OWNER/REPO", branch)
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
for _, s := range stubs {
http.StubResponse(s.ResponseCode, s.ResponseBody)
}
}
func TestPrMerge(t *testing.T) {
initWithStubs("master",
stubResponse{200, bytes.NewBufferString(`{ "data": { "repository": {
"pullRequest": { "number": 1, "closed": false, "state": "OPEN"}
} } }`)},
stubResponse{200, bytes.NewBufferString(`{"id": "THE-ID"}`)},
)
output, err := RunCommand("pr merge 1")
if err != nil {
t.Fatalf("error running command `pr merge`: %v", err)
}
r := regexp.MustCompile(`Merged pull request #1`)
if !r.MatchString(output.String()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}
func TestPrMerge_noPrNumberGiven(t *testing.T) {
cs, cmdTeardown := test.InitCmdStubber()
defer cmdTeardown()
cs.Stub("branch.blueberries.remote origin\nbranch.blueberries.merge refs/heads/blueberries") // git config --get-regexp ^branch\.master\.(remote|merge)
jsonFile, _ := os.Open("../test/fixtures/prViewPreviewWithMetadataByBranch.json")
defer jsonFile.Close()
initWithStubs("blueberries",
stubResponse{200, jsonFile},
stubResponse{200, bytes.NewBufferString(`{"id": "THE-ID"}`)},
)
output, err := RunCommand("pr merge")
if err != nil {
t.Fatalf("error running command `pr merge`: %v", err)
}
r := regexp.MustCompile(`Merged pull request #10`)
if !r.MatchString(output.String()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}
func TestPrMerge_rebase(t *testing.T) {
initWithStubs("master",
stubResponse{200, bytes.NewBufferString(`{ "data": { "repository": {
"pullRequest": { "number": 2, "closed": false, "state": "OPEN"}
} } }`)},
stubResponse{200, bytes.NewBufferString(`{"id": "THE-ID"}`)},
)
output, err := RunCommand("pr merge 2 --rebase")
if err != nil {
t.Fatalf("error running command `pr merge`: %v", err)
}
r := regexp.MustCompile(`Rebased and merged pull request #2`)
if !r.MatchString(output.String()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}
func TestPrMerge_squash(t *testing.T) {
initWithStubs("master",
stubResponse{200, bytes.NewBufferString(`{ "data": { "repository": {
"pullRequest": { "number": 3, "closed": false, "state": "OPEN"}
} } }`)},
stubResponse{200, bytes.NewBufferString(`{"id": "THE-ID"}`)},
)
output, err := RunCommand("pr merge 3 --squash")
if err != nil {
t.Fatalf("error running command `pr merge`: %v", err)
}
r := regexp.MustCompile(`Squashed and merged pull request #3`)
if !r.MatchString(output.String()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}
func TestPrMerge_alreadyMerged(t *testing.T) {
initWithStubs("master",
stubResponse{200, bytes.NewBufferString(`{ "data": { "repository": {
"pullRequest": { "number": 4, "closed": true, "state": "MERGED"}
} } }`)},
stubResponse{200, bytes.NewBufferString(`{"id": "THE-ID"}`)},
)
output, err := RunCommand("pr merge 4")
if err == nil {
t.Fatalf("expected an error running command `pr merge`: %v", err)
}
r := regexp.MustCompile(`Pull request #4 was already merged`)
if !r.MatchString(err.Error()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}

View file

@ -338,3 +338,17 @@ func formatRemoteURL(cmd *cobra.Command, fullRepoName string) string {
return fmt.Sprintf("https://%s/%s.git", defaultHostname, fullRepoName)
}
func determineEditor(cmd *cobra.Command) (string, error) {
editorCommand := os.Getenv("GH_EDITOR")
if editorCommand == "" {
ctx := contextForCommand(cmd)
cfg, err := ctx.Config()
if err != nil {
return "", fmt.Errorf("could not read config: %w", err)
}
editorCommand, _ = cfg.Get(defaultHostname, "editor")
}
return editorCommand, nil
}

View file

@ -2,7 +2,6 @@ package command
import (
"fmt"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/api"
@ -129,14 +128,9 @@ func selectTemplate(templatePaths []string) (string, error) {
}
func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClient *api.Client, repo ghrepo.Interface, providedTitle, providedBody string, defs defaults, templatePaths []string, allowReviewers, allowMetadata bool) error {
editorCommand := os.Getenv("GH_EDITOR")
if editorCommand == "" {
ctx := contextForCommand(cmd)
cfg, err := ctx.Config()
if err != nil {
return fmt.Errorf("could not read config: %w", err)
}
editorCommand, _ = cfg.Get(defaultHostname, "editor")
editorCommand, err := determineEditor(cmd)
if err != nil {
return err
}
issueState.Title = defs.Title
@ -184,7 +178,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie
qs = append(qs, bodyQuestion)
}
err := SurveyAsk(qs, issueState)
err = SurveyAsk(qs, issueState)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}

View file

@ -38,6 +38,7 @@ func init() {
type GhEditor struct {
*survey.Editor
EditorCommand string
BlankAllowed bool
}
func (e *GhEditor) editorCommand() string {
@ -58,13 +59,14 @@ var EditorQuestionTemplate = `
{{- else }}
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}
{{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
{{- color "cyan"}}[(e) to launch {{ .EditorCommand }}, enter to skip] {{color "reset"}}
{{- color "cyan"}}[(e) to launch {{ .EditorCommand }}{{- if .BlankAllowed }}, enter to skip{{ end }}] {{color "reset"}}
{{- end}}`
// EXTENDED to pass editor name (to use in prompt)
type EditorTemplateData struct {
survey.Editor
EditorCommand string
BlankAllowed bool
Answer string
ShowAnswer bool
ShowHelp bool
@ -75,9 +77,10 @@ type EditorTemplateData struct {
func (e *GhEditor) prompt(initialValue string, config *survey.PromptConfig) (interface{}, error) {
err := e.Render(
EditorQuestionTemplate,
// EXTENDED to support printing editor in prompt
// EXTENDED to support printing editor in prompt and BlankAllowed
EditorTemplateData{
Editor: *e.Editor,
BlankAllowed: e.BlankAllowed,
EditorCommand: filepath.Base(e.editorCommand()),
Config: config,
},
@ -96,7 +99,7 @@ func (e *GhEditor) prompt(initialValue string, config *survey.PromptConfig) (int
defer cursor.Show()
for {
// EXTENDED to handle the e to edit / enter to skip behavior
// EXTENDED to handle the e to edit / enter to skip behavior + BlankAllowed
r, _, err := rr.ReadRune()
if err != nil {
return "", err
@ -105,7 +108,11 @@ func (e *GhEditor) prompt(initialValue string, config *survey.PromptConfig) (int
break
}
if r == '\r' || r == '\n' {
return "", nil
if e.BlankAllowed {
return "", nil
} else {
continue
}
}
if r == terminal.KeyInterrupt {
return "", terminal.InterruptErr
@ -117,8 +124,9 @@ func (e *GhEditor) prompt(initialValue string, config *survey.PromptConfig) (int
err = e.Render(
EditorQuestionTemplate,
EditorTemplateData{
// EXTENDED to support printing editor in prompt
// EXTENDED to support printing editor in prompt, BlankAllowed
Editor: *e.Editor,
BlankAllowed: e.BlankAllowed,
EditorCommand: filepath.Base(e.editorCommand()),
ShowHelp: true,
Config: config,