Merge remote-tracking branch 'origin/master' into check-if-pr-exists

This commit is contained in:
Mislav Marohnić 2020-03-18 12:04:08 +01:00
commit 23dfeff84f
14 changed files with 616 additions and 228 deletions

View file

@ -14,7 +14,6 @@ import (
"github.com/cli/cli/git"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/githubtemplate"
"github.com/cli/cli/pkg/text"
"github.com/cli/cli/utils"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@ -23,7 +22,6 @@ import (
func init() {
RootCmd.AddCommand(issueCmd)
issueCmd.AddCommand(issueStatusCmd)
issueCmd.AddCommand(issueViewCmd)
issueCmd.AddCommand(issueCreateCmd)
issueCreateCmd.Flags().StringP("title", "t", "",
@ -39,6 +37,7 @@ func init() {
issueListCmd.Flags().IntP("limit", "L", 30, "Maximum number of issues to fetch")
issueListCmd.Flags().StringP("author", "A", "", "Filter by author")
issueCmd.AddCommand(issueViewCmd)
issueViewCmd.Flags().BoolP("preview", "p", false, "Display preview of issue content")
}
@ -67,7 +66,7 @@ var issueStatusCmd = &cobra.Command{
RunE: issueStatus,
}
var issueViewCmd = &cobra.Command{
Use: "view {<number> | <url> | <branch>}",
Use: "view {<number> | <url>}",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return FlagError{errors.New("issue number or URL required as argument")}
@ -138,23 +137,7 @@ func issueList(cmd *cobra.Command, args []string) error {
}
out := cmd.OutOrStdout()
table := utils.NewTablePrinter(out)
for _, issue := range issues {
issueNum := strconv.Itoa(issue.Number)
if table.IsTTY() {
issueNum = "#" + issueNum
}
labels := labelList(issue)
if labels != "" && table.IsTTY() {
labels = fmt.Sprintf("(%s)", labels)
}
table.AddField(issueNum, nil, colorFuncForState(issue.State))
table.AddField(replaceExcessiveWhitespace(issue.Title), nil, nil)
table.AddField(labels, nil, utils.Gray)
table.EndRow()
}
table.Render()
printIssues(out, "", len(issues), issues)
return nil
}
@ -358,7 +341,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
interactive := title == "" || body == ""
if interactive {
tb, err := titleBodySurvey(cmd, title, body, templateFiles)
tb, err := titleBodySurvey(cmd, title, body, defaults{}, templateFiles)
if err != nil {
return fmt.Errorf("could not collect title and/or body: %w", err)
}
@ -409,21 +392,26 @@ func issueCreate(cmd *cobra.Command, args []string) error {
}
func printIssues(w io.Writer, prefix string, totalCount int, issues []api.Issue) {
table := utils.NewTablePrinter(w)
for _, issue := range issues {
number := utils.Green("#" + strconv.Itoa(issue.Number))
coloredLabels := labelList(issue)
if coloredLabels != "" {
coloredLabels = utils.Gray(fmt.Sprintf(" (%s)", coloredLabels))
issueNum := strconv.Itoa(issue.Number)
if table.IsTTY() {
issueNum = "#" + issueNum
}
issueNum = prefix + issueNum
labels := labelList(issue)
if labels != "" && table.IsTTY() {
labels = fmt.Sprintf("(%s)", labels)
}
now := time.Now()
ago := now.Sub(issue.UpdatedAt)
fmt.Fprintf(w, "%s%s %s%s %s\n", prefix, number,
text.Truncate(70, replaceExcessiveWhitespace(issue.Title)),
coloredLabels,
utils.Gray(utils.FuzzyAgo(ago)))
table.AddField(issueNum, nil, colorFuncForState(issue.State))
table.AddField(replaceExcessiveWhitespace(issue.Title), nil, nil)
table.AddField(labels, nil, utils.Gray)
table.AddField(utils.FuzzyAgo(ago), nil, utils.Gray)
table.EndRow()
}
table.Render()
remaining := totalCount - len(issues)
if remaining > 0 {
fmt.Fprintf(w, utils.Gray("%sAnd %d more\n"), prefix, remaining)

View file

@ -10,6 +10,7 @@ import (
"strings"
"testing"
"github.com/cli/cli/test"
"github.com/cli/cli/utils"
)
@ -28,10 +29,10 @@ func TestIssueStatus(t *testing.T) {
}
expectedIssues := []*regexp.Regexp{
regexp.MustCompile(`#8.*carrots`),
regexp.MustCompile(`#9.*squash`),
regexp.MustCompile(`#10.*broccoli`),
regexp.MustCompile(`#11.*swiss chard`),
regexp.MustCompile(`(?m)8.*carrots.*about.*ago`),
regexp.MustCompile(`(?m)9.*squash.*about.*ago`),
regexp.MustCompile(`(?m)10.*broccoli.*about.*ago`),
regexp.MustCompile(`(?m)11.*swiss chard.*about.*ago`),
}
for _, r := range expectedIssues {
@ -226,7 +227,7 @@ func TestIssueView(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -260,7 +261,7 @@ func TestIssueView_numberArgWithHash(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -383,7 +384,7 @@ func TestIssueView_notFound(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -430,7 +431,7 @@ func TestIssueView_urlArg(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -515,7 +516,7 @@ func TestIssueCreate_web(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -541,7 +542,7 @@ func TestIssueCreate_webTitleBody(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()

View file

@ -7,6 +7,7 @@ import (
"testing"
"github.com/cli/cli/context"
"github.com/cli/cli/test"
"github.com/cli/cli/utils"
)
@ -46,7 +47,7 @@ func TestPRCheckout_sameRepo(t *testing.T) {
return &errorStub{"exit status: 1"}
default:
ranCommands = append(ranCommands, cmd.Args)
return &outputStub{}
return &test.OutputStub{}
}
})
defer restoreCmd()
@ -98,7 +99,7 @@ func TestPRCheckout_urlArg(t *testing.T) {
return &errorStub{"exit status: 1"}
default:
ranCommands = append(ranCommands, cmd.Args)
return &outputStub{}
return &test.OutputStub{}
}
})
defer restoreCmd()
@ -147,7 +148,7 @@ func TestPRCheckout_branchArg(t *testing.T) {
return &errorStub{"exit status: 1"}
default:
ranCommands = append(ranCommands, cmd.Args)
return &outputStub{}
return &test.OutputStub{}
}
})
defer restoreCmd()
@ -193,10 +194,10 @@ func TestPRCheckout_existingBranch(t *testing.T) {
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case "git show-ref --verify --quiet refs/heads/feature":
return &outputStub{}
return &test.OutputStub{}
default:
ranCommands = append(ranCommands, cmd.Args)
return &outputStub{}
return &test.OutputStub{}
}
})
defer restoreCmd()
@ -248,7 +249,7 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
return &errorStub{"exit status: 1"}
default:
ranCommands = append(ranCommands, cmd.Args)
return &outputStub{}
return &test.OutputStub{}
}
})
defer restoreCmd()
@ -300,7 +301,7 @@ func TestPRCheckout_differentRepo(t *testing.T) {
return &errorStub{"exit status 1"}
default:
ranCommands = append(ranCommands, cmd.Args)
return &outputStub{}
return &test.OutputStub{}
}
})
defer restoreCmd()
@ -349,10 +350,10 @@ func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case "git config branch.feature.merge":
return &outputStub{[]byte("refs/heads/feature\n")}
return &test.OutputStub{[]byte("refs/heads/feature\n")}
default:
ranCommands = append(ranCommands, cmd.Args)
return &outputStub{}
return &test.OutputStub{}
}
})
defer restoreCmd()
@ -399,10 +400,10 @@ func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case "git config branch.feature.merge":
return &outputStub{[]byte("refs/heads/feature\n")}
return &test.OutputStub{[]byte("refs/heads/feature\n")}
default:
ranCommands = append(ranCommands, cmd.Args)
return &outputStub{}
return &test.OutputStub{}
}
})
defer restoreCmd()
@ -452,7 +453,7 @@ func TestPRCheckout_maintainerCanModify(t *testing.T) {
return &errorStub{"exit status 1"}
default:
ranCommands = append(ranCommands, cmd.Args)
return &outputStub{}
return &test.OutputStub{}
}
})
defer restoreCmd()

