Merge remote-tracking branch 'origin/trunk' into cmbrose/no-ssh-api-call

This commit is contained in:
Caleb Brose 2022-09-20 15:22:27 +00:00
commit 83c67d3b57
23 changed files with 178 additions and 71 deletions

View file

@ -85,6 +85,7 @@ func GenMarkdown(cmd *cobra.Command, w io.Writer) error {
// GenMarkdownCustom creates custom markdown output.
func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error {
fmt.Fprint(w, "{% raw %}")
fmt.Fprintf(w, "## %s\n\n", cmd.CommandPath())
hasLong := cmd.Long != ""
@ -112,6 +113,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
if err := printOptions(w, cmd); err != nil {
return err
}
fmt.Fprint(w, "{% endraw %}\n")
if len(cmd.Example) > 0 {
fmt.Fprint(w, "### Examples\n\n{% highlight bash %}{% raw %}\n")

View file

@ -9,7 +9,6 @@ import (
"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"
@ -20,7 +19,7 @@ type LoginOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
HttpClient func() (*http.Client, error)
Prompter prompter.Prompter
Prompter shared.Prompt
MainExecutable string

View file

@ -4,14 +4,12 @@ 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/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"
)
@ -19,6 +17,7 @@ type LogoutOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
Config func() (config.Config, error)
Prompter shared.Prompt
Hostname string
}
@ -27,6 +26,7 @@ func NewCmdLogout(f *cmdutil.Factory, runF func(*LogoutOptions) error) *cobra.Co
HttpClient: f.HttpClient,
IO: f.IOStreams,
Config: f.Config,
Prompter: f.Prompter,
}
cmd := &cobra.Command{
@ -79,15 +79,12 @@ 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,
}, &hostname)
selected, err := opts.Prompter.Select(
"What account to you want to log out of?", "", candidates)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
hostname = candidates[selected]
}
} else {
var found bool

View file

@ -7,10 +7,10 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"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"
)
@ -92,21 +92,23 @@ func Test_NewCmdLogout(t *testing.T) {
func Test_logoutRun_tty(t *testing.T) {
tests := []struct {
name string
opts *LogoutOptions
askStubs func(*prompt.AskStubber)
cfgHosts []string
wantHosts string
wantErrOut *regexp.Regexp
wantErr string
name string
opts *LogoutOptions
prompterStubs func(*prompter.PrompterMock)
cfgHosts []string
wantHosts string
wantErrOut *regexp.Regexp
wantErr string
}{
{
name: "no arguments, multiple hosts",
opts: &LogoutOptions{},
cfgHosts: []string{"cheryl.mason", "github.com"},
wantHosts: "cheryl.mason:\n oauth_token: abc123\n",
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What account do you want to log out of?").AnswerWith("github.com")
prompterStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(_, _ string, opts []string) (int, error) {
return prompter.IndexFor(opts, "github.com")
}
},
wantErrOut: regexp.MustCompile(`Logged out of github.com account 'cybilb'`),
},
@ -158,11 +160,11 @@ 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)
pm := &prompter.PrompterMock{}
if tt.prompterStubs != nil {
tt.prompterStubs(pm)
}
tt.opts.Prompter = pm
err := logoutRun(tt.opts)
if tt.wantErr != "" {

View file

@ -5,15 +5,12 @@ import (
"net/http"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/authflow"
"github.com/cli/cli/v2/internal/config"
"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,7 +18,7 @@ type RefreshOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
httpClient *http.Client
Prompter prompter.Prompter
Prompter shared.Prompt
MainExecutable string
@ -97,15 +94,11 @@ 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,
}, &hostname)
selected, err := opts.Prompter.Select("What account do you want to refresh auth for?", "", candidates)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
hostname = candidates[selected]
}
} else {
var found bool

View file

@ -8,10 +8,10 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"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"
)
@ -132,14 +132,14 @@ type authArgs struct {
func Test_refreshRun(t *testing.T) {
tests := []struct {
name string
opts *RefreshOptions
askStubs func(*prompt.AskStubber)
cfgHosts []string
oldScopes string
wantErr string
nontty bool
wantAuthArgs authArgs
name string
opts *RefreshOptions
prompterStubs func(*prompter.PrompterMock)
cfgHosts []string
oldScopes string
wantErr string
nontty bool
wantAuthArgs authArgs
}{
{
name: "no hosts configured",
@ -193,8 +193,10 @@ func Test_refreshRun(t *testing.T) {
opts: &RefreshOptions{
Hostname: "",
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What account do you want to refresh auth for?").AnswerWith("github.com")
prompterStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(_, _ string, opts []string) (int, error) {
return prompter.IndexFor(opts, "github.com")
}
},
wantAuthArgs: authArgs{
hostname: "github.com",
@ -272,11 +274,11 @@ 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)
pm := &prompter.PrompterMock{}
if tt.prompterStubs != nil {
tt.prompterStubs(pm)
}
tt.opts.Prompter = pm
err := refreshRun(tt.opts)
if tt.wantErr != "" {

View file

@ -10,14 +10,13 @@ import (
"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/google/shlex"
)
type GitCredentialFlow struct {
Executable string
Prompter prompter.Prompter
Prompter Prompt
shouldSetup bool
helper string

View file

@ -10,7 +10,6 @@ import (
"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/ssh"
@ -34,7 +33,7 @@ type LoginOptions struct {
Scopes []string
Executable string
GitProtocol string
Prompter prompter.Prompter
Prompter Prompt
sshContext ssh.Context
}

View file

@ -0,0 +1,10 @@
package shared
type Prompt interface {
Select(string, string, []string) (int, error)
Confirm(string, bool) (bool, error)
InputHostname() (string, error)
AuthToken() (string, error)
Input(string, string) (string, error)
Password(string) (string, error)
}

View file

@ -7,6 +7,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
@ -92,9 +93,15 @@ func cloneRun(opts *CloneOptions) error {
}
func formatRemoteURL(hostname string, gistID string, protocol string) string {
if ghinstance.IsEnterprise(hostname) {
if protocol == "ssh" {
return fmt.Sprintf("git@%s:gist/%s.git", hostname, gistID)
}
return fmt.Sprintf("https://%s/gist/%s.git", hostname, gistID)
}
if protocol == "ssh" {
return fmt.Sprintf("git@gist.%s:%s.git", hostname, gistID)
}
return fmt.Sprintf("https://gist.%s/%s.git", hostname, gistID)
}

View file

@ -116,3 +116,60 @@ func Test_GistClone_flagError(t *testing.T) {
t.Errorf("unexpected error %v", err)
}
}
func Test_formatRemoteURL(t *testing.T) {
type args struct {
hostname string
gistID string
protocol string
}
tests := []struct {
name string
args args
want string
}{
{
name: "github.com HTTPS",
args: args{
hostname: "github.com",
protocol: "https",
gistID: "ID",
},
want: "https://gist.github.com/ID.git",
},
{
name: "github.com SSH",
args: args{
hostname: "github.com",
protocol: "ssh",
gistID: "ID",
},
want: "git@gist.github.com:ID.git",
},
{
name: "Enterprise HTTPS",
args: args{
hostname: "acme.org",
protocol: "https",
gistID: "ID",
},
want: "https://acme.org/gist/ID.git",
},
{
name: "Enterprise SSH",
args: args{
hostname: "acme.org",
protocol: "ssh",
gistID: "ID",
},
want: "git@acme.org:gist/ID.git",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := formatRemoteURL(tt.args.hostname, tt.args.gistID, tt.args.protocol); got != tt.want {
t.Errorf("formatRemoteURL() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -20,6 +20,7 @@ type ReposOptions struct {
Browser browser.Browser
Exporter cmdutil.Exporter
IO *iostreams.IOStreams
Now time.Time
Query search.Query
Searcher search.Searcher
WebMode bool
@ -148,10 +149,14 @@ func reposRun(opts *ReposOptions) error {
if len(result.Items) == 0 {
return cmdutil.NewNoResultsError("no repositories matched your search")
}
return displayResults(io, result)
return displayResults(io, opts.Now, result)
}
func displayResults(io *iostreams.IOStreams, results search.RepositoriesResult) error {
func displayResults(io *iostreams.IOStreams, now time.Time, results search.RepositoriesResult) error {
if now.IsZero() {
now = time.Now()
}
cs := io.ColorScheme()
tp := utils.NewTablePrinter(io)
for _, repo := range results.Items {
@ -172,13 +177,12 @@ func displayResults(io *iostreams.IOStreams, results search.RepositoriesResult)
tp.AddField(text.RemoveExcessiveWhitespace(description), nil, nil)
tp.AddField(info, nil, infoColor)
if tp.IsTTY() {
tp.AddField(text.FuzzyAgoAbbr(time.Now(), repo.UpdatedAt), nil, cs.Gray)
tp.AddField(text.FuzzyAgoAbbr(now, repo.UpdatedAt), nil, cs.Gray)
} else {
tp.AddField(repo.UpdatedAt.Format(time.RFC3339), nil, nil)
}
tp.EndRow()
}
if io.IsStdoutTTY() {
header := fmt.Sprintf("Showing %d of %d repositories\n\n", len(results.Items), results.Total)
fmt.Fprintf(io.Out, "\n%s", header)

View file

@ -149,6 +149,8 @@ func TestNewCmdRepos(t *testing.T) {
}
func TestReposRun(t *testing.T) {
var now = time.Date(2022, 2, 28, 12, 30, 0, 0, time.UTC)
var updatedAt = time.Date(2021, 2, 28, 12, 30, 0, 0, time.UTC)
var query = search.Query{
Keywords: []string{"cli"},
Kind: "repositories",
@ -158,7 +160,6 @@ func TestReposRun(t *testing.T) {
Topic: []string{"golang"},
},
}
var updatedAt = time.Date(2021, 2, 28, 12, 30, 0, 0, time.UTC)
tests := []struct {
errMsg string
name string
@ -270,6 +271,7 @@ func TestReposRun(t *testing.T) {
ios.SetStdoutTTY(tt.tty)
ios.SetStderrTTY(tt.tty)
tt.opts.IO = ios
tt.opts.Now = now
t.Run(tt.name, func(t *testing.T) {
err := reposRun(tt.opts)
if tt.wantErr {

View file

@ -31,6 +31,7 @@ type IssuesOptions struct {
Entity EntityType
Exporter cmdutil.Exporter
IO *iostreams.IOStreams
Now time.Time
Query search.Query
Searcher search.Searcher
WebMode bool
@ -88,10 +89,13 @@ func SearchIssues(opts *IssuesOptions) error {
return cmdutil.NewNoResultsError(msg)
}
return displayIssueResults(io, opts.Entity, result)
return displayIssueResults(io, opts.Now, opts.Entity, result)
}
func displayIssueResults(io *iostreams.IOStreams, et EntityType, results search.IssuesResult) error {
func displayIssueResults(io *iostreams.IOStreams, now time.Time, et EntityType, results search.IssuesResult) error {
if now.IsZero() {
now = time.Now()
}
cs := io.ColorScheme()
tp := utils.NewTablePrinter(io)
for _, issue := range results.Items {
@ -120,7 +124,7 @@ func displayIssueResults(io *iostreams.IOStreams, et EntityType, results search.
tp.AddField(text.RemoveExcessiveWhitespace(issue.Title), nil, nil)
tp.AddField(listIssueLabels(&issue, cs, tp.IsTTY()), nil, nil)
if tp.IsTTY() {
tp.AddField(text.FuzzyAgo(time.Now(), issue.UpdatedAt), nil, cs.Gray)
tp.AddField(text.FuzzyAgo(now, issue.UpdatedAt), nil, cs.Gray)
} else {
tp.AddField(issue.UpdatedAt.String(), nil, nil)
}

View file

@ -23,7 +23,9 @@ func TestSearcher(t *testing.T) {
}
func TestSearchIssues(t *testing.T) {
query := search.Query{
var now = time.Date(2022, 2, 28, 12, 30, 0, 0, time.UTC)
var updatedAt = time.Date(2021, 2, 28, 12, 30, 0, 0, time.UTC)
var query = search.Query{
Keywords: []string{"keyword"},
Kind: "issues",
Limit: 30,
@ -33,8 +35,6 @@ func TestSearchIssues(t *testing.T) {
Is: []string{"public", "locked"},
},
}
var updatedAt = time.Date(2021, 2, 28, 12, 30, 0, 0, time.UTC)
tests := []struct {
errMsg string
name string
@ -193,6 +193,7 @@ func TestSearchIssues(t *testing.T) {
ios.SetStdoutTTY(tt.tty)
ios.SetStderrTTY(tt.tty)
tt.opts.IO = ios
tt.opts.Now = now
t.Run(tt.name, func(t *testing.T) {
err := SearchIssues(tt.opts)
if tt.wantErr {

View file

@ -41,7 +41,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
Delete a secret on one of the following levels:
- repository (default): available to Actions runs or Dependabot in a repository
- environment: available to Actions runs for a deployment environment in a repository
- organization: available to Actions runs or Dependabot within an organization
- organization: available to Actions runs, Dependabot, or Codespaces within an organization
- user: available to Codespaces for your user
`),
Args: cobra.ExactArgs(1),

View file

@ -73,6 +73,15 @@ func TestNewCmdDelete(t *testing.T) {
Application: "Dependabot",
},
},
{
name: "Codespaces org",
cli: "cool --app codespaces --org UmbrellaCorporation",
wants: DeleteOptions{
SecretName: "cool",
OrgName: "UmbrellaCorporation",
Application: "Codespaces",
},
},
}
for _, tt := range tests {
@ -219,6 +228,14 @@ func Test_removeRun_org(t *testing.T) {
},
wantPath: "orgs/UmbrellaCorporation/dependabot/secrets/tVirus",
},
{
name: "Codespaces org",
opts: &DeleteOptions{
Application: "codespaces",
OrgName: "UmbrellaCorporation",
},
wantPath: "orgs/UmbrellaCorporation/codespaces/secrets/tVirus",
},
}
for _, tt := range tests {

View file

@ -46,7 +46,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
List secrets on one of the following levels:
- repository (default): available to Actions runs or Dependabot in a repository
- environment: available to Actions runs for a deployment environment in a repository
- organization: available to Actions runs or Dependabot within an organization
- organization: available to Actions runs, Dependabot, or Codespaces within an organization
- user: available to Codespaces for your user
`),
Aliases: []string{"ls"},

View file

@ -15,7 +15,7 @@ func NewCmdSecret(f *cmdutil.Factory) *cobra.Command {
Short: "Manage GitHub secrets",
Long: heredoc.Doc(`
Secrets can be set at the repository, or organization level for use in
GitHub Actions or Dependabot. User and repository secrets can be set for
GitHub Actions or Dependabot. User, organization, and repository secrets can be set for
use in GitHub Codespaces. Environment secrets can be set for use in
GitHub Actions. Run "gh help secret set" to learn how to get started.
`),

View file

@ -58,7 +58,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
Set a value for a secret on one of the following levels:
- repository (default): available to Actions runs or Dependabot in a repository
- environment: available to Actions runs for a deployment environment in a repository
- organization: available to Actions runs or Dependabot within an organization
- organization: available to Actions runs, Dependabot, or Codespaces within an organization
- user: available to Codespaces for your user
Organization and user secrets can optionally be restricted to only be available to

View file

@ -165,6 +165,18 @@ func TestNewCmdSet(t *testing.T) {
Application: "Dependabot",
},
},
{
name: "Codespaces org",
cli: `random_secret -ocoolOrg -b"random value" -vselected -r"coolRepo,cli/cli" -aCodespaces`,
wants: SetOptions{
SecretName: "random_secret",
Visibility: shared.Selected,
RepositoryNames: []string{"coolRepo", "cli/cli"},
Body: "random value",
OrgName: "coolOrg",
Application: "Codespaces",
},
},
}
for _, tt := range tests {

View file

@ -85,7 +85,7 @@ func IsSupportedSecretEntity(app App, entity SecretEntity) bool {
case Actions:
return entity == Repository || entity == Organization || entity == Environment
case Codespaces:
return entity == User || entity == Repository
return entity == User || entity == Organization || entity == Repository
case Dependabot:
return entity == Repository || entity == Organization
default:

View file

@ -167,9 +167,9 @@ func TestIsSupportedSecretEntity(t *testing.T) {
supportedEntities: []SecretEntity{
User,
Repository,
Organization,
},
unsupportedEntities: []SecretEntity{
Organization,
Environment,
Unknown,
},