Isolate pr create command

This commit is contained in:
Mislav Marohnić 2020-08-04 17:22:06 +02:00
parent e024184c6f
commit 558e3f3dea
7 changed files with 471 additions and 435 deletions

View file

@ -508,11 +508,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb { if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb {
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new") openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
if title != "" || body != "" { if title != "" || body != "" {
milestone := "" openURL, err = shared.WithPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestoneTitles)
if len(milestoneTitles) > 0 {
milestone = milestoneTitles[0]
}
openURL, err = withPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestone)
if err != nil { if err != nil {
return err return err
} }
@ -535,9 +531,9 @@ func issueCreate(cmd *cobra.Command, args []string) error {
return fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(baseRepo)) return fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(baseRepo))
} }
action := SubmitAction action := shared.SubmitAction
tb := issueMetadataState{ tb := shared.IssueMetadataState{
Type: issueMetadata, Type: shared.IssueMetadata,
Assignees: assignees, Assignees: assignees,
Labels: labelNames, Labels: labelNames,
Projects: projectNames, Projects: projectNames,
@ -558,14 +554,20 @@ func issueCreate(cmd *cobra.Command, args []string) error {
legacyTemplateFile = githubtemplate.FindLegacy(rootDir, "ISSUE_TEMPLATE") legacyTemplateFile = githubtemplate.FindLegacy(rootDir, "ISSUE_TEMPLATE")
} }
} }
err := titleBodySurvey(cmd, &tb, apiClient, baseRepo, title, body, defaults{}, nonLegacyTemplateFiles, legacyTemplateFile, false, repo.ViewerCanTriage())
editorCommand, err := cmdutil.DetermineEditor(ctx.Config)
if err != nil {
return err
}
err = shared.TitleBodySurvey(defaultStreams, editorCommand, &tb, apiClient, baseRepo, title, body, shared.Defaults{}, nonLegacyTemplateFiles, legacyTemplateFile, false, repo.ViewerCanTriage())
if err != nil { if err != nil {
return fmt.Errorf("could not collect title and/or body: %w", err) return fmt.Errorf("could not collect title and/or body: %w", err)
} }
action = tb.Action action = tb.Action
if tb.Action == CancelAction { if tb.Action == shared.CancelAction {
fmt.Fprintln(cmd.ErrOrStderr(), "Discarding.") fmt.Fprintln(cmd.ErrOrStderr(), "Discarding.")
return nil return nil
@ -583,26 +585,22 @@ func issueCreate(cmd *cobra.Command, args []string) error {
} }
} }
if action == PreviewAction { if action == shared.PreviewAction {
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new") openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
milestone := "" openURL, err = shared.WithPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestoneTitles)
if len(milestoneTitles) > 0 {
milestone = milestoneTitles[0]
}
openURL, err = withPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestone)
if err != nil { if err != nil {
return err return err
} }
// TODO could exceed max url length for explorer // TODO could exceed max url length for explorer
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", utils.DisplayURL(openURL)) fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", utils.DisplayURL(openURL))
return utils.OpenInBrowser(openURL) return utils.OpenInBrowser(openURL)
} else if action == SubmitAction { } else if action == shared.SubmitAction {
params := map[string]interface{}{ params := map[string]interface{}{
"title": title, "title": title,
"body": body, "body": body,
} }
err = addMetadataToIssueParams(apiClient, baseRepo, params, &tb) err = shared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb)
if err != nil { if err != nil {
return err return err
} }
@ -620,82 +618,6 @@ func issueCreate(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func addMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *issueMetadataState) error {
if !tb.HasMetadata() {
return nil
}
if tb.MetadataResult == nil {
resolveInput := api.RepoResolveInput{
Reviewers: tb.Reviewers,
Assignees: tb.Assignees,
Labels: tb.Labels,
Projects: tb.Projects,
Milestones: tb.Milestones,
}
var err error
tb.MetadataResult, err = api.RepoResolveMetadataIDs(client, baseRepo, resolveInput)
if err != nil {
return err
}
}
assigneeIDs, err := tb.MetadataResult.MembersToIDs(tb.Assignees)
if err != nil {
return fmt.Errorf("could not assign user: %w", err)
}
params["assigneeIds"] = assigneeIDs
labelIDs, err := tb.MetadataResult.LabelsToIDs(tb.Labels)
if err != nil {
return fmt.Errorf("could not add label: %w", err)
}
params["labelIds"] = labelIDs
projectIDs, err := tb.MetadataResult.ProjectsToIDs(tb.Projects)
if err != nil {
return fmt.Errorf("could not add to project: %w", err)
}
params["projectIds"] = projectIDs
if len(tb.Milestones) > 0 {
milestoneID, err := tb.MetadataResult.MilestoneToID(tb.Milestones[0])
if err != nil {
return fmt.Errorf("could not add to milestone '%s': %w", tb.Milestones[0], err)
}
params["milestoneId"] = milestoneID
}
if len(tb.Reviewers) == 0 {
return nil
}
var userReviewers []string
var teamReviewers []string
for _, r := range tb.Reviewers {
if strings.ContainsRune(r, '/') {
teamReviewers = append(teamReviewers, r)
} else {
userReviewers = append(userReviewers, r)
}
}
userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers)
if err != nil {
return fmt.Errorf("could not request reviewer: %w", err)
}
params["userReviewerIds"] = userReviewerIDs
teamReviewerIDs, err := tb.MetadataResult.TeamsToIDs(teamReviewers)
if err != nil {
return fmt.Errorf("could not request reviewer: %w", err)
}
params["teamReviewerIds"] = teamReviewerIDs
return nil
}
func printIssues(w io.Writer, prefix string, totalCount int, issues []api.Issue) { func printIssues(w io.Writer, prefix string, totalCount int, issues []api.Issue) {
table := utils.NewTablePrinter(w) table := utils.NewTablePrinter(w)
for _, issue := range issues { for _, issue := range issues {

View file

@ -19,7 +19,6 @@ func init() {
prCmd.PersistentFlags().StringP("repo", "R", "", "Select another repository using the `OWNER/REPO` format") prCmd.PersistentFlags().StringP("repo", "R", "", "Select another repository using the `OWNER/REPO` format")
RootCmd.AddCommand(prCmd) RootCmd.AddCommand(prCmd)
prCmd.AddCommand(prCreateCmd)
prCmd.AddCommand(prCloseCmd) prCmd.AddCommand(prCloseCmd)
prCmd.AddCommand(prReopenCmd) prCmd.AddCommand(prReopenCmd)
prCmd.AddCommand(prReadyCmd) prCmd.AddCommand(prReadyCmd)

View file

@ -24,6 +24,7 @@ import (
apiCmd "github.com/cli/cli/pkg/cmd/api" apiCmd "github.com/cli/cli/pkg/cmd/api"
gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create" gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create"
prCheckoutCmd "github.com/cli/cli/pkg/cmd/pr/checkout" prCheckoutCmd "github.com/cli/cli/pkg/cmd/pr/checkout"
prCreateCmd "github.com/cli/cli/pkg/cmd/pr/create"
prDiffCmd "github.com/cli/cli/pkg/cmd/pr/diff" prDiffCmd "github.com/cli/cli/pkg/cmd/pr/diff"
prMergeCmd "github.com/cli/cli/pkg/cmd/pr/merge" prMergeCmd "github.com/cli/cli/pkg/cmd/pr/merge"
prReviewCmd "github.com/cli/cli/pkg/cmd/pr/review" prReviewCmd "github.com/cli/cli/pkg/cmd/pr/review"
@ -179,6 +180,7 @@ func init() {
prCmd.AddCommand(prViewCmd.NewCmdView(&repoResolvingCmdFactory, nil)) prCmd.AddCommand(prViewCmd.NewCmdView(&repoResolvingCmdFactory, nil))
prCmd.AddCommand(prMergeCmd.NewCmdMerge(&repoResolvingCmdFactory, nil)) prCmd.AddCommand(prMergeCmd.NewCmdMerge(&repoResolvingCmdFactory, nil))
prCmd.AddCommand(prStatusCmd.NewCmdStatus(&repoResolvingCmdFactory, nil)) prCmd.AddCommand(prStatusCmd.NewCmdStatus(&repoResolvingCmdFactory, nil))
prCmd.AddCommand(prCreateCmd.NewCmdCreate(&repoResolvingCmdFactory, nil))
RootCmd.AddCommand(creditsCmd.NewCmdCredits(cmdFactory, nil)) RootCmd.AddCommand(creditsCmd.NewCmdCredits(cmdFactory, nil))
} }
@ -398,41 +400,6 @@ func determineBaseRepo(apiClient *api.Client, cmd *cobra.Command, ctx context.Co
return baseRepo, nil return baseRepo, nil
} }
// TODO there is a parallel implementation for isolated commands
func formatRemoteURL(cmd *cobra.Command, repo ghrepo.Interface) string {
ctx := contextForCommand(cmd)
var protocol string
cfg, err := ctx.Config()
if err != nil {
fmt.Fprintf(colorableErr(cmd), "%s failed to load config: %s. using defaults\n", utils.Yellow("!"), err)
} else {
protocol, _ = cfg.Get(repo.RepoHost(), "git_protocol")
}
if protocol == "ssh" {
return fmt.Sprintf("git@%s:%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
}
return fmt.Sprintf("https://%s/%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
}
// TODO there is a parallel implementation for isolated commands
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)
}
// TODO: consider supporting setting an editor per GHE host
editorCommand, _ = cfg.Get(ghinstance.Default(), "editor")
}
return editorCommand, nil
}
func ExecuteShellAlias(args []string) error { func ExecuteShellAlias(args []string) error {
externalCmd := exec.Command(args[0], args[1:]...) externalCmd := exec.Command(args[0], args[1:]...)
externalCmd.Stderr = os.Stderr externalCmd.Stderr = os.Stderr

View file

@ -1,9 +1,9 @@
package command package create
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/url" "net/http"
"strings" "strings"
"time" "time"
@ -11,60 +11,116 @@ import (
"github.com/cli/cli/api" "github.com/cli/cli/api"
"github.com/cli/cli/context" "github.com/cli/cli/context"
"github.com/cli/cli/git" "github.com/cli/cli/git"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmd/pr/shared"
"github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/githubtemplate" "github.com/cli/cli/pkg/githubtemplate"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/utils" "github.com/cli/cli/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type defaults struct { type CreateOptions struct {
Title string HttpClient func() (*http.Client, error)
Body string Config func() (config.Config, error)
IO *iostreams.IOStreams
Remotes func() (context.Remotes, error)
Branch func() (string, error)
RepoOverride string
Autofill bool
WebMode bool
IsDraft bool
Title string
TitleProvided bool
Body string
BodyProvided bool
BaseBranch string
Reviewers []string
Assignees []string
Labels []string
Projects []string
Milestone string
} }
func computeDefaults(baseRef, headRef string) (defaults, error) { func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
commits, err := git.Commits(baseRef, headRef) opts := &CreateOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
Config: f.Config,
Remotes: f.Remotes,
Branch: f.Branch,
}
cmd := &cobra.Command{
Use: "create",
Short: "Create a pull request",
Example: heredoc.Doc(`
$ gh pr create --title "The bug is fixed" --body "Everything works again"
$ gh issue create --label "bug,help wanted"
$ gh issue create --label bug --label "help wanted"
$ gh pr create --reviewer monalisa,hubot
$ gh pr create --project "Roadmap"
$ gh pr create --base develop
`),
Args: cmdutil.NoArgsQuoteReminder,
RunE: func(cmd *cobra.Command, args []string) error {
opts.TitleProvided = cmd.Flags().Changed("title")
opts.BodyProvided = cmd.Flags().Changed("body")
opts.RepoOverride, _ = cmd.Flags().GetString("repo")
isTerminal := opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY()
if !isTerminal && !opts.WebMode && !opts.TitleProvided && !opts.Autofill {
return errors.New("--title or --fill required when not attached to a terminal")
}
if opts.IsDraft && opts.WebMode {
return errors.New("the --draft flag is not supported with --web")
}
if len(opts.Reviewers) > 0 && opts.WebMode {
return errors.New("the --reviewer flag is not supported with --web")
}
if runF != nil {
return runF(opts)
}
return createRun(opts)
},
}
fl := cmd.Flags()
fl.BoolVarP(&opts.IsDraft, "draft", "d", false, "Mark pull request as a draft")
fl.StringVarP(&opts.Title, "title", "t", "", "Supply a title. Will prompt for one otherwise.")
fl.StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
fl.StringVarP(&opts.BaseBranch, "base", "B", "", "The branch into which you want your code merged")
fl.BoolVarP(&opts.WebMode, "web", "w", false, "Open the web browser to create a pull request")
fl.BoolVarP(&opts.Autofill, "fill", "f", false, "Do not prompt for title/body and just use commit info")
fl.StringSliceVarP(&opts.Reviewers, "reviewer", "r", nil, "Request reviews from people by their `login`")
fl.StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`")
fl.StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
fl.StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the pull request to projects by `name`")
fl.StringVarP(&opts.Milestone, "milestone", "m", "", "Add the pull request to a milestone by `name`")
return cmd
}
func createRun(opts *CreateOptions) error {
httpClient, err := opts.HttpClient()
if err != nil { if err != nil {
return defaults{}, err return err
} }
client := api.NewClientFromHTTP(httpClient)
out := defaults{} remotes, err := opts.Remotes()
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 i := len(commits) - 1; i >= 0; i-- {
body += fmt.Sprintf("- %s\n", commits[i].Title)
}
out.Body = body
}
return out, nil
}
func prCreate(cmd *cobra.Command, _ []string) error {
ctx := contextForCommand(cmd)
remotes, err := ctx.Remotes()
if err != nil { if err != nil {
return err return err
} }
client, err := apiClientForContext(ctx) repoContext, err := context.ResolveRemotesToRepos(remotes, client, opts.RepoOverride)
if err != nil {
return fmt.Errorf("could not initialize API client: %w", err)
}
baseRepoOverride, _ := cmd.Flags().GetString("repo")
repoContext, err := context.ResolveRemotesToRepos(remotes, client, baseRepoOverride)
if err != nil { if err != nil {
return err return err
} }
@ -74,7 +130,7 @@ func prCreate(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("could not determine base repository: %w", err) return fmt.Errorf("could not determine base repository: %w", err)
} }
headBranch, err := ctx.Branch() headBranch, err := opts.Branch()
if err != nil { if err != nil {
return fmt.Errorf("could not determine the current branch: %w", err) return fmt.Errorf("could not determine the current branch: %w", err)
} }
@ -102,10 +158,7 @@ func prCreate(cmd *cobra.Command, _ []string) error {
} }
} }
baseBranch, err := cmd.Flags().GetString("base") baseBranch := opts.BaseBranch
if err != nil {
return err
}
if baseBranch == "" { if baseBranch == "" {
baseBranch = baseRepo.DefaultBranchRef.Name baseBranch = baseRepo.DefaultBranchRef.Name
} }
@ -114,39 +167,12 @@ func prCreate(cmd *cobra.Command, _ []string) error {
} }
if ucc, err := git.UncommittedChangeCount(); err == nil && ucc > 0 { if ucc, err := git.UncommittedChangeCount(); err == nil && ucc > 0 {
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: %s\n", utils.Pluralize(ucc, "uncommitted change")) fmt.Fprintf(opts.IO.ErrOut, "Warning: %s\n", utils.Pluralize(ucc, "uncommitted change"))
} }
title, err := cmd.Flags().GetString("title")
if err != nil {
return fmt.Errorf("could not parse title: %w", err)
}
body, err := cmd.Flags().GetString("body")
if err != nil {
return fmt.Errorf("could not parse body: %w", err)
}
reviewers, err := cmd.Flags().GetStringSlice("reviewer")
if err != nil {
return fmt.Errorf("could not parse reviewers: %w", err)
}
assignees, err := cmd.Flags().GetStringSlice("assignee")
if err != nil {
return fmt.Errorf("could not parse assignees: %w", err)
}
labelNames, err := cmd.Flags().GetStringSlice("label")
if err != nil {
return fmt.Errorf("could not parse labels: %w", err)
}
projectNames, err := cmd.Flags().GetStringSlice("project")
if err != nil {
return fmt.Errorf("could not parse projects: %w", err)
}
var milestoneTitles []string var milestoneTitles []string
if milestoneTitle, err := cmd.Flags().GetString("milestone"); err != nil { if opts.Milestone != "" {
return fmt.Errorf("could not parse milestone: %w", err) milestoneTitles = []string{opts.Milestone}
} else if milestoneTitle != "" {
milestoneTitles = append(milestoneTitles, milestoneTitle)
} }
baseTrackingBranch := baseBranch baseTrackingBranch := baseBranch
@ -155,23 +181,16 @@ func prCreate(cmd *cobra.Command, _ []string) error {
} }
defs, defaultsErr := computeDefaults(baseTrackingBranch, headBranch) defs, defaultsErr := computeDefaults(baseTrackingBranch, headBranch)
isWeb, err := cmd.Flags().GetBool("web") title := opts.Title
if err != nil { body := opts.Body
return fmt.Errorf("could not parse web: %q", err)
}
autofill, err := cmd.Flags().GetBool("fill") action := shared.SubmitAction
if err != nil { if opts.WebMode {
return fmt.Errorf("could not parse fill: %q", err) action = shared.PreviewAction
}
action := SubmitAction
if isWeb {
action = PreviewAction
if (title == "" || body == "") && defaultsErr != nil { if (title == "" || body == "") && defaultsErr != nil {
return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr) return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr)
} }
} else if autofill { } else if opts.Autofill {
if defaultsErr != nil { if defaultsErr != nil {
return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr) return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr)
} }
@ -179,7 +198,7 @@ func prCreate(cmd *cobra.Command, _ []string) error {
body = defs.Body body = defs.Body
} }
if !isWeb { if !opts.WebMode {
headBranchLabel := headBranch headBranchLabel := headBranch
if headRepo != nil && !ghrepo.IsSame(baseRepo, headRepo) { if headRepo != nil && !ghrepo.IsSame(baseRepo, headRepo) {
headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), headBranch) headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), headBranch)
@ -194,46 +213,37 @@ func prCreate(cmd *cobra.Command, _ []string) error {
} }
} }
isDraft, err := cmd.Flags().GetBool("draft") isTerminal := opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY()
if err != nil {
return fmt.Errorf("could not parse draft: %w", err)
}
if !isWeb && !autofill { if !opts.WebMode && !opts.Autofill {
message := "\nCreating pull request for %s into %s in %s\n\n" message := "\nCreating pull request for %s into %s in %s\n\n"
if isDraft { if opts.IsDraft {
message = "\nCreating draft pull request for %s into %s in %s\n\n" message = "\nCreating draft pull request for %s into %s in %s\n\n"
} }
if connectedToTerminal(cmd) { if isTerminal {
fmt.Fprintf(colorableErr(cmd), message, fmt.Fprintf(opts.IO.ErrOut, message,
utils.Cyan(headBranch), utils.Cyan(headBranch),
utils.Cyan(baseBranch), utils.Cyan(baseBranch),
ghrepo.FullName(baseRepo)) ghrepo.FullName(baseRepo))
if (title == "" || body == "") && defaultsErr != nil { if (title == "" || body == "") && defaultsErr != nil {
fmt.Fprintf(colorableErr(cmd), "%s warning: could not compute title or body defaults: %s\n", utils.Yellow("!"), defaultsErr) fmt.Fprintf(opts.IO.ErrOut, "%s warning: could not compute title or body defaults: %s\n", utils.Yellow("!"), defaultsErr)
} }
} }
} }
tb := issueMetadataState{ tb := shared.IssueMetadataState{
Type: prMetadata, Type: shared.PRMetadata,
Reviewers: reviewers, Reviewers: opts.Reviewers,
Assignees: assignees, Assignees: opts.Assignees,
Labels: labelNames, Labels: opts.Labels,
Projects: projectNames, Projects: opts.Projects,
Milestones: milestoneTitles, Milestones: milestoneTitles,
} }
if !connectedToTerminal(cmd) { interactive := isTerminal && !(opts.TitleProvided && opts.BodyProvided)
if !isWeb && (!cmd.Flags().Changed("title") && !autofill) {
return errors.New("--title or --fill required when not attached to a tty")
}
}
interactive := connectedToTerminal(cmd) && !(cmd.Flags().Changed("title") && cmd.Flags().Changed("body")) if !opts.WebMode && !opts.Autofill && interactive {
if !isWeb && !autofill && interactive {
var nonLegacyTemplateFiles []string var nonLegacyTemplateFiles []string
var legacyTemplateFile *string var legacyTemplateFile *string
if rootDir, err := git.ToplevelDir(); err == nil { if rootDir, err := git.ToplevelDir(); err == nil {
@ -241,15 +251,21 @@ func prCreate(cmd *cobra.Command, _ []string) error {
nonLegacyTemplateFiles = githubtemplate.FindNonLegacy(rootDir, "PULL_REQUEST_TEMPLATE") nonLegacyTemplateFiles = githubtemplate.FindNonLegacy(rootDir, "PULL_REQUEST_TEMPLATE")
legacyTemplateFile = githubtemplate.FindLegacy(rootDir, "PULL_REQUEST_TEMPLATE") legacyTemplateFile = githubtemplate.FindLegacy(rootDir, "PULL_REQUEST_TEMPLATE")
} }
err := titleBodySurvey(cmd, &tb, client, baseRepo, title, body, defs, nonLegacyTemplateFiles, legacyTemplateFile, true, baseRepo.ViewerCanTriage())
editorCommand, err := cmdutil.DetermineEditor(opts.Config)
if err != nil {
return err
}
err = shared.TitleBodySurvey(opts.IO, editorCommand, &tb, client, baseRepo, title, body, defs, nonLegacyTemplateFiles, legacyTemplateFile, true, baseRepo.ViewerCanTriage())
if err != nil { if err != nil {
return fmt.Errorf("could not collect title and/or body: %w", err) return fmt.Errorf("could not collect title and/or body: %w", err)
} }
action = tb.Action action = tb.Action
if action == CancelAction { if action == shared.CancelAction {
fmt.Fprintln(cmd.ErrOrStderr(), "Discarding.") fmt.Fprintln(opts.IO.ErrOut, "Discarding.")
return nil return nil
} }
@ -261,17 +277,10 @@ func prCreate(cmd *cobra.Command, _ []string) error {
} }
} }
if action == SubmitAction && title == "" { if action == shared.SubmitAction && title == "" {
return errors.New("pull request title must not be blank") return errors.New("pull request title must not be blank")
} }
if isDraft && isWeb {
return errors.New("the --draft flag is not supported with --web")
}
if len(reviewers) > 0 && isWeb {
return errors.New("the --reviewer flag is not supported with --web")
}
didForkRepo := false didForkRepo := false
// if a head repository could not be determined so far, automatically create // if a head repository could not be determined so far, automatically create
// one by forking the base repository // one by forking the base repository
@ -303,7 +312,13 @@ func prCreate(cmd *cobra.Command, _ []string) error {
// In either case, we want to add the head repo as a new git remote so we // In either case, we want to add the head repo as a new git remote so we
// can push to it. // can push to it.
if headRemote == nil { if headRemote == nil {
headRepoURL := formatRemoteURL(cmd, headRepo) cfg, err := opts.Config()
if err != nil {
return err
}
cloneProtocol, _ := cfg.Get(headRepo.RepoHost(), "git_protocol")
headRepoURL := ghrepo.FormatRemoteURL(headRepo, cloneProtocol)
// TODO: prevent clashes with another remote of a same name // TODO: prevent clashes with another remote of a same name
gitRemote, err := git.AddRemote("fork", headRepoURL) gitRemote, err := git.AddRemote("fork", headRepoURL)
@ -326,7 +341,7 @@ func prCreate(cmd *cobra.Command, _ []string) error {
pushTries++ pushTries++
// first wait 2 seconds after forking, then 4s, then 6s // first wait 2 seconds after forking, then 4s, then 6s
waitSeconds := 2 * pushTries waitSeconds := 2 * pushTries
fmt.Fprintf(cmd.ErrOrStderr(), "waiting %s before retrying...\n", utils.Pluralize(waitSeconds, "second")) fmt.Fprintf(opts.IO.ErrOut, "waiting %s before retrying...\n", utils.Pluralize(waitSeconds, "second"))
time.Sleep(time.Duration(waitSeconds) * time.Second) time.Sleep(time.Duration(waitSeconds) * time.Second)
continue continue
} }
@ -336,16 +351,16 @@ func prCreate(cmd *cobra.Command, _ []string) error {
} }
} }
if action == SubmitAction { if action == shared.SubmitAction {
params := map[string]interface{}{ params := map[string]interface{}{
"title": title, "title": title,
"body": body, "body": body,
"draft": isDraft, "draft": opts.IsDraft,
"baseRefName": baseBranch, "baseRefName": baseBranch,
"headRefName": headBranchLabel, "headRefName": headBranchLabel,
} }
err = addMetadataToIssueParams(client, baseRepo, params, &tb) err = shared.AddMetadataToIssueParams(client, baseRepo, params, &tb)
if err != nil { if err != nil {
return err return err
} }
@ -355,19 +370,14 @@ func prCreate(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("failed to create pull request: %w", err) return fmt.Errorf("failed to create pull request: %w", err)
} }
fmt.Fprintln(cmd.OutOrStdout(), pr.URL) fmt.Fprintln(opts.IO.Out, pr.URL)
} else if action == PreviewAction { } else if action == shared.PreviewAction {
milestone := "" openURL, err := generateCompareURL(baseRepo, baseBranch, headBranchLabel, title, body, tb.Assignees, tb.Labels, tb.Projects, tb.Milestones)
if len(milestoneTitles) > 0 {
milestone = milestoneTitles[0]
}
openURL, err := generateCompareURL(baseRepo, baseBranch, headBranchLabel, title, body, assignees, labelNames, projectNames, milestone)
if err != nil { if err != nil {
return err return err
} }
if connectedToTerminal(cmd) { if isTerminal {
// TODO could exceed max url length for explorer fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", utils.DisplayURL(openURL))
} }
return utils.OpenInBrowser(openURL) return utils.OpenInBrowser(openURL)
} else { } else {
@ -377,6 +387,34 @@ func prCreate(cmd *cobra.Command, _ []string) error {
return nil return nil
} }
func computeDefaults(baseRef, headRef string) (shared.Defaults, error) {
out := shared.Defaults{}
commits, err := git.Commits(baseRef, headRef)
if err != nil {
return out, err
}
if len(commits) == 1 {
out.Title = commits[0].Title
body, err := git.CommitBody(commits[0].Sha)
if err != nil {
return out, err
}
out.Body = body
} else {
out.Title = utils.Humanize(headRef)
body := ""
for i := len(commits) - 1; i >= 0; i-- {
body += fmt.Sprintf("- %s\n", commits[i].Title)
}
out.Body = body
}
return out, nil
}
func determineTrackingBranch(remotes context.Remotes, headBranch string) *git.TrackingRef { func determineTrackingBranch(remotes context.Remotes, headBranch string) *git.TrackingRef {
refsForLookup := []string{"HEAD"} refsForLookup := []string{"HEAD"}
var trackingRefs []git.TrackingRef var trackingRefs []git.TrackingRef
@ -418,73 +456,11 @@ func determineTrackingBranch(remotes context.Remotes, headBranch string) *git.Tr
return nil return nil
} }
func withPrAndIssueQueryParams(baseURL, title, body string, assignees, labels, projects []string, milestone string) (string, error) { func generateCompareURL(r ghrepo.Interface, base, head, title, body string, assignees, labels, projects []string, milestones []string) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", err
}
q := u.Query()
if title != "" {
q.Set("title", title)
}
if body != "" {
q.Set("body", body)
}
if len(assignees) > 0 {
q.Set("assignees", strings.Join(assignees, ","))
}
if len(labels) > 0 {
q.Set("labels", strings.Join(labels, ","))
}
if len(projects) > 0 {
q.Set("projects", strings.Join(projects, ","))
}
if milestone != "" {
q.Set("milestone", milestone)
}
u.RawQuery = q.Encode()
return u.String(), nil
}
func generateCompareURL(r ghrepo.Interface, base, head, title, body string, assignees, labels, projects []string, milestone string) (string, error) {
u := ghrepo.GenerateRepoURL(r, "compare/%s...%s?expand=1", base, head) u := ghrepo.GenerateRepoURL(r, "compare/%s...%s?expand=1", base, head)
url, err := withPrAndIssueQueryParams(u, title, body, assignees, labels, projects, milestone) url, err := shared.WithPrAndIssueQueryParams(u, title, body, assignees, labels, projects, milestones)
if err != nil { if err != nil {
return "", err return "", err
} }
return url, nil return url, nil
} }
var prCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a pull request",
Args: cmdutil.NoArgsQuoteReminder,
RunE: prCreate,
Example: heredoc.Doc(`
$ gh pr create --title "The bug is fixed" --body "Everything works again"
$ gh issue create --label "bug,help wanted"
$ gh issue create --label bug --label "help wanted"
$ gh pr create --reviewer monalisa,hubot
$ gh pr create --project "Roadmap"
$ gh pr create --base develop
`),
}
func init() {
prCreateCmd.Flags().BoolP("draft", "d", false,
"Mark pull request as a draft")
prCreateCmd.Flags().StringP("title", "t", "",
"Supply a title. Will prompt for one otherwise.")
prCreateCmd.Flags().StringP("body", "b", "",
"Supply a body. Will prompt for one otherwise.")
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")
prCreateCmd.Flags().StringSliceP("reviewer", "r", nil, "Request reviews from people by their `login`")
prCreateCmd.Flags().StringSliceP("assignee", "a", nil, "Assign people by their `login`")
prCreateCmd.Flags().StringSliceP("label", "l", nil, "Add labels by `name`")
prCreateCmd.Flags().StringSliceP("project", "p", nil, "Add the pull request to projects by `name`")
prCreateCmd.Flags().StringP("milestone", "m", "", "Add the pull request to a milestone by `name`")
}

View file

@ -1,25 +1,93 @@
package command package create
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"net/http"
"reflect"
"strings" "strings"
"testing" "testing"
"github.com/cli/cli/context" "github.com/cli/cli/context"
"github.com/cli/cli/git" "github.com/cli/cli/git"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/httpmock" "github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/prompt" "github.com/cli/cli/pkg/prompt"
"github.com/cli/cli/test" "github.com/cli/cli/test"
"github.com/google/shlex"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func eq(t *testing.T, got interface{}, expected interface{}) {
t.Helper()
if !reflect.DeepEqual(got, expected) {
t.Errorf("expected: %v, got: %v", expected, got)
}
}
func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, isTTY bool, cli string) (*test.CmdOut, error) {
io, _, stdout, stderr := iostreams.Test()
io.SetStdoutTTY(isTTY)
io.SetStdinTTY(isTTY)
io.SetStderrTTY(isTTY)
factory := &cmdutil.Factory{
IOStreams: io,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: rt}, nil
},
Config: func() (config.Config, error) {
return config.NewBlankConfig(), nil
},
Remotes: func() (context.Remotes, error) {
if remotes != nil {
return remotes, nil
}
return context.Remotes{
{
Remote: &git.Remote{Name: "origin"},
Repo: ghrepo.New("OWNER", "REPO"),
},
}, nil
},
Branch: func() (string, error) {
return branch, nil
},
}
cmd := NewCmdCreate(factory, nil)
cmd.PersistentFlags().StringP("repo", "R", "", "")
argv, err := shlex.Split(cli)
if err != nil {
return nil, err
}
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(ioutil.Discard)
cmd.SetErr(ioutil.Discard)
_, err = cmd.ExecuteC()
return &test.CmdOut{
OutBuf: stdout,
ErrBuf: stderr,
}, err
}
func initFakeHTTP() *httpmock.Registry {
return &httpmock.Registry{}
}
func TestPRCreate_nontty_web(t *testing.T) { func TestPRCreate_nontty_web(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(false)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -36,8 +104,8 @@ func TestPRCreate_nontty_web(t *testing.T) {
cs.Stub("") // git push cs.Stub("") // git push
cs.Stub("") // browser cs.Stub("") // browser
output, err := RunCommand(`pr create --web`) output, err := runCommand(http, nil, "feature", false, `--web`)
eq(t, err, nil) require.NoError(t, err)
eq(t, output.String(), "") eq(t, output.String(), "")
eq(t, output.Stderr(), "") eq(t, output.Stderr(), "")
@ -50,33 +118,23 @@ func TestPRCreate_nontty_web(t *testing.T) {
} }
func TestPRCreate_nontty_insufficient_flags(t *testing.T) { func TestPRCreate_nontty_insufficient_flags(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(false)()
http := initFakeHTTP() http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO") defer http.Verify(t)
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [
] } } } }
`))
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequests": { "nodes" : [
] } } } }
`))
output, err := RunCommand("pr create") output, err := runCommand(http, nil, "feature", false, "")
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
assert.Equal(t, "--title or --fill required when not attached to a tty", err.Error()) assert.Equal(t, "--title or --fill required when not attached to a terminal", err.Error())
assert.Equal(t, "", output.String()) assert.Equal(t, "", output.String())
} }
func TestPRCreate_nontty(t *testing.T) { func TestPRCreate_nontty(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(false)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -101,8 +159,8 @@ func TestPRCreate_nontty(t *testing.T) {
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push cs.Stub("") // git push
output, err := RunCommand(`pr create -t "my title" -b "my body"`) output, err := runCommand(http, nil, "feature", false, `-t "my title" -b "my body"`)
eq(t, err, nil) require.NoError(t, err)
bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body) bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body)
reqBody := struct { reqBody := struct {
@ -129,9 +187,9 @@ func TestPRCreate_nontty(t *testing.T) {
} }
func TestPRCreate(t *testing.T) { func TestPRCreate(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -156,8 +214,8 @@ func TestPRCreate(t *testing.T) {
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push cs.Stub("") // git push
output, err := RunCommand(`pr create -t "my title" -b "my body"`) output, err := runCommand(http, nil, "feature", true, `-t "my title" -b "my body"`)
eq(t, err, nil) require.NoError(t, err)
bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body) bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body)
reqBody := struct { reqBody := struct {
@ -183,8 +241,6 @@ func TestPRCreate(t *testing.T) {
} }
func TestPRCreate_metadata(t *testing.T) { func TestPRCreate_metadata(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t) defer http.Verify(t)
@ -301,16 +357,16 @@ func TestPRCreate_metadata(t *testing.T) {
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push cs.Stub("") // git push
output, err := RunCommand(`pr create -t TITLE -b BODY -a monalisa -l bug -l todo -p roadmap -m 'big one.oh' -r hubot -r monalisa -r /core -r /robots`) output, err := runCommand(http, nil, "feature", true, `-t TITLE -b BODY -a monalisa -l bug -l todo -p roadmap -m 'big one.oh' -r hubot -r monalisa -r /core -r /robots`)
eq(t, err, nil) eq(t, err, nil)
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n") eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
} }
func TestPRCreate_withForking(t *testing.T) { func TestPRCreate_withForking(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponseWithPermission("OWNER", "REPO", "READ") http.StubRepoResponseWithPermission("OWNER", "REPO", "READ")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -344,17 +400,17 @@ func TestPRCreate_withForking(t *testing.T) {
cs.Stub("") // git remote add cs.Stub("") // git remote add
cs.Stub("") // git push cs.Stub("") // git push
output, err := RunCommand(`pr create -t title -b body`) output, err := runCommand(http, nil, "feature", true, `-t title -b body`)
eq(t, err, nil) require.NoError(t, err)
eq(t, http.Requests[3].URL.Path, "/repos/OWNER/REPO/forks") eq(t, http.Requests[3].URL.Path, "/repos/OWNER/REPO/forks")
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n") eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
} }
func TestPRCreate_alreadyExists(t *testing.T) { func TestPRCreate_alreadyExists(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -376,7 +432,7 @@ func TestPRCreate_alreadyExists(t *testing.T) {
cs.Stub("") // git status cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
_, err := RunCommand(`pr create`) _, err := runCommand(http, nil, "feature", true, ``)
if err == nil { if err == nil {
t.Fatal("error expected, got nil") t.Fatal("error expected, got nil")
} }
@ -386,9 +442,9 @@ func TestPRCreate_alreadyExists(t *testing.T) {
} }
func TestPRCreate_alreadyExistsDifferentBase(t *testing.T) { func TestPRCreate_alreadyExistsDifferentBase(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -412,16 +468,16 @@ func TestPRCreate_alreadyExistsDifferentBase(t *testing.T) {
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git rev-parse cs.Stub("") // git rev-parse
_, err := RunCommand(`pr create -BanotherBase -t"cool" -b"nah"`) _, err := runCommand(http, nil, "feature", true, `-BanotherBase -t"cool" -b"nah"`)
if err != nil { if err != nil {
t.Errorf("got unexpected error %q", err) t.Errorf("got unexpected error %q", err)
} }
} }
func TestPRCreate_web(t *testing.T) { func TestPRCreate_web(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -438,8 +494,8 @@ func TestPRCreate_web(t *testing.T) {
cs.Stub("") // git push cs.Stub("") // git push
cs.Stub("") // browser cs.Stub("") // browser
output, err := RunCommand(`pr create --web`) output, err := runCommand(http, nil, "feature", true, `--web`)
eq(t, err, nil) require.NoError(t, err)
eq(t, output.String(), "") eq(t, output.String(), "")
eq(t, output.Stderr(), "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n") eq(t, output.Stderr(), "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n")
@ -451,9 +507,8 @@ func TestPRCreate_web(t *testing.T) {
} }
func TestPRCreate_ReportsUncommittedChanges(t *testing.T) { func TestPRCreate_ReportsUncommittedChanges(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
@ -479,7 +534,7 @@ func TestPRCreate_ReportsUncommittedChanges(t *testing.T) {
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push cs.Stub("") // git push
output, err := RunCommand(`pr create -t "my title" -b "my body"`) output, err := runCommand(http, nil, "feature", true, `-t "my title" -b "my body"`)
eq(t, err, nil) eq(t, err, nil)
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n") eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
@ -487,17 +542,20 @@ func TestPRCreate_ReportsUncommittedChanges(t *testing.T) {
} }
func TestPRCreate_cross_repo_same_branch(t *testing.T) { func TestPRCreate_cross_repo_same_branch(t *testing.T) {
defer stubTerminal(true)() remotes := context.Remotes{
ctx := context.NewBlank() {
ctx.SetBranch("default") Remote: &git.Remote{Name: "origin"},
ctx.SetRemotes(map[string]string{ Repo: ghrepo.New("OWNER", "REPO"),
"origin": "OWNER/REPO", },
"fork": "MYSELF/REPO", {
}) Remote: &git.Remote{Name: "fork"},
initContext = func() context.Context { Repo: ghrepo.New("MYSELF", "REPO"),
return ctx },
} }
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repo_000": { { "data": { "repo_000": {
"id": "REPOID0", "id": "REPOID0",
@ -546,8 +604,8 @@ func TestPRCreate_cross_repo_same_branch(t *testing.T) {
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push cs.Stub("") // git push
output, err := RunCommand(`pr create -t "cross repo" -b "same branch"`) output, err := runCommand(http, remotes, "default", true, `-t "cross repo" -b "same branch"`)
eq(t, err, nil) require.NoError(t, err)
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body) bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
reqBody := struct { reqBody := struct {
@ -575,9 +633,9 @@ func TestPRCreate_cross_repo_same_branch(t *testing.T) {
} }
func TestPRCreate_survey_defaults_multicommit(t *testing.T) { func TestPRCreate_survey_defaults_multicommit(t *testing.T) {
initBlankContext("", "OWNER/REPO", "cool_bug-fixes")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -623,8 +681,8 @@ func TestPRCreate_survey_defaults_multicommit(t *testing.T) {
}, },
}) })
output, err := RunCommand(`pr create`) output, err := runCommand(http, nil, "cool_bug-fixes", true, ``)
eq(t, err, nil) require.NoError(t, err)
bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body) bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body)
reqBody := struct { reqBody := struct {
@ -652,10 +710,9 @@ func TestPRCreate_survey_defaults_multicommit(t *testing.T) {
} }
func TestPRCreate_survey_defaults_monocommit(t *testing.T) { func TestPRCreate_survey_defaults_monocommit(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t) defer http.Verify(t)
http.Register(httpmock.GraphQL(`query RepositoryNetwork\b`), httpmock.StringResponse(httpmock.RepoNetworkStubResponse("OWNER", "REPO", "master", "WRITE"))) http.Register(httpmock.GraphQL(`query RepositoryNetwork\b`), httpmock.StringResponse(httpmock.RepoNetworkStubResponse("OWNER", "REPO", "master", "WRITE")))
http.Register(httpmock.GraphQL(`query RepositoryFindFork\b`), httpmock.StringResponse(` http.Register(httpmock.GraphQL(`query RepositoryFindFork\b`), httpmock.StringResponse(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -708,15 +765,15 @@ func TestPRCreate_survey_defaults_monocommit(t *testing.T) {
}, },
}) })
output, err := RunCommand(`pr create`) output, err := runCommand(http, nil, "feature", true, ``)
eq(t, err, nil) eq(t, err, nil)
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n") eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
} }
func TestPRCreate_survey_autofill_nontty(t *testing.T) { func TestPRCreate_survey_autofill_nontty(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(false)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -744,8 +801,8 @@ func TestPRCreate_survey_autofill_nontty(t *testing.T) {
cs.Stub("") // git push cs.Stub("") // git push
cs.Stub("") // browser open cs.Stub("") // browser open
output, err := RunCommand(`pr create -f`) output, err := runCommand(http, nil, "feature", false, `-f`)
eq(t, err, nil) require.NoError(t, err)
bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body) bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body)
reqBody := struct { reqBody := struct {
@ -775,9 +832,9 @@ func TestPRCreate_survey_autofill_nontty(t *testing.T) {
} }
func TestPRCreate_survey_autofill(t *testing.T) { func TestPRCreate_survey_autofill(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -805,8 +862,8 @@ func TestPRCreate_survey_autofill(t *testing.T) {
cs.Stub("") // git push cs.Stub("") // git push
cs.Stub("") // browser open cs.Stub("") // browser open
output, err := RunCommand(`pr create -f`) output, err := runCommand(http, nil, "feature", true, `-f`)
eq(t, err, nil) require.NoError(t, err)
bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body) bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body)
reqBody := struct { reqBody := struct {
@ -834,9 +891,9 @@ func TestPRCreate_survey_autofill(t *testing.T) {
} }
func TestPRCreate_defaults_error_autofill(t *testing.T) { func TestPRCreate_defaults_error_autofill(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
cs, cmdTeardown := test.InitCmdStubber() cs, cmdTeardown := test.InitCmdStubber()
@ -847,15 +904,15 @@ func TestPRCreate_defaults_error_autofill(t *testing.T) {
cs.Stub("") // git status cs.Stub("") // git status
cs.Stub("") // git log cs.Stub("") // git log
_, err := RunCommand("pr create -f") _, err := runCommand(http, nil, "feature", true, "-f")
eq(t, err.Error(), "could not compute title or body defaults: could not find any commits between origin/master and feature") eq(t, err.Error(), "could not compute title or body defaults: could not find any commits between origin/master and feature")
} }
func TestPRCreate_defaults_error_web(t *testing.T) { func TestPRCreate_defaults_error_web(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
cs, cmdTeardown := test.InitCmdStubber() cs, cmdTeardown := test.InitCmdStubber()
@ -866,15 +923,15 @@ func TestPRCreate_defaults_error_web(t *testing.T) {
cs.Stub("") // git status cs.Stub("") // git status
cs.Stub("") // git log cs.Stub("") // git log
_, err := RunCommand("pr create -w") _, err := runCommand(http, nil, "feature", true, "-w")
eq(t, err.Error(), "could not compute title or body defaults: could not find any commits between origin/master and feature") eq(t, err.Error(), "could not compute title or body defaults: could not find any commits between origin/master and feature")
} }
func TestPRCreate_defaults_error_interactive(t *testing.T) { func TestPRCreate_defaults_error_interactive(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
defer stubTerminal(true)()
http := initFakeHTTP() http := initFakeHTTP()
defer http.Verify(t)
http.StubRepoResponse("OWNER", "REPO") http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(` http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [ { "data": { "repository": { "forks": { "nodes": [
@ -917,8 +974,8 @@ func TestPRCreate_defaults_error_interactive(t *testing.T) {
}, },
}) })
output, err := RunCommand(`pr create`) output, err := runCommand(http, nil, "feature", true, ``)
eq(t, err, nil) require.NoError(t, err)
stderr := string(output.Stderr()) stderr := string(output.Stderr())
eq(t, strings.Contains(stderr, "warning: could not compute title or body defaults: could not find any commits"), true) eq(t, strings.Contains(stderr, "warning: could not compute title or body defaults: could not find any commits"), true)

114
pkg/cmd/pr/shared/params.go Normal file
View file

@ -0,0 +1,114 @@
package shared
import (
"fmt"
"net/url"
"strings"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghrepo"
)
func WithPrAndIssueQueryParams(baseURL, title, body string, assignees, labels, projects []string, milestones []string) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", err
}
q := u.Query()
if title != "" {
q.Set("title", title)
}
if body != "" {
q.Set("body", body)
}
if len(assignees) > 0 {
q.Set("assignees", strings.Join(assignees, ","))
}
if len(labels) > 0 {
q.Set("labels", strings.Join(labels, ","))
}
if len(projects) > 0 {
q.Set("projects", strings.Join(projects, ","))
}
if len(milestones) > 0 {
q.Set("milestone", milestones[0])
}
u.RawQuery = q.Encode()
return u.String(), nil
}
func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *IssueMetadataState) error {
if !tb.HasMetadata() {
return nil
}
if tb.MetadataResult == nil {
resolveInput := api.RepoResolveInput{
Reviewers: tb.Reviewers,
Assignees: tb.Assignees,
Labels: tb.Labels,
Projects: tb.Projects,
Milestones: tb.Milestones,
}
var err error
tb.MetadataResult, err = api.RepoResolveMetadataIDs(client, baseRepo, resolveInput)
if err != nil {
return err
}
}
assigneeIDs, err := tb.MetadataResult.MembersToIDs(tb.Assignees)
if err != nil {
return fmt.Errorf("could not assign user: %w", err)
}
params["assigneeIds"] = assigneeIDs
labelIDs, err := tb.MetadataResult.LabelsToIDs(tb.Labels)
if err != nil {
return fmt.Errorf("could not add label: %w", err)
}
params["labelIds"] = labelIDs
projectIDs, err := tb.MetadataResult.ProjectsToIDs(tb.Projects)
if err != nil {
return fmt.Errorf("could not add to project: %w", err)
}
params["projectIds"] = projectIDs
if len(tb.Milestones) > 0 {
milestoneID, err := tb.MetadataResult.MilestoneToID(tb.Milestones[0])
if err != nil {
return fmt.Errorf("could not add to milestone '%s': %w", tb.Milestones[0], err)
}
params["milestoneId"] = milestoneID
}
if len(tb.Reviewers) == 0 {
return nil
}
var userReviewers []string
var teamReviewers []string
for _, r := range tb.Reviewers {
if strings.ContainsRune(r, '/') {
teamReviewers = append(teamReviewers, r)
} else {
userReviewers = append(userReviewers, r)
}
}
userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers)
if err != nil {
return fmt.Errorf("could not request reviewer: %w", err)
}
params["userReviewerIds"] = userReviewerIDs
teamReviewerIDs, err := tb.MetadataResult.TeamsToIDs(teamReviewers)
if err != nil {
return fmt.Errorf("could not request reviewer: %w", err)
}
params["teamReviewerIds"] = teamReviewerIDs
return nil
}

View file

@ -1,4 +1,4 @@
package command package shared
import ( import (
"fmt" "fmt"
@ -7,21 +7,26 @@ import (
"github.com/cli/cli/api" "github.com/cli/cli/api"
"github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/githubtemplate" "github.com/cli/cli/pkg/githubtemplate"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/prompt" "github.com/cli/cli/pkg/prompt"
"github.com/cli/cli/pkg/surveyext" "github.com/cli/cli/pkg/surveyext"
"github.com/cli/cli/utils" "github.com/cli/cli/utils"
"github.com/spf13/cobra"
) )
type Defaults struct {
Title string
Body string
}
type Action int type Action int
type metadataStateType int type metadataStateType int
const ( const (
issueMetadata metadataStateType = iota IssueMetadata metadataStateType = iota
prMetadata PRMetadata
) )
type issueMetadataState struct { type IssueMetadataState struct {
Type metadataStateType Type metadataStateType
Body string Body string
@ -38,7 +43,7 @@ type issueMetadataState struct {
MetadataResult *api.RepoMetadataResult MetadataResult *api.RepoMetadataResult
} }
func (tb *issueMetadataState) HasMetadata() bool { func (tb *IssueMetadataState) HasMetadata() bool {
return len(tb.Reviewers) > 0 || return len(tb.Reviewers) > 0 ||
len(tb.Assignees) > 0 || len(tb.Assignees) > 0 ||
len(tb.Labels) > 0 || len(tb.Labels) > 0 ||
@ -112,9 +117,9 @@ func selectTemplate(nonLegacyTemplatePaths []string, legacyTemplatePath *string,
for _, p := range nonLegacyTemplatePaths { for _, p := range nonLegacyTemplatePaths {
templateNames = append(templateNames, githubtemplate.ExtractName(p)) templateNames = append(templateNames, githubtemplate.ExtractName(p))
} }
if metadataType == issueMetadata { if metadataType == IssueMetadata {
templateNames = append(templateNames, "Open a blank issue") templateNames = append(templateNames, "Open a blank issue")
} else if metadataType == prMetadata { } else if metadataType == PRMetadata {
templateNames = append(templateNames, "Open a blank pull request") templateNames = append(templateNames, "Open a blank pull request")
} }
@ -143,12 +148,8 @@ func selectTemplate(nonLegacyTemplatePaths []string, legacyTemplatePath *string,
return string(templateContents), nil return string(templateContents), nil
} }
func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClient *api.Client, repo ghrepo.Interface, providedTitle, providedBody string, defs defaults, nonLegacyTemplatePaths []string, legacyTemplatePath *string, allowReviewers, allowMetadata bool) error { // FIXME: this command has too many parameters and responsibilities
editorCommand, err := determineEditor(cmd) func TitleBodySurvey(io *iostreams.IOStreams, editorCommand string, issueState *IssueMetadataState, apiClient *api.Client, repo ghrepo.Interface, providedTitle, providedBody string, defs Defaults, nonLegacyTemplatePaths []string, legacyTemplatePath *string, allowReviewers, allowMetadata bool) error {
if err != nil {
return err
}
issueState.Title = defs.Title issueState.Title = defs.Title
templateContents := "" templateContents := ""
@ -198,7 +199,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie
qs = append(qs, bodyQuestion) qs = append(qs, bodyQuestion)
} }
err = prompt.SurveyAsk(qs, issueState) err := prompt.SurveyAsk(qs, issueState)
if err != nil { if err != nil {
return fmt.Errorf("could not prompt: %w", err) return fmt.Errorf("could not prompt: %w", err)
} }
@ -249,7 +250,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie
Projects: isChosen("Projects"), Projects: isChosen("Projects"),
Milestones: isChosen("Milestone"), Milestones: isChosen("Milestone"),
} }
s := utils.Spinner(cmd.OutOrStderr()) s := utils.Spinner(io.ErrOut)
utils.StartSpinner(s) utils.StartSpinner(s)
issueState.MetadataResult, err = api.RepoMetadata(apiClient, repo, metadataInput) issueState.MetadataResult, err = api.RepoMetadata(apiClient, repo, metadataInput)
utils.StopSpinner(s) utils.StopSpinner(s)
@ -297,7 +298,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie
}, },
}) })
} else { } else {
cmd.PrintErrln("warning: no available reviewers") fmt.Fprintln(io.ErrOut, "warning: no available reviewers")
} }
} }
if isChosen("Assignees") { if isChosen("Assignees") {
@ -311,7 +312,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie
}, },
}) })
} else { } else {
cmd.PrintErrln("warning: no assignable users") fmt.Fprintln(io.ErrOut, "warning: no assignable users")
} }
} }
if isChosen("Labels") { if isChosen("Labels") {
@ -325,7 +326,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie
}, },
}) })
} else { } else {
cmd.PrintErrln("warning: no labels in the repository") fmt.Fprintln(io.ErrOut, "warning: no labels in the repository")
} }
} }
if isChosen("Projects") { if isChosen("Projects") {
@ -339,7 +340,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie
}, },
}) })
} else { } else {
cmd.PrintErrln("warning: no projects to choose from") fmt.Fprintln(io.ErrOut, "warning: no projects to choose from")
} }
} }
if isChosen("Milestone") { if isChosen("Milestone") {
@ -357,7 +358,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie
}, },
}) })
} else { } else {
cmd.PrintErrln("warning: no milestones in the repository") fmt.Fprintln(io.ErrOut, "warning: no milestones in the repository")
} }
} }
values := metadataValues{} values := metadataValues{}