Add flag to list codespaces under an organization (#5807)

Co-authored-by: Mislav Marohnić <mislav@github.com>
This commit is contained in:
Greggory Rothmeier 2022-06-21 06:57:40 -07:00 committed by GitHub
parent 9b86fe41c0
commit 4b36dc031f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 174 additions and 30 deletions

View file

@ -270,13 +270,28 @@ func (c *Codespace) ExportData(fields []string) map[string]interface{} {
// ListCodespaces returns a list of codespaces for the user. Pass a negative limit to request all pages from
// the API until all codespaces have been fetched.
func (a *API) ListCodespaces(ctx context.Context, limit int) (codespaces []*Codespace, err error) {
func (a *API) ListCodespaces(ctx context.Context, limit int, orgName string, userName string) (codespaces []*Codespace, err error) {
perPage := 100
if limit > 0 && limit < 100 {
perPage = limit
}
listURL := fmt.Sprintf("%s/user/codespaces?per_page=%d", a.githubAPI, perPage)
var listURL string
var spanName string
if orgName != "" {
if userName != "" {
listURL = fmt.Sprintf("%s/orgs/%s/members/%s/codespaces?per_page=%d", a.githubAPI, orgName, userName, perPage)
spanName = "/orgs/*/members/*/codespaces"
} else {
listURL = fmt.Sprintf("%s/orgs/%s/codespaces?per_page=%d", a.githubAPI, orgName, perPage)
spanName = "/orgs/*/codespaces"
}
} else {
listURL = fmt.Sprintf("%s/user/codespaces?per_page=%d", a.githubAPI, perPage)
spanName = "/user/codespaces"
}
for {
req, err := http.NewRequest(http.MethodGet, listURL, nil)
if err != nil {
@ -284,7 +299,7 @@ func (a *API) ListCodespaces(ctx context.Context, limit int) (codespaces []*Code
}
a.setHeaders(req)
resp, err := a.do(ctx, req, "/user/codespaces")
resp, err := a.do(ctx, req, spanName)
if err != nil {
return nil, fmt.Errorf("error making request: %w", err)
}

View file

@ -140,7 +140,7 @@ func TestListCodespaces_limited(t *testing.T) {
client: &http.Client{},
}
ctx := context.TODO()
codespaces, err := api.ListCodespaces(ctx, 200)
codespaces, err := api.ListCodespaces(ctx, 200, "", "")
if err != nil {
t.Fatal(err)
}
@ -165,7 +165,7 @@ func TestListCodespaces_unlimited(t *testing.T) {
client: &http.Client{},
}
ctx := context.TODO()
codespaces, err := api.ListCodespaces(ctx, -1)
codespaces, err := api.ListCodespaces(ctx, -1, "", "")
if err != nil {
t.Fatal(err)
}

View file

@ -107,7 +107,7 @@ func startLiveShareSession(ctx context.Context, codespace *api.Codespace, a *App
type apiClient interface {
GetUser(ctx context.Context) (*api.User, error)
GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
ListCodespaces(ctx context.Context, limit int) ([]*api.Codespace, error)
ListCodespaces(ctx context.Context, limit int, orgName string, userName string) ([]*api.Codespace, error)
DeleteCodespace(ctx context.Context, name string) error
StartCodespace(ctx context.Context, name string) error
StopCodespace(ctx context.Context, name string) error
@ -125,7 +125,7 @@ type apiClient interface {
var errNoCodespaces = errors.New("you have no codespaces")
func chooseCodespace(ctx context.Context, apiClient apiClient) (*api.Codespace, error) {
codespaces, err := apiClient.ListCodespaces(ctx, -1)
codespaces, err := apiClient.ListCodespaces(ctx, -1, "", "")
if err != nil {
return nil, fmt.Errorf("error getting codespaces: %w", err)
}

View file

@ -63,7 +63,7 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
nameFilter := opts.codespaceName
if nameFilter == "" {
a.StartProgressIndicatorWithLabel("Fetching codespaces")
codespaces, err = a.apiClient.ListCodespaces(ctx, -1)
codespaces, err = a.apiClient.ListCodespaces(ctx, -1, "", "")
a.StopProgressIndicator()
if err != nil {
return fmt.Errorf("error getting codespaces: %w", err)

View file

@ -167,7 +167,7 @@ func TestDelete(t *testing.T) {
},
}
if tt.opts.codespaceName == "" {
apiMock.ListCodespacesFunc = func(_ context.Context, num int) ([]*api.Codespace, error) {
apiMock.ListCodespacesFunc = func(_ context.Context, num int, orgName string, userName string) ([]*api.Codespace, error) {
return tt.codespaces, nil
}
} else {

View file

@ -11,8 +11,14 @@ import (
"github.com/spf13/cobra"
)
type listOptions struct {
limit int
orgName string
userName string
}
func newListCmd(app *App) *cobra.Command {
var limit int
opts := &listOptions{}
var exporter cmdutil.Exporter
listCmd := &cobra.Command{
@ -21,23 +27,25 @@ func newListCmd(app *App) *cobra.Command {
Aliases: []string{"ls"},
Args: noArgsConstraint,
RunE: func(cmd *cobra.Command, args []string) error {
if limit < 1 {
return cmdutil.FlagErrorf("invalid limit: %v", limit)
if opts.limit < 1 {
return cmdutil.FlagErrorf("invalid limit: %v", opts.limit)
}
return app.List(cmd.Context(), limit, exporter)
return app.List(cmd.Context(), opts, exporter)
},
}
listCmd.Flags().IntVarP(&limit, "limit", "L", 30, "Maximum number of codespaces to list")
listCmd.Flags().IntVarP(&opts.limit, "limit", "L", 30, "Maximum number of codespaces to list")
listCmd.Flags().StringVarP(&opts.orgName, "org", "o", "", "List codespaces for an organization")
listCmd.Flags().StringVarP(&opts.userName, "user", "u", "", "Used with --org to filter to a specific user")
cmdutil.AddJSONFlags(listCmd, &exporter, api.CodespaceFields)
return listCmd
}
func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) error {
func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Exporter) error {
a.StartProgressIndicatorWithLabel("Fetching codespaces")
codespaces, err := a.apiClient.ListCodespaces(ctx, limit)
codespaces, err := a.apiClient.ListCodespaces(ctx, opts.limit, opts.orgName, opts.userName)
a.StopProgressIndicator()
if err != nil {
return fmt.Errorf("error getting codespaces: %w", err)
@ -68,6 +76,9 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er
if tp.IsTTY() {
tp.AddField("NAME", nil, nil)
tp.AddField("DISPLAY NAME", nil, nil)
if opts.orgName != "" {
tp.AddField("OWNER", nil, nil)
}
tp.AddField("REPOSITORY", nil, nil)
tp.AddField("BRANCH", nil, nil)
tp.AddField("STATE", nil, nil)
@ -104,6 +115,9 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er
tp.AddField(formattedName, nil, nameColor)
tp.AddField(c.DisplayName, nil, nil)
if opts.orgName != "" {
tp.AddField(c.Owner.Login, nil, nil)
}
tp.AddField(c.Repository.FullName, nil, nil)
tp.AddField(c.branchWithGitStatus(), nil, cs.Cyan)
if c.PendingOperation {

View file

@ -0,0 +1,103 @@
package codespace
import (
"context"
"fmt"
"testing"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
)
func TestApp_List(t *testing.T) {
type fields struct {
apiClient apiClient
}
tests := []struct {
name string
fields fields
opts *listOptions
}{
{
name: "list codespaces, no flags",
fields: fields{
apiClient: &apiClientMock{
ListCodespacesFunc: func(ctx context.Context, limit int, orgName string, userName string) ([]*api.Codespace, error) {
if orgName != "" {
return nil, fmt.Errorf("should not be called with an orgName")
}
return []*api.Codespace{
{
DisplayName: "CS1",
},
}, nil
},
},
},
opts: &listOptions{},
},
{
name: "list codespaces, --org flag",
fields: fields{
apiClient: &apiClientMock{
ListCodespacesFunc: func(ctx context.Context, limit int, orgName string, userName string) ([]*api.Codespace, error) {
if orgName != "TestOrg" {
return nil, fmt.Errorf("Expected orgName to be TestOrg. Got %s", orgName)
}
if userName != "" {
return nil, fmt.Errorf("Expected userName to be blank. Got %s", userName)
}
return []*api.Codespace{
{
DisplayName: "CS1",
},
}, nil
},
},
},
opts: &listOptions{
orgName: "TestOrg",
},
},
{
name: "list codespaces, --org and --user flag",
fields: fields{
apiClient: &apiClientMock{
ListCodespacesFunc: func(ctx context.Context, limit int, orgName string, userName string) ([]*api.Codespace, error) {
if orgName != "TestOrg" {
return nil, fmt.Errorf("Expected orgName to be TestOrg. Got %s", orgName)
}
if userName != "jimmy" {
return nil, fmt.Errorf("Expected userName to be jimmy. Got %s", userName)
}
return []*api.Codespace{
{
DisplayName: "CS1",
},
}, nil
},
},
},
opts: &listOptions{
orgName: "TestOrg",
userName: "jimmy",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
a := &App{
io: ios,
apiClient: tt.fields.apiClient,
}
var exporter cmdutil.Exporter
err := a.List(context.Background(), tt.opts, exporter)
if err != nil {
t.Error(err)
}
})
}
}

View file

@ -49,7 +49,7 @@ import (
// GetUserFunc: func(ctx context.Context) (*api.User, error) {
// panic("mock out the GetUser method")
// },
// ListCodespacesFunc: func(ctx context.Context, limit int) ([]*api.Codespace, error) {
// ListCodespacesFunc: func(ctx context.Context, limit int, orgName string, userName string) ([]*api.Codespace, error) {
// panic("mock out the ListCodespaces method")
// },
// ListDevContainersFunc: func(ctx context.Context, repoID int, branch string, limit int) ([]api.DevContainerEntry, error) {
@ -102,7 +102,7 @@ type apiClientMock struct {
GetUserFunc func(ctx context.Context) (*api.User, error)
// ListCodespacesFunc mocks the ListCodespaces method.
ListCodespacesFunc func(ctx context.Context, limit int) ([]*api.Codespace, error)
ListCodespacesFunc func(ctx context.Context, limit int, orgName string, userName string) ([]*api.Codespace, error)
// ListDevContainersFunc mocks the ListDevContainers method.
ListDevContainersFunc func(ctx context.Context, repoID int, branch string, limit int) ([]api.DevContainerEntry, error)
@ -208,6 +208,10 @@ type apiClientMock struct {
Ctx context.Context
// Limit is the limit argument value.
Limit int
// OrgName is the orgName argument value.
OrgName string
// UserName is the userName argument value.
UserName string
}
// ListDevContainers holds details about calls to the ListDevContainers method.
ListDevContainers []struct {
@ -658,33 +662,41 @@ func (mock *apiClientMock) GetUserCalls() []struct {
}
// ListCodespaces calls ListCodespacesFunc.
func (mock *apiClientMock) ListCodespaces(ctx context.Context, limit int) ([]*api.Codespace, error) {
func (mock *apiClientMock) ListCodespaces(ctx context.Context, limit int, orgName string, userName string) ([]*api.Codespace, error) {
if mock.ListCodespacesFunc == nil {
panic("apiClientMock.ListCodespacesFunc: method is nil but apiClient.ListCodespaces was just called")
}
callInfo := struct {
Ctx context.Context
Limit int
Ctx context.Context
Limit int
OrgName string
UserName string
}{
Ctx: ctx,
Limit: limit,
Ctx: ctx,
Limit: limit,
OrgName: orgName,
UserName: userName,
}
mock.lockListCodespaces.Lock()
mock.calls.ListCodespaces = append(mock.calls.ListCodespaces, callInfo)
mock.lockListCodespaces.Unlock()
return mock.ListCodespacesFunc(ctx, limit)
return mock.ListCodespacesFunc(ctx, limit, orgName, userName)
}
// ListCodespacesCalls gets all the calls that were made to ListCodespaces.
// Check the length with:
// len(mockedapiClient.ListCodespacesCalls())
func (mock *apiClientMock) ListCodespacesCalls() []struct {
Ctx context.Context
Limit int
Ctx context.Context
Limit int
OrgName string
UserName string
} {
var calls []struct {
Ctx context.Context
Limit int
Ctx context.Context
Limit int
OrgName string
UserName string
}
mock.lockListCodespaces.RLock()
calls = mock.calls.ListCodespaces

View file

@ -235,7 +235,7 @@ func (a *App) printOpenSSHConfig(ctx context.Context, opts sshOptions) (err erro
var csList []*api.Codespace
if opts.codespace == "" {
a.StartProgressIndicatorWithLabel("Fetching codespaces")
csList, err = a.apiClient.ListCodespaces(ctx, -1)
csList, err = a.apiClient.ListCodespaces(ctx, -1, "", "")
a.StopProgressIndicator()
} else {
var codespace *api.Codespace

View file

@ -28,7 +28,7 @@ func newStopCmd(app *App) *cobra.Command {
func (a *App) StopCodespace(ctx context.Context, codespaceName string) error {
if codespaceName == "" {
a.StartProgressIndicatorWithLabel("Fetching codespaces")
codespaces, err := a.apiClient.ListCodespaces(ctx, -1)
codespaces, err := a.apiClient.ListCodespaces(ctx, -1, "", "")
a.StopProgressIndicator()
if err != nil {
return fmt.Errorf("failed to list codespaces: %w", err)