Files
gitea-tea/internal/app/teams.go
T

664 lines
26 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package app
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"strconv"
"strings"
"gitea.dev/tea/internal/clierr"
"gitea.dev/tea/internal/services"
"gitea.dev/tea/internal/terminal"
"github.com/spf13/cobra"
)
// teamArguments describes the <team> argument in help.
const teamArguments = `A team can be supplied as ORG/TEAM, such as acme/developers, where TEAM is the team's
name or ID, or as its numeric ID alone.`
// teamRef is a parsed <team> argument: ORG/TEAM, or a team ID.
type teamRef struct {
org, name string
id int64
}
func parseTeam(raw string) (teamRef, error) {
raw = strings.TrimSpace(raw)
if org, name, scoped := strings.Cut(raw, "/"); scoped {
if org != "" && name != "" && !strings.Contains(name, "/") {
return teamRef{org: org, name: name}, nil
}
} else if id, err := strconv.ParseInt(strings.TrimPrefix(raw, "#"), 10, 64); err == nil && id > 0 {
return teamRef{id: id}, nil
}
return teamRef{}, clierr.Wrap(clierr.Usage, fmt.Errorf("invalid team %q: use ORG/TEAM, such as acme/developers, or a team ID", raw))
}
func (r teamRef) find(ctx context.Context, service services.Teams) (services.Team, error) {
if r.org != "" {
return service.Find(ctx, r.org, r.name)
}
return service.View(ctx, r.id)
}
// team resolves the target and the team that raw addresses.
func (a *application) team(cmd *cobra.Command, raw string) (target, services.Teams, services.Team, error) {
ref, err := parseTeam(raw)
if err != nil {
return target{}, services.Teams{}, services.Team{}, err
}
t, err := a.resolve(cmd.Context(), false)
if err != nil {
return t, services.Teams{}, services.Team{}, err
}
service := services.Teams{Client: t.client}
team, err := ref.find(cmd.Context(), service)
return t, service, team, err
}
func teamURL(t target, team services.Team) string {
return webURL(t, "org", team.Organization, "teams", strings.ToLower(team.Name))
}
// completeTeamThen completes a team, then the values that next lists for that team.
func (a *application) completeTeamThen(next func(ctx context.Context, t target, team services.Team) ([]cobra.Completion, error)) cobra.CompletionFunc {
teams := a.completeTeams()
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) == 0 {
return teams(cmd, args, toComplete)
}
return a.apiCompletion(false, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
ref, err := parseTeam(args[0])
if err != nil {
return nil, err
}
team, err := ref.find(ctx, services.Teams{Client: t.client})
if err != nil {
return nil, err
}
return next(ctx, t, team)
})(cmd, args, toComplete)
}
}
func (a *application) teamCommand() *cobra.Command {
group := &cobra.Command{Use: "team", Short: "Manage organization teams and access", Long: `Manage the teams of an organization: what they may do in its repositories, who belongs to
them, and which repositories they can reach.`}
group.AddCommand(a.teamListCommand(), a.teamViewCommand(), a.teamCreateCommand(), a.teamEditCommand(), a.teamDeleteCommand(), a.teamMemberCommand(), a.teamRepoCommand())
return group
}
func (a *application) teamListCommand() *cobra.Command {
var limit, page int
cmd := &cobra.Command{
Use: "list <org>", Short: "List organization teams", Args: argsExact(1), ValidArgsFunction: a.completeOrgs(),
Long: "List the teams of an organization with their access to its repositories and their sizes.",
Example: `$ tea team list acme
# print the team names
$ tea team list acme --jq '.[].name'`,
RunE: func(cmd *cobra.Command, args []string) error {
target, err := a.resolve(cmd.Context(), false)
if err != nil {
return err
}
org := args[0]
service := services.Teams{Client: target.client}
teams, err := service.List(cmd.Context(), org, limit, page)
if err != nil {
return err
}
sizes := map[int64]services.TeamSize{}
if a.options.json == "" {
counted, err := service.Sizes(cmd.Context(), teams.Items)
if err != nil {
return err
}
for i, team := range teams.Items {
sizes[team.ID] = counted[i]
}
}
return writeTable(a, cmd, teams.Items, listSpec[services.Team]{
Columns: []terminal.Column{{Header: "ID"}, {Header: "Name"}, {Header: "Permission", Flex: true}, {Header: "Members"}, {Header: "Repos"}},
Row: func(team services.Team) []terminal.Cell {
size, repos := sizes[team.ID], "all"
if !team.AllRepositories {
repos = countText(size.Repositories)
}
return []terminal.Cell{{Text: strconv.FormatInt(team.ID, 10), Link: teamURL(target, team)}, {Text: team.Name, Bold: true}, {Text: team.Access()}, {Text: countText(size.Members)}, {Text: repos}}
},
Context: "teams in " + org, Empty: "no teams in " + org,
}, servicePage(teams, page))
},
}
pageFlags(cmd, &limit, &page)
return cmd
}
// countText shows a count, or nothing when it is unknown.
func countText(n *int) string {
if n == nil {
return ""
}
return strconv.Itoa(*n)
}
// listedNames shows the first names of a listing and how many more there are.
func listedNames[T any](page services.Page[T], name func(T) string) string {
names := make([]string, len(page.Items))
for i, item := range page.Items {
names[i] = name(item)
}
text := strings.Join(names, ", ")
switch {
case len(names) == 0:
text = "none"
case page.Counted && page.Total > len(names):
text += fmt.Sprintf(" and %d more", page.Total-len(names))
case page.More:
text += " and more"
}
return text
}
// teamViewNames is how many members and repositories a team view lists.
const teamViewNames = 20
func (a *application) teamViewCommand() *cobra.Command {
return &cobra.Command{
Use: "view <team>", Short: "View a team", Args: argsExact(1), ValidArgsFunction: a.completeTeams(),
Long: "View a team: its access to each repository unit, its members and its repositories.",
Example: `$ tea team view acme/developers
$ tea team view 2 --json units`,
Annotations: map[string]string{"tea.arguments": teamArguments},
RunE: func(cmd *cobra.Command, args []string) error {
target, service, team, err := a.team(cmd, args[0])
if err != nil {
return err
}
if a.options.json != "" {
return a.writeJSON(cmd, team)
}
members, err := service.Members(cmd.Context(), team.ID, teamViewNames, 1)
if err != nil {
return err
}
repos, err := service.Repositories(cmd.Context(), team.ID, teamViewNames, 1)
if err != nil {
return err
}
return writeView(a, cmd, team, func(team services.Team) terminal.View {
var units []string
for _, unit := range slices.Concat(services.TeamUnits, services.ExternalTeamUnits) {
if access := team.Units[unit]; access != "" && access != "none" {
units = append(units, strings.TrimPrefix(unit, "repo.")+": "+access)
}
}
repositories := listedNames(repos, func(repo services.Repository) string { return repo.FullName })
if team.AllRepositories {
repositories = "all repositories of " + team.Organization
}
return terminal.View{
Title: team.Slug(), ID: fmt.Sprintf("(ID %d)", team.ID), Badges: []terminal.Cell{{Text: team.Access(), Bold: true}},
Fields: []terminal.Field{
{Label: "Access", Value: team.Access(), PipedOnly: true},
{Label: "Units", Value: strings.Join(units, ", ")},
{Label: "Members", Value: listedNames(members, func(member services.OrganizationMember) string { return member.Username })},
{Label: "Repositories", Value: repositories},
{Label: "Can create repos", Value: yesNo(team.CanCreateRepo)},
},
Body: team.Description, Kind: "team", URL: teamURL(target, team),
}
})
},
}
}
var teamPermissions = []string{"read", "write", "admin"}
// teamFlags are the fields that team create and edit set.
type teamFlags struct {
name, description, permission string
units map[string]string
createRepo, allRepos bool
}
// register adds the flags to cmd. Edit flags have no defaults, so that only what is passed changes.
func (f *teamFlags) register(cmd *cobra.Command, create bool) {
cmd.Flags().StringVar(&f.name, "name", "", "Team name")
cmd.Flags().StringVarP(&f.description, "description", "d", "", "Team description")
initial, permissionUsage := "", "Access to every repository unit; admin also manages repositories"
if create {
initial = "read"
}
enumFlag(cmd, &f.permission, "permission", "", initial, permissionUsage, teamPermissions...)
unitUsage := "Unit access, such as repo.code=write,repo.wiki=read; overrides --permission per unit"
if !create {
unitUsage = "Unit access to change, such as repo.wiki=write; merged into the current policy"
}
cmd.Flags().StringToStringVar(&f.units, "unit", nil, unitUsage)
_ = cmd.RegisterFlagCompletionFunc("unit", completeTeamUnits)
cmd.Flags().BoolVar(&f.createRepo, "can-create-repo", false, "Allow creating organization repositories (use =false to revoke)")
cmd.Flags().BoolVar(&f.allRepos, "all-repositories", false, "Access every organization repository (use =false to revoke)")
}
// unitPolicy validates --unit and returns the unit policy to send: all units at the --permission
// level when it is passed (or, on create, without --unit), with --unit overriding single units.
// Administrator teams get no policy, since they can do everything.
func (f *teamFlags) unitPolicy(cmd *cobra.Command, create bool) (map[string]string, error) {
changed := cmd.Flags().Changed("unit")
if changed && len(f.units) == 0 {
return nil, clierr.Wrap(clierr.Usage, errors.New("--unit needs at least one unit, such as repo.code=write"))
}
if changed && f.permission == "admin" {
return nil, clierr.Wrap(clierr.Usage, errors.New("--unit cannot restrict administrator access"))
}
for unit, access := range f.units {
levels := services.UnitAccess(unit)
if levels == nil {
return nil, clierr.Wrap(clierr.Usage, fmt.Errorf("unknown unit %q; use one of %s", unit, strings.Join(slices.Concat(services.TeamUnits, services.ExternalTeamUnits), ", ")))
}
if !slices.Contains(levels, access) {
return nil, clierr.Wrap(clierr.Usage, fmt.Errorf("--unit %s=%s: access must be %s", unit, access, strings.Join(levels, ", ")))
}
}
var policy map[string]string
switch {
case f.permission == "admin":
return nil, nil
case cmd.Flags().Changed("permission") || (create && !changed):
policy = services.UnitPolicy(f.permission)
case changed:
policy = map[string]string{}
default:
return nil, nil
}
maps.Copy(policy, f.units)
return policy, nil
}
func (f *teamFlags) options(cmd *cobra.Command) services.TeamOptions {
opts := services.TeamOptions{}
for _, field := range []struct {
flag string
value *string
option **string
}{{"description", &f.description, &opts.Description}, {"permission", &f.permission, &opts.Permission}} {
if cmd.Flags().Changed(field.flag) {
*field.option = field.value
}
}
for _, field := range []struct {
flag string
value *bool
option **bool
}{{"can-create-repo", &f.createRepo, &opts.CanCreateRepo}, {"all-repositories", &f.allRepos, &opts.AllRepositories}} {
if cmd.Flags().Changed(field.flag) {
*field.option = field.value
}
}
return opts
}
// completeTeamUnits completes --unit as comma-separated UNIT=ACCESS entries.
var completeTeamUnits = commaSeparated(func(_ *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if unit, _, found := strings.Cut(toComplete, "="); found {
levels := services.UnitAccess(unit)
values := make([]string, len(levels))
for i, level := range levels {
values[i] = unit + "=" + level
}
return matching(values, toComplete), cobra.ShellCompDirectiveNoFileComp
}
units := slices.Concat(services.TeamUnits, services.ExternalTeamUnits)
for i, unit := range units {
units[i] = unit + "="
}
return matching(units, toComplete), cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
})
func (a *application) teamCreateCommand() *cobra.Command {
var flags teamFlags
cmd := &cobra.Command{
Use: "create <org> [<name>]", Short: "Create a team", Args: usageArgs(cobra.RangeArgs(1, 2)),
Long: `Create a team in an organization. Name it as the second argument, as ORG/NAME, or with --name.
--permission grants read or write access to every repository unit, or admin access. Use --unit
to set single units instead, or to override some of them.`,
Example: `$ tea team create acme developers --permission write
$ tea team create acme/reviewers --unit repo.code=read,repo.pulls=write
# give the team access to every repository of the organization
$ tea team create acme ops --permission admin --all-repositories`,
ValidArgsFunction: firstArg(a.completeOrgs()),
RunE: func(cmd *cobra.Command, args []string) error {
org, name, scoped := strings.Cut(args[0], "/")
names := 0
for _, given := range []bool{scoped, len(args) > 1, cmd.Flags().Changed("name")} {
if given {
names++
}
}
if names > 1 {
return clierr.Wrap(clierr.Usage, errors.New("supply the team name once: as ORG/NAME, as an argument, or with --name"))
}
if len(args) > 1 {
name = args[1]
}
if cmd.Flags().Changed("name") {
name = flags.name
}
units, err := flags.unitPolicy(cmd, true)
if err != nil {
return err
}
if _, err := a.requireInput(cmd, &name, "name", "Team name", nil); err != nil {
return err
}
opts := flags.options(cmd)
opts.Name, opts.Permission, opts.CanCreateRepo, opts.AllRepositories, opts.Units = &name, &flags.permission, &flags.createRepo, &flags.allRepos, units
target, err := a.resolve(cmd.Context(), false)
if err != nil {
return err
}
team, err := (services.Teams{Client: target.client}).Create(cmd.Context(), org, opts)
if err != nil {
return err
}
a.done(cmd, "Created team %s (ID %d) with %s access", team.Slug(), team.ID, team.Access())
return a.created(cmd, team, teamURL(target, team))
},
}
flags.register(cmd, true)
return cmd
}
func (a *application) teamEditCommand() *cobra.Command {
var flags teamFlags
var replaceUnits bool
cmd := &cobra.Command{
Use: "edit <team>", Short: "Edit a team", Args: argsExact(1), ValidArgsFunction: a.completeTeams(),
Long: `Edit a team. --permission sets every repository unit to read or write, or grants admin
access. --unit changes single units and keeps the others, unless --unit-replace is passed.`,
Example: `$ tea team edit acme/developers --permission write
# also let the team edit the wiki
$ tea team edit acme/developers --unit repo.wiki=write
$ tea team edit acme/developers --name devs --description "Core developers"`,
Annotations: map[string]string{"tea.arguments": teamArguments},
RunE: func(cmd *cobra.Command, args []string) error {
if _, err := parseTeam(args[0]); err != nil {
return err
}
if replaceUnits && !cmd.Flags().Changed("unit") {
return clierr.Wrap(clierr.Usage, errors.New("--unit-replace requires --unit"))
}
units, err := flags.unitPolicy(cmd, false)
if err != nil {
return err
}
opts := flags.options(cmd)
if cmd.Flags().Changed("name") {
if strings.TrimSpace(flags.name) == "" {
return clierr.Wrap(clierr.Usage, errors.New("team name cannot be empty"))
}
opts.Name = &flags.name
}
opts.Units, opts.ReplaceUnits = units, replaceUnits || cmd.Flags().Changed("permission")
if opts.Name == nil && opts.Description == nil && opts.Permission == nil && opts.Units == nil && opts.CanCreateRepo == nil && opts.AllRepositories == nil {
return clierr.Wrap(clierr.Usage, errors.New("supply team fields to edit"))
}
_, service, team, err := a.team(cmd, args[0])
if err != nil {
return err
}
if opts.Units != nil && !opts.ReplaceUnits && (team.Permission == "admin" || team.Permission == "owner") {
return clierr.Wrap(clierr.Usage, fmt.Errorf("--unit cannot restrict the %s access of team %s; pass --permission read or write, or --unit-replace", team.Permission, team.Slug()))
}
edited, err := service.Edit(cmd.Context(), team.ID, opts)
if err != nil {
return err
}
edited.Organization = team.Organization
a.done(cmd, "Edited team %s (ID %d)", edited.Slug(), edited.ID)
return a.jsonResult(cmd, edited)
},
}
flags.register(cmd, false)
cmd.Flags().BoolVar(&replaceUnits, "unit-replace", false, "Replace the whole unit policy with --unit; omitted units lose access")
return cmd
}
func (a *application) teamDeleteCommand() *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "delete <team>", Short: "Delete a team", Args: argsExact(1), ValidArgsFunction: a.completeTeams(),
Long: "Delete a team. Deleting an organization's owner team needs --force.",
Example: `$ tea team delete acme/reviewers
$ tea team delete 7 --yes`,
Annotations: map[string]string{"tea.text": "team deletion does not return a resource", "tea.confirm": "true", "tea.arguments": teamArguments},
RunE: func(cmd *cobra.Command, args []string) error {
_, service, team, err := a.team(cmd, args[0])
if err != nil {
return err
}
subject := fmt.Sprintf("team %q (ID %d)", team.Name, team.ID)
if team.Organization != "" {
subject += " from " + team.Organization
}
confirmation := terminal.Confirmation{Prompt: "Delete " + subject + "?"}
if team.Permission == "owner" {
if !force {
return clierr.Wrap(clierr.Usage, fmt.Errorf("%s is the organization's owner team and its members would lose owner access; pass --force to delete it", subject))
}
confirmation = terminal.Confirmation{Prompt: "Delete owner " + subject + "? Its members lose owner access.", Typed: team.Slug()}
}
if err := a.confirmAction(cmd, confirmation); err != nil {
return err
}
if err := service.Delete(cmd.Context(), team.ID); err != nil {
return err
}
a.done(cmd, "Deleted %s", subject)
return nil
},
}
cmd.Flags().BoolVar(&force, "force", false, "Allow deleting the organization's owner team")
return cmd
}
// joinNames lists names in a sentence: "bob", "bob and carol", "bob, carol and dave".
func joinNames(names []string) string {
if len(names) < 2 {
return strings.Join(names, "")
}
return strings.Join(names[:len(names)-1], ", ") + " and " + names[len(names)-1]
}
func (a *application) teamMemberCommand() *cobra.Command {
group := &cobra.Command{Use: "member", Short: "Manage team members", Long: `List, add and remove the members of a team. Adding a user to a team of an organization
also makes them a member of the organization.`}
var limit, page int
list := &cobra.Command{
Use: "list <team>", Short: "List team members", Args: argsExact(1), ValidArgsFunction: a.completeTeams(),
Example: "$ tea team member list acme/developers",
Annotations: map[string]string{"tea.arguments": teamArguments},
RunE: func(cmd *cobra.Command, args []string) error {
target, service, team, err := a.team(cmd, args[0])
if err != nil {
return err
}
members, err := service.Members(cmd.Context(), team.ID, limit, page)
if err != nil {
return err
}
return writeTable(a, cmd, members.Items, listSpec[services.OrganizationMember]{
Columns: []terminal.Column{{Header: "Username"}, {Header: "Name", Flex: true}},
Row: func(member services.OrganizationMember) []terminal.Cell {
return []terminal.Cell{{Text: member.Username, Link: webURL(target, member.Username), Bold: true}, {Text: member.FullName}}
},
Context: "members of team " + team.Slug(), Empty: "no members in team " + team.Slug(),
}, servicePage(members, page))
},
}
pageFlags(list, &limit, &page)
add := &cobra.Command{
Use: "add <team> <username>...", Short: "Add users to a team", Args: usageArgs(cobra.MinimumNArgs(2)),
Example: `$ tea team member add acme/developers bob
$ tea team member add 2 bob carol`,
Annotations: map[string]string{"tea.text": "team membership changes do not return a resource", "tea.arguments": teamArguments},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) == 0 {
return a.completeTeams()(cmd, args, toComplete)
}
return a.completeInstanceUsers()(cmd, args, toComplete)
},
RunE: func(cmd *cobra.Command, args []string) error {
_, service, team, err := a.team(cmd, args[0])
if err != nil {
return err
}
for _, username := range args[1:] {
if err := service.SetMember(cmd.Context(), team.ID, username, true); err != nil {
return err
}
a.done(cmd, "Added %s to team %s", username, team.Slug())
}
return nil
},
}
remove := &cobra.Command{
Use: "remove <team> <username>...", Short: "Remove users from a team", Args: usageArgs(cobra.MinimumNArgs(2)),
Example: "$ tea team member remove acme/developers carol",
Annotations: map[string]string{"tea.text": "team membership changes do not return a resource", "tea.confirm": "true", "tea.arguments": teamArguments},
ValidArgsFunction: a.completeTeamThen(func(ctx context.Context, t target, team services.Team) ([]cobra.Completion, error) {
members, err := services.Teams{Client: t.client}.Members(ctx, team.ID, completionLimit, 1)
return describe(members.Items, func(member services.OrganizationMember) (string, string) { return member.Username, member.FullName }), err
}),
RunE: func(cmd *cobra.Command, args []string) error {
_, service, team, err := a.team(cmd, args[0])
if err != nil {
return err
}
members := args[1:]
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: fmt.Sprintf("Remove %s from team %s?", joinNames(members), team.Slug())}); err != nil {
return err
}
for _, username := range members {
if err := service.SetMember(cmd.Context(), team.ID, username, false); err != nil {
return err
}
a.done(cmd, "Removed %s from team %s", username, team.Slug())
}
return nil
},
}
group.AddCommand(list, add, remove)
return group
}
// teamRepository resolves a repository argument of team repo add and remove: a name inside the
// team's organization, or an OWNER/NAME that the organization owns.
func teamRepository(team services.Team, raw string) (string, error) {
owner, name, scoped := strings.Cut(strings.TrimSpace(raw), "/")
if !scoped {
owner, name = team.Organization, owner
}
if owner == "" || name == "" || strings.Contains(name, "/") {
return "", clierr.Wrap(clierr.Usage, fmt.Errorf("invalid repository %q: use NAME or %s/NAME", raw, team.Organization))
}
if !strings.EqualFold(owner, team.Organization) {
return "", clierr.Wrap(clierr.Usage, fmt.Errorf("%s/%s is not owned by organization %s; teams can only be granted repositories of their organization", owner, name, team.Organization))
}
return team.Organization + "/" + name, nil
}
func (a *application) teamRepoCommand() *cobra.Command {
group := &cobra.Command{Use: "repo", Short: "Manage team repository access", Long: `List the repositories a team can reach, and grant or revoke its access to repositories of
its organization. Repositories are named alone, such as service, or as ORG/NAME.`}
var limit, page int
list := &cobra.Command{
Use: "list <team>", Short: "List team repositories", Args: argsExact(1), ValidArgsFunction: a.completeTeams(),
Example: "$ tea team repo list acme/developers",
Annotations: map[string]string{"tea.arguments": teamArguments},
RunE: func(cmd *cobra.Command, args []string) error {
_, service, team, err := a.team(cmd, args[0])
if err != nil {
return err
}
repos, err := service.Repositories(cmd.Context(), team.ID, limit, page)
if err != nil {
return err
}
return writeTable(a, cmd, repos.Items, listSpec[services.Repository]{
Columns: []terminal.Column{{Header: "Name"}, {Header: "Description", Flex: true}},
Row: func(repo services.Repository) []terminal.Cell {
return []terminal.Cell{{Text: repo.FullName, Link: repo.URL, Bold: true}, {Text: repo.Description}}
},
Context: "repositories of team " + team.Slug(), Empty: "team " + team.Slug() + " has no repositories",
}, servicePage(repos, page))
},
}
pageFlags(list, &limit, &page)
group.AddCommand(list, a.teamRepoAccessCommand(true), a.teamRepoAccessCommand(false))
return group
}
func (a *application) teamRepoAccessCommand(grant bool) *cobra.Command {
verb, short, example := "remove", "Revoke team access to repositories", "$ tea team repo remove acme/developers legacy"
annotations := map[string]string{"tea.text": "team access changes do not return a resource", "tea.arguments": teamArguments, "tea.confirm": "true"}
if grant {
verb, short, example = "add", "Grant a team access to repositories", "$ tea team repo add acme/developers service\n$ tea team repo add 2 acme/service acme/website"
delete(annotations, "tea.confirm")
}
return &cobra.Command{
Use: verb + " <team> <repository>...", Short: short, Args: usageArgs(cobra.MinimumNArgs(2)), Example: example, Annotations: annotations,
Long: short + ` of its organization. Name the repositories alone, such as service, or as ORG/NAME;
repositories of other owners cannot be granted to a team.`,
ValidArgsFunction: a.completeTeamThen(func(ctx context.Context, t target, team services.Team) ([]cobra.Completion, error) {
if grant {
repos, _, err := services.Repositories{Client: t.client}.List(ctx, services.RepositoryListOptions{Owner: team.Organization, Limit: completionLimit, Page: 1})
return describe(repos, func(repo services.Repository) (string, string) { return repo.Name, repo.Description }), err
}
repos, err := services.Teams{Client: t.client}.Repositories(ctx, team.ID, completionLimit, 1)
return describe(repos.Items, func(repo services.Repository) (string, string) { return repo.Name, repo.Description }), err
}),
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("repo") {
return clierr.Wrap(clierr.Usage, fmt.Errorf("--repo is not used by %s; name the repositories as arguments: %s <team> <repository>", cmd.CommandPath(), cmd.CommandPath()))
}
_, service, team, err := a.team(cmd, args[0])
if err != nil {
return err
}
var repos []string
for _, raw := range args[1:] {
repo, err := teamRepository(team, raw)
if err != nil {
return err
}
repos = append(repos, repo)
}
if !grant {
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: fmt.Sprintf("Revoke team %s access to %s?", team.Slug(), joinNames(repos))}); err != nil {
return err
}
}
for _, repo := range repos {
if err := service.SetRepository(cmd.Context(), team.ID, repo, grant); err != nil {
return err
}
if grant {
a.done(cmd, "Granted team %s access to %s", team.Slug(), repo)
} else {
a.done(cmd, "Revoked team %s access to %s", team.Slug(), repo)
}
}
return nil
},
}
}