Add wizard that prompts for issue/pr metadata on create
This commit is contained in:
parent
c6d8a4c151
commit
aeb08529e7
4 changed files with 243 additions and 62 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue