diff --git a/command/issue.go b/command/issue.go index 192ac593c..cca81db2f 100644 --- a/command/issue.go +++ b/command/issue.go @@ -508,11 +508,7 @@ func issueCreate(cmd *cobra.Command, args []string) error { if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb { openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new") if title != "" || body != "" { - milestone := "" - if len(milestoneTitles) > 0 { - milestone = milestoneTitles[0] - } - openURL, err = withPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestone) + openURL, err = shared.WithPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestoneTitles) if err != nil { 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)) } - action := SubmitAction - tb := issueMetadataState{ - Type: issueMetadata, + action := shared.SubmitAction + tb := shared.IssueMetadataState{ + Type: shared.IssueMetadata, Assignees: assignees, Labels: labelNames, Projects: projectNames, @@ -558,14 +554,20 @@ func issueCreate(cmd *cobra.Command, args []string) error { 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 { return fmt.Errorf("could not collect title and/or body: %w", err) } action = tb.Action - if tb.Action == CancelAction { + if tb.Action == shared.CancelAction { fmt.Fprintln(cmd.ErrOrStderr(), "Discarding.") 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") - milestone := "" - if len(milestoneTitles) > 0 { - milestone = milestoneTitles[0] - } - openURL, err = withPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestone) + openURL, err = shared.WithPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestoneTitles) if err != nil { return err } // TODO could exceed max url length for explorer fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", utils.DisplayURL(openURL)) return utils.OpenInBrowser(openURL) - } else if action == SubmitAction { + } else if action == shared.SubmitAction { params := map[string]interface{}{ "title": title, "body": body, } - err = addMetadataToIssueParams(apiClient, baseRepo, params, &tb) + err = shared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb) if err != nil { return err } @@ -620,82 +618,6 @@ func issueCreate(cmd *cobra.Command, args []string) error { 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) { table := utils.NewTablePrinter(w) for _, issue := range issues { diff --git a/command/pr.go b/command/pr.go index 49604d3b4..2ff6b4286 100644 --- a/command/pr.go +++ b/command/pr.go @@ -19,7 +19,6 @@ func init() { prCmd.PersistentFlags().StringP("repo", "R", "", "Select another repository using the `OWNER/REPO` format") RootCmd.AddCommand(prCmd) - prCmd.AddCommand(prCreateCmd) prCmd.AddCommand(prCloseCmd) prCmd.AddCommand(prReopenCmd) prCmd.AddCommand(prReadyCmd) diff --git a/command/root.go b/command/root.go index 287afee93..4c9412445 100644 --- a/command/root.go +++ b/command/root.go @@ -24,6 +24,7 @@ import ( apiCmd "github.com/cli/cli/pkg/cmd/api" gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create" 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" prMergeCmd "github.com/cli/cli/pkg/cmd/pr/merge" prReviewCmd "github.com/cli/cli/pkg/cmd/pr/review" @@ -179,6 +180,7 @@ func init() { prCmd.AddCommand(prViewCmd.NewCmdView(&repoResolvingCmdFactory, nil)) prCmd.AddCommand(prMergeCmd.NewCmdMerge(&repoResolvingCmdFactory, nil)) prCmd.AddCommand(prStatusCmd.NewCmdStatus(&repoResolvingCmdFactory, nil)) + prCmd.AddCommand(prCreateCmd.NewCmdCreate(&repoResolvingCmdFactory, 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 } -// 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 { externalCmd := exec.Command(args[0], args[1:]...) externalCmd.Stderr = os.Stderr diff --git a/command/pr_create.go b/pkg/cmd/pr/create/create.go similarity index 54% rename from command/pr_create.go rename to pkg/cmd/pr/create/create.go index b9efca0fa..c9e1fb6ab 100644 --- a/command/pr_create.go +++ b/pkg/cmd/pr/create/create.go @@ -1,9 +1,9 @@ -package command +package create import ( "errors" "fmt" - "net/url" + "net/http" "strings" "time" @@ -11,60 +11,116 @@ import ( "github.com/cli/cli/api" "github.com/cli/cli/context" "github.com/cli/cli/git" + "github.com/cli/cli/internal/config" "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/githubtemplate" + "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/utils" "github.com/spf13/cobra" ) -type defaults struct { - Title string - Body string +type CreateOptions struct { + HttpClient func() (*http.Client, error) + 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) { - commits, err := git.Commits(baseRef, headRef) +func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { + 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 { - return defaults{}, err + return err } + client := api.NewClientFromHTTP(httpClient) - 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 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() + remotes, err := opts.Remotes() if err != nil { return err } - client, err := apiClientForContext(ctx) - 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) + repoContext, err := context.ResolveRemotesToRepos(remotes, client, opts.RepoOverride) if err != nil { return err } @@ -74,7 +130,7 @@ func prCreate(cmd *cobra.Command, _ []string) error { return fmt.Errorf("could not determine base repository: %w", err) } - headBranch, err := ctx.Branch() + headBranch, err := opts.Branch() if err != nil { 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") - if err != nil { - return err - } + baseBranch := opts.BaseBranch if baseBranch == "" { baseBranch = baseRepo.DefaultBranchRef.Name } @@ -114,39 +167,12 @@ func prCreate(cmd *cobra.Command, _ []string) error { } 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 - if milestoneTitle, err := cmd.Flags().GetString("milestone"); err != nil { - return fmt.Errorf("could not parse milestone: %w", err) - } else if milestoneTitle != "" { - milestoneTitles = append(milestoneTitles, milestoneTitle) + if opts.Milestone != "" { + milestoneTitles = []string{opts.Milestone} } baseTrackingBranch := baseBranch @@ -155,23 +181,16 @@ func prCreate(cmd *cobra.Command, _ []string) error { } defs, defaultsErr := computeDefaults(baseTrackingBranch, headBranch) - isWeb, err := cmd.Flags().GetBool("web") - if err != nil { - return fmt.Errorf("could not parse web: %q", err) - } + title := opts.Title + body := opts.Body - autofill, err := cmd.Flags().GetBool("fill") - if err != nil { - return fmt.Errorf("could not parse fill: %q", err) - } - - action := SubmitAction - if isWeb { - action = PreviewAction + action := shared.SubmitAction + if opts.WebMode { + action = shared.PreviewAction if (title == "" || body == "") && defaultsErr != nil { return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr) } - } else if autofill { + } else if opts.Autofill { if defaultsErr != nil { 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 } - if !isWeb { + if !opts.WebMode { headBranchLabel := headBranch if headRepo != nil && !ghrepo.IsSame(baseRepo, headRepo) { 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") - if err != nil { - return fmt.Errorf("could not parse draft: %w", err) - } + isTerminal := opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY() - if !isWeb && !autofill { + if !opts.WebMode && !opts.Autofill { 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" } - if connectedToTerminal(cmd) { - fmt.Fprintf(colorableErr(cmd), message, + if isTerminal { + fmt.Fprintf(opts.IO.ErrOut, message, 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) + fmt.Fprintf(opts.IO.ErrOut, "%s warning: could not compute title or body defaults: %s\n", utils.Yellow("!"), defaultsErr) } } } - tb := issueMetadataState{ - Type: prMetadata, - Reviewers: reviewers, - Assignees: assignees, - Labels: labelNames, - Projects: projectNames, + tb := shared.IssueMetadataState{ + Type: shared.PRMetadata, + Reviewers: opts.Reviewers, + Assignees: opts.Assignees, + Labels: opts.Labels, + Projects: opts.Projects, Milestones: milestoneTitles, } - if !connectedToTerminal(cmd) { - if !isWeb && (!cmd.Flags().Changed("title") && !autofill) { - return errors.New("--title or --fill required when not attached to a tty") - } - } + interactive := isTerminal && !(opts.TitleProvided && opts.BodyProvided) - interactive := connectedToTerminal(cmd) && !(cmd.Flags().Changed("title") && cmd.Flags().Changed("body")) - - if !isWeb && !autofill && interactive { + if !opts.WebMode && !opts.Autofill && interactive { var nonLegacyTemplateFiles []string var legacyTemplateFile *string 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") 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 { return fmt.Errorf("could not collect title and/or body: %w", err) } action = tb.Action - if action == CancelAction { - fmt.Fprintln(cmd.ErrOrStderr(), "Discarding.") + if action == shared.CancelAction { + fmt.Fprintln(opts.IO.ErrOut, "Discarding.") 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") } - 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 // if a head repository could not be determined so far, automatically create // 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 // can push to it. 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 gitRemote, err := git.AddRemote("fork", headRepoURL) @@ -326,7 +341,7 @@ func prCreate(cmd *cobra.Command, _ []string) error { pushTries++ // first wait 2 seconds after forking, then 4s, then 6s 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) continue } @@ -336,16 +351,16 @@ func prCreate(cmd *cobra.Command, _ []string) error { } } - if action == SubmitAction { + if action == shared.SubmitAction { params := map[string]interface{}{ "title": title, "body": body, - "draft": isDraft, + "draft": opts.IsDraft, "baseRefName": baseBranch, "headRefName": headBranchLabel, } - err = addMetadataToIssueParams(client, baseRepo, params, &tb) + err = shared.AddMetadataToIssueParams(client, baseRepo, params, &tb) if err != nil { return err } @@ -355,19 +370,14 @@ func prCreate(cmd *cobra.Command, _ []string) error { return fmt.Errorf("failed to create pull request: %w", err) } - fmt.Fprintln(cmd.OutOrStdout(), pr.URL) - } else if action == PreviewAction { - milestone := "" - if len(milestoneTitles) > 0 { - milestone = milestoneTitles[0] - } - openURL, err := generateCompareURL(baseRepo, baseBranch, headBranchLabel, title, body, assignees, labelNames, projectNames, milestone) + fmt.Fprintln(opts.IO.Out, pr.URL) + } else if action == shared.PreviewAction { + openURL, err := generateCompareURL(baseRepo, baseBranch, headBranchLabel, title, body, tb.Assignees, tb.Labels, tb.Projects, tb.Milestones) if err != nil { return err } - if connectedToTerminal(cmd) { - // TODO could exceed max url length for explorer - fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", utils.DisplayURL(openURL)) + if isTerminal { + fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL)) } return utils.OpenInBrowser(openURL) } else { @@ -377,6 +387,34 @@ func prCreate(cmd *cobra.Command, _ []string) error { 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 { refsForLookup := []string{"HEAD"} var trackingRefs []git.TrackingRef @@ -418,73 +456,11 @@ func determineTrackingBranch(remotes context.Remotes, headBranch string) *git.Tr return nil } -func withPrAndIssueQueryParams(baseURL, title, body string, assignees, labels, projects []string, milestone 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) { +func generateCompareURL(r ghrepo.Interface, base, head, title, body string, assignees, labels, projects []string, milestones []string) (string, error) { 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 { return "", err } 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`") -} diff --git a/command/pr_create_test.go b/pkg/cmd/pr/create/create_test.go similarity index 88% rename from command/pr_create_test.go rename to pkg/cmd/pr/create/create_test.go index c6daf8bef..e50a72c63 100644 --- a/command/pr_create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1,25 +1,93 @@ -package command +package create import ( "bytes" "encoding/json" "io/ioutil" + "net/http" + "reflect" "strings" "testing" "github.com/cli/cli/context" "github.com/cli/cli/git" + "github.com/cli/cli/internal/config" "github.com/cli/cli/internal/ghrepo" + "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/httpmock" + "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/pkg/prompt" "github.com/cli/cli/test" + "github.com/google/shlex" "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) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(false)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") http.StubResponse(200, bytes.NewBufferString(` { "data": { "repository": { "forks": { "nodes": [ @@ -36,8 +104,8 @@ func TestPRCreate_nontty_web(t *testing.T) { cs.Stub("") // git push cs.Stub("") // browser - output, err := RunCommand(`pr create --web`) - eq(t, err, nil) + output, err := runCommand(http, nil, "feature", false, `--web`) + require.NoError(t, err) eq(t, output.String(), "") eq(t, output.Stderr(), "") @@ -50,33 +118,23 @@ func TestPRCreate_nontty_web(t *testing.T) { } func TestPRCreate_nontty_insufficient_flags(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(false)() http := initFakeHTTP() - http.StubRepoResponse("OWNER", "REPO") - http.StubResponse(200, bytes.NewBufferString(` - { "data": { "repository": { "forks": { "nodes": [ - ] } } } } - `)) - http.StubResponse(200, bytes.NewBufferString(` - { "data": { "repository": { "pullRequests": { "nodes" : [ - ] } } } } - `)) + defer http.Verify(t) - output, err := RunCommand("pr create") + output, err := runCommand(http, nil, "feature", false, "") if err == nil { 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()) } func TestPRCreate_nontty(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(false)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") http.StubResponse(200, bytes.NewBufferString(` { "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("") // git push - output, err := RunCommand(`pr create -t "my title" -b "my body"`) - eq(t, err, nil) + output, err := runCommand(http, nil, "feature", false, `-t "my title" -b "my body"`) + require.NoError(t, err) bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body) reqBody := struct { @@ -129,9 +187,9 @@ func TestPRCreate_nontty(t *testing.T) { } func TestPRCreate(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") http.StubResponse(200, bytes.NewBufferString(` { "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("") // git push - output, err := RunCommand(`pr create -t "my title" -b "my body"`) - eq(t, err, nil) + output, err := runCommand(http, nil, "feature", true, `-t "my title" -b "my body"`) + require.NoError(t, err) bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body) reqBody := struct { @@ -183,8 +241,6 @@ func TestPRCreate(t *testing.T) { } func TestPRCreate_metadata(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() 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("") // 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, output.String(), "https://github.com/OWNER/REPO/pull/12\n") } func TestPRCreate_withForking(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponseWithPermission("OWNER", "REPO", "READ") http.StubResponse(200, bytes.NewBufferString(` { "data": { "repository": { "forks": { "nodes": [ @@ -344,17 +400,17 @@ func TestPRCreate_withForking(t *testing.T) { cs.Stub("") // git remote add cs.Stub("") // git push - output, err := RunCommand(`pr create -t title -b body`) - eq(t, err, nil) + output, err := runCommand(http, nil, "feature", true, `-t title -b body`) + require.NoError(t, err) eq(t, http.Requests[3].URL.Path, "/repos/OWNER/REPO/forks") eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n") } func TestPRCreate_alreadyExists(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") http.StubResponse(200, bytes.NewBufferString(` { "data": { "repository": { "forks": { "nodes": [ @@ -376,7 +432,7 @@ func TestPRCreate_alreadyExists(t *testing.T) { cs.Stub("") // git status cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log - _, err := RunCommand(`pr create`) + _, err := runCommand(http, nil, "feature", true, ``) if err == nil { t.Fatal("error expected, got nil") } @@ -386,9 +442,9 @@ func TestPRCreate_alreadyExists(t *testing.T) { } func TestPRCreate_alreadyExistsDifferentBase(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") http.StubResponse(200, bytes.NewBufferString(` { "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("") // 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 { t.Errorf("got unexpected error %q", err) } } func TestPRCreate_web(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") http.StubResponse(200, bytes.NewBufferString(` { "data": { "repository": { "forks": { "nodes": [ @@ -438,8 +494,8 @@ func TestPRCreate_web(t *testing.T) { cs.Stub("") // git push cs.Stub("") // browser - output, err := RunCommand(`pr create --web`) - eq(t, err, nil) + output, err := runCommand(http, nil, "feature", true, `--web`) + require.NoError(t, err) eq(t, output.String(), "") 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) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() + defer http.Verify(t) http.StubRepoResponse("OWNER", "REPO") 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("") // 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, 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) { - defer stubTerminal(true)() - ctx := context.NewBlank() - ctx.SetBranch("default") - ctx.SetRemotes(map[string]string{ - "origin": "OWNER/REPO", - "fork": "MYSELF/REPO", - }) - initContext = func() context.Context { - return ctx + remotes := context.Remotes{ + { + Remote: &git.Remote{Name: "origin"}, + Repo: ghrepo.New("OWNER", "REPO"), + }, + { + Remote: &git.Remote{Name: "fork"}, + Repo: ghrepo.New("MYSELF", "REPO"), + }, } + http := initFakeHTTP() + defer http.Verify(t) + http.StubResponse(200, bytes.NewBufferString(` { "data": { "repo_000": { "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("") // git push - output, err := RunCommand(`pr create -t "cross repo" -b "same branch"`) - eq(t, err, nil) + output, err := runCommand(http, remotes, "default", true, `-t "cross repo" -b "same branch"`) + require.NoError(t, err) bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body) reqBody := struct { @@ -575,9 +633,9 @@ func TestPRCreate_cross_repo_same_branch(t *testing.T) { } func TestPRCreate_survey_defaults_multicommit(t *testing.T) { - initBlankContext("", "OWNER/REPO", "cool_bug-fixes") - defer stubTerminal(true)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") http.StubResponse(200, bytes.NewBufferString(` { "data": { "repository": { "forks": { "nodes": [ @@ -623,8 +681,8 @@ func TestPRCreate_survey_defaults_multicommit(t *testing.T) { }, }) - output, err := RunCommand(`pr create`) - eq(t, err, nil) + output, err := runCommand(http, nil, "cool_bug-fixes", true, ``) + require.NoError(t, err) bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body) reqBody := struct { @@ -652,10 +710,9 @@ func TestPRCreate_survey_defaults_multicommit(t *testing.T) { } func TestPRCreate_survey_defaults_monocommit(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() defer http.Verify(t) + http.Register(httpmock.GraphQL(`query RepositoryNetwork\b`), httpmock.StringResponse(httpmock.RepoNetworkStubResponse("OWNER", "REPO", "master", "WRITE"))) http.Register(httpmock.GraphQL(`query RepositoryFindFork\b`), httpmock.StringResponse(` { "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, output.String(), "https://github.com/OWNER/REPO/pull/12\n") } func TestPRCreate_survey_autofill_nontty(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(false)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") http.StubResponse(200, bytes.NewBufferString(` { "data": { "repository": { "forks": { "nodes": [ @@ -744,8 +801,8 @@ func TestPRCreate_survey_autofill_nontty(t *testing.T) { cs.Stub("") // git push cs.Stub("") // browser open - output, err := RunCommand(`pr create -f`) - eq(t, err, nil) + output, err := runCommand(http, nil, "feature", false, `-f`) + require.NoError(t, err) bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body) reqBody := struct { @@ -775,9 +832,9 @@ func TestPRCreate_survey_autofill_nontty(t *testing.T) { } func TestPRCreate_survey_autofill(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") http.StubResponse(200, bytes.NewBufferString(` { "data": { "repository": { "forks": { "nodes": [ @@ -805,8 +862,8 @@ func TestPRCreate_survey_autofill(t *testing.T) { cs.Stub("") // git push cs.Stub("") // browser open - output, err := RunCommand(`pr create -f`) - eq(t, err, nil) + output, err := runCommand(http, nil, "feature", true, `-f`) + require.NoError(t, err) bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body) reqBody := struct { @@ -834,9 +891,9 @@ func TestPRCreate_survey_autofill(t *testing.T) { } func TestPRCreate_defaults_error_autofill(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") cs, cmdTeardown := test.InitCmdStubber() @@ -847,15 +904,15 @@ func TestPRCreate_defaults_error_autofill(t *testing.T) { cs.Stub("") // git status 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") } func TestPRCreate_defaults_error_web(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") cs, cmdTeardown := test.InitCmdStubber() @@ -866,15 +923,15 @@ func TestPRCreate_defaults_error_web(t *testing.T) { cs.Stub("") // git status 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") } func TestPRCreate_defaults_error_interactive(t *testing.T) { - initBlankContext("", "OWNER/REPO", "feature") - defer stubTerminal(true)() http := initFakeHTTP() + defer http.Verify(t) + http.StubRepoResponse("OWNER", "REPO") http.StubResponse(200, bytes.NewBufferString(` { "data": { "repository": { "forks": { "nodes": [ @@ -917,8 +974,8 @@ func TestPRCreate_defaults_error_interactive(t *testing.T) { }, }) - output, err := RunCommand(`pr create`) - eq(t, err, nil) + output, err := runCommand(http, nil, "feature", true, ``) + require.NoError(t, err) stderr := string(output.Stderr()) eq(t, strings.Contains(stderr, "warning: could not compute title or body defaults: could not find any commits"), true) diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go new file mode 100644 index 000000000..d2556cafe --- /dev/null +++ b/pkg/cmd/pr/shared/params.go @@ -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 +} diff --git a/command/title_body_survey.go b/pkg/cmd/pr/shared/title_body_survey.go similarity index 88% rename from command/title_body_survey.go rename to pkg/cmd/pr/shared/title_body_survey.go index 94ec89914..3afeba803 100644 --- a/command/title_body_survey.go +++ b/pkg/cmd/pr/shared/title_body_survey.go @@ -1,4 +1,4 @@ -package command +package shared import ( "fmt" @@ -7,21 +7,26 @@ import ( "github.com/cli/cli/api" "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/pkg/githubtemplate" + "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/pkg/prompt" "github.com/cli/cli/pkg/surveyext" "github.com/cli/cli/utils" - "github.com/spf13/cobra" ) +type Defaults struct { + Title string + Body string +} + type Action int type metadataStateType int const ( - issueMetadata metadataStateType = iota - prMetadata + IssueMetadata metadataStateType = iota + PRMetadata ) -type issueMetadataState struct { +type IssueMetadataState struct { Type metadataStateType Body string @@ -38,7 +43,7 @@ type issueMetadataState struct { MetadataResult *api.RepoMetadataResult } -func (tb *issueMetadataState) HasMetadata() bool { +func (tb *IssueMetadataState) HasMetadata() bool { return len(tb.Reviewers) > 0 || len(tb.Assignees) > 0 || len(tb.Labels) > 0 || @@ -112,9 +117,9 @@ func selectTemplate(nonLegacyTemplatePaths []string, legacyTemplatePath *string, for _, p := range nonLegacyTemplatePaths { templateNames = append(templateNames, githubtemplate.ExtractName(p)) } - if metadataType == issueMetadata { + if metadataType == IssueMetadata { templateNames = append(templateNames, "Open a blank issue") - } else if metadataType == prMetadata { + } else if metadataType == PRMetadata { templateNames = append(templateNames, "Open a blank pull request") } @@ -143,12 +148,8 @@ func selectTemplate(nonLegacyTemplatePaths []string, legacyTemplatePath *string, 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 { - editorCommand, err := determineEditor(cmd) - if err != nil { - return err - } - +// FIXME: this command has too many parameters and responsibilities +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 { issueState.Title = defs.Title templateContents := "" @@ -198,7 +199,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie qs = append(qs, bodyQuestion) } - err = prompt.SurveyAsk(qs, issueState) + err := prompt.SurveyAsk(qs, issueState) if err != nil { return fmt.Errorf("could not prompt: %w", err) } @@ -249,7 +250,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie Projects: isChosen("Projects"), Milestones: isChosen("Milestone"), } - s := utils.Spinner(cmd.OutOrStderr()) + s := utils.Spinner(io.ErrOut) utils.StartSpinner(s) issueState.MetadataResult, err = api.RepoMetadata(apiClient, repo, metadataInput) utils.StopSpinner(s) @@ -297,7 +298,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie }, }) } else { - cmd.PrintErrln("warning: no available reviewers") + fmt.Fprintln(io.ErrOut, "warning: no available reviewers") } } if isChosen("Assignees") { @@ -311,7 +312,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie }, }) } else { - cmd.PrintErrln("warning: no assignable users") + fmt.Fprintln(io.ErrOut, "warning: no assignable users") } } if isChosen("Labels") { @@ -325,7 +326,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie }, }) } else { - cmd.PrintErrln("warning: no labels in the repository") + fmt.Fprintln(io.ErrOut, "warning: no labels in the repository") } } if isChosen("Projects") { @@ -339,7 +340,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie }, }) } else { - cmd.PrintErrln("warning: no projects to choose from") + fmt.Fprintln(io.ErrOut, "warning: no projects to choose from") } } if isChosen("Milestone") { @@ -357,7 +358,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie }, }) } else { - cmd.PrintErrln("warning: no milestones in the repository") + fmt.Fprintln(io.ErrOut, "warning: no milestones in the repository") } } values := metadataValues{}