Add wizard that prompts for issue/pr metadata on create

This commit is contained in:
Mislav Marohnić 2020-04-27 18:58:25 +02:00
parent c6d8a4c151
commit aeb08529e7
4 changed files with 243 additions and 62 deletions

View file

@ -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
}

View file

@ -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)
}

View file

@ -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,
},
})

View file

@ -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
}