From 783f316df122c675292dfde458eba84bd68bf3b1 Mon Sep 17 00:00:00 2001 From: Oleg Solomka Date: Wed, 23 Mar 2022 00:26:50 +0000 Subject: [PATCH 1/7] [codespaces]: add hidden `select` command --- pkg/cmd/codespace/root.go | 1 + pkg/cmd/codespace/select.go | 33 +++++++++++++++ pkg/cmd/codespace/select_test.go | 69 ++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 pkg/cmd/codespace/select.go create mode 100644 pkg/cmd/codespace/select_test.go diff --git a/pkg/cmd/codespace/root.go b/pkg/cmd/codespace/root.go index 0a04d2fdd..662f4c2de 100644 --- a/pkg/cmd/codespace/root.go +++ b/pkg/cmd/codespace/root.go @@ -20,6 +20,7 @@ func NewRootCmd(app *App) *cobra.Command { root.AddCommand(newSSHCmd(app)) root.AddCommand(newCpCmd(app)) root.AddCommand(newStopCmd(app)) + root.AddCommand(newSelectCmd(app)) return root } diff --git a/pkg/cmd/codespace/select.go b/pkg/cmd/codespace/select.go new file mode 100644 index 000000000..88c01e211 --- /dev/null +++ b/pkg/cmd/codespace/select.go @@ -0,0 +1,33 @@ +package codespace + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" +) + +func newSelectCmd(app *App) *cobra.Command { + codeCmd := &cobra.Command{ + Use: "select", + Short: "Select a codespace", + Hidden: true, + Args: noArgsConstraint, + RunE: func(cmd *cobra.Command, args []string) error { + return app.Select(cmd.Context(), "") + }, + } + + return codeCmd +} + +func (a *App) Select(ctx context.Context, name string) error { + codespace, err := getOrChooseCodespace(ctx, a.apiClient, name) + if err != nil { + return err + } + + a.io.Out.Write([]byte(fmt.Sprintf("%s\n", codespace.Name))); + + return nil +} diff --git a/pkg/cmd/codespace/select_test.go b/pkg/cmd/codespace/select_test.go new file mode 100644 index 000000000..09c162096 --- /dev/null +++ b/pkg/cmd/codespace/select_test.go @@ -0,0 +1,69 @@ +package codespace + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/iostreams" +) + +const CODESPACE_NAME = "monalisa-cli-cli-abcdef" + +func TestApp_Select(t *testing.T) { + tests := []struct { + name string + arg string + wantErr bool + wantStdout string + wantStderr string + }{ + { + name: "Select a codespace", + arg: CODESPACE_NAME, + wantErr: false, + wantStdout: fmt.Sprintf("%s\n", CODESPACE_NAME), + }, + { + name: "Select a codespace error", + arg: "non-existent-codespace-name", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, stdout, stderr := iostreams.Test() + io.SetStdinTTY(true) + io.SetStdoutTTY(true) + a := NewApp(io, nil, testSelectApiMock(), nil) + + if err := a.Select(context.Background(), tt.arg); (err != nil) != tt.wantErr { + t.Errorf("App.Select() error = %v, wantErr %v", err, tt.wantErr) + } + + if out := stdout.String(); out != tt.wantStdout { + t.Errorf("stdout = %q, want %q", out, tt.wantStdout) + } + if out := sortLines(stderr.String()); out != tt.wantStderr { + t.Errorf("stderr = %q, want %q", out, tt.wantStderr) + } + }) + } +} + +func testSelectApiMock() *apiClientMock { + testingCodespace := &api.Codespace{ + Name: CODESPACE_NAME, + } + return &apiClientMock{ + GetCodespaceFunc: func(_ context.Context, name string, includeConnection bool) (*api.Codespace, error) { + if name == CODESPACE_NAME { + return testingCodespace, nil + } + + return nil, errors.New("Cannot find codespace.") + }, + } +} From 07e18ea3d4172fd58b1432c6950366512522d3ed Mon Sep 17 00:00:00 2001 From: Oleg Solomka Date: Fri, 1 Apr 2022 11:35:40 -0700 Subject: [PATCH 2/7] [codespace select]: add unit tests for filePath flag, add command description --- pkg/cmd/codespace/select.go | 45 ++++++++++++++++++++++++++++---- pkg/cmd/codespace/select_test.go | 28 +++++++++++++++++++- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/codespace/select.go b/pkg/cmd/codespace/select.go index 88c01e211..c5800826a 100644 --- a/pkg/cmd/codespace/select.go +++ b/pkg/cmd/codespace/select.go @@ -3,31 +3,66 @@ package codespace import ( "context" "fmt" + "os" "github.com/spf13/cobra" ) +type selectOptions struct { + filePath string +} + func newSelectCmd(app *App) *cobra.Command { - codeCmd := &cobra.Command{ + opts := selectOptions{} + + selectCmd := &cobra.Command{ Use: "select", Short: "Select a codespace", Hidden: true, Args: noArgsConstraint, RunE: func(cmd *cobra.Command, args []string) error { - return app.Select(cmd.Context(), "") + return app.Select(cmd.Context(), "", opts) }, } - return codeCmd + selectCmd.Flags().StringVarP(&opts.filePath, "file", "f", "", "Output file path") + return selectCmd } -func (a *App) Select(ctx context.Context, name string) error { +// Hidden codespace `select` command allows to reuse the common codespace selection +// dialog by external GH CLI extensions. By default output selected codespace name +// into `stdout`. Pass `--file`(`-f`) flag along with a file path to output selected +// codespace into a file instead. +// +// ## Examples +// +// With `stdout` output: +// +// ```shell +// gh codespace select +// ``` +// +// With `into-a-file`` output: +// +// ```shell +// gh codespace select --file /tmp/selected_codespace.txt +// ``` +func (a *App) Select(ctx context.Context, name string, opts selectOptions) error { codespace, err := getOrChooseCodespace(ctx, a.apiClient, name) if err != nil { return err } - a.io.Out.Write([]byte(fmt.Sprintf("%s\n", codespace.Name))); + if opts.filePath != "" { + f, err := os.Create(opts.filePath) + if err != nil { + return err + } + f.WriteString(codespace.Name); + return nil + } + + a.io.Out.Write([]byte(fmt.Sprintf("%s\n", codespace.Name))); return nil } diff --git a/pkg/cmd/codespace/select_test.go b/pkg/cmd/codespace/select_test.go index 09c162096..cdae1d619 100644 --- a/pkg/cmd/codespace/select_test.go +++ b/pkg/cmd/codespace/select_test.go @@ -3,6 +3,7 @@ package codespace import ( "context" "errors" + "os" "fmt" "testing" @@ -11,14 +12,17 @@ import ( ) const CODESPACE_NAME = "monalisa-cli-cli-abcdef" +const OUTPUT_FILE_PATH = "../../../bin/codespace-selection-test.log" func TestApp_Select(t *testing.T) { tests := []struct { name string arg string + opts selectOptions wantErr bool wantStdout string wantStderr string + wantFileContents string }{ { name: "Select a codespace", @@ -31,6 +35,13 @@ func TestApp_Select(t *testing.T) { arg: "non-existent-codespace-name", wantErr: true, }, + { + name: "Select a codespace", + arg: CODESPACE_NAME, + wantErr: false, + wantFileContents: CODESPACE_NAME, + opts: selectOptions { filePath: OUTPUT_FILE_PATH }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -39,7 +50,7 @@ func TestApp_Select(t *testing.T) { io.SetStdoutTTY(true) a := NewApp(io, nil, testSelectApiMock(), nil) - if err := a.Select(context.Background(), tt.arg); (err != nil) != tt.wantErr { + if err := a.Select(context.Background(), tt.arg, tt.opts); (err != nil) != tt.wantErr { t.Errorf("App.Select() error = %v, wantErr %v", err, tt.wantErr) } @@ -49,6 +60,21 @@ func TestApp_Select(t *testing.T) { if out := sortLines(stderr.String()); out != tt.wantStderr { t.Errorf("stderr = %q, want %q", out, tt.wantStderr) } + + if tt.wantFileContents != "" { + if tt.opts.filePath == "" { + t.Errorf("wantFileContents is set but opts.filePath is not") + } + + dat, err := os.ReadFile(tt.opts.filePath) + if err != nil { + panic(err) + } + + if string(dat) != tt.wantFileContents { + t.Errorf("file contents = %q, want %q", string(dat), CODESPACE_NAME) + } + } }) } } From 9a0b208200eb486cca920dbf4520f2bb4971dbd8 Mon Sep 17 00:00:00 2001 From: Oleg Solomka Date: Fri, 1 Apr 2022 12:30:55 -0700 Subject: [PATCH 3/7] [select codespace]: cleanup description --- pkg/cmd/codespace/select.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/codespace/select.go b/pkg/cmd/codespace/select.go index c5800826a..1a1b4834e 100644 --- a/pkg/cmd/codespace/select.go +++ b/pkg/cmd/codespace/select.go @@ -29,10 +29,10 @@ func newSelectCmd(app *App) *cobra.Command { return selectCmd } -// Hidden codespace `select` command allows to reuse the common codespace selection +// Hidden codespace `select` command allows to reuse existing codespace selection // dialog by external GH CLI extensions. By default output selected codespace name // into `stdout`. Pass `--file`(`-f`) flag along with a file path to output selected -// codespace into a file instead. +// codespace name into a file instead. // // ## Examples // @@ -42,7 +42,7 @@ func newSelectCmd(app *App) *cobra.Command { // gh codespace select // ``` // -// With `into-a-file`` output: +// With `into-a-file` output: // // ```shell // gh codespace select --file /tmp/selected_codespace.txt From b4d29a8b7b75c14039fe9335f2bef21c8caca493 Mon Sep 17 00:00:00 2001 From: Oleg Solomka Date: Mon, 4 Apr 2022 10:34:04 -0700 Subject: [PATCH 4/7] address PR feedback --- pkg/cmd/codespace/.gitignore | 1 + pkg/cmd/codespace/select.go | 10 +++++++--- pkg/cmd/codespace/select_test.go | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 pkg/cmd/codespace/.gitignore diff --git a/pkg/cmd/codespace/.gitignore b/pkg/cmd/codespace/.gitignore new file mode 100644 index 000000000..59c4d1ba9 --- /dev/null +++ b/pkg/cmd/codespace/.gitignore @@ -0,0 +1 @@ +codespace-selection-test.log diff --git a/pkg/cmd/codespace/select.go b/pkg/cmd/codespace/select.go index 1a1b4834e..85bf9af25 100644 --- a/pkg/cmd/codespace/select.go +++ b/pkg/cmd/codespace/select.go @@ -47,7 +47,7 @@ func newSelectCmd(app *App) *cobra.Command { // ```shell // gh codespace select --file /tmp/selected_codespace.txt // ``` -func (a *App) Select(ctx context.Context, name string, opts selectOptions) error { +func (a *App) Select(ctx context.Context, name string, opts selectOptions) (err error) { codespace, err := getOrChooseCodespace(ctx, a.apiClient, name) if err != nil { return err @@ -56,13 +56,17 @@ func (a *App) Select(ctx context.Context, name string, opts selectOptions) error if opts.filePath != "" { f, err := os.Create(opts.filePath) if err != nil { - return err + return fmt.Errorf("failed to create output file: %w", err) } f.WriteString(codespace.Name); + + defer safeClose(f, &err) + return nil } - a.io.Out.Write([]byte(fmt.Sprintf("%s\n", codespace.Name))); + fmt.Fprintln(a.io.Out, codespace.Name) + return nil } diff --git a/pkg/cmd/codespace/select_test.go b/pkg/cmd/codespace/select_test.go index cdae1d619..099617ddf 100644 --- a/pkg/cmd/codespace/select_test.go +++ b/pkg/cmd/codespace/select_test.go @@ -12,7 +12,7 @@ import ( ) const CODESPACE_NAME = "monalisa-cli-cli-abcdef" -const OUTPUT_FILE_PATH = "../../../bin/codespace-selection-test.log" +const OUTPUT_FILE_PATH = "codespace-selection-test.log" func TestApp_Select(t *testing.T) { tests := []struct { @@ -68,7 +68,7 @@ func TestApp_Select(t *testing.T) { dat, err := os.ReadFile(tt.opts.filePath) if err != nil { - panic(err) + t.Fatal(err) } if string(dat) != tt.wantFileContents { @@ -89,7 +89,7 @@ func testSelectApiMock() *apiClientMock { return testingCodespace, nil } - return nil, errors.New("Cannot find codespace.") + return nil, errors.New("cannot find codespace") }, } } From d6edc6f8a607571c683af57536a001ee8a0d9375 Mon Sep 17 00:00:00 2001 From: Oleg Solomka Date: Mon, 4 Apr 2022 10:53:52 -0700 Subject: [PATCH 5/7] address more PR feedback --- pkg/cmd/codespace/select.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/codespace/select.go b/pkg/cmd/codespace/select.go index 85bf9af25..ce6a681e5 100644 --- a/pkg/cmd/codespace/select.go +++ b/pkg/cmd/codespace/select.go @@ -59,10 +59,13 @@ func (a *App) Select(ctx context.Context, name string, opts selectOptions) (err return fmt.Errorf("failed to create output file: %w", err) } - f.WriteString(codespace.Name); - defer safeClose(f, &err) + _, err = f.WriteString(codespace.Name); + if err != nil { + return fmt.Errorf("failed to write codespace name to output file: %w", err) + } + return nil } From 78573442ebdf1d122d0f04c30e4d585d02739b38 Mon Sep 17 00:00:00 2001 From: Oleg Solomka Date: Mon, 4 Apr 2022 17:13:52 -0700 Subject: [PATCH 6/7] address more PR feedback --- pkg/cmd/codespace/.gitignore | 1 - pkg/cmd/codespace/select.go | 2 +- pkg/cmd/codespace/select_test.go | 26 +++++++++++++++++++------- 3 files changed, 20 insertions(+), 9 deletions(-) delete mode 100644 pkg/cmd/codespace/.gitignore diff --git a/pkg/cmd/codespace/.gitignore b/pkg/cmd/codespace/.gitignore deleted file mode 100644 index 59c4d1ba9..000000000 --- a/pkg/cmd/codespace/.gitignore +++ /dev/null @@ -1 +0,0 @@ -codespace-selection-test.log diff --git a/pkg/cmd/codespace/select.go b/pkg/cmd/codespace/select.go index ce6a681e5..092783427 100644 --- a/pkg/cmd/codespace/select.go +++ b/pkg/cmd/codespace/select.go @@ -17,7 +17,7 @@ func newSelectCmd(app *App) *cobra.Command { selectCmd := &cobra.Command{ Use: "select", - Short: "Select a codespace", + Short: "Select a Codespace", Hidden: true, Args: noArgsConstraint, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/pkg/cmd/codespace/select_test.go b/pkg/cmd/codespace/select_test.go index 099617ddf..d80fd4282 100644 --- a/pkg/cmd/codespace/select_test.go +++ b/pkg/cmd/codespace/select_test.go @@ -6,20 +6,20 @@ import ( "os" "fmt" "testing" - + "io/ioutil" + "github.com/cli/cli/v2/internal/codespaces/api" "github.com/cli/cli/v2/pkg/iostreams" ) const CODESPACE_NAME = "monalisa-cli-cli-abcdef" -const OUTPUT_FILE_PATH = "codespace-selection-test.log" func TestApp_Select(t *testing.T) { tests := []struct { name string arg string - opts selectOptions wantErr bool + outputToFile bool wantStdout string wantStderr string wantFileContents string @@ -40,7 +40,7 @@ func TestApp_Select(t *testing.T) { arg: CODESPACE_NAME, wantErr: false, wantFileContents: CODESPACE_NAME, - opts: selectOptions { filePath: OUTPUT_FILE_PATH }, + outputToFile: true, }, } for _, tt := range tests { @@ -50,7 +50,19 @@ func TestApp_Select(t *testing.T) { io.SetStdoutTTY(true) a := NewApp(io, nil, testSelectApiMock(), nil) - if err := a.Select(context.Background(), tt.arg, tt.opts); (err != nil) != tt.wantErr { + opts := selectOptions{} + if tt.outputToFile { + file, err := ioutil.TempFile("", "codespace-selection-test") + if err != nil { + t.Fatal(err) + } + + defer os.Remove(file.Name()) + + opts = selectOptions { filePath: file.Name() } + } + + if err := a.Select(context.Background(), tt.arg, opts); (err != nil) != tt.wantErr { t.Errorf("App.Select() error = %v, wantErr %v", err, tt.wantErr) } @@ -62,11 +74,11 @@ func TestApp_Select(t *testing.T) { } if tt.wantFileContents != "" { - if tt.opts.filePath == "" { + if opts.filePath == "" { t.Errorf("wantFileContents is set but opts.filePath is not") } - dat, err := os.ReadFile(tt.opts.filePath) + dat, err := os.ReadFile(opts.filePath) if err != nil { t.Fatal(err) } From e7fb9c0c4d817c229c926590e405dd3d2272f62c Mon Sep 17 00:00:00 2001 From: Oleg Solomka Date: Tue, 5 Apr 2022 10:08:34 -0700 Subject: [PATCH 7/7] fix linter issues --- pkg/cmd/codespace/select.go | 12 +++++----- pkg/cmd/codespace/select_test.go | 38 ++++++++++++++++---------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/pkg/cmd/codespace/select.go b/pkg/cmd/codespace/select.go index 092783427..d95162c12 100644 --- a/pkg/cmd/codespace/select.go +++ b/pkg/cmd/codespace/select.go @@ -16,10 +16,10 @@ func newSelectCmd(app *App) *cobra.Command { opts := selectOptions{} selectCmd := &cobra.Command{ - Use: "select", - Short: "Select a Codespace", + Use: "select", + Short: "Select a Codespace", Hidden: true, - Args: noArgsConstraint, + Args: noArgsConstraint, RunE: func(cmd *cobra.Command, args []string) error { return app.Select(cmd.Context(), "", opts) }, @@ -33,7 +33,7 @@ func newSelectCmd(app *App) *cobra.Command { // dialog by external GH CLI extensions. By default output selected codespace name // into `stdout`. Pass `--file`(`-f`) flag along with a file path to output selected // codespace name into a file instead. -// +// // ## Examples // // With `stdout` output: @@ -61,11 +61,11 @@ func (a *App) Select(ctx context.Context, name string, opts selectOptions) (err defer safeClose(f, &err) - _, err = f.WriteString(codespace.Name); + _, err = f.WriteString(codespace.Name) if err != nil { return fmt.Errorf("failed to write codespace name to output file: %w", err) } - + return nil } diff --git a/pkg/cmd/codespace/select_test.go b/pkg/cmd/codespace/select_test.go index d80fd4282..b2253f1d4 100644 --- a/pkg/cmd/codespace/select_test.go +++ b/pkg/cmd/codespace/select_test.go @@ -3,11 +3,11 @@ package codespace import ( "context" "errors" - "os" "fmt" - "testing" "io/ioutil" - + "os" + "testing" + "github.com/cli/cli/v2/internal/codespaces/api" "github.com/cli/cli/v2/pkg/iostreams" ) @@ -16,31 +16,31 @@ const CODESPACE_NAME = "monalisa-cli-cli-abcdef" func TestApp_Select(t *testing.T) { tests := []struct { - name string - arg string - wantErr bool - outputToFile bool - wantStdout string - wantStderr string + name string + arg string + wantErr bool + outputToFile bool + wantStdout string + wantStderr string wantFileContents string }{ { - name: "Select a codespace", - arg: CODESPACE_NAME, - wantErr: false, + name: "Select a codespace", + arg: CODESPACE_NAME, + wantErr: false, wantStdout: fmt.Sprintf("%s\n", CODESPACE_NAME), }, { - name: "Select a codespace error", - arg: "non-existent-codespace-name", + name: "Select a codespace error", + arg: "non-existent-codespace-name", wantErr: true, }, { - name: "Select a codespace", - arg: CODESPACE_NAME, - wantErr: false, + name: "Select a codespace", + arg: CODESPACE_NAME, + wantErr: false, wantFileContents: CODESPACE_NAME, - outputToFile: true, + outputToFile: true, }, } for _, tt := range tests { @@ -59,7 +59,7 @@ func TestApp_Select(t *testing.T) { defer os.Remove(file.Name()) - opts = selectOptions { filePath: file.Name() } + opts = selectOptions{filePath: file.Name()} } if err := a.Select(context.Background(), tt.arg, opts); (err != nil) != tt.wantErr {