From 54560c19dd9406ab5e1a80d44636498aadaf6618 Mon Sep 17 00:00:00 2001 From: David Gardiner Date: Fri, 26 May 2023 09:54:28 -0700 Subject: [PATCH 1/8] Add gh cs view command --- internal/codespaces/api/api.go | 36 ++++++++++++- pkg/cmd/codespace/list.go | 2 +- pkg/cmd/codespace/root.go | 1 + pkg/cmd/codespace/view.go | 99 ++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 pkg/cmd/codespace/view.go diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index c45eafb87..b6330a59a 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -192,6 +192,13 @@ type Codespace struct { PendingOperationDisabledReason string `json:"pending_operation_disabled_reason"` IdleTimeoutNotice string `json:"idle_timeout_notice"` WebURL string `json:"web_url"` + DevContainerPath string `json:"devcontainer_path"` + Prebuild bool `json:"prebuild"` + Location string `json:"location"` + IdleTimeoutMinutes int `json:"idle_timeout_minutes"` + RetentionExpiresAt string `json:"retention_expires_at"` + RecentFolders []string `json:"recent_folders"` + BillableOwner User `json:"billable_owner"` } type CodespaceGitStatus struct { @@ -230,8 +237,8 @@ type CodespaceConnection struct { HostPublicKeys []string `json:"hostPublicKeys"` } -// CodespaceFields is the list of exportable fields for a codespace. -var CodespaceFields = []string{ +// ListCodespaceFields is the list of exportable fields for a codespace when using the `gh cs list` command. +var ListCodespaceFields = []string{ "displayName", "name", "owner", @@ -244,6 +251,27 @@ var CodespaceFields = []string{ "vscsTarget", } +// ViewCodespaceFields is the list of exportable fields for a codespace when using the `gh cs view` command. +var ViewCodespaceFields = []string{ + "name", + "displayName", + "state", + "owner", + "billableOwner", + "location", + "repository", + "gitStatus", + "devcontainerPath", + "machineName", + "machineDisplayName", + "prebuild", + "createdAt", + "lastUsedAt", + "idleTimeoutMinutes", + "retentionExpiresAt", + "recentFolders", +} + func (c *Codespace) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(c).Elem() data := map[string]interface{}{} @@ -256,11 +284,15 @@ func (c *Codespace) ExportData(fields []string) map[string]interface{} { data[f] = c.Repository.FullName case "machineName": data[f] = c.Machine.Name + case "machineDisplayName": + data[f] = c.Machine.DisplayName case "gitStatus": data[f] = map[string]interface{}{ "ref": c.GitStatus.Ref, "hasUnpushedChanges": c.GitStatus.HasUnpushedChanges, "hasUncommittedChanges": c.GitStatus.HasUncommittedChanges, + "ahead": c.GitStatus.Ahead, + "behind": c.GitStatus.Behind, } case "vscsTarget": if c.VSCSTarget != "" && c.VSCSTarget != VSCSTargetProduction { diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 6b1cd1cc0..bba34ff45 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -70,7 +70,7 @@ func newListCmd(app *App) *cobra.Command { listCmd.Flags().StringVarP(&opts.orgName, "org", "o", "", "The `login` handle of the organization to list codespaces for (admin-only)") listCmd.Flags().StringVarP(&opts.userName, "user", "u", "", "The `username` to list codespaces for (used with --org)") - cmdutil.AddJSONFlags(listCmd, &exporter, api.CodespaceFields) + cmdutil.AddJSONFlags(listCmd, &exporter, api.ListCodespaceFields) listCmd.Flags().BoolVarP(&opts.useWeb, "web", "w", false, "List codespaces in the web browser, cannot be used with --user or --org") diff --git a/pkg/cmd/codespace/root.go b/pkg/cmd/codespace/root.go index 8439430aa..28a80edae 100644 --- a/pkg/cmd/codespace/root.go +++ b/pkg/cmd/codespace/root.go @@ -16,6 +16,7 @@ func NewRootCmd(app *App) *cobra.Command { root.AddCommand(newDeleteCmd(app)) root.AddCommand(newJupyterCmd(app)) root.AddCommand(newListCmd(app)) + root.AddCommand(newViewCmd(app)) root.AddCommand(newLogsCmd(app)) root.AddCommand(newPortsCmd(app)) root.AddCommand(newSSHCmd(app)) diff --git a/pkg/cmd/codespace/view.go b/pkg/cmd/codespace/view.go new file mode 100644 index 000000000..6f83ed89d --- /dev/null +++ b/pkg/cmd/codespace/view.go @@ -0,0 +1,99 @@ +package codespace + +import ( + "context" + "strconv" + "strings" + + "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" +) + +type viewOptions struct { + selector *CodespaceSelector +} + +func newViewCmd(app *App) *cobra.Command { + opts := &viewOptions{} + var exporter cmdutil.Exporter + + viewCmd := &cobra.Command{ + Use: "view", + Short: "View details about a codespace", + Args: noArgsConstraint, + RunE: func(cmd *cobra.Command, args []string) error { + return app.ViewCodespace(cmd.Context(), opts, exporter) + }, + } + opts.selector = AddCodespaceSelector(viewCmd, app.apiClient) + cmdutil.AddJSONFlags(viewCmd, &exporter, api.ViewCodespaceFields) + + return viewCmd +} + +func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions, exporter cmdutil.Exporter) error { + selectedCodespace, err := opts.selector.Select(ctx) + if err != nil { + return err + } + + 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, selectedCodespace) + } + + //nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter + tp := utils.NewTablePrinter(a.io) + c := codespace{selectedCodespace} + formattedName := formatNameForVSCSTarget(c.Name, c.VSCSTarget) + + // Create an array of fields to display in the table with their values + fields := []struct { + name string + value string + }{ + {"Name", formattedName}, + {"Display Name", c.DisplayName}, + {"State", c.State}, + {"Machine Name", c.Machine.Name}, + {"Machine Display Name", c.Machine.DisplayName}, + {"Prebuild", strconv.FormatBool(c.Prebuild)}, + {"Owner", c.Owner.Login}, + {"BillableOwner", c.BillableOwner.Login}, + {"Location", c.Location}, + {"Repository", c.Repository.FullName}, + {"Branch", c.GitStatus.Ref}, + {"Devcontainer Path", c.DevContainerPath}, + {"Commits Ahead", strconv.Itoa(c.GitStatus.Ahead)}, + {"Commits Behind", strconv.Itoa(c.GitStatus.Behind)}, + {"Has Uncommitted Changes", strconv.FormatBool(c.GitStatus.HasUncommittedChanges)}, + {"Has Unpushed Changes", strconv.FormatBool(c.GitStatus.HasUnpushedChanges)}, + {"Created At", c.CreatedAt}, + {"Last Used At", c.LastUsedAt}, + {"Idle Timeout Minutes", strconv.Itoa(c.IdleTimeoutMinutes)}, + {"Retention Expires At", c.RetentionExpiresAt}, + {"Recent Folders", strings.Join(c.RecentFolders, ", ")}, + } + + for _, field := range fields { + // Only display the field if it has a value. + if field.value != "" { + tp.AddField(field.name, nil, nil) + tp.AddField(field.value, nil, nil) + tp.EndRow() + } + } + + err = tp.Render() + if err != nil { + return err + } + + return nil +} From 99cd0af8b44b8a82f3b521546fe7aec1043e4970 Mon Sep 17 00:00:00 2001 From: David Gardiner Date: Fri, 26 May 2023 15:41:34 -0700 Subject: [PATCH 2/8] Check if running in a codespace --- internal/codespaces/api/api.go | 1 + pkg/cmd/codespace/view.go | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index b6330a59a..8fdf4f1d3 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -196,6 +196,7 @@ type Codespace struct { Prebuild bool `json:"prebuild"` Location string `json:"location"` IdleTimeoutMinutes int `json:"idle_timeout_minutes"` + RetentionPeriodMinutes int `json:"retention_period_minutes"` RetentionExpiresAt string `json:"retention_expires_at"` RecentFolders []string `json:"recent_folders"` BillableOwner User `json:"billable_owner"` diff --git a/pkg/cmd/codespace/view.go b/pkg/cmd/codespace/view.go index 6f83ed89d..28d490251 100644 --- a/pkg/cmd/codespace/view.go +++ b/pkg/cmd/codespace/view.go @@ -2,8 +2,9 @@ package codespace import ( "context" + "fmt" + "os" "strconv" - "strings" "github.com/cli/cli/v2/internal/codespaces/api" "github.com/cli/cli/v2/pkg/cmdutil" @@ -34,6 +35,10 @@ func newViewCmd(app *App) *cobra.Command { } func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions, exporter cmdutil.Exporter) error { + if codespaceName := os.Getenv("CODESPACES_NAME"); os.Getenv("CODESPACES") == "true" && codespaceName != "" { + opts.selector.codespaceName = codespaceName + } + selectedCodespace, err := opts.selector.Select(ctx) if err != nil { return err @@ -70,15 +75,12 @@ func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions, exporter cmd {"Repository", c.Repository.FullName}, {"Branch", c.GitStatus.Ref}, {"Devcontainer Path", c.DevContainerPath}, - {"Commits Ahead", strconv.Itoa(c.GitStatus.Ahead)}, - {"Commits Behind", strconv.Itoa(c.GitStatus.Behind)}, - {"Has Uncommitted Changes", strconv.FormatBool(c.GitStatus.HasUncommittedChanges)}, - {"Has Unpushed Changes", strconv.FormatBool(c.GitStatus.HasUnpushedChanges)}, + {"Git Status", formatGitStatus(c)}, {"Created At", c.CreatedAt}, {"Last Used At", c.LastUsedAt}, {"Idle Timeout Minutes", strconv.Itoa(c.IdleTimeoutMinutes)}, + {"Retention Period Days", strconv.Itoa(c.RetentionPeriodMinutes / 1440)}, {"Retention Expires At", c.RetentionExpiresAt}, - {"Recent Folders", strings.Join(c.RecentFolders, ", ")}, } for _, field := range fields { @@ -97,3 +99,8 @@ func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions, exporter cmd return nil } + +func formatGitStatus(codespace codespace) string { + branchWithGitStatus := codespace.branchWithGitStatus() + return fmt.Sprintf("%s \u25cf %d\u2193 %d\u2191", branchWithGitStatus, codespace.GitStatus.Behind, codespace.GitStatus.Ahead) +} From f6a5398bf9bcce1efe268655e72789d644a240ae Mon Sep 17 00:00:00 2001 From: David Gardiner Date: Fri, 26 May 2023 22:59:28 +0000 Subject: [PATCH 3/8] Update fields to show --- internal/codespaces/api/api.go | 3 +++ pkg/cmd/codespace/view.go | 20 ++++++-------------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 8fdf4f1d3..e9388d5ed 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -269,6 +269,7 @@ var ViewCodespaceFields = []string{ "createdAt", "lastUsedAt", "idleTimeoutMinutes", + "retentionPeriodDays", "retentionExpiresAt", "recentFolders", } @@ -287,6 +288,8 @@ func (c *Codespace) ExportData(fields []string) map[string]interface{} { data[f] = c.Machine.Name case "machineDisplayName": data[f] = c.Machine.DisplayName + case "retentionPeriodDays": + data[f] = c.RetentionPeriodMinutes / 1440 case "gitStatus": data[f] = map[string]interface{}{ "ref": c.GitStatus.Ref, diff --git a/pkg/cmd/codespace/view.go b/pkg/cmd/codespace/view.go index 28d490251..e923c92b4 100644 --- a/pkg/cmd/codespace/view.go +++ b/pkg/cmd/codespace/view.go @@ -35,7 +35,8 @@ func newViewCmd(app *App) *cobra.Command { } func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions, exporter cmdutil.Exporter) error { - if codespaceName := os.Getenv("CODESPACES_NAME"); os.Getenv("CODESPACES") == "true" && codespaceName != "" { + // If we are in a codespace, show the details for it + if codespaceName := os.Getenv("CODESPACE_NAME"); os.Getenv("CODESPACES") == "true" && codespaceName != "" { opts.selector.codespaceName = codespaceName } @@ -66,21 +67,12 @@ func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions, exporter cmd {"Name", formattedName}, {"Display Name", c.DisplayName}, {"State", c.State}, - {"Machine Name", c.Machine.Name}, - {"Machine Display Name", c.Machine.DisplayName}, - {"Prebuild", strconv.FormatBool(c.Prebuild)}, - {"Owner", c.Owner.Login}, - {"BillableOwner", c.BillableOwner.Login}, - {"Location", c.Location}, - {"Repository", c.Repository.FullName}, - {"Branch", c.GitStatus.Ref}, - {"Devcontainer Path", c.DevContainerPath}, {"Git Status", formatGitStatus(c)}, + {"Devcontainer Path", c.DevContainerPath}, + {"Machine Display Name", c.Machine.DisplayName}, {"Created At", c.CreatedAt}, - {"Last Used At", c.LastUsedAt}, - {"Idle Timeout Minutes", strconv.Itoa(c.IdleTimeoutMinutes)}, - {"Retention Period Days", strconv.Itoa(c.RetentionPeriodMinutes / 1440)}, - {"Retention Expires At", c.RetentionExpiresAt}, + {"Idle Timeout", strconv.Itoa(c.IdleTimeoutMinutes) + " minutes"}, + {"Retention Period", strconv.Itoa(c.RetentionPeriodMinutes/1440) + " days"}, } for _, field := range fields { From 3c05c99c83538212e5e22521650fcafb0c6f6ba0 Mon Sep 17 00:00:00 2001 From: David Gardiner Date: Tue, 30 May 2023 12:20:23 -0700 Subject: [PATCH 4/8] Address comments --- internal/codespaces/api/api.go | 3 ++ pkg/cmd/codespace/view.go | 72 +++++++++++++++++++++++++++------- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index e9388d5ed..d66aff065 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -200,6 +200,7 @@ type Codespace struct { RetentionExpiresAt string `json:"retention_expires_at"` RecentFolders []string `json:"recent_folders"` BillableOwner User `json:"billable_owner"` + EnvironmentId string `json:"environment_id"` } type CodespaceGitStatus struct { @@ -272,6 +273,8 @@ var ViewCodespaceFields = []string{ "retentionPeriodDays", "retentionExpiresAt", "recentFolders", + "vscsTarget", + "environmentId", } func (c *Codespace) ExportData(fields []string) map[string]interface{} { diff --git a/pkg/cmd/codespace/view.go b/pkg/cmd/codespace/view.go index e923c92b4..7a381280d 100644 --- a/pkg/cmd/codespace/view.go +++ b/pkg/cmd/codespace/view.go @@ -4,39 +4,73 @@ import ( "context" "fmt" "os" - "strconv" + "github.com/MakeNowJust/heredoc" "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" ) +const ( + minutesInDay = 1440 +) + type viewOptions struct { selector *CodespaceSelector + exporter cmdutil.Exporter + orgName string + userName string } func newViewCmd(app *App) *cobra.Command { opts := &viewOptions{} - var exporter cmdutil.Exporter viewCmd := &cobra.Command{ Use: "view", Short: "View details about a codespace", - Args: noArgsConstraint, + Long: heredoc.Doc(` + View details about a codespace. + + For more fine-grained details, you can add the "--json" flag to view the full list of fields available. + + If this command doesn't provide enough information, you can use the "gh api" command to view the full JSON response: + $ gh api /user/codespaces/ + `), + Example: heredoc.Doc(` + # select a codespace from a list of all codespaces you own + $ gh cs view + + # view the details of a specific codespace + $ gh cs view -c + + # select a codespace from a list of codespaces in a repository + $ gh cs view --repo / + + # select a codespace from a list of codespaces with a specific repository owner + $ gh cs view --repo-owner + + # view the list of all available fields for a codespace + $ gh cs view --json + + # view specific fields for a codespace + $ gh cs view --json displayName,machineDisplayName,state + `), + Args: noArgsConstraint, RunE: func(cmd *cobra.Command, args []string) error { - return app.ViewCodespace(cmd.Context(), opts, exporter) + return app.ViewCodespace(cmd.Context(), opts) }, } opts.selector = AddCodespaceSelector(viewCmd, app.apiClient) - cmdutil.AddJSONFlags(viewCmd, &exporter, api.ViewCodespaceFields) + cmdutil.AddJSONFlags(viewCmd, &opts.exporter, api.ViewCodespaceFields) return viewCmd } -func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions, exporter cmdutil.Exporter) error { - // If we are in a codespace, show the details for it - if codespaceName := os.Getenv("CODESPACE_NAME"); os.Getenv("CODESPACES") == "true" && codespaceName != "" { +func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions) error { + // If we are in a codespace and a codespace name wasn't provided, show the details for the codespace we are connected to + if (os.Getenv("CODESPACES") == "true") && opts.selector.codespaceName == "" { + codespaceName := os.Getenv("CODESPACE_NAME") opts.selector.codespaceName = codespaceName } @@ -50,8 +84,8 @@ func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions, exporter cmd } defer a.io.StopPager() - if exporter != nil { - return exporter.Write(a.io, selectedCodespace) + if opts.exporter != nil { + return opts.exporter.Write(a.io, selectedCodespace) } //nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter @@ -65,14 +99,14 @@ func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions, exporter cmd value string }{ {"Name", formattedName}, - {"Display Name", c.DisplayName}, {"State", c.State}, + {"Repository", c.Repository.FullName}, {"Git Status", formatGitStatus(c)}, {"Devcontainer Path", c.DevContainerPath}, {"Machine Display Name", c.Machine.DisplayName}, + {"Idle Timeout", fmt.Sprintf("%d minutes", c.IdleTimeoutMinutes)}, {"Created At", c.CreatedAt}, - {"Idle Timeout", strconv.Itoa(c.IdleTimeoutMinutes) + " minutes"}, - {"Retention Period", strconv.Itoa(c.RetentionPeriodMinutes/1440) + " days"}, + {"Retention Period", formatRetentionPeriodDays(c)}, } for _, field := range fields { @@ -94,5 +128,15 @@ func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions, exporter cmd func formatGitStatus(codespace codespace) string { branchWithGitStatus := codespace.branchWithGitStatus() - return fmt.Sprintf("%s \u25cf %d\u2193 %d\u2191", branchWithGitStatus, codespace.GitStatus.Behind, codespace.GitStatus.Ahead) + // u2193 and u2191 are the unicode arrows for down and up + return fmt.Sprintf("%s - %d\u2193 %d\u2191", branchWithGitStatus, codespace.GitStatus.Behind, codespace.GitStatus.Ahead) +} + +func formatRetentionPeriodDays(codespace codespace) string { + days := codespace.RetentionPeriodMinutes / minutesInDay + if days == 1 { + return "1 day" + } + + return fmt.Sprintf("%d days", days) } From 5b83d5518622f12d7257b595ccbcf590e06420fc Mon Sep 17 00:00:00 2001 From: David Gardiner Date: Tue, 30 May 2023 12:45:53 -0700 Subject: [PATCH 5/8] Don't display retention period if 0 days --- pkg/cmd/codespace/view.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/view.go b/pkg/cmd/codespace/view.go index 7a381280d..4ee43d6da 100644 --- a/pkg/cmd/codespace/view.go +++ b/pkg/cmd/codespace/view.go @@ -134,7 +134,10 @@ func formatGitStatus(codespace codespace) string { func formatRetentionPeriodDays(codespace codespace) string { days := codespace.RetentionPeriodMinutes / minutesInDay - if days == 1 { + // Don't display the retention period if it is 0 days + if days == 0 { + return "" + } else if days == 1 { return "1 day" } From 82650b364b75871594e53ff0c2dc015cb71d60c6 Mon Sep 17 00:00:00 2001 From: David Gardiner Date: Tue, 30 May 2023 12:53:46 -0700 Subject: [PATCH 6/8] Fix linting error --- pkg/cmd/codespace/view.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/cmd/codespace/view.go b/pkg/cmd/codespace/view.go index 4ee43d6da..11f772b71 100644 --- a/pkg/cmd/codespace/view.go +++ b/pkg/cmd/codespace/view.go @@ -19,8 +19,6 @@ const ( type viewOptions struct { selector *CodespaceSelector exporter cmdutil.Exporter - orgName string - userName string } func newViewCmd(app *App) *cobra.Command { From 15e1fa510dc1acd468af91c38750458d5e9ea136 Mon Sep 17 00:00:00 2001 From: David Gardiner Date: Tue, 30 May 2023 14:19:15 -0700 Subject: [PATCH 7/8] Add test --- pkg/cmd/codespace/view_test.go | 128 +++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 pkg/cmd/codespace/view_test.go diff --git a/pkg/cmd/codespace/view_test.go b/pkg/cmd/codespace/view_test.go new file mode 100644 index 000000000..549a53fc6 --- /dev/null +++ b/pkg/cmd/codespace/view_test.go @@ -0,0 +1,128 @@ +package codespace + +import ( + "context" + "fmt" + "testing" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/iostreams" +) + +func Test_NewCmdView(t *testing.T) { + type fields struct { + apiClient apiClient + } + tests := []struct { + tName string + codespaceName string + opts *viewOptions + cliArgs []string + wantErr bool + wantStdout string + errMsg string + }{ + { + tName: "selector throws because no terminal found", + opts: &viewOptions{}, + wantErr: true, + errMsg: "choosing codespace: error getting answers: no terminal", + }, + { + tName: "command fails because provided codespace doesn't exist", + codespaceName: "i-dont-exist", + opts: &viewOptions{}, + wantErr: true, + errMsg: "getting full codespace details: codespace not found", + }, + { + tName: "command succeeds because codespace exists (no details)", + codespaceName: "monalisa-cli-cli-abcdef", + opts: &viewOptions{}, + wantErr: false, + wantStdout: "Name\tmonalisa-cli-cli-abcdef\nGit Status\t - 0↓ 0↑\nIdle Timeout\t0 minutes\n", + }, + { + tName: "command succeeds because codespace exists (with details)", + codespaceName: "monalisa-cli-cli-hijklm", + opts: &viewOptions{}, + wantErr: false, + wantStdout: "Name\tmonalisa-cli-cli-hijklm\nGit Status\tmain* - 2↓ 1↑\nIdle Timeout\t30 minutes\nRetention Period\t1 day\n", + }, + } + + for _, tt := range tests { + t.Run(tt.tName, func(t *testing.T) { + ios, _, stdout, _ := iostreams.Test() + a := &App{ + apiClient: testViewApiMock(), + io: ios, + } + selector := &CodespaceSelector{api: a.apiClient, codespaceName: tt.codespaceName} + tt.opts.selector = selector + + var err error + if tt.cliArgs == nil { + if tt.opts.selector == nil { + t.Fatalf("selector must be set in opts if cliArgs are not provided") + } + + err = a.ViewCodespace(context.Background(), tt.opts) + } else { + cmd := newViewCmd(a) + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetOut(ios.ErrOut) + cmd.SetErr(ios.ErrOut) + cmd.SetArgs(tt.cliArgs) + _, err = cmd.ExecuteC() + } + + if tt.wantErr { + if err == nil { + t.Error("Edit() expected error, got nil") + } else if err.Error() != tt.errMsg { + t.Errorf("Edit() error = %q, want %q", err, tt.errMsg) + } + } else if err != nil { + t.Errorf("Edit() expected no error, got %v", err) + } + + if out := stdout.String(); out != tt.wantStdout { + t.Errorf("stdout = %q, want %q", out, tt.wantStdout) + } + }) + } +} + +func testViewApiMock() *apiClientMock { + codespaceWithNoDetails := &api.Codespace{ + Name: "monalisa-cli-cli-abcdef", + } + codespaceWithDetails := &api.Codespace{ + Name: "monalisa-cli-cli-hijklm", + GitStatus: api.CodespaceGitStatus{ + Ahead: 1, + Behind: 2, + Ref: "main", + HasUnpushedChanges: true, + HasUncommittedChanges: true, + }, + IdleTimeoutMinutes: 30, + RetentionPeriodMinutes: 1440, + } + return &apiClientMock{ + GetCodespaceFunc: func(_ context.Context, name string, _ bool) (*api.Codespace, error) { + if name == codespaceWithDetails.Name { + return codespaceWithDetails, nil + } else if name == codespaceWithNoDetails.Name { + return codespaceWithNoDetails, nil + } + + return nil, fmt.Errorf("codespace not found") + }, + ListCodespacesFunc: func(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error) { + return []*api.Codespace{codespaceWithNoDetails, codespaceWithDetails}, nil + }, + } +} From 290b802bf7c9e2ceef144e940177520aef2e6191 Mon Sep 17 00:00:00 2001 From: David Gardiner Date: Tue, 30 May 2023 14:22:12 -0700 Subject: [PATCH 8/8] Remove unused type --- pkg/cmd/codespace/view_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/cmd/codespace/view_test.go b/pkg/cmd/codespace/view_test.go index 549a53fc6..edcfefef6 100644 --- a/pkg/cmd/codespace/view_test.go +++ b/pkg/cmd/codespace/view_test.go @@ -10,9 +10,6 @@ import ( ) func Test_NewCmdView(t *testing.T) { - type fields struct { - apiClient apiClient - } tests := []struct { tName string codespaceName string