diff --git a/cmd/teams.go b/cmd/teams.go index e6138f33..a5561a00 100644 --- a/cmd/teams.go +++ b/cmd/teams.go @@ -6,6 +6,7 @@ package cmd import ( "gitea.dev/tea/cmd/teams" "gitea.dev/tea/cmd/teams/members" + "gitea.dev/tea/cmd/teams/repos" "github.com/urfave/cli/v3" ) @@ -23,6 +24,7 @@ var CmdTeams = cli.Command{ &teams.CmdTeamsDelete, &teams.CmdTeamsEdit, &members.CmdTeamsMembers, + &repos.CmdTeamsRepos, }, Flags: []cli.Flag{ &cli.StringFlag{ diff --git a/cmd/teams/repos/create.go b/cmd/teams/repos/create.go new file mode 100644 index 00000000..8d9bd317 --- /dev/null +++ b/cmd/teams/repos/create.go @@ -0,0 +1,69 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repos + +import ( + stdctx "context" + "errors" + "fmt" + "strings" + + "gitea.dev/tea/cmd/flags" + "gitea.dev/tea/cmd/teams" + "gitea.dev/tea/modules/context" + "github.com/urfave/cli/v3" +) + +// CmdTeamsReposCreate grants a team access to a repository. +var CmdTeamsReposCreate = cli.Command{ + Name: "create", + Aliases: []string{"add", "c"}, + Usage: "Grant a team access to a repository", + Description: "Grant a team access to a repository", + ArgsUsage: " /", + Action: runTeamsReposCreate, + Flags: flags.AllDefaultFlags, +} + +func runTeamsReposCreate(ctx stdctx.Context, cmd *cli.Command) error { + if cmd.Args().Len() < 2 { + return errors.New("team ID and / are required") + } + + teamID, err := teams.RequireTeamID(cmd) + if err != nil { + return err + } + + owner, repo, err := parseRepoSlug(cmd.Args().Get(1)) + if err != nil { + return err + } + + c, err := context.InitCommand(cmd) + if err != nil { + return err + } + client := c.Login.Client() + + team, _, err := client.Organizations.GetTeam(ctx, teamID) + if err != nil { + return err + } + + if _, err = client.Organizations.AddTeamRepository(ctx, teamID, owner, repo); err != nil { + return err + } + fmt.Printf("Repository '%s/%s' granted to team '%s' (ID: %d)\n", owner, repo, team.Name, team.ID) + return nil +} + +// parseRepoSlug splits "owner/repo" into its parts. +func parseRepoSlug(slug string) (owner, repo string, err error) { + parts := strings.SplitN(slug, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("invalid repository %q: expected /", slug) + } + return parts[0], parts[1], nil +} diff --git a/cmd/teams/repos/delete.go b/cmd/teams/repos/delete.go new file mode 100644 index 00000000..59be4c49 --- /dev/null +++ b/cmd/teams/repos/delete.go @@ -0,0 +1,75 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repos + +import ( + stdctx "context" + "errors" + "fmt" + + "gitea.dev/tea/cmd/flags" + "gitea.dev/tea/cmd/teams" + "gitea.dev/tea/modules/context" + "github.com/urfave/cli/v3" +) + +// CmdTeamsReposDelete revokes a team's access to a repository. +var CmdTeamsReposDelete = cli.Command{ + Name: "delete", + Aliases: []string{"rm", "remove"}, + Usage: "Revoke a team's access to a repository", + Description: "Revoke a team's access to a repository", + ArgsUsage: " /", + Action: runTeamsReposDelete, + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "confirm", + Aliases: []string{"y"}, + Usage: "confirm removal without prompting", + }, + }, flags.AllDefaultFlags...), +} + +func runTeamsReposDelete(ctx stdctx.Context, cmd *cli.Command) error { + if cmd.Args().Len() < 2 { + return errors.New("team ID and / are required") + } + + teamID, err := teams.RequireTeamID(cmd) + if err != nil { + return err + } + + owner, repo, err := parseRepoSlug(cmd.Args().Get(1)) + if err != nil { + return err + } + + c, err := context.InitCommand(cmd) + if err != nil { + return err + } + client := c.Login.Client() + + team, _, err := client.Organizations.GetTeam(ctx, teamID) + if err != nil { + return err + } + + if !cmd.Bool("confirm") { + fmt.Printf("Are you sure you want to revoke '%s/%s' access from team '%s' (ID: %d)? [y/N] ", owner, repo, team.Name, team.ID) + var response string + fmt.Scanln(&response) + if response != "y" && response != "Y" && response != "yes" { + fmt.Println("Removal canceled.") + return nil + } + } + + if _, err = client.Organizations.RemoveTeamRepository(ctx, teamID, owner, repo); err != nil { + return err + } + fmt.Printf("Repository '%s/%s' access revoked from team '%s' (ID: %d)\n", owner, repo, team.Name, team.ID) + return nil +} diff --git a/cmd/teams/repos/list.go b/cmd/teams/repos/list.go new file mode 100644 index 00000000..39d1b374 --- /dev/null +++ b/cmd/teams/repos/list.go @@ -0,0 +1,58 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repos + +import ( + stdctx "context" + + gitea "gitea.dev/sdk" + + "gitea.dev/tea/cmd/flags" + "gitea.dev/tea/cmd/teams" + "gitea.dev/tea/modules/context" + "gitea.dev/tea/modules/print" + "github.com/urfave/cli/v3" +) + +// CmdTeamsReposList lists the repositories a team has access to. +var CmdTeamsReposList = cli.Command{ + Name: "list", + Aliases: []string{"ls"}, + Usage: "List team repositories", + Description: "List all repositories a team has access to", + ArgsUsage: "", + Action: runTeamsReposList, + Flags: append([]cli.Flag{ + &flags.PaginationPageFlag, + &flags.PaginationLimitFlag, + }, flags.AllDefaultFlags...), +} + +func runTeamsReposList(ctx stdctx.Context, cmd *cli.Command) error { + teamID, err := teams.RequireTeamID(cmd) + if err != nil { + return err + } + + c, err := context.InitCommand(cmd) + if err != nil { + return err + } + client := c.Login.Client() + + team, _, err := client.Organizations.GetTeam(ctx, teamID) + if err != nil { + return err + } + + repos, _, err := client.Organizations.ListTeamRepositories(ctx, teamID, gitea.ListTeamRepositoriesOptions{ + ListOptions: flags.GetListOptions(cmd), + }) + if err != nil { + return err + } + + print.TeamRepositoriesList(repos, c.Output, team.Name) + return nil +} diff --git a/cmd/teams/repos/repos.go b/cmd/teams/repos/repos.go new file mode 100644 index 00000000..032e5589 --- /dev/null +++ b/cmd/teams/repos/repos.go @@ -0,0 +1,26 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repos + +import ( + stdctx "context" + + "github.com/urfave/cli/v3" +) + +// CmdTeamsRepos is the parent command for managing which repositories a team can access. +var CmdTeamsRepos = cli.Command{ + Name: "repos", + Aliases: []string{"repo", "repositories"}, + Usage: "Manage team repository access", + Description: "List, add, or remove repositories a team can access", + Commands: []*cli.Command{ + &CmdTeamsReposList, + &CmdTeamsReposCreate, + &CmdTeamsReposDelete, + }, + Action: func(ctx stdctx.Context, cmd *cli.Command) error { + return cli.ShowCommandHelp(ctx, cmd, "repos") + }, +} diff --git a/modules/print/team.go b/modules/print/team.go index 0cd9dcfc..abd7d7e5 100644 --- a/modules/print/team.go +++ b/modules/print/team.go @@ -95,3 +95,36 @@ func TeamMembersList(members []*gitea.User, output, teamName string) { fmt.Printf("Members of team '%s':\n", teamName) t.print(output) } + +// TeamRepositoriesList prints a list of repositories accessible by a team. +func TeamRepositoriesList(repos []*gitea.Repository, output, teamName string) { + if len(repos) == 0 { + fmt.Printf("No repositories found for team '%s'\n", teamName) + return + } + + t := tableWithHeader( + "ID", + "Name", + "Full Name", + "Private", + "Fork", + "Size", + "Updated", + ) + + for _, repo := range repos { + t.addRow( + strconv.FormatInt(repo.ID, 10), + repo.Name, + repo.FullName, + yesNo(repo.Private), + yesNo(repo.Fork), + formatSize(int64(repo.Size)), + FormatTime(repo.Updated, output != ""), + ) + } + + fmt.Printf("Repositories accessible by team '%s':\n", teamName) + t.print(output) +}