From aeb08529e7bbbd0434884e8c5bb8b9e479a7a8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 27 Apr 2020 18:58:25 +0200 Subject: [PATCH] Add wizard that prompts for issue/pr metadata on create --- command/issue.go | 35 +++--- command/pr_create.go | 45 ++++--- command/pr_create_test.go | 6 +- command/title_body_survey.go | 219 ++++++++++++++++++++++++++++++----- 4 files changed, 243 insertions(+), 62 deletions(-) diff --git a/command/issue.go b/command/issue.go index 486f99cd2..29a3bb34d 100644 --- a/command/issue.go +++ b/command/issue.go @@ -371,8 +371,6 @@ func issueCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("could not parse milestone: %w", err) } - hasMetadata := len(assignees) > 0 || len(labelNames) > 0 || len(projectNames) > 0 || milestoneTitle != "" - if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb { // TODO: move URL generation into GitHubRepository openURL := fmt.Sprintf("https://github.com/%s/issues/new", ghrepo.FullName(baseRepo)) @@ -405,11 +403,17 @@ func issueCreate(cmd *cobra.Command, args []string) error { } action := SubmitAction + tb := titleBody{ + Assignees: assignees, + Labels: labelNames, + Projects: projectNames, + Milestone: milestoneTitle, + } interactive := title == "" || body == "" if interactive { - tb, err := titleBodySurvey(cmd, title, body, defaults{}, templateFiles, !hasMetadata) + err := titleBodySurvey(cmd, &tb, apiClient, baseRepo, title, body, defaults{}, templateFiles, false) if err != nil { return fmt.Errorf("could not collect title and/or body: %w", err) } @@ -446,19 +450,22 @@ func issueCreate(cmd *cobra.Command, args []string) error { "body": body, } - if hasMetadata { - metadataInput := api.RepoMetadataInput{ - Assignees: len(assignees) > 0, - Labels: len(labelNames) > 0, - Projects: len(projectNames) > 0, - Milestones: milestoneTitle != "", + if tb.HasMetadata() { + if tb.MetadataResult == nil { + metadataInput := api.RepoMetadataInput{ + Assignees: len(tb.Assignees) > 0, + Labels: len(tb.Labels) > 0, + Projects: len(tb.Projects) > 0, + Milestones: tb.Milestone != "", + } + + tb.MetadataResult, err = api.RepoMetadata(apiClient, baseRepo, metadataInput) + if err != nil { + return err + } } - metadata, err := api.RepoMetadata(apiClient, baseRepo, metadataInput) - if err != nil { - return err - } - err = addMetadataToIssueParams(params, metadata, assignees, labelNames, projectNames, milestoneTitle) + err = addMetadataToIssueParams(params, tb.MetadataResult, tb.Assignees, tb.Labels, tb.Projects, tb.Milestone) if err != nil { return err } diff --git a/command/pr_create.go b/command/pr_create.go index afd8d1101..d24a05277 100644 --- a/command/pr_create.go +++ b/command/pr_create.go @@ -145,8 +145,6 @@ func prCreate(cmd *cobra.Command, _ []string) error { return fmt.Errorf("could not parse milestone: %w", err) } - hasMetadata := len(reviewers) > 0 || len(assignees) > 0 || len(labelNames) > 0 || len(projectNames) > 0 || milestoneTitle != "" - baseTrackingBranch := baseBranch if baseRemote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName()); err == nil { baseTrackingBranch = fmt.Sprintf("%s/%s", baseRemote.Name, baseBranch) @@ -202,6 +200,14 @@ func prCreate(cmd *cobra.Command, _ []string) error { } } + tb := titleBody{ + Reviewers: reviewers, + Assignees: assignees, + Labels: labelNames, + Projects: projectNames, + Milestone: milestoneTitle, + } + // TODO: only drop into interactive mode if stdin & stdout are a tty if !isWeb && !autofill && (title == "" || body == "") { var templateFiles []string @@ -210,7 +216,7 @@ func prCreate(cmd *cobra.Command, _ []string) error { templateFiles = githubtemplate.Find(rootDir, "PULL_REQUEST_TEMPLATE") } - tb, err := titleBodySurvey(cmd, title, body, defs, templateFiles, !hasMetadata) + err := titleBodySurvey(cmd, &tb, client, baseRepo, title, body, defs, templateFiles, true) if err != nil { return fmt.Errorf("could not collect title and/or body: %w", err) } @@ -316,27 +322,30 @@ func prCreate(cmd *cobra.Command, _ []string) error { "headRefName": headBranchLabel, } - if hasMetadata { - metadataInput := api.RepoMetadataInput{ - Reviewers: len(reviewers) > 0, - Assignees: len(assignees) > 0, - Labels: len(labelNames) > 0, - Projects: len(projectNames) > 0, - Milestones: milestoneTitle != "", + if tb.HasMetadata() { + if tb.MetadataResult == nil { + metadataInput := api.RepoMetadataInput{ + Reviewers: len(tb.Reviewers) > 0, + Assignees: len(tb.Assignees) > 0, + Labels: len(tb.Labels) > 0, + Projects: len(tb.Projects) > 0, + Milestones: tb.Milestone != "", + } + + tb.MetadataResult, err = api.RepoMetadata(client, baseRepo, metadataInput) + if err != nil { + return err + } } - metadata, err := api.RepoMetadata(client, baseRepo, metadataInput) - if err != nil { - return err - } - err = addMetadataToIssueParams(params, metadata, assignees, labelNames, projectNames, milestoneTitle) + err = addMetadataToIssueParams(params, tb.MetadataResult, tb.Assignees, tb.Labels, tb.Projects, tb.Milestone) if err != nil { return err } var userReviewers []string var teamReviewers []string - for _, r := range reviewers { + for _, r := range tb.Reviewers { if strings.ContainsRune(r, '/') { teamReviewers = append(teamReviewers, r) } else { @@ -344,13 +353,13 @@ func prCreate(cmd *cobra.Command, _ []string) error { } } - userReviewerIDs, err := metadata.MembersToIDs(userReviewers) + userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers) if err != nil { return fmt.Errorf("could not request reviewer: %w", err) } params["userReviewerIds"] = userReviewerIDs - teamReviewerIDs, err := metadata.TeamsToIDs(teamReviewers) + teamReviewerIDs, err := tb.MetadataResult.TeamsToIDs(teamReviewers) if err != nil { return fmt.Errorf("could not request reviewer: %w", err) } diff --git a/command/pr_create_test.go b/command/pr_create_test.go index f3a0954c8..a13b6e736 100644 --- a/command/pr_create_test.go +++ b/command/pr_create_test.go @@ -373,7 +373,7 @@ func TestPRCreate_survey_defaults_multicommit(t *testing.T) { as.Stub([]*QuestionStub{ { Name: "confirmation", - Value: 1, + Value: 0, }, }) @@ -450,7 +450,7 @@ func TestPRCreate_survey_defaults_monocommit(t *testing.T) { as.Stub([]*QuestionStub{ { Name: "confirmation", - Value: 1, + Value: 0, }, }) @@ -617,7 +617,7 @@ func TestPRCreate_defaults_error_interactive(t *testing.T) { as.Stub([]*QuestionStub{ { Name: "confirmation", - Value: 0, + Value: 1, }, }) diff --git a/command/title_body_survey.go b/command/title_body_survey.go index d74d070ee..9199d8015 100644 --- a/command/title_body_survey.go +++ b/command/title_body_survey.go @@ -5,6 +5,8 @@ import ( "os" "github.com/AlecAivazis/survey/v2" + "github.com/cli/cli/api" + "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/pkg/githubtemplate" "github.com/cli/cli/pkg/surveyext" "github.com/spf13/cobra" @@ -16,24 +18,52 @@ type titleBody struct { Body string Title string Action Action + + Metadata []string + Reviewers []string + Assignees []string + Labels []string + Projects []string + Milestone string + + MetadataResult *api.RepoMetadataResult +} + +func (tb *titleBody) HasMetadata() bool { + return len(tb.Reviewers) > 0 || + len(tb.Assignees) > 0 || + len(tb.Labels) > 0 || + len(tb.Projects) > 0 || + tb.Milestone != "" } const ( - PreviewAction Action = iota - SubmitAction + SubmitAction Action = iota + PreviewAction CancelAction + MetadataAction ) var SurveyAsk = func(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error { return survey.Ask(qs, response, opts...) } -func confirmSubmission(allowPreview bool) (Action, error) { - options := []string{} +func confirmSubmission(allowPreview bool, allowMetadata bool) (Action, error) { + const ( + submitLabel = "Submit" + previewLabel = "Continue in browser" + metadataLabel = "Add metadata" + cancelLabel = "Cancel" + ) + + options := []string{submitLabel} if allowPreview { - options = append(options, "Preview in browser") + options = append(options, previewLabel) } - options = append(options, "Submit", "Cancel") + if allowMetadata { + options = append(options, metadataLabel) + } + options = append(options, cancelLabel) confirmAnswers := struct { Confirmation int @@ -53,11 +83,18 @@ func confirmSubmission(allowPreview bool) (Action, error) { return -1, fmt.Errorf("could not prompt: %w", err) } - choice := confirmAnswers.Confirmation - if !allowPreview { - choice++ + switch options[confirmAnswers.Confirmation] { + case submitLabel: + return SubmitAction, nil + case previewLabel: + return PreviewAction, nil + case metadataLabel: + return MetadataAction, nil + case cancelLabel: + return CancelAction, nil + default: + return -1, fmt.Errorf("invalid index: %d", confirmAnswers.Confirmation) } - return Action(choice), nil } func selectTemplate(templatePaths []string) (string, error) { @@ -88,19 +125,18 @@ func selectTemplate(templatePaths []string) (string, error) { return string(templateContents), nil } -func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, defs defaults, templatePaths []string, allowPreview bool) (*titleBody, error) { +func titleBodySurvey(cmd *cobra.Command, data *titleBody, apiClient *api.Client, repo ghrepo.Interface, providedTitle, providedBody string, defs defaults, templatePaths []string, allowReviewers bool) error { editorCommand := os.Getenv("GH_EDITOR") if editorCommand == "" { ctx := contextForCommand(cmd) cfg, err := ctx.Config() if err != nil { - return nil, fmt.Errorf("could not read config: %w", err) + return fmt.Errorf("could not read config: %w", err) } editorCommand, _ = cfg.Get(defaultHostname, "editor") } - var inProgress titleBody - inProgress.Title = defs.Title + data.Title = defs.Title templateContents := "" if providedBody == "" { @@ -108,11 +144,11 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, def var err error templateContents, err = selectTemplate(templatePaths) if err != nil { - return nil, err + return err } - inProgress.Body = templateContents + data.Body = templateContents } else { - inProgress.Body = defs.Body + data.Body = defs.Body } } @@ -120,7 +156,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, def Name: "title", Prompt: &survey.Input{ Message: "Title", - Default: inProgress.Title, + Default: data.Title, }, } bodyQuestion := &survey.Question{ @@ -130,7 +166,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, def Editor: &survey.Editor{ Message: "Body", FileName: "*.md", - Default: inProgress.Body, + Default: data.Body, HideDefault: true, AppendDefault: true, }, @@ -145,21 +181,150 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, def qs = append(qs, bodyQuestion) } - err := SurveyAsk(qs, &inProgress) + err := SurveyAsk(qs, data) if err != nil { - return nil, fmt.Errorf("could not prompt: %w", err) + return fmt.Errorf("could not prompt: %w", err) } - if inProgress.Body == "" { - inProgress.Body = templateContents + if data.Body == "" { + data.Body = templateContents } - confirmA, err := confirmSubmission(allowPreview) + confirmA, err := confirmSubmission(!data.HasMetadata(), true) if err != nil { - return nil, fmt.Errorf("unable to confirm: %w", err) + return fmt.Errorf("unable to confirm: %w", err) } - inProgress.Action = confirmA + if confirmA == MetadataAction { + isChosen := func(m string) bool { + for _, c := range data.Metadata { + if m == c { + return true + } + } + return false + } - return &inProgress, nil + extraFieldsOptions := []string{} + if allowReviewers { + extraFieldsOptions = append(extraFieldsOptions, "Reviewers") + } + extraFieldsOptions = append(extraFieldsOptions, "Assignees", "Labels", "Projects", "Milestone") + + err = SurveyAsk([]*survey.Question{ + { + Name: "metadata", + Prompt: &survey.MultiSelect{ + Message: "What would you like to add?", + Options: extraFieldsOptions, + }, + }, + }, data) + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } + + // TODO: show spinner while preloading repo metadata + metadataInput := api.RepoMetadataInput{ + Reviewers: isChosen("Reviewers"), + Assignees: isChosen("Assignees"), + Labels: isChosen("Labels"), + Projects: isChosen("Projects"), + Milestones: isChosen("Milestone"), + } + data.MetadataResult, err = api.RepoMetadata(apiClient, repo, metadataInput) + if err != nil { + return fmt.Errorf("error fetching metadata options: %w", err) + } + + var users []string + for _, u := range data.MetadataResult.AssignableUsers { + users = append(users, u.Login) + } + var teams []string + for _, t := range data.MetadataResult.Teams { + teams = append(teams, fmt.Sprintf("%s/%s", repo.RepoOwner(), t.Slug)) + } + var labels []string + for _, l := range data.MetadataResult.Labels { + labels = append(labels, l.Name) + } + var projects []string + for _, l := range data.MetadataResult.Projects { + projects = append(projects, l.Name) + } + milestones := []string{"(none)"} + for _, m := range data.MetadataResult.Milestones { + milestones = append(milestones, m.Title) + } + + var mqs []*survey.Question + if isChosen("Reviewers") { + mqs = append(mqs, &survey.Question{ + Name: "reviewers", + Prompt: &survey.MultiSelect{ + Message: "Reviewers", + Options: append(users, teams...), + Default: data.Reviewers, + }, + }) + } + if isChosen("Assignees") { + mqs = append(mqs, &survey.Question{ + Name: "assignees", + Prompt: &survey.MultiSelect{ + Message: "Assignees", + Options: users, + Default: data.Assignees, + }, + }) + } + if isChosen("Labels") { + mqs = append(mqs, &survey.Question{ + Name: "labels", + Prompt: &survey.MultiSelect{ + Message: "Labels", + Options: labels, + Default: data.Labels, + }, + }) + } + if isChosen("Projects") { + mqs = append(mqs, &survey.Question{ + Name: "projects", + Prompt: &survey.MultiSelect{ + Message: "Projects", + Options: projects, + Default: data.Projects, + }, + }) + } + if isChosen("Milestone") { + mqs = append(mqs, &survey.Question{ + Name: "milestone", + Prompt: &survey.Select{ + Message: "Milestone", + Options: milestones, + Default: data.Milestone, + }, + }) + } + + err = SurveyAsk(mqs, data, survey.WithKeepFilter(true)) + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } + + if data.Milestone == "(none)" { + data.Milestone = "" + } + + confirmA, err = confirmSubmission(!data.HasMetadata(), false) + if err != nil { + return fmt.Errorf("unable to confirm: %w", err) + } + } + + data.Action = confirmA + return nil }