Merge pull request #7816 from cli/prompts-again

port repo edit prompts
This commit is contained in:
Nate Smith 2023-08-10 17:45:49 -07:00 committed by GitHub
commit d9ea221b1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 242 additions and 120 deletions

View file

@ -14,7 +14,7 @@ import (
//go:generate moq -rm -out prompter_mock.go . Prompter
type Prompter interface {
Select(string, string, []string) (int, error)
MultiSelect(string, []string, []string) ([]int, error)
MultiSelect(prompt string, defaults []string, options []string) ([]int, error)
Input(string, string) (string, error)
InputHostname() (string, error)
Password(string) (string, error)

View file

@ -10,21 +10,25 @@ import (
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/pkg/set"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
type iprompter interface {
MultiSelect(prompt string, defaults []string, options []string) ([]int, error)
Input(string, string) (string, error)
Confirm(string, bool) (bool, error)
Select(string, string, []string) (int, error)
}
const (
allowMergeCommits = "Allow Merge Commits"
allowSquashMerge = "Allow Squash Merging"
@ -53,7 +57,7 @@ type EditOptions struct {
RemoveTopics []string
InteractiveMode bool
Detector fd.Detector
Prompter prompter.Prompter
Prompter iprompter
// Cache of current repo topics to avoid retrieving them
// in multiple flows.
topicsCache []string
@ -279,7 +283,7 @@ func editRun(ctx context.Context, opts *EditOptions) error {
return nil
}
func interactiveChoice(r *api.Repository) ([]string, error) {
func interactiveChoice(p iprompter, r *api.Repository) ([]string, error) {
options := []string{
optionDefaultBranchName,
optionDescription,
@ -298,11 +302,14 @@ func interactiveChoice(r *api.Repository) ([]string, error) {
options = append(options, optionAllowForking)
}
var answers []string
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(&survey.MultiSelect{
Message: "What do you want to edit?",
Options: options,
}, &answers, survey.WithPageSize(11))
selected, err := p.MultiSelect("What do you want to edit?", nil, options)
if err != nil {
return nil, err
}
for _, i := range selected {
answers = append(answers, options[i])
}
return answers, err
}
@ -310,38 +317,27 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
for _, v := range r.RepositoryTopics.Nodes {
opts.topicsCache = append(opts.topicsCache, v.Topic.Name)
}
choices, err := interactiveChoice(r)
p := opts.Prompter
choices, err := interactiveChoice(p, r)
if err != nil {
return err
}
for _, c := range choices {
switch c {
case optionDescription:
opts.Edits.Description = &r.Description
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Input{
Message: "Description of the repository",
Default: r.Description,
}, opts.Edits.Description)
answer, err := p.Input("Description of the repository", r.Description)
if err != nil {
return err
}
opts.Edits.Description = &answer
case optionHomePageURL:
opts.Edits.Homepage = &r.HomepageURL
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Input{
Message: "Repository home page URL",
Default: r.HomepageURL,
}, opts.Edits.Homepage)
a, err := p.Input("Repository home page URL", r.HomepageURL)
if err != nil {
return err
}
opts.Edits.Homepage = &a
case optionTopics:
var addTopics string
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Input{
Message: "Add topics?(csv format)",
}, &addTopics)
addTopics, err := p.Input("Add topics?(csv format)", "")
if err != nil {
return err
}
@ -350,69 +346,41 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
if len(opts.topicsCache) > 0 {
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.MultiSelect{
Message: "Remove Topics",
Options: opts.topicsCache,
}, &opts.RemoveTopics)
selected, err := p.MultiSelect("Remove Topics", nil, opts.topicsCache)
if err != nil {
return err
}
for _, i := range selected {
opts.RemoveTopics = append(opts.RemoveTopics, opts.topicsCache[i])
}
}
case optionDefaultBranchName:
opts.Edits.DefaultBranch = &r.DefaultBranchRef.Name
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Input{
Message: "Default branch name",
Default: r.DefaultBranchRef.Name,
}, opts.Edits.DefaultBranch)
name, err := p.Input("Default branch name", r.DefaultBranchRef.Name)
if err != nil {
return err
}
opts.Edits.DefaultBranch = &name
case optionWikis:
opts.Edits.EnableWiki = &r.HasWikiEnabled
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Enable Wikis?",
Default: r.HasWikiEnabled,
}, opts.Edits.EnableWiki)
c, err := p.Confirm("Enable Wikis?", r.HasWikiEnabled)
if err != nil {
return err
}
opts.Edits.EnableWiki = &c
case optionIssues:
opts.Edits.EnableIssues = &r.HasIssuesEnabled
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Enable Issues?",
Default: r.HasIssuesEnabled,
}, opts.Edits.EnableIssues)
a, err := p.Confirm("Enable Issues?", r.HasIssuesEnabled)
if err != nil {
return err
}
opts.Edits.EnableIssues = &a
case optionProjects:
opts.Edits.EnableProjects = &r.HasProjectsEnabled
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Enable Projects?",
Default: r.HasProjectsEnabled,
}, opts.Edits.EnableProjects)
if err != nil {
return err
}
case optionDiscussions:
opts.Edits.EnableDiscussions = &r.HasDiscussionsEnabled
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Enable Discussions?",
Default: r.HasDiscussionsEnabled,
}, opts.Edits.EnableDiscussions)
a, err := p.Confirm("Enable Projects?", r.HasProjectsEnabled)
if err != nil {
return err
}
opts.Edits.EnableProjects = &a
case optionVisibility:
opts.Edits.Visibility = &r.Visibility
visibilityOptions := []string{"public", "private", "internal"}
selected, err := opts.Prompter.Select("Visibility", strings.ToLower(r.Visibility), visibilityOptions)
selected, err := p.Select("Visibility", strings.ToLower(r.Visibility), visibilityOptions)
if err != nil {
return err
}
@ -421,7 +389,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
(r.StargazerCount > 0 || r.Watchers.TotalCount > 0) {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.ErrOut, "%s Changing the repository visibility to private will cause permanent loss of stars and watchers.\n", cs.WarningIcon())
confirmed, err = opts.Prompter.Confirm("Do you want to change visibility to private?", false)
confirmed, err = p.Confirm("Do you want to change visibility to private?", false)
if err != nil {
return err
}
@ -441,15 +409,17 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
if r.RebaseMergeAllowed {
defaultMergeOptions = append(defaultMergeOptions, allowRebaseMerge)
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.MultiSelect{
Message: "Allowed merge strategies",
Default: defaultMergeOptions,
Options: []string{allowMergeCommits, allowSquashMerge, allowRebaseMerge},
}, &selectedMergeOptions)
mergeOpts := []string{allowMergeCommits, allowSquashMerge, allowRebaseMerge}
selected, err := p.MultiSelect(
"Allowed merge strategies",
defaultMergeOptions,
mergeOpts)
if err != nil {
return err
}
for _, i := range selected {
selectedMergeOptions = append(selectedMergeOptions, mergeOpts[i])
}
enableMergeCommit := isIncluded(allowMergeCommits, selectedMergeOptions)
opts.Edits.EnableMergeCommit = &enableMergeCommit
enableSquashMerge := isIncluded(allowSquashMerge, selectedMergeOptions)
@ -461,44 +431,33 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
opts.Edits.EnableAutoMerge = &r.AutoMergeAllowed
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Enable Auto Merge?",
Default: r.AutoMergeAllowed,
}, opts.Edits.EnableAutoMerge)
c, err := p.Confirm("Enable Auto Merge?", r.AutoMergeAllowed)
if err != nil {
return err
}
opts.Edits.EnableAutoMerge = &c
opts.Edits.DeleteBranchOnMerge = &r.DeleteBranchOnMerge
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Automatically delete head branches after merging?",
Default: r.DeleteBranchOnMerge,
}, opts.Edits.DeleteBranchOnMerge)
c, err = p.Confirm(
"Automatically delete head branches after merging?", r.DeleteBranchOnMerge)
if err != nil {
return err
}
opts.Edits.DeleteBranchOnMerge = &c
case optionTemplateRepo:
opts.Edits.IsTemplate = &r.IsTemplate
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Convert into a template repository?",
Default: r.IsTemplate,
}, opts.Edits.IsTemplate)
c, err := p.Confirm("Convert into a template repository?", r.IsTemplate)
if err != nil {
return err
}
opts.Edits.IsTemplate = &c
case optionAllowForking:
opts.Edits.AllowForking = &r.ForkingAllowed
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Allow forking (of an organization repository)?",
Default: r.ForkingAllowed,
}, opts.Edits.AllowForking)
c, err := p.Confirm(
"Allow forking (of an organization repository)?",
r.ForkingAllowed)
if err != nil {
return err
}
opts.Edits.AllowForking = &c
}
}
return nil