View file

@ -15,6 +15,39 @@ import (
"github.com/spf13/cobra"
)
type defaults struct {
Title string
Body string
}
func computeDefaults(baseRef, headRef string) (defaults, error) {
commits, err := git.Commits(baseRef, headRef)
if err != nil {
return defaults{}, err
}
out := defaults{}
if len(commits) == 1 {
out.Title = commits[0].Title
body, err := git.CommitBody(commits[0].Sha)
if err != nil {
return defaults{}, err
}
out.Body = body
} else {
out.Title = utils.Humanize(headRef)
body := ""
for _, c := range commits {
body += fmt.Sprintf("- %s\n", c.Title)
}
out.Body = body
}
return out, nil
}
func prCreate(cmd *cobra.Command, _ []string) error {
ctx := contextForCommand(cmd)
remotes, err := ctx.Remotes()
@ -73,30 +106,50 @@ func prCreate(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("could not parse body: %w", err)
}
defs, defaultsErr := computeDefaults(baseBranch, headBranch)
isWeb, err := cmd.Flags().GetBool("web")
if err != nil {
return fmt.Errorf("could not parse web: %q", err)
}
autofill, err := cmd.Flags().GetBool("fill")
if err != nil {
return fmt.Errorf("could not parse fill: %q", err)
}
action := SubmitAction
if isWeb {
action = PreviewAction
if (title == "" || body == "") && defaultsErr != nil {
return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr)
}
} else if autofill {
if defaultsErr != nil {
return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr)
}
action = SubmitAction
title = defs.Title
body = defs.Body
} else {
fmt.Fprintf(colorableErr(cmd), "\nCreating pull request for %s into %s in %s\n\n",
utils.Cyan(headBranch),
utils.Cyan(baseBranch),
ghrepo.FullName(baseRepo))
if (title == "" || body == "") && defaultsErr != nil {
fmt.Fprintf(colorableErr(cmd), "%s warning: could not compute title or body defaults: %s\n", utils.Yellow("!"), defaultsErr)
}
}
// TODO: only drop into interactive mode if stdin & stdout are a tty
if !isWeb && (title == "" || body == "") {
if !isWeb && !autofill && (title == "" || body == "") {
var templateFiles []string
if rootDir, err := git.ToplevelDir(); err == nil {
// TODO: figure out how to stub this in tests
templateFiles = githubtemplate.Find(rootDir, "PULL_REQUEST_TEMPLATE")
}
tb, err := titleBodySurvey(cmd, title, body, templateFiles)
tb, err := titleBodySurvey(cmd, title, body, defs, templateFiles)
if err != nil {
return fmt.Errorf("could not collect title and/or body: %w", err)
}
@ -243,4 +296,5 @@ func init() {
prCreateCmd.Flags().StringP("base", "B", "",
"The branch into which you want your code merged")
prCreateCmd.Flags().BoolP("web", "w", false, "Open the web browser to create a pull request")
prCreateCmd.Flags().BoolP("fill", "f", false, "Do not prompt for title/body and just use commit info")
}

View file

@ -3,43 +3,13 @@ package command
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
"testing"
"github.com/cli/cli/context"
"github.com/cli/cli/git"
"github.com/cli/cli/test"
"github.com/cli/cli/utils"
)
func TestPrCreateHelperProcess(*testing.T) {
if test.SkipTestHelperProcess() {
return
}
args := test.GetTestHelperProcessArgs()
switch args[1] {
case "status":
switch args[0] {
case "clean":
case "dirty":
fmt.Println(" M git/git.go")
default:
fmt.Fprintf(os.Stderr, "unknown scenario: %q", args[0])
os.Exit(1)
}
case "push":
default:
fmt.Fprintf(os.Stderr, "unknown command: %q", args[1])
os.Exit(1)
}
os.Exit(0)
}
func TestPRCreate(t *testing.T) {
initBlankContext("OWNER/REPO", "feature")
http := initFakeHTTP()
@ -50,11 +20,12 @@ func TestPRCreate(t *testing.T) {
} } } }
`))
origGitCommand := git.GitCommand
git.GitCommand = test.StubExecCommand("TestPrCreateHelperProcess", "clean")
defer func() {
git.GitCommand = origGitCommand
}()
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push
output, err := RunCommand(prCreateCmd, `pr create -t "my title" -b "my body"`)
eq(t, err, nil)
@ -87,12 +58,13 @@ func TestPRCreate_web(t *testing.T) {
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
ranCommands := [][]string{}
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
ranCommands = append(ranCommands, cmd.Args)
return &outputStub{}
})
defer restoreCmd()
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push
cs.Stub("") // browser
output, err := RunCommand(prCreateCmd, `pr create --web`)
eq(t, err, nil)
@ -100,9 +72,10 @@ func TestPRCreate_web(t *testing.T) {
eq(t, output.String(), "")
eq(t, output.Stderr(), "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n")
eq(t, len(ranCommands), 3)
eq(t, strings.Join(ranCommands[1], " "), "git push --set-upstream origin HEAD:feature")
eq(t, ranCommands[2][len(ranCommands[2])-1], "https://github.com/OWNER/REPO/compare/master...feature?expand=1")
eq(t, len(cs.Calls), 4)
eq(t, strings.Join(cs.Calls[2].Args, " "), "git push --set-upstream origin HEAD:feature")
browserCall := cs.Calls[3].Args
eq(t, browserCall[len(browserCall)-1], "https://github.com/OWNER/REPO/compare/master...feature?expand=1")
}
func TestPRCreate_ReportsUncommittedChanges(t *testing.T) {
@ -116,11 +89,12 @@ func TestPRCreate_ReportsUncommittedChanges(t *testing.T) {
} } } }
`))
origGitCommand := git.GitCommand
git.GitCommand = test.StubExecCommand("TestPrCreateHelperProcess", "dirty")
defer func() {
git.GitCommand = origGitCommand
}()
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub(" M git/git.go") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push
output, err := RunCommand(prCreateCmd, `pr create -t "my title" -b "my body"`)
eq(t, err, nil)
@ -181,11 +155,12 @@ func TestPRCreate_cross_repo_same_branch(t *testing.T) {
} } } }
`))
origGitCommand := git.GitCommand
git.GitCommand = test.StubExecCommand("TestPrCreateHelperProcess", "clean")
defer func() {
git.GitCommand = origGitCommand
}()
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push
output, err := RunCommand(prCreateCmd, `pr create -t "cross repo" -b "same branch"`)
eq(t, err, nil)
@ -214,3 +189,262 @@ func TestPRCreate_cross_repo_same_branch(t *testing.T) {
// goal: only care that gql is formatted properly
}
func TestPRCreate_survey_defaults_multicommit(t *testing.T) {
initBlankContext("OWNER/REPO", "cool_bug-fixes")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "createPullRequest": { "pullRequest": {
"URL": "https://github.com/OWNER/REPO/pull/12"
} } } }
`))
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git rev-parse
cs.Stub("") // git push
as, surveyTeardown := initAskStubber()
defer surveyTeardown()
as.Stub([]*QuestionStub{
&QuestionStub{
Name: "title",
Default: true,
},
&QuestionStub{
Name: "body",
Default: true,
},
})
as.Stub([]*QuestionStub{
&QuestionStub{
Name: "confirmation",
Value: 1,
},
})
output, err := RunCommand(prCreateCmd, `pr create`)
eq(t, err, nil)
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
reqBody := struct {
Variables struct {
Input struct {
RepositoryID string
Title string
Body string
BaseRefName string
HeadRefName string
}
}
}{}
json.Unmarshal(bodyBytes, &reqBody)
expectedBody := "- commit 0\n- commit 1\n"
eq(t, reqBody.Variables.Input.RepositoryID, "REPOID")
eq(t, reqBody.Variables.Input.Title, "cool bug fixes")
eq(t, reqBody.Variables.Input.Body, expectedBody)
eq(t, reqBody.Variables.Input.BaseRefName, "master")
eq(t, reqBody.Variables.Input.HeadRefName, "cool_bug-fixes")
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
}
func TestPRCreate_survey_defaults_monocommit(t *testing.T) {
initBlankContext("OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "createPullRequest": { "pullRequest": {
"URL": "https://github.com/OWNER/REPO/pull/12"
} } } }
`))
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git status
cs.Stub("1234567890,the sky above the port") // git log
cs.Stub("was the color of a television, turned to a dead channel") // git show
cs.Stub("") // git rev-parse
cs.Stub("") // git push
as, surveyTeardown := initAskStubber()
defer surveyTeardown()
as.Stub([]*QuestionStub{
&QuestionStub{
Name: "title",
Default: true,
},
&QuestionStub{
Name: "body",
Default: true,
},
})
as.Stub([]*QuestionStub{
&QuestionStub{
Name: "confirmation",
Value: 1,
},
})
output, err := RunCommand(prCreateCmd, `pr create`)
eq(t, err, nil)
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
reqBody := struct {
Variables struct {
Input struct {
RepositoryID string
Title string
Body string
BaseRefName string
HeadRefName string
}
}
}{}
json.Unmarshal(bodyBytes, &reqBody)
expectedBody := "was the color of a television, turned to a dead channel"
eq(t, reqBody.Variables.Input.RepositoryID, "REPOID")
eq(t, reqBody.Variables.Input.Title, "the sky above the port")
eq(t, reqBody.Variables.Input.Body, expectedBody)
eq(t, reqBody.Variables.Input.BaseRefName, "master")
eq(t, reqBody.Variables.Input.HeadRefName, "feature")
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
}
func TestPRCreate_survey_autofill(t *testing.T) {
initBlankContext("OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "createPullRequest": { "pullRequest": {
"URL": "https://github.com/OWNER/REPO/pull/12"
} } } }
`))
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git status
cs.Stub("1234567890,the sky above the port") // git log
cs.Stub("was the color of a television, turned to a dead channel") // git show
cs.Stub("") // git rev-parse
cs.Stub("") // git push
cs.Stub("") // browser open
output, err := RunCommand(prCreateCmd, `pr create -f`)
eq(t, err, nil)
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
reqBody := struct {
Variables struct {
Input struct {
RepositoryID string
Title string
Body string
BaseRefName string
HeadRefName string
}
}
}{}
json.Unmarshal(bodyBytes, &reqBody)
expectedBody := "was the color of a television, turned to a dead channel"
eq(t, reqBody.Variables.Input.RepositoryID, "REPOID")
eq(t, reqBody.Variables.Input.Title, "the sky above the port")
eq(t, reqBody.Variables.Input.Body, expectedBody)
eq(t, reqBody.Variables.Input.BaseRefName, "master")
eq(t, reqBody.Variables.Input.HeadRefName, "feature")
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
}
func TestPRCreate_defaults_error_autofill(t *testing.T) {
initBlankContext("OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git status
cs.Stub("") // git log
_, err := RunCommand(prCreateCmd, "pr create -f")
eq(t, err.Error(), "could not compute title or body defaults: could not find any commits between master and feature")
}
func TestPRCreate_defaults_error_web(t *testing.T) {
initBlankContext("OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git status
cs.Stub("") // git log
_, err := RunCommand(prCreateCmd, "pr create -w")
eq(t, err.Error(), "could not compute title or body defaults: could not find any commits between master and feature")
}
func TestPRCreate_defaults_error_interactive(t *testing.T) {
initBlankContext("OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "createPullRequest": { "pullRequest": {
"URL": "https://github.com/OWNER/REPO/pull/12"
} } } }
`))
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git status
cs.Stub("") // git log
cs.Stub("") // git rev-parse
cs.Stub("") // git push
cs.Stub("") // browser open
as, surveyTeardown := initAskStubber()
defer surveyTeardown()
as.Stub([]*QuestionStub{
&QuestionStub{
Name: "title",
Default: true,
},
&QuestionStub{
Name: "body",
Value: "social distancing",
},
})
as.Stub([]*QuestionStub{
&QuestionStub{
Name: "confirmation",
Value: 0,
},
})
output, err := RunCommand(prCreateCmd, `pr create`)
eq(t, err, nil)
stderr := string(output.Stderr())
eq(t, strings.Contains(stderr, "warning: could not compute title or body defaults: could not find any commits"), true)
}

View file

@ -11,6 +11,7 @@ import (
"strings"
"testing"
"github.com/cli/cli/test"
"github.com/cli/cli/utils"
"github.com/google/shlex"
"github.com/spf13/cobra"
@ -110,7 +111,7 @@ func TestPRStatus_fork(t *testing.T) {
defer utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case `git config --get-regexp ^branch\.blueberries\.(remote|merge)$`:
return &outputStub{[]byte(`branch.blueberries.remote origin
return &test.OutputStub{[]byte(`branch.blueberries.remote origin
branch.blueberries.merge refs/heads/blueberries`)}
default:
panic("not implemented")
@ -421,7 +422,7 @@ func TestPRView_previewCurrentBranch(t *testing.T) {
http.StubResponse(200, jsonFile)
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -456,7 +457,7 @@ func TestPRView_previewCurrentBranchWithEmptyBody(t *testing.T) {
http.StubResponse(200, jsonFile)
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -493,10 +494,10 @@ func TestPRView_currentBranch(t *testing.T) {
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case `git config --get-regexp ^branch\.blueberries\.(remote|merge)$`:
return &outputStub{}
return &test.OutputStub{}
default:
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
}
})
defer restoreCmd()
@ -531,10 +532,10 @@ func TestPRView_noResultsForBranch(t *testing.T) {
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case `git config --get-regexp ^branch\.blueberries\.(remote|merge)$`:
return &outputStub{}
return &test.OutputStub{}
default:
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
}
})
defer restoreCmd()
@ -563,7 +564,7 @@ func TestPRView_numberArg(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -595,7 +596,7 @@ func TestPRView_numberArgWithHash(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -627,7 +628,7 @@ func TestPRView_urlArg(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -661,7 +662,7 @@ func TestPRView_branchArg(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -696,7 +697,7 @@ func TestPRView_branchWithOwnerArg(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()

View file

@ -11,6 +11,7 @@ import (
"time"
"github.com/cli/cli/context"
"github.com/cli/cli/test"
"github.com/cli/cli/utils"
)
@ -117,7 +118,7 @@ func TestRepoFork_in_parent_yes(t *testing.T) {
var seenCmds []*exec.Cmd
defer utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmds = append(seenCmds, cmd)
return &outputStub{}
return &test.OutputStub{}
})()
output, err := RunCommand(repoForkCmd, "repo fork --remote")
@ -156,7 +157,7 @@ func TestRepoFork_outside_yes(t *testing.T) {
var seenCmd *exec.Cmd
defer utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})()
output, err := RunCommand(repoForkCmd, "repo fork --clone OWNER/REPO")
@ -188,7 +189,7 @@ func TestRepoFork_outside_survey_yes(t *testing.T) {
var seenCmd *exec.Cmd
defer utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})()
oldConfirm := Confirm
@ -227,7 +228,7 @@ func TestRepoFork_outside_survey_no(t *testing.T) {
cmdRun := false
defer utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
cmdRun = true
return &outputStub{}
return &test.OutputStub{}
})()
oldConfirm := Confirm
@ -263,7 +264,7 @@ func TestRepoFork_in_parent_survey_yes(t *testing.T) {
var seenCmds []*exec.Cmd
defer utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmds = append(seenCmds, cmd)
return &outputStub{}
return &test.OutputStub{}
})()
oldConfirm := Confirm
@ -311,7 +312,7 @@ func TestRepoFork_in_parent_survey_no(t *testing.T) {
cmdRun := false
defer utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
cmdRun = true
return &outputStub{}
return &test.OutputStub{}
})()
oldConfirm := Confirm
@ -369,7 +370,7 @@ func TestRepoClone(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -409,7 +410,7 @@ func TestRepoCreate(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -474,7 +475,7 @@ func TestRepoCreate_org(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -539,7 +540,7 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -591,7 +592,7 @@ func TestRepoView(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -624,7 +625,7 @@ func TestRepoView_ownerRepo(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()
@ -656,7 +657,7 @@ func TestRepoView_fullURL(t *testing.T) {
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &outputStub{}
return &test.OutputStub{}
})
defer restoreCmd()

View file

@ -2,11 +2,103 @@ package command
import (
"errors"
"fmt"
"os/exec"
"reflect"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/core"
"github.com/cli/cli/api"
"github.com/cli/cli/context"
"github.com/cli/cli/test"
"github.com/cli/cli/utils"
)
// TODO this is split between here and test/helpers.go. I did that because otherwise our test
// package would have to import utils (for utils.Runnable) which felt wrong. while utils_test
// currently doesn't import test helpers, i don't see why that ought to be precluded.
// I'm wondering if this is a case for having a global package just for storing Interfaces.
type CmdStubber struct {
Stubs []*test.OutputStub
Count int
Calls []*exec.Cmd
}
func initCmdStubber() (*CmdStubber, func()) {
cs := CmdStubber{}
teardown := utils.SetPrepareCmd(createStubbedPrepareCmd(&cs))
return &cs, teardown
}
func (cs *CmdStubber) Stub(desiredOutput string) {
// TODO maybe have some kind of command mapping but going simple for now
cs.Stubs = append(cs.Stubs, &test.OutputStub{[]byte(desiredOutput)})
}
func createStubbedPrepareCmd(cs *CmdStubber) func(*exec.Cmd) utils.Runnable {
return func(cmd *exec.Cmd) utils.Runnable {
cs.Calls = append(cs.Calls, cmd)
call := cs.Count
cs.Count += 1
if call >= len(cs.Stubs) {
panic(fmt.Sprintf("more execs than stubs. most recent call: %v", cmd))
}
return cs.Stubs[call]
}
}
type askStubber struct {
Asks [][]*survey.Question
Count int
Stubs [][]*QuestionStub
}
func initAskStubber() (*askStubber, func()) {
origSurveyAsk := SurveyAsk
as := askStubber{}
SurveyAsk = func(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error {
as.Asks = append(as.Asks, qs)
count := as.Count
as.Count += 1
if count >= len(as.Stubs) {
panic(fmt.Sprintf("more asks than stubs. most recent call: %v", qs))
}
// actually set response
stubbedQuestions := as.Stubs[count]
for i, sq := range stubbedQuestions {
q := qs[i]
if q.Name != sq.Name {
panic(fmt.Sprintf("stubbed question mismatch: %s != %s", q.Name, sq.Name))
}
if sq.Default {
defaultValue := reflect.ValueOf(q.Prompt).Elem().FieldByName("Default")
core.WriteAnswer(response, q.Name, defaultValue)
} else {
core.WriteAnswer(response, q.Name, sq.Value)
}
}
return nil
}
teardown := func() {
SurveyAsk = origSurveyAsk
}
return &as, teardown
}
type QuestionStub struct {
Name string
Value interface{}
Default bool
}
func (as *askStubber) Stub(stubbedQuestions []*QuestionStub) {
// A call to .Ask takes a list of questions; a stub is then a list of questions in the same order.
as.Stubs = append(as.Stubs, stubbedQuestions)
}
func initBlankContext(repo, branch string) {
initContext = func() context.Context {
ctx := context.NewBlank()
@ -27,19 +119,6 @@ func initFakeHTTP() *api.FakeHTTP {
return http
}
// outputStub implements a simple utils.Runnable
type outputStub struct {
output []byte
}
func (s outputStub) Output() ([]byte, error) {
return s.output, nil
}
func (s outputStub) Run() error {
return nil
}
type errorStub struct {
message string
}

View file

@ -23,7 +23,11 @@ const (
CancelAction
)
func confirm() (Action, error) {
var SurveyAsk = func(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error {
return survey.Ask(qs, response, opts...)
}
func confirmSubmission() (Action, error) {
confirmAnswers := struct {
Confirmation int
}{}
@ -41,7 +45,7 @@ func confirm() (Action, error) {
},
}
err := survey.Ask(confirmQs, &confirmAnswers)
err := SurveyAsk(confirmQs, &confirmAnswers)
if err != nil {
return -1, fmt.Errorf("could not prompt: %w", err)
}
@ -68,7 +72,7 @@ func selectTemplate(templatePaths []string) (string, error) {
},
},
}
if err := survey.Ask(selectQs, &templateResponse); err != nil {
if err := SurveyAsk(selectQs, &templateResponse); err != nil {
return "", fmt.Errorf("could not prompt: %w", err)
}
}
@ -77,17 +81,22 @@ func selectTemplate(templatePaths []string) (string, error) {
return string(templateContents), nil
}
func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody string, templatePaths []string) (*titleBody, error) {
func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, defs defaults, templatePaths []string) (*titleBody, error) {
var inProgress titleBody
inProgress.Title = defs.Title
templateContents := ""
if providedBody == "" && len(templatePaths) > 0 {
var err error
templateContents, err = selectTemplate(templatePaths)
if err != nil {
return nil, err
if providedBody == "" {
if len(templatePaths) > 0 {
var err error
templateContents, err = selectTemplate(templatePaths)
if err != nil {
return nil, err
}
inProgress.Body = templateContents
} else {
inProgress.Body = defs.Body
}
inProgress.Body = templateContents
}
titleQuestion := &survey.Question{
@ -118,7 +127,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody stri
qs = append(qs, bodyQuestion)
}
err := survey.Ask(qs, &inProgress)
err := SurveyAsk(qs, &inProgress)
if err != nil {
return nil, fmt.Errorf("could not prompt: %w", err)
}
@ -127,7 +136,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody stri
inProgress.Body = templateContents
}
confirmA, err := confirm()
confirmA, err := confirmSubmission()
if err != nil {
return nil, fmt.Errorf("unable to confirm: %w", err)
}