Change choose codespace prompt

- Repo + branch is favored
- Codespace name is included to disambiguate between two codespaces
- Move Codespace model out of out API into its own package
- Update call sites and group behavior under codespace.Codespace
This commit is contained in:
Jose Garcia 2021-10-05 11:23:12 -04:00
parent 93cea6d370
commit 975bd7c08a
12 changed files with 237 additions and 175 deletions

View file

@ -38,6 +38,7 @@ import (
"strings"
"time"
"github.com/cli/cli/v2/internal/codespaces/codespace"
"github.com/opentracing/opentracing-go"
)
@ -140,46 +141,7 @@ func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error
return &response, nil
}
type Codespace struct {
Name string `json:"name"`
GUID string `json:"guid"`
CreatedAt string `json:"created_at"`
LastUsedAt string `json:"last_used_at"`
Branch string `json:"branch"`
RepositoryName string `json:"repository_name"`
RepositoryNWO string `json:"repository_nwo"`
OwnerLogin string `json:"owner_login"`
Environment CodespaceEnvironment `json:"environment"`
}
type CodespaceEnvironment struct {
State string `json:"state"`
Connection CodespaceEnvironmentConnection `json:"connection"`
GitStatus CodespaceEnvironmentGitStatus `json:"gitStatus"`
}
type CodespaceEnvironmentGitStatus struct {
Ahead int `json:"ahead"`
Behind int `json:"behind"`
Branch string `json:"branch"`
Commit string `json:"commit"`
HasUnpushedChanges bool `json:"hasUnpushedChanges"`
HasUncommitedChanges bool `json:"hasUncommitedChanges"`
}
const (
CodespaceEnvironmentStateAvailable = "Available"
)
type CodespaceEnvironmentConnection struct {
SessionID string `json:"sessionId"`
SessionToken string `json:"sessionToken"`
RelayEndpoint string `json:"relayEndpoint"`
RelaySAS string `json:"relaySas"`
HostPublicKeys []string `json:"hostPublicKeys"`
}
func (a *API) ListCodespaces(ctx context.Context) ([]*Codespace, error) {
func (a *API) ListCodespaces(ctx context.Context) ([]*codespace.Codespace, error) {
req, err := http.NewRequest(
http.MethodGet, a.githubAPI+"/user/codespaces", nil,
)
@ -204,7 +166,7 @@ func (a *API) ListCodespaces(ctx context.Context) ([]*Codespace, error) {
}
var response struct {
Codespaces []*Codespace `json:"codespaces"`
Codespaces []*codespace.Codespace `json:"codespaces"`
}
if err := json.Unmarshal(b, &response); err != nil {
return nil, fmt.Errorf("error unmarshaling response: %w", err)
@ -267,10 +229,10 @@ func (a *API) GetCodespaceToken(ctx context.Context, ownerLogin, codespaceName s
return response.RepositoryToken, nil
}
func (a *API) GetCodespace(ctx context.Context, token, owner, codespace string) (*Codespace, error) {
func (a *API) GetCodespace(ctx context.Context, token, owner, codespaceName string) (*codespace.Codespace, error) {
req, err := http.NewRequest(
http.MethodGet,
a.githubAPI+"/vscs_internal/user/"+owner+"/codespaces/"+codespace,
a.githubAPI+"/vscs_internal/user/"+owner+"/codespaces/"+codespaceName,
nil,
)
if err != nil {
@ -294,7 +256,7 @@ func (a *API) GetCodespace(ctx context.Context, token, owner, codespace string)
return nil, jsonErrorResponse(b)
}
var response Codespace
var response codespace.Codespace
if err := json.Unmarshal(b, &response); err != nil {
return nil, fmt.Errorf("error unmarshaling response: %w", err)
}
@ -302,7 +264,7 @@ func (a *API) GetCodespace(ctx context.Context, token, owner, codespace string)
return &response, nil
}
func (a *API) StartCodespace(ctx context.Context, token string, codespace *Codespace) error {
func (a *API) StartCodespace(ctx context.Context, token string, codespace *codespace.Codespace) error {
req, err := http.NewRequest(
http.MethodPost,
a.githubAPI+"/vscs_internal/proxy/environments/"+codespace.GUID+"/start",
@ -429,7 +391,7 @@ type CreateCodespaceParams struct {
// CreateCodespace creates a codespace with the given parameters and returns a non-nil error if it
// fails to create.
func (a *API) CreateCodespace(ctx context.Context, params *CreateCodespaceParams) (*Codespace, error) {
func (a *API) CreateCodespace(ctx context.Context, params *CreateCodespaceParams) (*codespace.Codespace, error) {
codespace, err := a.startCreate(ctx, params.RepositoryID, params.Machine, params.Branch, params.Location)
if err != errProvisioningInProgress {
return codespace, err
@ -482,7 +444,7 @@ var errProvisioningInProgress = errors.New("provisioning in progress")
// It may return success or an error, or errProvisioningInProgress indicating that the operation
// did not complete before the GitHub API's time limit for RPCs (10s), in which case the caller
// must poll the server to learn the outcome.
func (a *API) startCreate(ctx context.Context, repoID int, machine, branch, location string) (*Codespace, error) {
func (a *API) startCreate(ctx context.Context, repoID int, machine, branch, location string) (*codespace.Codespace, error) {
requestBody, err := json.Marshal(startCreateRequest{repoID, branch, location, machine})
if err != nil {
return nil, fmt.Errorf("error marshaling request: %w", err)
@ -512,7 +474,7 @@ func (a *API) startCreate(ctx context.Context, repoID int, machine, branch, loca
return nil, errProvisioningInProgress // RPC finished before result of creation known
}
var response Codespace
var response codespace.Codespace
if err := json.Unmarshal(b, &response); err != nil {
return nil, fmt.Errorf("error unmarshaling response: %w", err)
}
@ -554,7 +516,7 @@ type getCodespaceRepositoryContentsResponse struct {
Content string `json:"content"`
}
func (a *API) GetCodespaceRepositoryContents(ctx context.Context, codespace *Codespace, path string) ([]byte, error) {
func (a *API) GetCodespaceRepositoryContents(ctx context.Context, codespace *codespace.Codespace, path string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, a.githubAPI+"/repos/"+codespace.RepositoryNWO+"/contents/"+path, nil)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)

View file

@ -7,10 +7,12 @@ import (
"net/http"
"net/http/httptest"
"testing"
"github.com/cli/cli/v2/internal/codespaces/codespace"
)
func TestListCodespaces(t *testing.T) {
codespaces := []*Codespace{
codespaces := []*codespace.Codespace{
{
Name: "testcodespace",
CreatedAt: "2021-08-09T10:10:24+02:00",
@ -19,7 +21,7 @@ func TestListCodespaces(t *testing.T) {
}
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
response := struct {
Codespaces []*Codespace `json:"codespaces"`
Codespaces []*codespace.Codespace `json:"codespaces"`
}{
Codespaces: codespaces,
}

View file

@ -0,0 +1,76 @@
package codespace
import "fmt"
type Codespace struct {
Name string `json:"name"`
CreatedAt string `json:"created_at"`
LastUsedAt string `json:"last_used_at"`
GUID string `json:"guid"`
Branch string `json:"branch"`
RepositoryName string `json:"repository_name"`
RepositoryNWO string `json:"repository_nwo"`
OwnerLogin string `json:"owner_login"`
Environment Environment `json:"environment"`
}
// DisplayName returns the repository nwo and branch.
// If includeName is true, the name of the codespace is included.
// If includeGitStatus is true, the branch will include a star if
// the codespace has unsaved changes.
func (c *Codespace) DisplayName(includeName, includeGitStatus bool) string {
branch := c.Branch
if includeGitStatus {
branch = c.BranchWithGitStatus()
}
if includeName {
return fmt.Sprintf(
"%s: %s [%s]", c.RepositoryNWO, branch, c.Name,
)
}
return c.RepositoryNWO + ": " + branch
}
// BranchWithGitStatus returns the branch with a star
// if the branch is currently being worked on.
func (c *Codespace) BranchWithGitStatus() string {
if c.HasUnsavedChanges() {
return c.Branch + "*"
}
return c.Branch
}
// HasUnsavedChanges returns whether the environment has
// unsaved changes.
func (c *Codespace) HasUnsavedChanges() bool {
return c.Environment.GitStatus.HasUncommitedChanges || c.Environment.GitStatus.HasUnpushedChanges
}
type Environment struct {
State string `json:"state"`
Connection EnvironmentConnection `json:"connection"`
GitStatus EnvironmentGitStatus `json:"gitStatus"`
}
type EnvironmentGitStatus struct {
Ahead int `json:"ahead"`
Behind int `json:"behind"`
Branch string `json:"branch"`
Commit string `json:"commit"`
HasUnpushedChanges bool `json:"hasUnpushedChanges"`
HasUncommitedChanges bool `json:"hasUncommitedChanges"`
}
const (
EnvironmentStateAvailable = "Available"
)
type EnvironmentConnection struct {
SessionID string `json:"sessionId"`
SessionToken string `json:"sessionToken"`
RelayEndpoint string `json:"relayEndpoint"`
RelaySAS string `json:"relaySas"`
HostPublicKeys []string `json:"hostPublicKeys"`
}

View file

@ -6,7 +6,7 @@ import (
"fmt"
"time"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/internal/codespaces/codespace"
"github.com/cli/cli/v2/pkg/liveshare"
)
@ -15,33 +15,33 @@ type logger interface {
Println(v ...interface{}) (int, error)
}
func connectionReady(codespace *api.Codespace) bool {
return codespace.Environment.Connection.SessionID != "" &&
codespace.Environment.Connection.SessionToken != "" &&
codespace.Environment.Connection.RelayEndpoint != "" &&
codespace.Environment.Connection.RelaySAS != "" &&
codespace.Environment.State == api.CodespaceEnvironmentStateAvailable
func connectionReady(cs *codespace.Codespace) bool {
return cs.Environment.Connection.SessionID != "" &&
cs.Environment.Connection.SessionToken != "" &&
cs.Environment.Connection.RelayEndpoint != "" &&
cs.Environment.Connection.RelaySAS != "" &&
cs.Environment.State == codespace.EnvironmentStateAvailable
}
type apiClient interface {
GetCodespace(ctx context.Context, token, user, name string) (*api.Codespace, error)
GetCodespace(ctx context.Context, token, user, name string) (*codespace.Codespace, error)
GetCodespaceToken(ctx context.Context, user, codespace string) (string, error)
StartCodespace(ctx context.Context, token string, codespace *api.Codespace) error
StartCodespace(ctx context.Context, token string, codespace *codespace.Codespace) error
}
// ConnectToLiveshare waits for a Codespace to become running,
// and connects to it using a Live Share session.
func ConnectToLiveshare(ctx context.Context, log logger, apiClient apiClient, userLogin, token string, codespace *api.Codespace) (*liveshare.Session, error) {
func ConnectToLiveshare(ctx context.Context, log logger, apiClient apiClient, userLogin, token string, cs *codespace.Codespace) (*liveshare.Session, error) {
var startedCodespace bool
if codespace.Environment.State != api.CodespaceEnvironmentStateAvailable {
if cs.Environment.State != codespace.EnvironmentStateAvailable {
startedCodespace = true
log.Print("Starting your codespace...")
if err := apiClient.StartCodespace(ctx, token, codespace); err != nil {
if err := apiClient.StartCodespace(ctx, token, cs); err != nil {
return nil, fmt.Errorf("error starting codespace: %w", err)
}
}
for retries := 0; !connectionReady(codespace); retries++ {
for retries := 0; !connectionReady(cs); retries++ {
if retries > 1 {
if retries%2 == 0 {
log.Print(".")
@ -55,7 +55,7 @@ func ConnectToLiveshare(ctx context.Context, log logger, apiClient apiClient, us
}
var err error
codespace, err = apiClient.GetCodespace(ctx, token, userLogin, codespace.Name)
cs, err = apiClient.GetCodespace(ctx, token, userLogin, cs.Name)
if err != nil {
return nil, fmt.Errorf("error getting codespace: %w", err)
}
@ -68,10 +68,10 @@ func ConnectToLiveshare(ctx context.Context, log logger, apiClient apiClient, us
log.Println("Connecting to your codespace...")
return liveshare.Connect(ctx, liveshare.Options{
SessionID: codespace.Environment.Connection.SessionID,
SessionToken: codespace.Environment.Connection.SessionToken,
RelaySAS: codespace.Environment.Connection.RelaySAS,
RelayEndpoint: codespace.Environment.Connection.RelayEndpoint,
HostPublicKeys: codespace.Environment.Connection.HostPublicKeys,
SessionID: cs.Environment.Connection.SessionID,
SessionToken: cs.Environment.Connection.SessionToken,
RelaySAS: cs.Environment.Connection.RelaySAS,
RelayEndpoint: cs.Environment.Connection.RelayEndpoint,
HostPublicKeys: cs.Environment.Connection.HostPublicKeys,
})
}

View file

@ -10,6 +10,7 @@ import (
"time"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/internal/codespaces/codespace"
"github.com/cli/cli/v2/pkg/liveshare"
)
@ -36,13 +37,13 @@ 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, log logger, apiClient apiClient, user *api.User, codespace *api.Codespace, poller func([]PostCreateState)) (err error) {
token, err := apiClient.GetCodespaceToken(ctx, user.Login, codespace.Name)
func PollPostCreateStates(ctx context.Context, log logger, apiClient apiClient, user *api.User, c *codespace.Codespace, poller func([]PostCreateState)) (err error) {
token, err := apiClient.GetCodespaceToken(ctx, user.Login, c.Name)
if err != nil {
return fmt.Errorf("getting codespace token: %w", err)
}
session, err := ConnectToLiveshare(ctx, log, apiClient, user.Login, token, codespace)
session, err := ConnectToLiveshare(ctx, log, apiClient, user.Login, token, c)
if err != nil {
return fmt.Errorf("connect to Live Share: %w", err)
}
@ -83,7 +84,7 @@ func PollPostCreateStates(ctx context.Context, log logger, apiClient apiClient,
return fmt.Errorf("connection failed: %w", err)
case <-t.C:
states, err := getPostCreateOutput(ctx, localPort, codespace, sshUser)
states, err := getPostCreateOutput(ctx, localPort, sshUser)
if err != nil {
return fmt.Errorf("get post create output: %w", err)
}
@ -93,7 +94,7 @@ func PollPostCreateStates(ctx context.Context, log logger, apiClient apiClient,
}
}
func getPostCreateOutput(ctx context.Context, tunnelPort int, codespace *api.Codespace, user string) ([]PostCreateState, error) {
func getPostCreateOutput(ctx context.Context, tunnelPort int, user string) ([]PostCreateState, error) {
cmd, err := NewRemoteCommand(
ctx, tunnelPort, fmt.Sprintf("%s@localhost", user),
"cat /workspaces/.codespaces/shared/postCreateOutput.json",

View file

@ -9,10 +9,12 @@ import (
"io"
"os"
"sort"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/internal/codespaces/codespace"
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
"github.com/spf13/cobra"
"golang.org/x/term"
@ -34,21 +36,21 @@ func NewApp(logger *output.Logger, apiClient apiClient) *App {
type apiClient interface {
GetUser(ctx context.Context) (*api.User, error)
GetCodespaceToken(ctx context.Context, user, name string) (string, error)
GetCodespace(ctx context.Context, token, user, name string) (*api.Codespace, error)
ListCodespaces(ctx context.Context) ([]*api.Codespace, error)
GetCodespace(ctx context.Context, token, user, name string) (*codespace.Codespace, error)
ListCodespaces(ctx context.Context) ([]*codespace.Codespace, error)
DeleteCodespace(ctx context.Context, user, name string) error
StartCodespace(ctx context.Context, token string, codespace *api.Codespace) error
CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error)
StartCodespace(ctx context.Context, token string, codespace *codespace.Codespace) error
CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*codespace.Codespace, error)
GetRepository(ctx context.Context, nwo string) (*api.Repository, error)
AuthorizedKeys(ctx context.Context, user string) ([]byte, error)
GetCodespaceRegionLocation(ctx context.Context) (string, error)
GetCodespacesSKUs(ctx context.Context, user *api.User, repository *api.Repository, branch, location string) ([]*api.SKU, error)
GetCodespaceRepositoryContents(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error)
GetCodespaceRepositoryContents(ctx context.Context, codespace *codespace.Codespace, path string) ([]byte, error)
}
var errNoCodespaces = errors.New("you have no codespaces")
func chooseCodespace(ctx context.Context, apiClient apiClient) (*api.Codespace, error) {
func chooseCodespace(ctx context.Context, apiClient apiClient) (*codespace.Codespace, error) {
codespaces, err := apiClient.ListCodespaces(ctx)
if err != nil {
return nil, fmt.Errorf("error getting codespaces: %w", err)
@ -56,7 +58,9 @@ func chooseCodespace(ctx context.Context, apiClient apiClient) (*api.Codespace,
return chooseCodespaceFromList(ctx, codespaces)
}
func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (*api.Codespace, error) {
// chooseCodespaceFromList returns the selected codespace from the list,
// or an error if there are no codespaces.
func chooseCodespaceFromList(ctx context.Context, codespaces []*codespace.Codespace) (*codespace.Codespace, error) {
if len(codespaces) == 0 {
return nil, errNoCodespaces
}
@ -65,14 +69,38 @@ func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (
return codespaces[i].CreatedAt > codespaces[j].CreatedAt
})
codespacesByName := make(map[string]*api.Codespace)
codespacesNames := make([]string, 0, len(codespaces))
for _, codespace := range codespaces {
codespacesByName[codespace.Name] = codespace
codespacesNames = append(codespacesNames, codespace.Name)
type codespaceWithIndex struct {
cs *codespace.Codespace
idx int
}
sshSurvey := []*survey.Question{
codespacesByName := make(map[string]codespaceWithIndex)
codespacesNames := make([]string, 0, len(codespaces))
for _, cs := range codespaces {
csName := cs.DisplayName(false, false)
displayNameWithGitStatus := cs.DisplayName(false, true)
if seenCodespace, ok := codespacesByName[csName]; ok {
// there is an existing codespace on the repo and branch
// we need to disambiguate by adding the codespace name
// to the existing entry and the one we are adding now
fullDisplayName := seenCodespace.cs.DisplayName(true, false)
fullDisplayNameWithGitStatus := seenCodespace.cs.DisplayName(true, true)
codespacesByName[fullDisplayName] = codespaceWithIndex{seenCodespace.cs, seenCodespace.idx}
codespacesNames[seenCodespace.idx] = fullDisplayNameWithGitStatus
delete(codespacesByName, csName) // delete the existing map entry with old name
// update this codespace names to include the name to disambiguate
csName = cs.DisplayName(true, false)
displayNameWithGitStatus = cs.DisplayName(true, true)
}
codespacesByName[csName] = codespaceWithIndex{cs, len(codespacesNames)}
codespacesNames = append(codespacesNames, displayNameWithGitStatus)
}
csSurvey := []*survey.Question{
{
Name: "codespace",
Prompt: &survey.Select{
@ -87,17 +115,18 @@ func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (
var answers struct {
Codespace string
}
if err := ask(sshSurvey, &answers); err != nil {
if err := ask(csSurvey, &answers); err != nil {
return nil, fmt.Errorf("error getting answers: %w", err)
}
codespace := codespacesByName[answers.Codespace]
selectedCodespace := strings.Replace(answers.Codespace, "*", "", -1)
codespace := codespacesByName[selectedCodespace].cs
return codespace, nil
}
// getOrChooseCodespace prompts the user to choose a codespace if the codespaceName is empty.
// It then fetches the codespace token and the codespace record.
func getOrChooseCodespace(ctx context.Context, apiClient apiClient, user *api.User, codespaceName string) (codespace *api.Codespace, token string, err error) {
func getOrChooseCodespace(ctx context.Context, apiClient apiClient, user *api.User, codespaceName string) (codespace *codespace.Codespace, token string, err error) {
if codespaceName == "" {
codespace, err = chooseCodespace(ctx, apiClient)
if err != nil {

View file

@ -10,6 +10,7 @@ import (
"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/internal/codespaces/codespace"
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
"github.com/fatih/camelcase"
"github.com/spf13/cobra"
@ -108,7 +109,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 showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, user *api.User, codespace *api.Codespace) error {
func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, user *api.User, cs *codespace.Codespace) error {
var lastState codespaces.PostCreateState
var breakNextState bool
@ -157,7 +158,7 @@ func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, us
}
}
err := codespaces.PollPostCreateStates(ctx, log, apiClient, user, codespace, poller)
err := codespaces.PollPostCreateStates(ctx, log, apiClient, user, cs, 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

View file

@ -8,7 +8,7 @@ import (
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/internal/codespaces/codespace"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
@ -64,7 +64,7 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) error {
return fmt.Errorf("error getting user: %w", err)
}
var codespaces []*api.Codespace
var codespaces []*codespace.Codespace
nameFilter := opts.codespaceName
if nameFilter == "" {
codespaces, err = a.apiClient.ListCodespaces(ctx)
@ -86,15 +86,15 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) error {
return fmt.Errorf("error getting codespace token: %w", err)
}
codespace, err := a.apiClient.GetCodespace(ctx, token, user.Login, nameFilter)
cs, err := a.apiClient.GetCodespace(ctx, token, user.Login, nameFilter)
if err != nil {
return fmt.Errorf("error fetching codespace information: %w", err)
}
codespaces = []*api.Codespace{codespace}
codespaces = []*codespace.Codespace{cs}
}
codespacesToDelete := make([]*api.Codespace, 0, len(codespaces))
codespacesToDelete := make([]*codespace.Codespace, 0, len(codespaces))
lastUpdatedCutoffTime := opts.now().AddDate(0, 0, -int(opts.keepDays))
for _, c := range codespaces {
if nameFilter != "" && c.Name != nameFilter {
@ -146,16 +146,14 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) error {
return nil
}
func confirmDeletion(p prompter, codespace *api.Codespace, isInteractive bool) (bool, error) {
gs := codespace.Environment.GitStatus
hasUnsavedChanges := gs.HasUncommitedChanges || gs.HasUnpushedChanges
if !hasUnsavedChanges {
func confirmDeletion(p prompter, cs *codespace.Codespace, isInteractive bool) (bool, error) {
if !cs.HasUnsavedChanges() {
return true, nil
}
if !isInteractive {
return false, fmt.Errorf("codespace %s has unsaved changes (use --force to override)", codespace.Name)
return false, fmt.Errorf("codespace %s has unsaved changes (use --force to override)", cs.Name)
}
return p.Confirm(fmt.Sprintf("Codespace %s has unsaved changes. OK to delete?", codespace.Name))
return p.Confirm(fmt.Sprintf("Codespace %s has unsaved changes. OK to delete?", cs.Name))
}
type surveyPrompter struct{}

View file

@ -12,6 +12,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/internal/codespaces/codespace"
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
)
@ -25,7 +26,7 @@ func TestDelete(t *testing.T) {
tests := []struct {
name string
opts deleteOptions
codespaces []*api.Codespace
codespaces []*codespace.Codespace
confirms map[string]bool
deleteErr error
wantErr bool
@ -38,7 +39,7 @@ func TestDelete(t *testing.T) {
opts: deleteOptions{
codespaceName: "hubot-robawt-abc",
},
codespaces: []*api.Codespace{
codespaces: []*codespace.Codespace{
{
Name: "hubot-robawt-abc",
},
@ -50,7 +51,7 @@ func TestDelete(t *testing.T) {
opts: deleteOptions{
repoFilter: "monalisa/spoon-knife",
},
codespaces: []*api.Codespace{
codespaces: []*codespace.Codespace{
{
Name: "monalisa-spoonknife-123",
RepositoryNWO: "monalisa/Spoon-Knife",
@ -72,7 +73,7 @@ func TestDelete(t *testing.T) {
deleteAll: true,
keepDays: 3,
},
codespaces: []*api.Codespace{
codespaces: []*codespace.Codespace{
{
Name: "monalisa-spoonknife-123",
LastUsedAt: daysAgo(1),
@ -93,7 +94,7 @@ func TestDelete(t *testing.T) {
opts: deleteOptions{
deleteAll: true,
},
codespaces: []*api.Codespace{
codespaces: []*codespace.Codespace{
{
Name: "monalisa-spoonknife-123",
},
@ -116,27 +117,27 @@ func TestDelete(t *testing.T) {
deleteAll: true,
skipConfirm: false,
},
codespaces: []*api.Codespace{
codespaces: []*codespace.Codespace{
{
Name: "monalisa-spoonknife-123",
Environment: api.CodespaceEnvironment{
GitStatus: api.CodespaceEnvironmentGitStatus{
Environment: codespace.Environment{
GitStatus: codespace.EnvironmentGitStatus{
HasUnpushedChanges: true,
},
},
},
{
Name: "hubot-robawt-abc",
Environment: api.CodespaceEnvironment{
GitStatus: api.CodespaceEnvironmentGitStatus{
Environment: codespace.Environment{
GitStatus: codespace.EnvironmentGitStatus{
HasUncommitedChanges: true,
},
},
},
{
Name: "monalisa-spoonknife-c4f3",
Environment: api.CodespaceEnvironment{
GitStatus: api.CodespaceEnvironmentGitStatus{
Environment: codespace.Environment{
GitStatus: codespace.EnvironmentGitStatus{
HasUnpushedChanges: false,
HasUncommitedChanges: false,
},
@ -167,7 +168,7 @@ func TestDelete(t *testing.T) {
},
}
if tt.opts.codespaceName == "" {
apiMock.ListCodespacesFunc = func(_ context.Context) ([]*api.Codespace, error) {
apiMock.ListCodespacesFunc = func(_ context.Context) ([]*codespace.Codespace, error) {
return tt.codespaces, nil
}
} else {
@ -177,7 +178,7 @@ func TestDelete(t *testing.T) {
}
return "CS_TOKEN", nil
}
apiMock.GetCodespaceFunc = func(_ context.Context, token, userLogin, name string) (*api.Codespace, error) {
apiMock.GetCodespaceFunc = func(_ context.Context, token, userLogin, name string) (*codespace.Codespace, error) {
if userLogin != user.Login {
return nil, fmt.Errorf("unexpected user %q", userLogin)
}

View file

@ -5,7 +5,6 @@ import (
"fmt"
"os"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
"github.com/spf13/cobra"
)
@ -39,7 +38,7 @@ func (a *App) List(ctx context.Context, asJSON bool) error {
table.Append([]string{
codespace.Name,
codespace.RepositoryNWO,
codespace.Branch + dirtyStar(codespace.Environment.GitStatus),
codespace.BranchWithGitStatus(),
codespace.Environment.State,
codespace.CreatedAt,
})
@ -48,11 +47,3 @@ func (a *App) List(ctx context.Context, asJSON bool) error {
table.Render()
return nil
}
func dirtyStar(status api.CodespaceEnvironmentGitStatus) string {
if status.HasUncommitedChanges || status.HasUnpushedChanges {
return "*"
}
return ""
}

View file

@ -8,6 +8,7 @@ import (
"sync"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/internal/codespaces/codespace"
)
// apiClientMock is a mock implementation of apiClient.
@ -19,19 +20,19 @@ import (
// AuthorizedKeysFunc: func(ctx context.Context, user string) ([]byte, error) {
// panic("mock out the AuthorizedKeys method")
// },
// CreateCodespaceFunc: func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) {
// CreateCodespaceFunc: func(ctx context.Context, params *api.CreateCodespaceParams) (*codespace.Codespace, error) {
// panic("mock out the CreateCodespace method")
// },
// DeleteCodespaceFunc: func(ctx context.Context, user string, name string) error {
// panic("mock out the DeleteCodespace method")
// },
// GetCodespaceFunc: func(ctx context.Context, token string, user string, name string) (*api.Codespace, error) {
// GetCodespaceFunc: func(ctx context.Context, token string, user string, name string) (*codespace.Codespace, error) {
// panic("mock out the GetCodespace method")
// },
// GetCodespaceRegionLocationFunc: func(ctx context.Context) (string, error) {
// panic("mock out the GetCodespaceRegionLocation method")
// },
// GetCodespaceRepositoryContentsFunc: func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) {
// GetCodespaceRepositoryContentsFunc: func(ctx context.Context, codespaceMoqParam *codespace.Codespace, path string) ([]byte, error) {
// panic("mock out the GetCodespaceRepositoryContents method")
// },
// GetCodespaceTokenFunc: func(ctx context.Context, user string, name string) (string, error) {
@ -46,10 +47,10 @@ import (
// GetUserFunc: func(ctx context.Context) (*api.User, error) {
// panic("mock out the GetUser method")
// },
// ListCodespacesFunc: func(ctx context.Context) ([]*api.Codespace, error) {
// ListCodespacesFunc: func(ctx context.Context) ([]*codespace.Codespace, error) {
// panic("mock out the ListCodespaces method")
// },
// StartCodespaceFunc: func(ctx context.Context, token string, codespace *api.Codespace) error {
// StartCodespaceFunc: func(ctx context.Context, token string, codespaceMoqParam *codespace.Codespace) error {
// panic("mock out the StartCodespace method")
// },
// }
@ -63,19 +64,19 @@ type apiClientMock struct {
AuthorizedKeysFunc func(ctx context.Context, user string) ([]byte, error)
// CreateCodespaceFunc mocks the CreateCodespace method.
CreateCodespaceFunc func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error)
CreateCodespaceFunc func(ctx context.Context, params *api.CreateCodespaceParams) (*codespace.Codespace, error)
// DeleteCodespaceFunc mocks the DeleteCodespace method.
DeleteCodespaceFunc func(ctx context.Context, user string, name string) error
// GetCodespaceFunc mocks the GetCodespace method.
GetCodespaceFunc func(ctx context.Context, token string, user string, name string) (*api.Codespace, error)
GetCodespaceFunc func(ctx context.Context, token string, user string, name string) (*codespace.Codespace, error)
// GetCodespaceRegionLocationFunc mocks the GetCodespaceRegionLocation method.
GetCodespaceRegionLocationFunc func(ctx context.Context) (string, error)
// GetCodespaceRepositoryContentsFunc mocks the GetCodespaceRepositoryContents method.
GetCodespaceRepositoryContentsFunc func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error)
GetCodespaceRepositoryContentsFunc func(ctx context.Context, codespaceMoqParam *codespace.Codespace, path string) ([]byte, error)
// GetCodespaceTokenFunc mocks the GetCodespaceToken method.
GetCodespaceTokenFunc func(ctx context.Context, user string, name string) (string, error)
@ -90,10 +91,10 @@ type apiClientMock struct {
GetUserFunc func(ctx context.Context) (*api.User, error)
// ListCodespacesFunc mocks the ListCodespaces method.
ListCodespacesFunc func(ctx context.Context) ([]*api.Codespace, error)
ListCodespacesFunc func(ctx context.Context) ([]*codespace.Codespace, error)
// StartCodespaceFunc mocks the StartCodespace method.
StartCodespaceFunc func(ctx context.Context, token string, codespace *api.Codespace) error
StartCodespaceFunc func(ctx context.Context, token string, codespaceMoqParam *codespace.Codespace) error
// calls tracks calls to the methods.
calls struct {
@ -140,8 +141,8 @@ type apiClientMock struct {
GetCodespaceRepositoryContents []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Codespace is the codespace argument value.
Codespace *api.Codespace
// CodespaceMoqParam is the codespaceMoqParam argument value.
CodespaceMoqParam *codespace.Codespace
// Path is the path argument value.
Path string
}
@ -190,8 +191,8 @@ type apiClientMock struct {
Ctx context.Context
// Token is the token argument value.
Token string
// Codespace is the codespace argument value.
Codespace *api.Codespace
// CodespaceMoqParam is the codespaceMoqParam argument value.
CodespaceMoqParam *codespace.Codespace
}
}
lockAuthorizedKeys sync.RWMutex
@ -244,7 +245,7 @@ func (mock *apiClientMock) AuthorizedKeysCalls() []struct {
}
// CreateCodespace calls CreateCodespaceFunc.
func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) {
func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*codespace.Codespace, error) {
if mock.CreateCodespaceFunc == nil {
panic("apiClientMock.CreateCodespaceFunc: method is nil but apiClient.CreateCodespace was just called")
}
@ -318,7 +319,7 @@ func (mock *apiClientMock) DeleteCodespaceCalls() []struct {
}
// GetCodespace calls GetCodespaceFunc.
func (mock *apiClientMock) GetCodespace(ctx context.Context, token string, user string, name string) (*api.Codespace, error) {
func (mock *apiClientMock) GetCodespace(ctx context.Context, token string, user string, name string) (*codespace.Codespace, error) {
if mock.GetCodespaceFunc == nil {
panic("apiClientMock.GetCodespaceFunc: method is nil but apiClient.GetCodespace was just called")
}
@ -392,37 +393,37 @@ func (mock *apiClientMock) GetCodespaceRegionLocationCalls() []struct {
}
// GetCodespaceRepositoryContents calls GetCodespaceRepositoryContentsFunc.
func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) {
func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, codespaceMoqParam *codespace.Codespace, path string) ([]byte, error) {
if mock.GetCodespaceRepositoryContentsFunc == nil {
panic("apiClientMock.GetCodespaceRepositoryContentsFunc: method is nil but apiClient.GetCodespaceRepositoryContents was just called")
}
callInfo := struct {
Ctx context.Context
Codespace *api.Codespace
Path string
Ctx context.Context
CodespaceMoqParam *codespace.Codespace
Path string
}{
Ctx: ctx,
Codespace: codespace,
Path: path,
Ctx: ctx,
CodespaceMoqParam: codespaceMoqParam,
Path: path,
}
mock.lockGetCodespaceRepositoryContents.Lock()
mock.calls.GetCodespaceRepositoryContents = append(mock.calls.GetCodespaceRepositoryContents, callInfo)
mock.lockGetCodespaceRepositoryContents.Unlock()
return mock.GetCodespaceRepositoryContentsFunc(ctx, codespace, path)
return mock.GetCodespaceRepositoryContentsFunc(ctx, codespaceMoqParam, path)
}
// GetCodespaceRepositoryContentsCalls gets all the calls that were made to GetCodespaceRepositoryContents.
// Check the length with:
// len(mockedapiClient.GetCodespaceRepositoryContentsCalls())
func (mock *apiClientMock) GetCodespaceRepositoryContentsCalls() []struct {
Ctx context.Context
Codespace *api.Codespace
Path string
Ctx context.Context
CodespaceMoqParam *codespace.Codespace
Path string
} {
var calls []struct {
Ctx context.Context
Codespace *api.Codespace
Path string
Ctx context.Context
CodespaceMoqParam *codespace.Codespace
Path string
}
mock.lockGetCodespaceRepositoryContents.RLock()
calls = mock.calls.GetCodespaceRepositoryContents
@ -583,7 +584,7 @@ func (mock *apiClientMock) GetUserCalls() []struct {
}
// ListCodespaces calls ListCodespacesFunc.
func (mock *apiClientMock) ListCodespaces(ctx context.Context) ([]*api.Codespace, error) {
func (mock *apiClientMock) ListCodespaces(ctx context.Context) ([]*codespace.Codespace, error) {
if mock.ListCodespacesFunc == nil {
panic("apiClientMock.ListCodespacesFunc: method is nil but apiClient.ListCodespaces was just called")
}
@ -614,37 +615,37 @@ func (mock *apiClientMock) ListCodespacesCalls() []struct {
}
// StartCodespace calls StartCodespaceFunc.
func (mock *apiClientMock) StartCodespace(ctx context.Context, token string, codespace *api.Codespace) error {
func (mock *apiClientMock) StartCodespace(ctx context.Context, token string, codespaceMoqParam *codespace.Codespace) error {
if mock.StartCodespaceFunc == nil {
panic("apiClientMock.StartCodespaceFunc: method is nil but apiClient.StartCodespace was just called")
}
callInfo := struct {
Ctx context.Context
Token string
Codespace *api.Codespace
Ctx context.Context
Token string
CodespaceMoqParam *codespace.Codespace
}{
Ctx: ctx,
Token: token,
Codespace: codespace,
Ctx: ctx,
Token: token,
CodespaceMoqParam: codespaceMoqParam,
}
mock.lockStartCodespace.Lock()
mock.calls.StartCodespace = append(mock.calls.StartCodespace, callInfo)
mock.lockStartCodespace.Unlock()
return mock.StartCodespaceFunc(ctx, token, codespace)
return mock.StartCodespaceFunc(ctx, token, codespaceMoqParam)
}
// StartCodespaceCalls gets all the calls that were made to StartCodespace.
// Check the length with:
// len(mockedapiClient.StartCodespaceCalls())
func (mock *apiClientMock) StartCodespaceCalls() []struct {
Ctx context.Context
Token string
Codespace *api.Codespace
Ctx context.Context
Token string
CodespaceMoqParam *codespace.Codespace
} {
var calls []struct {
Ctx context.Context
Token string
Codespace *api.Codespace
Ctx context.Context
Token string
CodespaceMoqParam *codespace.Codespace
}
mock.lockStartCodespace.RLock()
calls = mock.calls.StartCodespace

View file

@ -12,7 +12,7 @@ import (
"strings"
"github.com/cli/cli/v2/internal/codespaces"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/internal/codespaces/codespace"
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
"github.com/cli/cli/v2/pkg/liveshare"
"github.com/muhammadmuzzammil1998/jsonc"
@ -119,7 +119,7 @@ type portAttribute struct {
Label string `json:"label"`
}
func getDevContainer(ctx context.Context, apiClient apiClient, codespace *api.Codespace) <-chan devContainerResult {
func getDevContainer(ctx context.Context, apiClient apiClient, codespace *codespace.Codespace) <-chan devContainerResult {
ch := make(chan devContainerResult, 1)
go func() {
contents, err := apiClient.GetCodespaceRepositoryContents(ctx, codespace, ".devcontainer/devcontainer.json")