99 "io"
1010 "os"
1111 "sort"
12+ "strings"
1213
1314 "github.com/AlecAivazis/survey/v2"
1415 "github.com/AlecAivazis/survey/v2/terminal"
@@ -55,6 +56,8 @@ func chooseCodespace(ctx context.Context, apiClient apiClient) (*api.Codespace,
5556 return chooseCodespaceFromList (ctx , codespaces )
5657}
5758
59+ // chooseCodespaceFromList returns the selected codespace from the list,
60+ // or an error if there are no codespaces.
5861func chooseCodespaceFromList (ctx context.Context , codespaces []* api.Codespace ) (* api.Codespace , error ) {
5962 if len (codespaces ) == 0 {
6063 return nil , errNoCodespaces
@@ -64,14 +67,47 @@ func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (
6467 return codespaces [i ].CreatedAt > codespaces [j ].CreatedAt
6568 })
6669
67- codespacesByName := make (map [string ]* api.Codespace )
70+ type codespaceWithIndex struct {
71+ cs codespace
72+ idx int
73+ }
74+
75+ namesWithConflict := make (map [string ]bool )
76+ codespacesByName := make (map [string ]codespaceWithIndex )
6877 codespacesNames := make ([]string , 0 , len (codespaces ))
69- for _ , codespace := range codespaces {
70- codespacesByName [codespace .Name ] = codespace
71- codespacesNames = append (codespacesNames , codespace .Name )
78+ for _ , apiCodespace := range codespaces {
79+ cs := codespace {apiCodespace }
80+ csName := cs .displayName (false , false )
81+ displayNameWithGitStatus := cs .displayName (false , true )
82+
83+ _ , hasExistingConflict := namesWithConflict [csName ]
84+ if seenCodespace , ok := codespacesByName [csName ]; ok || hasExistingConflict {
85+ // There is an existing codespace on the repo and branch.
86+ // We need to disambiguate by adding the codespace name
87+ // to the existing entry and the one we are processing now.
88+ if ! hasExistingConflict {
89+ fullDisplayName := seenCodespace .cs .displayName (true , false )
90+ fullDisplayNameWithGitStatus := seenCodespace .cs .displayName (true , true )
91+
92+ codespacesByName [fullDisplayName ] = codespaceWithIndex {seenCodespace .cs , seenCodespace .idx }
93+ codespacesNames [seenCodespace .idx ] = fullDisplayNameWithGitStatus
94+ delete (codespacesByName , csName ) // delete the existing map entry with old name
95+
96+ // All other codespaces with the same name should update
97+ // to their specific name, this tracks conflicting names going forward
98+ namesWithConflict [csName ] = true
99+ }
100+
101+ // update this codespace names to include the name to disambiguate
102+ csName = cs .displayName (true , false )
103+ displayNameWithGitStatus = cs .displayName (true , true )
104+ }
105+
106+ codespacesByName [csName ] = codespaceWithIndex {cs , len (codespacesNames )}
107+ codespacesNames = append (codespacesNames , displayNameWithGitStatus )
72108 }
73109
74- sshSurvey := []* survey.Question {
110+ csSurvey := []* survey.Question {
75111 {
76112 Name : "codespace" ,
77113 Prompt : & survey.Select {
@@ -86,12 +122,15 @@ func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (
86122 var answers struct {
87123 Codespace string
88124 }
89- if err := ask (sshSurvey , & answers ); err != nil {
125+ if err := ask (csSurvey , & answers ); err != nil {
90126 return nil , fmt .Errorf ("error getting answers: %w" , err )
91127 }
92128
93- codespace := codespacesByName [answers .Codespace ]
94- return codespace , nil
129+ // Codespaces are indexed without the git status included as compared
130+ // to how it is displayed in the prompt, so the git status symbol needs
131+ // cleaning up in case it is included.
132+ selectedCodespace := strings .Replace (answers .Codespace , gitStatusDirty , "" , - 1 )
133+ return codespacesByName [selectedCodespace ].cs .Codespace , nil
95134}
96135
97136// getOrChooseCodespace prompts the user to choose a codespace if the codespaceName is empty.
@@ -171,3 +210,44 @@ func noArgsConstraint(cmd *cobra.Command, args []string) error {
171210 }
172211 return nil
173212}
213+
214+ type codespace struct {
215+ * api.Codespace
216+ }
217+
218+ // displayName returns the repository nwo and branch.
219+ // If includeName is true, the name of the codespace is included.
220+ // If includeGitStatus is true, the branch will include a star if
221+ // the codespace has unsaved changes.
222+ func (c codespace ) displayName (includeName , includeGitStatus bool ) string {
223+ branch := c .Branch
224+ if includeGitStatus {
225+ branch = c .branchWithGitStatus ()
226+ }
227+
228+ if includeName {
229+ return fmt .Sprintf (
230+ "%s: %s [%s]" , c .RepositoryNWO , branch , c .Name ,
231+ )
232+ }
233+ return c .RepositoryNWO + ": " + branch
234+ }
235+
236+ // gitStatusDirty represents an unsaved changes status.
237+ const gitStatusDirty = "*"
238+
239+ // branchWithGitStatus returns the branch with a star
240+ // if the branch is currently being worked on.
241+ func (c codespace ) branchWithGitStatus () string {
242+ if c .hasUnsavedChanges () {
243+ return c .Branch + gitStatusDirty
244+ }
245+
246+ return c .Branch
247+ }
248+
249+ // hasUnsavedChanges returns whether the environment has
250+ // unsaved changes.
251+ func (c codespace ) hasUnsavedChanges () bool {
252+ return c .Environment .GitStatus .HasUncommitedChanges || c .Environment .GitStatus .HasUnpushedChanges
253+ }
0 commit comments