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 <username>' without a subcommand is a shortcut
for 'tea collaborators permission <username>'.

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.
This commit is contained in:
Ross Golder 2026-07-30 10:08:27 +07:00
parent 58931b5d17
commit d1e0849df7
No known key found for this signature in database
GPG key ID: 253A7E508D2D59CD
7 changed files with 341 additions and 0 deletions

View file

@ -42,6 +42,7 @@ func App() *cli.Command {
&CmdWiki,
&CmdWebhooks,
&CmdComments,
&CmdCollaborators,
&CmdOpen,
&CmdNotifications,

73
cmd/collaborators.go Normal file
View file

@ -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: "[<username>]",
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
}

View file

@ -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: "<username>",
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
}

View file

@ -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: "<username>",
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
}

50
cmd/collaborators/list.go Normal file
View file

@ -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
}

View file

@ -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: "<username>",
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
}

View file

@ -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)
}
}