feat(control-plane): resolve repos by name with --project scope · Entire

feat(control-plane): resolve repos by name with --project scope

eb7fb56·

toothbrush·3w ago·4 files·+132 added/-14 removed

repo get/delete and grant repo add/list/remove now accept a repo name (not just a ULID) when --project <name|ULID> is given to scope the lookup — repo names are unique only within a project. A ULID still passes through unchanged and ignores --project.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

ab8e423ff72fView transcript

Changes

4

347 unmodified lines

func newGrantRepoAddCmd() *cobra.Command {
    var provider, providerUserID, role, granteeType string
    var provider, providerUserID, role, granteeType, project string
    cmd := &cobra.Command{
        Use:   "add <repo>",
        Short: "Grant access to a repo",

}

bindGranteeFlags(cmd, &provider, &providerUserID)
    cmd.Flags().StringVar(&role, "role", "", "repo role (required)")
    cmd.Flags().StringVar(&granteeType, "grantee-type", "", "grantee kind: account, org, or team (default account)")
    bindRepoProjectFlag(cmd, &project)
    markRequired(cmd, "role")
    return cmd
}

func newGrantRepoListCmd() *cobra.Command {
    return &cobra.Command{
        var project string
        cmd := &cobra.Command{
            Use:   "list <repo>",
            Short: "List repo grants",
            Args:  cobra.ExactArgs(1),
            RunE: func(cmd *cobra.Command, args []string) error {
                return runCoreList(cmd, projectGrantColumns, repoGrantRow, func(ctx context.Context, c *coreapi.Client) ([]coreapi.RepoGrant, error) {
                    out, err := c.ListRepoGrants(ctx, coreapi.ListRepoGrantsParams{RepoId: args[0]})
                    repoID, err := resolveRepoRef(ctx, c, args[0], project)
                    if err != nil {
                        return nil, err
                    }
                    out, err := c.ListRepoGrants(ctx, coreapi.ListRepoGrantsParams{RepoId: repoID})
                    if err != nil {
                        return nil, err
                    }
                })
            },
        }
        bindRepoProjectFlag(cmd, &project)
        return cmd
}

func newGrantRepoRemoveCmd() *cobra.Command {
    var granteeType, granteeID, provider, providerUserID string
    var granteeType, granteeID, provider, providerUserID, project string
    cmd := &cobra.Command{
        Use:   "remove <repo>",
        Short: "Revoke repo access from a grantee",
        
    }

return cmd
}

Mcmd/entire/cli/grant.go+24/-7

150 unmodified lines

func newRepoGetCmd() *cobra.Command {
    return &cobra.Command{
        var project string
        cmd := &cobra.Command{
            Use:   "get <repo>",
            Short: "Show a repository by ULID",
            Args:  cobra.ExactArgs(1),
            RunE: func(cmd *cobra.Command, args []string) error {
                return runCoreObject(cmd, repoColumns, repoRow, func(ctx context.Context, c *coreapi.Client) (*coreapi.Repo, error) {
                    sc, err := c.GetRepo(ctx, coreapi.GetRepoParams{RepoId: args[0]})
                    repoID, err := resolveRepoRef(ctx, c, args[0], project)
                    if err != nil {
                        return nil, err
                    }
                    return sc, nil
                })
        },
        }
        bindRepoProjectFlag(cmd, &project)
        return cmd
}

func newRepoDeleteCmd() *cobra.Command {
    return &cobra.Command{
        var project string
        cmd := &cobra.Command{
            Use:   "delete <repo>",
            Short: "Delete a repository by ULID",
            Args:  cobra.ExactArgs(1),
            RunE: func(cmd *cobra.Command, args []string) error {
                return runCore(cmd, func(ctx context.Context, c *coreapi.Client) error {
                    if err := c.DeleteRepo(ctx, coreapi.DeleteRepoParams{RepoId: args[0]}); err != nil {
                        repoID, err := resolveRepoRef(ctx, c, args[0], project)
                        if err != nil {
                            return err
                        }
                        if err := c.DeleteRepo(ctx, coreapi.DeleteRepoParams{RepoId: repoID}); err != nil {
                            return err
                        }
                        cmd.Printf("Deleted repo %s\n", args[0])
                    }
                })
        }
    }
    bindRepoProjectFlag(cmd, &project)
    return cmd
}

// bindRepoProjectFlag wires the shared --project scope used to resolve a repo
// addressed by name (a repo name is unique only within its project). Ignored
// when the repo arg is already a ULID.
func bindRepoProjectFlag(cmd *cobra.Command, project *string) {
    cmd.Flags().StringVar(project, "project", "", "owning project (name or ULID); required when <repo> is a name")
}

Mcmd/entire/cli/repo.go+24/-7

145 unmodified lines

func resolveRepoRef(ctx context.Context, c *coreapi.Client, ref, projectRef string) (string, error) {
    if looksLikeULID(ref) {
        return ref, nil
    }
    if projectRef == "" {
        return "", fmt.Errorf("repo %q is a name; pass --project <name|ULID> to resolve it, or use a repo ULID", ref)
    }
    projID, err := resolveProjectRef(ctx, c, projectRef)
    if err != nil {
        return "", err
    }
    out, err := c.ListProjectRepos(ctx, coreapi.ListProjectReposParams{ProjectId: projID})
    if err != nil {
        return "", err
    }
    return pickRepo(out.Repos, ref)
}

// pickRepo selects the single repo named name within an already-scoped project
// listing. Repo names are unique per project, so a name matches at most one;
// zero is an error pointing at `repo list`, and (defensively) multiple lists
// the colliding ids.
func pickRepo(repos []coreapi.Repo, name string) (string, error) {
    var matches []coreapi.Repo
    for _, r := range repos {
        if r.Name == name {
            matches = append(matches, r)
        }
    }
    switch len(matches) {
    case 1:
        return matches[0].ID, nil
    case 0:
        return "", fmt.Errorf("no repo named %q in that project (run `entire repo list <project>` to see names, or pass a ULID)", name)
    default:
        ids := make([]string, len(matches))
        for i, r := range matches {
            ids[i] = r.ID
        }
        return "", fmt.Errorf("repo name %q is ambiguous (%s); pass a ULID instead", name, strings.Join(ids, ", "))
    }
}

// filterProjectsByName narrows projects to exact name matches, returning all of
// them when name is empty. Used by `project list --org` to apply --name
// client-side, since the org-scoped list endpoint has no name parameter.

Mcmd/entire/cli/resolveref.go+47

142 unmodified lines

func TestPickRepo(t *testing.T) {
    t.Parallel()
    repos := []coreapi.Repo{
        {ID: "01J0REPO000000000000000001", Name: "api"},
        {ID: "01J0REPO000000000000000002", Name: "web"},
    }

t.Run("unique match", func(t *testing.T) {
        t.Parallel()
        got, err := pickRepo(repos, "web")
        if err != nil {
            t.Fatalf("pickRepo: %v", err)
        }
        if got != "01J0REPO000000000000000002" {
            t.Errorf("pickRepo = %q, want web id", got)
        }
    })

t.Run("no match", func(t *testing.T) {
        t.Parallel()
        if _, err := pickRepo(repos, "missing"); err == nil {
            t.Error("pickRepo expected error for unknown name")
        }
    })

t.Run("ambiguous", func(t *testing.T) {
        t.Parallel()
        dupes := []coreapi.Repo{
            {ID: "01J0REPO00000000000000000A", Name: "dup"},
            {ID: "01J0REPO00000000000000000B", Name: "dup"},
        }
        if _, err := pickRepo(dupes, "dup"); err == nil {
            t.Error("pickRepo expected error for ambiguous name")
        }
    })
}

func TestFilterProjectsByName(t *testing.T) {
    t.Parallel()
    projects := []coreapi.Project{