Merge branch 'main' into feat/add-object-format-validation

This commit is contained in:
Lunny Xiao
2025-08-11 19:38:09 -07:00
36 changed files with 731 additions and 401 deletions

View File

@ -39,3 +39,43 @@ jobs:
GPGSIGN_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
release-image:
runs-on: ubuntu-latest
env:
DOCKER_ORG: gitea
DOCKER_LATEST: nightly
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get tag version without v
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Build and push
uses: docker/build-push-action@v6
env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
gitea/tea:${{ env.VERSION }}

View File

@ -1,63 +0,0 @@
# comparing git forge commandline interfaces
[tea]: https://gitea.com/gitea/tea
[sip]: https://gitea.com/jolheiser/sip
[gitlab]: https://github.com/makkes/gitlab-cli
[glab]: https://github.com/profclems/glab
[gh]: https://cli.github.com
last update: 2020-12-11
## general
/ | [tea][tea] | [sip][sip] | [gitlab][gitlab] | [gh][gh]
-----------------------|:-----:|:-----:|:-----:|:-----:
forge|gitea|gitea|gitlab|github
official forge support|✓|✘|✘|✓
dev status|adding features|maintenance||
platform|any|any|any|any
## philosophy
/ | [tea][tea] | [sip][sip] | [gitlab][gitlab] | [gh][gh]
-----------------------|:-----:|:-----:|:-----:|:-----:
aims to replace git cli|✘|||✓
works with decentralization in mind|✓|✓|✓|✘
per-repo setup needed|✘||✓|✘
workflow helpers|✓|||
interactive mode |[(✓)](https://gitea.com/gitea/tea/issues?type=all&state=open&labels=&milestone=0&assignee=0&q=interactive)|✘| |✓
programmatic mode|✓|||✓
machine readable output|✓|||
follows XDG spec|✓|||
## features
/ | [tea][tea] | [sip][sip] | [gitlab][gitlab] | [gh][gh]
-----------------------|:-----:|:-----:|:-----:|:-----:
open web UI|✓|||
search repos|✓|||
search issues|✘|✓||
textual item search filter syntax|✘|✓||
CRUD repos|[(✓)](https://gitea.com/gitea/tea/issues/239)|||
CRUD issues|[(✓)](https://gitea.com/gitea/tea/issues/229)|||
CRUD milestones|[(✓)](https://gitea.com/gitea/tea/issues/246)|||
CRUD releases|✓|||
CRUD labels|✓|||
CRUD PRs|✓|||
CRUD time tracking|✓|||x
CRUD orgs|[(✓)](https://gitea.com/gitea/tea/issues/287)|||
create PRs from local repo|✓|||
create PRs from remote repo|✓|||
code review|[u](https://gitea.com/gitea/tea/issues/131)|||
merge PRs||||
read comments|[u](https://gitea.com/gitea/tea/issues/172)|||
post comments||||
manage CI|✘|✘|✓|
manage notifications|[(✓)]()|||
administration|[u](https://gitea.com/gitea/tea/issues/161)|✘||✘
markdown rendering|✓|||✓
issue import/export|[u](https://gitea.com/gitea/tea/issues/132)|||
checkout PRs|✓|||
- ✓: supported
- (✓): partial support
- u: upcoming
- ✘: not supported
- ?: unknown

View File

@ -100,6 +100,22 @@ There are different ways to get `tea`:
5. asdf (thirdparty): [mvaldes14/asdf-tea](https://github.com/mvaldes14/asdf-tea)
### Log in to Gitea from tea
Gitea can use many different authentication schemes, and not every authentication method will work with every Gitea deployment. When you are a Gitea instance administrator you can tweak your settings to fit your use case. For the method that is most likely to work with any Gitea deployment use the following steps:
1. Open your Gitea instance in a web browser
2. Log in to Gitea in your web browser. Any MFA, IDP, or whatever else should be available this way.
3. In your "user settings", generate an application token with at least **user read** permissions. If you want to do anything useful with the token add additional permissions/scopes.
4. Run `tea login add`, select **application token** authentication when asked for authentication type, and answer **yes** to the question if you have a token. Paste the generated token when asked for one.
You should now be logged in to your gitea instance from tea.
Since 0.10 Gitea supports the much simpler oauth workflow but oauth may not be available on all Gitea deployments, and gets much more complex when running tea on a remote system.
### Shell completion
If you installed from source or the package does not provide the completions with it you can add them yourself with `tea completion <shell>` command which is not visible in help. To generate the completions run one of the following commands depending on your shell.

View File

@ -5,6 +5,7 @@ package cmd
import (
stdctx "context"
"errors"
"fmt"
"io"
"strings"
@ -14,10 +15,11 @@ import (
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/theme"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/huh"
"github.com/urfave/cli/v3"
)
@ -56,17 +58,22 @@ func runAddComment(_ stdctx.Context, cmd *cli.Command) error {
body = strings.Join([]string{body, string(bodyStdin)}, "\n\n")
}
} else if len(body) == 0 {
if err = survey.AskOne(interact.NewMultiline(interact.Multiline{
Message: "Comment:",
Syntax: "md",
UseEditor: config.GetPreferences().Editor,
}), &body); err != nil {
if err := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title("Comment(markdown):").
ExternalEditor(config.GetPreferences().Editor).
EditorExtension("md").
Value(&body),
),
).WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
}
if len(body) == 0 {
return fmt.Errorf("No comment body provided")
return errors.New("no comment content provided")
}
client := ctx.Login.Client()

View File

@ -30,7 +30,11 @@ func runIssuesCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.NumFlags() == 0 {
return interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo)
err := interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo)
if err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}
opts, err := flags.GetIssuePRCreateFlags(ctx)

View File

@ -53,6 +53,9 @@ func runIssuesEdit(_ stdctx.Context, cmd *cli.Command) error {
var err error
opts, err = interact.EditIssue(*ctx, opts.Index)
if err != nil {
if interact.IsQuitting(err) {
return nil // user quit
}
return err
}
}

View File

@ -5,6 +5,7 @@ package login
import (
"context"
"fmt"
"code.gitea.io/tea/modules/auth"
"code.gitea.io/tea/modules/interact"
@ -112,7 +113,10 @@ var CmdLoginAdd = cli.Command{
func runLoginAdd(_ context.Context, cmd *cli.Command) error {
// if no args create login interactive
if cmd.NumFlags() == 0 {
return interact.CreateLogin()
if err := interact.CreateLogin(); err != nil && !interact.IsQuitting(err) {
return fmt.Errorf("error adding login: %w", err)
}
return nil
}
// if OAuth flag is provided, use OAuth2 PKCE flow

View File

@ -68,7 +68,10 @@ func runMilestonesCreate(_ stdctx.Context, cmd *cli.Command) error {
}
if ctx.NumFlags() == 0 {
return interact.CreateMilestone(ctx.Login, ctx.Owner, ctx.Repo)
if err := interact.CreateMilestone(ctx.Login, ctx.Owner, ctx.Repo); err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}
return task.CreateMilestone(

View File

@ -47,5 +47,8 @@ func runPullsCheckout(_ stdctx.Context, cmd *cli.Command) error {
return err
}
return task.PullCheckout(ctx.Login, ctx.Owner, ctx.Repo, ctx.Bool("branch"), idx, interact.PromptPassword)
if err := task.PullCheckout(ctx.Login, ctx.Owner, ctx.Repo, ctx.Bool("branch"), idx, interact.PromptPassword); err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}

View File

@ -43,5 +43,8 @@ func runPullsClean(_ stdctx.Context, cmd *cli.Command) error {
return err
}
return task.PullClean(ctx.Login, ctx.Owner, ctx.Repo, idx, ctx.Bool("ignore-sha"), interact.PromptPassword)
if err := task.PullClean(ctx.Login, ctx.Owner, ctx.Repo, idx, ctx.Bool("ignore-sha"), interact.PromptPassword); err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}

View File

@ -44,7 +44,10 @@ func runPullsCreate(_ stdctx.Context, cmd *cli.Command) error {
// no args -> interactive mode
if ctx.NumFlags() == 0 {
return interact.CreatePull(ctx)
if err := interact.CreatePull(ctx); err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}
// else use args to create PR

View File

@ -46,7 +46,10 @@ var CmdPullsMerge = cli.Command{
if ctx.Args().Len() != 1 {
// If no PR index is provided, try interactive mode
return interact.MergePull(ctx)
if err := interact.MergePull(ctx); err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}
idx, err := utils.ArgToIndex(ctx.Args().First())

View File

@ -26,7 +26,7 @@ var CmdPullsReview = cli.Command{
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() != 1 {
return fmt.Errorf("Must specify a PR index")
return fmt.Errorf("must specify a PR index")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
@ -34,7 +34,10 @@ var CmdPullsReview = cli.Command{
return err
}
return interact.ReviewPull(ctx, idx)
if err := interact.ReviewPull(ctx, idx); err != nil && !interact.IsQuitting(err) {
return err
}
return nil
},
Flags: flags.AllDefaultFlags,
}

View File

@ -10,7 +10,7 @@ import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/huh"
"github.com/urfave/cli/v3"
)
@ -53,7 +53,6 @@ func runRepoDelete(_ stdctx.Context, cmd *cli.Command) error {
var owner string
if ctx.IsSet("owner") {
owner = ctx.String("owner")
} else {
owner = ctx.Login.User
}
@ -64,15 +63,16 @@ func runRepoDelete(_ stdctx.Context, cmd *cli.Command) error {
if !ctx.Bool("force") {
var enteredRepoSlug string
promptRepoName := &survey.Input{
Message: fmt.Sprintf("Confirm the deletion of the repository '%s' by typing its name: ", repoSlug),
}
if err := survey.AskOne(promptRepoName, &enteredRepoSlug, survey.WithValidator(survey.Required)); err != nil {
if err := huh.NewInput().
Title(fmt.Sprintf("Confirm the deletion of the repository '%s' by typing its name: ", repoSlug)).
Validate(huh.ValidateNotEmpty()).
Value(&enteredRepoSlug).
Run(); err != nil {
return err
}
if enteredRepoSlug != repoSlug {
return fmt.Errorf("Entered wrong repository name '%s', expected '%s'", enteredRepoSlug, repoSlug)
return fmt.Errorf("entered wrong repository name '%s', expected '%s'", enteredRepoSlug, repoSlug)
}
}

16
go.mod
View File

@ -8,10 +8,10 @@ require (
code.gitea.io/gitea-vet v0.2.3
code.gitea.io/sdk/gitea v0.21.0
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/adrg/xdg v0.5.3
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/huh v0.7.0
github.com/enescakir/emoji v1.0.0
github.com/go-git/go-git/v5 v5.16.2
github.com/muesli/termenv v0.16.0
@ -32,13 +32,18 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.0 // indirect
github.com/charmbracelet/bubbletea v1.3.5 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
@ -46,7 +51,9 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
@ -55,14 +62,16 @@ require (
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.8 // indirect
@ -77,6 +86,7 @@ require (
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.33.0 // indirect

69
go.sum
View File

@ -8,13 +8,11 @@ gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c h1:8fTkq2UaV
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c/go.mod h1:Fc8iyPm4NINRWujeIk2bTfcbGc4ZYY29/oMAAGcr4qI=
github.com/42wim/httpsig v1.2.2 h1:ofAYoHUNs/MJOLqQ8hIxeyz2QxOz8qdSVvp3PX/oPgA=
github.com/42wim/httpsig v1.2.2/go.mod h1:P/UYo7ytNBFwc+dg35IubuAUIs8zj5zzFIgUCEl55WY=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
@ -31,34 +29,54 @@ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhP
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc=
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -68,12 +86,16 @@ github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454Wv
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog=
github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@ -98,12 +120,8 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -115,21 +133,24 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
@ -169,7 +190,6 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:s
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@ -182,7 +202,6 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
@ -192,14 +211,12 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -207,45 +224,37 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -17,8 +17,9 @@ import (
"time"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/theme"
"code.gitea.io/tea/modules/utils"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/huh"
"golang.org/x/oauth2"
)
@ -278,8 +279,13 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client {
options = append(options, gitea.SetToken(l.Token), gitea.SetHTTPClient(httpClient))
if ok, err := utils.IsKeyEncrypted(l.SSHKey); ok && err == nil && l.SSHPassphrase == "" {
promptPW := &survey.Password{Message: "ssh-key is encrypted please enter the passphrase: "}
if err = survey.AskOne(promptPW, &l.SSHPassphrase, survey.WithValidator(survey.Required)); err != nil {
if err := huh.NewInput().
Title("ssh-key is encrypted please enter the passphrase: ").
Validate(huh.ValidateNotEmpty()).
EchoMode(huh.EchoModePassword).
Value(&l.SSHPassphrase).
WithTheme(theme.GetTheme()).
Run(); err != nil {
log.Fatal(err)
}
}

View File

@ -9,7 +9,9 @@ import (
"log"
"os"
"path"
"strconv"
"strings"
"time"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config"
@ -20,9 +22,7 @@ import (
"github.com/urfave/cli/v3"
)
var (
errNotAGiteaRepo = errors.New("No Gitea login found. You might want to specify --repo (and --login) to work outside of a repository")
)
var errNotAGiteaRepo = errors.New("No Gitea login found. You might want to specify --repo (and --login) to work outside of a repository")
// TeaContext contains all context derived during command initialization and wraps cli.Context
type TeaContext struct {
@ -125,6 +125,16 @@ func InitCommand(cmd *cli.Command) *TeaContext {
c.RepoSlug = repoFlag
}
// override config user with env variable
envLogin := GetLoginByEnvVar()
if envLogin != nil {
_, err := utils.ValidateAuthenticationMethod(envLogin.URL, envLogin.Token, "", "", false, "", "")
if err != nil {
log.Fatal(err.Error())
}
c.Login = envLogin
}
// override login from flag, or use default login if repo based detection failed
if len(loginFlag) != 0 {
c.Login = config.GetLoginByName(loginFlag)
@ -230,3 +240,40 @@ func contextFromLocalRepo(repoPath, remoteValue string) (*git.TeaRepo, *config.L
return repo, nil, "", errNotAGiteaRepo
}
// GetLoginByEnvVar returns a login based on environment variables, or nil if no login can be created
func GetLoginByEnvVar() *config.Login {
var token string
giteaToken := os.Getenv("GITEA_TOKEN")
githubToken := os.Getenv("GH_TOKEN")
giteaInstanceURL := os.Getenv("GITEA_INSTANCE_URL")
instanceInsecure := os.Getenv("GITEA_INSTANCE_INSECURE")
insecure := false
if len(instanceInsecure) > 0 {
insecure, _ = strconv.ParseBool(instanceInsecure)
}
// if no tokens are set, or no instance url for gitea fail fast
if len(giteaInstanceURL) == 0 || (len(giteaToken) == 0 && len(githubToken) == 0) {
return nil
}
token = giteaToken
if len(giteaToken) == 0 {
token = githubToken
}
return &config.Login{
Name: "GITEA_LOGIN_VIA_ENV",
URL: giteaInstanceURL,
Token: token,
Insecure: insecure,
SSHKey: "",
SSHCertPrincipal: "",
SSHKeyFingerprint: "",
SSHAgent: false,
Created: time.Now().Unix(),
VersionCheck: false,
}
}

View File

@ -10,8 +10,9 @@ import (
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/theme"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/huh"
"golang.org/x/term"
)
@ -46,9 +47,12 @@ func ShowCommentsPaginated(ctx *context.TeaContext, idx int64, totalComments int
// NOTE: as of gitea 1.13, pagination is not provided by this endpoint, but handles
// this function gracefully anyways.
for {
loadComments := false
confirm := survey.Confirm{Message: prompt, Default: true}
if err := survey.AskOne(&confirm, &loadComments); err != nil {
loadComments := true
if err := huh.NewConfirm().
Title(prompt).
Value(&loadComments).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
} else if !loadComments {
break

View File

@ -4,19 +4,28 @@
package interact
import (
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/theme"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/huh"
)
// IsQuitting checks if the user has aborted the interactive prompt
func IsQuitting(err error) bool {
return err == huh.ErrUserAborted
}
// CreateIssue interactively creates an issue
func CreateIssue(login *config.Login, owner, repo string) error {
owner, repo, err := promptRepoSlug(owner, repo)
if err != nil {
return err
}
printTitleAndContent("Target repo:", owner+"/"+repo)
var opts gitea.CreateIssueOption
if err := promptIssueProperties(login, owner, repo, &opts); err != nil {
@ -28,29 +37,36 @@ func CreateIssue(login *config.Login, owner, repo string) error {
func promptIssueProperties(login *config.Login, owner, repo string, o *gitea.CreateIssueOption) error {
var milestoneName string
var labels []string
var err error
selectableChan := make(chan (issueSelectables), 1)
go fetchIssueSelectables(login, owner, repo, selectableChan)
// title
promptOpts := survey.WithValidator(survey.Required)
promptI := &survey.Input{Message: "Issue title:", Default: o.Title}
if err = survey.AskOne(promptI, &o.Title, promptOpts); err != nil {
if err := huh.NewInput().
Title("Issue title:").
Value(&o.Title).
Validate(huh.ValidateNotEmpty()).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Issue title:", o.Title)
// description
promptD := NewMultiline(Multiline{
Message: "Issue description:",
Default: o.Body,
Syntax: "md",
UseEditor: config.GetPreferences().Editor,
})
if err = survey.AskOne(promptD, &o.Body); err != nil {
if err := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title("Issue description(markdown):").
ExternalEditor(config.GetPreferences().Editor).
EditorExtension("md").
Value(&o.Body),
),
).WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Issue description(markdown):", o.Body)
// wait until selectables are fetched
selectables := <-selectableChan
@ -67,6 +83,7 @@ func promptIssueProperties(login *config.Login, owner, repo string, o *gitea.Cre
if o.Assignees, err = promptMultiSelect("Assignees:", selectables.Assignees, "[other]"); err != nil {
return err
}
printTitleAndContent("Assignees:", strings.Join(o.Assignees, "\n"))
// milestone
if len(selectables.MilestoneList) != 0 {
@ -74,24 +91,40 @@ func promptIssueProperties(login *config.Login, owner, repo string, o *gitea.Cre
return err
}
o.Milestone = selectables.MilestoneMap[milestoneName]
printTitleAndContent("Milestone:", milestoneName)
}
// labels
if len(selectables.LabelList) != 0 {
promptL := &survey.MultiSelect{Message: "Labels:", Options: selectables.LabelList, VimMode: true, Default: o.Labels}
if err := survey.AskOne(promptL, &labels); err != nil {
options := make([]huh.Option[int64], 0, len(selectables.LabelList))
labelsMap := make(map[int64]string, len(selectables.LabelList))
for _, l := range selectables.LabelList {
options = append(options, huh.Option[int64]{Key: l, Value: selectables.LabelMap[l]})
labelsMap[selectables.LabelMap[l]] = l
}
if err := huh.NewMultiSelect[int64]().
Title("Labels:").
Options(options...).
Value(&o.Labels).
Run(); err != nil {
return err
}
o.Labels = make([]int64, len(labels))
for i, l := range labels {
o.Labels[i] = selectables.LabelMap[l]
var labels []string
for _, labelID := range o.Labels {
labels = append(labels, labelsMap[labelID])
}
printTitleAndContent("Labels:", strings.Join(labels, "\n"))
}
// deadline
if o.Deadline, err = promptDatetime("Due date:"); err != nil {
return err
}
deadlineStr := "No due date"
if o.Deadline != nil && !o.Deadline.IsZero() {
deadlineStr = o.Deadline.Format("2006-01-02")
}
printTitleAndContent("Due date:", deadlineStr)
return nil
}

View File

@ -5,23 +5,26 @@ package interact
import (
"slices"
"strings"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/theme"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/huh"
)
// EditIssue interactively edits an issue
func EditIssue(ctx context.TeaContext, index int64) (*task.EditIssueOption, error) {
var opts = task.EditIssueOption{}
opts := task.EditIssueOption{}
var err error
ctx.Owner, ctx.Repo, err = promptRepoSlug(ctx.Owner, ctx.Repo)
if err != nil {
return &opts, err
}
printTitleAndContent("Target repo:", ctx.Owner+"/"+ctx.Repo)
c := ctx.Login.Client()
i, _, err := c.GetIssue(ctx.Owner, ctx.Repo, index)
@ -68,25 +71,31 @@ func promptIssueEditProperties(ctx *context.TeaContext, o *task.EditIssueOption)
go fetchIssueSelectables(ctx.Login, ctx.Owner, ctx.Repo, selectableChan)
// title
promptOpts := survey.WithValidator(survey.Required)
promptI := &survey.Input{Message: "Issue title:", Default: *o.Title}
if err = survey.AskOne(promptI, o.Title, promptOpts); err != nil {
if err := huh.NewInput().
Title("Issue title:").
Value(o.Title).
Validate(huh.ValidateNotEmpty()).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Issue title:", *o.Title)
// description
promptD := NewMultiline(Multiline{
Message: "Issue description:",
Default: *o.Body,
Syntax: "md",
UseEditor: config.GetPreferences().Editor,
EditorAppendDefault: true,
EditorHideDefault: true,
})
if err = survey.AskOne(promptD, o.Body); err != nil {
if err := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title("Issue description(markdown):").
ExternalEditor(config.GetPreferences().Editor).
EditorExtension("md").
Value(o.Body),
),
).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Issue description(markdown):", *o.Body)
// wait until selectables are fetched
selectables := <-selectableChan
@ -112,6 +121,7 @@ func promptIssueEditProperties(ctx *context.TeaContext, o *task.EditIssueOption)
if o.AddAssignees, err = promptMultiSelect("Add Assignees:", newAssignees, "[other]"); err != nil {
return err
}
printTitleAndContent("Assignees:", strings.Join(o.AddAssignees, "\n"))
// milestone
if len(selectables.MilestoneList) != 0 {
@ -123,14 +133,22 @@ func promptIssueEditProperties(ctx *context.TeaContext, o *task.EditIssueOption)
return err
}
o.Milestone = &milestoneName
printTitleAndContent("Milestone:", milestoneName)
}
// labels
if len(selectables.LabelList) != 0 {
promptL := &survey.MultiSelect{Message: "Labels:", Options: selectables.LabelList, VimMode: true, Default: o.AddLabels}
if err := survey.AskOne(promptL, &labelsSelected); err != nil {
copy(labelsSelected, o.AddLabels)
if err := huh.NewMultiSelect[string]().
Title("Labels:").
Options(huh.NewOptions(selectables.LabelList...)...).
Value(&labelsSelected).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Labels:", strings.Join(labelsSelected, "\n"))
// removed labels
for _, l := range o.AddLabels {
if !slices.Contains(labelsSelected, l) {
@ -148,6 +166,11 @@ func promptIssueEditProperties(ctx *context.TeaContext, o *task.EditIssueOption)
if o.Deadline, err = promptDatetime("Due date:"); err != nil {
return err
}
deadlineStr := "No due date"
if o.Deadline != nil && !o.Deadline.IsZero() {
deadlineStr = o.Deadline.Format("2006-01-02")
}
printTitleAndContent("Due date:", deadlineStr)
return nil
}

View File

@ -4,113 +4,180 @@
package interact
import (
"errors"
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/auth"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/theme"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/huh"
)
// CreateLogin create an login interactive
func CreateLogin() error {
var (
name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint string
insecure, sshAgent, versionCheck, helper bool
name, token, user, passwd, otp, scopes, sshKey, sshCertPrincipal, sshKeyFingerprint string
insecure, sshAgent, versionCheck, helper bool
)
versionCheck = true
helper = false
promptI := &survey.Input{Message: "URL of Gitea instance: "}
if err := survey.AskOne(promptI, &giteaURL, survey.WithValidator(survey.Required)); err != nil {
giteaURL := "https://gitea.com"
if err := huh.NewInput().
Title("URL of Gitea instance: ").
Value(&giteaURL).
Validate(func(s string) error {
s = strings.TrimSpace(s)
if len(s) == 0 {
return fmt.Errorf("URL is required")
}
_, err := url.Parse(s)
if err != nil {
return fmt.Errorf("Invalid URL: %v", err)
}
return nil
}).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("URL of Gitea instance: ", giteaURL)
giteaURL = strings.TrimSuffix(strings.TrimSpace(giteaURL), "/")
if len(giteaURL) == 0 {
fmt.Println("URL is required!")
return nil
}
name, err := task.GenerateLoginName(giteaURL, "")
if err != nil {
return err
}
promptI = &survey.Input{Message: "Name of new Login: ", Default: name}
if err := survey.AskOne(promptI, &name); err != nil {
if err := huh.NewInput().
Title("Name of new Login: ").
Value(&name).
Validate(huh.ValidateNotEmpty()).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Name of new Login: ", name)
loginMethod, err := promptSelectV2("Login with: ", []string{"token", "ssh-key/certificate", "oauth"})
if err != nil {
return err
}
printTitleAndContent("Login with: ", loginMethod)
switch loginMethod {
case "oauth":
promptYN := &survey.Confirm{
Message: "Allow Insecure connections: ",
Default: false,
}
if err = survey.AskOne(promptYN, &insecure); err != nil {
if err := huh.NewConfirm().
Title("Allow Insecure connections:").
Value(&insecure).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Allow Insecure connections:", strconv.FormatBool(insecure))
return auth.OAuthLoginWithOptions(name, giteaURL, insecure)
default: // token
var hasToken bool
promptYN := &survey.Confirm{
Message: "Do you have an access token?",
Default: false,
}
if err = survey.AskOne(promptYN, &hasToken); err != nil {
if err := huh.NewConfirm().
Title("Do you have an access token?").
Value(&hasToken).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Do you have an access token?", strconv.FormatBool(hasToken))
if hasToken {
promptI = &survey.Input{Message: "Token: "}
if err := survey.AskOne(promptI, &token, survey.WithValidator(survey.Required)); err != nil {
if err := huh.NewInput().
Title("Token:").
Value(&token).
Validate(huh.ValidateNotEmpty()).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Token:", token)
} else {
promptI = &survey.Input{Message: "Username: "}
if err = survey.AskOne(promptI, &user, survey.WithValidator(survey.Required)); err != nil {
if err := huh.NewInput().
Title("Username:").
Value(&user).
Validate(huh.ValidateNotEmpty()).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Username:", user)
promptPW := &survey.Password{Message: "Password: "}
if err = survey.AskOne(promptPW, &passwd, survey.WithValidator(survey.Required)); err != nil {
if err := huh.NewInput().
Title("Password:").
Value(&passwd).
Validate(huh.ValidateNotEmpty()).
EchoMode(huh.EchoModePassword).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Password:", "********")
var tokenScopes []string
promptS := &survey.MultiSelect{Message: "Token Scopes:", Options: tokenScopeOpts}
if err := survey.AskOne(promptS, &tokenScopes, survey.WithValidator(survey.Required)); err != nil {
if err := huh.NewMultiSelect[string]().
Title("Token Scopes:").
Options(huh.NewOptions(tokenScopeOpts...)...).
Value(&tokenScopes).
Validate(func(s []string) error {
if len(s) == 0 {
return errors.New("At least one scope is required")
}
return nil
}).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Token Scopes:", strings.Join(tokenScopes, "\n"))
scopes = strings.Join(tokenScopes, ",")
// Ask for OTP last so it's less likely to timeout
promptO := &survey.Input{Message: "OTP (if applicable)"}
if err := survey.AskOne(promptO, &otp); err != nil {
if err := huh.NewInput().
Title("OTP (if applicable):").
Value(&otp).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("OTP (if applicable):", otp)
}
case "ssh-key/certificate":
promptI = &survey.Input{Message: "SSH Key/Certificate Path (leave empty for auto-discovery in ~/.ssh and ssh-agent):"}
if err := survey.AskOne(promptI, &sshKey); err != nil {
if err := huh.NewInput().
Title("SSH Key/Certificate Path (leave empty for auto-discovery in ~/.ssh and ssh-agent):").
Value(&sshKey).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("SSH Key/Certificate Path (leave empty for auto-discovery in ~/.ssh and ssh-agent):", sshKey)
if sshKey == "" {
sshKey, err = promptSelect("Select ssh-key: ", task.ListSSHPubkey(), "", "", "")
pubKeys := task.ListSSHPubkey()
if len(pubKeys) == 0 {
fmt.Println("No SSH keys found in ~/.ssh or ssh-agent")
return nil
}
sshKey, err = promptSelect("Select ssh-key: ", pubKeys, "", "", "")
if err != nil {
return err
}
printTitleAndContent("Selected ssh-key:", sshKey)
// ssh certificate
if strings.Contains(sshKey, "principals") {
@ -136,42 +203,51 @@ func CreateLogin() error {
}
var optSettings bool
promptYN := &survey.Confirm{
Message: "Set Optional settings: ",
Default: false,
}
if err = survey.AskOne(promptYN, &optSettings); err != nil {
if err := huh.NewConfirm().
Title("Set Optional settings:").
Value(&optSettings).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Set Optional settings:", strconv.FormatBool(optSettings))
if optSettings {
promptI = &survey.Input{Message: "SSH Key Path (leave empty for auto-discovery):"}
if err := survey.AskOne(promptI, &sshKey); err != nil {
if err := huh.NewInput().
Title("SSH Key Path (leave empty for auto-discovery):").
Value(&sshKey).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("SSH Key Path (leave empty for auto-discovery):", sshKey)
promptYN = &survey.Confirm{
Message: "Allow Insecure connections: ",
Default: false,
}
if err = survey.AskOne(promptYN, &insecure); err != nil {
if err := huh.NewConfirm().
Title("Allow Insecure connections:").
Value(&insecure).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Allow Insecure connections:", strconv.FormatBool(insecure))
promptYN = &survey.Confirm{
Message: "Add git helper: ",
Default: false,
}
if err = survey.AskOne(promptYN, &helper); err != nil {
if err := huh.NewConfirm().
Title("Add git helper:").
Value(&helper).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Add git helper:", strconv.FormatBool(helper))
promptYN = &survey.Confirm{
Message: "Check version of Gitea instance: ",
Default: true,
}
if err = survey.AskOne(promptYN, &versionCheck); err != nil {
if err := huh.NewConfirm().
Title("Check version of Gitea instance:").
Value(&versionCheck).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Check version of Gitea instance:", strconv.FormatBool(versionCheck))
}
return task.CreateLogin(name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint, insecure, sshAgent, versionCheck, helper)

View File

@ -4,46 +4,59 @@
package interact
import (
"fmt"
"time"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/theme"
"code.gitea.io/sdk/gitea"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/huh"
)
// CreateMilestone interactively creates a milestone
func CreateMilestone(login *config.Login, owner, repo string) error {
var title, description string
var deadline *time.Time
var title, description, deadline string
// owner, repo
owner, repo, err := promptRepoSlug(owner, repo)
if err != nil {
return err
}
printTitleAndContent("Target repo:", fmt.Sprintf("%s/%s", owner, repo))
// title
promptOpts := survey.WithValidator(survey.Required)
promptI := &survey.Input{Message: "Milestone title:"}
if err := survey.AskOne(promptI, &title, promptOpts); err != nil {
if err := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Milestone title:").
Validate(huh.ValidateNotEmpty()).
Value(&title),
huh.NewText().
Title("Milestone description(markdown):").
ExternalEditor(config.GetPreferences().Editor).
EditorExtension("md").
Value(&description),
huh.NewInput().
Title("Milestone deadline:").
Placeholder("YYYY-MM-DD").
Validate(func(s string) error {
if s == "" {
return nil // no deadline
}
_, err := time.Parse("2006-01-02", s)
return err
}).
Value(&deadline),
),
).WithTheme(theme.GetTheme()).Run(); err != nil {
return err
}
// description
promptM := NewMultiline(Multiline{
Message: "Milestone description:",
Syntax: "md",
UseEditor: config.GetPreferences().Editor,
})
if err := survey.AskOne(promptM, &description); err != nil {
return err
}
// deadline
if deadline, err = promptDatetime("Milestone deadline:"); err != nil {
return err
var deadlineTM *time.Time
if deadline != "" {
tm, _ := time.Parse("2006-01-02", deadline)
deadlineTM = &tm
}
return task.CreateMilestone(
@ -52,6 +65,6 @@ func CreateMilestone(login *config.Login, owner, repo string) error {
repo,
title,
description,
deadline,
deadlineTM,
gitea.StateOpen)
}

20
modules/interact/print.go Normal file
View File

@ -0,0 +1,20 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package interact
import (
"fmt"
"code.gitea.io/tea/modules/theme"
"github.com/charmbracelet/lipgloss"
)
// printTitleAndContent prints a title and content with the gitea theme
func printTitleAndContent(title, content string) {
style := lipgloss.NewStyle().
Foreground(theme.GetTheme().Blurred.Title.GetForeground()).Bold(true).
Padding(0, 1)
fmt.Print(style.Render(title), content+"\n")
}

View File

@ -5,45 +5,23 @@ package interact
import (
"fmt"
"slices"
"strings"
"time"
"code.gitea.io/tea/modules/theme"
"code.gitea.io/tea/modules/utils"
"github.com/AlecAivazis/survey/v2"
"github.com/araddon/dateparse"
"github.com/charmbracelet/huh"
)
// Multiline represents options for a prompt that expects multiline input
type Multiline struct {
Message string
Default string
Syntax string
UseEditor bool
EditorAppendDefault bool
EditorHideDefault bool
}
// NewMultiline creates a prompt that switches between the inline multiline text
// and a texteditor based prompt
func NewMultiline(opts Multiline) (prompt survey.Prompt) {
if opts.UseEditor {
prompt = &survey.Editor{
Message: opts.Message,
Default: opts.Default,
FileName: "*." + opts.Syntax,
AppendDefault: opts.EditorAppendDefault,
HideDefault: opts.EditorHideDefault,
}
} else {
prompt = &survey.Multiline{Message: opts.Message, Default: opts.Default}
}
return
}
// PromptPassword asks for a password and blocks until input was made.
func PromptPassword(name string) (pass string, err error) {
promptPW := &survey.Password{Message: name + " password:"}
err = survey.AskOne(promptPW, &pass, survey.WithValidator(survey.Required))
err = huh.NewInput().
Title(name + " password:").
Validate(huh.ValidateNotEmpty()).EchoMode(huh.EchoModePassword).
Value(&pass).
WithTheme(theme.GetTheme()).
Run()
return
}
@ -60,28 +38,21 @@ func promptRepoSlug(defaultOwner, defaultRepo string) (owner, repo string, err e
owner = defaultOwner
repo = defaultRepo
repoSlug = defaultVal
err = survey.AskOne(
&survey.Input{
Message: prompt,
Default: defaultVal,
},
&repoSlug,
survey.WithValidator(func(input interface{}) error {
if str, ok := input.(string); ok {
if !required && len(str) == 0 {
return nil
}
split := strings.Split(str, "/")
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
return fmt.Errorf("must follow the <owner>/<repo> syntax")
}
} else {
return fmt.Errorf("invalid result type")
err = huh.NewInput().
Title(prompt).
Value(&repoSlug).
Validate(func(str string) error {
if !required && len(str) == 0 {
return nil
}
split := strings.Split(str, "/")
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
return fmt.Errorf("must follow the <owner>/<repo> syntax")
}
return nil
}),
)
}).WithTheme(theme.GetTheme()).Run()
if err == nil && len(repoSlug) != 0 {
repoSlugSplit := strings.Split(repoSlug, "/")
@ -94,38 +65,39 @@ func promptRepoSlug(defaultOwner, defaultRepo string) (owner, repo string, err e
// promptDatetime prompts for a date or datetime string.
// Supports all formats understood by araddon/dateparse.
func promptDatetime(prompt string) (val *time.Time, err error) {
var input string
err = survey.AskOne(
&survey.Input{Message: prompt},
&input,
survey.WithValidator(func(input interface{}) error {
if str, ok := input.(string); ok {
if len(str) == 0 {
return nil
}
t, err := dateparse.ParseAny(str)
if err != nil {
return err
}
val = &t
} else {
return fmt.Errorf("invalid result type")
var date string
if err := huh.NewInput().
Title(prompt).
Placeholder("YYYY-MM-DD").
Validate(func(s string) error {
if s == "" {
return nil
}
return nil
}),
)
return
_, err := time.Parse("2006-01-02", s)
return err
}).
Value(&date).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return nil, err
}
if date == "" {
return nil, nil // no date
}
t, _ := time.Parse("2006-01-02", date)
return &t, nil
}
// promptSelect creates a generic multiselect prompt, with processing of custom values.
func promptMultiSelect(prompt string, options []string, customVal string) ([]string, error) {
var selection []string
promptA := &survey.MultiSelect{
Message: prompt,
Options: makeSelectOpts(options, customVal, ""),
VimMode: true,
}
if err := survey.AskOne(promptA, &selection); err != nil {
if err := huh.NewMultiSelect[string]().
Title(prompt).
Options(huh.NewOptions(makeSelectOpts(options, customVal, "")...)...).
Value(&selection).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return nil, err
}
return promptCustomVal(prompt, customVal, selection)
@ -136,14 +108,13 @@ func promptSelectV2(prompt string, options []string) (string, error) {
if len(options) == 0 {
return "", nil
}
var selection string
promptA := &survey.Select{
Message: prompt,
Options: options,
VimMode: true,
Default: options[0],
}
if err := survey.AskOne(promptA, &selection); err != nil {
selection := options[0]
if err := huh.NewSelect[string]().
Title(prompt).
Options(huh.NewOptions(options...)...).
Value(&selection).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return "", err
}
return selection, nil
@ -154,17 +125,20 @@ func promptSelect(prompt string, options []string, customVal, noneVal, defaultVa
var selection string
if defaultVal == "" && noneVal != "" {
defaultVal = noneVal
}
promptA := &survey.Select{
Message: prompt,
Options: makeSelectOpts(options, customVal, noneVal),
VimMode: true,
Default: defaultVal,
if len(options) > 0 && !slices.Contains(options, defaultVal) {
defaultVal = options[0]
}
if err := survey.AskOne(promptA, &selection); err != nil {
selection = defaultVal
if err := huh.NewSelect[string]().
Title(prompt).
Options(huh.NewOptions(makeSelectOpts(options, customVal, noneVal)...)...).
Value(&selection).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return "", err
}
if noneVal != "" && selection == noneVal {
return "", nil
}
@ -193,11 +167,14 @@ func makeSelectOpts(opts []string, customVal, noneVal string) []string {
// for custom input to add to the selection instead.
func promptCustomVal(prompt, customVal string, selection []string) ([]string, error) {
// check for custom value & prompt again with text input
// HACK until https://github.com/AlecAivazis/survey/issues/339 is implemented
if otherIndex := utils.IndexOf(selection, customVal); otherIndex != -1 {
var customAssignees string
promptA := &survey.Input{Message: prompt, Help: "comma separated list"}
if err := survey.AskOne(promptA, &customAssignees); err != nil {
if err := huh.NewInput().
Title(prompt).
Description("comma separated list").
Value(&customAssignees).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return nil, err
}
selection = append(selection[:otherIndex], selection[otherIndex+1:]...)

View File

@ -8,14 +8,14 @@ import (
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/huh"
)
// CreatePull interactively creates a PR
func CreatePull(ctx *context.TeaContext) (err error) {
var (
base, head string
allowMaintainerEdits bool
allowMaintainerEdits = true
)
// owner, repo
@ -27,32 +27,37 @@ func CreatePull(ctx *context.TeaContext) (err error) {
if base, err = task.GetDefaultPRBase(ctx.Login, ctx.Owner, ctx.Repo); err != nil {
return err
}
promptI := &survey.Input{Message: "Target branch:", Default: base}
if err := survey.AskOne(promptI, &base); err != nil {
return err
}
// head
var headOwner, headBranch string
promptOpts := survey.WithValidator(survey.Required)
validator := huh.ValidateNotEmpty()
if ctx.LocalRepo != nil {
headOwner, headBranch, err = task.GetDefaultPRHead(ctx.LocalRepo)
if err == nil {
promptOpts = nil
validator = nil
}
}
promptI = &survey.Input{Message: "Source repo owner:", Default: headOwner}
if err := survey.AskOne(promptI, &headOwner); err != nil {
return err
}
promptI = &survey.Input{Message: "Source branch:", Default: headBranch}
if err := survey.AskOne(promptI, &headBranch, promptOpts); err != nil {
return err
}
promptC := &survey.Confirm{Message: "Allow Maintainers to push to the base branch", Default: true}
if err := survey.AskOne(promptC, &allowMaintainerEdits); err != nil {
if err := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Target branch:").
Value(&base).
Validate(huh.ValidateNotEmpty()),
huh.NewInput().
Title("Source repo owner:").
Value(&headOwner),
huh.NewInput().
Title("Source branch:").
Value(&headBranch).
Validate(validator),
huh.NewConfirm().
Title("Allow maintainers to push to the base branch:").
Value(&allowMaintainerEdits),
),
).Run(); err != nil {
return err
}

View File

@ -7,12 +7,12 @@ import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"github.com/AlecAivazis/survey/v2"
"code.gitea.io/sdk/gitea"
"github.com/charmbracelet/huh"
)
// MergePull interactively creates a PR
@ -76,15 +76,15 @@ func getPullIndex(ctx *context.TeaContext, branch string) (int64, error) {
prOptions = append(prOptions, loadMoreOption)
q := &survey.Select{
Message: "Select a PR to merge",
Options: prOptions,
PageSize: 10,
}
err = survey.AskOne(q, &selected)
if err != nil {
if err := huh.NewSelect[string]().
Title("Select a PR to merge:").
Options(huh.NewOptions(prOptions...)...).
Value(&selected).
Filtering(true).
Run(); err != nil {
return 0, err
}
if selected != loadMoreOption {
break
}

View File

@ -6,13 +6,15 @@ package interact
import (
"fmt"
"os"
"strconv"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/theme"
"code.gitea.io/sdk/gitea"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/huh"
)
var reviewStates = map[string]gitea.ReviewStateType{
@ -30,11 +32,16 @@ func ReviewPull(ctx *context.TeaContext, idx int64) error {
var err error
// codeComments
var reviewDiff bool
promptDiff := &survey.Confirm{Message: "Review / comment the diff?", Default: true}
if err = survey.AskOne(promptDiff, &reviewDiff); err != nil {
reviewDiff := true
if err := huh.NewConfirm().
Title("Review / comment the diff?").
Value(&reviewDiff).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Review / comment the diff?", strconv.FormatBool(reviewDiff))
if reviewDiff {
if codeComments, err = DoDiffReview(ctx, idx); err != nil {
fmt.Printf("Error during diff review: %s\n", err)
@ -44,25 +51,31 @@ func ReviewPull(ctx *context.TeaContext, idx int64) error {
// state
var stateString string
promptState := &survey.Select{Message: "Your assessment:", Options: reviewStateOptions, VimMode: true}
if err = survey.AskOne(promptState, &stateString); err != nil {
if err := huh.NewSelect[string]().
Title("Your assessment:").
Options(huh.NewOptions(reviewStateOptions...)...).
Value(&stateString).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
printTitleAndContent("Your assessment:", stateString)
state = reviewStates[stateString]
// comment
var promptOpts survey.AskOpt
field := huh.NewText().
Title("Concluding comment(markdown):").
ExternalEditor(config.GetPreferences().Editor).
EditorExtension("md").
Value(&comment)
if (state == gitea.ReviewStateComment && len(codeComments) == 0) || state == gitea.ReviewStateRequestChanges {
promptOpts = survey.WithValidator(survey.Required)
field = field.Validate(huh.ValidateNotEmpty())
}
err = survey.AskOne(NewMultiline(Multiline{
Message: "Concluding comment:",
Syntax: "md",
UseEditor: config.GetPreferences().Editor,
}), &comment, promptOpts)
if err != nil {
if err := huh.NewForm(huh.NewGroup(field)).WithTheme(theme.GetTheme()).Run(); err != nil {
return err
}
printTitleAndContent("Concluding comment(markdown):", comment)
return task.CreatePullReview(ctx, idx, state, comment, codeComments)
}

View File

@ -4,9 +4,18 @@
package print
import (
"fmt"
"code.gitea.io/sdk/gitea"
)
func formatByteSize(size int64) string {
if size < 1024 {
return fmt.Sprintf("%d B", size)
}
return formatSize(size / 1024)
}
// ReleaseAttachmentsList prints a listing of release attachments
func ReleaseAttachmentsList(attachments []*gitea.Attachment, output string) {
t := tableWithHeader(
@ -17,7 +26,7 @@ func ReleaseAttachmentsList(attachments []*gitea.Attachment, output string) {
for _, attachment := range attachments {
t.addRow(
attachment.Name,
formatSize(attachment.Size),
formatByteSize(attachment.Size),
)
}

View File

@ -29,17 +29,17 @@ func getRepoURL(resourceURL string) string {
// formatSize get kb in int and return string
func formatSize(kb int64) string {
if kb < 1024 {
return fmt.Sprintf("%d Kb", kb)
return fmt.Sprintf("%d KB", kb)
}
mb := kb / 1024
if mb < 1024 {
return fmt.Sprintf("%d Mb", mb)
return fmt.Sprintf("%d MB", mb)
}
gb := mb / 1024
if gb < 1024 {
return fmt.Sprintf("%d Gb", gb)
return fmt.Sprintf("%d GB", gb)
}
return fmt.Sprintf("%d Tb", gb/1024)
return fmt.Sprintf("%d TB", gb/1024)
}
// FormatTime provides a string for the given time value.

View File

@ -15,7 +15,7 @@ func MilestoneDetails(milestone *gitea.Milestone) {
milestone.Title,
)
if len(milestone.Description) != 0 {
fmt.Printf("\n%s\n", milestone.Description)
outputMarkdown(milestone.Description, "")
}
if milestone.Deadline != nil && !milestone.Deadline.IsZero() {
fmt.Printf("\nDeadline: %s\n", FormatTime(*milestone.Deadline, false))
@ -24,7 +24,7 @@ func MilestoneDetails(milestone *gitea.Milestone) {
// MilestonesList prints a listing of milestones
func MilestonesList(news []*gitea.Milestone, output string, fields []string) {
var printables = make([]printable, len(news))
printables := make([]printable, len(news))
for i, x := range news {
printables[i] = &printableMilestone{x}
}

View File

@ -14,7 +14,7 @@ func ReleasesList(releases []*gitea.Release, output string) {
"Title",
"Published At",
"Status",
"Tar URL",
"Tar/Zip URL",
)
for _, release := range releases {
@ -29,7 +29,7 @@ func ReleasesList(releases []*gitea.Release, output string) {
release.Title,
FormatTime(release.PublishedAt, isMachineReadable(output)),
status,
release.TarURL,
release.TarURL+"\n"+release.ZipURL,
)
}

View File

@ -13,7 +13,6 @@ import (
// CreateIssue creates an issue in the given repo and prints the result
func CreateIssue(login *config.Login, repoOwner, repoName string, opts gitea.CreateIssueOption) error {
// title is required
if len(opts.Title) == 0 {
return fmt.Errorf("Title is required")

View File

@ -63,21 +63,17 @@ func CreateLogin(name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCe
return fmt.Errorf("token already been used, delete login '%s' first", login.Name)
}
if !sshAgent && sshCertPrincipal == "" && sshKey == "" {
// .. if we have enough information to authenticate
if len(token) == 0 && (len(user)+len(passwd)) == 0 {
return fmt.Errorf("No token set")
} else if len(user) != 0 && len(passwd) == 0 {
return fmt.Errorf("No password set")
} else if len(user) == 0 && len(passwd) != 0 {
return fmt.Errorf("No user set")
}
}
// Normalize URL
serverURL, err := utils.NormalizeURL(giteaURL)
serverURL, err := utils.ValidateAuthenticationMethod(
giteaURL,
token,
user,
passwd,
sshAgent,
sshKey,
sshCertPrincipal,
)
if err != nil {
return fmt.Errorf("Unable to parse URL: %s", err)
return err
}
// check if it's a certificate the principal doesn't matter as the user

23
modules/theme/theme.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package theme
import (
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)
var giteaTheme = func() *huh.Theme {
theme := huh.ThemeCharm()
title := lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"}
theme.Focused.Title = theme.Focused.Title.Foreground(title).Bold(true)
theme.Blurred = theme.Focused
return theme
}()
// GetTheme returns the Gitea theme for Huh
func GetTheme() *huh.Theme {
return giteaTheme
}

38
modules/utils/validate.go Normal file
View File

@ -0,0 +1,38 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package utils
import (
"fmt"
"net/url"
)
// ValidateAuthenticationMethod checks the provided authentication method parameters
func ValidateAuthenticationMethod(
giteaURL string,
token string,
user string,
passwd string,
sshAgent bool,
sshKey string,
sshCertPrincipal string,
) (*url.URL, error) {
// Normalize URL
serverURL, err := NormalizeURL(giteaURL)
if err != nil {
return nil, fmt.Errorf("Unable to parse URL: %s", err)
}
if !sshAgent && sshCertPrincipal == "" && sshKey == "" {
// .. if we have enough information to authenticate
if len(token) == 0 && (len(user)+len(passwd)) == 0 {
return nil, fmt.Errorf("No token set")
} else if len(user) != 0 && len(passwd) == 0 {
return nil, fmt.Errorf("No password set")
} else if len(user) == 0 && len(passwd) != 0 {
return nil, fmt.Errorf("No user set")
}
}
return serverURL, nil
}