feat!: rebuild tea on Cobra with explicit noun/verb commands (#1126)

- **switch tea to the Cobra command tree:** `internal/app` wires the packages from parts 1–4 into explicit noun/verb commands with `--repo`, `--json` / `--jq`, `--sudo` and `--debug`; `main.go` points at it. Build, release and docs tooling (`tools/cli-docs`, Makefile, goreleaser, snap, CI) and README / CONTRIBUTING are updated.
- **remove the urfave/cli command tree:** deletes `cmd/`, `modules/`, the old integration tests, contrib completion scripts and the generated `docs/CLI.md`, and drops their dependencies. This commit only deletes.

## Breaking changes

This is a breaking release. It requires Gitea 28 or newer; configuration moves to separate `config.yml` and `hosts.yml` files and requires fresh authentication, and command names, aliases, flags and output formats change. `docs/migration-1.0.md` maps every old command, flag and environment variable to its replacement.

## Issues

fixes #1118
fixes #1116
fixes #1115
fixes #1094
fixes #1091
fixes #1073
fixes #1052
fixes #1032
fixes #1023
fixes #966
fixes #908
fixes #833
fixes #831
fixes #817
fixes #804
fixes #779
fixes #770
fixes #737
fixes #697
fixes #726
fixes #677
fixes #672
fixes #633
fixes #632
fixes #560
fixes #556
fixes #523
fixes #516
fixes #510
fixes #504
fixes #500
fixes #482
fixes #406
fixes #403
fixes #373
fixes #351
fixes #344
fixes #326
fixes #325
fixes #323
fixes #246
fixes #240
fixes #232
fixes #197
fixes #172
fixes #141
fixes #69
fixes #1110
fixes #569
fixes #480
fixes #666
fixes #670
fixes #457
fixes #643
fixes #498
fixes #490
fixes #1069
fixes #834
fixes #602
fixes #328
fixes #417
fixes #571
fixes #1029
fixes #489
fixes #1050
fixes #162
fixes #430
fixes #1087
fixes #683

Reviewed-on: https://gitea.com/gitea/tea/pulls/1126
Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
This commit is contained in:
bircni committed 2026-10-11 17:23:06 +00:00
1 parent 12e415a7a7
commit f02245449b
456 files changed
+18892 -34015

No files matched your search

-190
View File
@@ -1,190 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"errors"
"fmt"
"os"
"strings"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/git"
"gitea.dev/tea/modules/theme"
"gitea.dev/tea/modules/utils"
"charm.land/huh/v2"
"github.com/urfave/cli/v3"
"golang.org/x/term"
)
var errNotAGiteaRepo = errors.New("no Gitea login found; you might want to specify --repo (and --login) to work outside of a repository")
// ErrCommandCanceled is returned when the user explicitly cancels an interactive prompt.
var ErrCommandCanceled = errors.New("command canceled")
// TeaContext contains all context derived during command initialization and wraps cli.Context
type TeaContext struct {
*cli.Command
Login *config.Login // config data & client for selected login
RepoSlug string // <owner>/<repo>, optional
Owner string // repo owner as derived from context or provided in flag, optional
Repo string // repo name as derived from context or provided in flag, optional
Org string // organization name, optional
IsGlobal bool // true if operating on global level
Output string // value of output flag
LocalRepo *git.TeaRepo // is set if flags specified a local repo via --repo, or if $PWD is a git repo
}
// InitOptions controls which optional sources InitCommand may inspect.
type InitOptions struct {
// SkipLocalRepo avoids probing the current directory for a git repository.
SkipLocalRepo bool
}
// GetRemoteRepoHTMLURL returns the web-ui url of the remote repo,
// after ensuring a remote repo is present in the context.
func (ctx *TeaContext) GetRemoteRepoHTMLURL() (string, error) {
if err := ctx.Ensure(CtxRequirement{RemoteRepo: true}); err != nil {
return "", err
}
return strings.TrimRight(ctx.Login.URL, "/") + "/" + ctx.Owner + "/" + ctx.Repo, nil
}
// IsInteractiveMode returns true if the command is running in interactive mode
// (no flags provided and stdout is a terminal)
func (ctx *TeaContext) IsInteractiveMode() bool {
return ctx.Command.NumFlags() == 0
}
func shouldPromptFallbackLogin(login *config.Login, canPrompt bool) bool {
return login != nil && !login.Default && canPrompt
}
// InitCommand resolves the application context, and returns the active login, and if
// available the repo slug. It does this by reading the config file for logins, parsing
// the remotes of the .git repo specified in repoFlag or $PWD, and using overrides from
// command flags. If a local git repo can't be found, repo slug values are unset.
func InitCommand(cmd *cli.Command) (*TeaContext, error) {
return InitCommandWithOptions(cmd, InitOptions{})
}
// InitCommandWithOptions resolves the application context like InitCommand, with
// optional controls for commands that do not need repository context.
func InitCommandWithOptions(cmd *cli.Command, opts InitOptions) (*TeaContext, error) {
// these flags are used as overrides to the context detection via local git repo
repoFlag := cmd.String("repo")
loginFlag := cmd.String("login")
remoteFlag := cmd.String("remote")
orgFlag := cmd.String("org")
globalFlag := cmd.Bool("global")
var (
c TeaContext
err error
repoPath string // empty means PWD
repoFlagPathExists bool
)
// check if repoFlag can be interpreted as path to local repo.
if len(repoFlag) != 0 && !opts.SkipLocalRepo {
if repoFlagPathExists, err = utils.DirExists(repoFlag); err != nil {
return nil, err
}
if repoFlagPathExists {
repoPath = repoFlag
} else {
c.RepoSlug = repoFlag
}
} else if len(repoFlag) != 0 {
c.RepoSlug = repoFlag
}
if len(remoteFlag) == 0 {
remoteFlag = config.GetPreferences().FlagDefaults.Remote
}
// Create env login before repo context detection so it participates in remote URL matching
var extraLogins []config.Login
envLogin := GetLoginByEnvVar()
if envLogin != nil {
if _, err := utils.ValidateAuthenticationMethod(envLogin.URL, envLogin.Token, "", "", false, "", ""); err != nil {
return nil, err
}
extraLogins = append(extraLogins, *envLogin)
}
if !opts.SkipLocalRepo {
// try to read local git repo & extract context: if repoFlag specifies a valid path, read repo in that dir,
// otherwise attempt PWD. if no repo is found, continue with default login
if repoPath == "" {
if repoPath, err = os.Getwd(); err != nil {
return nil, err
}
}
var localSlug string
if c.LocalRepo, c.Login, localSlug, err = contextFromLocalRepo(repoPath, remoteFlag, extraLogins); err != nil {
if err == errNotAGiteaRepo || err == git.ErrRepositoryNotExists {
// we can deal with that, commands needing the optional values use ctx.Ensure()
} else {
return nil, err
}
}
if c.RepoSlug == "" && localSlug != "" {
c.RepoSlug = localSlug
}
}
// If env vars are set, always use the env login (but repo slug was already
// resolved by contextFromLocalRepo with the env login in the match list)
if envLogin != nil {
c.Login = envLogin
}
// override login from flag, or use default login if repo based detection failed
if len(loginFlag) != 0 {
if c.Login, err = config.GetLoginByName(loginFlag); err != nil {
return nil, err
}
if c.Login == nil {
return nil, fmt.Errorf("login name '%s' does not exist", loginFlag)
}
} else if c.Login == nil {
if c.Login, err = config.GetDefaultLogin(); err != nil {
if err.Error() == "No available login" {
return nil, fmt.Errorf(`no gitea login configured. To start using tea, first run
tea login add
and then run your command again`)
}
return nil, err
}
// Only prompt for confirmation if the fallback login is not explicitly set as default
canPrompt := term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd()))
if shouldPromptFallbackLogin(c.Login, canPrompt) {
fallback := false
if err := huh.NewConfirm().
Title(fmt.Sprintf("NOTE: no login matched this repository. Fall back to login '%s'?", c.Login.Name)).
Value(&fallback).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return nil, fmt.Errorf("get confirm failed: %w", err)
}
if !fallback {
return nil, ErrCommandCanceled
}
} else if !c.Login.Default {
fmt.Fprintf(os.Stderr, "NOTE: no login matched this repository, falling back to login '%s' in non-interactive mode.\n", c.Login.Name)
}
}
// parse reposlug (owner falling back to login owner if reposlug contains only repo name)
c.Owner, c.Repo = utils.GetOwnerAndRepo(c.RepoSlug, c.Login.User)
c.Org = orgFlag
c.IsGlobal = globalFlag
c.Command = cmd
c.Output = cmd.String("output")
return &c, nil
}