Files
gitea-tea/internal/app/teams.go
T
bircni 1f4b5f868f refactor(prompts): fold Asker into Prompter and share delete confirmations
FormPrompter is the only production prompter and implements the rich
prompt API, so Ask, AskText and ConfirmDefault become the Prompter
interface and the plain Input/Confirm fallbacks go away.

Confirmations that default to no derive their non-terminal wording
from the prompt, and single deletes share deleteConfirmed for the
prompt, the request and the done line.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-12 10:47:54 +02:00

679 lines
27 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))
}
// find looks the team up on the instance.
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
}
// teamURL is the page of a team on its instance.
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.
` + teamArguments}
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
}
// options returns the supplied team fields.
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: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return a.completeOrgs()(cmd, args, toComplete)
},
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
}
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. Its members lose the access the team gave them.
Deleting an organization's owner team takes away owner access, so it needs --force and, on a
terminal, typing ORG/TEAM.`,
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.
` + teamArguments}
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.
` + teamArguments}
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
}
// teamRepoAccessCommand grants or revokes team access to repositories.
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
},
}
}