feat(control-plane): resolve repos by name with --project scope · Entire
feat(control-plane): resolve repos by name with --project scope
ec08c2b·
toothbrush·3w ago·4 files·+133 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
cmd/entire/cli
Mgrant.go+24/-7
Mrepo.go+24/-7
Mresolveref.go+48
Mresolveref_test.go+37
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",
// ...
}
}
func newGrantRepoListCmd() *cobra.Command {
return &cobra.Command{
Use: "list
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",
// ...
}
}
func newRepoGetCmd() *cobra.Command {
return &cobra.Command{
Use: "get
func newRepoDeleteCmd() *cobra.Command {
return &cobra.Command{
Use: "delete <repo>",
Short: "Delete a repository by name or ULID",
// ...
}
}
// 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")
}
// resolveRepoRef turns a repo reference into its ULID. A ULID passes through. // A name requires a project scope (projectRef, itself a name or ULID) because // repo names are unique only within a project: we list that project's repos and // match by name. 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. Matching is case-insensitive (EqualFold), consistent with
// pickOrg/pickProject.
func pickRepo(repos []coreapi.Repo, name string) (string, error) {
var matches []coreapi.Repo
for _, r := range repos {
if strings.EqualFold(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 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. Matching is