Merge pull request #5968 from cli/prompt-factory

Add Prompter
This commit is contained in:
Nate Smith 2022-08-15 16:35:26 -05:00 committed by GitHub
commit 5ab5b45d2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 936 additions and 349 deletions

View file

@ -119,6 +119,7 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, e
// hide the spinner in case a command started the progress indicator before base repo was fully
// resolved, e.g. in `gh issue view`
io.StopProgressIndicator()
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(&survey.Select{
Message: "Which should be the base repository (used for e.g. querying issues) for this directory?",
Options: repoNames,

View file

@ -36,12 +36,7 @@ func NormalizeHostname(h string) string {
return hostname
}
func HostnameValidator(v interface{}) error {
hostname, valid := v.(string)
if !valid {
return errors.New("hostname is not a string")
}
func HostnameValidator(hostname string) error {
if len(strings.TrimSpace(hostname)) < 1 {
return errors.New("a value is required")
}

View file

@ -95,7 +95,7 @@ func TestNormalizeHostname(t *testing.T) {
func TestHostnameValidator(t *testing.T) {
tests := []struct {
name string
input interface{}
input string
wantsErr bool
}{
{
@ -118,11 +118,6 @@ func TestHostnameValidator(t *testing.T) {
input: "internal.instance:2205",
wantsErr: true,
},
{
name: "non-string hostname",
input: 62,
wantsErr: true,
},
}
for _, tt := range tests {

View file

@ -0,0 +1,135 @@
package prompter
import (
"fmt"
"io"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/pkg/surveyext"
)
//go:generate moq -rm -out prompter_mock.go . Prompter
type Prompter interface {
Select(string, string, []string) (int, error)
MultiSelect(string, string, []string) (int, error)
Input(string, string) (string, error)
InputHostname() (string, error)
Password(string) (string, error)
AuthToken() (string, error)
Confirm(string, bool) (bool, error)
MarkdownEditor(string, string, bool) (string, error)
}
func New(editorCmd string, stdin io.Reader, stdout, stderr io.Writer) Prompter {
return &surveyPrompter{
editorCmd: editorCmd,
stdin: stdin.(terminal.FileReader),
stdout: stdout.(terminal.FileWriter),
stderr: stderr,
}
}
type surveyPrompter struct {
editorCmd string
stdin terminal.FileReader
stdout terminal.FileWriter
stderr io.Writer
}
func (p *surveyPrompter) Select(message, defaultValue string, options []string) (result int, err error) {
q := &survey.Select{
Message: message,
Default: defaultValue,
Options: options,
PageSize: 20,
}
err = p.ask(q, &result)
return
}
func (p *surveyPrompter) MultiSelect(message, defaultValue string, options []string) (result int, err error) {
q := &survey.MultiSelect{
Message: message,
Default: defaultValue,
Options: options,
PageSize: 20,
}
err = p.ask(q, &result)
return
}
func (p *surveyPrompter) ask(q survey.Prompt, response interface{}, opts ...survey.AskOpt) error {
opts = append(opts, survey.WithStdio(p.stdin, p.stdout, p.stderr))
err := survey.AskOne(q, response, opts...)
if err == nil {
return nil
}
return fmt.Errorf("could not prompt: %w", err)
}
func (p *surveyPrompter) Input(prompt, defaultValue string) (result string, err error) {
err = p.ask(&survey.Input{
Message: prompt,
Default: defaultValue,
}, &result)
return
}
func (p *surveyPrompter) InputHostname() (result string, err error) {
err = p.ask(
&survey.Input{
Message: "GHE hostname:",
}, &result, survey.WithValidator(func(v interface{}) error {
return ghinstance.HostnameValidator(v.(string))
}))
return
}
func (p *surveyPrompter) Password(prompt string) (result string, err error) {
err = p.ask(&survey.Password{
Message: prompt,
}, &result)
return
}
func (p *surveyPrompter) Confirm(prompt string, defaultValue bool) (result bool, err error) {
err = p.ask(&survey.Confirm{
Message: prompt,
Default: defaultValue,
}, &result)
return
}
func (p *surveyPrompter) MarkdownEditor(message, defaultValue string, blankAllowed bool) (result string, err error) {
err = p.ask(&surveyext.GhEditor{
BlankAllowed: blankAllowed,
EditorCommand: p.editorCmd,
Editor: &survey.Editor{
Message: message,
Default: defaultValue,
FileName: "*.md",
HideDefault: true,
AppendDefault: true,
},
}, &result)
return
}
func (p *surveyPrompter) AuthToken() (result string, err error) {
err = p.ask(&survey.Password{
Message: "Paste your authentication token:",
}, &result, survey.WithValidator(survey.Required))
return
}

View file

@ -0,0 +1,408 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package prompter
import (
"sync"
)
// Ensure, that PrompterMock does implement Prompter.
// If this is not the case, regenerate this file with moq.
var _ Prompter = &PrompterMock{}
// PrompterMock is a mock implementation of Prompter.
//
// func TestSomethingThatUsesPrompter(t *testing.T) {
//
// // make and configure a mocked Prompter
// mockedPrompter := &PrompterMock{
// AuthTokenFunc: func() (string, error) {
// panic("mock out the AuthToken method")
// },
// ConfirmFunc: func(s string, b bool) (bool, error) {
// panic("mock out the Confirm method")
// },
// InputFunc: func(s1 string, s2 string) (string, error) {
// panic("mock out the Input method")
// },
// InputHostnameFunc: func() (string, error) {
// panic("mock out the InputHostname method")
// },
// MarkdownEditorFunc: func(s1 string, s2 string, b bool) (string, error) {
// panic("mock out the MarkdownEditor method")
// },
// MultiSelectFunc: func(s1 string, s2 string, strings []string) (int, error) {
// panic("mock out the MultiSelect method")
// },
// PasswordFunc: func(s string) (string, error) {
// panic("mock out the Password method")
// },
// SelectFunc: func(s1 string, s2 string, strings []string) (int, error) {
// panic("mock out the Select method")
// },
// }
//
// // use mockedPrompter in code that requires Prompter
// // and then make assertions.
//
// }
type PrompterMock struct {
// AuthTokenFunc mocks the AuthToken method.
AuthTokenFunc func() (string, error)
// ConfirmFunc mocks the Confirm method.
ConfirmFunc func(s string, b bool) (bool, error)
// InputFunc mocks the Input method.
InputFunc func(s1 string, s2 string) (string, error)
// InputHostnameFunc mocks the InputHostname method.
InputHostnameFunc func() (string, error)
// MarkdownEditorFunc mocks the MarkdownEditor method.
MarkdownEditorFunc func(s1 string, s2 string, b bool) (string, error)
// MultiSelectFunc mocks the MultiSelect method.
MultiSelectFunc func(s1 string, s2 string, strings []string) (int, error)
// PasswordFunc mocks the Password method.
PasswordFunc func(s string) (string, error)
// SelectFunc mocks the Select method.
SelectFunc func(s1 string, s2 string, strings []string) (int, error)
// calls tracks calls to the methods.
calls struct {
// AuthToken holds details about calls to the AuthToken method.
AuthToken []struct {
}
// Confirm holds details about calls to the Confirm method.
Confirm []struct {
// S is the s argument value.
S string
// B is the b argument value.
B bool
}
// Input holds details about calls to the Input method.
Input []struct {
// S1 is the s1 argument value.
S1 string
// S2 is the s2 argument value.
S2 string
}
// InputHostname holds details about calls to the InputHostname method.
InputHostname []struct {
}
// MarkdownEditor holds details about calls to the MarkdownEditor method.
MarkdownEditor []struct {
// S1 is the s1 argument value.
S1 string
// S2 is the s2 argument value.
S2 string
// B is the b argument value.
B bool
}
// MultiSelect holds details about calls to the MultiSelect method.
MultiSelect []struct {
// S1 is the s1 argument value.
S1 string
// S2 is the s2 argument value.
S2 string
// Strings is the strings argument value.
Strings []string
}
// Password holds details about calls to the Password method.
Password []struct {
// S is the s argument value.
S string
}
// Select holds details about calls to the Select method.
Select []struct {
// S1 is the s1 argument value.
S1 string
// S2 is the s2 argument value.
S2 string
// Strings is the strings argument value.
Strings []string
}
}
lockAuthToken sync.RWMutex
lockConfirm sync.RWMutex
lockInput sync.RWMutex
lockInputHostname sync.RWMutex
lockMarkdownEditor sync.RWMutex
lockMultiSelect sync.RWMutex
lockPassword sync.RWMutex
lockSelect sync.RWMutex
}
// AuthToken calls AuthTokenFunc.
func (mock *PrompterMock) AuthToken() (string, error) {
if mock.AuthTokenFunc == nil {
panic("PrompterMock.AuthTokenFunc: method is nil but Prompter.AuthToken was just called")
}
callInfo := struct {
}{}
mock.lockAuthToken.Lock()
mock.calls.AuthToken = append(mock.calls.AuthToken, callInfo)
mock.lockAuthToken.Unlock()
return mock.AuthTokenFunc()
}
// AuthTokenCalls gets all the calls that were made to AuthToken.
// Check the length with:
// len(mockedPrompter.AuthTokenCalls())
func (mock *PrompterMock) AuthTokenCalls() []struct {
} {
var calls []struct {
}
mock.lockAuthToken.RLock()
calls = mock.calls.AuthToken
mock.lockAuthToken.RUnlock()
return calls
}
// Confirm calls ConfirmFunc.
func (mock *PrompterMock) Confirm(s string, b bool) (bool, error) {
if mock.ConfirmFunc == nil {
panic("PrompterMock.ConfirmFunc: method is nil but Prompter.Confirm was just called")
}
callInfo := struct {
S string
B bool
}{
S: s,
B: b,
}
mock.lockConfirm.Lock()
mock.calls.Confirm = append(mock.calls.Confirm, callInfo)
mock.lockConfirm.Unlock()
return mock.ConfirmFunc(s, b)
}
// ConfirmCalls gets all the calls that were made to Confirm.
// Check the length with:
// len(mockedPrompter.ConfirmCalls())
func (mock *PrompterMock) ConfirmCalls() []struct {
S string
B bool
} {
var calls []struct {
S string
B bool
}
mock.lockConfirm.RLock()
calls = mock.calls.Confirm
mock.lockConfirm.RUnlock()
return calls
}
// Input calls InputFunc.
func (mock *PrompterMock) Input(s1 string, s2 string) (string, error) {
if mock.InputFunc == nil {
panic("PrompterMock.InputFunc: method is nil but Prompter.Input was just called")
}
callInfo := struct {
S1 string
S2 string
}{
S1: s1,
S2: s2,
}
mock.lockInput.Lock()
mock.calls.Input = append(mock.calls.Input, callInfo)
mock.lockInput.Unlock()
return mock.InputFunc(s1, s2)
}
// InputCalls gets all the calls that were made to Input.
// Check the length with:
// len(mockedPrompter.InputCalls())
func (mock *PrompterMock) InputCalls() []struct {
S1 string
S2 string
} {
var calls []struct {
S1 string
S2 string
}
mock.lockInput.RLock()
calls = mock.calls.Input
mock.lockInput.RUnlock()
return calls
}
// InputHostname calls InputHostnameFunc.
func (mock *PrompterMock) InputHostname() (string, error) {
if mock.InputHostnameFunc == nil {
panic("PrompterMock.InputHostnameFunc: method is nil but Prompter.InputHostname was just called")
}
callInfo := struct {
}{}
mock.lockInputHostname.Lock()
mock.calls.InputHostname = append(mock.calls.InputHostname, callInfo)
mock.lockInputHostname.Unlock()
return mock.InputHostnameFunc()
}
// InputHostnameCalls gets all the calls that were made to InputHostname.
// Check the length with:
// len(mockedPrompter.InputHostnameCalls())
func (mock *PrompterMock) InputHostnameCalls() []struct {
} {
var calls []struct {
}
mock.lockInputHostname.RLock()
calls = mock.calls.InputHostname
mock.lockInputHostname.RUnlock()
return calls
}
// MarkdownEditor calls MarkdownEditorFunc.
func (mock *PrompterMock) MarkdownEditor(s1 string, s2 string, b bool) (string, error) {
if mock.MarkdownEditorFunc == nil {
panic("PrompterMock.MarkdownEditorFunc: method is nil but Prompter.MarkdownEditor was just called")
}
callInfo := struct {
S1 string
S2 string
B bool
}{
S1: s1,
S2: s2,
B: b,
}
mock.lockMarkdownEditor.Lock()
mock.calls.MarkdownEditor = append(mock.calls.MarkdownEditor, callInfo)
mock.lockMarkdownEditor.Unlock()
return mock.MarkdownEditorFunc(s1, s2, b)
}
// MarkdownEditorCalls gets all the calls that were made to MarkdownEditor.
// Check the length with:
// len(mockedPrompter.MarkdownEditorCalls())
func (mock *PrompterMock) MarkdownEditorCalls() []struct {
S1 string
S2 string
B bool
} {
var calls []struct {
S1 string
S2 string
B bool
}
mock.lockMarkdownEditor.RLock()
calls = mock.calls.MarkdownEditor
mock.lockMarkdownEditor.RUnlock()
return calls
}
// MultiSelect calls MultiSelectFunc.
func (mock *PrompterMock) MultiSelect(s1 string, s2 string, strings []string) (int, error) {
if mock.MultiSelectFunc == nil {
panic("PrompterMock.MultiSelectFunc: method is nil but Prompter.MultiSelect was just called")
}
callInfo := struct {
S1 string
S2 string
Strings []string
}{
S1: s1,
S2: s2,
Strings: strings,
}
mock.lockMultiSelect.Lock()
mock.calls.MultiSelect = append(mock.calls.MultiSelect, callInfo)
mock.lockMultiSelect.Unlock()
return mock.MultiSelectFunc(s1, s2, strings)
}
// MultiSelectCalls gets all the calls that were made to MultiSelect.
// Check the length with:
// len(mockedPrompter.MultiSelectCalls())
func (mock *PrompterMock) MultiSelectCalls() []struct {
S1 string
S2 string
Strings []string
} {
var calls []struct {
S1 string
S2 string
Strings []string
}
mock.lockMultiSelect.RLock()
calls = mock.calls.MultiSelect
mock.lockMultiSelect.RUnlock()
return calls
}
// Password calls PasswordFunc.
func (mock *PrompterMock) Password(s string) (string, error) {
if mock.PasswordFunc == nil {
panic("PrompterMock.PasswordFunc: method is nil but Prompter.Password was just called")
}
callInfo := struct {
S string
}{
S: s,
}
mock.lockPassword.Lock()
mock.calls.Password = append(mock.calls.Password, callInfo)
mock.lockPassword.Unlock()
return mock.PasswordFunc(s)
}
// PasswordCalls gets all the calls that were made to Password.
// Check the length with:
// len(mockedPrompter.PasswordCalls())
func (mock *PrompterMock) PasswordCalls() []struct {
S string
} {
var calls []struct {
S string
}
mock.lockPassword.RLock()
calls = mock.calls.Password
mock.lockPassword.RUnlock()
return calls
}
// Select calls SelectFunc.
func (mock *PrompterMock) Select(s1 string, s2 string, strings []string) (int, error) {
if mock.SelectFunc == nil {
panic("PrompterMock.SelectFunc: method is nil but Prompter.Select was just called")
}
callInfo := struct {
S1 string
S2 string
Strings []string
}{
S1: s1,
S2: s2,
Strings: strings,
}
mock.lockSelect.Lock()
mock.calls.Select = append(mock.calls.Select, callInfo)
mock.lockSelect.Unlock()
return mock.SelectFunc(s1, s2, strings)
}
// SelectCalls gets all the calls that were made to Select.
// Check the length with:
// len(mockedPrompter.SelectCalls())
func (mock *PrompterMock) SelectCalls() []struct {
S1 string
S2 string
Strings []string
} {
var calls []struct {
S1 string
S2 string
Strings []string
}
mock.lockSelect.RLock()
calls = mock.calls.Select
mock.lockSelect.RUnlock()
return calls
}

22
internal/prompter/test.go Normal file
View file

@ -0,0 +1,22 @@
package prompter
import "fmt"
// Test helpers
func IndexFor(options []string, answer string) (int, error) {
for ix, a := range options {
if a == answer {
return ix, nil
}
}
return -1, NoSuchAnswerErr(answer)
}
func NoSuchAnswerErr(answer string) error {
return fmt.Errorf("no such answer '%s'", answer)
}
func NoSuchPromptErr(prompt string) error {
return fmt.Errorf("no such prompt '%s'", prompt)
}

View file

@ -6,14 +6,13 @@ import (
"net/http"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/spf13/cobra"
)
@ -21,6 +20,7 @@ type LoginOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
HttpClient func() (*http.Client, error)
Prompter prompter.Prompter
MainExecutable string
@ -38,6 +38,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
IO: f.IOStreams,
Config: f.Config,
HttpClient: f.HttpClient,
Prompter: f.Prompter,
}
var tokenStdin bool
@ -129,7 +130,7 @@ func loginRun(opts *LoginOptions) error {
hostname := opts.Hostname
if opts.Interactive && hostname == "" {
var err error
hostname, err = promptForHostname()
hostname, err = promptForHostname(opts)
if err != nil {
return err
}
@ -159,15 +160,9 @@ func loginRun(opts *LoginOptions) error {
existingToken, _ := cfg.AuthToken(hostname)
if existingToken != "" && opts.Interactive {
if err := shared.HasMinimumScopes(httpClient, hostname, existingToken); err == nil {
var keepGoing bool
err = prompt.SurveyAskOne(&survey.Confirm{
Message: fmt.Sprintf(
"You're already logged into %s. Do you want to re-authenticate?",
hostname),
Default: false,
}, &keepGoing)
keepGoing, err := opts.Prompter.Confirm(fmt.Sprintf("You're already logged into %s. Do you want to re-authenticate?", hostname), false)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
return err
}
if !keepGoing {
return nil
@ -185,34 +180,28 @@ func loginRun(opts *LoginOptions) error {
Scopes: opts.Scopes,
Executable: opts.MainExecutable,
GitProtocol: opts.GitProtocol,
Prompter: opts.Prompter,
})
}
func promptForHostname() (string, error) {
var hostType int
err := prompt.SurveyAskOne(&survey.Select{
Message: "What account do you want to log into?",
Options: []string{
func promptForHostname(opts *LoginOptions) (string, error) {
hostType, err := opts.Prompter.Select(
"What account do you want to log into?",
"",
[]string{
"GitHub.com",
"GitHub Enterprise Server",
},
}, &hostType)
})
if err != nil {
return "", fmt.Errorf("could not prompt: %w", err)
return "", err
}
isEnterprise := hostType == 1
hostname := ghinstance.Default()
if isEnterprise {
err := prompt.SurveyAskOne(&survey.Input{
Message: "GHE hostname:",
}, &hostname, survey.WithValidator(ghinstance.HostnameValidator))
if err != nil {
return "", fmt.Errorf("could not prompt: %w", err)
}
hostname, err = opts.Prompter.InputHostname()
}
return hostname, nil
return hostname, err
}

View file

@ -10,11 +10,11 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
@ -362,14 +362,14 @@ func Test_loginRun_Survey(t *testing.T) {
stubHomeDir(t, t.TempDir())
tests := []struct {
name string
opts *LoginOptions
httpStubs func(*httpmock.Registry)
askStubs func(*prompt.AskStubber)
runStubs func(*run.CommandStubber)
wantHosts string
wantErrOut *regexp.Regexp
cfgStubs func(*config.ConfigMock)
name string
opts *LoginOptions
httpStubs func(*httpmock.Registry)
prompterStubs func(*prompter.PrompterMock)
runStubs func(*run.CommandStubber)
wantHosts string
wantErrOut *regexp.Regexp
cfgStubs func(*config.ConfigMock)
}{
{
name: "already authenticated",
@ -384,9 +384,13 @@ func Test_loginRun_Survey(t *testing.T) {
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What account do you want to log into?").AnswerWith("GitHub.com")
as.StubPrompt("You're already logged into github.com. Do you want to re-authenticate?").AnswerWith(false)
prompterStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
if prompt == "What account do you want to log into?" {
return prompter.IndexFor(opts, "GitHub.com")
}
return -1, prompter.NoSuchPromptErr(prompt)
}
},
wantHosts: "",
wantErrOut: nil,
@ -403,11 +407,16 @@ func Test_loginRun_Survey(t *testing.T) {
user: jillv
git_protocol: https
`),
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What is your preferred protocol for Git operations?").AnswerWith("HTTPS")
as.StubPrompt("Authenticate Git with your GitHub credentials?").AnswerWith(false)
as.StubPrompt("How would you like to authenticate GitHub CLI?").AnswerWith("Paste an authentication token")
as.StubPrompt("Paste your authentication token:").AnswerWith("def456")
prompterStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
switch prompt {
case "What is your preferred protocol for Git operations?":
return prompter.IndexFor(opts, "HTTPS")
case "How would you like to authenticate GitHub CLI?":
return prompter.IndexFor(opts, "Paste an authentication token")
}
return -1, prompter.NoSuchPromptErr(prompt)
}
},
runStubs: func(rs *run.CommandStubber) {
rs.Register(`git config credential\.https:/`, 1, "")
@ -432,13 +441,21 @@ func Test_loginRun_Survey(t *testing.T) {
opts: &LoginOptions{
Interactive: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What account do you want to log into?").AnswerWith("GitHub Enterprise Server")
as.StubPrompt("GHE hostname:").AnswerWith("brad.vickers")
as.StubPrompt("What is your preferred protocol for Git operations?").AnswerWith("HTTPS")
as.StubPrompt("Authenticate Git with your GitHub credentials?").AnswerWith(false)
as.StubPrompt("How would you like to authenticate GitHub CLI?").AnswerWith("Paste an authentication token")
as.StubPrompt("Paste your authentication token:").AnswerWith("def456")
prompterStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
switch prompt {
case "What account do you want to log into?":
return prompter.IndexFor(opts, "GitHub Enterprise Server")
case "What is your preferred protocol for Git operations?":
return prompter.IndexFor(opts, "HTTPS")
case "How would you like to authenticate GitHub CLI?":
return prompter.IndexFor(opts, "Paste an authentication token")
}
return -1, prompter.NoSuchPromptErr(prompt)
}
pm.InputHostnameFunc = func() (string, error) {
return "brad.vickers", nil
}
},
runStubs: func(rs *run.CommandStubber) {
rs.Register(`git config credential\.https:/`, 1, "")
@ -463,12 +480,18 @@ func Test_loginRun_Survey(t *testing.T) {
opts: &LoginOptions{
Interactive: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What account do you want to log into?").AnswerWith("GitHub.com")
as.StubPrompt("What is your preferred protocol for Git operations?").AnswerWith("HTTPS")
as.StubPrompt("Authenticate Git with your GitHub credentials?").AnswerWith(false)
as.StubPrompt("How would you like to authenticate GitHub CLI?").AnswerWith("Paste an authentication token")
as.StubPrompt("Paste your authentication token:").AnswerWith("def456")
prompterStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
switch prompt {
case "What account do you want to log into?":
return prompter.IndexFor(opts, "GitHub.com")
case "What is your preferred protocol for Git operations?":
return prompter.IndexFor(opts, "HTTPS")
case "How would you like to authenticate GitHub CLI?":
return prompter.IndexFor(opts, "Paste an authentication token")
}
return -1, prompter.NoSuchPromptErr(prompt)
}
},
runStubs: func(rs *run.CommandStubber) {
rs.Register(`git config credential\.https:/`, 1, "")
@ -487,12 +510,18 @@ func Test_loginRun_Survey(t *testing.T) {
opts: &LoginOptions{
Interactive: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What account do you want to log into?").AnswerWith("GitHub.com")
as.StubPrompt("What is your preferred protocol for Git operations?").AnswerWith("SSH")
as.StubPrompt("Generate a new SSH key to add to your GitHub account?").AnswerWith(false)
as.StubPrompt("How would you like to authenticate GitHub CLI?").AnswerWith("Paste an authentication token")
as.StubPrompt("Paste your authentication token:").AnswerWith("def456")
prompterStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
switch prompt {
case "What account do you want to log into?":
return prompter.IndexFor(opts, "GitHub.com")
case "What is your preferred protocol for Git operations?":
return prompter.IndexFor(opts, "SSH")
case "How would you like to authenticate GitHub CLI?":
return prompter.IndexFor(opts, "Paste an authentication token")
}
return -1, prompter.NoSuchPromptErr(prompt)
}
},
wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://github.com/settings/tokens"),
},
@ -535,10 +564,17 @@ func Test_loginRun_Survey(t *testing.T) {
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
}
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)
pm := &prompter.PrompterMock{}
pm.ConfirmFunc = func(_ string, _ bool) (bool, error) {
return false, nil
}
pm.AuthTokenFunc = func() (string, error) {
return "def456", nil
}
if tt.prompterStubs != nil {
tt.prompterStubs(pm)
}
tt.opts.Prompter = pm
rs, restoreRun := run.Stub()
defer restoreRun(t)

View file

@ -79,6 +79,7 @@ func logoutRun(opts *LogoutOptions) error {
if len(candidates) == 1 {
hostname = candidates[0]
} else {
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Select{
Message: "What account do you want to log out of?",
Options: candidates,

View file

@ -158,6 +158,7 @@ func Test_logoutRun_tty(t *testing.T) {
return &http.Client{Transport: reg}, nil
}
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)

View file

@ -94,6 +94,7 @@ func refreshRun(opts *RefreshOptions) error {
if len(candidates) == 1 {
hostname = candidates[0]
} else {
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(&survey.Select{
Message: "What account do you want to refresh auth for?",
Options: candidates,

View file

@ -272,6 +272,7 @@ func Test_refreshRun(t *testing.T) {
)
tt.opts.httpClient = &http.Client{Transport: httpReg}
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)

View file

@ -7,17 +7,17 @@ import (
"path/filepath"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/google/shlex"
)
type GitCredentialFlow struct {
Executable string
Prompter prompter.Prompter
shouldSetup bool
helper string
@ -32,13 +32,12 @@ func (flow *GitCredentialFlow) Prompt(hostname string) error {
return nil
}
err := prompt.SurveyAskOne(&survey.Confirm{
Message: "Authenticate Git with your GitHub credentials?",
Default: true,
}, &flow.shouldSetup)
result, err := flow.Prompter.Confirm("Authenticate Git with your GitHub credentials?", true)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
return err
}
flow.shouldSetup = result
if flow.shouldSetup {
if isGitMissing(gitErr) {
return gitErr

View file

@ -6,14 +6,13 @@ import (
"os"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/authflow"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/ssh-key/add"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/pkg/ssh"
)
@ -35,6 +34,7 @@ type LoginOptions struct {
Scopes []string
Executable string
GitProtocol string
Prompter prompter.Prompter
sshContext ssh.Context
}
@ -47,23 +47,23 @@ func Login(opts *LoginOptions) error {
gitProtocol := strings.ToLower(opts.GitProtocol)
if opts.Interactive && gitProtocol == "" {
var proto string
err := prompt.SurveyAskOne(&survey.Select{
Message: "What is your preferred protocol for Git operations?",
Options: []string{
"HTTPS",
"SSH",
},
}, &proto)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
options := []string{
"HTTPS",
"SSH",
}
result, err := opts.Prompter.Select(
"What is your preferred protocol for Git operations?",
"", options)
if err != nil {
return err
}
proto := options[result]
gitProtocol = strings.ToLower(proto)
}
var additionalScopes []string
credentialFlow := &GitCredentialFlow{Executable: opts.Executable}
credentialFlow := &GitCredentialFlow{Executable: opts.Executable, Prompter: opts.Prompter}
if opts.Interactive && gitProtocol == "https" {
if err := credentialFlow.Prompt(hostname); err != nil {
return err
@ -80,34 +80,28 @@ func Login(opts *LoginOptions) error {
}
if len(pubKeys) > 0 {
var keyChoice int
err := prompt.SurveyAskOne(&survey.Select{
Message: "Upload your SSH public key to your GitHub account?",
Options: append(pubKeys, "Skip"),
}, &keyChoice)
options := append(pubKeys, "Skip")
keyChoice, err := opts.Prompter.Select(
"Upload your SSH public key to your GitHub account?", "",
options)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
return err
}
if keyChoice < len(pubKeys) {
keyToUpload = pubKeys[keyChoice]
}
} else if opts.sshContext.HasKeygen() {
var sshChoice bool
err := prompt.SurveyAskOne(&survey.Confirm{
Message: "Generate a new SSH key to add to your GitHub account?",
Default: true,
}, &sshChoice)
sshChoice, err := opts.Prompter.Confirm("Generate a new SSH key to add to your GitHub account?", true)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
return err
}
if sshChoice {
passphrase, err := promptForSshKeyPassphrase()
passphrase, err := opts.Prompter.Password(
"Enter a passphrase for your new SSH key (Optional)")
if err != nil {
return fmt.Errorf("could not prompt for key passphrase: %w", err)
return err
}
keyPair, err := opts.sshContext.GenerateSSHKey("id_ed25519", passphrase)
if err != nil {
return err
@ -117,12 +111,11 @@ func Login(opts *LoginOptions) error {
}
if keyToUpload != "" {
err := prompt.SurveyAskOne(&survey.Input{
Message: "Title for your SSH key:",
Default: defaultSSHKeyTitle,
}, &keyTitle)
var err error
keyTitle, err = opts.Prompter.Input(
"Title for your SSH key:", defaultSSHKeyTitle)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
return err
}
additionalScopes = append(additionalScopes, "admin:public_key")
@ -133,15 +126,14 @@ func Login(opts *LoginOptions) error {
if opts.Web {
authMode = 0
} else if opts.Interactive {
err := prompt.SurveyAskOne(&survey.Select{
Message: "How would you like to authenticate GitHub CLI?",
Options: []string{
var err error
authMode, err = opts.Prompter.Select(
"How would you like to authenticate GitHub CLI?", "",
[]string{
"Login with a web browser",
"Paste an authentication token",
},
}, &authMode)
"Paste an authentication token"})
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
return err
}
}
@ -163,11 +155,9 @@ func Login(opts *LoginOptions) error {
The minimum required scopes are %s.
`, hostname, scopesSentence(minimumScopes, ghinstance.IsEnterprise(hostname))))
err := prompt.SurveyAskOne(&survey.Password{
Message: "Paste your authentication token:",
}, &authToken, survey.WithValidator(survey.Required))
authToken, err := opts.Prompter.AuthToken()
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
return err
}
if err := HasMinimumScopes(httpClient, hostname, authToken); err != nil {
@ -221,19 +211,6 @@ func Login(opts *LoginOptions) error {
return nil
}
func promptForSshKeyPassphrase() (string, error) {
var sshPassphrase string
err := prompt.SurveyAskOne(&survey.Password{
Message: "Enter a passphrase for your new SSH key (Optional)",
}, &sshPassphrase)
if err != nil {
return "", err
}
return sshPassphrase, nil
}
func scopesSentence(scopes []string, isEnterprise bool) string {
quoted := make([]string, len(scopes))
for i, s := range scopes {

View file

@ -8,10 +8,10 @@ import (
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/pkg/ssh"
"github.com/stretchr/testify/assert"
)
@ -47,14 +47,28 @@ func TestLogin_ssh(t *testing.T) {
httpmock.REST("POST", "api/v3/user/keys"),
httpmock.StringResponse(`{}`))
ask := prompt.NewAskStubber(t)
ask.StubPrompt("What is your preferred protocol for Git operations?").AnswerWith("SSH")
ask.StubPrompt("Generate a new SSH key to add to your GitHub account?").AnswerWith(true)
ask.StubPrompt("Enter a passphrase for your new SSH key (Optional)").AnswerWith("monkey")
ask.StubPrompt("Title for your SSH key:").AnswerWith("Test Key")
ask.StubPrompt("How would you like to authenticate GitHub CLI?").AnswerWith("Paste an authentication token")
ask.StubPrompt("Paste your authentication token:").AnswerWith("ATOKEN")
pm := &prompter.PrompterMock{}
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
switch prompt {
case "What is your preferred protocol for Git operations?":
return prompter.IndexFor(opts, "SSH")
case "How would you like to authenticate GitHub CLI?":
return prompter.IndexFor(opts, "Paste an authentication token")
}
return -1, prompter.NoSuchPromptErr(prompt)
}
pm.PasswordFunc = func(_ string) (string, error) {
return "monkey", nil
}
pm.ConfirmFunc = func(prompt string, _ bool) (bool, error) {
return true, nil
}
pm.AuthTokenFunc = func() (string, error) {
return "ATOKEN", nil
}
pm.InputFunc = func(_, _ string) (string, error) {
return "Test Key", nil
}
rs, runRestore := run.Stub()
defer runRestore(t)
@ -77,6 +91,7 @@ func TestLogin_ssh(t *testing.T) {
err := Login(&LoginOptions{
IO: ios,
Config: &cfg,
Prompter: pm,
HTTPClient: &http.Client{Transport: &tr},
Hostname: "example.com",
Interactive: true,

View file

@ -6,13 +6,11 @@ import (
"os"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -20,6 +18,7 @@ import (
func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
m := f.ExtensionManager
io := f.IOStreams
prompter := f.Prompter
extCmd := cobra.Command{
Use: "extension",
@ -244,22 +243,15 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
},
func() *cobra.Command {
promptCreate := func() (string, extensions.ExtTemplateType, error) {
var extName string
var extTmplType int
err := prompt.SurveyAskOne(&survey.Input{
Message: "Extension name:",
}, &extName)
extName, err := prompter.Input("Extension name:", "")
if err != nil {
return extName, -1, err
}
err = prompt.SurveyAskOne(&survey.Select{
Message: "What kind of extension?",
Options: []string{
"Script (Bash, Ruby, Python, etc)",
"Go",
"Other Precompiled (C++, Rust, etc)",
},
}, &extTmplType)
extTmplType, err := prompter.Select("What kind of extension?", "", []string{
"Script (Bash, Ruby, Python, etc)",
"Go",
"Other Precompiled (C++, Rust, etc)",
})
return extName, extensions.ExtTemplateType(extTmplType), err
}
var flagType string

View file

@ -12,11 +12,11 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"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/extensions"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
@ -28,15 +28,15 @@ func TestNewCmdExtension(t *testing.T) {
t.Cleanup(func() { _ = os.Chdir(oldWd) })
tests := []struct {
name string
args []string
managerStubs func(em *extensions.ExtensionManagerMock) func(*testing.T)
askStubs func(as *prompt.AskStubber)
isTTY bool
wantErr bool
errMsg string
wantStdout string
wantStderr string
name string
args []string
managerStubs func(em *extensions.ExtensionManagerMock) func(*testing.T)
prompterStubs func(pm *prompter.PrompterMock)
isTTY bool
wantErr bool
errMsg string
wantStdout string
wantStderr string
}{
{
name: "install an extension",
@ -387,11 +387,16 @@ func TestNewCmdExtension(t *testing.T) {
}
},
isTTY: true,
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("Extension name:").AnswerWith("test")
as.StubPrompt("What kind of extension?").
AssertOptions([]string{"Script (Bash, Ruby, Python, etc)", "Go", "Other Precompiled (C++, Rust, etc)"}).
AnswerDefault()
prompterStubs: func(pm *prompter.PrompterMock) {
pm.InputFunc = func(prompt, defVal string) (string, error) {
if prompt == "Extension name:" {
return "test", nil
}
return "", nil
}
pm.SelectFunc = func(prompt, defVal string, opts []string) (int, error) {
return prompter.IndexFor(opts, "Script (Bash, Ruby, Python, etc)")
}
},
wantStdout: heredoc.Doc(`
Created directory gh-test
@ -562,9 +567,9 @@ func TestNewCmdExtension(t *testing.T) {
assertFunc = tt.managerStubs(em)
}
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)
pm := &prompter.PrompterMock{}
if tt.prompterStubs != nil {
tt.prompterStubs(pm)
}
reg := httpmock.Registry{}
@ -577,6 +582,7 @@ func TestNewCmdExtension(t *testing.T) {
},
IOStreams: ios,
ExtensionManager: em,
Prompter: pm,
HttpClient: func() (*http.Client, error) {
return &client, nil
},

View file

@ -12,6 +12,7 @@ import (
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/extension"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -31,6 +32,7 @@ func New(appVersion string) *cmdutil.Factory {
f.HttpClient = httpClientFunc(f, appVersion) // Depends on Config, IOStreams, and appVersion
f.Remotes = remotesFunc(f) // Depends on Config
f.BaseRepo = BaseRepoFunc(f) // Depends on Remotes
f.Prompter = newPrompter(f) // Depends on Config and IOStreams
f.Browser = browser(f) // Depends on Config, and IOStreams
f.ExtensionManager = extensionManager(f) // Depends on Config, HttpClient, and IOStreams
@ -108,6 +110,12 @@ func browser(f *cmdutil.Factory) cmdutil.Browser {
return cmdutil.NewBrowser(browserLauncher(f), io.Out, io.ErrOut)
}
func newPrompter(f *cmdutil.Factory) prompter.Prompter {
editor, _ := cmdutil.DetermineEditor(f.Config)
io := f.IOStreams
return prompter.New(editor, io.In, io.Out, io.ErrOut)
}
// Browser precedence
// 1. GH_BROWSER
// 2. browser from config

View file

@ -189,6 +189,7 @@ func editRun(opts *EditOptions) error {
if !opts.IO.CanPrompt() {
return errors.New("unsure what file to edit; either specify --filename or run interactively")
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Select{
Message: "Edit which file?",
Options: candidates,
@ -249,6 +250,7 @@ func editRun(opts *EditOptions) error {
choice := ""
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Select{
Message: "What next?",
Options: []string{

View file

@ -486,6 +486,7 @@ func Test_editRun(t *testing.T) {
}
t.Run(tt.name, func(t *testing.T) {
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)

View file

@ -246,6 +246,7 @@ func promptGists(client *http.Client, host string, cs *iostreams.ColorScheme) (g
Options: opts,
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(questions, &result)
if err != nil {

View file

@ -355,6 +355,7 @@ func Test_viewRun(t *testing.T) {
)),
)
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("Select a gist").AnswerDefault()
}
@ -469,6 +470,7 @@ func Test_promptGists(t *testing.T) {
client := &http.Client{Transport: reg}
t.Run(tt.name, func(t *testing.T) {
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)

View file

@ -402,6 +402,7 @@ func TestIssueCreate_recover(t *testing.T) {
assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
}))
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("Title").AnswerDefault()
@ -469,6 +470,7 @@ func TestIssueCreate_nonLegacyTemplate(t *testing.T) {
}),
)
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("Choose a template").AnswerWith("Submit a request")
@ -499,6 +501,7 @@ func TestIssueCreate_continueInBrowser(t *testing.T) {
} } }`),
)
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("Title").AnswerWith("hello")

View file

@ -79,6 +79,7 @@ func deleteRun(opts *DeleteOptions) error {
// already provided. Otherwise skip confirmation.
if opts.IO.CanPrompt() && !opts.Confirmed {
answer := ""
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(
&survey.Input{
Message: fmt.Sprintf("You're going to delete issue #%d. This action cannot be reversed. To confirm, type the issue number:", issue.Number),

View file

@ -76,6 +76,7 @@ func TestIssueDelete(t *testing.T) {
}),
)
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("You're going to delete issue #13. This action cannot be reversed. To confirm, type the issue number:").AnswerWith("13")
@ -136,6 +137,7 @@ func TestIssueDelete_cancel(t *testing.T) {
} } }`),
)
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("You're going to delete issue #13. This action cannot be reversed. To confirm, type the issue number:").AnswerWith("14")

View file

@ -67,6 +67,7 @@ func deleteRun(opts *deleteOptions) error {
if !opts.Confirmed {
var valid string
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(
&survey.Input{Message: fmt.Sprintf("Type %s to confirm deletion:", opts.Name)},
&valid,

View file

@ -153,6 +153,7 @@ func TestDeleteRun(t *testing.T) {
return &http.Client{Transport: reg}, nil
}
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)

View file

@ -569,6 +569,7 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) {
pushOptions = append(pushOptions, "Cancel")
var selectedOption int
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Select{
Message: fmt.Sprintf("Where should we push the '%s' branch?", headBranch),
Options: pushOptions,

View file

@ -370,6 +370,7 @@ func (m *mergeContext) deleteLocalBranch() error {
if m.merged {
// prompt for delete
if m.opts.IO.CanPrompt() && !m.opts.IsDeleteBranchIndicated {
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(&survey.Confirm{
Message: fmt.Sprintf("Pull request #%d was already merged. Delete the branch locally?", m.pr.Number),
Default: false,
@ -571,6 +572,7 @@ func mergeMethodSurvey(baseRepo *api.Repository) (PullRequestMergeMethod, error)
}
var result int
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(mergeQuestion, &result)
return mergeOpts[result].method, err
}
@ -589,6 +591,7 @@ func deleteBranchSurvey(opts *MergeOptions, crossRepoPR, localBranchExists bool)
Message: message,
Default: false,
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(submit, &result)
return result, err
}
@ -615,6 +618,7 @@ func confirmSurvey(allowEditMsg bool) (shared.Action, error) {
Message: "What's next?",
Options: options,
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(submit, &result)
if err != nil {
return shared.CancelAction, fmt.Errorf("could not prompt: %w", err)

View file

@ -960,6 +960,7 @@ func TestPrMerge_alreadyMerged(t *testing.T) {
cs.Register(`git branch -D blueberries`, 0, "")
cs.Register(`git pull --ff-only`, 0, "")
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("Pull request #4 was already merged. Delete the branch locally?").AnswerWith(true)
@ -1021,6 +1022,7 @@ func TestPrMerge_alreadyMerged_withMergeStrategy_TTY(t *testing.T) {
cs.Register(`git rev-parse --verify refs/heads/`, 0, "")
cs.Register(`git branch -D `, 0, "")
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("Pull request #4 was already merged. Delete the branch locally?").AnswerWith(true)
@ -1100,6 +1102,7 @@ func TestPRMergeTTY(t *testing.T) {
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("What merge method would you like to use?").AnswerDefault()
as.StubPrompt("Delete the branch locally and on GitHub?").AnswerDefault()
@ -1161,6 +1164,7 @@ func TestPRMergeTTY_withDeleteBranch(t *testing.T) {
cs.Register(`git branch -D blueberries`, 0, "")
cs.Register(`git pull --ff-only`, 0, "")
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("What merge method would you like to use?").AnswerDefault()
as.StubPrompt("What's next?").AnswerWith("Submit")
@ -1220,6 +1224,7 @@ func TestPRMergeTTY_squashEditCommitMsgAndSubject(t *testing.T) {
_, cmdTeardown := run.Stub()
defer cmdTeardown(t)
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("What merge method would you like to use?").AnswerWith("Squash and merge")
as.StubPrompt("Delete the branch on GitHub?").AnswerDefault()
@ -1298,6 +1303,7 @@ func TestPRTTY_cancelled(t *testing.T) {
cs.Register(`git rev-parse --verify refs/heads/`, 0, "")
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("What merge method would you like to use?").AnswerDefault()
as.StubPrompt("Delete the branch locally and on GitHub?").AnswerDefault()
@ -1317,6 +1323,7 @@ func Test_mergeMethodSurvey(t *testing.T) {
RebaseMergeAllowed: true,
SquashMergeAllowed: true,
}
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("What merge method would you like to use?").AnswerWith("Rebase and merge")
@ -1624,6 +1631,7 @@ func TestPrAddToMergeQueueAdmin(t *testing.T) {
defer cmdTeardown(t)
cs.Register(`git rev-parse --verify refs/heads/`, 0, "")
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("What merge method would you like to use?").AnswerDefault()
as.StubPrompt("Delete the branch locally and on GitHub?").AnswerDefault()

View file

@ -5,16 +5,14 @@ import (
"fmt"
"net/http"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/markdown"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/pkg/surveyext"
"github.com/spf13/cobra"
)
@ -22,6 +20,7 @@ type ReviewOptions struct {
HttpClient func() (*http.Client, error)
Config func() (config.Config, error)
IO *iostreams.IOStreams
Prompter prompter.Prompter
Finder shared.PRFinder
@ -36,6 +35,7 @@ func NewCmdReview(f *cmdutil.Factory, runF func(*ReviewOptions) error) *cobra.Co
IO: f.IOStreams,
HttpClient: f.HttpClient,
Config: f.Config,
Prompter: f.Prompter,
}
var (
@ -160,7 +160,7 @@ func reviewRun(opts *ReviewOptions) error {
if err != nil {
return err
}
reviewData, err = reviewSurvey(opts.IO, editorCommand)
reviewData, err = reviewSurvey(opts, editorCommand)
if err != nil {
return err
}
@ -204,95 +204,51 @@ func reviewRun(opts *ReviewOptions) error {
return nil
}
func reviewSurvey(io *iostreams.IOStreams, editorCommand string) (*api.PullRequestReviewInput, error) {
typeAnswers := struct {
ReviewType string
}{}
typeQs := []*survey.Question{
{
Name: "reviewType",
Prompt: &survey.Select{
Message: "What kind of review do you want to give?",
Options: []string{
"Comment",
"Approve",
"Request changes",
},
},
},
}
err := prompt.SurveyAsk(typeQs, &typeAnswers)
func reviewSurvey(opts *ReviewOptions, editorCommand string) (*api.PullRequestReviewInput, error) {
reviewType, err := opts.Prompter.Select(
"What kind of review do you want to give?", "",
[]string{"Comment", "Approve", "Request Changes"})
if err != nil {
return nil, err
}
var reviewState api.PullRequestReviewState
switch typeAnswers.ReviewType {
case "Approve":
reviewState = api.ReviewApprove
case "Request changes":
reviewState = api.ReviewRequestChanges
case "Comment":
switch reviewType {
case 0:
reviewState = api.ReviewComment
case 1:
reviewState = api.ReviewApprove
case 2:
reviewState = api.ReviewRequestChanges
default:
panic("unreachable state")
}
bodyAnswers := struct {
Body string
}{}
blankAllowed := false
if reviewState == api.ReviewApprove {
blankAllowed = true
}
bodyQs := []*survey.Question{
{
Name: "body",
Prompt: &surveyext.GhEditor{
BlankAllowed: blankAllowed,
EditorCommand: editorCommand,
Editor: &survey.Editor{
Message: "Review body",
FileName: "*.md",
},
},
},
}
err = prompt.SurveyAsk(bodyQs, &bodyAnswers)
body, err := opts.Prompter.MarkdownEditor("Review body", "", blankAllowed)
if err != nil {
return nil, err
}
if bodyAnswers.Body == "" && (reviewState == api.ReviewComment || reviewState == api.ReviewRequestChanges) {
if body == "" && (reviewState == api.ReviewComment || reviewState == api.ReviewRequestChanges) {
return nil, errors.New("this type of review cannot be blank")
}
if len(bodyAnswers.Body) > 0 {
renderedBody, err := markdown.Render(bodyAnswers.Body, markdown.WithIO(io))
if len(body) > 0 {
renderedBody, err := markdown.Render(body, markdown.WithIO(opts.IO))
if err != nil {
return nil, err
}
fmt.Fprintf(io.Out, "Got:\n%s", renderedBody)
fmt.Fprintf(opts.IO.Out, "Got:\n%s", renderedBody)
}
confirm := false
confirmQs := []*survey.Question{
{
Name: "confirm",
Prompt: &survey.Confirm{
Message: "Submit?",
Default: true,
},
},
}
err = prompt.SurveyAsk(confirmQs, &confirm)
confirm, err := opts.Prompter.Confirm("Submit?", true)
if err != nil {
return nil, err
}
@ -302,7 +258,7 @@ func reviewSurvey(io *iostreams.IOStreams, editorCommand string) (*api.PullReque
}
return &api.PullRequestReviewInput{
Body: bodyAnswers.Body,
Body: body,
State: reviewState,
}, nil
}

View file

@ -14,11 +14,11 @@ import (
"github.com/cli/cli/v2/context"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/test"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
@ -166,7 +166,7 @@ func Test_NewCmdReview(t *testing.T) {
}
}
func runCommand(rt http.RoundTripper, remotes context.Remotes, isTTY bool, cli string) (*test.CmdOut, error) {
func runCommand(rt http.RoundTripper, prompter prompter.Prompter, remotes context.Remotes, isTTY bool, cli string) (*test.CmdOut, error) {
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(isTTY)
ios.SetStdinTTY(isTTY)
@ -180,6 +180,7 @@ func runCommand(rt http.RoundTripper, remotes context.Remotes, isTTY bool, cli s
Config: func() (config.Config, error) {
return config.NewBlankConfig(), nil
},
Prompter: prompter,
}
cmd := NewCmdReview(factory, nil)
@ -248,7 +249,7 @@ func TestPRReview(t *testing.T) {
}),
)
output, err := runCommand(http, nil, false, tt.args)
output, err := runCommand(http, nil, nil, false, tt.args)
assert.NoError(t, err)
assert.Equal(t, "", output.String())
assert.Equal(t, "", output.Stderr())
@ -271,33 +272,13 @@ func TestPRReview_interactive(t *testing.T) {
}),
)
//nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber
as, teardown := prompt.InitAskStubber()
defer teardown()
pm := &prompter.PrompterMock{
SelectFunc: func(_, _ string, _ []string) (int, error) { return 1, nil },
MarkdownEditorFunc: func(_, _ string, _ bool) (string, error) { return "cool story", nil },
ConfirmFunc: func(_ string, _ bool) (bool, error) { return true, nil },
}
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{
Name: "reviewType",
Value: "Approve",
},
})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{
Name: "body",
Value: "cool story",
},
})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{
Name: "confirm",
Value: true,
},
})
output, err := runCommand(http, nil, true, "")
output, err := runCommand(http, pm, nil, true, "")
assert.NoError(t, err)
assert.Equal(t, heredoc.Doc(`
Got:
@ -314,12 +295,12 @@ func TestPRReview_interactive_no_body(t *testing.T) {
shared.RunCommandFinder("", &api.PullRequest{ID: "THE-ID", Number: 123}, ghrepo.New("OWNER", "REPO"))
as := prompt.NewAskStubber(t)
pm := &prompter.PrompterMock{
SelectFunc: func(_, _ string, _ []string) (int, error) { return 2, nil },
MarkdownEditorFunc: func(_, _ string, _ bool) (string, error) { return "", nil },
}
as.StubPrompt("What kind of review do you want to give?").AnswerWith("Request changes")
as.StubPrompt("Review body").AnswerWith("")
_, err := runCommand(http, nil, true, "")
_, err := runCommand(http, pm, nil, true, "")
assert.EqualError(t, err, "this type of review cannot be blank")
}
@ -338,33 +319,13 @@ func TestPRReview_interactive_blank_approve(t *testing.T) {
}),
)
//nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber
as, teardown := prompt.InitAskStubber()
defer teardown()
pm := &prompter.PrompterMock{
SelectFunc: func(_, _ string, _ []string) (int, error) { return 1, nil },
MarkdownEditorFunc: func(_, defVal string, _ bool) (string, error) { return defVal, nil },
ConfirmFunc: func(_ string, _ bool) (bool, error) { return true, nil },
}
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{
Name: "reviewType",
Value: "Approve",
},
})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{
Name: "body",
Default: true,
},
})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{
Name: "confirm",
Value: true,
},
})
output, err := runCommand(http, nil, true, "")
output, err := runCommand(http, pm, nil, true, "")
assert.NoError(t, err)
assert.Equal(t, "", output.String())
assert.Equal(t, "✓ Approved pull request #123\n", output.Stderr())

View file

@ -71,6 +71,7 @@ func confirmSubmission(allowPreview, allowMetadata, allowDraft, isDraft bool) (A
},
}
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err := prompt.SurveyAsk(confirmQs, &confirmAnswers)
if err != nil {
return -1, fmt.Errorf("could not prompt: %w", err)
@ -122,6 +123,7 @@ func BodySurvey(state *IssueMetadataState, templateContent, editorCommand string
},
}
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err := prompt.SurveyAsk(qs, state)
if err != nil {
return err
@ -148,6 +150,7 @@ func TitleSurvey(state *IssueMetadataState) error {
},
}
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err := prompt.SurveyAsk(qs, state)
if err != nil {
return err
@ -197,6 +200,7 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
}
extraFieldsOptions = append(extraFieldsOptions, "Assignees", "Labels", "Projects", "Milestone")
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err := prompt.SurveyAsk([]*survey.Question{
{
Name: "metadata",
@ -327,6 +331,7 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
Milestone string
}{}
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err = prompt.SurveyAsk(mqs, &values)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)

View file

@ -189,6 +189,7 @@ func (m *templateManager) Choose() (Template, error) {
}
var selectedOption int
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(&survey.Select{
Message: "Choose a template",
Options: append(names, blankOption),

View file

@ -47,6 +47,7 @@ func TestTemplateManager_hasAPI(t *testing.T) {
assert.Equal(t, "LEGACY", string(m.LegacyBody()))
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("Choose a template").
AssertOptions([]string{"Bug report", "Feature request", "Open a blank issue"}).
@ -93,6 +94,7 @@ func TestTemplateManager_hasAPI_PullRequest(t *testing.T) {
assert.Equal(t, "LEGACY", string(m.LegacyBody()))
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("Choose a template").
AssertOptions([]string{"bug_pr.md", "feature_pr.md", "Open a blank pull request"}).

View file

@ -195,6 +195,7 @@ func createRun(opts *CreateOptions) error {
Options: options,
Default: options[0],
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(q, &tag)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
@ -209,6 +210,7 @@ func createRun(opts *CreateOptions) error {
q := &survey.Input{
Message: "Tag name",
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(q, &opts.TagName)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
@ -309,6 +311,7 @@ func createRun(opts *CreateOptions) error {
},
},
}
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err = prompt.SurveyAsk(qs, opts)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
@ -373,6 +376,7 @@ func createRun(opts *CreateOptions) error {
},
}
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err = prompt.SurveyAsk(qs, opts)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)

View file

@ -851,6 +851,7 @@ func Test_createRun_interactive(t *testing.T) {
}
t.Run(tt.name, func(t *testing.T) {
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)

View file

@ -69,6 +69,7 @@ func deleteAssetRun(opts *DeleteAssetOptions) error {
if !opts.SkipConfirm && opts.IO.CanPrompt() {
var confirmed bool
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(&survey.Confirm{
Message: fmt.Sprintf("Delete asset %s in release %s in %s?", opts.AssetName, release.TagName, ghrepo.FullName(baseRepo)),
Default: true,

View file

@ -69,6 +69,7 @@ func deleteRun(opts *DeleteOptions) error {
if !opts.SkipConfirm && opts.IO.CanPrompt() {
var confirmed bool
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(&survey.Confirm{
Message: fmt.Sprintf("Delete release %s in %s?", release.TagName, ghrepo.FullName(baseRepo)),
Default: true,

View file

@ -115,6 +115,7 @@ func archiveRun(opts *ArchiveOptions) error {
Message: fmt.Sprintf("Archive %s?", fullName),
Default: false,
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(p, &opts.Confirmed)
if err != nil {
return fmt.Errorf("failed to prompt: %w", err)

View file

@ -232,6 +232,7 @@ func createRun(opts *CreateOptions) error {
"Create a new repository on GitHub from scratch",
"Push an existing local repository to GitHub",
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
if err := prompt.SurveyAskOne(&survey.Select{
Message: "What would you like to do?",
Options: modeOptions,
@ -353,6 +354,7 @@ func createFromScratch(opts *CreateOptions) error {
Message: "Clone the new repository locally?",
Default: true,
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(cloneQuestion, &opts.Clone)
if err != nil {
return err
@ -507,6 +509,7 @@ func createFromLocal(opts *CreateOptions) error {
Message: `Add a remote?`,
Default: true,
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(remoteQuesiton, &addRemote)
if err != nil {
return err
@ -520,6 +523,7 @@ func createFromLocal(opts *CreateOptions) error {
Message: "What should the new remote be called?",
Default: "origin",
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(pushQuestion, &baseRemote)
if err != nil {
return err
@ -536,6 +540,7 @@ func createFromLocal(opts *CreateOptions) error {
Message: fmt.Sprintf(`Would you like to push commits from the current branch to %q?`, baseRemote),
Default: true,
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(pushQuestion, &opts.Push)
if err != nil {
return err
@ -684,6 +689,7 @@ func interactiveGitIgnore(client *http.Client, hostname string) (string, error)
}
addGitIgnoreSurvey = append(addGitIgnoreSurvey, addGitIgnoreQuestion)
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err := prompt.SurveyAsk(addGitIgnoreSurvey, &addGitIgnore)
if err != nil {
return "", err
@ -706,6 +712,7 @@ func interactiveGitIgnore(client *http.Client, hostname string) (string, error)
},
}
gitIg = append(gitIg, gitIgnoreQuestion)
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err = prompt.SurveyAsk(gitIg, &wantedIgnoreTemplate)
if err != nil {
return "", err
@ -730,6 +737,7 @@ func interactiveLicense(client *http.Client, hostname string) (string, error) {
}
addLicenseSurvey = append(addLicenseSurvey, addLicenseQuestion)
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err := prompt.SurveyAsk(addLicenseSurvey, &addLicense)
if err != nil {
return "", err
@ -757,6 +765,7 @@ func interactiveLicense(client *http.Client, hostname string) (string, error) {
},
}
licenseQs = append(licenseQs, licenseQuestion)
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err = prompt.SurveyAsk(licenseQs, &wantedLicense)
if err != nil {
return "", err
@ -794,6 +803,7 @@ func interactiveRepoInfo(defaultName string) (string, string, string, error) {
RepoVisibility string
}{}
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err := prompt.SurveyAsk(qs, &answer)
if err != nil {
return "", "", "", err
@ -808,6 +818,7 @@ func interactiveSource() (string, error) {
Message: "Path to local repository",
Default: "."}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(sourcePrompt, &sourcePath)
if err != nil {
return "", err
@ -823,6 +834,7 @@ func confirmSubmission(repoWithOwner, visibility string) error {
var answer struct {
ConfirmSubmit bool
}
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err := prompt.SurveyAsk([]*survey.Question{{
Name: "confirmSubmit",
Prompt: &survey.Confirm{

View file

@ -8,10 +8,9 @@ import (
"github.com/cli/cli/v2/api"
"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/prompt"
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
@ -19,6 +18,7 @@ import (
type DeleteOptions struct {
HttpClient func() (*http.Client, error)
BaseRepo func() (ghrepo.Interface, error)
Prompter prompter.Prompter
IO *iostreams.IOStreams
RepoArg string
Confirmed bool
@ -29,6 +29,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
IO: f.IOStreams,
HttpClient: f.HttpClient,
BaseRepo: f.BaseRepo,
Prompter: f.Prompter,
}
cmd := &cobra.Command{
@ -91,19 +92,13 @@ func deleteRun(opts *DeleteOptions) error {
fullName := ghrepo.FullName(toDelete)
if !opts.Confirmed {
var valid string
err := prompt.SurveyAskOne(
&survey.Input{Message: fmt.Sprintf("Type %s to confirm deletion:", fullName)},
&valid,
survey.WithValidator(
func(val interface{}) error {
if str := val.(string); !strings.EqualFold(str, fullName) {
return fmt.Errorf("You entered %s", str)
}
return nil
}))
result, err := opts.Prompter.Input(
fmt.Sprintf("Type %s to confirm deletion:", fullName), "")
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
return err
}
if !strings.EqualFold(result, fullName) {
return fmt.Errorf("You entered %s", result)
}
}

View file

@ -6,10 +6,10 @@ import (
"testing"
"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"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
@ -77,25 +77,24 @@ func TestNewCmdDelete(t *testing.T) {
func Test_deleteRun(t *testing.T) {
tests := []struct {
name string
tty bool
opts *DeleteOptions
httpStubs func(*httpmock.Registry)
askStubs func(*prompt.AskStubber)
wantStdout string
wantErr bool
errMsg string
name string
tty bool
opts *DeleteOptions
httpStubs func(*httpmock.Registry)
prompterStubs func(*prompter.PrompterMock)
wantStdout string
wantErr bool
errMsg string
}{
{
name: "prompting confirmation tty",
tty: true,
opts: &DeleteOptions{RepoArg: "OWNER/REPO"},
wantStdout: "✓ Deleted repository OWNER/REPO\n",
askStubs: func(q *prompt.AskStubber) {
// TODO: survey stubber doesn't have WithValidator support
// so this always passes regardless of prompt input
//nolint:staticcheck // SA1019: q.StubOne is deprecated: use StubPrompt
q.StubOne("OWNER/REPO")
prompterStubs: func(p *prompter.PrompterMock) {
p.InputFunc = func(_, _ string) (string, error) {
return "OWNER/REPO", nil
}
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
@ -108,9 +107,10 @@ func Test_deleteRun(t *testing.T) {
tty: true,
opts: &DeleteOptions{},
wantStdout: "✓ Deleted repository OWNER/REPO\n",
askStubs: func(q *prompt.AskStubber) {
//nolint:staticcheck // SA1019: q.StubOne is deprecated: use StubPrompt
q.StubOne("OWNER/REPO")
prompterStubs: func(p *prompter.PrompterMock) {
p.InputFunc = func(_, _ string) (string, error) {
return "OWNER/REPO", nil
}
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
@ -135,9 +135,10 @@ func Test_deleteRun(t *testing.T) {
opts: &DeleteOptions{RepoArg: "REPO"},
wantStdout: "✓ Deleted repository OWNER/REPO\n",
tty: true,
askStubs: func(q *prompt.AskStubber) {
//nolint:staticcheck // SA1019: q.StubOne is deprecated: use StubPrompt
q.StubOne("OWNER/REPO")
prompterStubs: func(p *prompter.PrompterMock) {
p.InputFunc = func(_, _ string) (string, error) {
return "OWNER/REPO", nil
}
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
@ -150,12 +151,11 @@ func Test_deleteRun(t *testing.T) {
},
}
for _, tt := range tests {
//nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber
q, teardown := prompt.InitAskStubber()
defer teardown()
if tt.askStubs != nil {
tt.askStubs(q)
pm := &prompter.PrompterMock{}
if tt.prompterStubs != nil {
tt.prompterStubs(pm)
}
tt.opts.Prompter = pm
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil

View file

@ -282,6 +282,7 @@ 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,
@ -301,6 +302,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
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,
@ -310,6 +312,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
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,
@ -319,6 +322,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
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)
@ -330,6 +334,7 @@ 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,
@ -340,6 +345,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
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,
@ -349,6 +355,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
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,
@ -358,6 +365,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
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,
@ -367,6 +375,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
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,
@ -376,6 +385,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
case optionVisibility:
opts.Edits.Visibility = &r.Visibility
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(&survey.Select{
Message: "Visibility",
Options: []string{"public", "private", "internal"},
@ -396,6 +406,7 @@ 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,
@ -415,6 +426,7 @@ 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,
@ -424,6 +436,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
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,
@ -433,6 +446,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
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,
@ -442,6 +456,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
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,

View file

@ -319,6 +319,7 @@ func Test_editRun_interactive(t *testing.T) {
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)

View file

@ -260,6 +260,7 @@ func forkRun(opts *ForkOptions) error {
remoteDesired := opts.Remote
if opts.PromptRemote {
//nolint:staticcheck // SA1019: prompt.Confirm is deprecated: use Prompter
err = prompt.Confirm("Would you like to add a remote for the fork?", &remoteDesired)
if err != nil {
return fmt.Errorf("failed to prompt: %w", err)
@ -302,6 +303,7 @@ func forkRun(opts *ForkOptions) error {
} else {
cloneDesired := opts.Clone
if opts.PromptClone {
//nolint:staticcheck // SA1019: prompt.Confirm is deprecated: use Prompter
err = prompt.Confirm("Would you like to clone the fork?", &cloneDesired)
if err != nil {
return fmt.Errorf("failed to prompt: %w", err)

View file

@ -89,6 +89,7 @@ func renameRun(opts *RenameOptions) error {
}
if newRepoName == "" {
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(
&survey.Input{
Message: fmt.Sprintf("Rename %s to: ", ghrepo.FullName(currRepo)),
@ -106,6 +107,7 @@ func renameRun(opts *RenameOptions) error {
Message: fmt.Sprintf("Rename %s to %s?", ghrepo.FullName(currRepo), newRepoName),
Default: false,
}
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err = prompt.SurveyAskOne(p, &confirmed)
if err != nil {
return fmt.Errorf("failed to prompt: %w", err)

View file

@ -198,6 +198,7 @@ func matchAnyPattern(patterns []string, name string) bool {
type surveyPrompter struct{}
func (sp *surveyPrompter) Prompt(message string, options []string, result interface{}) error {
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
return prompt.SurveyAskOne(&survey.MultiSelect{
Message: message,
Options: options,

View file

@ -356,6 +356,7 @@ func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) {
// TODO consider custom filter so it's fuzzier. right now matches start anywhere in string but
// become contiguous
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(&survey.Select{
Message: "Select a workflow run",
Options: candidates,

View file

@ -446,6 +446,7 @@ func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, er
}
var selected int
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(&survey.Select{
Message: "View a specific job in this run?",
Options: candidates,

View file

@ -277,6 +277,7 @@ func TestWatchRun(t *testing.T) {
}
t.Run(tt.name, func(t *testing.T) {
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)

View file

@ -373,6 +373,7 @@ func getBody(opts *SetOptions) ([]byte, error) {
if opts.IO.CanPrompt() {
var bodyInput string
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(&survey.Password{
Message: "Paste your secret",
}, &bodyInput)

View file

@ -560,6 +560,7 @@ func Test_getBodyPrompt(t *testing.T) {
ios.SetStdinTTY(true)
ios.SetStdoutTTY(true)
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
as.StubPrompt("Paste your secret").AnswerWith("cool secret")

View file

@ -278,6 +278,7 @@ func TestDisableRun(t *testing.T) {
}
t.Run(tt.name, func(t *testing.T) {
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)

View file

@ -327,6 +327,7 @@ func TestEnableRun(t *testing.T) {
}
t.Run(tt.name, func(t *testing.T) {
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)

View file

@ -230,6 +230,7 @@ func collectInputs(yamlContent []byte) (map[string]string, error) {
inputAnswer := InputAnswer{
providedInputs: providedInputs,
}
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
err = prompt.SurveyAsk(qs, &inputAnswer)
if err != nil {
return nil, err

View file

@ -632,6 +632,7 @@ jobs:
}
t.Run(tt.name, func(t *testing.T) {
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)

View file

@ -104,6 +104,7 @@ func SelectWorkflow(workflows []Workflow, promptMsg string, states []WorkflowSta
var selected int
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
err := prompt.SurveyAskOne(&survey.Select{
Message: promptMsg,
Options: candidates,

View file

@ -9,6 +9,7 @@ import (
"github.com/cli/cli/v2/context"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/iostreams"
)
@ -20,6 +21,7 @@ type Browser interface {
type Factory struct {
IOStreams *iostreams.IOStreams
Browser Browser
Prompter prompter.Prompter
HttpClient func() (*http.Client, error)
BaseRepo func() (ghrepo.Interface, error)

View file

@ -2,6 +2,7 @@ package prompt
import "github.com/AlecAivazis/survey/v2"
// Deprecated: use PrompterMock
func StubConfirm(result bool) func() {
orig := Confirm
Confirm = func(_ string, r *bool) error {
@ -13,6 +14,7 @@ func StubConfirm(result bool) func() {
}
}
// Deprecated: use Prompter
var Confirm = func(prompt string, result *bool) error {
p := &survey.Confirm{
Message: prompt,
@ -21,10 +23,12 @@ var Confirm = func(prompt string, result *bool) error {
return SurveyAskOne(p, result)
}
// Deprecated: use Prompter
var SurveyAskOne = func(p survey.Prompt, response interface{}, opts ...survey.AskOpt) error {
return survey.AskOne(p, response, opts...)
}
// Deprecated: use Prompter
var SurveyAsk = func(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error {
return survey.Ask(qs, response, opts...)
}

View file

@ -18,6 +18,7 @@ type testing interface {
Cleanup(func())
}
// Deprecated: use PrompterMock
func NewAskStubber(t testing) *AskStubber {
as, teardown := InitAskStubber()
t.Cleanup(func() {