From d1e0849df7ce1385aee77b2c0a3117823a3ae735 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Thu, 30 Jul 2026 10:08:27 +0700 Subject: [PATCH] feat(collaborators): add repository collaborator management commands Add a 'tea collaborators' command for managing repository collaborators, mirroring the conventions of the recently-added 'tea deploy-keys' command: - list: List all collaborators of a repository - create: Add a collaborator with specified permission (read/write/admin) - delete: Remove a collaborator from a repository - permission: Check a collaborator's current permission level Running 'tea collaborators ' without a subcommand is a shortcut for 'tea collaborators permission '. Subcommand naming follows the codebase's create/delete convention; aliases ('add' for create, 'rm'/'remove' for delete) preserve the existing muscle memory. The deploy-keys functionality previously bundled here is intentionally omitted: it is covered by the already-open upstream PR #1067 and would conflict. --- cmd/cmd.go | 1 + cmd/collaborators.go | 73 +++++++++++++++++++++++++++++++++ cmd/collaborators/create.go | 61 +++++++++++++++++++++++++++ cmd/collaborators/delete.go | 65 +++++++++++++++++++++++++++++ cmd/collaborators/list.go | 50 ++++++++++++++++++++++ cmd/collaborators/permission.go | 50 ++++++++++++++++++++++ modules/print/collaborator.go | 41 ++++++++++++++++++ 7 files changed, 341 insertions(+) create mode 100644 cmd/collaborators.go create mode 100644 cmd/collaborators/create.go create mode 100644 cmd/collaborators/delete.go create mode 100644 cmd/collaborators/list.go create mode 100644 cmd/collaborators/permission.go create mode 100644 modules/print/collaborator.go diff --git a/cmd/cmd.go b/cmd/cmd.go index a2306d9c..bc6e3622 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -42,6 +42,7 @@ func App() *cli.Command { &CmdWiki, &CmdWebhooks, &CmdComments, + &CmdCollaborators, &CmdOpen, &CmdNotifications, diff --git a/cmd/collaborators.go b/cmd/collaborators.go new file mode 100644 index 00000000..de835c53 --- /dev/null +++ b/cmd/collaborators.go @@ -0,0 +1,73 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + stdctx "context" + + "gitea.dev/tea/cmd/collaborators" + "gitea.dev/tea/modules/context" + "gitea.dev/tea/modules/print" + + "github.com/urfave/cli/v3" +) + +// CmdCollaborators represents the collaborators command for managing repository +// collaborators. +var CmdCollaborators = cli.Command{ + Name: "collaborators", + Aliases: []string{"collaborator", "collab"}, + Category: catEntities, + Usage: "Manage repository collaborators", + Description: "List, create, delete, and check permissions for repository collaborators", + ArgsUsage: "[]", + Action: runCollaboratorsDefault, + Commands: []*cli.Command{ + &collaborators.CmdCollaboratorsList, + &collaborators.CmdCollaboratorsCreate, + &collaborators.CmdCollaboratorsDelete, + &collaborators.CmdCollaboratorsPermission, + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "repo", + Usage: "repository to operate on", + }, + &cli.StringFlag{ + Name: "login", + Usage: "gitea login instance to use", + }, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "output format [table, csv, simple, tsv, yaml, json]", + }, + }, +} + +func runCollaboratorsDefault(ctx stdctx.Context, cmd *cli.Command) error { + if cmd.Args().Len() == 1 { + return runCollaboratorPermission(ctx, cmd, cmd.Args().First()) + } + return collaborators.RunCollaboratorsList(ctx, cmd) +} + +func runCollaboratorPermission(ctx stdctx.Context, cmd *cli.Command, username string) error { + c, err := context.InitCommand(cmd) + if err != nil { + return err + } + if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil { + return err + } + client := c.Login.Client() + + permission, _, err := client.CollaboratorPermission(ctx, c.Owner, c.Repo, username) + if err != nil { + return err + } + + print.CollaboratorPermission(username, permission) + return nil +} diff --git a/cmd/collaborators/create.go b/cmd/collaborators/create.go new file mode 100644 index 00000000..0bea95e4 --- /dev/null +++ b/cmd/collaborators/create.go @@ -0,0 +1,61 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package collaborators + +import ( + stdctx "context" + "errors" + "fmt" + + gitea "gitea.dev/sdk" + + "gitea.dev/tea/cmd/flags" + "gitea.dev/tea/modules/context" + "github.com/urfave/cli/v3" +) + +// CmdCollaboratorsCreate represents a sub command of collaborators to create a collaborator +var CmdCollaboratorsCreate = cli.Command{ + Name: "create", + Aliases: []string{"add", "c"}, + Usage: "Create a collaborator", + Description: "Add a collaborator to a repository. To change permissions, run 'delete' followed by 'create'.", + ArgsUsage: "", + Action: runCollaboratorsCreate, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "permission", + Usage: "permission level for the collaborator (read, write, admin)", + Value: "write", + }, + }, flags.AllDefaultFlags...), +} + +func runCollaboratorsCreate(ctx stdctx.Context, cmd *cli.Command) error { + if cmd.Args().Len() == 0 { + return errors.New("username is required") + } + + c, err := context.InitCommand(cmd) + if err != nil { + return err + } + if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil { + return err + } + client := c.Login.Client() + + username := cmd.Args().First() + permission := gitea.AccessMode(cmd.String("permission")) + + _, err = client.AddCollaborator(ctx, c.Owner, c.Repo, username, gitea.AddCollaboratorOption{ + Permission: &permission, + }) + if err != nil { + return err + } + + fmt.Printf("User %s added as collaborator with %s permission\n", username, permission) + return nil +} diff --git a/cmd/collaborators/delete.go b/cmd/collaborators/delete.go new file mode 100644 index 00000000..63d3d3d2 --- /dev/null +++ b/cmd/collaborators/delete.go @@ -0,0 +1,65 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package collaborators + +import ( + stdctx "context" + "errors" + "fmt" + + "gitea.dev/tea/cmd/flags" + "gitea.dev/tea/modules/context" + "github.com/urfave/cli/v3" +) + +// CmdCollaboratorsDelete represents a sub command of collaborators to delete a collaborator +var CmdCollaboratorsDelete = cli.Command{ + Name: "delete", + Aliases: []string{"rm", "remove"}, + Usage: "Delete a collaborator", + Description: "Remove a collaborator from a repository", + ArgsUsage: "", + Action: runCollaboratorsDelete, + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "confirm", + Aliases: []string{"y"}, + Usage: "confirm deletion without prompting", + }, + }, flags.AllDefaultFlags...), +} + +func runCollaboratorsDelete(ctx stdctx.Context, cmd *cli.Command) error { + if cmd.Args().Len() == 0 { + return errors.New("username is required") + } + + c, err := context.InitCommand(cmd) + if err != nil { + return err + } + if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil { + return err + } + client := c.Login.Client() + + username := cmd.Args().First() + + if !cmd.Bool("confirm") { + fmt.Printf("Are you sure you want to remove %s as collaborator? [y/N] ", username) + var response string + fmt.Scanln(&response) + if response != "y" && response != "Y" && response != "yes" { + fmt.Println("Deletion canceled.") + return nil + } + } + + if _, err = client.DeleteCollaborator(ctx, c.Owner, c.Repo, username); err != nil { + return err + } + + fmt.Printf("Collaborator %s removed successfully\n", username) + return nil +} diff --git a/cmd/collaborators/list.go b/cmd/collaborators/list.go new file mode 100644 index 00000000..7924521f --- /dev/null +++ b/cmd/collaborators/list.go @@ -0,0 +1,50 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package collaborators + +import ( + stdctx "context" + + gitea "gitea.dev/sdk" + + "gitea.dev/tea/cmd/flags" + "gitea.dev/tea/modules/context" + "gitea.dev/tea/modules/print" + "github.com/urfave/cli/v3" +) + +// CmdCollaboratorsList represents a sub command of collaborators to list collaborators +var CmdCollaboratorsList = cli.Command{ + Name: "list", + Aliases: []string{"ls"}, + Usage: "List collaborators", + Description: "List collaborators of a repository", + Action: RunCollaboratorsList, + Flags: append([]cli.Flag{ + &flags.PaginationPageFlag, + &flags.PaginationLimitFlag, + }, flags.AllDefaultFlags...), +} + +// RunCollaboratorsList lists collaborators +func RunCollaboratorsList(ctx stdctx.Context, cmd *cli.Command) error { + c, err := context.InitCommand(cmd) + if err != nil { + return err + } + if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil { + return err + } + client := c.Login.Client() + + collaborators, _, err := client.ListCollaborators(ctx, c.Owner, c.Repo, gitea.ListCollaboratorsOptions{ + ListOptions: flags.GetListOptions(cmd), + }) + if err != nil { + return err + } + + print.CollaboratorsList(collaborators, c.Output) + return nil +} diff --git a/cmd/collaborators/permission.go b/cmd/collaborators/permission.go new file mode 100644 index 00000000..819ebe60 --- /dev/null +++ b/cmd/collaborators/permission.go @@ -0,0 +1,50 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package collaborators + +import ( + stdctx "context" + "errors" + + "gitea.dev/tea/cmd/flags" + "gitea.dev/tea/modules/context" + "gitea.dev/tea/modules/print" + "github.com/urfave/cli/v3" +) + +// CmdCollaboratorsPermission represents a sub command of collaborators to check permission +var CmdCollaboratorsPermission = cli.Command{ + Name: "permission", + Aliases: []string{"perm"}, + Usage: "Check collaborator permission", + Description: "Check the permission level of a collaborator on a repository", + ArgsUsage: "", + Action: runCollaboratorsPermission, + Flags: flags.AllDefaultFlags, +} + +func runCollaboratorsPermission(ctx stdctx.Context, cmd *cli.Command) error { + if cmd.Args().Len() == 0 { + return errors.New("username is required") + } + + c, err := context.InitCommand(cmd) + if err != nil { + return err + } + if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil { + return err + } + client := c.Login.Client() + + username := cmd.Args().First() + + permission, _, err := client.CollaboratorPermission(ctx, c.Owner, c.Repo, username) + if err != nil { + return err + } + + print.CollaboratorPermission(username, permission) + return nil +} diff --git a/modules/print/collaborator.go b/modules/print/collaborator.go new file mode 100644 index 00000000..6f7daaad --- /dev/null +++ b/modules/print/collaborator.go @@ -0,0 +1,41 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package print + +import ( + "fmt" + "strconv" + + "gitea.dev/sdk" +) + +// CollaboratorsList prints a listing of collaborators +func CollaboratorsList(collaborators []*gitea.User, output string) { + t := tableWithHeader( + "ID", + "Username", + "Full Name", + "Email", + ) + + for _, user := range collaborators { + t.addRow( + strconv.FormatInt(user.ID, 10), + user.UserName, + user.FullName, + user.Email, + ) + } + + t.print(output) +} + +// CollaboratorPermission prints detailed information about a collaborator's permissions +func CollaboratorPermission(username string, permission *gitea.CollaboratorPermissionResult) { + fmt.Printf("# Collaborator: %s\n\n", username) + fmt.Printf("- **Permission**: %s\n", permission.Permission) + if permission.Role != "" { + fmt.Printf("- **Role**: %s\n", permission.Role) + } +}