27 Commits

Author SHA1 Message Date
6acb29efd7 Fix yaml output single quote (#814)
Fix #659

Reviewed-on: https://gitea.com/gitea/tea/pulls/814
2025-09-14 00:23:12 +00:00
4f513ca3e3 generate man page (#811)
[CLI.md](src/branch/main/docs/CLI.md) already gets generated using `urfave/cli-docs`. `cli-docs` can also generate man pages.

This change extends the doc generator to also generate a man page for `tea`.

* Add a subcommand to the doc generator to print the generated man page to stdout

Closes #777.

Co-authored-by: Valentin Brandl <mail@vbrandl.net>
Reviewed-on: https://gitea.com/gitea/tea/pulls/811
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: TheFox0x7 <thefox0x7@noreply.gitea.com>
Co-authored-by: Valentin Brandl <vbrandl@noreply.gitea.com>
Co-committed-by: Valentin Brandl <vbrandl@noreply.gitea.com>
2025-09-14 00:17:28 +00:00
cc20b52ab3 feat: add validation for object-format flag in repo create command (#741)
This PR adds validation for the `--object-format` flag in the `repo create` command. The flag now accepts only `sha1` or `sha256` as valid values, and returns an error for any other value.

Changes:
- Added validation in `runRepoCreate` to check for valid object format values
- Added unit tests to verify the validation logic
- Fixed the field name from `ObjectFormat` to `ObjectFormatName` to match the SDK

The changes ensure that users get clear error messages when using invalid object format values, improving the user experience.

Fix #727
Fix #660
Fix #767

Co-authored-by: techknowlogick <techknowlogick@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/741
Reviewed-by: TheFox0x7 <thefox0x7@noreply.gitea.com>
2025-09-12 16:51:43 +00:00
2ca114e309 Fix release version (#815)
Reviewed-on: https://gitea.com/gitea/tea/pulls/815
Reviewed-by: TheFox0x7 <thefox0x7@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-09-11 19:18:19 +00:00
45771265c4 update gitea sdk to v0.22 (#813)
needed because of: 25b5fb0ff7
closes: https://gitea.com/gitea/tea/issues/812

Reviewed-on: https://gitea.com/gitea/tea/pulls/813
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-09-10 23:18:05 +00:00
8faa1d33f4 don't fallback login directly (#806)
Fix #499

Reviewed-on: https://gitea.com/gitea/tea/pulls/806
2025-09-10 21:30:15 +00:00
ddf5c0a5bb Check duplicated login name in interact mode when creating new login (#803)
Reviewed-on: https://gitea.com/gitea/tea/pulls/803
2025-09-10 20:42:49 +00:00
d3c73cd5dc Fix bug when output json with special chars (#801)
Fix #800

Reviewed-on: https://gitea.com/gitea/tea/pulls/801
2025-09-10 20:36:27 +00:00
6c958eec99 add debug mode and update readme (#805)
Fix #456
Fix #207

Reviewed-on: https://gitea.com/gitea/tea/pulls/805
2025-09-10 19:10:02 +00:00
d531c6fdb0 update go.mod to retract the wrong tag v1.3.3 (#802)
Fix #674

Reviewed-on: https://gitea.com/gitea/tea/pulls/802
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-27 15:38:58 +00:00
cd58296995 revert completion scripts removal (#808)
partial revert of: https://gitea.com/gitea/tea/pulls/782
closes: https://gitea.com/gitea/tea/issues/784

Old versions of tea have hardcoded completion fetched from main branch

Those should not be used from v0.10 onward.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/808
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-08-26 23:15:36 +00:00
b74405530a Remove pagination from context (#807)
Pagination related flags now write directly to ListOption struct and enforce non negative numbers.
Flag tests were added to cover the validation

Reviewed-on: https://gitea.com/gitea/tea/pulls/807
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-08-26 23:13:27 +00:00
8876fe3cb8 Continue auth when failed to open browser (#794)
When users login gitea on a headless server via ssh, xdg-open might not be installed on that machine. So tea may fail to open URL itself. In this case, users can use the other machine to open the URL for authentication.

Github CLI act like this, too.

Signed-off-by: Chen Linxuan <me@black-desk.cn>

Reviewed-on: https://gitea.com/gitea/tea/pulls/794
Reviewed-by: blumia <blumia@noreply.gitea.com>
Co-authored-by: Chen Linxuan <me@black-desk.cn>
Co-committed-by: Chen Linxuan <me@black-desk.cn>
2025-08-18 03:12:25 +00:00
07ca1ba106 Fix bug (#793)
Partially fix #791

Reviewed-on: https://gitea.com/gitea/tea/pulls/793
Reviewed-by: hiifong <i@hiif.ong>
2025-08-15 02:38:45 +00:00
d643e94a69 Fix tea login add with ssh public key bug (#789)
Fix #705

Reviewed-on: https://gitea.com/gitea/tea/pulls/789
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
2025-08-11 19:23:28 +00:00
d2ccead88b Add temporary authentication via environment variables (#639)
#633

Co-authored-by: Tim Riedl <mail@tim-riedl.de>
Co-authored-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Lunny Xiao <lunny@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/639
Co-authored-by: Tim Riedl <uvulpos@noreply.gitea.com>
Co-committed-by: Tim Riedl <uvulpos@noreply.gitea.com>
2025-08-11 18:53:09 +00:00
449b2e3117 Fix attachment size (#787)
Fix `tea releases assets <tag_name>` displayed wrong attachment size.

Reviewed-on: https://gitea.com/gitea/tea/pulls/787
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-11 18:45:12 +00:00
9e8c71e13e deploy image when tagging (#792)
Reviewed-on: https://gitea.com/gitea/tea/pulls/792
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-11 18:44:34 +00:00
2ddb3bd4a1 Add Zip URL for release list (#788)
Fix #780

Reviewed-on: https://gitea.com/gitea/tea/pulls/788
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-11 18:43:34 +00:00
4c00b8b571 Use bubbletea instead of survey for interacting with TUI (#786)
Fix #772

Reviewed-on: https://gitea.com/gitea/tea/pulls/786
Reviewed-by: Bo-Yi Wu (吳柏毅) <appleboy.tw@gmail.com>
2025-08-11 18:23:52 +00:00
c0eb30af03 capitalize a few items 2025-08-11 15:29:19 +00:00
e462acfcd6 rm out of date comparison file 2025-08-11 15:27:04 +00:00
ee111d7c12 README: Document logging in to gitea (#790)
References: #569 #675 #697 #767 #775
Reviewed-on: https://gitea.com/gitea/tea/pulls/790
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Michal Suchanek <msuchanek@suse.de>
Co-committed-by: Michal Suchanek <msuchanek@suse.de>
2025-08-11 15:25:42 +00:00
5f35cebcf1 remove autocomplete command (#782)
Add a note in readme about adding shell completions

Closes: https://gitea.com/gitea/tea/issues/781
Co-authored-by: techknowlogick <techknowlogick@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/782
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-07-21 21:24:28 +00:00
a010c9bc7f chore(deps): update ghcr.io/devcontainers/features/git-lfs docker tag to v1.2.5 (#773)
Reviewed-on: https://gitea.com/gitea/tea/pulls/773
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2025-07-21 21:22:04 +00:00
ab4ad92d40 replace arch package url (#783)
Direct users to main repository instead of AUR package

Reviewed-on: https://gitea.com/gitea/tea/pulls/783
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-07-21 21:21:25 +00:00
15052b4dcc fix: Reenable -p and --limit switches (#778)
Reduced version of #776, without the new tests.

Fixes #771.

Reviewed-on: https://gitea.com/gitea/tea/pulls/778
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Kirill Müller <kirill@cynkra.com>
Co-committed-by: Kirill Müller <kirill@cynkra.com>
2025-07-14 14:28:35 +00:00
67 changed files with 1462 additions and 777 deletions

View File

@ -2,7 +2,7 @@
"name": "Tea DevContainer", "name": "Tea DevContainer",
"image": "mcr.microsoft.com/devcontainers/go:1.24-bullseye", "image": "mcr.microsoft.com/devcontainers/go:1.24-bullseye",
"features": { "features": {
"ghcr.io/devcontainers/features/git-lfs:1.2.4": {} "ghcr.io/devcontainers/features/git-lfs:1.2.5": {}
}, },
"customizations": { "customizations": {
"vscode": { "vscode": {

View File

@ -39,3 +39,43 @@ jobs:
GPGSIGN_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }} GPGSIGN_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }} 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

@ -4,8 +4,21 @@ on:
- pull_request - pull_request
jobs: jobs:
govulncheck_job:
runs-on: ubuntu-latest
name: Run govulncheck
steps:
- id: govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-file: 'go.mod'
check-and-test: check-and-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
HTTP_PROXY: ""
GITEA_TEA_TEST_URL: "http://gitea:3000"
GITEA_TEA_TEST_USERNAME: "test01"
GITEA_TEA_TEST_PASSWORD: "test01"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
@ -20,7 +33,32 @@ jobs:
make misspell-check make misspell-check
make docs-check make docs-check
make build make build
- run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance
- name: test and coverage - name: test and coverage
run: | run: |
make test make test
make unit-test-coverage make unit-test-coverage
services:
gitea:
image: docker.gitea.com/gitea:1.24.5
cmd:
- bash
- -c
- >-
mkdir -p /tmp/conf/
&& mkdir -p /tmp/data/
&& echo "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT = true" > /tmp/conf/app.ini
&& echo "[security]" >> /tmp/conf/app.ini
&& echo "INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE1NTg4MzY4ODB9.LoKQyK5TN_0kMJFVHWUW0uDAyoGjDP6Mkup4ps2VJN4" >> /tmp/conf/app.ini
&& echo "INSTALL_LOCK = true" >> /tmp/conf/app.ini
&& echo "SECRET_KEY = 2crAW4UANgvLipDS6U5obRcFosjSJHQANll6MNfX7P0G3se3fKcCwwK3szPyGcbo" >> /tmp/conf/app.ini
&& echo "PASSWORD_COMPLEXITY = off" >> /tmp/conf/app.ini
&& echo "[database]" >> /tmp/conf/app.ini
&& echo "DB_TYPE = sqlite3" >> /tmp/conf/app.ini
&& echo "[repository]" >> /tmp/conf/app.ini
&& echo "ROOT = /tmp/data/" >> /tmp/conf/app.ini
&& echo "[server]" >> /tmp/conf/app.ini
&& echo "ROOT_URL = http://gitea:3000" >> /tmp/conf/app.ini
&& gitea migrate -c /tmp/conf/app.ini
&& gitea admin user create --username=test01 --password=test01 --email=test01@gitea.io --admin=true --must-change-password=false --access-token -c /tmp/conf/app.ini
&& gitea web -c /tmp/conf/app.ini

View File

@ -58,7 +58,7 @@ builds:
flags: flags:
- -trimpath - -trimpath
ldflags: ldflags:
- -s -w -X main.Version={{ .Version }} - -s -w -X code.gitea.io/tea/cmd.Version={{ .Version }}
binary: >- binary: >-
{{ .ProjectName }}- {{ .ProjectName }}-
{{- .Version }}- {{- .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

168
README.md
View File

@ -11,70 +11,79 @@
![demo gif](./demo.gif) ![demo gif](./demo.gif)
``` ```
tea - command line tool to interact with Gitea NAME:
version 0.8.0-preview tea - command line tool to interact with Gitea
USAGE USAGE:
tea command [subcommand] [command options] [arguments...] tea [global options] [command [command options]]
DESCRIPTION VERSION:
tea is a productivity helper for Gitea. It can be used to manage most entities on Version: 0.10.1+15-g8876fe3 golang: 1.25.0 go-sdk: v0.21.0
one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.
tea tries to make use of context provided by the repository in $PWD if available. DESCRIPTION:
tea works best in a upstream/fork workflow, when the local main branch tracks the tea is a productivity helper for Gitea. It can be used to manage most entities on
upstream repo. tea assumes that local git state is published on the remote before one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.
doing operations with tea. Configuration is persisted in $XDG_CONFIG_HOME/tea.
COMMANDS tea tries to make use of context provided by the repository in $PWD if available.
help, h Shows a list of commands or help for one command tea works best in a upstream/fork workflow, when the local main branch tracks the
ENTITIES: upstream repo. tea assumes that local git state is published on the remote before
issues, issue, i List, create and update issues doing operations with tea. Configuration is persisted in $XDG_CONFIG_HOME/tea.
pulls, pull, pr Manage and checkout pull requests
labels, label Manage issue labels
milestones, milestone, ms List and create milestones
releases, release, r Manage releases
release assets, release asset, r a Manage release attachments
times, time, t Operate on tracked times of a repository's issues & pulls
organizations, organization, org List, create, delete organizations
repos, repo Show repository details
comment, c Add a comment to an issue / pr
HELPERS:
open, o Open something of the repository in web browser
notifications, notification, n Show notifications
clone, C Clone a repository locally
SETUP:
logins, login Log in to a Gitea server
logout Log out from a Gitea server
shellcompletion, autocomplete Install shell completion for tea
whoami Show current logged in user
OPTIONS COMMANDS:
--help, -h show help (default: false) help, h Shows a list of commands or help for one command
--version, -v print the version (default: false)
EXAMPLES ENTITIES:
tea login add # add a login once to get started issues, issue, i List, create and update issues
pulls, pull, pr Manage and checkout pull requests
labels, label Manage issue labels
milestones, milestone, ms List and create milestones
releases, release, r Manage releases
times, time, t Operate on tracked times of a repository's issues & pulls
organizations, organization, org List, create, delete organizations
repos, repo Show repository details
branches, branch, b Consult branches
comment, c Add a comment to an issue / pr
tea pulls # list open pulls for the repo in $PWD HELPERS:
tea pulls --repo $HOME/foo # list open pulls for the repo in $HOME/foo open, o Open something of the repository in web browser
tea pulls --remote upstream # list open pulls for the repo pointed at by notifications, notification, n Show notifications
# your local "upstream" git remote clone, C Clone a repository locally
# list open pulls for any gitea repo at the given login instance
tea pulls --repo gitea/tea --login gitea.com
tea milestone issues 0.7.0 # view open issues for milestone '0.7.0' MISCELLANEOUS:
tea issue 189 # view contents of issue 189 whoami Show current logged in user
tea open 189 # open web ui for issue 189 admin, a Operations requiring admin access on the Gitea instance
tea open milestones # open web ui for milestones
# send gitea desktop notifications every 5 minutes (bash + libnotify) SETUP:
while :; do tea notifications --mine -o simple | xargs -i notify-send {}; sleep 300; done logins, login Log in to a Gitea server
logout Log out from a Gitea server
ABOUT GLOBAL OPTIONS:
Written & maintained by The Gitea Authors. --debug, --vvv Enable debug mode (default: false)
If you find a bug or want to contribute, we'll welcome you at https://gitea.com/gitea/tea. --help, -h show help
More info about Gitea itself on https://about.gitea.com. --version, -v print the version
EXAMPLES
tea login add # add a login once to get started
tea pulls # list open pulls for the repo in $PWD
tea pulls --repo $HOME/foo # list open pulls for the repo in $HOME/foo
tea pulls --remote upstream # list open pulls for the repo pointed at by
# your local "upstream" git remote
# list open pulls for any gitea repo at the given login instance
tea pulls --repo gitea/tea --login gitea.com
tea milestone issues 0.7.0 # view open issues for milestone '0.7.0'
tea issue 189 # view contents of issue 189
tea open 189 # open web ui for issue 189
tea open milestones # open web ui for milestones
# send gitea desktop notifications every 5 minutes (bash + libnotify)
while :; do tea notifications --mine -o simple | xargs -i notify-send {}; sleep 300; done
ABOUT
Written & maintained by The Gitea Authors.
If you find a bug or want to contribute, we'll welcome you at https://gitea.com/gitea/tea.
More info about Gitea itself on https://about.gitea.com.
``` ```
- [Compare features with other git forge CLIs](./FEATURE-COMPARISON.md) - [Compare features with other git forge CLIs](./FEATURE-COMPARISON.md)
@ -89,7 +98,7 @@ There are different ways to get `tea`:
```sh ```sh
brew install tea brew install tea
``` ```
- arch linux ([gitea-tea-git](https://aur.archlinux.org/packages/gitea-tea-git), thirdparty) - arch linux ([tea](https://archlinux.org/packages/extra/x86_64/tea/), thirdparty)
- alpine linux ([tea](https://pkgs.alpinelinux.org/packages?name=tea&branch=edge), thirdparty) - alpine linux ([tea](https://pkgs.alpinelinux.org/packages?name=tea&branch=edge), thirdparty)
- Windows via `MSYS2` ([tea](https://packages.msys2.org/base/mingw-w64-tea), thirdparty) - Windows via `MSYS2` ([tea](https://packages.msys2.org/base/mingw-w64-tea), thirdparty)
@ -101,6 +110,55 @@ There are different ways to get `tea`:
5. asdf (thirdparty): [mvaldes14/asdf-tea](https://github.com/mvaldes14/asdf-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.
```shell
# .bashrc
source <(tea completion bash)
# .zshrc
source <(tea completion zsh)
# fish
tea completion fish > ~/.config/fish/completions/tea.fish
# Powershell
Output the script to path/to/autocomplete/tea.ps1 an run it.
```
### Man Page
The hidden command `tea man` can be used to generate the `tea` man page.
```shell
# for bash or zsh
man <(tea man)
# for fish
man (tea man | psub)
# write man page to a file
tea man --out ./tea.man
```
## Compilation ## Compilation
Make sure you have a current go version installed (1.13 or newer). Make sure you have a current go version installed (1.13 or newer).

View File

@ -43,7 +43,7 @@ func RunUserList(_ stdctx.Context, cmd *cli.Command) error {
client := ctx.Login.Client() client := ctx.Login.Client()
users, _, err := client.AdminListUsers(gitea.AdminListUsersOptions{ users, _, err := client.AdminListUsers(gitea.AdminListUsersOptions{
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
}) })
if err != nil { if err != nil {
return err return err

View File

@ -46,7 +46,7 @@ func RunReleaseAttachmentList(_ stdctx.Context, cmd *cli.Command) error {
} }
attachments, _, err := ctx.Login.Client().ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{ attachments, _, err := ctx.Login.Client().ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
}) })
if err != nil { if err != nil {
return err return err

View File

@ -1,138 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"github.com/adrg/xdg"
"github.com/urfave/cli/v3"
)
// CmdAutocomplete manages autocompletion
var CmdAutocomplete = cli.Command{
Name: "shellcompletion",
Aliases: []string{"autocomplete"},
Category: catSetup,
Usage: "Install shell completion for tea",
Description: "Install shell completion for tea",
ArgsUsage: "<shell type> (bash, zsh, powershell, fish)",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "install",
Usage: "Persist in shell config instead of printing commands",
},
},
Action: runAutocompleteAdd,
}
func runAutocompleteAdd(_ context.Context, cmd *cli.Command) error {
var remoteFile, localFile, cmds string
shell := cmd.Args().First()
switch shell {
case "zsh":
remoteFile = "contrib/autocomplete.zsh"
localFile = "autocomplete.zsh"
cmds = "echo 'PROG=tea _CLI_ZSH_AUTOCOMPLETE_HACK=1 source \"%s\"' >> ~/.zshrc && source ~/.zshrc"
case "bash":
remoteFile = "contrib/autocomplete.sh"
localFile = "autocomplete.sh"
cmds = "echo 'PROG=tea source \"%s\"' >> ~/.bashrc && source ~/.bashrc"
case "powershell":
remoteFile = "contrib/autocomplete.ps1"
localFile = "tea.ps1"
cmds = "\"& %s\" >> $profile"
case "fish":
// fish is different, in that urfave/cli provides a generator for the shell script needed.
// this also means that the fish completion can become out of sync with the tea binary!
// writing to this directory suffices, as fish reads files there on startup, no cmds needed.
return writeFishAutoCompleteFile(cmd)
default:
return fmt.Errorf("Must specify valid %s", cmd.ArgsUsage)
}
localPath, err := xdg.ConfigFile("tea/" + localFile)
if err != nil {
return err
}
cmds = fmt.Sprintf(cmds, localPath)
if err = writeRemoteAutoCompleteFile(remoteFile, localPath); err != nil {
return err
}
if cmd.Bool("install") {
fmt.Println("Installing in your shellrc")
installer := exec.Command(shell, "-c", cmds)
if shell == "powershell" {
installer = exec.Command("powershell.exe", "-Command", cmds)
}
out, err := installer.CombinedOutput()
if err != nil {
return fmt.Errorf("Couldn't run the commands: %s %s", err, out)
}
} else {
fmt.Println("\n# Run the following commands to install autocompletion (or use --install)")
fmt.Println(cmds)
}
return nil
}
func writeRemoteAutoCompleteFile(file, destPath string) error {
url := fmt.Sprintf("https://gitea.com/gitea/tea/raw/branch/master/%s", file)
fmt.Println("Fetching " + url)
res, err := http.Get(url)
if err != nil {
return err
}
defer res.Body.Close()
writer, err := os.Create(destPath)
if err != nil {
return err
}
defer writer.Close()
_, err = io.Copy(writer, res.Body)
return err
}
func writeFishAutoCompleteFile(cmd *cli.Command) error {
// NOTE: to make sure this file is in sync with tea commands, we'd need to
// - check if the file exists
// - if it does, check if the tea version that wrote it is the currently running version
// - if not, rewrite the file
// on each application run
// NOTE: this generates a completion that also suggests file names, which looks kinda messy..
script, err := cmd.ToFishCompletion()
if err != nil {
return err
}
localPath, err := xdg.ConfigFile("fish/conf.d/tea_completion.fish")
if err != nil {
return err
}
writer, err := os.Create(localPath)
if err != nil {
return err
}
if _, err = io.WriteString(writer, script); err != nil {
return err
}
fmt.Printf("Installed tab completion to %s\n", localPath)
return nil
}

View File

@ -50,7 +50,7 @@ func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error {
var protections []*gitea.BranchProtection var protections []*gitea.BranchProtection
var err error var err error
branches, _, err = ctx.Login.Client().ListRepoBranches(owner, ctx.Repo, gitea.ListRepoBranchesOptions{ branches, _, err = ctx.Login.Client().ListRepoBranches(owner, ctx.Repo, gitea.ListRepoBranchesOptions{
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
}) })
if err != nil { if err != nil {
@ -58,7 +58,7 @@ func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error {
} }
protections, _, err = ctx.Login.Client().ListBranchProtections(owner, ctx.Repo, gitea.ListBranchProtectionsOptions{ protections, _, err = ctx.Login.Client().ListBranchProtections(owner, ctx.Repo, gitea.ListBranchProtectionsOptions{
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
}) })
if err != nil { if err != nil {

View File

@ -10,6 +10,7 @@ import (
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/context" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/debug"
"code.gitea.io/tea/modules/git" "code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/interact" "code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task" "code.gitea.io/tea/modules/task"
@ -68,12 +69,15 @@ func runRepoClone(ctx stdctx.Context, cmd *cli.Command) error {
return err return err
} }
debug.Printf("Cloning repository %s into %s", url.String(), dir)
owner, repo = utils.GetOwnerAndRepo(url.Path, login.User) owner, repo = utils.GetOwnerAndRepo(url.Path, login.User)
if url.Host != "" { if url.Host != "" {
login = config.GetLoginByHost(url.Host) login = config.GetLoginByHost(url.Host)
if login == nil { if login == nil {
return fmt.Errorf("No login configured matching host '%s', run `tea login add` first", url.Host) return fmt.Errorf("No login configured matching host '%s', run `tea login add` first", url.Host)
} }
debug.Printf("Matched login '%s' for host '%s'", login.Name, url.Host)
} }
_, err = task.RepoClone( _, err = task.RepoClone(

View File

@ -13,6 +13,8 @@ import (
) )
// Version holds the current tea version // Version holds the current tea version
// If the Version is moved to another package or name changed,
// build flags in .goreleaser.yaml or Makefile need to be updated accordingly.
var Version = "development" var Version = "development"
// Tags holds the build tags used // Tags holds the build tags used
@ -36,7 +38,6 @@ func App() *cli.Command {
Commands: []*cli.Command{ Commands: []*cli.Command{
&CmdLogin, &CmdLogin,
&CmdLogout, &CmdLogout,
&CmdAutocomplete,
&CmdWhoami, &CmdWhoami,
&CmdIssues, &CmdIssues,
@ -55,6 +56,8 @@ func App() *cli.Command {
&CmdRepoClone, &CmdRepoClone,
&CmdAdmin, &CmdAdmin,
&CmdGenerateManPage,
}, },
EnableShellCompletion: true, EnableShellCompletion: true,
} }

View File

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

View File

@ -1,9 +1,12 @@
// Copyright 2019 The Gitea Authors. All rights reserved. // Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package flags package flags
import ( import (
"errors"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -35,18 +38,53 @@ var OutputFlag = cli.StringFlag{
Usage: "Output format. (simple, table, csv, tsv, yaml, json)", Usage: "Output format. (simple, table, csv, tsv, yaml, json)",
} }
var (
paging gitea.ListOptions
// ErrPage indicates that the provided page value is invalid (less than -1 or equal to 0).
ErrPage = errors.New("page cannot be smaller than 1")
// ErrLimit indicates that the provided limit value is invalid (negative).
ErrLimit = errors.New("limit cannot be negative")
)
// GetListOptions returns configured paging struct
func GetListOptions() gitea.ListOptions {
return paging
}
// PaginationFlags provides all pagination related flags
var PaginationFlags = []cli.Flag{
&PaginationPageFlag,
&PaginationLimitFlag,
}
// PaginationPageFlag provides flag for pagination options // PaginationPageFlag provides flag for pagination options
var PaginationPageFlag = cli.StringFlag{ var PaginationPageFlag = cli.IntFlag{
Name: "page", Name: "page",
Aliases: []string{"p"}, Aliases: []string{"p"},
Usage: "specify page, default is 1", Usage: "specify page",
Value: 1,
Validator: func(i int) error {
if i < 1 && i != -1 {
return ErrPage
}
return nil
},
Destination: &paging.Page,
} }
// PaginationLimitFlag provides flag for pagination options // PaginationLimitFlag provides flag for pagination options
var PaginationLimitFlag = cli.StringFlag{ var PaginationLimitFlag = cli.IntFlag{
Name: "limit", Name: "limit",
Aliases: []string{"lm"}, Aliases: []string{"lm"},
Usage: "specify limit of items per page", Usage: "specify limit of items per page",
Value: 30,
Validator: func(i int) error {
if i < 0 {
return ErrLimit
}
return nil
},
Destination: &paging.PageSize,
} }
// LoginOutputFlags defines login and output flags that should // LoginOutputFlags defines login and output flags that should

125
cmd/flags/generic_test.go Normal file
View File

@ -0,0 +1,125 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package flags
import (
"context"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
)
func TestPaginationFlags(t *testing.T) {
var (
defaultPage = PaginationPageFlag.Value
defaultLimit = PaginationLimitFlag.Value
)
cases := []struct {
name string
args []string
expectedPage int
expectedLimit int
}{
{
name: "no flags",
args: []string{"test"},
expectedPage: defaultPage,
expectedLimit: defaultLimit,
},
{
name: "only paging",
args: []string{"test", "--page", "5"},
expectedPage: 5,
expectedLimit: defaultLimit,
},
{
name: "only limit",
args: []string{"test", "--limit", "10"},
expectedPage: defaultPage,
expectedLimit: 10,
},
{
name: "only limit",
args: []string{"test", "--limit", "10"},
expectedPage: defaultPage,
expectedLimit: 10,
},
{
name: "both flags",
args: []string{"test", "--page", "2", "--limit", "20"},
expectedPage: 2,
expectedLimit: 20,
},
{ //TODO: Should no paging be applied as -1 or a separate flag? It's not obvious that page=-1 turns off paging and limit is ignored
name: "no paging",
args: []string{"test", "--limit", "20", "--page", "-1"},
expectedPage: -1,
expectedLimit: 20,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cmd := cli.Command{
Name: "test-paging",
Action: func(_ context.Context, cmd *cli.Command) error {
assert.Equal(t, tc.expectedPage, cmd.Int("page"))
assert.Equal(t, tc.expectedLimit, cmd.Int("limit"))
return nil
},
Flags: PaginationFlags,
}
err := cmd.Run(context.Background(), tc.args)
require.NoError(t, err)
})
}
}
func TestPaginationFailures(t *testing.T) {
cases := []struct {
name string
args []string
expectedError error
}{
{
name: "negative limit",
args: []string{"test", "--limit", "-10"},
expectedError: ErrLimit,
},
{
name: "negative paging",
args: []string{"test", "--page", "-2"},
expectedError: ErrPage,
},
{
name: "zero paging",
args: []string{"test", "--page", "0"},
expectedError: ErrPage,
},
{
//urfave does not validate all flags in one pass
name: "negative paging and paging",
args: []string{"test", "--page", "-2", "--limit", "-10"},
expectedError: ErrPage,
},
}
for _, tc := range cases {
cmd := cli.Command{
Name: "test-paging",
Flags: PaginationFlags,
Writer: io.Discard,
ErrWriter: io.Discard,
}
t.Run(tc.name, func(t *testing.T) {
err := cmd.Run(context.Background(), tc.args)
require.ErrorContains(t, err, tc.expectedError.Error())
// require.ErrorIs(t, err, tc.expectedError)
})
}
}

View File

@ -30,7 +30,11 @@ func runIssuesCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.NumFlags() == 0 { 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) opts, err := flags.GetIssuePRCreateFlags(ctx)

View File

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

View File

@ -85,7 +85,7 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
var issues []*gitea.Issue var issues []*gitea.Issue
if ctx.Repo != "" { if ctx.Repo != "" {
issues, _, err = ctx.Login.Client().ListRepoIssues(owner, ctx.Repo, gitea.ListIssueOption{ issues, _, err = ctx.Login.Client().ListRepoIssues(owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
State: state, State: state,
Type: kind, Type: kind,
KeyWord: ctx.String("keyword"), KeyWord: ctx.String("keyword"),
@ -103,7 +103,7 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
} }
} else { } else {
issues, _, err = ctx.Login.Client().ListIssues(gitea.ListIssueOption{ issues, _, err = ctx.Login.Client().ListIssues(gitea.ListIssueOption{
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
State: state, State: state,
Type: kind, Type: kind,
KeyWord: ctx.String("keyword"), KeyWord: ctx.String("keyword"),

View File

@ -41,7 +41,7 @@ func RunLabelsList(_ stdctx.Context, cmd *cli.Command) error {
client := ctx.Login.Client() client := ctx.Login.Client()
labels, _, err := client.ListRepoLabels(ctx.Owner, ctx.Repo, gitea.ListLabelsOptions{ labels, _, err := client.ListRepoLabels(ctx.Owner, ctx.Repo, gitea.ListLabelsOptions{
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
}) })
if err != nil { if err != nil {
return err return err

View File

@ -5,6 +5,7 @@ package login
import ( import (
"context" "context"
"fmt"
"code.gitea.io/tea/modules/auth" "code.gitea.io/tea/modules/auth"
"code.gitea.io/tea/modules/interact" "code.gitea.io/tea/modules/interact"
@ -112,7 +113,10 @@ var CmdLoginAdd = cli.Command{
func runLoginAdd(_ context.Context, cmd *cli.Command) error { func runLoginAdd(_ context.Context, cmd *cli.Command) error {
// if no args create login interactive // if no args create login interactive
if cmd.NumFlags() == 0 { 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 // if OAuth flag is provided, use OAuth2 PKCE flow

62
cmd/man.go Normal file
View File

@ -0,0 +1,62 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"os"
"path/filepath"
docs "github.com/urfave/cli-docs/v3"
"github.com/urfave/cli/v3"
)
// DocRenderFlags are the flags for documentation generation, used by `./docs/docs.go` and the `generate-man-page` sub command
var DocRenderFlags = []cli.Flag{
&cli.StringFlag{
Name: "out",
Usage: "Path to output docs to, otherwise prints to stdout",
Aliases: []string{"o"},
},
}
// CmdGenerateManPage is the sub command to generate the `tea` man page
var CmdGenerateManPage = cli.Command{
Name: "man",
Usage: "Generate man page",
Hidden: true,
Flags: DocRenderFlags,
Action: func(ctx context.Context, cmd *cli.Command) error {
return RenderDocs(cmd, cmd.Root(), docs.ToMan)
},
}
// RenderDocs renders the documentation for `target` using the supplied `render` function
func RenderDocs(cmd, target *cli.Command, render func(*cli.Command) (string, error)) error {
out, err := render(target)
if err != nil {
return err
}
outPath := cmd.String("out")
if outPath == "" {
fmt.Print(out)
return nil
}
if err = os.MkdirAll(filepath.Dir(outPath), os.ModePerm); err != nil {
return err
}
fi, err := os.Create(outPath)
if err != nil {
return err
}
defer fi.Close()
if _, err = fi.WriteString(out); err != nil {
return err
}
return nil
}

View File

@ -68,7 +68,10 @@ func runMilestonesCreate(_ stdctx.Context, cmd *cli.Command) error {
} }
if ctx.NumFlags() == 0 { 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( return task.CreateMilestone(

View File

@ -103,7 +103,7 @@ func runMilestoneIssueList(_ stdctx.Context, cmd *cli.Command) error {
} }
issues, _, err := client.ListRepoIssues(ctx.Owner, ctx.Repo, gitea.ListIssueOption{ issues, _, err := client.ListRepoIssues(ctx.Owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
Milestones: []string{milestone}, Milestones: []string{milestone},
Type: kind, Type: kind,
State: state, State: state,

View File

@ -61,7 +61,7 @@ func RunMilestonesList(_ stdctx.Context, cmd *cli.Command) error {
client := ctx.Login.Client() client := ctx.Login.Client()
milestones, _, err := client.ListRepoMilestones(ctx.Owner, ctx.Repo, gitea.ListMilestoneOption{ milestones, _, err := client.ListRepoMilestones(ctx.Owner, ctx.Repo, gitea.ListMilestoneOption{
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
State: state, State: state,
}) })

View File

@ -69,7 +69,7 @@ func listNotifications(_ stdctx.Context, cmd *cli.Command, status []gitea.Notify
all := ctx.Bool("mine") all := ctx.Bool("mine")
// This enforces pagination (see https://github.com/go-gitea/gitea/issues/16733) // This enforces pagination (see https://github.com/go-gitea/gitea/issues/16733)
listOpts := ctx.GetListOptions() listOpts := flags.GetListOptions()
if listOpts.Page == 0 { if listOpts.Page == 0 {
listOpts.Page = 1 listOpts.Page = 1
} }

View File

@ -33,7 +33,7 @@ func RunOrganizationList(_ stdctx.Context, cmd *cli.Command) error {
client := ctx.Login.Client() client := ctx.Login.Client()
userOrganizations, _, err := client.ListUserOrgs(ctx.Login.User, gitea.ListOrgsOptions{ userOrganizations, _, err := client.ListUserOrgs(ctx.Login.User, gitea.ListOrgsOptions{
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
}) })
if err != nil { if err != nil {
return err return err

View File

@ -47,5 +47,8 @@ func runPullsCheckout(_ stdctx.Context, cmd *cli.Command) error {
return err 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 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

@ -6,6 +6,7 @@ package pulls
import ( import (
stdctx "context" stdctx "context"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact" "code.gitea.io/tea/modules/interact"
@ -44,7 +45,10 @@ func runPullsCreate(_ stdctx.Context, cmd *cli.Command) error {
// no args -> interactive mode // no args -> interactive mode
if ctx.NumFlags() == 0 { 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 // else use args to create PR
@ -53,11 +57,16 @@ func runPullsCreate(_ stdctx.Context, cmd *cli.Command) error {
return err return err
} }
var allowMaintainerEdits *bool
if ctx.IsSet("allow-maintainer-edits") {
allowMaintainerEdits = gitea.OptionalBool(ctx.Bool("allow-maintainer-edits"))
}
return task.CreatePull( return task.CreatePull(
ctx, ctx,
ctx.String("base"), ctx.String("base"),
ctx.String("head"), ctx.String("head"),
ctx.Bool("allow-maintainer-edits"), allowMaintainerEdits,
opts, opts,
) )
} }

View File

@ -46,7 +46,10 @@ var CmdPullsMerge = cli.Command{
if ctx.Args().Len() != 1 { if ctx.Args().Len() != 1 {
// If no PR index is provided, try interactive mode // 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()) idx, err := utils.ArgToIndex(ctx.Args().First())

View File

@ -26,7 +26,7 @@ var CmdPullsReview = cli.Command{
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() != 1 { 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()) idx, err := utils.ArgToIndex(ctx.Args().First())
@ -34,7 +34,10 @@ var CmdPullsReview = cli.Command{
return err 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, Flags: flags.AllDefaultFlags,
} }

View File

@ -35,7 +35,7 @@ func RunReleasesList(_ stdctx.Context, cmd *cli.Command) error {
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
releases, _, err := ctx.Login.Client().ListReleases(ctx.Owner, ctx.Repo, gitea.ListReleasesOptions{ releases, _, err := ctx.Login.Client().ListReleases(ctx.Owner, ctx.Repo, gitea.ListReleasesOptions{
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
}) })
if err != nil { if err != nil {
return err return err

View File

@ -88,6 +88,17 @@ var CmdRepoCreate = cli.Command{
Name: "trustmodel", Name: "trustmodel",
Usage: "select trust model (committer,collaborator,collaborator+committer)", Usage: "select trust model (committer,collaborator,collaborator+committer)",
}, },
&cli.StringFlag{
Name: "object-format",
Required: false,
Usage: "select git object format (sha1,sha256)",
Validator: func(v string) error {
if v != "sha1" && v != "sha256" {
return fmt.Errorf("invalid object format '%s', must be either 'sha1' or 'sha256'", v)
}
return nil
},
},
}, flags.LoginOutputFlags...), }, flags.LoginOutputFlags...),
} }
@ -114,17 +125,18 @@ func runRepoCreate(_ stdctx.Context, cmd *cli.Command) error {
} }
opts := gitea.CreateRepoOption{ opts := gitea.CreateRepoOption{
Name: ctx.String("name"), Name: ctx.String("name"),
Description: ctx.String("description"), Description: ctx.String("description"),
Private: ctx.Bool("private"), Private: ctx.Bool("private"),
AutoInit: ctx.Bool("init"), AutoInit: ctx.Bool("init"),
IssueLabels: ctx.String("labels"), IssueLabels: ctx.String("labels"),
Gitignores: ctx.String("gitignores"), Gitignores: ctx.String("gitignores"),
License: ctx.String("license"), License: ctx.String("license"),
Readme: ctx.String("readme"), Readme: ctx.String("readme"),
DefaultBranch: ctx.String("branch"), DefaultBranch: ctx.String("branch"),
Template: ctx.Bool("template"), Template: ctx.Bool("template"),
TrustModel: trustmodel, TrustModel: trustmodel,
ObjectFormatName: ctx.String("object-format"),
} }
if len(ctx.String("owner")) != 0 { if len(ctx.String("owner")) != 0 {
repo, _, err = client.CreateOrgRepo(ctx.String("owner"), opts) repo, _, err = client.CreateOrgRepo(ctx.String("owner"), opts)

88
cmd/repos/create_test.go Normal file
View File

@ -0,0 +1,88 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repos
import (
"context"
"fmt"
"os"
"testing"
"time"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/task"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func TestCreateRepoObjectFormat(t *testing.T) {
giteaURL := os.Getenv("GITEA_TEA_TEST_URL")
if giteaURL == "" {
t.Skip("GITEA_TEA_TEST_URL is not set, skipping test")
}
timestamp := time.Now().Unix()
tests := []struct {
name string
args []string
wantOpts gitea.CreateRepoOption
wantErr bool
errContains string
}{
{
name: "create repo with sha1 object format",
args: []string{"--name", fmt.Sprintf("test-sha1-%d", timestamp), "--object-format", "sha1"},
wantOpts: gitea.CreateRepoOption{
Name: fmt.Sprintf("test-sha1-%d", timestamp),
ObjectFormatName: "sha1",
},
wantErr: false,
},
{
name: "create repo with sha256 object format",
args: []string{"--name", fmt.Sprintf("test-sha256-%d", timestamp), "--object-format", "sha256"},
wantOpts: gitea.CreateRepoOption{
Name: fmt.Sprintf("test-sha256-%d", timestamp),
ObjectFormatName: "sha256",
},
wantErr: false,
},
{
name: "create repo with invalid object format",
args: []string{"--name", fmt.Sprintf("test-invalid-%d", timestamp), "--object-format", "invalid"},
wantErr: true,
errContains: "invalid object format",
},
}
giteaUserName := os.Getenv("GITEA_TEA_TEST_USERNAME")
giteaUserPasword := os.Getenv("GITEA_TEA_TEST_PASSWORD")
err := task.CreateLogin("test", "", giteaUserName, giteaUserPasword, "", "", "", giteaURL, "", "", true, false, false, false)
if err != nil && err.Error() != "login name 'test' has already been used" {
t.Fatal(err)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reposCmd := &cli.Command{
Name: "repos",
Commands: []*cli.Command{&CmdRepoCreate},
}
tt.args = append(tt.args, "--login", "test")
args := append([]string{"repos", "create"}, tt.args...)
err := reposCmd.Run(context.Background(), args)
if tt.wantErr {
assert.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
return
}
assert.NoError(t, err)
})
}
}

View File

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

View File

@ -65,14 +65,14 @@ func RunReposList(_ stdctx.Context, cmd *cli.Command) error {
return err return err
} }
rps, _, err = client.SearchRepos(gitea.SearchRepoOptions{ rps, _, err = client.SearchRepos(gitea.SearchRepoOptions{
ListOptions: teaCmd.GetListOptions(), ListOptions: flags.GetListOptions(),
StarredByUserID: user.ID, StarredByUserID: user.ID,
}) })
} else if teaCmd.Bool("watched") { } else if teaCmd.Bool("watched") {
rps, _, err = client.GetMyWatchedRepos() // TODO: this does not expose pagination.. rps, _, err = client.GetMyWatchedRepos() // TODO: this does not expose pagination..
} else { } else {
rps, _, err = client.ListMyRepos(gitea.ListReposOptions{ rps, _, err = client.ListMyRepos(gitea.ListReposOptions{
ListOptions: teaCmd.GetListOptions(), ListOptions: flags.GetListOptions(),
}) })
} }

View File

@ -109,7 +109,7 @@ func runReposSearch(_ stdctx.Context, cmd *cli.Command) error {
} }
rps, _, err := client.SearchRepos(gitea.SearchRepoOptions{ rps, _, err := client.SearchRepos(gitea.SearchRepoOptions{
ListOptions: teaCmd.GetListOptions(), ListOptions: flags.GetListOptions(),
OwnerID: ownerID, OwnerID: ownerID,
IsPrivate: isPrivate, IsPrivate: isPrivate,
IsArchived: isArchived, IsArchived: isArchived,

View File

@ -95,12 +95,6 @@ Refresh an OAuth token
Log out from a Gitea server Log out from a Gitea server
## shellcompletion, autocomplete
Install shell completion for tea
**--install**: Persist in shell config instead of printing commands
## whoami ## whoami
Show current logged in user Show current logged in user
@ -129,7 +123,7 @@ List, create and update issues
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@ -143,7 +137,7 @@ List, create and update issues
**--owner, --org**="": **--owner, --org**="":
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -175,7 +169,7 @@ List issues of the repository
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@ -189,7 +183,7 @@ List issues of the repository
**--owner, --org**="": **--owner, --org**="":
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -283,13 +277,13 @@ Manage and checkout pull requests
index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments
(default: index,title,state,author,milestone,updated,labels) (default: index,title,state,author,milestone,updated,labels)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -305,13 +299,13 @@ List pull requests of the repository
index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments
(default: index,title,state,author,milestone,updated,labels) (default: index,title,state,author,milestone,updated,labels)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -459,13 +453,13 @@ Merge a pull request
Manage issue labels Manage issue labels
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -477,13 +471,13 @@ Manage issue labels
List labels List labels
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -553,13 +547,13 @@ List and create milestones
title,state,items_open,items_closed,items,duedate,description,created,updated,closed,id title,state,items_open,items_closed,items,duedate,description,created,updated,closed,id
(default: title,items,duedate) (default: title,items,duedate)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -575,13 +569,13 @@ List milestones of the repository
title,state,items_open,items_closed,items,duedate,description,created,updated,closed,id title,state,items_open,items_closed,items,duedate,description,created,updated,closed,id
(default: title,items,duedate) (default: title,items,duedate)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -657,13 +651,13 @@ manage issue/pull of an milestone
**--kind**="": Filter by kind (issue|pull) **--kind**="": Filter by kind (issue|pull)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -711,13 +705,13 @@ Manage releases
List Releases List Releases
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -807,13 +801,13 @@ Manage release assets
List Release Attachments List Release Attachments
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -933,13 +927,13 @@ List tracked times on issues & pulls
List, create, delete organizations List, create, delete organizations
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -949,13 +943,13 @@ List, create, delete organizations
List Organizations List Organizations
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -995,13 +989,13 @@ Show repository details
description,forks,id,name,owner,stars,ssh,updated,url,permission,type description,forks,id,name,owner,stars,ssh,updated,url,permission,type
(default: owner,name,type,ssh) (default: owner,name,type,ssh)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--starred, -s**: List your starred repos instead **--starred, -s**: List your starred repos instead
@ -1017,13 +1011,13 @@ List repositories you have access to
description,forks,id,name,owner,stars,ssh,updated,url,permission,type description,forks,id,name,owner,stars,ssh,updated,url,permission,type
(default: owner,name,type,ssh) (default: owner,name,type,ssh)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--starred, -s**: List your starred repos instead **--starred, -s**: List your starred repos instead
@ -1041,7 +1035,7 @@ Find any repo on an Gitea instance
description,forks,id,name,owner,stars,ssh,updated,url,permission,type description,forks,id,name,owner,stars,ssh,updated,url,permission,type
(default: owner,name,type,ssh) (default: owner,name,type,ssh)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@ -1049,7 +1043,7 @@ Find any repo on an Gitea instance
**--owner, -O**="": Filter by owner **--owner, -O**="": Filter by owner
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--private**="": Filter private repos (true|false) **--private**="": Filter private repos (true|false)
@ -1077,6 +1071,8 @@ Create a repository
**--name, -**="": name of new repo **--name, -**="": name of new repo
**--object-format**="": select git object format (sha1,sha256)
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--owner, -O**="": name of repo owner **--owner, -O**="": name of repo owner
@ -1201,13 +1197,13 @@ Consult branches
name,protected,user-can-merge,user-can-push,protection name,protected,user-can-merge,user-can-push,protection
(default: name,protected,user-can-merge,user-can-push) (default: name,protected,user-can-merge,user-can-push)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -1221,13 +1217,13 @@ List branches of the repository
name,protected,user-can-merge,user-can-push,protection name,protected,user-can-merge,user-can-push,protection
(default: name,protected,user-can-merge,user-can-push) (default: name,protected,user-can-merge,user-can-push)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -1241,13 +1237,13 @@ Protect branches
name,protected,user-can-merge,user-can-push,protection name,protected,user-can-merge,user-can-push,protection
(default: name,protected,user-can-merge,user-can-push) (default: name,protected,user-can-merge,user-can-push)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -1261,13 +1257,13 @@ Unprotect branches
name,protected,user-can-merge,user-can-push,protection name,protected,user-can-merge,user-can-push,protection
(default: name,protected,user-can-merge,user-can-push) (default: name,protected,user-can-merge,user-can-push)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -1303,7 +1299,7 @@ Show notifications
id,status,updated,index,type,state,title,repository id,status,updated,index,type,state,title,repository
(default: id,status,index,type,state,title) (default: id,status,index,type,state,title)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@ -1311,7 +1307,7 @@ Show notifications
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -1333,7 +1329,7 @@ List notifications
id,status,updated,index,type,state,title,repository id,status,updated,index,type,state,title,repository
(default: id,status,index,type,state,title) (default: id,status,index,type,state,title)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@ -1341,7 +1337,7 @@ List notifications
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -1359,7 +1355,7 @@ List notifications
Mark all filtered or a specific notification as read Mark all filtered or a specific notification as read
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@ -1367,7 +1363,7 @@ Mark all filtered or a specific notification as read
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -1381,7 +1377,7 @@ Mark all filtered or a specific notification as read
Mark all filtered or a specific notification as unread Mark all filtered or a specific notification as unread
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@ -1389,7 +1385,7 @@ Mark all filtered or a specific notification as unread
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -1403,7 +1399,7 @@ Mark all filtered or a specific notification as unread
Mark all filtered or a specific notification as pinned Mark all filtered or a specific notification as pinned
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@ -1411,7 +1407,7 @@ Mark all filtered or a specific notification as pinned
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -1425,7 +1421,7 @@ Mark all filtered or a specific notification as pinned
Unpin all pinned or a specific notification Unpin all pinned or a specific notification
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@ -1433,7 +1429,7 @@ Unpin all pinned or a specific notification
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -1463,13 +1459,13 @@ Manage registered users
id,login,full_name,email,avatar_url,language,is_admin,restricted,prohibit_login,location,website,description,visibility,activated,lastlogin_at,created_at id,login,full_name,email,avatar_url,language,is_admin,restricted,prohibit_login,location,website,description,visibility,activated,lastlogin_at,created_at
(default: id,login,full_name,email,activated) (default: id,login,full_name,email,activated)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
@ -1483,13 +1479,13 @@ List Users
id,login,full_name,email,avatar_url,language,is_admin,restricted,prohibit_login,location,website,description,visibility,activated,lastlogin_at,created_at id,login,full_name,email,avatar_url,language,is_admin,restricted,prohibit_login,location,website,description,visibility,activated,lastlogin_at,created_at
(default: id,login,full_name,email,activated) (default: id,login,full_name,email,activated)
**--limit, --lm**="": specify limit of items per page **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page, default is 1 **--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional

View File

@ -6,9 +6,7 @@ package main
import ( import (
"context" "context"
"fmt"
"os" "os"
"path/filepath"
"code.gitea.io/tea/cmd" "code.gitea.io/tea/cmd"
docs "github.com/urfave/cli-docs/v3" docs "github.com/urfave/cli-docs/v3"
@ -21,40 +19,9 @@ func main() {
Name: "docs", Name: "docs",
Hidden: true, Hidden: true,
Description: "Generate CLI docs", Description: "Generate CLI docs",
Action: func(ctx context.Context, c *cli.Command) error { Flags: cmd.DocRenderFlags,
Action: func(ctx context.Context, params *cli.Command) error {
md, err := docs.ToMarkdown(cmd.App()) return cmd.RenderDocs(params, cmd.App(), docs.ToMarkdown)
if err != nil {
return err
}
outPath := c.String("out")
if outPath == "" {
fmt.Print(md)
return nil
}
if err := os.MkdirAll(filepath.Dir(outPath), os.ModePerm); err != nil {
return err
}
fi, err := os.Create(outPath)
if err != nil {
return err
}
defer fi.Close()
if _, err := fi.WriteString(md); err != nil {
return err
}
return nil
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "out",
Usage: "Path to output docs to, otherwise prints to stdout",
Aliases: []string{"o"},
},
}, },
} }
cli.Run(context.Background(), os.Args) cli.Run(context.Background(), os.Args)

24
go.mod
View File

@ -6,12 +6,13 @@ toolchain go1.24.4
require ( require (
code.gitea.io/gitea-vet v0.2.3 code.gitea.io/gitea-vet v0.2.3
code.gitea.io/sdk/gitea v0.21.0 code.gitea.io/sdk/gitea v0.22.0
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c 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/adrg/xdg v0.5.3
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/charmbracelet/glamour v0.10.0 github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/huh v0.7.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/enescakir/emoji v1.0.0 github.com/enescakir/emoji v1.0.0
github.com/go-git/go-git/v5 v5.16.2 github.com/go-git/go-git/v5 v5.16.2
github.com/muesli/termenv v0.16.0 github.com/muesli/termenv v0.16.0
@ -28,17 +29,21 @@ require (
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/42wim/httpsig v1.2.2 // indirect github.com/42wim/httpsig v1.2.3 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // 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/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // 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/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/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // 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/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/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // 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/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dlclark/regexp2 v1.11.0 // 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/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/fatih/color v1.18.0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // 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/gorilla/css v1.0.1 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // 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/kevinburke/ssh_config v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful 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-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/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/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/muesli/reflow v0.3.0 // indirect
github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.8 // indirect github.com/olekukonko/ll v0.0.8 // indirect
@ -77,8 +86,11 @@ require (
github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/net v0.40.0 // 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/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.33.0 // indirect golang.org/x/tools v0.33.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
) )
retract v1.3.3 // accidental release, tag deleted

81
go.sum
View File

@ -1,20 +1,18 @@
code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI= code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c h1:8fTkq2UaVkLHZCF+iB4wTxINmVAToe2geZGayk9LMbA= gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c h1:8fTkq2UaVkLHZCF+iB4wTxINmVAToe2geZGayk9LMbA=
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c/go.mod h1:Fc8iyPm4NINRWujeIk2bTfcbGc4ZYY29/oMAAGcr4qI= 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.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.2/go.mod h1:P/UYo7ytNBFwc+dg35IubuAUIs8zj5zzFIgUCEl55WY= github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= 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.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= 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/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 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 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 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 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 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 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 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 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 h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 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 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= 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 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= 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 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 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 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 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/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
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/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 h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= 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 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 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 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 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 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 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= 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/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 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 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 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 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 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog=
github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0= 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 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@ -86,8 +108,6 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.1 h1:TuxMBWNL7R05tXsUGi0kh1vi4tq0WfXNLlIrAkXG1k8=
github.com/go-git/go-git/v5 v5.16.1/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
@ -100,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/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 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 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 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 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 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 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= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -117,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/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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/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 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 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 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
@ -171,14 +190,11 @@ 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/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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.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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI= github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI=
github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU= github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU=
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E= github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@ -186,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 h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 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.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.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
@ -196,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-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-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-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.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 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= 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 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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.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 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 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= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -211,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-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-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-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 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 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 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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-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-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 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-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-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-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-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-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-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-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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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-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 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/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 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= 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-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-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.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 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 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= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -10,10 +10,12 @@ import (
"os" "os"
"code.gitea.io/tea/cmd" "code.gitea.io/tea/cmd"
"code.gitea.io/tea/modules/debug"
) )
func main() { func main() {
app := cmd.App() app := cmd.App()
app.Flags = append(app.Flags, debug.CliFlag())
err := app.Run(context.Background(), os.Args) err := app.Run(context.Background(), os.Args)
if err != nil { if err != nil {
// app.Run already exits for errors implementing ErrorCoder, // app.Run already exits for errors implementing ErrorCoder,

View File

@ -336,7 +336,7 @@ func startLocalServerAndOpenBrowser(authURL, expectedState string, opts OAuthOpt
// Open browser // Open browser
fmt.Println("Opening browser for authorization...") fmt.Println("Opening browser for authorization...")
if err := openBrowser(authURL); err != nil { if err := openBrowser(authURL); err != nil {
return "", "", fmt.Errorf("failed to open browser: %s", err) fmt.Println("Failed to open browser: ", err)
} }
// Wait for code, error, or timeout // Wait for code, error, or timeout

View File

@ -17,8 +17,10 @@ import (
"time" "time"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/debug"
"code.gitea.io/tea/modules/theme"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"github.com/AlecAivazis/survey/v2" "github.com/charmbracelet/huh"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -276,10 +278,18 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client {
} }
options = append(options, gitea.SetToken(l.Token), gitea.SetHTTPClient(httpClient)) options = append(options, gitea.SetToken(l.Token), gitea.SetHTTPClient(httpClient))
if debug.IsDebug() {
options = append(options, gitea.SetDebugMode())
}
if ok, err := utils.IsKeyEncrypted(l.SSHKey); ok && err == nil && l.SSHPassphrase == "" { 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 := huh.NewInput().
if err = survey.AskOne(promptPW, &l.SSHPassphrase, survey.WithValidator(survey.Required)); err != nil { 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) log.Fatal(err)
} }
} }

View File

@ -9,20 +9,21 @@ import (
"log" "log"
"os" "os"
"path" "path"
"strconv"
"strings" "strings"
"time"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/git" "code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/theme"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"github.com/charmbracelet/huh"
gogit "github.com/go-git/go-git/v5" gogit "github.com/go-git/go-git/v5"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
var ( var errNotAGiteaRepo = errors.New("No Gitea login found. You might want to specify --repo (and --login) to work outside of a repository")
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 // TeaContext contains all context derived during command initialization and wraps cli.Context
type TeaContext struct { type TeaContext struct {
@ -35,22 +36,6 @@ type TeaContext struct {
LocalRepo *git.TeaRepo // is set if flags specified a local repo via --repo, or if $PWD is a git repo LocalRepo *git.TeaRepo // is set if flags specified a local repo via --repo, or if $PWD is a git repo
} }
// GetListOptions return ListOptions based on PaginationFlags
func (ctx *TeaContext) GetListOptions() gitea.ListOptions {
page := ctx.Int("page")
limit := ctx.Int("limit")
if limit < 0 {
limit = 0
}
if limit != 0 && page == 0 {
page = 1
}
return gitea.ListOptions{
Page: page,
PageSize: limit,
}
}
// GetRemoteRepoHTMLURL returns the web-ui url of the remote repo, // GetRemoteRepoHTMLURL returns the web-ui url of the remote repo,
// after ensuring a remote repo is present in the context. // after ensuring a remote repo is present in the context.
func (ctx *TeaContext) GetRemoteRepoHTMLURL() string { func (ctx *TeaContext) GetRemoteRepoHTMLURL() string {
@ -125,6 +110,16 @@ func InitCommand(cmd *cli.Command) *TeaContext {
c.RepoSlug = repoFlag 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 // override login from flag, or use default login if repo based detection failed
if len(loginFlag) != 0 { if len(loginFlag) != 0 {
c.Login = config.GetLoginByName(loginFlag) c.Login = config.GetLoginByName(loginFlag)
@ -142,7 +137,18 @@ and then run your command again.`)
} }
os.Exit(1) os.Exit(1)
} }
fmt.Fprintf(os.Stderr, "NOTE: no gitea login detected, falling back to login '%s'\n", c.Login.Name)
fallback := false
if err := huh.NewConfirm().
Title(fmt.Sprintf("NOTE: no gitea login detected, whether falling back to login '%s'?", c.Login.Name)).
Value(&fallback).
WithTheme(theme.GetTheme()).
Run(); err != nil {
log.Fatalf("Get confirm failed: %v", err)
}
if !fallback {
os.Exit(1)
}
} }
// parse reposlug (owner falling back to login owner if reposlug contains only repo name) // parse reposlug (owner falling back to login owner if reposlug contains only repo name)
@ -230,3 +236,40 @@ func contextFromLocalRepo(repoPath, remoteValue string) (*git.TeaRepo, *config.L
return repo, nil, "", errNotAGiteaRepo 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,
}
}

44
modules/debug/debug.go Normal file
View File

@ -0,0 +1,44 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package debug
import (
"context"
"fmt"
"github.com/urfave/cli/v3"
)
var debug bool
// IsDebug returns true if debug mode is enabled
func IsDebug() bool {
return debug
}
// SetDebug sets the debug mode
func SetDebug(on bool) {
debug = on
}
// Printf prints debug information if debug mode is enabled
func Printf(info string, args ...any) {
if debug {
fmt.Printf("DEBUG: "+info+"\n", args...)
}
}
// CliFlag returns the CLI flag for debug mode
func CliFlag() cli.Flag {
return &cli.BoolFlag{
Name: "debug",
Aliases: []string{"vvv"},
Usage: "Enable debug mode",
Value: false,
Action: func(ctx context.Context, cmd *cli.Command, v bool) error {
SetDebug(v)
return nil
},
}
}

View File

@ -8,10 +8,12 @@ import (
"os" "os"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "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" "golang.org/x/term"
) )
@ -19,7 +21,7 @@ import (
// If that flag is unset, and output is not piped, prompts the user first. // If that flag is unset, and output is not piped, prompts the user first.
func ShowCommentsMaybeInteractive(ctx *context.TeaContext, idx int64, totalComments int) error { func ShowCommentsMaybeInteractive(ctx *context.TeaContext, idx int64, totalComments int) error {
if ctx.Bool("comments") { if ctx.Bool("comments") {
opts := gitea.ListIssueCommentOptions{ListOptions: ctx.GetListOptions()} opts := gitea.ListIssueCommentOptions{ListOptions: flags.GetListOptions()}
c := ctx.Login.Client() c := ctx.Login.Client()
comments, _, err := c.ListIssueComments(ctx.Owner, ctx.Repo, idx, opts) comments, _, err := c.ListIssueComments(ctx.Owner, ctx.Repo, idx, opts)
if err != nil { if err != nil {
@ -38,7 +40,7 @@ func ShowCommentsMaybeInteractive(ctx *context.TeaContext, idx int64, totalComme
// ShowCommentsPaginated prompts if issue/pr comments should be shown and continues to do so. // ShowCommentsPaginated prompts if issue/pr comments should be shown and continues to do so.
func ShowCommentsPaginated(ctx *context.TeaContext, idx int64, totalComments int) error { func ShowCommentsPaginated(ctx *context.TeaContext, idx int64, totalComments int) error {
c := ctx.Login.Client() c := ctx.Login.Client()
opts := gitea.ListIssueCommentOptions{ListOptions: ctx.GetListOptions()} opts := gitea.ListIssueCommentOptions{ListOptions: flags.GetListOptions()}
prompt := "show comments?" prompt := "show comments?"
commentsLoaded := 0 commentsLoaded := 0
@ -46,9 +48,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 // NOTE: as of gitea 1.13, pagination is not provided by this endpoint, but handles
// this function gracefully anyways. // this function gracefully anyways.
for { for {
loadComments := false loadComments := true
confirm := survey.Confirm{Message: prompt, Default: true} if err := huh.NewConfirm().
if err := survey.AskOne(&confirm, &loadComments); err != nil { Title(prompt).
Value(&loadComments).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err return err
} else if !loadComments { } else if !loadComments {
break break

View File

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

View File

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

View File

@ -4,113 +4,199 @@
package interact package interact
import ( import (
"errors"
"fmt" "fmt"
"net/url"
"regexp" "regexp"
"strconv"
"strings" "strings"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/auth" "code.gitea.io/tea/modules/auth"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/task" "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 // CreateLogin create an login interactive
func CreateLogin() error { func CreateLogin() error {
var ( var (
name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint string name, token, user, passwd, otp, scopes, sshKey, sshCertPrincipal, sshKeyFingerprint string
insecure, sshAgent, versionCheck, helper bool insecure, sshAgent, versionCheck, helper bool
) )
versionCheck = true versionCheck = true
helper = false helper = false
promptI := &survey.Input{Message: "URL of Gitea instance: "} giteaURL := "https://gitea.com"
if err := survey.AskOne(promptI, &giteaURL, survey.WithValidator(survey.Required)); err != nil { 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 return err
} }
printTitleAndContent("URL of Gitea instance: ", giteaURL)
giteaURL = strings.TrimSuffix(strings.TrimSpace(giteaURL), "/") giteaURL = strings.TrimSuffix(strings.TrimSpace(giteaURL), "/")
if len(giteaURL) == 0 {
fmt.Println("URL is required!")
return nil
}
name, err := task.GenerateLoginName(giteaURL, "") name, err := task.GenerateLoginName(giteaURL, "")
if err != nil { if err != nil {
return err return err
} }
promptI = &survey.Input{Message: "Name of new Login: ", Default: name} validateFunc := func(s string) error {
if err := survey.AskOne(promptI, &name); err != nil { if err := huh.ValidateNotEmpty()(s); err != nil {
return err
}
logins, err := config.GetLogins()
if err != nil {
return err
}
for _, login := range logins {
if login.Name == name {
return fmt.Errorf("Login with name '%s' already exists", name)
}
}
return nil
}
if err := huh.NewInput().
Title("Name of new Login: ").
Value(&name).
Validate(validateFunc).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err return err
} }
printTitleAndContent("Name of new Login: ", name)
loginMethod, err := promptSelectV2("Login with: ", []string{"token", "ssh-key/certificate", "oauth"}) loginMethod, err := promptSelectV2("Login with: ", []string{"token", "ssh-key/certificate", "oauth"})
if err != nil { if err != nil {
return err return err
} }
printTitleAndContent("Login with: ", loginMethod)
switch loginMethod { switch loginMethod {
case "oauth": case "oauth":
promptYN := &survey.Confirm{ if err := huh.NewConfirm().
Message: "Allow Insecure connections: ", Title("Allow Insecure connections:").
Default: false, Value(&insecure).
} WithTheme(theme.GetTheme()).
if err = survey.AskOne(promptYN, &insecure); err != nil { Run(); err != nil {
return err return err
} }
printTitleAndContent("Allow Insecure connections:", strconv.FormatBool(insecure))
return auth.OAuthLoginWithOptions(name, giteaURL, insecure) return auth.OAuthLoginWithOptions(name, giteaURL, insecure)
default: // token default: // token
var hasToken bool var hasToken bool
promptYN := &survey.Confirm{ if err := huh.NewConfirm().
Message: "Do you have an access token?", Title("Do you have an access token?").
Default: false, Value(&hasToken).
} WithTheme(theme.GetTheme()).
if err = survey.AskOne(promptYN, &hasToken); err != nil { Run(); err != nil {
return err return err
} }
printTitleAndContent("Do you have an access token?", strconv.FormatBool(hasToken))
if hasToken { if hasToken {
promptI = &survey.Input{Message: "Token: "} if err := huh.NewInput().
if err := survey.AskOne(promptI, &token, survey.WithValidator(survey.Required)); err != nil { Title("Token:").
Value(&token).
Validate(huh.ValidateNotEmpty()).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err return err
} }
printTitleAndContent("Token:", token)
} else { } else {
promptI = &survey.Input{Message: "Username: "} if err := huh.NewInput().
if err = survey.AskOne(promptI, &user, survey.WithValidator(survey.Required)); err != nil { Title("Username:").
Value(&user).
Validate(huh.ValidateNotEmpty()).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err return err
} }
printTitleAndContent("Username:", user)
promptPW := &survey.Password{Message: "Password: "} if err := huh.NewInput().
if err = survey.AskOne(promptPW, &passwd, survey.WithValidator(survey.Required)); err != nil { Title("Password:").
Value(&passwd).
Validate(huh.ValidateNotEmpty()).
EchoMode(huh.EchoModePassword).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err return err
} }
printTitleAndContent("Password:", "********")
var tokenScopes []string var tokenScopes []string
promptS := &survey.MultiSelect{Message: "Token Scopes:", Options: tokenScopeOpts} if err := huh.NewMultiSelect[string]().
if err := survey.AskOne(promptS, &tokenScopes, survey.WithValidator(survey.Required)); err != nil { 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 return err
} }
printTitleAndContent("Token Scopes:", strings.Join(tokenScopes, "\n"))
scopes = strings.Join(tokenScopes, ",") scopes = strings.Join(tokenScopes, ",")
// Ask for OTP last so it's less likely to timeout // Ask for OTP last so it's less likely to timeout
promptO := &survey.Input{Message: "OTP (if applicable)"} if err := huh.NewInput().
if err := survey.AskOne(promptO, &otp); err != nil { Title("OTP (if applicable):").
Value(&otp).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err return err
} }
printTitleAndContent("OTP (if applicable):", otp)
} }
case "ssh-key/certificate": case "ssh-key/certificate":
promptI = &survey.Input{Message: "SSH Key/Certificate Path (leave empty for auto-discovery in ~/.ssh and ssh-agent):"} if err := huh.NewInput().
if err := survey.AskOne(promptI, &sshKey); err != nil { 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 return err
} }
printTitleAndContent("SSH Key/Certificate Path (leave empty for auto-discovery in ~/.ssh and ssh-agent):", sshKey)
if 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 { if err != nil {
return err return err
} }
printTitleAndContent("Selected ssh-key:", sshKey)
// ssh certificate // ssh certificate
if strings.Contains(sshKey, "principals") { if strings.Contains(sshKey, "principals") {
@ -136,42 +222,51 @@ func CreateLogin() error {
} }
var optSettings bool var optSettings bool
promptYN := &survey.Confirm{ if err := huh.NewConfirm().
Message: "Set Optional settings: ", Title("Set Optional settings:").
Default: false, Value(&optSettings).
} WithTheme(theme.GetTheme()).
if err = survey.AskOne(promptYN, &optSettings); err != nil { Run(); err != nil {
return err return err
} }
printTitleAndContent("Set Optional settings:", strconv.FormatBool(optSettings))
if optSettings { if optSettings {
promptI = &survey.Input{Message: "SSH Key Path (leave empty for auto-discovery):"} if err := huh.NewInput().
if err := survey.AskOne(promptI, &sshKey); err != nil { Title("SSH Key Path (leave empty for auto-discovery):").
Value(&sshKey).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err return err
} }
printTitleAndContent("SSH Key Path (leave empty for auto-discovery):", sshKey)
promptYN = &survey.Confirm{ if err := huh.NewConfirm().
Message: "Allow Insecure connections: ", Title("Allow Insecure connections:").
Default: false, Value(&insecure).
} WithTheme(theme.GetTheme()).
if err = survey.AskOne(promptYN, &insecure); err != nil { Run(); err != nil {
return err return err
} }
printTitleAndContent("Allow Insecure connections:", strconv.FormatBool(insecure))
promptYN = &survey.Confirm{ if err := huh.NewConfirm().
Message: "Add git helper: ", Title("Add git helper:").
Default: false, Value(&helper).
} WithTheme(theme.GetTheme()).
if err = survey.AskOne(promptYN, &helper); err != nil { Run(); err != nil {
return err return err
} }
printTitleAndContent("Add git helper:", strconv.FormatBool(helper))
promptYN = &survey.Confirm{ if err := huh.NewConfirm().
Message: "Check version of Gitea instance: ", Title("Check version of Gitea instance:").
Default: true, Value(&versionCheck).
} WithTheme(theme.GetTheme()).
if err = survey.AskOne(promptYN, &versionCheck); err != nil { Run(); err != nil {
return err 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) 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 package interact
import ( import (
"fmt"
"time" "time"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/task" "code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/theme"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/AlecAivazis/survey/v2" "github.com/charmbracelet/huh"
) )
// CreateMilestone interactively creates a milestone // CreateMilestone interactively creates a milestone
func CreateMilestone(login *config.Login, owner, repo string) error { func CreateMilestone(login *config.Login, owner, repo string) error {
var title, description string var title, description, deadline string
var deadline *time.Time
// owner, repo // owner, repo
owner, repo, err := promptRepoSlug(owner, repo) owner, repo, err := promptRepoSlug(owner, repo)
if err != nil { if err != nil {
return err return err
} }
printTitleAndContent("Target repo:", fmt.Sprintf("%s/%s", owner, repo))
// title if err := huh.NewForm(
promptOpts := survey.WithValidator(survey.Required) huh.NewGroup(
promptI := &survey.Input{Message: "Milestone title:"} huh.NewInput().
if err := survey.AskOne(promptI, &title, promptOpts); err != nil { 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 return err
} }
// description var deadlineTM *time.Time
promptM := NewMultiline(Multiline{ if deadline != "" {
Message: "Milestone description:", tm, _ := time.Parse("2006-01-02", deadline)
Syntax: "md", deadlineTM = &tm
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
} }
return task.CreateMilestone( return task.CreateMilestone(
@ -52,6 +65,6 @@ func CreateMilestone(login *config.Login, owner, repo string) error {
repo, repo,
title, title,
description, description,
deadline, deadlineTM,
gitea.StateOpen) 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 ( import (
"fmt" "fmt"
"slices"
"strings" "strings"
"time" "time"
"code.gitea.io/tea/modules/theme"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"github.com/AlecAivazis/survey/v2" "github.com/charmbracelet/huh"
"github.com/araddon/dateparse"
) )
// 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. // PromptPassword asks for a password and blocks until input was made.
func PromptPassword(name string) (pass string, err error) { func PromptPassword(name string) (pass string, err error) {
promptPW := &survey.Password{Message: name + " password:"} err = huh.NewInput().
err = survey.AskOne(promptPW, &pass, survey.WithValidator(survey.Required)) Title(name + " password:").
Validate(huh.ValidateNotEmpty()).EchoMode(huh.EchoModePassword).
Value(&pass).
WithTheme(theme.GetTheme()).
Run()
return return
} }
@ -60,28 +38,21 @@ func promptRepoSlug(defaultOwner, defaultRepo string) (owner, repo string, err e
owner = defaultOwner owner = defaultOwner
repo = defaultRepo repo = defaultRepo
repoSlug = defaultVal
err = survey.AskOne( err = huh.NewInput().
&survey.Input{ Title(prompt).
Message: prompt, Value(&repoSlug).
Default: defaultVal, Validate(func(str string) error {
}, if !required && len(str) == 0 {
&repoSlug, return nil
survey.WithValidator(func(input interface{}) error { }
if str, ok := input.(string); ok { split := strings.Split(str, "/")
if !required && len(str) == 0 { if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
return nil return fmt.Errorf("must follow the <owner>/<repo> syntax")
}
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")
} }
return nil return nil
}), }).WithTheme(theme.GetTheme()).Run()
)
if err == nil && len(repoSlug) != 0 { if err == nil && len(repoSlug) != 0 {
repoSlugSplit := strings.Split(repoSlug, "/") 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. // promptDatetime prompts for a date or datetime string.
// Supports all formats understood by araddon/dateparse. // Supports all formats understood by araddon/dateparse.
func promptDatetime(prompt string) (val *time.Time, err error) { func promptDatetime(prompt string) (val *time.Time, err error) {
var input string var date string
err = survey.AskOne( if err := huh.NewInput().
&survey.Input{Message: prompt}, Title(prompt).
&input, Placeholder("YYYY-MM-DD").
survey.WithValidator(func(input interface{}) error { Validate(func(s string) error {
if str, ok := input.(string); ok { if s == "" {
if len(str) == 0 { return nil
return nil
}
t, err := dateparse.ParseAny(str)
if err != nil {
return err
}
val = &t
} else {
return fmt.Errorf("invalid result type")
} }
return nil _, err := time.Parse("2006-01-02", s)
}), return err
) }).
return 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. // promptSelect creates a generic multiselect prompt, with processing of custom values.
func promptMultiSelect(prompt string, options []string, customVal string) ([]string, error) { func promptMultiSelect(prompt string, options []string, customVal string) ([]string, error) {
var selection []string var selection []string
promptA := &survey.MultiSelect{ if err := huh.NewMultiSelect[string]().
Message: prompt, Title(prompt).
Options: makeSelectOpts(options, customVal, ""), Options(huh.NewOptions(makeSelectOpts(options, customVal, "")...)...).
VimMode: true, Value(&selection).
} WithTheme(theme.GetTheme()).
if err := survey.AskOne(promptA, &selection); err != nil { Run(); err != nil {
return nil, err return nil, err
} }
return promptCustomVal(prompt, customVal, selection) return promptCustomVal(prompt, customVal, selection)
@ -136,14 +108,13 @@ func promptSelectV2(prompt string, options []string) (string, error) {
if len(options) == 0 { if len(options) == 0 {
return "", nil return "", nil
} }
var selection string selection := options[0]
promptA := &survey.Select{ if err := huh.NewSelect[string]().
Message: prompt, Title(prompt).
Options: options, Options(huh.NewOptions(options...)...).
VimMode: true, Value(&selection).
Default: options[0], WithTheme(theme.GetTheme()).
} Run(); err != nil {
if err := survey.AskOne(promptA, &selection); err != nil {
return "", err return "", err
} }
return selection, nil return selection, nil
@ -154,17 +125,20 @@ func promptSelect(prompt string, options []string, customVal, noneVal, defaultVa
var selection string var selection string
if defaultVal == "" && noneVal != "" { if defaultVal == "" && noneVal != "" {
defaultVal = noneVal defaultVal = noneVal
} }
promptA := &survey.Select{ if len(options) > 0 && !slices.Contains(options, defaultVal) {
Message: prompt, defaultVal = options[0]
Options: makeSelectOpts(options, customVal, noneVal),
VimMode: true,
Default: defaultVal,
} }
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 return "", err
} }
if noneVal != "" && selection == noneVal { if noneVal != "" && selection == noneVal {
return "", nil return "", nil
} }
@ -193,11 +167,14 @@ func makeSelectOpts(opts []string, customVal, noneVal string) []string {
// for custom input to add to the selection instead. // for custom input to add to the selection instead.
func promptCustomVal(prompt, customVal string, selection []string) ([]string, error) { func promptCustomVal(prompt, customVal string, selection []string) ([]string, error) {
// check for custom value & prompt again with text input // 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 { if otherIndex := utils.IndexOf(selection, customVal); otherIndex != -1 {
var customAssignees string var customAssignees string
promptA := &survey.Input{Message: prompt, Help: "comma separated list"} if err := huh.NewInput().
if err := survey.AskOne(promptA, &customAssignees); err != nil { Title(prompt).
Description("comma separated list").
Value(&customAssignees).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return nil, err return nil, err
} }
selection = append(selection[:otherIndex], selection[otherIndex+1:]...) 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/context"
"code.gitea.io/tea/modules/task" "code.gitea.io/tea/modules/task"
"github.com/AlecAivazis/survey/v2" "github.com/charmbracelet/huh"
) )
// CreatePull interactively creates a PR // CreatePull interactively creates a PR
func CreatePull(ctx *context.TeaContext) (err error) { func CreatePull(ctx *context.TeaContext) (err error) {
var ( var (
base, head string base, head string
allowMaintainerEdits bool allowMaintainerEdits = true
) )
// owner, repo // 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 { if base, err = task.GetDefaultPRBase(ctx.Login, ctx.Owner, ctx.Repo); err != nil {
return err 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 var headOwner, headBranch string
promptOpts := survey.WithValidator(survey.Required) validator := huh.ValidateNotEmpty()
if ctx.LocalRepo != nil { if ctx.LocalRepo != nil {
headOwner, headBranch, err = task.GetDefaultPRHead(ctx.LocalRepo) headOwner, headBranch, err = task.GetDefaultPRHead(ctx.LocalRepo)
if err == nil { 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 := huh.NewForm(
if err := survey.AskOne(promptC, &allowMaintainerEdits); err != nil { 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 return err
} }
@ -67,6 +72,6 @@ func CreatePull(ctx *context.TeaContext) (err error) {
ctx, ctx,
base, base,
head, head,
allowMaintainerEdits, &allowMaintainerEdits,
&opts) &opts)
} }

View File

@ -7,12 +7,13 @@ import (
"fmt" "fmt"
"strings" "strings"
"code.gitea.io/sdk/gitea" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task" "code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils" "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 // MergePull interactively creates a PR
@ -43,7 +44,7 @@ func getPullIndex(ctx *context.TeaContext, branch string) (int64, error) {
c := ctx.Login.Client() c := ctx.Login.Client()
opts := gitea.ListPullRequestsOptions{ opts := gitea.ListPullRequestsOptions{
State: gitea.StateOpen, State: gitea.StateOpen,
ListOptions: ctx.GetListOptions(), ListOptions: flags.GetListOptions(),
} }
selected := "" selected := ""
loadMoreOption := "PR not found? Load more PRs..." loadMoreOption := "PR not found? Load more PRs..."
@ -76,15 +77,15 @@ func getPullIndex(ctx *context.TeaContext, branch string) (int64, error) {
prOptions = append(prOptions, loadMoreOption) prOptions = append(prOptions, loadMoreOption)
q := &survey.Select{ if err := huh.NewSelect[string]().
Message: "Select a PR to merge", Title("Select a PR to merge:").
Options: prOptions, Options(huh.NewOptions(prOptions...)...).
PageSize: 10, Value(&selected).
} Filtering(true).
err = survey.AskOne(q, &selected) Run(); err != nil {
if err != nil {
return 0, err return 0, err
} }
if selected != loadMoreOption { if selected != loadMoreOption {
break break
} }

View File

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

View File

@ -4,9 +4,18 @@
package print package print
import ( import (
"fmt"
"code.gitea.io/sdk/gitea" "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 // ReleaseAttachmentsList prints a listing of release attachments
func ReleaseAttachmentsList(attachments []*gitea.Attachment, output string) { func ReleaseAttachmentsList(attachments []*gitea.Attachment, output string) {
t := tableWithHeader( t := tableWithHeader(
@ -17,7 +26,7 @@ func ReleaseAttachmentsList(attachments []*gitea.Attachment, output string) {
for _, attachment := range attachments { for _, attachment := range attachments {
t.addRow( t.addRow(
attachment.Name, 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 // formatSize get kb in int and return string
func formatSize(kb int64) string { func formatSize(kb int64) string {
if kb < 1024 { if kb < 1024 {
return fmt.Sprintf("%d Kb", kb) return fmt.Sprintf("%d KB", kb)
} }
mb := kb / 1024 mb := kb / 1024
if mb < 1024 { if mb < 1024 {
return fmt.Sprintf("%d Mb", mb) return fmt.Sprintf("%d MB", mb)
} }
gb := mb / 1024 gb := mb / 1024
if gb < 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. // FormatTime provides a string for the given time value.

View File

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

View File

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

View File

@ -4,6 +4,7 @@
package print package print
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -103,15 +104,17 @@ func (t *table) fprint(f io.Writer, output string) {
} }
// outputTable prints structured data as table // outputTable prints structured data as table
func outputTable(f io.Writer, headers []string, values [][]string) { func outputTable(f io.Writer, headers []string, values [][]string) error {
table := tablewriter.NewWriter(f) table := tablewriter.NewWriter(f)
if len(headers) > 0 { if len(headers) > 0 {
table.Header(headers) table.Header(headers)
} }
for _, value := range values { for _, value := range values {
table.Append(value) if err := table.Append(value); err != nil {
return err
}
} }
table.Render() return table.Render()
} }
// outputSimple prints structured data as space delimited value // outputSimple prints structured data as space delimited value
@ -144,9 +147,9 @@ func outputYaml(f io.Writer, headers []string, values [][]string) {
for j, val := range value { for j, val := range value {
intVal, _ := strconv.Atoi(val) intVal, _ := strconv.Atoi(val)
if strconv.Itoa(intVal) == val { if strconv.Itoa(intVal) == val {
fmt.Fprintf(f, " %s: %s\n", headers[j], val) fmt.Fprintf(f, " %s: %s\n", headers[j], val)
} else { } else {
fmt.Fprintf(f, " %s: '%s'\n", headers[j], val) fmt.Fprintf(f, " %s: '%s'\n", headers[j], strings.ReplaceAll(val, "'", "''"))
} }
} }
} }
@ -164,6 +167,8 @@ func toSnakeCase(str string) string {
} }
// outputJSON prints structured data as json // outputJSON prints structured data as json
// Since golang's map is unordered, we need to ensure consistent ordering, we have
// to output the JSON ourselves.
func outputJSON(f io.Writer, headers []string, values [][]string) { func outputJSON(f io.Writer, headers []string, values [][]string) {
fmt.Fprintln(f, "[") fmt.Fprintln(f, "[")
itemCount := len(values) itemCount := len(values)
@ -172,12 +177,17 @@ func outputJSON(f io.Writer, headers []string, values [][]string) {
for i, value := range values { for i, value := range values {
fmt.Fprintf(f, "%s{\n", space) fmt.Fprintf(f, "%s{\n", space)
for j, val := range value { for j, val := range value {
intVal, _ := strconv.Atoi(val) v, err := json.Marshal(val)
if strconv.Itoa(intVal) == val { if err != nil {
fmt.Fprintf(f, "%s%s\"%s\": %s", space, space, toSnakeCase(headers[j]), val) fmt.Printf("Failed to format JSON for value '%s': %v\n", val, err)
} else { return
fmt.Fprintf(f, "%s%s\"%s\": \"%s\"", space, space, toSnakeCase(headers[j]), val)
} }
key, err := json.Marshal(toSnakeCase(headers[j]))
if err != nil {
fmt.Printf("Failed to format JSON for header '%s': %v\n", headers[j], err)
return
}
fmt.Fprintf(f, "%s:%s", key, v)
if j != headersCount-1 { if j != headersCount-1 {
fmt.Fprintln(f, ",") fmt.Fprintln(f, ",")
} else { } else {

View File

@ -21,6 +21,9 @@ func TestPrint(t *testing.T) {
values: [][]string{ values: [][]string{
{"new a", "some bbbb"}, {"new a", "some bbbb"},
{"AAAAA", "b2"}, {"AAAAA", "b2"},
{"\"abc", "\"def"},
{"'abc", "de'f"},
{"\\abc", "'def\\"},
}, },
} }
@ -33,7 +36,37 @@ func TestPrint(t *testing.T) {
}{} }{}
assert.NoError(t, json.NewDecoder(buf).Decode(&result)) assert.NoError(t, json.NewDecoder(buf).Decode(&result))
if assert.Len(t, result, 2) { if assert.Len(t, result, 5) {
assert.EqualValues(t, "new a", result[0].A) assert.EqualValues(t, "new a", result[0].A)
assert.EqualValues(t, "some bbbb", result[0].B)
assert.EqualValues(t, "AAAAA", result[1].A)
assert.EqualValues(t, "b2", result[1].B)
assert.EqualValues(t, "\"abc", result[2].A)
assert.EqualValues(t, "\"def", result[2].B)
assert.EqualValues(t, "'abc", result[3].A)
assert.EqualValues(t, "de'f", result[3].B)
assert.EqualValues(t, "\\abc", result[4].A)
assert.EqualValues(t, "'def\\", result[4].B)
} }
buf.Reset()
tData.fprint(buf, "yaml")
assert.Equal(t, `-
A: 'new a'
B: 'some bbbb'
-
A: 'AAAAA'
B: 'b2'
-
A: '"abc'
B: '"def'
-
A: '''abc'
B: 'de''f'
-
A: '\abc'
B: '''def\'
`, buf.String())
} }

View File

@ -13,7 +13,6 @@ import (
// CreateIssue creates an issue in the given repo and prints the result // CreateIssue creates an issue in the given repo and prints the result
func CreateIssue(login *config.Login, repoOwner, repoName string, opts gitea.CreateIssueOption) error { func CreateIssue(login *config.Login, repoOwner, repoName string, opts gitea.CreateIssueOption) error {
// title is required // title is required
if len(opts.Title) == 0 { if len(opts.Title) == 0 {
return fmt.Errorf("Title is required") 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) return fmt.Errorf("token already been used, delete login '%s' first", login.Name)
} }
if !sshAgent && sshCertPrincipal == "" && sshKey == "" { serverURL, err := utils.ValidateAuthenticationMethod(
// .. if we have enough information to authenticate giteaURL,
if len(token) == 0 && (len(user)+len(passwd)) == 0 { token,
return fmt.Errorf("No token set") user,
} else if len(user) != 0 && len(passwd) == 0 { passwd,
return fmt.Errorf("No password set") sshAgent,
} else if len(user) == 0 && len(passwd) != 0 { sshKey,
return fmt.Errorf("No user set") sshCertPrincipal,
} )
}
// Normalize URL
serverURL, err := utils.NormalizeURL(giteaURL)
if err != nil { 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 // check if it's a certificate the principal doesn't matter as the user
@ -171,8 +167,12 @@ func generateToken(login config.Login, user, pass, otp, scopes string) (string,
} }
var tokenScopes []gitea.AccessTokenScope var tokenScopes []gitea.AccessTokenScope
for _, scope := range strings.Split(scopes, ",") { if len(scopes) == 0 {
tokenScopes = append(tokenScopes, gitea.AccessTokenScope(strings.TrimSpace(scope))) tokenScopes = []gitea.AccessTokenScope{gitea.AccessTokenScopeAll}
} else {
for _, scope := range strings.Split(scopes, ",") {
tokenScopes = append(tokenScopes, gitea.AccessTokenScope(strings.TrimSpace(scope)))
}
} }
t, _, err := client.CreateAccessToken(gitea.CreateAccessTokenOption{ t, _, err := client.CreateAccessToken(gitea.CreateAccessTokenOption{

View File

@ -23,7 +23,7 @@ var (
) )
// CreatePull creates a PR in the given repo and prints the result // CreatePull creates a PR in the given repo and prints the result
func CreatePull(ctx *context.TeaContext, base, head string, allowMaintainerEdits bool, opts *gitea.CreateIssueOption) (err error) { func CreatePull(ctx *context.TeaContext, base, head string, allowMaintainerEdits *bool, opts *gitea.CreateIssueOption) (err error) {
// default is default branch // default is default branch
if len(base) == 0 { if len(base) == 0 {
base, err = GetDefaultPRBase(ctx.Login, ctx.Owner, ctx.Repo) base, err = GetDefaultPRBase(ctx.Login, ctx.Owner, ctx.Repo)
@ -75,9 +75,9 @@ func CreatePull(ctx *context.TeaContext, base, head string, allowMaintainerEdits
return fmt.Errorf("could not create PR from %s to %s:%s: %s", head, ctx.Owner, base, err) return fmt.Errorf("could not create PR from %s to %s:%s: %s", head, ctx.Owner, base, err)
} }
if pr.AllowMaintainerEdit != allowMaintainerEdits { if allowMaintainerEdits != nil && pr.AllowMaintainerEdit != *allowMaintainerEdits {
pr, _, err = client.EditPullRequest(ctx.Owner, ctx.Repo, pr.Index, gitea.EditPullRequestOption{ pr, _, err = client.EditPullRequest(ctx.Owner, ctx.Repo, pr.Index, gitea.EditPullRequestOption{
AllowMaintainerEdit: gitea.OptionalBool(allowMaintainerEdits), AllowMaintainerEdit: allowMaintainerEdits,
}) })
if err != nil { if err != nil {
return fmt.Errorf("could not enable maintainer edit on pull: %v", err) return fmt.Errorf("could not enable maintainer edit on pull: %v", err)

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
}