From 42ce8faafa85b9903db88e2d4236c53f1ccc5114 Mon Sep 17 00:00:00 2001 From: nate smith Date: Tue, 19 Oct 2021 14:15:48 -0500 Subject: [PATCH 01/22] dispatch binary extensions directly --- pkg/cmd/extension/extension.go | 4 ++++ pkg/cmd/extension/manager.go | 8 +++++-- pkg/extensions/extension.go | 1 + pkg/extensions/extension_mock.go | 36 ++++++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/extension/extension.go b/pkg/cmd/extension/extension.go index ead9204cd..b4106228f 100644 --- a/pkg/cmd/extension/extension.go +++ b/pkg/cmd/extension/extension.go @@ -48,3 +48,7 @@ func (e *Extension) UpdateAvailable() bool { } return true } + +func (e *Extension) IsBinary() bool { + return e.kind == BinaryKind +} diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index aec54f5b2..7110254a4 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -69,9 +69,11 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri forwardArgs := args[1:] exts, _ := m.list(false) + var ext Extension for _, e := range exts { if e.Name() == extName { - exe = e.Path() + ext = e + exe = ext.Path() break } } @@ -81,7 +83,9 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri var externalCmd *exec.Cmd - if runtime.GOOS == "windows" { + if ext.IsBinary() { + externalCmd = m.newCommand(exe, forwardArgs...) + } else if runtime.GOOS == "windows" { // Dispatch all extension calls through the `sh` interpreter to support executable files with a // shebang line on Windows. shExe, err := m.findSh() diff --git a/pkg/extensions/extension.go b/pkg/extensions/extension.go index f0962a87a..ee55bfabc 100644 --- a/pkg/extensions/extension.go +++ b/pkg/extensions/extension.go @@ -13,6 +13,7 @@ type Extension interface { URL() string IsLocal() bool UpdateAvailable() bool + IsBinary() bool } //go:generate moq -rm -out manager_mock.go . ExtensionManager diff --git a/pkg/extensions/extension_mock.go b/pkg/extensions/extension_mock.go index 17e2a3f6b..b999ab955 100644 --- a/pkg/extensions/extension_mock.go +++ b/pkg/extensions/extension_mock.go @@ -17,6 +17,9 @@ var _ Extension = &ExtensionMock{} // // // make and configure a mocked Extension // mockedExtension := &ExtensionMock{ +// IsBinaryFunc: func() bool { +// panic("mock out the IsBinary method") +// }, // IsLocalFunc: func() bool { // panic("mock out the IsLocal method") // }, @@ -39,6 +42,9 @@ var _ Extension = &ExtensionMock{} // // } type ExtensionMock struct { + // IsBinaryFunc mocks the IsBinary method. + IsBinaryFunc func() bool + // IsLocalFunc mocks the IsLocal method. IsLocalFunc func() bool @@ -56,6 +62,9 @@ type ExtensionMock struct { // calls tracks calls to the methods. calls struct { + // IsBinary holds details about calls to the IsBinary method. + IsBinary []struct { + } // IsLocal holds details about calls to the IsLocal method. IsLocal []struct { } @@ -72,6 +81,7 @@ type ExtensionMock struct { UpdateAvailable []struct { } } + lockIsBinary sync.RWMutex lockIsLocal sync.RWMutex lockName sync.RWMutex lockPath sync.RWMutex @@ -79,6 +89,32 @@ type ExtensionMock struct { lockUpdateAvailable sync.RWMutex } +// IsBinary calls IsBinaryFunc. +func (mock *ExtensionMock) IsBinary() bool { + if mock.IsBinaryFunc == nil { + panic("ExtensionMock.IsBinaryFunc: method is nil but Extension.IsBinary was just called") + } + callInfo := struct { + }{} + mock.lockIsBinary.Lock() + mock.calls.IsBinary = append(mock.calls.IsBinary, callInfo) + mock.lockIsBinary.Unlock() + return mock.IsBinaryFunc() +} + +// IsBinaryCalls gets all the calls that were made to IsBinary. +// Check the length with: +// len(mockedExtension.IsBinaryCalls()) +func (mock *ExtensionMock) IsBinaryCalls() []struct { +} { + var calls []struct { + } + mock.lockIsBinary.RLock() + calls = mock.calls.IsBinary + mock.lockIsBinary.RUnlock() + return calls +} + // IsLocal calls IsLocalFunc. func (mock *ExtensionMock) IsLocal() bool { if mock.IsLocalFunc == nil { From c696416a118b3adec106fa49a95841b80b310826 Mon Sep 17 00:00:00 2001 From: nate smith Date: Tue, 19 Oct 2021 14:52:14 -0500 Subject: [PATCH 02/22] add test --- pkg/cmd/extension/manager_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index 121f47765..e46a30ef5 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -139,6 +139,30 @@ func TestManager_Dispatch(t *testing.T) { assert.Equal(t, "", stderr.String()) } +func TestManager_Dispatch_binary(t *testing.T) { + tempDir := t.TempDir() + extPath := filepath.Join(tempDir, "extensions", "gh-hello") + exePath := filepath.Join(extPath, "gh-hello") + bm := binManifest{ + Owner: "owner", + Name: "gh-hello", + Host: "github.com", + Tag: "v1.0.0", + } + assert.NoError(t, stubBinaryExtension(extPath, bm)) + + m := newTestManager(tempDir, nil, nil) + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + found, err := m.Dispatch([]string{"hello", "one", "two"}, nil, stdout, stderr) + assert.NoError(t, err) + assert.True(t, found) + + assert.Equal(t, fmt.Sprintf("[%s one two]\n", exePath), stdout.String()) + assert.Equal(t, "", stderr.String()) +} + func TestManager_Remove(t *testing.T) { tempDir := t.TempDir() assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) From 8fb5e5e1d519cc781bb762263d13991b632c574e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 20 Oct 2021 14:21:22 -0400 Subject: [PATCH 03/22] Report error if no filename is remote --- pkg/cmd/codespace/ssh.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 5d7c570a8..24607ca92 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -193,7 +193,7 @@ users; see https://lwn.net/Articles/835962/ for discussion. // Copy copies files between the local and remote file systems. // The mechanics are similar to 'ssh' but using 'scp'. -func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) (err error) { +func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) error { if len(args) < 2 { return fmt.Errorf("cp requires source and destination arguments") } @@ -201,8 +201,10 @@ func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) (err erro opts.scpArgs = append(opts.scpArgs, "-r") } opts.scpArgs = append(opts.scpArgs, "--") + hasRemote := false for _, arg := range args { if rest := strings.TrimPrefix(arg, "remote:"); rest != arg { + hasRemote = true // scp treats each filename argument as a shell expression, // subjecting it to expansion of environment variables, braces, // tilde, backticks, globs and so on. Because these present a @@ -225,6 +227,9 @@ func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) (err erro } opts.scpArgs = append(opts.scpArgs, arg) } + if !hasRemote { + return fmt.Errorf("cp: no argument is a 'remote:' filename") + } return a.SSH(ctx, nil, opts.sshOptions) } From 9468e9e7df37ed3a90a494fd08c23d14cdb30f31 Mon Sep 17 00:00:00 2001 From: stdtom <61379168+stdtom@users.noreply.github.com> Date: Thu, 21 Oct 2021 12:34:20 +0200 Subject: [PATCH 04/22] Fix copy/paste mistake in docs --- pkg/cmd/pr/ready/ready.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/ready/ready.go b/pkg/cmd/pr/ready/ready.go index f024ea574..493b1f70a 100644 --- a/pkg/cmd/pr/ready/ready.go +++ b/pkg/cmd/pr/ready/ready.go @@ -35,7 +35,7 @@ func NewCmdReady(f *cmdutil.Factory, runF func(*ReadyOptions) error) *cobra.Comm Mark a pull request as ready for review Without an argument, the pull request that belongs to the current branch - is displayed. + is marked as ready. `), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { From 721552212370a9975fa9eff14221d92e86a59a84 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 21 Oct 2021 10:06:11 -0400 Subject: [PATCH 05/22] use FlagError --- pkg/cmd/codespace/ssh.go | 3 ++- pkg/cmd/repo/fork/fork.go | 2 +- pkg/cmd/workflow/run/run.go | 4 ++-- pkg/cmdutil/errors.go | 7 ++++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 24607ca92..a0f596e49 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/cli/cli/v2/internal/codespaces" + "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/liveshare" "github.com/spf13/cobra" ) @@ -228,7 +229,7 @@ func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) error { opts.scpArgs = append(opts.scpArgs, arg) } if !hasRemote { - return fmt.Errorf("cp: no argument is a 'remote:' filename") + return &cmdutil.FlagError{Err: fmt.Errorf("at least one argument must have a 'remote:' prefix")} } return a.SSH(ctx, nil, opts.sshOptions) } diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index 4134a7ba2..dcd5571f7 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -61,7 +61,7 @@ func NewCmdFork(f *cmdutil.Factory, runF func(*ForkOptions) error) *cobra.Comman Use: "fork [] [-- ...]", Args: func(cmd *cobra.Command, args []string) error { if cmd.ArgsLenAtDash() == 0 && len(args[1:]) > 0 { - return cmdutil.FlagError{Err: fmt.Errorf("repository argument required when passing 'git clone' flags")} + return &cmdutil.FlagError{Err: fmt.Errorf("repository argument required when passing 'git clone' flags")} } return nil }, diff --git a/pkg/cmd/workflow/run/run.go b/pkg/cmd/workflow/run/run.go index 0ab3f1749..5f10e515c 100644 --- a/pkg/cmd/workflow/run/run.go +++ b/pkg/cmd/workflow/run/run.go @@ -78,7 +78,7 @@ func NewCmdRun(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command `), Args: func(cmd *cobra.Command, args []string) error { if len(opts.MagicFields)+len(opts.RawFields) > 0 && len(args) == 0 { - return cmdutil.FlagError{Err: fmt.Errorf("workflow argument required when passing -f or -F")} + return &cmdutil.FlagError{Err: fmt.Errorf("workflow argument required when passing -f or -F")} } return nil }, @@ -103,7 +103,7 @@ func NewCmdRun(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command } opts.JSONInput = string(jsonIn) } else if opts.JSON { - return cmdutil.FlagError{Err: errors.New("--json specified but nothing on STDIN")} + return &cmdutil.FlagError{Err: errors.New("--json specified but nothing on STDIN")} } if opts.Selector == "" { diff --git a/pkg/cmdutil/errors.go b/pkg/cmdutil/errors.go index 8ae139ef4..1e40a0185 100644 --- a/pkg/cmdutil/errors.go +++ b/pkg/cmdutil/errors.go @@ -6,16 +6,17 @@ import ( "github.com/AlecAivazis/survey/v2/terminal" ) -// FlagError is the kind of error raised in flag processing +// A *FlagError indicates an error processing command-line flags or other arguments. +// Such errors cause the application to display the usage message. type FlagError struct { Err error } -func (fe FlagError) Error() string { +func (fe *FlagError) Error() string { return fe.Err.Error() } -func (fe FlagError) Unwrap() error { +func (fe *FlagError) Unwrap() error { return fe.Err } From f4491c7a8027c50dd4b493eb22b987a2b69779ca Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 21 Oct 2021 11:17:43 -0400 Subject: [PATCH 06/22] Add FlagErrorf; encapsulate FlagError.error --- cmd/gh/main_test.go | 2 +- pkg/cmd/api/api.go | 4 ++-- pkg/cmd/auth/login/login.go | 6 +++--- pkg/cmd/auth/logout/logout.go | 2 +- pkg/cmd/auth/refresh/refresh.go | 2 +- pkg/cmd/codespace/list.go | 2 +- pkg/cmd/codespace/ssh.go | 2 +- pkg/cmd/completion/completion.go | 3 +-- pkg/cmd/extension/command.go | 6 +++--- pkg/cmd/gist/clone/clone.go | 2 +- pkg/cmd/gist/create/create.go | 2 +- pkg/cmd/gist/list/list.go | 3 +-- pkg/cmd/gpg-key/add/add.go | 2 +- pkg/cmd/issue/create/create.go | 5 ++--- pkg/cmd/issue/edit/edit.go | 3 +-- pkg/cmd/issue/list/list.go | 2 +- pkg/cmd/pr/checks/checks.go | 3 +-- pkg/cmd/pr/comment/comment.go | 4 +--- pkg/cmd/pr/create/create.go | 4 ++-- pkg/cmd/pr/diff/diff.go | 4 ++-- pkg/cmd/pr/edit/edit.go | 3 +-- pkg/cmd/pr/list/list.go | 2 +- pkg/cmd/pr/merge/merge.go | 6 +++--- pkg/cmd/pr/ready/ready.go | 3 +-- pkg/cmd/pr/review/review.go | 12 ++++++------ pkg/cmd/pr/shared/commentable.go | 6 +++--- pkg/cmd/pr/view/view.go | 3 +-- pkg/cmd/release/download/download.go | 2 +- pkg/cmd/repo/clone/clone.go | 2 +- pkg/cmd/repo/create/create.go | 11 +++++------ pkg/cmd/repo/delete/delete.go | 4 +--- pkg/cmd/repo/fork/fork.go | 9 ++++----- pkg/cmd/repo/list/list.go | 8 ++++---- pkg/cmd/root/help.go | 2 +- pkg/cmd/run/cancel/cancel.go | 2 +- pkg/cmd/run/list/list.go | 2 +- pkg/cmd/run/rerun/rerun.go | 2 +- pkg/cmd/run/view/view.go | 6 +++--- pkg/cmd/run/watch/watch.go | 3 +-- pkg/cmd/secret/set/set.go | 14 +++++--------- pkg/cmd/ssh-key/add/add.go | 3 +-- pkg/cmd/workflow/disable/disable.go | 2 +- pkg/cmd/workflow/enable/enable.go | 2 +- pkg/cmd/workflow/list/list.go | 2 +- pkg/cmd/workflow/run/run.go | 10 +++++----- pkg/cmd/workflow/view/view.go | 5 ++--- pkg/cmdutil/args.go | 9 ++++----- pkg/cmdutil/errors.go | 19 +++++++++++++++---- 48 files changed, 103 insertions(+), 114 deletions(-) diff --git a/cmd/gh/main_test.go b/cmd/gh/main_test.go index b428ff4b3..01552b2bd 100644 --- a/cmd/gh/main_test.go +++ b/cmd/gh/main_test.go @@ -49,7 +49,7 @@ check your internet connection or https://githubstatus.com { name: "Cobra flag error", args: args{ - err: &cmdutil.FlagError{Err: errors.New("unknown flag --foo")}, + err: cmdutil.FlagErrorf("unknown flag --foo"), cmd: cmd, debug: false, }, diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 18498249e..94100d820 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -173,12 +173,12 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command if c.Flags().Changed("hostname") { if err := ghinstance.HostnameValidator(opts.Hostname); err != nil { - return &cmdutil.FlagError{Err: fmt.Errorf("error parsing `--hostname`: %w", err)} + return cmdutil.FlagErrorf("error parsing `--hostname`: %w", err) } } if opts.Paginate && !strings.EqualFold(opts.RequestMethod, "GET") && opts.RequestPath != "graphql" { - return &cmdutil.FlagError{Err: errors.New("the `--paginate` option is not supported for non-GET requests")} + return cmdutil.FlagErrorf("the `--paginate` option is not supported for non-GET requests") } if err := cmdutil.MutuallyExclusive( diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 08bcf1dfd..f591fcbc6 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -69,11 +69,11 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm `), RunE: func(cmd *cobra.Command, args []string) error { if !opts.IO.CanPrompt() && !(tokenStdin || opts.Web) { - return &cmdutil.FlagError{Err: errors.New("--web or --with-token required when not running interactively")} + return cmdutil.FlagErrorf("--web or --with-token required when not running interactively") } if tokenStdin && opts.Web { - return &cmdutil.FlagError{Err: errors.New("specify only one of --web or --with-token")} + return cmdutil.FlagErrorf("specify only one of --web or --with-token") } if tokenStdin { @@ -91,7 +91,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm if cmd.Flags().Changed("hostname") { if err := ghinstance.HostnameValidator(opts.Hostname); err != nil { - return &cmdutil.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)} + return cmdutil.FlagErrorf("error parsing --hostname: %w", err) } } diff --git a/pkg/cmd/auth/logout/logout.go b/pkg/cmd/auth/logout/logout.go index 670c4cc45..3873da324 100644 --- a/pkg/cmd/auth/logout/logout.go +++ b/pkg/cmd/auth/logout/logout.go @@ -48,7 +48,7 @@ func NewCmdLogout(f *cmdutil.Factory, runF func(*LogoutOptions) error) *cobra.Co `), RunE: func(cmd *cobra.Command, args []string) error { if opts.Hostname == "" && !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("--hostname required when not running interactively")} + return cmdutil.FlagErrorf("--hostname required when not running interactively") } if runF != nil { diff --git a/pkg/cmd/auth/refresh/refresh.go b/pkg/cmd/auth/refresh/refresh.go index 4137341a9..f2b9cbbb4 100644 --- a/pkg/cmd/auth/refresh/refresh.go +++ b/pkg/cmd/auth/refresh/refresh.go @@ -62,7 +62,7 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra. opts.Interactive = opts.IO.CanPrompt() if !opts.Interactive && opts.Hostname == "" { - return &cmdutil.FlagError{Err: errors.New("--hostname required when not running interactively")} + return cmdutil.FlagErrorf("--hostname required when not running interactively") } opts.MainExecutable = f.Executable() diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index e130c9ed7..c5bc7a10b 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -20,7 +20,7 @@ func newListCmd(app *App) *cobra.Command { Args: noArgsConstraint, RunE: func(cmd *cobra.Command, args []string) error { if limit < 1 { - return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", limit)} + return cmdutil.FlagErrorf("invalid limit: %v", limit) } return app.List(cmd.Context(), asJSON, limit) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index a0f596e49..32fdade34 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -229,7 +229,7 @@ func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) error { opts.scpArgs = append(opts.scpArgs, arg) } if !hasRemote { - return &cmdutil.FlagError{Err: fmt.Errorf("at least one argument must have a 'remote:' prefix")} + return cmdutil.FlagErrorf("at least one argument must have a 'remote:' prefix") } return a.SSH(ctx, nil, opts.sshOptions) } diff --git a/pkg/cmd/completion/completion.go b/pkg/cmd/completion/completion.go index e711d6db6..496f06784 100644 --- a/pkg/cmd/completion/completion.go +++ b/pkg/cmd/completion/completion.go @@ -1,7 +1,6 @@ package completion import ( - "errors" "fmt" "github.com/MakeNowJust/heredoc" @@ -68,7 +67,7 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { if shellType == "" { if io.IsStdoutTTY() { - return &cmdutil.FlagError{Err: errors.New("error: the value for `--shell` is required")} + return cmdutil.FlagErrorf("error: the value for `--shell` is required") } shellType = "bash" } diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index 7b51e1498..bc3f0dab4 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -117,13 +117,13 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { Short: "Upgrade installed extensions", Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 && !flagAll { - return &cmdutil.FlagError{Err: errors.New("must specify an extension to upgrade")} + return cmdutil.FlagErrorf("must specify an extension to upgrade") } if len(args) > 0 && flagAll { - return &cmdutil.FlagError{Err: errors.New("cannot use `--all` with extension name")} + return cmdutil.FlagErrorf("cannot use `--all` with extension name") } if len(args) > 1 { - return &cmdutil.FlagError{Err: errors.New("too many arguments")} + return cmdutil.FlagErrorf("too many arguments") } return nil }, diff --git a/pkg/cmd/gist/clone/clone.go b/pkg/cmd/gist/clone/clone.go index 460e6fa1d..cbd14b324 100644 --- a/pkg/cmd/gist/clone/clone.go +++ b/pkg/cmd/gist/clone/clone.go @@ -61,7 +61,7 @@ func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Comm if err == pflag.ErrHelp { return err } - return &cmdutil.FlagError{Err: fmt.Errorf("%w\nSeparate git clone flags with '--'.", err)} + return cmdutil.FlagErrorf("%w\nSeparate git clone flags with '--'.", err) }) return cmd diff --git a/pkg/cmd/gist/create/create.go b/pkg/cmd/gist/create/create.go index 098e13f0f..5bb2bb2a3 100644 --- a/pkg/cmd/gist/create/create.go +++ b/pkg/cmd/gist/create/create.go @@ -82,7 +82,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co return nil } if opts.IO.IsStdinTTY() { - return &cmdutil.FlagError{Err: errors.New("no filenames passed and nothing on STDIN")} + return cmdutil.FlagErrorf("no filenames passed and nothing on STDIN") } return nil }, diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index 374f15852..229ebdc8e 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -1,7 +1,6 @@ package list import ( - "fmt" "net/http" "strings" "time" @@ -40,7 +39,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { if opts.Limit < 1 { - return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.Limit)} + return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit) } opts.Visibility = "all" diff --git a/pkg/cmd/gpg-key/add/add.go b/pkg/cmd/gpg-key/add/add.go index 4aa6d242a..3e8da7ad7 100644 --- a/pkg/cmd/gpg-key/add/add.go +++ b/pkg/cmd/gpg-key/add/add.go @@ -35,7 +35,7 @@ func NewCmdAdd(f *cmdutil.Factory, runF func(*AddOptions) error) *cobra.Command RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { if opts.IO.IsStdoutTTY() && opts.IO.IsStdinTTY() { - return &cmdutil.FlagError{Err: errors.New("GPG key file missing")} + return cmdutil.FlagErrorf("GPG key file missing") } opts.KeyFile = "-" } else { diff --git a/pkg/cmd/issue/create/create.go b/pkg/cmd/issue/create/create.go index 10c83bdb4..db0ab7c0d 100644 --- a/pkg/cmd/issue/create/create.go +++ b/pkg/cmd/issue/create/create.go @@ -1,7 +1,6 @@ package create import ( - "errors" "fmt" "net/http" @@ -83,13 +82,13 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co } if !opts.IO.CanPrompt() && opts.RecoverFile != "" { - return &cmdutil.FlagError{Err: errors.New("`--recover` only supported when running interactively")} + return cmdutil.FlagErrorf("`--recover` only supported when running interactively") } opts.Interactive = !(titleProvided && bodyProvided) if opts.Interactive && !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("must provide title and body when not running interactively")} + return cmdutil.FlagErrorf("must provide title and body when not running interactively") } if runF != nil { diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index e95bf07d6..433141583 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -1,7 +1,6 @@ package edit import ( - "errors" "fmt" "net/http" @@ -106,7 +105,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman } if opts.Interactive && !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("field to edit flag required when not running interactively")} + return cmdutil.FlagErrorf("field to edit flag required when not running interactively") } if runF != nil { diff --git a/pkg/cmd/issue/list/list.go b/pkg/cmd/issue/list/list.go index 2db0815b4..8e26a8afe 100644 --- a/pkg/cmd/issue/list/list.go +++ b/pkg/cmd/issue/list/list.go @@ -68,7 +68,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman opts.BaseRepo = f.BaseRepo if opts.LimitResults < 1 { - return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.LimitResults)} + return cmdutil.FlagErrorf("invalid limit: %v", opts.LimitResults) } if runF != nil { diff --git a/pkg/cmd/pr/checks/checks.go b/pkg/cmd/pr/checks/checks.go index abd4d9a7b..516c06ca7 100644 --- a/pkg/cmd/pr/checks/checks.go +++ b/pkg/cmd/pr/checks/checks.go @@ -1,7 +1,6 @@ package checks import ( - "errors" "fmt" "sort" "time" @@ -49,7 +48,7 @@ func NewCmdChecks(f *cmdutil.Factory, runF func(*ChecksOptions) error) *cobra.Co opts.Finder = shared.NewFinder(f) if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 { - return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")} + return cmdutil.FlagErrorf("argument required when using the --repo flag") } if len(args) > 0 { diff --git a/pkg/cmd/pr/comment/comment.go b/pkg/cmd/pr/comment/comment.go index ec6bf7f23..ff04c2813 100644 --- a/pkg/cmd/pr/comment/comment.go +++ b/pkg/cmd/pr/comment/comment.go @@ -1,8 +1,6 @@ package comment import ( - "errors" - "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/pr/shared" @@ -39,7 +37,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err Args: cobra.MaximumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 { - return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")} + return cmdutil.FlagErrorf("argument required when using the --repo flag") } var selector string if len(args) > 0 { diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 3664f910c..b9e450e71 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -126,11 +126,11 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co opts.MaintainerCanModify = !noMaintainerEdit if !opts.IO.CanPrompt() && opts.RecoverFile != "" { - return &cmdutil.FlagError{Err: errors.New("`--recover` only supported when running interactively")} + return cmdutil.FlagErrorf("`--recover` only supported when running interactively") } if !opts.IO.CanPrompt() && !opts.WebMode && !opts.TitleProvided && !opts.Autofill { - return &cmdutil.FlagError{Err: errors.New("`--title` or `--fill` required when not running interactively")} + return cmdutil.FlagErrorf("`--title` or `--fill` required when not running interactively") } if opts.IsDraft && opts.WebMode { diff --git a/pkg/cmd/pr/diff/diff.go b/pkg/cmd/pr/diff/diff.go index d89fe868a..c8e7cc482 100644 --- a/pkg/cmd/pr/diff/diff.go +++ b/pkg/cmd/pr/diff/diff.go @@ -50,7 +50,7 @@ func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Comman opts.Finder = shared.NewFinder(f) if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 { - return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")} + return cmdutil.FlagErrorf("argument required when using the --repo flag") } if len(args) > 0 { @@ -58,7 +58,7 @@ func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Comman } if !validColorFlag(opts.UseColor) { - return &cmdutil.FlagError{Err: fmt.Errorf("did not understand color: %q. Expected one of always, never, or auto", opts.UseColor)} + return cmdutil.FlagErrorf("did not understand color: %q. Expected one of always, never, or auto", opts.UseColor) } if opts.UseColor == "auto" && !opts.IO.IsStdoutTTY() { diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 3a016aff6..17f35a2ee 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -1,7 +1,6 @@ package edit import ( - "errors" "fmt" "net/http" @@ -120,7 +119,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman } if opts.Interactive && !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("--tile, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively")} + return cmdutil.FlagErrorf("--tile, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively") } if runF != nil { diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index c86bae796..acc4ffb14 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -75,7 +75,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman opts.BaseRepo = f.BaseRepo if opts.LimitResults < 1 { - return &cmdutil.FlagError{Err: fmt.Errorf("invalid value for --limit: %v", opts.LimitResults)} + return cmdutil.FlagErrorf("invalid value for --limit: %v", opts.LimitResults) } if cmd.Flags().Changed("draft") { diff --git a/pkg/cmd/pr/merge/merge.go b/pkg/cmd/pr/merge/merge.go index 38c08bb1c..584a535b6 100644 --- a/pkg/cmd/pr/merge/merge.go +++ b/pkg/cmd/pr/merge/merge.go @@ -75,7 +75,7 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm opts.Finder = shared.NewFinder(f) if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 { - return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")} + return cmdutil.FlagErrorf("argument required when using the --repo flag") } if len(args) > 0 { @@ -97,11 +97,11 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm } if methodFlags == 0 { if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("--merge, --rebase, or --squash required when not running interactively")} + return cmdutil.FlagErrorf("--merge, --rebase, or --squash required when not running interactively") } opts.InteractiveMode = true } else if methodFlags > 1 { - return &cmdutil.FlagError{Err: errors.New("only one of --merge, --rebase, or --squash can be enabled")} + return cmdutil.FlagErrorf("only one of --merge, --rebase, or --squash can be enabled") } opts.IsDeleteBranchIndicated = cmd.Flags().Changed("delete-branch") diff --git a/pkg/cmd/pr/ready/ready.go b/pkg/cmd/pr/ready/ready.go index 493b1f70a..196b1e290 100644 --- a/pkg/cmd/pr/ready/ready.go +++ b/pkg/cmd/pr/ready/ready.go @@ -1,7 +1,6 @@ package ready import ( - "errors" "fmt" "net/http" @@ -42,7 +41,7 @@ func NewCmdReady(f *cmdutil.Factory, runF func(*ReadyOptions) error) *cobra.Comm opts.Finder = shared.NewFinder(f) if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 { - return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")} + return cmdutil.FlagErrorf("argument required when using the --repo flag") } if len(args) > 0 { diff --git a/pkg/cmd/pr/review/review.go b/pkg/cmd/pr/review/review.go index 75a8e362e..d3b15a4ec 100644 --- a/pkg/cmd/pr/review/review.go +++ b/pkg/cmd/pr/review/review.go @@ -72,7 +72,7 @@ func NewCmdReview(f *cmdutil.Factory, runF func(*ReviewOptions) error) *cobra.Co opts.Finder = shared.NewFinder(f) if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 { - return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")} + return cmdutil.FlagErrorf("argument required when using the --repo flag") } if len(args) > 0 { @@ -106,26 +106,26 @@ func NewCmdReview(f *cmdutil.Factory, runF func(*ReviewOptions) error) *cobra.Co found++ opts.ReviewType = api.ReviewRequestChanges if opts.Body == "" { - return &cmdutil.FlagError{Err: errors.New("body cannot be blank for request-changes review")} + return cmdutil.FlagErrorf("body cannot be blank for request-changes review") } } if flagComment { found++ opts.ReviewType = api.ReviewComment if opts.Body == "" { - return &cmdutil.FlagError{Err: errors.New("body cannot be blank for comment review")} + return cmdutil.FlagErrorf("body cannot be blank for comment review") } } if found == 0 && opts.Body == "" { if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("--approve, --request-changes, or --comment required when not running interactively")} + return cmdutil.FlagErrorf("--approve, --request-changes, or --comment required when not running interactively") } opts.InteractiveMode = true } else if found == 0 && opts.Body != "" { - return &cmdutil.FlagError{Err: errors.New("--body unsupported without --approve, --request-changes, or --comment")} + return cmdutil.FlagErrorf("--body unsupported without --approve, --request-changes, or --comment") } else if found > 1 { - return &cmdutil.FlagError{Err: errors.New("need exactly one of --approve, --request-changes, or --comment")} + return cmdutil.FlagErrorf("need exactly one of --approve, --request-changes, or --comment") } if runF != nil { diff --git a/pkg/cmd/pr/shared/commentable.go b/pkg/cmd/pr/shared/commentable.go index 73f27b935..b3f1601d5 100644 --- a/pkg/cmd/pr/shared/commentable.go +++ b/pkg/cmd/pr/shared/commentable.go @@ -65,15 +65,15 @@ func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error { if inputFlags == 0 { if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("`--body`, `--body-file` or `--web` required when not running interactively")} + return cmdutil.FlagErrorf("`--body`, `--body-file` or `--web` required when not running interactively") } opts.Interactive = true } else if inputFlags == 1 { if !opts.IO.CanPrompt() && opts.InputType == InputTypeEditor { - return &cmdutil.FlagError{Err: errors.New("`--body`, `--body-file` or `--web` required when not running interactively")} + return cmdutil.FlagErrorf("`--body`, `--body-file` or `--web` required when not running interactively") } } else if inputFlags > 1 { - return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--body`, `--body-file`, `--editor`, or `--web`")} + return cmdutil.FlagErrorf("specify only one of `--body`, `--body-file`, `--editor`, or `--web`") } return nil diff --git a/pkg/cmd/pr/view/view.go b/pkg/cmd/pr/view/view.go index b62022d5e..bc3f517e4 100644 --- a/pkg/cmd/pr/view/view.go +++ b/pkg/cmd/pr/view/view.go @@ -1,7 +1,6 @@ package view import ( - "errors" "fmt" "sort" "strconv" @@ -55,7 +54,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman opts.Finder = shared.NewFinder(f) if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 { - return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")} + return cmdutil.FlagErrorf("argument required when using the --repo flag") } if len(args) > 0 { diff --git a/pkg/cmd/release/download/download.go b/pkg/cmd/release/download/download.go index 97f4c3e7a..5a2b48706 100644 --- a/pkg/cmd/release/download/download.go +++ b/pkg/cmd/release/download/download.go @@ -61,7 +61,7 @@ func NewCmdDownload(f *cmdutil.Factory, runF func(*DownloadOptions) error) *cobr if len(args) == 0 { if len(opts.FilePatterns) == 0 { - return &cmdutil.FlagError{Err: errors.New("the '--pattern' flag is required when downloading the latest release")} + return cmdutil.FlagErrorf("the '--pattern' flag is required when downloading the latest release") } } else { opts.TagName = args[0] diff --git a/pkg/cmd/repo/clone/clone.go b/pkg/cmd/repo/clone/clone.go index 9dc6ddc73..ebfd65ec1 100644 --- a/pkg/cmd/repo/clone/clone.go +++ b/pkg/cmd/repo/clone/clone.go @@ -62,7 +62,7 @@ func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Comm if err == pflag.ErrHelp { return err } - return &cmdutil.FlagError{Err: fmt.Errorf("%w\nSeparate git clone flags with '--'.", err)} + return cmdutil.FlagErrorf("%w\nSeparate git clone flags with '--'.", err) }) return cmd diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index a0fe837e7..dc1e0bf8f 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -1,7 +1,6 @@ package create import ( - "errors" "fmt" "net/http" "path" @@ -94,25 +93,25 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co } if len(args) == 0 && (opts.GitIgnoreTemplate != "" || opts.LicenseTemplate != "") { - return &cmdutil.FlagError{Err: errors.New(".gitignore and license templates are added only when a specific repository name is passed")} + return cmdutil.FlagErrorf(".gitignore and license templates are added only when a specific repository name is passed") } if opts.Template != "" && (opts.GitIgnoreTemplate != "" || opts.LicenseTemplate != "") { - return &cmdutil.FlagError{Err: errors.New(".gitignore and license templates are not added when template is provided")} + return cmdutil.FlagErrorf(".gitignore and license templates are not added when template is provided") } if !opts.IO.CanPrompt() { if opts.Name == "" { - return &cmdutil.FlagError{Err: errors.New("name argument required when not running interactively")} + return cmdutil.FlagErrorf("name argument required when not running interactively") } if !opts.Internal && !opts.Private && !opts.Public { - return &cmdutil.FlagError{Err: errors.New("`--public`, `--private`, or `--internal` required when not running interactively")} + return cmdutil.FlagErrorf("`--public`, `--private`, or `--internal` required when not running interactively") } } if opts.Template != "" && (opts.Homepage != "" || opts.Team != "" || cmd.Flags().Changed("enable-issues") || cmd.Flags().Changed("enable-wiki")) { - return &cmdutil.FlagError{Err: errors.New("The `--template` option is not supported with `--homepage`, `--team`, `--enable-issues`, or `--enable-wiki`")} + return cmdutil.FlagErrorf("The `--template` option is not supported with `--homepage`, `--team`, `--enable-issues`, or `--enable-wiki`") } if runF != nil { diff --git a/pkg/cmd/repo/delete/delete.go b/pkg/cmd/repo/delete/delete.go index 9dda14e2d..3036f7697 100644 --- a/pkg/cmd/repo/delete/delete.go +++ b/pkg/cmd/repo/delete/delete.go @@ -1,7 +1,6 @@ package delete import ( - "errors" "fmt" "net/http" "strings" @@ -41,8 +40,7 @@ To authorize, run "gh auth refresh -s delete_repo"`, RunE: func(cmd *cobra.Command, args []string) error { opts.RepoArg = args[0] if !opts.IO.CanPrompt() && !opts.Confirmed { - return &cmdutil.FlagError{ - Err: errors.New("could not prompt: confirmation with prompt or --confirm flag required")} + return cmdutil.FlagErrorf("could not prompt: confirmation with prompt or --confirm flag required") } if runF != nil { return runF(opts) diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index dcd5571f7..773f46641 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -1,7 +1,6 @@ package fork import ( - "errors" "fmt" "net/http" "net/url" @@ -61,7 +60,7 @@ func NewCmdFork(f *cmdutil.Factory, runF func(*ForkOptions) error) *cobra.Comman Use: "fork [] [-- ...]", Args: func(cmd *cobra.Command, args []string) error { if cmd.ArgsLenAtDash() == 0 && len(args[1:]) > 0 { - return &cmdutil.FlagError{Err: fmt.Errorf("repository argument required when passing 'git clone' flags")} + return cmdutil.FlagErrorf("repository argument required when passing 'git clone' flags") } return nil }, @@ -84,11 +83,11 @@ Additional 'git clone' flags can be passed in by listing them after '--'.`, } if cmd.Flags().Changed("org") && opts.Organization == "" { - return &cmdutil.FlagError{Err: errors.New("--org cannot be blank")} + return cmdutil.FlagErrorf("--org cannot be blank") } if opts.RemoteName == "" { - return &cmdutil.FlagError{Err: errors.New("--remote-name cannot be blank")} + return cmdutil.FlagErrorf("--remote-name cannot be blank") } else if !cmd.Flags().Changed("remote-name") { opts.Rename = true // Any existing 'origin' will be renamed to upstream } @@ -109,7 +108,7 @@ Additional 'git clone' flags can be passed in by listing them after '--'.`, if err == pflag.ErrHelp { return err } - return &cmdutil.FlagError{Err: fmt.Errorf("%w\nSeparate git clone flags with '--'.", err)} + return cmdutil.FlagErrorf("%w\nSeparate git clone flags with '--'.", err) }) cmd.Flags().BoolVar(&opts.Clone, "clone", false, "Clone the fork {true|false}") diff --git a/pkg/cmd/repo/list/list.go b/pkg/cmd/repo/list/list.go index af6f505c0..e1ed57884 100644 --- a/pkg/cmd/repo/list/list.go +++ b/pkg/cmd/repo/list/list.go @@ -54,17 +54,17 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman Short: "List repositories owned by user or organization", RunE: func(c *cobra.Command, args []string) error { if opts.Limit < 1 { - return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.Limit)} + return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit) } if flagPrivate && flagPublic { - return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--public` or `--private`")} + return cmdutil.FlagErrorf("specify only one of `--public` or `--private`") } if opts.Source && opts.Fork { - return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--source` or `--fork`")} + return cmdutil.FlagErrorf("specify only one of `--source` or `--fork`") } if opts.Archived && opts.NonArchived { - return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--archived` or `--no-archived`")} + return cmdutil.FlagErrorf("specify only one of `--archived` or `--no-archived`") } if flagPrivate { diff --git a/pkg/cmd/root/help.go b/pkg/cmd/root/help.go index e9eb0703e..d154df6a8 100644 --- a/pkg/cmd/root/help.go +++ b/pkg/cmd/root/help.go @@ -38,7 +38,7 @@ func rootFlagErrorFunc(cmd *cobra.Command, err error) error { if err == pflag.ErrHelp { return err } - return &cmdutil.FlagError{Err: err} + return cmdutil.FlagErrorWrap(err) } var hasFailed bool diff --git a/pkg/cmd/run/cancel/cancel.go b/pkg/cmd/run/cancel/cancel.go index 8499dbaa6..b2d1d4543 100644 --- a/pkg/cmd/run/cancel/cancel.go +++ b/pkg/cmd/run/cancel/cancel.go @@ -40,7 +40,7 @@ func NewCmdCancel(f *cmdutil.Factory, runF func(*CancelOptions) error) *cobra.Co if len(args) > 0 { opts.RunID = args[0] } else if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("run ID required when not running interactively")} + return cmdutil.FlagErrorf("run ID required when not running interactively") } else { opts.Prompt = true } diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index 103b39196..cf4e61586 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -48,7 +48,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman opts.PlainOutput = !terminal if opts.Limit < 1 { - return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.Limit)} + return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit) } if runF != nil { diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 7f43d66df..feec21599 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -40,7 +40,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm if len(args) > 0 { opts.RunID = args[0] } else if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("run ID required when not running interactively")} + return cmdutil.FlagErrorf("run ID required when not running interactively") } else { opts.Prompt = true } diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 966bb6dbf..76191e211 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -118,7 +118,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman if len(args) == 0 && opts.JobID == "" { if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("run or job ID required when not running interactively")} + return cmdutil.FlagErrorf("run or job ID required when not running interactively") } else { opts.Prompt = true } @@ -135,11 +135,11 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman } if opts.Web && opts.Log { - return &cmdutil.FlagError{Err: errors.New("specify only one of --web or --log")} + return cmdutil.FlagErrorf("specify only one of --web or --log") } if opts.Log && opts.LogFailed { - return &cmdutil.FlagError{Err: errors.New("specify only one of --log or --log-failed")} + return cmdutil.FlagErrorf("specify only one of --log or --log-failed") } if runF != nil { diff --git a/pkg/cmd/run/watch/watch.go b/pkg/cmd/run/watch/watch.go index 8f62c1e9e..53b1437bc 100644 --- a/pkg/cmd/run/watch/watch.go +++ b/pkg/cmd/run/watch/watch.go @@ -1,7 +1,6 @@ package watch import ( - "errors" "fmt" "net/http" "runtime" @@ -57,7 +56,7 @@ func NewCmdWatch(f *cmdutil.Factory, runF func(*WatchOptions) error) *cobra.Comm if len(args) > 0 { opts.RunID = args[0] } else if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("run ID required when not running interactively")} + return cmdutil.FlagErrorf("run ID required when not running interactively") } else { opts.Prompt = true } diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index 47fe26fc6..77f818914 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -71,7 +71,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command `), Args: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { - return &cmdutil.FlagError{Err: errors.New("must pass single secret name")} + return cmdutil.FlagErrorf("must pass single secret name") } return nil }, @@ -92,23 +92,19 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command if cmd.Flags().Changed("visibility") { if opts.OrgName == "" { - return &cmdutil.FlagError{Err: errors.New( - "--visibility not supported for repository secrets; did you mean to pass --org?")} + return cmdutil.FlagErrorf("--visibility not supported for repository secrets; did you mean to pass --org?") } if opts.Visibility != shared.All && opts.Visibility != shared.Private && opts.Visibility != shared.Selected { - return &cmdutil.FlagError{Err: errors.New( - "--visibility must be one of `all`, `private`, or `selected`")} + return cmdutil.FlagErrorf("--visibility must be one of `all`, `private`, or `selected`") } if opts.Visibility != shared.Selected && cmd.Flags().Changed("repos") { - return &cmdutil.FlagError{Err: errors.New( - "--repos only supported when --visibility='selected'")} + return cmdutil.FlagErrorf("--repos only supported when --visibility='selected'") } if opts.Visibility == shared.Selected && !cmd.Flags().Changed("repos") { - return &cmdutil.FlagError{Err: errors.New( - "--repos flag required when --visibility='selected'")} + return cmdutil.FlagErrorf("--repos flag required when --visibility='selected'") } } else { if cmd.Flags().Changed("repos") { diff --git a/pkg/cmd/ssh-key/add/add.go b/pkg/cmd/ssh-key/add/add.go index 53759acfb..8892527ed 100644 --- a/pkg/cmd/ssh-key/add/add.go +++ b/pkg/cmd/ssh-key/add/add.go @@ -1,7 +1,6 @@ package add import ( - "errors" "fmt" "io" "net/http" @@ -36,7 +35,7 @@ func NewCmdAdd(f *cmdutil.Factory, runF func(*AddOptions) error) *cobra.Command RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { if opts.IO.IsStdoutTTY() && opts.IO.IsStdinTTY() { - return &cmdutil.FlagError{Err: errors.New("public key file missing")} + return cmdutil.FlagErrorf("public key file missing") } opts.KeyFile = "-" } else { diff --git a/pkg/cmd/workflow/disable/disable.go b/pkg/cmd/workflow/disable/disable.go index 5869b3201..909ab6a39 100644 --- a/pkg/cmd/workflow/disable/disable.go +++ b/pkg/cmd/workflow/disable/disable.go @@ -40,7 +40,7 @@ func NewCmdDisable(f *cmdutil.Factory, runF func(*DisableOptions) error) *cobra. if len(args) > 0 { opts.Selector = args[0] } else if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("workflow ID or name required when not running interactively")} + return cmdutil.FlagErrorf("workflow ID or name required when not running interactively") } else { opts.Prompt = true } diff --git a/pkg/cmd/workflow/enable/enable.go b/pkg/cmd/workflow/enable/enable.go index f0d9e077d..342601ccf 100644 --- a/pkg/cmd/workflow/enable/enable.go +++ b/pkg/cmd/workflow/enable/enable.go @@ -40,7 +40,7 @@ func NewCmdEnable(f *cmdutil.Factory, runF func(*EnableOptions) error) *cobra.Co if len(args) > 0 { opts.Selector = args[0] } else if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("workflow ID or name required when not running interactively")} + return cmdutil.FlagErrorf("workflow ID or name required when not running interactively") } else { opts.Prompt = true } diff --git a/pkg/cmd/workflow/list/list.go b/pkg/cmd/workflow/list/list.go index 87ebb4c01..cd3e66c25 100644 --- a/pkg/cmd/workflow/list/list.go +++ b/pkg/cmd/workflow/list/list.go @@ -45,7 +45,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman opts.PlainOutput = !terminal if opts.Limit < 1 { - return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.Limit)} + return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit) } if runF != nil { diff --git a/pkg/cmd/workflow/run/run.go b/pkg/cmd/workflow/run/run.go index 5f10e515c..99de08d51 100644 --- a/pkg/cmd/workflow/run/run.go +++ b/pkg/cmd/workflow/run/run.go @@ -78,7 +78,7 @@ func NewCmdRun(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command `), Args: func(cmd *cobra.Command, args []string) error { if len(opts.MagicFields)+len(opts.RawFields) > 0 && len(args) == 0 { - return &cmdutil.FlagError{Err: fmt.Errorf("workflow argument required when passing -f or -F")} + return cmdutil.FlagErrorf("workflow argument required when passing -f or -F") } return nil }, @@ -91,7 +91,7 @@ func NewCmdRun(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command if len(args) > 0 { opts.Selector = args[0] } else if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("workflow ID, name, or filename required when not running interactively")} + return cmdutil.FlagErrorf("workflow ID, name, or filename required when not running interactively") } else { opts.Prompt = true } @@ -103,16 +103,16 @@ func NewCmdRun(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command } opts.JSONInput = string(jsonIn) } else if opts.JSON { - return &cmdutil.FlagError{Err: errors.New("--json specified but nothing on STDIN")} + return cmdutil.FlagErrorf("--json specified but nothing on STDIN") } if opts.Selector == "" { if opts.JSONInput != "" { - return &cmdutil.FlagError{Err: errors.New("workflow argument required when passing JSON")} + return cmdutil.FlagErrorf("workflow argument required when passing JSON") } } else { if opts.JSON && inputFieldsPassed { - return &cmdutil.FlagError{Err: errors.New("only one of STDIN or -f/-F can be passed")} + return cmdutil.FlagErrorf("only one of STDIN or -f/-F can be passed") } } diff --git a/pkg/cmd/workflow/view/view.go b/pkg/cmd/workflow/view/view.go index dc38ed42c..9370a0e26 100644 --- a/pkg/cmd/workflow/view/view.go +++ b/pkg/cmd/workflow/view/view.go @@ -1,7 +1,6 @@ package view import ( - "errors" "fmt" "net/http" "strings" @@ -59,13 +58,13 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman if len(args) > 0 { opts.Selector = args[0] } else if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("workflow argument required when not running interactively")} + return cmdutil.FlagErrorf("workflow argument required when not running interactively") } else { opts.Prompt = true } if !opts.YAML && opts.Ref != "" { - return &cmdutil.FlagError{Err: errors.New("`--yaml` required when specifying `--ref`")} + return cmdutil.FlagErrorf("`--yaml` required when specifying `--ref`") } if runF != nil { diff --git a/pkg/cmdutil/args.go b/pkg/cmdutil/args.go index ee9c5e350..859013ae5 100644 --- a/pkg/cmdutil/args.go +++ b/pkg/cmdutil/args.go @@ -1,7 +1,6 @@ package cmdutil import ( - "errors" "fmt" "github.com/spf13/cobra" @@ -15,7 +14,7 @@ func MinimumArgs(n int, msg string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) < n { - return &FlagError{Err: errors.New(msg)} + return FlagErrorf("%s", msg) } return nil } @@ -25,11 +24,11 @@ func ExactArgs(n int, msg string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) > n { - return &FlagError{Err: errors.New("too many arguments")} + return FlagErrorf("too many arguments") } if len(args) < n { - return &FlagError{Err: errors.New(msg)} + return FlagErrorf("%s", msg) } return nil @@ -57,5 +56,5 @@ func NoArgsQuoteReminder(cmd *cobra.Command, args []string) error { errMsg += "; please quote all values that have spaces" } - return &FlagError{Err: errors.New(errMsg)} + return FlagErrorf("%s", errMsg) } diff --git a/pkg/cmdutil/errors.go b/pkg/cmdutil/errors.go index 1e40a0185..3a45c3377 100644 --- a/pkg/cmdutil/errors.go +++ b/pkg/cmdutil/errors.go @@ -2,22 +2,33 @@ package cmdutil import ( "errors" + "fmt" "github.com/AlecAivazis/survey/v2/terminal" ) +// FlagErrorf returns a new FlagError that wraps an error produced by +// fmt.Errorf(format, args...). +func FlagErrorf(format string, args ...interface{}) error { + return FlagErrorWrap(fmt.Errorf(format, args...)) +} + +// FlagError returns a new FlagError that wraps the specified error. +func FlagErrorWrap(err error) error { return &FlagError{err} } + // A *FlagError indicates an error processing command-line flags or other arguments. // Such errors cause the application to display the usage message. type FlagError struct { - Err error + // Note: not struct{error}: only *FlagError should satisfy error. + err error } func (fe *FlagError) Error() string { - return fe.Err.Error() + return fe.err.Error() } func (fe *FlagError) Unwrap() error { - return fe.Err + return fe.err } // SilentError is an error that triggers exit code 1 without any error messaging @@ -38,7 +49,7 @@ func MutuallyExclusive(message string, conditions ...bool) error { } } if numTrue > 1 { - return &FlagError{Err: errors.New(message)} + return FlagErrorf("%s", message) } return nil } From bbea5ac95e2a794798b02d02b4cf3512e9c24320 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Thu, 21 Oct 2021 12:04:04 -0400 Subject: [PATCH 07/22] codespace: progress indication, logging (#4555) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rework logging, showing progress, and printing from `codespace` commands * Change rendering of the general progress indicator so that it's visible on both dark and light backgrounds * The progress indicator now "spins" faster Co-authored-by: Mislav Marohnić --- internal/codespaces/codespaces.go | 40 +++++++++++-------------------- internal/codespaces/ssh.go | 8 +++++-- internal/codespaces/states.go | 17 +++++++++---- pkg/cmd/codespace/common.go | 23 ++++++++++++++---- pkg/cmd/codespace/create.go | 33 ++++++++++++------------- pkg/cmd/codespace/delete.go | 22 ++++++++++------- pkg/cmd/codespace/delete_test.go | 21 +++++++--------- pkg/cmd/codespace/list.go | 5 ++-- pkg/cmd/codespace/logs.go | 15 ++++++------ pkg/cmd/codespace/ports.go | 26 +++++++++++--------- pkg/cmd/codespace/ssh.go | 22 +++++++++-------- pkg/cmd/codespace/stop.go | 7 +++++- pkg/cmd/pr/shared/finder.go | 1 + pkg/cmd/root/root.go | 8 +++---- pkg/iostreams/iostreams.go | 29 +++++++++++++++++++++- 15 files changed, 166 insertions(+), 111 deletions(-) diff --git a/internal/codespaces/codespaces.go b/internal/codespaces/codespaces.go index 7755e5272..330a6a772 100644 --- a/internal/codespaces/codespaces.go +++ b/internal/codespaces/codespaces.go @@ -10,18 +10,6 @@ import ( "github.com/cli/cli/v2/pkg/liveshare" ) -type logger interface { - Print(v ...interface{}) (int, error) - Println(v ...interface{}) (int, error) -} - -// TODO(josebalius): clean this up once we standardrize -// logging for codespaces -type liveshareLogger interface { - Println(v ...interface{}) - Printf(f string, v ...interface{}) -} - func connectionReady(codespace *api.Codespace) bool { return codespace.Connection.SessionID != "" && codespace.Connection.SessionToken != "" && @@ -35,13 +23,21 @@ type apiClient interface { StartCodespace(ctx context.Context, name string) error } +type progressIndicator interface { + StartProgressIndicatorWithLabel(s string) + StopProgressIndicator() +} + +type logger interface { + Println(v ...interface{}) + Printf(f string, v ...interface{}) +} + // ConnectToLiveshare waits for a Codespace to become running, // and connects to it using a Live Share session. -func ConnectToLiveshare(ctx context.Context, log logger, sessionLogger liveshareLogger, apiClient apiClient, codespace *api.Codespace) (*liveshare.Session, error) { - var startedCodespace bool +func ConnectToLiveshare(ctx context.Context, progress progressIndicator, sessionLogger logger, apiClient apiClient, codespace *api.Codespace) (sess *liveshare.Session, err error) { if codespace.State != api.CodespaceStateAvailable { - startedCodespace = true - log.Print("Starting your codespace...") + progress.StartProgressIndicatorWithLabel("Starting codespace") if err := apiClient.StartCodespace(ctx, codespace.Name); err != nil { return nil, fmt.Errorf("error starting codespace: %w", err) } @@ -49,10 +45,6 @@ func ConnectToLiveshare(ctx context.Context, log logger, sessionLogger liveshare for retries := 0; !connectionReady(codespace); retries++ { if retries > 1 { - if retries%2 == 0 { - log.Print(".") - } - time.Sleep(1 * time.Second) } @@ -60,18 +52,14 @@ func ConnectToLiveshare(ctx context.Context, log logger, sessionLogger liveshare return nil, errors.New("timed out while waiting for the codespace to start") } - var err error codespace, err = apiClient.GetCodespace(ctx, codespace.Name, true) if err != nil { return nil, fmt.Errorf("error getting codespace: %w", err) } } - if startedCodespace { - fmt.Print("\n") - } - - log.Println("Connecting to your codespace...") + progress.StartProgressIndicatorWithLabel("Connecting to codespace") + defer progress.StopProgressIndicator() return liveshare.Connect(ctx, liveshare.Options{ ClientName: "gh", diff --git a/internal/codespaces/ssh.go b/internal/codespaces/ssh.go index 89ce36acf..1096014e7 100644 --- a/internal/codespaces/ssh.go +++ b/internal/codespaces/ssh.go @@ -9,17 +9,21 @@ import ( "strings" ) +type printer interface { + Printf(fmt string, v ...interface{}) +} + // Shell runs an interactive secure shell over an existing // port-forwarding session. It runs until the shell is terminated // (including by cancellation of the context). -func Shell(ctx context.Context, log logger, sshArgs []string, port int, destination string, usingCustomPort bool) error { +func Shell(ctx context.Context, p printer, sshArgs []string, port int, destination string, usingCustomPort bool) error { cmd, connArgs, err := newSSHCommand(ctx, port, destination, sshArgs) if err != nil { return fmt.Errorf("failed to create ssh command: %w", err) } if usingCustomPort { - log.Println("Connection Details: ssh " + destination + " " + strings.Join(connArgs, " ")) + p.Printf("Connection Details: ssh %s %s", destination, connArgs) } return cmd.Run() diff --git a/internal/codespaces/states.go b/internal/codespaces/states.go index b686c1888..ca5ae49e7 100644 --- a/internal/codespaces/states.go +++ b/internal/codespaces/states.go @@ -38,10 +38,10 @@ type PostCreateState struct { // PollPostCreateStates watches for state changes in a codespace, // and calls the supplied poller for each batch of state changes. // It runs until it encounters an error, including cancellation of the context. -func PollPostCreateStates(ctx context.Context, logger logger, apiClient apiClient, codespace *api.Codespace, poller func([]PostCreateState)) (err error) { +func PollPostCreateStates(ctx context.Context, progress progressIndicator, apiClient apiClient, codespace *api.Codespace, poller func([]PostCreateState)) (err error) { noopLogger := log.New(ioutil.Discard, "", 0) - session, err := ConnectToLiveshare(ctx, logger, noopLogger, apiClient, codespace) + session, err := ConnectToLiveshare(ctx, progress, noopLogger, apiClient, codespace) if err != nil { return fmt.Errorf("connect to Live Share: %w", err) } @@ -58,12 +58,14 @@ func PollPostCreateStates(ctx context.Context, logger logger, apiClient apiClien } localPort := listen.Addr().(*net.TCPAddr).Port - logger.Println("Fetching SSH Details...") + progress.StartProgressIndicatorWithLabel("Fetching SSH Details") + defer progress.StopProgressIndicator() remoteSSHServerPort, sshUser, err := session.StartSSHServer(ctx) if err != nil { return fmt.Errorf("error getting ssh server details: %w", err) } + progress.StartProgressIndicatorWithLabel("Fetching status") tunnelClosed := make(chan error, 1) // buffered to avoid sender stuckness go func() { fwd := liveshare.NewPortForwarder(session, "sshd", remoteSSHServerPort, false) @@ -73,7 +75,7 @@ func PollPostCreateStates(ctx context.Context, logger logger, apiClient apiClien t := time.NewTicker(1 * time.Second) defer t.Stop() - for { + for ticks := 0; ; ticks++ { select { case <-ctx.Done(): return ctx.Err() @@ -83,6 +85,13 @@ func PollPostCreateStates(ctx context.Context, logger logger, apiClient apiClien case <-t.C: states, err := getPostCreateOutput(ctx, localPort, sshUser) + // There is an active progress indicator before the first tick + // to show that we are fetching statuses. + // Once the first tick happens, we stop the indicator and let + // the subsequent post create states manage their own progress. + if ticks == 0 { + progress.StopProgressIndicator() + } if err != nil { return fmt.Errorf("get post create output: %w", err) } diff --git a/pkg/cmd/codespace/common.go b/pkg/cmd/codespace/common.go index 6b1c445d8..0f6f9cf4e 100644 --- a/pkg/cmd/codespace/common.go +++ b/pkg/cmd/codespace/common.go @@ -16,23 +16,37 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" "github.com/cli/cli/v2/internal/codespaces/api" - "github.com/cli/cli/v2/pkg/cmd/codespace/output" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" "golang.org/x/term" ) type App struct { + io *iostreams.IOStreams apiClient apiClient - logger *output.Logger + errLogger *log.Logger } -func NewApp(logger *output.Logger, apiClient apiClient) *App { +func NewApp(io *iostreams.IOStreams, apiClient apiClient) *App { + errLogger := log.New(io.ErrOut, "", 0) + return &App{ + io: io, apiClient: apiClient, - logger: logger, + errLogger: errLogger, } } +// StartProgressIndicatorWithLabel starts a progress indicator with a message. +func (a *App) StartProgressIndicatorWithLabel(s string) { + a.io.StartProgressIndicatorWithLabel(s) +} + +// StopProgressIndicator stops the progress indicator. +func (a *App) StopProgressIndicator() { + a.io.StopProgressIndicator() +} + //go:generate moq -fmt goimports -rm -skip-ensure -out mock_api.go . apiClient type apiClient interface { GetUser(ctx context.Context) (*api.User, error) @@ -138,6 +152,7 @@ func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) ( // getOrChooseCodespace prompts the user to choose a codespace if the codespaceName is empty. // It then fetches the codespace record with full connection details. +// TODO(josebalius): accept a progress indicator or *App and show progress when fetching. func getOrChooseCodespace(ctx context.Context, apiClient apiClient, codespaceName string) (codespace *api.Codespace, err error) { if codespaceName == "" { codespace, err = chooseCodespace(ctx, apiClient) diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index 1858c1552..e0a4de7a4 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -4,12 +4,10 @@ import ( "context" "errors" "fmt" - "os" "github.com/AlecAivazis/survey/v2" "github.com/cli/cli/v2/internal/codespaces" "github.com/cli/cli/v2/internal/codespaces/api" - "github.com/cli/cli/v2/pkg/cmd/codespace/output" "github.com/spf13/cobra" ) @@ -54,7 +52,9 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { return fmt.Errorf("error getting branch name: %w", err) } + a.StartProgressIndicatorWithLabel("Fetching repository") repository, err := a.apiClient.GetRepository(ctx, repo) + a.StopProgressIndicator() if err != nil { return fmt.Errorf("error getting repository: %w", err) } @@ -77,37 +77,36 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { return errors.New("there are no available machine types for this repository") } - a.logger.Print("Creating your codespace...") + a.StartProgressIndicatorWithLabel("Creating codespace") codespace, err := a.apiClient.CreateCodespace(ctx, &api.CreateCodespaceParams{ RepositoryID: repository.ID, Branch: branch, Machine: machine, Location: locationResult.Location, }) - a.logger.Print("\n") + a.StopProgressIndicator() if err != nil { return fmt.Errorf("error creating codespace: %w", err) } if opts.showStatus { - if err := showStatus(ctx, a.logger, a.apiClient, userResult.User, codespace); err != nil { + if err := a.showStatus(ctx, userResult.User, codespace); err != nil { return fmt.Errorf("show status: %w", err) } } - a.logger.Printf("Codespace created: ") - - fmt.Fprintln(os.Stdout, codespace.Name) - + fmt.Fprintln(a.io.Out, codespace.Name) return nil } // showStatus polls the codespace for a list of post create states and their status. It will keep polling // until all states have finished. Once all states have finished, we poll once more to check if any new // states have been introduced and stop polling otherwise. -func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, user *api.User, codespace *api.Codespace) error { - var lastState codespaces.PostCreateState - var breakNextState bool +func (a *App) showStatus(ctx context.Context, user *api.User, codespace *api.Codespace) error { + var ( + lastState codespaces.PostCreateState + breakNextState bool + ) finishedStates := make(map[string]bool) ctx, stopPolling := context.WithCancel(ctx) @@ -121,26 +120,24 @@ func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, us } if state.Name != lastState.Name { - log.Print(state.Name) + a.StartProgressIndicatorWithLabel(state.Name) if state.Status == codespaces.PostCreateStateRunning { inProgress = true lastState = state - log.Print("...") break } finishedStates[state.Name] = true - log.Println("..." + state.Status) + a.StopProgressIndicator() } else { if state.Status == codespaces.PostCreateStateRunning { inProgress = true - log.Print(".") break } finishedStates[state.Name] = true - log.Println(state.Status) + a.StopProgressIndicator() lastState = codespaces.PostCreateState{} // reset the value } } @@ -154,7 +151,7 @@ func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, us } } - err := codespaces.PollPostCreateStates(ctx, log, apiClient, codespace, poller) + err := codespaces.PollPostCreateStates(ctx, a, a.apiClient, codespace, poller) if err != nil { if errors.Is(err, context.Canceled) && breakNextState { return nil // we cancelled the context to stop polling, we can ignore the error diff --git a/pkg/cmd/codespace/delete.go b/pkg/cmd/codespace/delete.go index 23d2abc08..941475f24 100644 --- a/pkg/cmd/codespace/delete.go +++ b/pkg/cmd/codespace/delete.go @@ -62,7 +62,9 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) { var codespaces []*api.Codespace nameFilter := opts.codespaceName if nameFilter == "" { + a.StartProgressIndicatorWithLabel("Fetching codespaces") codespaces, err = a.apiClient.ListCodespaces(ctx, -1) + a.StopProgressIndicator() if err != nil { return fmt.Errorf("error getting codespaces: %w", err) } @@ -75,7 +77,9 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) { nameFilter = c.Name } } else { + a.StartProgressIndicatorWithLabel("Fetching codespace") codespace, err := a.apiClient.GetCodespace(ctx, nameFilter, false) + a.StopProgressIndicator() if err != nil { return fmt.Errorf("error fetching codespace information: %w", err) } @@ -117,12 +121,19 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) { return errors.New("no codespaces to delete") } - g := errgroup.Group{} + progressLabel := "Deleting codespace" + if len(codespacesToDelete) > 1 { + progressLabel = "Deleting codespaces" + } + a.StartProgressIndicatorWithLabel(progressLabel) + defer a.StopProgressIndicator() + + var g errgroup.Group for _, c := range codespacesToDelete { codespaceName := c.Name g.Go(func() error { if err := a.apiClient.DeleteCodespace(ctx, codespaceName); err != nil { - _, _ = a.logger.Errorf("error deleting codespace %q: %v\n", codespaceName, err) + a.errLogger.Printf("error deleting codespace %q: %v\n", codespaceName, err) return err } return nil @@ -132,13 +143,6 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) { if err := g.Wait(); err != nil { return errors.New("some codespaces failed to delete") } - - noun := "Codespace" - if len(codespacesToDelete) > 1 { - noun = noun + "s" - } - a.logger.Println(noun + " deleted.") - return nil } diff --git a/pkg/cmd/codespace/delete_test.go b/pkg/cmd/codespace/delete_test.go index 8ffdc3fee..58090c809 100644 --- a/pkg/cmd/codespace/delete_test.go +++ b/pkg/cmd/codespace/delete_test.go @@ -1,7 +1,6 @@ package codespace import ( - "bytes" "context" "errors" "fmt" @@ -12,7 +11,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/codespaces/api" - "github.com/cli/cli/v2/pkg/cmd/codespace/output" + "github.com/cli/cli/v2/pkg/iostreams" ) func TestDelete(t *testing.T) { @@ -44,7 +43,7 @@ func TestDelete(t *testing.T) { }, }, wantDeleted: []string{"hubot-robawt-abc"}, - wantStdout: "Codespace deleted.\n", + wantStdout: "", }, { name: "by repo", @@ -72,7 +71,7 @@ func TestDelete(t *testing.T) { }, }, wantDeleted: []string{"monalisa-spoonknife-123", "monalisa-spoonknife-c4f3"}, - wantStdout: "Codespaces deleted.\n", + wantStdout: "", }, { name: "unused", @@ -95,7 +94,7 @@ func TestDelete(t *testing.T) { }, }, wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"}, - wantStdout: "Codespaces deleted.\n", + wantStdout: "", }, { name: "deletion failed", @@ -151,7 +150,7 @@ func TestDelete(t *testing.T) { "Codespace hubot-robawt-abc has unsaved changes. OK to delete?": true, }, wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"}, - wantStdout: "Codespaces deleted.\n", + wantStdout: "", }, } for _, tt := range tests { @@ -188,12 +187,10 @@ func TestDelete(t *testing.T) { }, } - stdout := &bytes.Buffer{} - stderr := &bytes.Buffer{} - app := &App{ - apiClient: apiMock, - logger: output.NewLogger(stdout, stderr, false), - } + io, _, stdout, stderr := iostreams.Test() + io.SetStdinTTY(true) + io.SetStdoutTTY(true) + app := NewApp(io, apiMock) err := app.Delete(context.Background(), opts) if (err != nil) != tt.wantErr { t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr) diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index e130c9ed7..dab3f87d5 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -3,7 +3,6 @@ package codespace import ( "context" "fmt" - "os" "github.com/cli/cli/v2/pkg/cmd/codespace/output" "github.com/cli/cli/v2/pkg/cmdutil" @@ -34,12 +33,14 @@ func newListCmd(app *App) *cobra.Command { } func (a *App) List(ctx context.Context, asJSON bool, limit int) error { + a.StartProgressIndicatorWithLabel("Fetching codespaces") codespaces, err := a.apiClient.ListCodespaces(ctx, limit) + a.StopProgressIndicator() if err != nil { return fmt.Errorf("error getting codespaces: %w", err) } - table := output.NewTable(os.Stdout, asJSON) + table := output.NewTable(a.io.Out, asJSON) table.SetHeader([]string{"Name", "Repository", "Branch", "State", "Created At"}) for _, apiCodespace := range codespaces { cs := codespace{apiCodespace} diff --git a/pkg/cmd/codespace/logs.go b/pkg/cmd/codespace/logs.go index 1b6e57a84..c42ef42d7 100644 --- a/pkg/cmd/codespace/logs.go +++ b/pkg/cmd/codespace/logs.go @@ -36,6 +36,11 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err ctx, cancel := context.WithCancel(ctx) defer cancel() + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + if err != nil { + return fmt.Errorf("get or choose codespace: %w", err) + } + user, err := a.apiClient.GetUser(ctx) if err != nil { return fmt.Errorf("getting user: %w", err) @@ -46,12 +51,7 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err authkeys <- checkAuthorizedKeys(ctx, a.apiClient, user.Login) }() - codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) - if err != nil { - return fmt.Errorf("get or choose codespace: %w", err) - } - - session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace) + session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace) if err != nil { return fmt.Errorf("connecting to Live Share: %w", err) } @@ -69,8 +69,9 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err defer listen.Close() localPort := listen.Addr().(*net.TCPAddr).Port - a.logger.Println("Fetching SSH Details...") + a.StartProgressIndicatorWithLabel("Fetching SSH Details") remoteSSHServerPort, sshUser, err := session.StartSSHServer(ctx) + a.StopProgressIndicator() if err != nil { return fmt.Errorf("error getting ssh server details: %w", err) } diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index a563ec87e..f3e380451 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "net" - "os" "strconv" "strings" @@ -59,14 +58,15 @@ func (a *App) ListPorts(ctx context.Context, codespaceName string, asJSON bool) devContainerCh := getDevContainer(ctx, a.apiClient, codespace) - session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace) + session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace) if err != nil { return fmt.Errorf("error connecting to Live Share: %w", err) } defer safeClose(session, &err) - a.logger.Println("Loading ports...") + a.StartProgressIndicatorWithLabel("Fetching ports") ports, err := session.GetSharedServers(ctx) + a.StopProgressIndicator() if err != nil { return fmt.Errorf("error getting ports of shared servers: %w", err) } @@ -74,10 +74,10 @@ func (a *App) ListPorts(ctx context.Context, codespaceName string, asJSON bool) devContainerResult := <-devContainerCh if devContainerResult.err != nil { // Warn about failure to read the devcontainer file. Not a codespace command error. - _, _ = a.logger.Errorf("Failed to get port names: %v\n", devContainerResult.err.Error()) + a.errLogger.Printf("Failed to get port names: %v\n", devContainerResult.err.Error()) } - table := output.NewTable(os.Stdout, asJSON) + table := output.NewTable(a.io.Out, asJSON) table.SetHeader([]string{"Label", "Port", "Visibility", "Browse URL"}) for _, port := range ports { sourcePort := strconv.Itoa(port.SourcePort) @@ -168,6 +168,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar if err != nil { return fmt.Errorf("error parsing port arguments: %w", err) } + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) if err != nil { if err == errNoCodespaces { @@ -176,18 +177,20 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar return fmt.Errorf("error getting codespace: %w", err) } - session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace) + session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace) if err != nil { return fmt.Errorf("error connecting to Live Share: %w", err) } defer safeClose(session, &err) + // TODO: check if port visibility can be updated in parallel instead of sequentially for _, port := range ports { - if err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility); err != nil { + a.StartProgressIndicatorWithLabel(fmt.Sprintf("Updating port %d visibility to: %s", port.number, port.visibility)) + err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility) + a.StopProgressIndicator() + if err != nil { return fmt.Errorf("error update port to public: %w", err) } - - a.logger.Printf("Port %d is now %s scoped.\n", port.number, port.visibility) } return nil @@ -250,7 +253,7 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st return fmt.Errorf("error getting codespace: %w", err) } - session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace) + session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace) if err != nil { return fmt.Errorf("error connecting to Live Share: %w", err) } @@ -267,7 +270,8 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st return err } defer listen.Close() - a.logger.Printf("Forwarding ports: remote %d <=> local %d\n", pair.remote, pair.local) + + a.errLogger.Printf("Forwarding ports: remote %d <=> local %d", pair.remote, pair.local) name := fmt.Sprintf("share-%d", pair.remote) fwd := liveshare.NewPortForwarder(session, name, pair.remote, false) return fwd.ForwardToListener(ctx, listen) // error always non-nil diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index a0f596e49..44da6713a 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -53,6 +53,13 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e ctx, cancel := context.WithCancel(ctx) defer cancel() + codespace, err := getOrChooseCodespace(ctx, a.apiClient, opts.codespace) + if err != nil { + return fmt.Errorf("get or choose codespace: %w", err) + } + + // TODO(josebalius): We can fetch the user in parallel to everything else + // we should convert this call and others to happen async user, err := a.apiClient.GetUser(ctx) if err != nil { return fmt.Errorf("error getting user: %w", err) @@ -63,11 +70,6 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e authkeys <- checkAuthorizedKeys(ctx, a.apiClient, user.Login) }() - codespace, err := getOrChooseCodespace(ctx, a.apiClient, opts.codespace) - if err != nil { - return fmt.Errorf("get or choose codespace: %w", err) - } - liveshareLogger := noopLogger() if opts.debug { debugLogger, err := newFileLogger(opts.debugFile) @@ -77,10 +79,10 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e defer safeClose(debugLogger, &err) liveshareLogger = debugLogger.Logger - a.logger.Println("Debug file located at: " + debugLogger.Name()) + a.errLogger.Printf("Debug file located at: %s", debugLogger.Name()) } - session, err := codespaces.ConnectToLiveshare(ctx, a.logger, liveshareLogger, a.apiClient, codespace) + session, err := codespaces.ConnectToLiveshare(ctx, a, liveshareLogger, a.apiClient, codespace) if err != nil { return fmt.Errorf("error connecting to Live Share: %w", err) } @@ -90,8 +92,9 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e return err } - a.logger.Println("Fetching SSH Details...") + a.StartProgressIndicatorWithLabel("Fetching SSH Details") remoteSSHServerPort, sshUser, err := session.StartSSHServer(ctx) + a.StopProgressIndicator() if err != nil { return fmt.Errorf("error getting ssh server details: %w", err) } @@ -114,7 +117,6 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e connectDestination = fmt.Sprintf("%s@localhost", sshUser) } - a.logger.Println("Ready...") tunnelClosed := make(chan error, 1) go func() { fwd := liveshare.NewPortForwarder(session, "sshd", remoteSSHServerPort, true) @@ -127,7 +129,7 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e if opts.scpArgs != nil { err = codespaces.Copy(ctx, opts.scpArgs, localSSHServerPort, connectDestination) } else { - err = codespaces.Shell(ctx, a.logger, sshArgs, localSSHServerPort, connectDestination, usingCustomPort) + err = codespaces.Shell(ctx, a.errLogger, sshArgs, localSSHServerPort, connectDestination, usingCustomPort) } shellClosed <- err }() diff --git a/pkg/cmd/codespace/stop.go b/pkg/cmd/codespace/stop.go index be439b1e8..6b2268fad 100644 --- a/pkg/cmd/codespace/stop.go +++ b/pkg/cmd/codespace/stop.go @@ -27,7 +27,9 @@ func newStopCmd(app *App) *cobra.Command { func (a *App) StopCodespace(ctx context.Context, codespaceName string) error { if codespaceName == "" { + a.StartProgressIndicatorWithLabel("Fetching codespaces") codespaces, err := a.apiClient.ListCodespaces(ctx, -1) + a.StopProgressIndicator() if err != nil { return fmt.Errorf("failed to list codespaces: %w", err) } @@ -49,7 +51,9 @@ func (a *App) StopCodespace(ctx context.Context, codespaceName string) error { } codespaceName = codespace.Name } else { + a.StartProgressIndicatorWithLabel("Fetching codespace") c, err := a.apiClient.GetCodespace(ctx, codespaceName, false) + a.StopProgressIndicator() if err != nil { return fmt.Errorf("failed to get codespace: %q: %w", codespaceName, err) } @@ -59,10 +63,11 @@ func (a *App) StopCodespace(ctx context.Context, codespaceName string) error { } } + a.StartProgressIndicatorWithLabel("Stopping codespace") + defer a.StopProgressIndicator() if err := a.apiClient.StopCodespace(ctx, codespaceName); err != nil { return fmt.Errorf("failed to stop codespace: %w", err) } - a.logger.Println("Codespace stopped") return nil } diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 40d2d4aad..316f4f156 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -122,6 +122,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err return nil, nil, err } + // TODO(josebalius): Should we be guarding here? if f.progress != nil { f.progress.StartProgressIndicator() defer f.progress.StopProgressIndicator() diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index d03aa6ce8..3cb079fac 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -12,7 +12,6 @@ import ( authCmd "github.com/cli/cli/v2/pkg/cmd/auth" browseCmd "github.com/cli/cli/v2/pkg/cmd/browse" codespaceCmd "github.com/cli/cli/v2/pkg/cmd/codespace" - "github.com/cli/cli/v2/pkg/cmd/codespace/output" completionCmd "github.com/cli/cli/v2/pkg/cmd/completion" configCmd "github.com/cli/cli/v2/pkg/cmd/config" extensionCmd "github.com/cli/cli/v2/pkg/cmd/extension" @@ -130,10 +129,11 @@ func bareHTTPClient(f *cmdutil.Factory, version string) func() (*http.Client, er } func newCodespaceCmd(f *cmdutil.Factory) *cobra.Command { - cmd := codespaceCmd.NewRootCmd(codespaceCmd.NewApp( - output.NewLogger(f.IOStreams.Out, f.IOStreams.ErrOut, !f.IOStreams.IsStdoutTTY()), + app := codespaceCmd.NewApp( + f.IOStreams, codespacesAPI.New("", &lazyLoadedHTTPClient{factory: f}), - )) + ) + cmd := codespaceCmd.NewRootCmd(app) cmd.Use = "codespace" cmd.Aliases = []string{"cs"} cmd.Hidden = true diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index 166f45860..e07111450 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -10,6 +10,7 @@ import ( "os/exec" "strconv" "strings" + "sync" "time" "github.com/briandowns/spinner" @@ -37,6 +38,7 @@ type IOStreams struct { progressIndicatorEnabled bool progressIndicator *spinner.Spinner + progressIndicatorMu sync.Mutex stdinTTYOverride bool stdinIsTTY bool @@ -229,15 +231,40 @@ func (s *IOStreams) SetNeverPrompt(v bool) { } func (s *IOStreams) StartProgressIndicator() { + s.StartProgressIndicatorWithLabel("") +} + +func (s *IOStreams) StartProgressIndicatorWithLabel(label string) { if !s.progressIndicatorEnabled { return } - sp := spinner.New(spinner.CharSets[11], 400*time.Millisecond, spinner.WithWriter(s.ErrOut)) + + s.progressIndicatorMu.Lock() + defer s.progressIndicatorMu.Unlock() + + if s.progressIndicator != nil { + if label == "" { + s.progressIndicator.Prefix = "" + } else { + s.progressIndicator.Prefix = label + " " + } + return + } + + // https://github.com/briandowns/spinner#available-character-sets + dotStyle := spinner.CharSets[11] + sp := spinner.New(dotStyle, 120*time.Millisecond, spinner.WithWriter(s.ErrOut), spinner.WithColor("fgCyan")) + if label != "" { + sp.Prefix = label + " " + } + sp.Start() s.progressIndicator = sp } func (s *IOStreams) StopProgressIndicator() { + s.progressIndicatorMu.Lock() + defer s.progressIndicatorMu.Unlock() if s.progressIndicator == nil { return } From 67595110e862630904789fe7b259ff95e0e99bdf Mon Sep 17 00:00:00 2001 From: Mateusz Urbanek Date: Thu, 21 Oct 2021 17:42:08 +0100 Subject: [PATCH 08/22] Improve table output from codespace list command (#4516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jose Garcia Co-authored-by: Mislav Marohnić --- internal/codespaces/api/api.go | 4 +++ pkg/cmd/codespace/list.go | 60 ++++++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index bf3f204ee..505e292ae 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -162,6 +162,10 @@ type CodespaceGitStatus struct { const ( // CodespaceStateAvailable is the state for a running codespace environment. CodespaceStateAvailable = "Available" + // CodespaceStateShutdown is the state for a shutdown codespace environment. + CodespaceStateShutdown = "Shutdown" + // CodespaceStateStarting is the state for a starting codespace environment. + CodespaceStateStarting = "Starting" ) type CodespaceConnection struct { diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index dab3f87d5..0e5bf9242 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -3,9 +3,11 @@ package codespace import ( "context" "fmt" + "time" - "github.com/cli/cli/v2/pkg/cmd/codespace/output" + "github.com/cli/cli/v2/internal/codespaces/api" "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/utils" "github.com/spf13/cobra" ) @@ -40,19 +42,49 @@ func (a *App) List(ctx context.Context, asJSON bool, limit int) error { return fmt.Errorf("error getting codespaces: %w", err) } - table := output.NewTable(a.io.Out, asJSON) - table.SetHeader([]string{"Name", "Repository", "Branch", "State", "Created At"}) - for _, apiCodespace := range codespaces { - cs := codespace{apiCodespace} - table.Append([]string{ - cs.Name, - cs.Repository.FullName, - cs.branchWithGitStatus(), - cs.State, - cs.CreatedAt, - }) + if err := a.io.StartPager(); err != nil { + a.errLogger.Printf("error starting pager: %v", err) + } + defer a.io.StopPager() + + tp := utils.NewTablePrinter(a.io) + if tp.IsTTY() { + tp.AddField("NAME", nil, nil) + tp.AddField("REPOSITORY", nil, nil) + tp.AddField("BRANCH", nil, nil) + tp.AddField("STATE", nil, nil) + tp.AddField("CREATED AT", nil, nil) + tp.EndRow() } - table.Render() - return nil + cs := a.io.ColorScheme() + for _, apiCodespace := range codespaces { + c := codespace{apiCodespace} + + var stateColor func(string) string + switch c.State { + case api.CodespaceStateStarting: + stateColor = cs.Yellow + case api.CodespaceStateAvailable: + stateColor = cs.Green + } + + tp.AddField(c.Name, nil, cs.Yellow) + tp.AddField(c.Repository.FullName, nil, nil) + tp.AddField(c.branchWithGitStatus(), nil, cs.Cyan) + tp.AddField(c.State, nil, stateColor) + + if tp.IsTTY() { + ct, err := time.Parse(time.RFC3339, c.CreatedAt) + if err != nil { + return fmt.Errorf("error parsing date %q: %w", c.CreatedAt, err) + } + tp.AddField(utils.FuzzyAgoAbbr(time.Now(), ct), nil, cs.Gray) + } else { + tp.AddField(c.CreatedAt, nil, nil) + } + tp.EndRow() + } + + return tp.Render() } From f65c1537dc37e742e378a9d7155bd316315f8794 Mon Sep 17 00:00:00 2001 From: nate smith Date: Thu, 21 Oct 2021 11:58:25 -0500 Subject: [PATCH 09/22] review feedback --- pkg/cmd/extension/manager.go | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 7110254a4..f06594d05 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -83,7 +83,7 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri var externalCmd *exec.Cmd - if ext.IsBinary() { + if ext.IsBinary() || runtime.GOOS != "windows" { externalCmd = m.newCommand(exe, forwardArgs...) } else if runtime.GOOS == "windows" { // Dispatch all extension calls through the `sh` interpreter to support executable files with a @@ -97,8 +97,6 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri } forwardArgs = append([]string{"-c", `command "$@"`, "--", exe}, forwardArgs...) externalCmd = m.newCommand(shExe, forwardArgs...) - } else { - externalCmd = m.newCommand(exe, forwardArgs...) } externalCmd.Stdin = stdin externalCmd.Stdout = stdout @@ -272,7 +270,17 @@ func (m *Manager) getLatestVersion(ext Extension) (string, error) { if ext.isLocal { return "", fmt.Errorf("unable to get latest version for local extensions") } - if ext.kind == GitKind { + if ext.IsBinary() { + repo, err := ghrepo.FromFullName(ext.url) + if err != nil { + return "", err + } + r, err := fetchLatestRelease(m.client, repo) + if err != nil { + return "", err + } + return r.Tag, nil + } else { gitExe, err := m.lookPath("git") if err != nil { return "", err @@ -286,16 +294,6 @@ func (m *Manager) getLatestVersion(ext Extension) (string, error) { } remoteSha := bytes.SplitN(lsRemote, []byte("\t"), 2)[0] return string(remoteSha), nil - } else { - repo, err := ghrepo.FromFullName(ext.url) - if err != nil { - return "", err - } - r, err := fetchLatestRelease(m.client, repo) - if err != nil { - return "", err - } - return r.Tag, nil } } @@ -481,7 +479,7 @@ func (m *Manager) upgradeExtension(ext Extension, force bool) error { return upToDateError } var err error - if ext.kind == BinaryKind { + if ext.IsBinary() { err = m.upgradeBinExtension(ext) } else { err = m.upgradeGitExtension(ext, force) From e9dafd7c32631ebd821fa484039d1d43a60e828b Mon Sep 17 00:00:00 2001 From: dimas Date: Fri, 22 Oct 2021 01:25:23 +0700 Subject: [PATCH 10/22] fixing numbering in releasing docs for consistency. --- docs/releasing.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index f17902c7b..e762d845e 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -31,6 +31,6 @@ If the build fails, there is not a clean way to re-run it. The easiest way would A local release can be created for testing without creating anything official on the release page. -0. Make sure GoReleaser is installed: `brew install goreleaser` -1. `goreleaser --skip-validate --skip-publish --rm-dist` -2. Find the built products under `dist/`. +1. Make sure GoReleaser is installed: `brew install goreleaser` +2. `goreleaser --skip-validate --skip-publish --rm-dist` +3. Find the built products under `dist/`. From 0007dce7f7aef297e955a9e196627f5032910448 Mon Sep 17 00:00:00 2001 From: dimas Date: Fri, 22 Oct 2021 01:29:53 +0700 Subject: [PATCH 11/22] fixing numbering in project-layout and source docs for consistency. --- docs/project-layout.md | 8 ++++---- docs/source.md | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/project-layout.md b/docs/project-layout.md index 60d0c2aac..89337596a 100644 --- a/docs/project-layout.md +++ b/docs/project-layout.md @@ -60,14 +60,14 @@ and talk through which code gets run in order. ## How to add a new command -0. First, check on our issue tracker to verify that our team had approved the plans for a new command. -1. Create a package for the new command, e.g. for a new command `gh boom` create the following directory +1. First, check on our issue tracker to verify that our team had approved the plans for a new command. +2. Create a package for the new command, e.g. for a new command `gh boom` create the following directory structure: `pkg/cmd/boom/` -2. The new package should expose a method, e.g. `NewCmdBoom()`, that accepts a `*cmdutil.Factory` type and +3. The new package should expose a method, e.g. `NewCmdBoom()`, that accepts a `*cmdutil.Factory` type and returns a `*cobra.Command`. * Any logic specific to this command should be kept within the command's package and not added to any "global" packages like `api` or `utils`. -3. Use the method from the previous step to generate the command and add it to the command tree, typically +4. Use the method from the previous step to generate the command and add it to the command tree, typically somewhere in the `NewCmdRoot()` method. ## How to write tests diff --git a/docs/source.md b/docs/source.md index a8225c371..485c7671c 100644 --- a/docs/source.md +++ b/docs/source.md @@ -1,6 +1,6 @@ # Installation from source -0. Verify that you have Go 1.16+ installed +1. Verify that you have Go 1.16+ installed ```sh $ go version @@ -8,14 +8,14 @@ If `go` is not installed, follow instructions on [the Go website](https://golang.org/doc/install). -1. Clone this repository +2. Clone this repository ```sh $ git clone https://github.com/cli/cli.git gh-cli $ cd gh-cli ``` -2. Build and install +3. Build and install #### Unix-like systems ```sh @@ -33,7 +33,7 @@ ``` There is no install step available on Windows. -3. Run `gh version` to check if it worked. +4. Run `gh version` to check if it worked. #### Windows Run `bin\gh version` to check if it worked. From badbf515cb19079f5c7b834dba08691b66bb3981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 22 Oct 2021 14:13:15 +0200 Subject: [PATCH 12/22] codespace list: support `--json` and `--template` export flags --- internal/codespaces/api/api.go | 38 ++++++++++++++++++++++++++++++++++ pkg/cmd/codespace/list.go | 12 +++++++---- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 505e292ae..f509e2661 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -35,6 +35,7 @@ import ( "io/ioutil" "net/http" "net/url" + "reflect" "regexp" "strconv" "strings" @@ -176,6 +177,43 @@ type CodespaceConnection struct { HostPublicKeys []string `json:"hostPublicKeys"` } +var CodespaceFields = []string{ + "name", + "owner", + "repository", + "state", + "gitStatus", + "createdAt", + "lastUsedAt", +} + +func (c *Codespace) ExportData(fields []string) *map[string]interface{} { + v := reflect.ValueOf(c).Elem() + data := map[string]interface{}{} + + for _, f := range fields { + switch f { + case "owner": + data[f] = c.Owner.Login + case "repository": + data[f] = c.Repository.FullName + case "gitStatus": + data[f] = map[string]interface{}{ + "ref": c.GitStatus.Ref, + "hasUnpushedChanges": c.GitStatus.HasUnpushedChanges, + "hasUncommitedChanges": c.GitStatus.HasUncommitedChanges, + } + default: + sf := v.FieldByNameFunc(func(s string) bool { + return strings.EqualFold(f, s) + }) + data[f] = sf.Interface() + } + } + + return &data +} + // ListCodespaces returns a list of codespaces for the user. Pass a negative limit to request all pages from // the API until all codespaces have been fetched. func (a *API) ListCodespaces(ctx context.Context, limit int) (codespaces []*Codespace, err error) { diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index ba573a226..7fe71d6fa 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -12,8 +12,8 @@ import ( ) func newListCmd(app *App) *cobra.Command { - var asJSON bool var limit int + var exporter cmdutil.Exporter listCmd := &cobra.Command{ Use: "list", @@ -24,17 +24,17 @@ func newListCmd(app *App) *cobra.Command { return cmdutil.FlagErrorf("invalid limit: %v", limit) } - return app.List(cmd.Context(), asJSON, limit) + return app.List(cmd.Context(), limit, exporter) }, } - listCmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON") listCmd.Flags().IntVarP(&limit, "limit", "L", 30, "Maximum number of codespaces to list") + cmdutil.AddJSONFlags(listCmd, &exporter, api.CodespaceFields) return listCmd } -func (a *App) List(ctx context.Context, asJSON bool, limit int) error { +func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) error { a.StartProgressIndicatorWithLabel("Fetching codespaces") codespaces, err := a.apiClient.ListCodespaces(ctx, limit) a.StopProgressIndicator() @@ -47,6 +47,10 @@ func (a *App) List(ctx context.Context, asJSON bool, limit int) error { } defer a.io.StopPager() + if exporter != nil { + return exporter.Write(a.io, codespaces) + } + tp := utils.NewTablePrinter(a.io) if tp.IsTTY() { tp.AddField("NAME", nil, nil) From cbeb675e56ed1368cdb0d6890bba8bd57214a5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 22 Oct 2021 14:13:43 +0200 Subject: [PATCH 13/22] codespace ports: support `--json` and `--template` export flags --- pkg/cmd/codespace/ports.go | 118 ++++++++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 27 deletions(-) diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index f3e380451..eba8e6fc8 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -12,8 +12,9 @@ import ( "github.com/cli/cli/v2/internal/codespaces" "github.com/cli/cli/v2/internal/codespaces/api" - "github.com/cli/cli/v2/pkg/cmd/codespace/output" + "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/liveshare" + "github.com/cli/cli/v2/utils" "github.com/muhammadmuzzammil1998/jsonc" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" @@ -22,22 +23,20 @@ import ( // newPortsCmd returns a Cobra "ports" command that displays a table of available ports, // according to the specified flags. func newPortsCmd(app *App) *cobra.Command { - var ( - codespace string - asJSON bool - ) + var codespace string + var exporter cmdutil.Exporter portsCmd := &cobra.Command{ Use: "ports", Short: "List ports in a codespace", Args: noArgsConstraint, RunE: func(cmd *cobra.Command, args []string) error { - return app.ListPorts(cmd.Context(), codespace, asJSON) + return app.ListPorts(cmd.Context(), codespace, exporter) }, } portsCmd.PersistentFlags().StringVarP(&codespace, "codespace", "c", "", "Name of the codespace") - portsCmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON") + cmdutil.AddJSONFlags(portsCmd, &exporter, portFields) portsCmd.AddCommand(newPortsForwardCmd(app)) portsCmd.AddCommand(newPortsVisibilityCmd(app)) @@ -46,7 +45,7 @@ func newPortsCmd(app *App) *cobra.Command { } // ListPorts lists known ports in a codespace. -func (a *App) ListPorts(ctx context.Context, codespaceName string, asJSON bool) (err error) { +func (a *App) ListPorts(ctx context.Context, codespaceName string, exporter cmdutil.Exporter) (err error) { codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) if err != nil { // TODO(josebalius): remove special handling of this error here and it other places @@ -74,30 +73,95 @@ func (a *App) ListPorts(ctx context.Context, codespaceName string, asJSON bool) devContainerResult := <-devContainerCh if devContainerResult.err != nil { // Warn about failure to read the devcontainer file. Not a codespace command error. - a.errLogger.Printf("Failed to get port names: %v\n", devContainerResult.err.Error()) + a.errLogger.Printf("Failed to get port names: %v", devContainerResult.err.Error()) } - table := output.NewTable(a.io.Out, asJSON) - table.SetHeader([]string{"Label", "Port", "Visibility", "Browse URL"}) - for _, port := range ports { - sourcePort := strconv.Itoa(port.SourcePort) - var portName string - if devContainerResult.devContainer != nil { - if attributes, ok := devContainerResult.devContainer.PortAttributes[sourcePort]; ok { - portName = attributes.Label - } + portInfos := make([]*portInfo, len(ports)) + for i, p := range ports { + portInfos[i] = &portInfo{ + Port: p, + codespace: codespace, + devContainer: devContainerResult.devContainer, } - - table.Append([]string{ - portName, - sourcePort, - port.Privacy, - fmt.Sprintf("https://%s-%s.githubpreview.dev/", codespace.Name, sourcePort), - }) } - table.Render() - return nil + if err := a.io.StartPager(); err != nil { + a.errLogger.Printf("error starting pager: %v", err) + } + defer a.io.StopPager() + + if exporter != nil { + return exporter.Write(a.io, portInfos) + } + + cs := a.io.ColorScheme() + tp := utils.NewTablePrinter(a.io) + + if tp.IsTTY() { + tp.AddField("LABEL", nil, nil) + tp.AddField("PORT", nil, nil) + tp.AddField("VISIBILITY", nil, nil) + tp.AddField("BROWSE URL", nil, nil) + tp.EndRow() + } + + for _, port := range portInfos { + tp.AddField(port.Label(), nil, nil) + tp.AddField(strconv.Itoa(port.SourcePort), nil, cs.Yellow) + tp.AddField(port.Privacy, nil, nil) + tp.AddField(port.BrowseURL(), nil, nil) + tp.EndRow() + } + return tp.Render() +} + +type portInfo struct { + *liveshare.Port + codespace *api.Codespace + devContainer *devContainer +} + +func (pi *portInfo) BrowseURL() string { + return fmt.Sprintf("https://%s-%d.githubpreview.dev", pi.codespace.Name, pi.Port.SourcePort) +} + +func (pi *portInfo) Label() string { + if pi.devContainer != nil { + portStr := strconv.Itoa(pi.Port.SourcePort) + if attributes, ok := pi.devContainer.PortAttributes[portStr]; ok { + return attributes.Label + } + } + return "" +} + +var portFields = []string{ + "sourcePort", + // "destinationPort", // TODO(mislav): this appears to always be blank? + "visibility", + "label", + "browseUrl", +} + +func (pi *portInfo) ExportData(fields []string) *map[string]interface{} { + data := map[string]interface{}{} + + for _, f := range fields { + switch f { + case "sourcePort": + data[f] = pi.Port.SourcePort + case "destinationPort": + data[f] = pi.Port.DestinationPort + case "visibility": + data[f] = pi.Port.Privacy + case "label": + data[f] = pi.Label() + case "browseUrl": + data[f] = pi.BrowseURL() + } + } + + return &data } type devContainerResult struct { From a5fa70a07dc88abbb8df4b1d5ce9413bbf6b8f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 22 Oct 2021 14:15:09 +0200 Subject: [PATCH 14/22] :fire: obsolete `codespace/output` package --- go.mod | 1 - pkg/cmd/codespace/output/format_json.go | 55 ----------------- pkg/cmd/codespace/output/format_table.go | 31 ---------- pkg/cmd/codespace/output/format_tsv.go | 25 -------- pkg/cmd/codespace/output/logger.go | 78 ------------------------ 5 files changed, 190 deletions(-) delete mode 100644 pkg/cmd/codespace/output/format_json.go delete mode 100644 pkg/cmd/codespace/output/format_table.go delete mode 100644 pkg/cmd/codespace/output/format_tsv.go delete mode 100644 pkg/cmd/codespace/output/logger.go diff --git a/go.mod b/go.mod index c65e7341b..f28c0fe8a 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,6 @@ require ( github.com/muesli/reflow v0.2.1-0.20210502190812-c80126ec2ad5 github.com/muesli/termenv v0.9.0 github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 - github.com/olekukonko/tablewriter v0.0.5 github.com/opentracing/opentracing-go v1.1.0 github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f diff --git a/pkg/cmd/codespace/output/format_json.go b/pkg/cmd/codespace/output/format_json.go deleted file mode 100644 index 8488e8dfa..000000000 --- a/pkg/cmd/codespace/output/format_json.go +++ /dev/null @@ -1,55 +0,0 @@ -package output - -import ( - "encoding/json" - "io" - "strings" - "unicode" -) - -type jsonwriter struct { - w io.Writer - pretty bool - cols []string - data []interface{} -} - -func (j *jsonwriter) SetHeader(cols []string) { - j.cols = cols -} - -func (j *jsonwriter) Append(values []string) { - row := make(map[string]string) - for i, v := range values { - row[camelize(j.cols[i])] = v - } - j.data = append(j.data, row) -} - -func (j *jsonwriter) Render() { - enc := json.NewEncoder(j.w) - if j.pretty { - enc.SetIndent("", " ") - } - _ = enc.Encode(j.data) -} - -func camelize(s string) string { - var b strings.Builder - capitalizeNext := false - for i, r := range s { - if r == ' ' { - capitalizeNext = true - continue - } - if capitalizeNext { - b.WriteRune(unicode.ToUpper(r)) - capitalizeNext = false - } else if i == 0 { - b.WriteRune(unicode.ToLower(r)) - } else { - b.WriteRune(r) - } - } - return b.String() -} diff --git a/pkg/cmd/codespace/output/format_table.go b/pkg/cmd/codespace/output/format_table.go deleted file mode 100644 index e0345672d..000000000 --- a/pkg/cmd/codespace/output/format_table.go +++ /dev/null @@ -1,31 +0,0 @@ -package output - -import ( - "io" - "os" - - "github.com/olekukonko/tablewriter" - "golang.org/x/term" -) - -type Table interface { - SetHeader([]string) - Append([]string) - Render() -} - -func NewTable(w io.Writer, asJSON bool) Table { - isTTY := isTTY(w) - if asJSON { - return &jsonwriter{w: w, pretty: isTTY} - } - if isTTY { - return tablewriter.NewWriter(w) - } - return &tabwriter{w: w} -} - -func isTTY(w io.Writer) bool { - f, ok := w.(*os.File) - return ok && term.IsTerminal(int(f.Fd())) -} diff --git a/pkg/cmd/codespace/output/format_tsv.go b/pkg/cmd/codespace/output/format_tsv.go deleted file mode 100644 index 3f1d226ca..000000000 --- a/pkg/cmd/codespace/output/format_tsv.go +++ /dev/null @@ -1,25 +0,0 @@ -package output - -import ( - "fmt" - "io" -) - -type tabwriter struct { - w io.Writer -} - -func (j *tabwriter) SetHeader([]string) {} - -func (j *tabwriter) Append(values []string) { - var sep string - for i, v := range values { - if i == 1 { - sep = "\t" - } - fmt.Fprintf(j.w, "%s%s", sep, v) - } - fmt.Fprint(j.w, "\n") -} - -func (j *tabwriter) Render() {} diff --git a/pkg/cmd/codespace/output/logger.go b/pkg/cmd/codespace/output/logger.go deleted file mode 100644 index fdefcad0f..000000000 --- a/pkg/cmd/codespace/output/logger.go +++ /dev/null @@ -1,78 +0,0 @@ -package output - -import ( - "fmt" - "io" - "sync" -) - -// NewLogger returns a Logger that will write to the given stdout/stderr writers. -// Disable the Logger to prevent it from writing to stdout in a TTY environment. -func NewLogger(stdout, stderr io.Writer, disabled bool) *Logger { - enabled := !disabled - if isTTY(stdout) && !enabled { - enabled = false - } - return &Logger{ - out: stdout, - errout: stderr, - enabled: enabled, - } -} - -// Logger writes to the given stdout/stderr writers. -// If not enabled, Print functions will noop but Error functions will continue -// to write to the stderr writer. -type Logger struct { - mu sync.Mutex // guards the writers - out io.Writer - errout io.Writer - enabled bool -} - -// Print writes the arguments to the stdout writer. -func (l *Logger) Print(v ...interface{}) (int, error) { - if !l.enabled { - return 0, nil - } - - l.mu.Lock() - defer l.mu.Unlock() - return fmt.Fprint(l.out, v...) -} - -// Println writes the arguments to the stdout writer with a newline at the end. -func (l *Logger) Println(v ...interface{}) (int, error) { - if !l.enabled { - return 0, nil - } - - l.mu.Lock() - defer l.mu.Unlock() - return fmt.Fprintln(l.out, v...) -} - -// Printf writes the formatted arguments to the stdout writer. -func (l *Logger) Printf(f string, v ...interface{}) (int, error) { - if !l.enabled { - return 0, nil - } - - l.mu.Lock() - defer l.mu.Unlock() - return fmt.Fprintf(l.out, f, v...) -} - -// Errorf writes the formatted arguments to the stderr writer. -func (l *Logger) Errorf(f string, v ...interface{}) (int, error) { - l.mu.Lock() - defer l.mu.Unlock() - return fmt.Fprintf(l.errout, f, v...) -} - -// Errorln writes the arguments to the stderr writer with a newline at the end. -func (l *Logger) Errorln(v ...interface{}) (int, error) { - l.mu.Lock() - defer l.mu.Unlock() - return fmt.Fprintln(l.errout, v...) -} From 3492109e12a4a44905d8a97b50cfc54213ba85ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 22 Oct 2021 14:39:21 +0200 Subject: [PATCH 15/22] codespace create: avoid unnecessarily fetching user --- pkg/cmd/codespace/create.go | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index e0a4de7a4..a1a21d880 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -41,7 +41,6 @@ func newCreateCmd(app *App) *cobra.Command { // Create creates a new Codespace func (a *App) Create(ctx context.Context, opts createOptions) error { locationCh := getLocation(ctx, a.apiClient) - userCh := getUser(ctx, a.apiClient) repo, err := getRepoName(opts.repo) if err != nil { @@ -64,11 +63,6 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { return fmt.Errorf("error getting codespace region location: %w", locationResult.Err) } - userResult := <-userCh - if userResult.Err != nil { - return fmt.Errorf("error getting codespace user: %w", userResult.Err) - } - machine, err := getMachineName(ctx, a.apiClient, repository.ID, opts.machine, branch, locationResult.Location) if err != nil { return fmt.Errorf("error getting machine type: %w", err) @@ -90,7 +84,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { } if opts.showStatus { - if err := a.showStatus(ctx, userResult.User, codespace); err != nil { + if err := a.showStatus(ctx, codespace); err != nil { return fmt.Errorf("show status: %w", err) } } @@ -102,7 +96,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { // showStatus polls the codespace for a list of post create states and their status. It will keep polling // until all states have finished. Once all states have finished, we poll once more to check if any new // states have been introduced and stop polling otherwise. -func (a *App) showStatus(ctx context.Context, user *api.User, codespace *api.Codespace) error { +func (a *App) showStatus(ctx context.Context, codespace *api.Codespace) error { var ( lastState codespaces.PostCreateState breakNextState bool @@ -163,21 +157,6 @@ func (a *App) showStatus(ctx context.Context, user *api.User, codespace *api.Cod return nil } -type getUserResult struct { - User *api.User - Err error -} - -// getUser fetches the user record associated with the GITHUB_TOKEN -func getUser(ctx context.Context, apiClient apiClient) <-chan getUserResult { - ch := make(chan getUserResult, 1) - go func() { - user, err := apiClient.GetUser(ctx) - ch <- getUserResult{user, err} - }() - return ch -} - type locationResult struct { Location string Err error From 436762dd542f6e2e20e8484f755bfbc89c5bff45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 22 Oct 2021 15:09:43 +0200 Subject: [PATCH 16/22] codespace create: make the branch input optional When blank, the branch name will default to the default branch for the repository. --- internal/codespaces/api/api.go | 5 ++- pkg/cmd/codespace/create.go | 76 ++++++++++++++++------------------ 2 files changed, 38 insertions(+), 43 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 505e292ae..ab294ef4a 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -104,8 +104,9 @@ func (a *API) GetUser(ctx context.Context) (*User, error) { // Repository represents a GitHub repository. type Repository struct { - ID int `json:"id"` - FullName string `json:"full_name"` + ID int `json:"id"` + FullName string `json:"full_name"` + DefaultBranch string `json:"default_branch"` } // GetRepository returns the repository associated with the given owner and name. diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index a1a21d880..ac74658c3 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -42,22 +42,50 @@ func newCreateCmd(app *App) *cobra.Command { func (a *App) Create(ctx context.Context, opts createOptions) error { locationCh := getLocation(ctx, a.apiClient) - repo, err := getRepoName(opts.repo) - if err != nil { - return fmt.Errorf("error getting repository name: %w", err) + userInputs := struct { + Repository string + Branch string + }{ + Repository: opts.repo, + Branch: opts.branch, } - branch, err := getBranchName(opts.branch) - if err != nil { - return fmt.Errorf("error getting branch name: %w", err) + + if userInputs.Repository == "" { + branchPrompt := "Branch (leave blank for default branch):" + if userInputs.Branch != "" { + branchPrompt = "Branch:" + } + questions := []*survey.Question{ + { + Name: "repository", + Prompt: &survey.Input{Message: "Repository:"}, + Validate: survey.Required, + }, + { + Name: "branch", + Prompt: &survey.Input{ + Message: branchPrompt, + Default: userInputs.Branch, + }, + }, + } + if err := ask(questions, &userInputs); err != nil { + return fmt.Errorf("failed to prompt: %w", err) + } } a.StartProgressIndicatorWithLabel("Fetching repository") - repository, err := a.apiClient.GetRepository(ctx, repo) + repository, err := a.apiClient.GetRepository(ctx, userInputs.Repository) a.StopProgressIndicator() if err != nil { return fmt.Errorf("error getting repository: %w", err) } + branch := userInputs.Branch + if branch == "" { + branch = repository.DefaultBranch + } + locationResult := <-locationCh if locationResult.Err != nil { return fmt.Errorf("error getting codespace region location: %w", locationResult.Err) @@ -172,40 +200,6 @@ func getLocation(ctx context.Context, apiClient apiClient) <-chan locationResult return ch } -// getRepoName prompts the user for the name of the repository, or returns the repository if non-empty. -func getRepoName(repo string) (string, error) { - if repo != "" { - return repo, nil - } - - repoSurvey := []*survey.Question{ - { - Name: "repository", - Prompt: &survey.Input{Message: "Repository:"}, - Validate: survey.Required, - }, - } - err := ask(repoSurvey, &repo) - return repo, err -} - -// getBranchName prompts the user for the name of the branch, or returns the branch if non-empty. -func getBranchName(branch string) (string, error) { - if branch != "" { - return branch, nil - } - - branchSurvey := []*survey.Question{ - { - Name: "branch", - Prompt: &survey.Input{Message: "Branch:"}, - Validate: survey.Required, - }, - } - err := ask(branchSurvey, &branch) - return branch, err -} - // getMachineName prompts the user to select the machine type, or validates the machine if non-empty. func getMachineName(ctx context.Context, apiClient apiClient, repoID int, machine, branch, location string) (string, error) { machines, err := apiClient.GetCodespacesMachines(ctx, repoID, branch, location) From 905cb3b9faa478344358604dddc5322079ba25b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 25 Oct 2021 15:45:10 +0200 Subject: [PATCH 17/22] Touch-up codespaces exporting functionality --- internal/codespaces/api/api.go | 1 + pkg/cmd/codespace/ports.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index f509e2661..7f24e490b 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -177,6 +177,7 @@ type CodespaceConnection struct { HostPublicKeys []string `json:"hostPublicKeys"` } +// CodespaceFields is the list of exportable fields for a codespace. var CodespaceFields = []string{ "name", "owner", diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index eba8e6fc8..19fbb4135 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -158,6 +158,8 @@ func (pi *portInfo) ExportData(fields []string) *map[string]interface{} { data[f] = pi.Label() case "browseUrl": data[f] = pi.BrowseURL() + default: + panic("unkown field: " + f) } } From 83a08aa3ba12caf416ef2a279c17199c17b08924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 25 Oct 2021 17:16:54 +0200 Subject: [PATCH 18/22] Remove unnecessary pointers to Go maps --- api/export_pr.go | 8 ++++---- api/export_repo.go | 4 ++-- internal/codespaces/api/api.go | 4 ++-- pkg/cmd/codespace/ports.go | 4 ++-- pkg/cmd/release/shared/fetch.go | 4 ++-- pkg/cmdutil/json_flags.go | 2 +- pkg/cmdutil/json_flags_test.go | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/api/export_pr.go b/api/export_pr.go index 29a5c4a63..18bce025b 100644 --- a/api/export_pr.go +++ b/api/export_pr.go @@ -5,7 +5,7 @@ import ( "strings" ) -func (issue *Issue) ExportData(fields []string) *map[string]interface{} { +func (issue *Issue) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(issue).Elem() data := map[string]interface{}{} @@ -25,10 +25,10 @@ func (issue *Issue) ExportData(fields []string) *map[string]interface{} { } } - return &data + return data } -func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} { +func (pr *PullRequest) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(pr).Elem() data := map[string]interface{}{} @@ -102,7 +102,7 @@ func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} { } } - return &data + return data } func fieldByName(v reflect.Value, field string) reflect.Value { diff --git a/api/export_repo.go b/api/export_repo.go index 8d4e669ad..a07246ab9 100644 --- a/api/export_repo.go +++ b/api/export_repo.go @@ -4,7 +4,7 @@ import ( "reflect" ) -func (repo *Repository) ExportData(fields []string) *map[string]interface{} { +func (repo *Repository) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(repo).Elem() data := map[string]interface{}{} @@ -38,7 +38,7 @@ func (repo *Repository) ExportData(fields []string) *map[string]interface{} { } } - return &data + return data } func miniRepoExport(r *Repository) map[string]interface{} { diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 5020843e2..8f2124d8c 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -189,7 +189,7 @@ var CodespaceFields = []string{ "lastUsedAt", } -func (c *Codespace) ExportData(fields []string) *map[string]interface{} { +func (c *Codespace) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(c).Elem() data := map[string]interface{}{} @@ -213,7 +213,7 @@ func (c *Codespace) ExportData(fields []string) *map[string]interface{} { } } - return &data + return data } // ListCodespaces returns a list of codespaces for the user. Pass a negative limit to request all pages from diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 19fbb4135..ff07b059c 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -143,7 +143,7 @@ var portFields = []string{ "browseUrl", } -func (pi *portInfo) ExportData(fields []string) *map[string]interface{} { +func (pi *portInfo) ExportData(fields []string) map[string]interface{} { data := map[string]interface{}{} for _, f := range fields { @@ -163,7 +163,7 @@ func (pi *portInfo) ExportData(fields []string) *map[string]interface{} { } } - return &data + return data } type devContainerResult struct { diff --git a/pkg/cmd/release/shared/fetch.go b/pkg/cmd/release/shared/fetch.go index 12e52ef7e..06bc83f7a 100644 --- a/pkg/cmd/release/shared/fetch.go +++ b/pkg/cmd/release/shared/fetch.go @@ -74,7 +74,7 @@ type ReleaseAsset struct { BrowserDownloadURL string `json:"browser_download_url"` } -func (rel *Release) ExportData(fields []string) *map[string]interface{} { +func (rel *Release) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(rel).Elem() fieldByName := func(v reflect.Value, field string) reflect.Value { return v.FieldByNameFunc(func(s string) bool { @@ -114,7 +114,7 @@ func (rel *Release) ExportData(fields []string) *map[string]interface{} { } } - return &data + return data } // FetchRelease finds a repository release by its tagName. diff --git a/pkg/cmdutil/json_flags.go b/pkg/cmdutil/json_flags.go index 8b7ed4dd7..e9200c752 100644 --- a/pkg/cmdutil/json_flags.go +++ b/pkg/cmdutil/json_flags.go @@ -179,7 +179,7 @@ func (e *exportFormat) exportData(v reflect.Value) interface{} { } type exportable interface { - ExportData([]string) *map[string]interface{} + ExportData([]string) map[string]interface{} } var exportableType = reflect.TypeOf((*exportable)(nil)).Elem() diff --git a/pkg/cmdutil/json_flags_test.go b/pkg/cmdutil/json_flags_test.go index fcc7f9ccb..ce8464aa5 100644 --- a/pkg/cmdutil/json_flags_test.go +++ b/pkg/cmdutil/json_flags_test.go @@ -198,10 +198,10 @@ type exportableItem struct { Name string } -func (e *exportableItem) ExportData(fields []string) *map[string]interface{} { +func (e *exportableItem) ExportData(fields []string) map[string]interface{} { m := map[string]interface{}{} for _, f := range fields { m[f] = fmt.Sprintf("%s:%s", e.Name, f) } - return &m + return m } From 32f1ea5ec06e47f6a2920cde77ded527699b8ccc Mon Sep 17 00:00:00 2001 From: Srivatsn Narayanan Date: Mon, 25 Oct 2021 16:53:28 +0000 Subject: [PATCH 19/22] Update branding of VSCode in the code command --- pkg/cmd/codespace/code.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/codespace/code.go b/pkg/cmd/codespace/code.go index 4261bcf7d..17b658963 100644 --- a/pkg/cmd/codespace/code.go +++ b/pkg/cmd/codespace/code.go @@ -17,7 +17,7 @@ func newCodeCmd(app *App) *cobra.Command { codeCmd := &cobra.Command{ Use: "code", - Short: "Open a codespace in VS Code", + Short: "Open a codespace in Visual Studio Code", Args: noArgsConstraint, RunE: func(cmd *cobra.Command, args []string) error { return app.VSCode(cmd.Context(), codespace, useInsiders) @@ -25,7 +25,7 @@ func newCodeCmd(app *App) *cobra.Command { } codeCmd.Flags().StringVarP(&codespace, "codespace", "c", "", "Name of the codespace") - codeCmd.Flags().BoolVar(&useInsiders, "insiders", false, "Use the insiders version of VS Code") + codeCmd.Flags().BoolVar(&useInsiders, "insiders", false, "Use the insiders version of Visual Studio Code") return codeCmd } @@ -45,7 +45,7 @@ func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool url := vscodeProtocolURL(codespaceName, useInsiders) if err := open.Run(url); err != nil { - return fmt.Errorf("error opening vscode URL %s: %s. (Is VS Code installed?)", url, err) + return fmt.Errorf("error opening vscode URL %s: %s. (Is Visual Studio Code installed?)", url, err) } return nil From 31ba2ea4d56182879c86e88a1cf7016f58e61905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 25 Oct 2021 19:38:00 +0200 Subject: [PATCH 20/22] Publish codespace commands (#4606) --- pkg/cmd/codespace/root.go | 14 ++------------ pkg/cmd/codespace/ssh.go | 3 ++- pkg/cmd/root/root.go | 1 - 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/codespace/root.go b/pkg/cmd/codespace/root.go index e1e28315d..5b2c0d8fc 100644 --- a/pkg/cmd/codespace/root.go +++ b/pkg/cmd/codespace/root.go @@ -1,23 +1,13 @@ package codespace import ( - "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" ) func NewRootCmd(app *App) *cobra.Command { root := &cobra.Command{ - Use: "codespace", - SilenceUsage: true, // don't print usage message after each error (see #80) - SilenceErrors: false, // print errors automatically so that main need not - Short: "List, create, delete and SSH into codespaces", - Long: `Work with GitHub codespaces`, - Example: heredoc.Doc(` - $ gh codespace list - $ gh codespace create - $ gh codespace delete - $ gh codespace ssh - `), + Use: "codespace", + Short: "Connect to and manage your codespaces", } root.AddCommand(newCodeCmd(app)) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 857bdb91d..9c0ae7f1b 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -31,11 +31,12 @@ func newSSHCmd(app *App) *cobra.Command { var opts sshOptions sshCmd := &cobra.Command{ - Use: "ssh [flags] [--] [ssh-flags] [command]", + Use: "ssh [...] [-- ...] []", Short: "SSH into a codespace", RunE: func(cmd *cobra.Command, args []string) error { return app.SSH(cmd.Context(), args, opts) }, + DisableFlagsInUseLine: true, } sshCmd.Flags().StringVarP(&opts.profile, "profile", "", "", "Name of the SSH profile to use") diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 3cb079fac..b1f791fd8 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -136,7 +136,6 @@ func newCodespaceCmd(f *cmdutil.Factory) *cobra.Command { cmd := codespaceCmd.NewRootCmd(app) cmd.Use = "codespace" cmd.Aliases = []string{"cs"} - cmd.Hidden = true return cmd } From a843cbd72813025817a2293a09b31c4597a3f655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 25 Oct 2021 19:42:22 +0200 Subject: [PATCH 21/22] Mark `codespace` a core command --- pkg/cmd/root/root.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index b1f791fd8..7a1e08674 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -136,6 +136,7 @@ func newCodespaceCmd(f *cmdutil.Factory) *cobra.Command { cmd := codespaceCmd.NewRootCmd(app) cmd.Use = "codespace" cmd.Aliases = []string{"cs"} + cmd.Annotations = map[string]string{"IsCore": "true"} return cmd } From cbd6569cb477be4b5425502f2dbc7eb6f1b976e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 25 Oct 2021 19:43:41 +0200 Subject: [PATCH 22/22] Bump bluemonday to silence the security alert (#4607) GitHub CLI is not affected by GHSA-x95h-979x-cf3j, since we are not vulnerable to XSS via HTML injection, but upgrading the library might silence the security alert. --- go.mod | 1 + go.sum | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index f28c0fe8a..93feba312 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/mattn/go-colorable v0.1.11 github.com/mattn/go-isatty v0.0.14 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d + github.com/microcosm-cc/bluemonday v1.0.16 // indirect github.com/muesli/reflow v0.2.1-0.20210502190812-c80126ec2ad5 github.com/muesli/termenv v0.9.0 github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 diff --git a/go.sum b/go.sum index 036ad8a29..a82789ee4 100644 --- a/go.sum +++ b/go.sum @@ -263,8 +263,9 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/microcosm-cc/bluemonday v1.0.6 h1:ZOvqHKtnx0fUpnbQm3m3zKFWE+DRC+XB1onh8JoEObE= github.com/microcosm-cc/bluemonday v1.0.6/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI= +github.com/microcosm-cc/bluemonday v1.0.16 h1:kHmAq2t7WPWLjiGvzKa5o3HzSfahUKiOq7fAPUiMNIc= +github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -432,8 +433,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=