View file

@ -7,9 +7,8 @@ import (
"net/http"
"testing"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
@ -175,23 +174,163 @@ func Test_editRun(t *testing.T) {
}
func Test_editRun_interactive(t *testing.T) {
editList := []string{
"Default Branch Name",
"Description",
"Home Page URL",
"Issues",
"Merge Options",
"Projects",
"Template Repository",
"Topics",
"Visibility",
"Wikis"}
tests := []struct {
name string
opts EditOptions
askStubs func(*prompt.AskStubber)
promptStubs func(*prompter.MockPrompter)
httpStubs func(*testing.T, *httpmock.Registry)
wantsStderr string
wantsErr string
}{
{
name: "forking of org repo",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
InteractiveMode: true,
},
promptStubs: func(pm *prompter.MockPrompter) {
el := append(editList, optionAllowForking)
pm.RegisterMultiSelect("What do you want to edit?", nil, el,
func(_ string, _, opts []string) ([]int, error) {
return []int{10}, nil
})
pm.RegisterConfirm("Allow forking (of an organization repository)?", func(_ string, _ bool) (bool, error) {
return true, nil
})
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{
"data": {
"repository": {
"visibility": "public",
"description": "description",
"homePageUrl": "https://url.com",
"defaultBranchRef": {
"name": "main"
},
"isInOrganization": true,
"repositoryTopics": {
"nodes": [{
"topic": {
"name": "x"
}
}]
}
}
}
}`))
reg.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO"),
httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
assert.Equal(t, true, payload["allow_forking"])
}))
},
},
{
name: "the rest",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
InteractiveMode: true,
},
promptStubs: func(pm *prompter.MockPrompter) {
pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
func(_ string, _, opts []string) ([]int, error) {
return []int{0, 2, 3, 5, 6, 8, 9}, nil
})
pm.RegisterInput("Default branch name", func(_, _ string) (string, error) {
return "trunk", nil
})
pm.RegisterInput("Repository home page URL", func(_, _ string) (string, error) {
return "https://zombo.com", nil
})
pm.RegisterConfirm("Enable Issues?", func(_ string, _ bool) (bool, error) {
return true, nil
})
pm.RegisterConfirm("Enable Projects?", func(_ string, _ bool) (bool, error) {
return true, nil
})
pm.RegisterConfirm("Convert into a template repository?", func(_ string, _ bool) (bool, error) {
return true, nil
})
pm.RegisterSelect("Visibility", []string{"public", "private", "internal"},
func(_, _ string, opts []string) (int, error) {
return prompter.IndexFor(opts, "private")
})
pm.RegisterConfirm("Do you want to change visibility to private?", func(_ string, _ bool) (bool, error) {
return true, nil
})
pm.RegisterConfirm("Enable Wikis?", func(_ string, _ bool) (bool, error) {
return true, nil
})
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{
"data": {
"repository": {
"visibility": "public",
"description": "description",
"homePageUrl": "https://url.com",
"defaultBranchRef": {
"name": "main"
},
"stargazerCount": 10,
"isInOrganization": false,
"repositoryTopics": {
"nodes": [{
"topic": {
"name": "x"
}
}]
}
}
}
}`))
reg.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO"),
httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
assert.Equal(t, "trunk", payload["default_branch"])
assert.Equal(t, "https://zombo.com", payload["homepage"])
assert.Equal(t, true, payload["has_issues"])
assert.Equal(t, true, payload["has_projects"])
assert.Equal(t, "private", payload["visibility"])
assert.Equal(t, true, payload["is_template"])
assert.Equal(t, true, payload["has_wiki"])
}))
},
},
{
name: "updates repo description",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
InteractiveMode: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Description"})
as.StubPrompt("Description of the repository").AnswerWith("awesome repo description")
promptStubs: func(pm *prompter.MockPrompter) {
pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
func(_ string, _, opts []string) ([]int, error) {
return []int{1}, nil
})
pm.RegisterInput("Description of the repository",
func(_, _ string) (string, error) {
return "awesome repo description", nil
})
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
@ -229,11 +368,23 @@ func Test_editRun_interactive(t *testing.T) {
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
InteractiveMode: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Description", "Topics"})
as.StubPrompt("Description of the repository").AnswerWith("awesome repo description")
as.StubPrompt("Add topics?(csv format)").AnswerWith("a, b,c,d ")
as.StubPrompt("Remove Topics").AnswerWith([]string{"x"})
promptStubs: func(pm *prompter.MockPrompter) {
pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
func(_ string, _, opts []string) ([]int, error) {
return []int{1, 7}, nil
})
pm.RegisterInput("Description of the repository",
func(_, _ string) (string, error) {
return "awesome repo description", nil
})
pm.RegisterInput("Add topics?(csv format)",
func(_, _ string) (string, error) {
return "a, b,c,d ", nil
})
pm.RegisterMultiSelect("Remove Topics", nil, []string{"x"},
func(_ string, _, opts []string) ([]int, error) {
return []int{0}, nil
})
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
@ -276,11 +427,22 @@ func Test_editRun_interactive(t *testing.T) {
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
InteractiveMode: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Merge Options"})
as.StubPrompt("Allowed merge strategies").AnswerWith([]string{allowMergeCommits, allowRebaseMerge})
as.StubPrompt("Enable Auto Merge?").AnswerWith(false)
as.StubPrompt("Automatically delete head branches after merging?").AnswerWith(false)
promptStubs: func(pm *prompter.MockPrompter) {
pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
func(_ string, _, opts []string) ([]int, error) {
return []int{4}, nil
})
pm.RegisterMultiSelect("Allowed merge strategies", nil,
[]string{allowMergeCommits, allowSquashMerge, allowRebaseMerge},
func(_ string, _, opts []string) ([]int, error) {
return []int{0, 2}, nil
})
pm.RegisterConfirm("Enable Auto Merge?", func(_ string, _ bool) (bool, error) {
return false, nil
})
pm.RegisterConfirm("Automatically delete head branches after merging?", func(_ string, _ bool) (bool, error) {
return false, nil
})
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
@ -333,14 +495,15 @@ func Test_editRun_interactive(t *testing.T) {
tt.httpStubs(t, httpReg)
}
pm := prompter.NewMockPrompter(t)
tt.opts.Prompter = pm
if tt.promptStubs != nil {
tt.promptStubs(pm)
}
opts := &tt.opts
opts.HTTPClient = &http.Client{Transport: httpReg}
opts.IO = ios
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)
}
err := editRun(context.Background(), opts)
if tt.wantsErr == "" {