diff --git a/internal/cmd/stack/list.go b/internal/cmd/stack/list.go index bcaf7b5..32a3f19 100644 --- a/internal/cmd/stack/list.go +++ b/internal/cmd/stack/list.go @@ -3,12 +3,11 @@ package stack import ( "context" "fmt" - "sort" "strings" - "github.com/pkg/errors" + "github.com/shurcooL/graphql" + "github.com/spacelift-io/spacectl/client/structs" "github.com/spacelift-io/spacectl/internal/cmd" - "github.com/spacelift-io/spacectl/internal/cmd/authenticated" "github.com/urfave/cli/v2" ) @@ -23,49 +22,35 @@ func listStacks() cli.ActionFunc { case cmd.OutputFormatTable: return listStacksTable(cliCtx) case cmd.OutputFormatJSON: - return listStacksJSON(cliCtx.Context) + return listStacksJSON(cliCtx) } return fmt.Errorf("unknown output format: %v", outputFormat) } } -func listStacksJSON(ctx context.Context) error { - var query struct { - Stacks []stack `graphql:"stacks" json:"stacks,omitempty"` +func listStacksJSON(ctx *cli.Context) error { + stacks, err := searchAllStacks(ctx.Context, structs.SearchInput{ + First: graphql.NewInt(50), + }) + if err != nil { + return err } - if err := authenticated.Client.Query(ctx, &query, map[string]interface{}{}); err != nil { - return errors.Wrap(err, "failed to query list of stacks") - } - return cmd.OutputJSON(query.Stacks) + return cmd.OutputJSON(stacks) } func listStacksTable(ctx *cli.Context) error { - var query struct { - Stacks []struct { - ID string `graphql:"id" json:"id,omitempty"` - LockedBy string `graphql:"lockedBy"` - Name string `graphql:"name"` - State string `graphql:"state"` - Labels []string `graphql:"labels"` - TrackedCommit struct { - AuthorName string `graphql:"authorName"` - Hash string `graphql:"hash"` - } `graphql:"trackedCommit"` - WorkerPool struct { - Name string `graphql:"name"` - } `graphql:"workerPool"` - } `graphql:"stacks"` - } - - if err := authenticated.Client.Query(ctx.Context, &query, map[string]interface{}{}); err != nil { - return errors.Wrap(err, "failed to query list of stacks") - } - - sort.SliceStable(query.Stacks, func(i, j int) bool { - return strings.Compare(strings.ToLower(query.Stacks[i].Name), strings.ToLower(query.Stacks[j].Name)) < 0 + stacks, err := searchAllStacks(ctx.Context, structs.SearchInput{ + First: graphql.NewInt(50), + OrderBy: &structs.QueryOrder{ + Field: "starred", + Direction: "DESC", + }, }) + if err != nil { + return err + } columns := []string{"Name", "ID", "Commit", "Author", "State", "Worker Pool", "Locked By"} if ctx.Bool(flagShowLabels.Name) { @@ -73,18 +58,18 @@ func listStacksTable(ctx *cli.Context) error { } tableData := [][]string{columns} - for _, stack := range query.Stacks { + for _, s := range stacks { row := []string{ - stack.Name, - stack.ID, - cmd.HumanizeGitHash(stack.TrackedCommit.Hash), - stack.TrackedCommit.AuthorName, - stack.State, - stack.WorkerPool.Name, - stack.LockedBy, + s.Name, + s.ID, + cmd.HumanizeGitHash(s.TrackedCommit.Hash), + s.TrackedCommit.AuthorName, + s.State, + s.WorkerPool.Name, + s.LockedBy, } if ctx.Bool(flagShowLabels.Name) { - row = append(row, strings.Join(stack.Labels, ", ")) + row = append(row, strings.Join(s.Labels, ", ")) } tableData = append(tableData, row) @@ -93,6 +78,27 @@ func listStacksTable(ctx *cli.Context) error { return cmd.OutputTable(tableData, true) } +func searchAllStacks(ctx context.Context, input structs.SearchInput) ([]stack, error) { + out := []stack{} + + for { + result, err := searchStacks(ctx, input) + if err != nil { + return nil, err + } + + out = append(out, result.Stacks...) + + if result.PageInfo.HasNextPage { + input.After = graphql.NewString(graphql.String(result.PageInfo.EndCursor)) + } else { + break + } + } + + return out, nil +} + type stack struct { ID string `graphql:"id" json:"id,omitempty"` Administrative bool `graphql:"administrative" json:"administrative,omitempty"` diff --git a/internal/cmd/stack/open_command.go b/internal/cmd/stack/open_command.go index 807879a..eec44e8 100644 --- a/internal/cmd/stack/open_command.go +++ b/internal/cmd/stack/open_command.go @@ -163,7 +163,12 @@ type stackSearchParams struct { branch *string } -func searchStacks(ctx context.Context, p *stackSearchParams) ([]stack, error) { +type searchStacksResult struct { + Stacks []stack + PageInfo structs.PageInfo +} + +func searchStacks(ctx context.Context, input structs.SearchInput) (searchStacksResult, error) { var query struct { SearchStacksOutput struct { Edges []struct { @@ -172,52 +177,23 @@ func searchStacks(ctx context.Context, p *stackSearchParams) ([]stack, error) { PageInfo structs.PageInfo `graphql:"pageInfo"` } `graphql:"searchStacks(input: $input)"` } - conditions := []structs.QueryPredicate{ - { - Field: graphql.String("repository"), - Constraint: structs.QueryFieldConstraint{ - StringMatches: &[]graphql.String{graphql.String(p.repositoryName)}, - }, - }, - } - - if p.projectRoot != nil && *p.projectRoot != "" { - root := strings.TrimSuffix(*p.projectRoot, "/") - conditions = append(conditions, structs.QueryPredicate{ - Field: graphql.String("projectRoot"), - Constraint: structs.QueryFieldConstraint{ - StringMatches: &[]graphql.String{graphql.String(root), graphql.String(root + "/")}, - }, - }) - } - - if p.branch != nil { - conditions = append(conditions, structs.QueryPredicate{ - Field: graphql.String("branch"), - Constraint: structs.QueryFieldConstraint{ - StringMatches: &[]graphql.String{graphql.String(*p.branch)}, - }, - }) - } - - variables := map[string]interface{}{"input": structs.SearchInput{ - First: graphql.NewInt(graphql.Int(p.count)), //nolint: gosec - Predicates: &conditions, - }} if err := authenticated.Client.Query( ctx, &query, - variables, + map[string]interface{}{"input": input}, graphql.WithHeader("Spacelift-GraphQL-Query", "StacksPage"), ); err != nil { - return nil, errors.Wrap(err, "failed search for stacks") + return searchStacksResult{}, errors.Wrap(err, "failed search for stacks") } - result := make([]stack, 0) + stacks := make([]stack, 0) for _, q := range query.SearchStacksOutput.Edges { - result = append(result, q.Node) + stacks = append(stacks, q.Node) } - return result, nil + return searchStacksResult{ + Stacks: stacks, + PageInfo: query.SearchStacksOutput.PageInfo, + }, nil } diff --git a/internal/cmd/stack/stack_selector.go b/internal/cmd/stack/stack_selector.go index 4f2053a..02c4f2d 100644 --- a/internal/cmd/stack/stack_selector.go +++ b/internal/cmd/stack/stack_selector.go @@ -8,13 +8,14 @@ import ( "github.com/manifoldco/promptui" "github.com/pkg/errors" "github.com/shurcooL/graphql" + "github.com/spacelift-io/spacectl/client/structs" "github.com/spacelift-io/spacectl/internal/cmd/authenticated" "github.com/urfave/cli/v2" ) var errNoStackFound = errors.New("no stack found") -// getStackID will try to retreive a stack ID from multiple sources. +// getStackID will try to retrieve a stack ID from multiple sources. // It will do so in the following order: // 1. Check the --id flag, if set, use that value. // 2. Check the current directory to determine repository and subdirectory and search for a stack. @@ -90,16 +91,49 @@ func stackGetByID(ctx context.Context, stackID string) (*stack, error) { } func findAndSelectStack(ctx context.Context, p *stackSearchParams, forcePrompt bool) (*stack, error) { - stacks, err := searchStacks(ctx, p) + conditions := []structs.QueryPredicate{ + { + Field: graphql.String("repository"), + Constraint: structs.QueryFieldConstraint{ + StringMatches: &[]graphql.String{graphql.String(p.repositoryName)}, + }, + }, + } + + if p.projectRoot != nil && *p.projectRoot != "" { + root := strings.TrimSuffix(*p.projectRoot, "/") + conditions = append(conditions, structs.QueryPredicate{ + Field: "projectRoot", + Constraint: structs.QueryFieldConstraint{ + StringMatches: &[]graphql.String{graphql.String(root), graphql.String(root + "/")}, + }, + }) + } + + if p.branch != nil { + conditions = append(conditions, structs.QueryPredicate{ + Field: "branch", + Constraint: structs.QueryFieldConstraint{ + StringMatches: &[]graphql.String{graphql.String(*p.branch)}, + }, + }) + } + + input := structs.SearchInput{ + First: graphql.NewInt(graphql.Int(p.count)), //nolint: gosec + Predicates: &conditions, + } + + result, err := searchStacks(ctx, input) if err != nil { return nil, err } items := []string{} found := map[string]stack{} - for _, stack := range stacks { - items = append(items, stack.Name) - found[stack.Name] = stack + for _, s := range result.Stacks { + items = append(items, s.Name) + found[s.Name] = s } if len(found) == 0 {