From 54560c19dd9406ab5e1a80d44636498aadaf6618 Mon Sep 17 00:00:00 2001 From: David Gardiner Date: Fri, 26 May 2023 09:54:28 -0700 Subject: [PATCH] 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 +}