refactor(repo,org,team,release): trim help texts, obvious comments and single-use helpers

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-12 20:48:01 +02:00
1 parent d5aef95855
commit b79845b9f3
11 files changed
+55 -129

No files matched your search

+3 -9
View File
@@ -21,11 +21,7 @@ func (a *application) adminCommand() *cobra.Command {
group := &cobra.Command{Use: "admin", Short: "Manage instance administration", Long: "Administer the Gitea instance. These commands need an administrator account."}
users := &cobra.Command{Use: "user", Short: "Manage registered accounts", Long: `List, view, create, edit and delete the accounts of the instance. These commands need an
administrator account.`}
users.AddCommand(a.adminUserListCommand(), a.adminUserViewCommand())
for _, verb := range []string{"create", "edit"} {
users.AddCommand(a.adminUserChangeCommand(verb))
}
users.AddCommand(a.adminUserDeleteCommand())
users.AddCommand(a.adminUserListCommand(), a.adminUserViewCommand(), a.adminUserChangeCommand("create"), a.adminUserChangeCommand("edit"), a.adminUserDeleteCommand())
group.AddCommand(users)
return group
}
@@ -149,7 +145,6 @@ $ tea admin user view bob --json email,admin`,
}
}
// adminUserChangeCommand creates or edits an account.
func (a *application) adminUserChangeCommand(verb string) *cobra.Command {
var username, email, fullName, description, website, location, visibility, password, passwordFile string
var admin, active, restricted, prohibitLogin, mustChangePassword, allowGitHook, allowImportLocal, allowCreateOrganization bool
@@ -330,9 +325,8 @@ func (a *application) adminUserDeleteCommand() *cobra.Command {
var purge bool
cmd := &cobra.Command{
Use: "delete <username>", Short: "Delete an account", Args: argsExact(1), ValidArgsFunction: a.completeAdminUsers(),
Long: `Delete an account. Gitea refuses while the user owns repositories or is the last owner of an
organization: transfer or delete those first, or pass --purge to delete the account with
everything it owns. A purge cannot be undone, so a terminal asks you to type the username.`,
Long: `Delete an account. Gitea refuses while the user owns repositories or organizations, unless
--purge deletes the account with everything it owns.`,
Example: `$ tea admin user delete spammer
# also delete the user's repositories, packages and organization memberships
$ tea admin user delete spammer --purge`,
+11 -26
View File
@@ -18,23 +18,17 @@ import (
// validateBranch rejects invalid Git refs before making requests.
func validateBranch(name string) error {
if name == "" || name == "@" || strings.HasPrefix(name, "-") || strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") || strings.HasSuffix(name, ".") || strings.Contains(name, "..") || strings.Contains(name, "@{") || strings.Contains(name, "//") || strings.ContainsAny(name, " ~^:?*[\\\t\r\n") {
return clierr.Wrap(clierr.Usage, errors.New("invalid branch name"))
}
invalid := name == "" || name == "@" || strings.HasPrefix(name, "-") || strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") || strings.HasSuffix(name, ".") || strings.Contains(name, "..") || strings.Contains(name, "@{") || strings.Contains(name, "//") || strings.ContainsAny(name, " ~^:?*[\\\t\r\n") ||
strings.ContainsFunc(name, func(r rune) bool { return r < 32 || r == 127 })
for part := range strings.SplitSeq(name, "/") {
if strings.HasPrefix(part, ".") || strings.HasSuffix(part, ".lock") {
return clierr.Wrap(clierr.Usage, errors.New("invalid branch name"))
}
invalid = invalid || strings.HasPrefix(part, ".") || strings.HasSuffix(part, ".lock")
}
for _, r := range name {
if r < 32 || r == 127 {
return clierr.Wrap(clierr.Usage, errors.New("invalid branch name"))
}
if invalid {
return clierr.Wrap(clierr.Usage, errors.New("invalid branch name"))
}
return nil
}
// validateBranches validates every branch name.
func validateBranches(names []string) error {
for _, name := range names {
if err := validateBranch(name); err != nil {
@@ -249,7 +243,10 @@ $ tea branch delete old-a old-b --yes`,
if err != nil {
return err
}
subject := branchSubject(args)
subject := "branch " + args[0]
if len(args) > 1 {
subject = fmt.Sprintf("%d branches (%s)", len(args), strings.Join(args, ", "))
}
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Delete " + subject + " from " + target.Repo + "?"}); err != nil {
return err
}
@@ -263,14 +260,6 @@ $ tea branch delete old-a old-b --yes`,
return command
}
// branchSubject names one or more branches in a prompt: "branch main" or "2 branches (a, b)".
func branchSubject(names []string) string {
if len(names) == 1 {
return "branch " + names[0]
}
return fmt.Sprintf("%d branches (%s)", len(names), strings.Join(names, ", "))
}
func (a *application) branchProtectCommand() *cobra.Command {
var rule services.Protection
var pattern string
@@ -278,12 +267,8 @@ func (a *application) branchProtectCommand() *cobra.Command {
command := &cobra.Command{
Use: "protect <branch>...",
Short: "Protect branches",
Long: `Add a protection rule for each branch. By default nobody can push to a protected branch directly
and pull requests need one approving review to merge; --allow-push lets users or teams push, and
--required-approvals changes the review count.
The branches must exist. --pattern protects every branch that matches a glob instead, such as
release/*, including branches created later.`,
Long: `Add a protection rule for each existing branch, or with --pattern for every branch matching a
glob. By default nobody can push directly and pull requests need one approving review.`,
Example: `# Require two approvals and passing CI on main
$ tea branch protect main --required-approvals 2 --status-check ci/build
+7 -19
View File
@@ -19,7 +19,6 @@ import (
var visibilities = []string{"public", "limited", "private"}
// visibilityColors color the visibility of organizations and accounts.
var visibilityColors = map[string]string{"public": "green", "limited": "yellow", "private": "red"}
// timeField shows an API timestamp relative to now on a TTY, and as RFC3339 for scripts.
@@ -38,14 +37,6 @@ func timeField(label, raw string, d terminal.Display) terminal.Field {
return terminal.Field{Label: label, Value: terminal.RelativeTime(now, t)}
}
// countOf words a count, such as "1 member" or "2 members".
func countOf(n int, singular, plural string) string {
if n == 1 {
return "1 " + singular
}
return strconv.Itoa(n) + " " + plural
}
func (a *application) organizationCommand() *cobra.Command {
group := &cobra.Command{Use: "org", Short: "Manage organizations", Long: `Work with Gitea organizations: list the ones you belong to, view, create, edit and delete
them, and manage their members.
@@ -104,7 +95,7 @@ $ tea org list --all`,
}
func (a *application) organizationViewCommand() *cobra.Command {
cmd := &cobra.Command{
return &cobra.Command{
Use: "view <org>", Short: "View an organization", Args: argsExact(1),
Example: `$ tea org view acme
# print the description only
@@ -131,7 +122,6 @@ $ tea org view acme --jq .description`,
})
},
}
return cmd
}
func organizationView(org services.Organization, size services.OrganizationSize, address string) terminal.View {
@@ -149,7 +139,11 @@ func organizationView(org services.Organization, size services.OrganizationSize,
singular, plural string
}{{"Members", size.Members, "member", "members"}, {"Teams", size.Teams, "team", "teams"}, {"Repositories", size.Repositories, "repository", "repositories"}} {
if count.n != nil {
counts = append(counts, countOf(*count.n, count.singular, count.plural))
noun := count.plural
if *count.n == 1 {
noun = count.singular
}
counts = append(counts, strconv.Itoa(*count.n)+" "+noun)
view.Fields = append(view.Fields, terminal.Field{Label: count.label, Value: strconv.Itoa(*count.n), PipedOnly: true})
}
}
@@ -200,7 +194,6 @@ func organizationFlags(cmd *cobra.Command, create bool) func() services.Organiza
}
}
// organizationName rejects an empty organization argument.
func organizationName(raw string) (string, error) {
if name := strings.TrimSpace(raw); name != "" {
return name, nil
@@ -270,11 +263,7 @@ $ tea org edit acme --visibility private`,
func (a *application) organizationDeleteCommand() *cobra.Command {
return &cobra.Command{
Use: "delete <org>", Short: "Delete an organization", Args: argsExact(1), ValidArgsFunction: a.completeOrgs(),
Long: `Delete an organization with its teams. Gitea refuses while the organization owns
repositories: transfer or delete them first.
The deletion cannot be undone, so a terminal asks you to type the organization's name; --yes
skips the question.`,
Long: "Delete an organization with its teams. Gitea refuses while the organization owns repositories.",
Example: `$ tea org delete acme-old
$ tea org delete acme-old --yes`,
Annotations: map[string]string{"tea.text": "organization deletion does not return a resource", "tea.confirm": "true"},
@@ -371,7 +360,6 @@ func (a *application) completeOrganizationMembers() cobra.CompletionFunc {
}
}
// organizationVisibilityCommand publicizes or conceals an organization membership.
func (a *application) organizationVisibilityCommand(public bool) *cobra.Command {
verb, alias, short, state := "conceal", "private", "Hide a membership", "concealed"
if public {
+3 -7
View File
@@ -165,11 +165,9 @@ func (a *application) releaseDownloadCommand() *cobra.Command {
command := &cobra.Command{
Use: "download [<tag>] [<asset>...]",
Short: "Download release assets",
Long: `Download assets of a release, by name or ID, or those matching --pattern; without either, every
asset. --archive downloads the source code archive instead. Without a tag, the latest release is used.
Files are written to the current directory under their names, to --dir, or to --output; an existing
file is replaced only with --clobber. --output - writes one asset to standard output.`,
Long: `Download the assets of a release (default: the latest) by name or ID, those matching --pattern,
or all of them; --archive downloads the source code instead. Existing files are replaced only with
--clobber, and --output - writes one asset to standard output.`,
Example: `# Download every asset of the latest release
$ tea release download
@@ -285,7 +283,6 @@ func selectAssets(release services.Release, names, patterns []string) ([]service
return selected, nil
}
// downloadArchive downloads the source code of the tag as a zip or tar.gz file.
func (a *application) downloadArchive(cmd *cobra.Command, service services.Releases, flags downloadFlags, tag, format string) error {
_, repo, _ := strings.Cut(service.Repository, "/")
name := repo + "-" + strings.ReplaceAll(tag, "/", "-") + "." + format
@@ -311,7 +308,6 @@ func (a *application) downloadArchive(cmd *cobra.Command, service services.Relea
return nil
}
// countingReader counts the bytes read through it.
type countingReader struct {
reader io.Reader
n int64
-2
View File
@@ -139,7 +139,6 @@ $ tea release view v1.0.0 --web`,
return command
}
// releaseView shows a release with its notes as the body.
func (a *application) releaseView(item services.Release, latest int64) terminal.View {
view := terminal.View{Title: cmp.Or(item.Title, item.Tag), Body: item.Body, Kind: "release", URL: item.URL}
if item.Title != "" && item.Title != item.Tag {
@@ -187,7 +186,6 @@ func (f *releaseFields) register(a *application, cmd *cobra.Command) {
_ = cmd.RegisterFlagCompletionFunc("target", a.completeBranches())
}
// options returns the release fields whose flags were passed.
func (f *releaseFields) options(a *application, cmd *cobra.Command) (services.ReleaseOptions, error) {
opts := services.ReleaseOptions{}
changed := cmd.Flags().Changed
+13 -27
View File
@@ -166,14 +166,12 @@ $ tea repo edit --default-merge-style squash --add-topic cli --add-topic go`,
return services.Classify(err, "branch", strconv.Quote(defaultBranch), t.Repo)
}
}
if options.Archived != nil && *options.Archived {
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Archive " + t.Repo + "? It becomes read-only."}); err != nil {
return err
}
}
// Gitea refuses topic changes on an archived repository, so archiving comes last.
repo, archive := t.Repo, options.Archived != nil && *options.Archived
if archive {
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Archive " + t.Repo + "? It becomes read-only."}); err != nil {
return err
}
options.Archived = nil
}
if options != (services.RepositoryEdit{}) {
@@ -241,12 +239,9 @@ func (a *application) repoCloneCommand() *cobra.Command {
command := &cobra.Command{
Use: "clone <repository> [<directory>] [-- <gitflags>...]",
Short: "Clone a repository locally",
Long: `Clone a repository with the configured Git protocol (git_protocol: https or ssh) and tea's
credentials. Git flags can follow --.
A fork's parent is added as the "upstream" remote, which its default branch pulls from while pushes
stay on the fork; tea commands in the clone then target the parent. A pull mirror's source is added
as the "source" remote.`,
Long: `Clone a repository with the configured Git protocol and tea's credentials. Git flags can
follow --. A fork's parent is added as the "upstream" remote, and tea commands in the clone then
target the parent.`,
Example: `# Clone a repository into ./project
$ tea repo clone alice/project
@@ -304,12 +299,11 @@ func (a *application) repoForkCommand() *cobra.Command {
command := &cobra.Command{
Use: "fork [<repository>] [-- <gitflags>...]",
Short: "Fork a repository",
Long: `Fork a repository for your account, or into an organization with --org. When the account or
organization already has a fork, tea reports it instead of failing.
Long: `Fork a repository for your account, or into an organization with --org. An existing fork is
reported instead of failing.
With --clone, the fork is cloned, and its parent is added as the "upstream" remote; Git flags can
follow --. Inside a clone of the repository, --remote adds the fork as the "origin" remote (or
--remote-name), renaming the current "origin" to "upstream".`,
--clone clones the fork (Git flags can follow --). Inside a clone of the repository, --remote adds
the fork as "origin" and renames the current "origin" to "upstream".`,
Example: `# Fork the repository of the current clone and add the fork as origin
$ tea repo fork --remote
@@ -502,18 +496,13 @@ func (a *application) inferredRepository(t target, verb string, refuseYes bool)
if t.RepoSource == "--repo" {
return "", nil
}
source := describeRepoSource(t.RepoSource)
source := strings.Replace(t.RepoSource, "remote ", "git remote ", 1)
if refuseYes && a.options.yes {
return "", clierr.Wrap(clierr.Usage, fmt.Errorf("--yes requires an explicit repository, not one inferred from %s: tea repo %s %s --yes", source, verb, t.Repo))
}
return " (from " + source + ")", nil
}
// describeRepoSource words a repository source for people: git remote upstream, TEA_REPO, ...
func describeRepoSource(source string) string {
return strings.Replace(source, "remote ", "git remote ", 1)
}
// repoArchiveCommand builds repo archive, or repo unarchive.
func (a *application) repoArchiveCommand(archive bool) *cobra.Command {
command := &cobra.Command{
@@ -581,7 +570,7 @@ $ tea repo archive alice/old-project --yes`,
}
func (a *application) repoRenameCommand() *cobra.Command {
command := &cobra.Command{
return &cobra.Command{
Use: "rename <new-name>",
Short: "Rename a repository",
Long: "Rename a repository; its owner stays the same. Gitea redirects the old name to the new one.",
@@ -625,16 +614,13 @@ $ tea repo rename new-name -R alice/old-name`,
return a.jsonResult(cmd, item)
},
}
return command
}
func (a *application) repoDeleteCommand() *cobra.Command {
command := &cobra.Command{
Use: "delete [<repository>]",
Short: "Delete a repository",
Long: `Delete a repository with its issues, pull requests, releases and wiki. This cannot be undone.
Type the repository's name to confirm. With --yes, name the repository explicitly.`,
Long: "Delete a repository with its issues, pull requests, releases and wiki. This cannot be undone.",
Example: `# Delete a repository after typing its name
$ tea repo delete alice/old-project
+2 -5
View File
@@ -61,10 +61,8 @@ func (a *application) repoSetDefaultCommand() *cobra.Command {
command := &cobra.Command{
Use: "set-default [<repository>]",
Short: "Set the default repository for this clone",
Long: `Set the repository that tea commands use in the current Git clone, instead of the one found through
its Git remotes. It is saved as tea.repository in the clone's Git configuration.
Without an argument, the repository that tea selects now is saved.`,
Long: `Set the repository that tea commands use in the current Git clone instead of the one of its Git
remotes (saved as tea.repository in the clone's Git configuration).`,
Example: `# Make tea commands in this clone use the fork's parent
$ tea repo set-default alice/project
@@ -280,7 +278,6 @@ $ tea repo view alice/project --web`,
return view
}
// repositoryView shows a repository with its README as the body.
func (a *application) repositoryView(item services.Repository, readme string) terminal.View {
badges := []terminal.Cell{{Text: "public", Color: "green"}}
if item.Private {
+6 -11
View File
@@ -36,11 +36,9 @@ func (a *application) repoCreateCommand() *cobra.Command {
Short: "Create a repository",
Long: `Create a repository for your account, or for an organization as ORG/NAME or with --org.
A new repository is empty unless --auto-init adds a first commit with a README and, optionally, a
.gitignore and a license from the server's templates. --template copies another repository instead.
--source publishes an existing local Git repository: it adds the new repository as a remote of
it (origin unless --remote names another), and --push pushes the current branch there.`,
--auto-init adds a first commit with a README, .gitignore and license; --template copies another
repository. --source adds the new repository as a remote of a local Git repository, and --push
pushes its current branch there.`,
Example: `# Create a private repository with a README and an MIT license
$ tea repo create project --private --auto-init --license MIT
@@ -83,7 +81,7 @@ $ tea repo create project --source . --push`,
return err
}
}
prompted, err := a.requireInput(cmd, &opts.Name, "name", "Repository name", func(name string) error { return validateRepositoryName(name) })
prompted, err := a.requireInput(cmd, &opts.Name, "name", "Repository name", validateRepositoryName)
if err != nil {
return err
}
@@ -130,11 +128,8 @@ $ tea repo create project --source . --push`,
}
var item services.Repository
if template != "" {
templateOpts.Name = opts.Name
templateOpts.Owner = org
templateOpts.Description = opts.Description
templateOpts.Private = opts.Private
templateOpts.DefaultBranch = opts.DefaultBranch
templateOpts.Name, templateOpts.Owner, templateOpts.Description = opts.Name, org, opts.Description
templateOpts.Private, templateOpts.DefaultBranch = opts.Private, opts.DefaultBranch
item, err = service.Generate(cmd.Context(), target.Repo, templateOpts)
} else {
item, err = service.Create(cmd.Context(), org, opts)
+5 -8
View File
@@ -44,12 +44,9 @@ func (a *application) repoMigrateCommand() *cobra.Command {
command := &cobra.Command{
Use: "migrate [<clone-url>]",
Short: "Import a repository from another server",
Long: `Import a repository into Gitea from a Git URL, or from another forge with --service, which also
copies its issues, labels, milestones, pull requests, releases or wiki on request. --mirror creates
a pull mirror that Gitea updates from the source instead.
The repository is named after the last segment of the URL unless --name is given. Imports from a
forge with --service need an access token for it.`,
Long: `Import a repository from a Git URL, or from another forge with --service (which needs an access
token and can also copy issues, pull requests, releases and more). --mirror creates a pull mirror
instead. The name defaults to the last segment of the URL.`,
Example: `# Import a public Git repository
$ tea repo migrate https://github.com/go-gitea/tea.git
@@ -81,7 +78,7 @@ $ tea repo migrate https://github.com/acme/tool --service github --issues --pull
if err := validateMigrationFlags(cmd, opts, passwordFile, tokenFile); err != nil {
return err
}
validURL := func(raw string) error { return validateMigrationURL(raw) }
validURL := validateMigrationURL
if cmd.Flags().Changed("server-path") {
validURL = nil
}
@@ -95,7 +92,7 @@ $ tea repo migrate https://github.com/acme/tool --service github --issues --pull
}
}
opts.Name = cmp.Or(opts.Name, migrationName(opts.CloneURL))
named, err := a.requireInput(cmd, &opts.Name, "name", "Repository name", func(name string) error { return validateRepositoryName(name) })
named, err := a.requireInput(cmd, &opts.Name, "name", "Repository name", validateRepositoryName)
if err != nil {
return err
}
+4 -7
View File
@@ -21,14 +21,11 @@ func (a *application) repositorySyncCommand() *cobra.Command {
command := &cobra.Command{
Use: "sync [<repository>]",
Short: "Sync a branch with its remote or its fork's parent",
Long: `Fast-forward a local branch to the branch of the same name on the repository's Git remote.
Long: `Fast-forward a local branch (default: the repository's default branch) to the same branch on
the repository's Git remote, without switching branches. Diverged branches are left unchanged.
The branch defaults to the repository's default branch. It is created when it is missing, and it is
never checked out: the branch you are on stays checked out. When the branch is checked out, the
working tree must be clean. Branches that have diverged from the remote are left unchanged.
Given a repository that has no remote in the current directory, sync updates it on the server
instead: a fork's branch is fast-forwarded to its parent's, and a pull mirror fetches its source.`,
Given a repository without a Git remote here, sync updates it on the server instead: a fork
fast-forwards to its parent, and a pull mirror fetches its source.`,
Example: `# Fast-forward the default branch from the repository's remote
$ tea repo sync
+1 -8
View File
@@ -40,7 +40,6 @@ func parseTeam(raw string) (teamRef, error) {
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)
@@ -63,7 +62,6 @@ func (a *application) team(cmd *cobra.Command, raw string) (target, services.Tea
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))
}
@@ -284,7 +282,6 @@ func (f *teamFlags) unitPolicy(cmd *cobra.Command, create bool) (map[string]stri
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 {
@@ -436,10 +433,7 @@ 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.`,
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},
@@ -610,7 +604,6 @@ its organization. Repositories are named alone, such as service, or as ORG/NAME.
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"}