22 Commits

Author SHA1 Message Date
appleboy
5bb73667d1 docs: add v0.13.0 release notes to CHANGELOG (#945)
Add v0.13.0 release notes to CHANGELOG.md covering 21 commits since v0.12.0: 5 new features, 2 enhancements, and dependency updates.

Reviewed-on: https://gitea.com/gitea/tea/pulls/945
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2026-04-05 16:42:27 +00:00
appleboy
f329f6fab2 feat(pulls): add edit subcommand for pull requests (#944)
## Summary

- Add `tea pr edit` subcommand to support editing pull request properties (description, title, milestone, deadline, assignees, labels, reviewers)
- Add `--add-reviewers` / `--remove-reviewers` flags for managing PR reviewers via `CreateReviewRequests` / `DeleteReviewRequests` API
- Extract shared helpers (`ResolveLabelOpts`, `ApplyLabelChanges`, `ApplyReviewerChanges`, `ResolveMilestoneID`) into `modules/task/labels.go` to reduce duplication between issue and PR editing
- Refactor existing `EditIssue` to use the same shared helpers
- Wrap original error in `ResolveMilestoneID` to preserve underlying error context

## Usage

```bash
# Edit PR description
tea pr edit 1 --description "new description"

# Edit PR title
tea pr edit 1 --title "new title"

# Edit multiple fields
tea pr edit 1 --title "new title" --description "new desc" --add-labels "bug"

# Edit multiple PRs
tea pr edit 1 2 3 --add-assignees "user1"

# Add reviewers
tea pr edit 1 --add-reviewers "user1,user2"

# Remove reviewers
tea pr edit 1 --remove-reviewers "user1"
```

## Test plan

- [x] `go build .` succeeds
- [x] `go test ./...` passes
- [x] `make clean && make vet && make lint && make fmt-check && make docs-check && make build` all pass
- [x] `tea pr edit <idx> --description "test"` updates PR description on a Gitea instance
- [x] `tea pr edit <idx> --title "test"` updates PR title
- [x] `tea pr edit <idx> --add-labels "bug"` adds label
- [x] `tea pr edit <idx> --add-reviewers "user"` requests review
- [x] `tea pr edit <idx> --remove-reviewers "user"` removes reviewer
- [x] Existing `tea issues edit` still works correctly after refactor

Reviewed-on: https://gitea.com/gitea/tea/pulls/944
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2026-04-05 05:35:15 +00:00
Renovate Bot
366069315f fix(deps): update module github.com/go-git/go-git/v5 to v5.17.2 (#943)
Reviewed-on: https://gitea.com/gitea/tea/pulls/943
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-01 18:14:14 +00:00
Renovate Bot
1e13681663 fix(deps): update module github.com/go-git/go-git/v5 to v5.17.1 (#942)
Reviewed-on: https://gitea.com/gitea/tea/pulls/942
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-30 07:00:31 +00:00
Renovate Bot
bfbec3fc00 fix(deps): update module code.gitea.io/sdk/gitea to v0.24.1 (#936)
Reviewed-on: https://gitea.com/gitea/tea/pulls/936
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-27 06:04:29 +00:00
Renovate Bot
e31a167e54 fix(deps): update module github.com/go-authgate/sdk-go to v0.6.1 (#935)
Reviewed-on: https://gitea.com/gitea/tea/pulls/935
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-27 03:47:06 +00:00
Renovate Bot
6a7c3e4efa fix(deps): update module github.com/urfave/cli/v3 to v3.8.0 (#937)
Reviewed-on: https://gitea.com/gitea/tea/pulls/937
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-27 03:46:50 +00:00
techknowlogick
b05e03416b replace log.Fatal/os.Exit with error returns (#941)
* Use stdlib encoders
* Reduce some duplication
* Remove global pagination state
* Dedupe JSON detail types
* Bump golangci-lint

Reviewed-on: https://gitea.com/gitea/tea/pulls/941
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-03-27 03:36:44 +00:00
Renovate Bot
21881525a8 chore(deps): update docker.gitea.com/gitea docker tag to v1.25.5 (#934)
Reviewed-on: https://gitea.com/gitea/tea/pulls/934
Reviewed-by: techknowlogick <9+techknowlogick@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-15 23:04:40 +00:00
Renovate Bot
9a462247bd fix(deps): update module github.com/olekukonko/tablewriter to v1.1.4 (#933)
Reviewed-on: https://gitea.com/gitea/tea/pulls/933
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-13 00:18:26 +00:00
Renovate Bot
5f74fb37df chore(deps): update mcr.microsoft.com/devcontainers/go docker tag to v2.1 (#930)
Reviewed-on: https://gitea.com/gitea/tea/pulls/930
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-12 17:13:25 +00:00
Bo-Yi Wu
ec658cfc33 chore(deps): update Go dependencies and CI workflow action versions (#932)
## Summary

- Run `go get -u ./...` and `go mod tidy` to update all Go dependencies
- Update CI workflow action versions:
  - `crazy-max/ghaction-import-gpg`: v6 → v7
  - `goreleaser/goreleaser-action`: v6 → v7
  - `docker/setup-qemu-action`: v3 → v4
  - `docker/setup-buildx-action`: v3 → v4
  - `docker/login-action`: v3 → v4
  - `docker/build-push-action`: v6 → v7

## Notable Go dependency updates

- `github.com/urfave/cli/v3`: v3.6.2 → v3.7.0
- `github.com/ProtonMail/go-crypto`: v1.3.0 → v1.4.0
- `charm.land/huh/v2`: v2.0.1 → v2.0.3
- `golang.org/x/crypto`: v0.48.0 → v0.49.0
- `golang.org/x/net`: v0.49.0 → v0.52.0

Reviewed-on: https://gitea.com/gitea/tea/pulls/932
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-03-12 05:28:47 +00:00
Bo-Yi Wu
cb9824b451 feat(repos): support owner-based repository listing with robust lookup (#931)
- Add an owner flag to the repos list command to list repositories for a specific user or organization
- Implement owner-based repository listing by detecting whether the owner is an organization or a user and calling the appropriate API
- Improve error handling for owner lookup by checking HTTP status codes instead of relying on error string matching
- Align repository search logic with the updated owner lookup behavior using HTTP response validation

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/tea/pulls/931
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-03-12 04:22:28 +00:00
Bo-Yi Wu
a531faa626 feat(repos): add repo edit subcommand (#928)
## Summary
- Add `tea repo edit` subcommand to update repository properties via the Gitea API
- Support flags: `--name`, `--description`/`--desc`, `--website`, `--private`, `--template`, `--archived`, `--default-branch`
- Boolean-like flags use string type to distinguish "not set" from "false", following the pattern in `releases/edit.go`

## Test plan
- [x] `go build ./...` passes
- [x] `go vet ./...` passes
- [x] `tea repo edit --help` shows all flags correctly
- [x] Manual test: `tea repo edit --private true` on a test repo
- [x] Manual test: `tea repo edit --name new-name --description "new desc"` on a test repo

Reviewed-on: https://gitea.com/gitea/tea/pulls/928
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-03-12 03:06:44 +00:00
Bo-Yi Wu
302c946cb8 feat: store OAuth tokens in OS keyring via credstore (#926)
## Summary

- Introduce `github.com/go-authgate/sdk-go/credstore` to store OAuth tokens securely in the OS keyring (macOS Keychain / Linux Secret Service / Windows Credential Manager), with automatic fallback to an encrypted JSON file
- Add `AuthMethod` field to `Login` struct; new OAuth logins are marked `auth_method: oauth` and no longer write `token`/`refresh_token`/`token_expiry` to `config.yml`
- Add `GetAccessToken()` / `GetRefreshToken()` / `GetTokenExpiry()` accessors that transparently read from credstore for OAuth logins, with fallback to YAML fields for legacy logins
- Update all token reference sites across the codebase to use the new accessors
- Non-OAuth logins (token, SSH) are completely unaffected; no migration of existing tokens

## Key files

| File | Role |
|------|------|
| `modules/config/credstore.go` | **New** — credstore wrapper (Load/Save/Delete) |
| `modules/config/login.go` | Login struct, token accessors, refresh logic |
| `modules/auth/oauth.go` | OAuth flow, token creation / re-authentication |
| `modules/api/client.go`, `cmd/login/helper.go`, `cmd/login/oauth_refresh.go` | Token reference updates |
| `modules/task/pull_*.go`, `modules/task/repo_clone.go` | Git operation token reference updates |

## Test plan

- [x] `go build ./...` compiles successfully
- [x] `go test ./...` all tests pass
- [x] `tea login add --oauth` completes OAuth flow; verify config.yml has `auth_method: oauth` but no token/refresh_token/token_expiry
- [x] `tea repos ls` API calls work (token read from credstore)
- [x] `tea login delete <name>` credstore token is also removed
- [x] Existing non-OAuth logins continue to work unchanged

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://gitea.com/gitea/tea/pulls/926
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-03-12 02:49:14 +00:00
techknowlogick
0346e1cbb5 add function comment 2026-03-10 10:00:39 -04:00
techknowlogick
cd4051ed38 make vet&fmt pass 2026-03-10 09:55:10 -04:00
Michal Suchanek
c797624fcf Update to charm libraries v2 (#923)
Reviewed-on: https://gitea.com/gitea/tea/pulls/923
Reviewed-by: techknowlogick <9+techknowlogick@noreply.gitea.com>
Co-authored-by: Michal Suchanek <msuchanek@suse.de>
Co-committed-by: Michal Suchanek <msuchanek@suse.de>
2026-03-09 16:36:00 +00:00
Renovate Bot
3372c9ec59 fix(deps): update module golang.org/x/oauth2 to v0.36.0 (#919)
Reviewed-on: https://gitea.com/gitea/tea/pulls/919
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-09 16:19:28 +00:00
techknowlogick
1ac8492ac7 go 1.26 2026-03-09 15:57:54 +00:00
Renovate Bot
d019f0dd72 fix(deps): update module github.com/go-git/go-git/v5 to v5.17.0 (#910)
Reviewed-on: https://gitea.com/gitea/tea/pulls/910
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-26 17:59:28 +00:00
techknowlogick
c031db2413 Parse multiple values in api subcommand (#911)
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-26 17:43:46 +00:00
152 changed files with 3160 additions and 953 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "Tea DevContainer", "name": "Tea DevContainer",
"image": "mcr.microsoft.com/devcontainers/go:2.0-trixie", "image": "mcr.microsoft.com/devcontainers/go:2.1-trixie",
"features": { "features": {
"ghcr.io/devcontainers/features/git-lfs:1.2.5": {} "ghcr.io/devcontainers/features/git-lfs:1.2.5": {}
}, },

View File

@@ -17,7 +17,7 @@ jobs:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: import gpg - name: import gpg
id: import_gpg id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6 uses: crazy-max/ghaction-import-gpg@v7
with: with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }} gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
@@ -25,7 +25,7 @@ jobs:
id: sdk_version id: sdk_version
run: echo "version=$(go list -f '{{.Version}}' -m code.gitea.io/sdk/gitea)" >> "$GITHUB_OUTPUT" run: echo "version=$(go list -f '{{.Version}}' -m code.gitea.io/sdk/gitea)" >> "$GITHUB_OUTPUT"
- name: goreleaser - name: goreleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v7
with: with:
distribution: goreleaser-pro distribution: goreleaser-pro
version: "~> v1" version: "~> v1"
@@ -54,19 +54,19 @@ jobs:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker BuildX - name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
env: env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119 ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with: with:

View File

@@ -18,7 +18,7 @@ jobs:
go-version-file: 'go.mod' go-version-file: 'go.mod'
- name: import gpg - name: import gpg
id: import_gpg id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6 uses: crazy-max/ghaction-import-gpg@v7
with: with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }} gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
@@ -26,7 +26,7 @@ jobs:
id: sdk_version id: sdk_version
run: echo "version=$(go list -f '{{.Version}}' -m code.gitea.io/sdk/gitea)" >> "$GITHUB_OUTPUT" run: echo "version=$(go list -f '{{.Version}}' -m code.gitea.io/sdk/gitea)" >> "$GITHUB_OUTPUT"
- name: goreleaser - name: goreleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v7
with: with:
distribution: goreleaser-pro distribution: goreleaser-pro
version: "~> v1" version: "~> v1"
@@ -55,13 +55,13 @@ jobs:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker BuildX - name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
@@ -71,7 +71,7 @@ jobs:
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
env: env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119 ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with: with:

View File

@@ -39,7 +39,7 @@ jobs:
make unit-test-coverage make unit-test-coverage
services: services:
gitea: gitea:
image: docker.gitea.com/gitea:1.25.4 image: docker.gitea.com/gitea:1.25.5
cmd: cmd:
- bash - bash
- -c - -c

View File

@@ -1,5 +1,20 @@
# Changelog # Changelog
## [v0.13.0](https://gitea.com/gitea/tea/releases/tag/v0.13.0) - 2026-04-05
* FEATURES
* Add `tea pr edit` subcommand for pull requests (#944)
* Add `tea repo edit` subcommand (#928)
* Support owner-based repository listing in `tea repo ls` (#931)
* Store OAuth tokens in OS keyring via credstore (#926)
* Support parsing multiple values in `tea api` subcommand (#911)
* ENHANCEMENTS
* Replace log.Fatal/os.Exit with proper error returns (#941)
* Update to charm libraries v2 (#923)
* MISC
* Bump Go version to 1.26
* Update dependencies: go-git/v5 v5.17.2, gitea SDK v0.24.1, urfave/cli/v3 v3.8.0, oauth2 v0.36.0, tablewriter v1.1.4, go-authgate/sdk-go v0.6.1
## [v0.9.1](https://gitea.com/gitea/tea/releases/tag/v0.9.1) - 2023-02-15 ## [v0.9.1](https://gitea.com/gitea/tea/releases/tag/v0.9.1) - 2023-02-15
* BUGFIXES * BUGFIXES

View File

@@ -8,7 +8,7 @@ GOFILES := $(shell find . -name "*.go" -type f ! -path "*/bindata.go")
# Tool packages with pinned versions # Tool packages with pinned versions
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
ifneq ($(DRONE_TAG),) ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG)) VERSION ?= $(subst v,,$(DRONE_TAG))

View File

@@ -169,7 +169,7 @@ 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.26 or newer).
- To compile the source yourself with the recommended flags & tags: - To compile the source yourself with the recommended flags & tags:
```sh ```sh

View File

@@ -36,7 +36,13 @@ func runRunsDelete(ctx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("run ID is required") return fmt.Errorf("run ID is required")
} }
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
runIDStr := cmd.Args().First() runIDStr := cmd.Args().First()

View File

@@ -83,7 +83,13 @@ func parseTimeFlag(value string) (time.Time, error) {
// RunRunsList lists workflow runs // RunRunsList lists workflow runs
func RunRunsList(ctx stdctx.Context, cmd *cli.Command) error { func RunRunsList(ctx stdctx.Context, cmd *cli.Command) error {
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
// Parse time filters // Parse time filters
@@ -98,7 +104,7 @@ func RunRunsList(ctx stdctx.Context, cmd *cli.Command) error {
} }
// Build list options // Build list options
listOpts := flags.GetListOptions() listOpts := flags.GetListOptions(cmd)
runs, _, err := client.ListRepoActionRuns(c.Owner, c.Repo, gitea.ListRepoActionRunsOptions{ runs, _, err := client.ListRepoActionRuns(c.Owner, c.Repo, gitea.ListRepoActionRunsOptions{
ListOptions: listOpts, ListOptions: listOpts,
@@ -112,15 +118,13 @@ func RunRunsList(ctx stdctx.Context, cmd *cli.Command) error {
} }
if runs == nil { if runs == nil {
print.ActionRunsList(nil, c.Output) return print.ActionRunsList(nil, c.Output)
return nil
} }
// Filter by time if specified // Filter by time if specified
filteredRuns := filterRunsByTime(runs.WorkflowRuns, since, until) filteredRuns := filterRunsByTime(runs.WorkflowRuns, since, until)
print.ActionRunsList(filteredRuns, c.Output) return print.ActionRunsList(filteredRuns, c.Output)
return nil
} }
// filterRunsByTime filters runs based on time range // filterRunsByTime filters runs based on time range

View File

@@ -4,10 +4,15 @@
package runs package runs
import ( import (
stdctx "context"
"os"
"testing" "testing"
"time" "time"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
) )
func TestFilterRunsByTime(t *testing.T) { func TestFilterRunsByTime(t *testing.T) {
@@ -75,3 +80,32 @@ func TestFilterRunsByTime(t *testing.T) {
}) })
} }
} }
func TestRunRunsListRequiresRepoContext(t *testing.T) {
oldWd, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(t.TempDir()))
t.Cleanup(func() {
require.NoError(t, os.Chdir(oldWd))
})
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "test",
URL: "https://gitea.example.com",
Token: "token",
User: "tester",
Default: true,
}},
})
cmd := &cli.Command{
Name: CmdRunsList.Name,
Flags: CmdRunsList.Flags,
}
require.NoError(t, cmd.Set("login", "test"))
err = RunRunsList(stdctx.Background(), cmd)
require.ErrorContains(t, err, "remote repository required")
}

View File

@@ -42,7 +42,13 @@ func runRunsLogs(ctx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("run ID is required") return fmt.Errorf("run ID is required")
} }
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
runIDStr := cmd.Args().First() runIDStr := cmd.Args().First()
@@ -78,7 +84,7 @@ func runRunsLogs(ctx stdctx.Context, cmd *cli.Command) error {
// Otherwise, fetch all jobs and their logs // Otherwise, fetch all jobs and their logs
jobs, _, err := client.ListRepoActionRunJobs(c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{ jobs, _, err := client.ListRepoActionRunJobs(c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to get jobs: %w", err) return fmt.Errorf("failed to get jobs: %w", err)

View File

@@ -38,7 +38,13 @@ func runRunsView(ctx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("run ID is required") return fmt.Errorf("run ID is required")
} }
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
runIDStr := cmd.Args().First() runIDStr := cmd.Args().First()
@@ -59,7 +65,7 @@ func runRunsView(ctx stdctx.Context, cmd *cli.Command) error {
// Fetch and print jobs if requested // Fetch and print jobs if requested
if cmd.Bool("jobs") { if cmd.Bool("jobs") {
jobs, _, err := client.ListRepoActionRunJobs(c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{ jobs, _, err := client.ListRepoActionRunJobs(c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to get jobs: %w", err) return fmt.Errorf("failed to get jobs: %w", err)
@@ -67,7 +73,9 @@ func runRunsView(ctx stdctx.Context, cmd *cli.Command) error {
if jobs != nil && len(jobs.Jobs) > 0 { if jobs != nil && len(jobs.Jobs) > 0 {
fmt.Printf("\nJobs:\n\n") fmt.Printf("\nJobs:\n\n")
print.ActionWorkflowJobsList(jobs.Jobs, c.Output) if err := print.ActionWorkflowJobsList(jobs.Jobs, c.Output); err != nil {
return err
}
} }
} }

View File

@@ -40,7 +40,13 @@ func runSecretsCreate(ctx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("secret name is required") return fmt.Errorf("secret name is required")
} }
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
secretName := cmd.Args().First() secretName := cmd.Args().First()
@@ -56,8 +62,7 @@ func runSecretsCreate(ctx stdctx.Context, cmd *cli.Command) error {
return err return err
} }
_, err = client.CreateRepoActionSecret(c.Owner, c.Repo, gitea.CreateSecretOption{ _, err = client.CreateRepoActionSecret(c.Owner, c.Repo, secretName, gitea.CreateOrUpdateSecretOption{
Name: secretName,
Data: secretValue, Data: secretValue,
}) })
if err != nil { if err != nil {

View File

@@ -35,7 +35,13 @@ func runSecretsDelete(ctx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("secret name is required") return fmt.Errorf("secret name is required")
} }
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
secretName := cmd.Args().First() secretName := cmd.Args().First()
@@ -50,7 +56,7 @@ func runSecretsDelete(ctx stdctx.Context, cmd *cli.Command) error {
} }
} }
_, err := client.DeleteRepoActionSecret(c.Owner, c.Repo, secretName) _, err = client.DeleteRepoActionSecret(c.Owner, c.Repo, secretName)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -29,16 +29,21 @@ var CmdSecretsList = cli.Command{
// RunSecretsList list action secrets // RunSecretsList list action secrets
func RunSecretsList(ctx stdctx.Context, cmd *cli.Command) error { func RunSecretsList(ctx stdctx.Context, cmd *cli.Command) error {
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
secrets, _, err := client.ListRepoActionSecret(c.Owner, c.Repo, gitea.ListRepoActionSecretOption{ secrets, _, err := client.ListRepoActionSecret(c.Owner, c.Repo, gitea.ListRepoActionSecretOption{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err != nil { if err != nil {
return err return err
} }
print.ActionSecretsList(secrets, c.Output) return print.ActionSecretsList(secrets, c.Output)
return nil
} }

View File

@@ -4,7 +4,13 @@
package secrets package secrets
import ( import (
stdctx "context"
"os"
"testing" "testing"
"code.gitea.io/tea/modules/config"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
) )
func TestSecretsListFlags(t *testing.T) { func TestSecretsListFlags(t *testing.T) {
@@ -61,3 +67,32 @@ func TestSecretsListValidation(t *testing.T) {
// This is fine - list commands typically ignore extra args // This is fine - list commands typically ignore extra args
} }
} }
func TestRunSecretsListRequiresRepoContext(t *testing.T) {
oldWd, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(t.TempDir()))
t.Cleanup(func() {
require.NoError(t, os.Chdir(oldWd))
})
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "test",
URL: "https://gitea.example.com",
Token: "token",
User: "tester",
Default: true,
}},
})
cmd := &cli.Command{
Name: CmdSecretsList.Name,
Flags: CmdSecretsList.Flags,
}
require.NoError(t, cmd.Set("login", "test"))
err = RunSecretsList(stdctx.Background(), cmd)
require.ErrorContains(t, err, "remote repository required")
}

View File

@@ -35,7 +35,13 @@ func runVariablesDelete(ctx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("variable name is required") return fmt.Errorf("variable name is required")
} }
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
variableName := cmd.Args().First() variableName := cmd.Args().First()
@@ -50,7 +56,7 @@ func runVariablesDelete(ctx stdctx.Context, cmd *cli.Command) error {
} }
} }
_, err := client.DeleteRepoActionVariable(c.Owner, c.Repo, variableName) _, err = client.DeleteRepoActionVariable(c.Owner, c.Repo, variableName)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -31,7 +31,13 @@ var CmdVariablesList = cli.Command{
// RunVariablesList list action variables // RunVariablesList list action variables
func RunVariablesList(ctx stdctx.Context, cmd *cli.Command) error { func RunVariablesList(ctx stdctx.Context, cmd *cli.Command) error {
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
if name := cmd.String("name"); name != "" { if name := cmd.String("name"); name != "" {

View File

@@ -4,7 +4,13 @@
package variables package variables
import ( import (
stdctx "context"
"os"
"testing" "testing"
"code.gitea.io/tea/modules/config"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
) )
func TestVariablesListFlags(t *testing.T) { func TestVariablesListFlags(t *testing.T) {
@@ -61,3 +67,32 @@ func TestVariablesListValidation(t *testing.T) {
// This is fine - list commands typically ignore extra args // This is fine - list commands typically ignore extra args
} }
} }
func TestRunVariablesListRequiresRepoContext(t *testing.T) {
oldWd, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(t.TempDir()))
t.Cleanup(func() {
require.NoError(t, os.Chdir(oldWd))
})
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "test",
URL: "https://gitea.example.com",
Token: "token",
User: "tester",
Default: true,
}},
})
cmd := &cli.Command{
Name: CmdVariablesList.Name,
Flags: CmdVariablesList.Flags,
}
require.NoError(t, cmd.Set("login", "test"))
err = RunVariablesList(stdctx.Background(), cmd)
require.ErrorContains(t, err, "remote repository required")
}

View File

@@ -40,7 +40,13 @@ func runVariablesSet(ctx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("variable name is required") return fmt.Errorf("variable name is required")
} }
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
variableName := cmd.Args().First() variableName := cmd.Args().First()

View File

@@ -32,7 +32,13 @@ var CmdWorkflowsList = cli.Command{
// RunWorkflowsList lists workflow files in the repository // RunWorkflowsList lists workflow files in the repository
func RunWorkflowsList(ctx stdctx.Context, cmd *cli.Command) error { func RunWorkflowsList(ctx stdctx.Context, cmd *cli.Command) error {
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
// Try to list workflow files from .gitea/workflows directory // Try to list workflow files from .gitea/workflows directory
@@ -71,7 +77,7 @@ func RunWorkflowsList(ctx stdctx.Context, cmd *cli.Command) error {
// Get recent runs to check activity // Get recent runs to check activity
runs, _, err := client.ListRepoActionRuns(c.Owner, c.Repo, gitea.ListRepoActionRunsOptions{ runs, _, err := client.ListRepoActionRuns(c.Owner, c.Repo, gitea.ListRepoActionRunsOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err == nil && runs != nil { if err == nil && runs != nil {
for _, run := range runs.WorkflowRuns { for _, run := range runs.WorkflowRuns {
@@ -81,6 +87,5 @@ func RunWorkflowsList(ctx stdctx.Context, cmd *cli.Command) error {
} }
} }
print.WorkflowsList(workflows, workflowStatus, c.Output) return print.WorkflowsList(workflows, workflowStatus, c.Output)
return nil
} }

View File

@@ -44,7 +44,10 @@ var cmdAdminUsers = cli.Command{
} }
func runAdminUserDetail(_ stdctx.Context, cmd *cli.Command, u string) error { func runAdminUserDetail(_ stdctx.Context, cmd *cli.Command, u string) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
user, _, err := client.GetUserInfo(u) user, _, err := client.GetUserInfo(u)
if err != nil { if err != nil {

View File

@@ -34,7 +34,10 @@ var CmdUserList = cli.Command{
// RunUserList list users // RunUserList list users
func RunUserList(_ stdctx.Context, cmd *cli.Command) error { func RunUserList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
fields, err := userFieldsFlag.GetValues(cmd) fields, err := userFieldsFlag.GetValues(cmd)
if err != nil { if err != nil {
@@ -43,13 +46,11 @@ 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: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err != nil { if err != nil {
return err return err
} }
print.UserList(users, ctx.Output, fields) return print.UserList(users, ctx.Output, fields)
return nil
} }

View File

@@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"bytes"
stdctx "context" stdctx "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -20,23 +21,11 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
// CmdApi represents the api command // apiFlags returns a fresh set of flag instances for the api command.
var CmdApi = cli.Command{ // This is a factory function so that each invocation gets independent flag
Name: "api", // objects, avoiding shared hasBeenSet state across tests.
Usage: "Make an authenticated API request", func apiFlags() []cli.Flag {
Description: `Makes an authenticated HTTP request to the Gitea API and prints the response. return []cli.Flag{
The endpoint argument is the path to the API endpoint, which will be prefixed
with /api/v1/ if it doesn't start with /api/ or http(s)://.
Placeholders like {owner} and {repo} in the endpoint will be replaced with
values from the current repository context.
Use -f for string fields and -F for typed fields (numbers, booleans, null).
With -F, prefix value with @ to read from file (@- for stdin).`,
ArgsUsage: "<endpoint>",
Action: runApi,
Flags: append([]cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "method", Name: "method",
Aliases: []string{"X"}, Aliases: []string{"X"},
@@ -58,6 +47,11 @@ With -F, prefix value with @ to read from file (@- for stdin).`,
Aliases: []string{"H"}, Aliases: []string{"H"},
Usage: "Add a custom header (key:value)", Usage: "Add a custom header (key:value)",
}, },
&cli.StringFlag{
Name: "data",
Aliases: []string{"d"},
Usage: "Raw JSON request body (use @file to read from file, @- for stdin)",
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "include", Name: "include",
Aliases: []string{"i"}, Aliases: []string{"i"},
@@ -68,80 +62,74 @@ With -F, prefix value with @ to read from file (@- for stdin).`,
Aliases: []string{"o"}, Aliases: []string{"o"},
Usage: "Write response body to file instead of stdout (use '-' for stdout)", Usage: "Write response body to file instead of stdout (use '-' for stdout)",
}, },
}, flags.LoginRepoFlags...), }
}
// CmdApi represents the api command
var CmdApi = cli.Command{
Name: "api",
Category: catHelpers,
DisableSliceFlagSeparator: true,
Usage: "Make an authenticated API request",
Description: `Makes an authenticated HTTP request to the Gitea API and prints the response.
The endpoint argument is the path to the API endpoint, which will be prefixed
with /api/v1/ if it doesn't start with /api/ or http(s)://.
Placeholders like {owner} and {repo} in the endpoint will be replaced with
values from the current repository context.
Use -f for string fields and -F for typed fields (numbers, booleans, null).
With -F, prefix value with @ to read from file (@- for stdin). Values starting
with [ or { are parsed as JSON arrays/objects. Wrap values in quotes to force
string type (e.g., -F key="null" for literal string "null").
Use -d/--data to send a raw JSON body. Use @file to read from a file, or @-
to read from stdin. The -d flag cannot be combined with -f or -F.
When a request body is provided via -f, -F, or -d, the method defaults to POST
unless explicitly set with -X/--method.
Note: if your endpoint contains ? or &, quote it to prevent shell expansion
(e.g., '/repos/{owner}/{repo}/issues?state=open').`,
ArgsUsage: "<endpoint>",
Action: runApi,
Flags: append(apiFlags(), flags.LoginRepoFlags...),
}
type preparedAPIRequest struct {
Method string
Endpoint string
Headers map[string]string
Body []byte
} }
func runApi(_ stdctx.Context, cmd *cli.Command) error { func runApi(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
// Get the endpoint argument return err
if cmd.NArg() < 1 {
return fmt.Errorf("endpoint argument required")
} }
endpoint := cmd.Args().First() request, err := prepareAPIRequest(cmd, ctx)
if err != nil {
// Expand placeholders in endpoint return err
endpoint = expandPlaceholders(endpoint, ctx)
// Parse headers
headers := make(map[string]string)
for _, h := range cmd.StringSlice("header") {
parts := strings.SplitN(h, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid header format: %q (expected key:value)", h)
}
headers[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
} }
// Build request body from fields
var body io.Reader var body io.Reader
stringFields := cmd.StringSlice("field") if request.Body != nil {
typedFields := cmd.StringSlice("Field") body = bytes.NewReader(request.Body)
if len(stringFields) > 0 || len(typedFields) > 0 {
bodyMap := make(map[string]any)
// Process string fields (-f)
for _, f := range stringFields {
parts := strings.SplitN(f, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid field format: %q (expected key=value)", f)
}
bodyMap[parts[0]] = parts[1]
}
// Process typed fields (-F)
for _, f := range typedFields {
parts := strings.SplitN(f, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid field format: %q (expected key=value)", f)
}
key := parts[0]
value := parts[1]
parsedValue, err := parseTypedValue(value)
if err != nil {
return fmt.Errorf("failed to parse field %q: %w", key, err)
}
bodyMap[key] = parsedValue
}
bodyBytes, err := json.Marshal(bodyMap)
if err != nil {
return fmt.Errorf("failed to encode request body: %w", err)
}
body = strings.NewReader(string(bodyBytes))
} }
// Create API client and make request // Create API client and make request
client := api.NewClient(ctx.Login) client := api.NewClient(ctx.Login)
method := strings.ToUpper(cmd.String("method")) resp, err := client.Do(request.Method, request.Endpoint, body, request.Headers)
resp, err := client.Do(method, endpoint, body, headers)
if err != nil { if err != nil {
return fmt.Errorf("request failed: %w", err) return fmt.Errorf("request failed: %w", err)
} }
defer resp.Body.Close() defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
// Print headers to stderr if requested (so redirects/pipes work correctly) // Print headers to stderr if requested (so redirects/pipes work correctly)
if cmd.Bool("include") { if cmd.Bool("include") {
@@ -172,7 +160,11 @@ func runApi(_ stdctx.Context, cmd *cli.Command) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to create output file: %w", err) return fmt.Errorf("failed to create output file: %w", err)
} }
defer file.Close() defer func() {
if closeErr := file.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close output file: %v\n", closeErr)
}
}()
output = file output = file
} }
@@ -190,15 +182,139 @@ func runApi(_ stdctx.Context, cmd *cli.Command) error {
return nil return nil
} }
func prepareAPIRequest(cmd *cli.Command, ctx *context.TeaContext) (*preparedAPIRequest, error) {
var err error
// Get the endpoint argument
if cmd.NArg() < 1 {
return nil, fmt.Errorf("endpoint argument required")
}
endpoint := cmd.Args().First()
// Expand placeholders in endpoint
endpoint = expandPlaceholders(endpoint, ctx)
// Parse headers
headers := make(map[string]string)
for _, h := range cmd.StringSlice("header") {
parts := strings.SplitN(h, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid header format: %q (expected key:value)", h)
}
headers[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
// Build request body from fields
var bodyBytes []byte
stringFields := cmd.StringSlice("field")
typedFields := cmd.StringSlice("Field")
dataRaw := cmd.String("data")
if dataRaw != "" && (len(stringFields) > 0 || len(typedFields) > 0) {
return nil, fmt.Errorf("--data/-d cannot be combined with --field/-f or --Field/-F")
}
if dataRaw != "" {
var dataBytes []byte
var dataSource string
if strings.HasPrefix(dataRaw, "@") {
filename := dataRaw[1:]
if filename == "-" {
dataBytes, err = io.ReadAll(os.Stdin)
dataSource = "stdin"
} else {
dataBytes, err = os.ReadFile(filename)
dataSource = filename
}
if err != nil {
return nil, fmt.Errorf("failed to read %q: %w", dataRaw, err)
}
} else {
dataBytes = []byte(dataRaw)
}
if !json.Valid(dataBytes) {
if dataSource != "" {
return nil, fmt.Errorf("--data/-d value from %s is not valid JSON", dataSource)
}
return nil, fmt.Errorf("--data/-d value is not valid JSON")
}
bodyBytes = dataBytes
} else if len(stringFields) > 0 || len(typedFields) > 0 {
bodyMap := make(map[string]any)
// Process string fields (-f)
for _, f := range stringFields {
parts := strings.SplitN(f, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid field format: %q (expected key=value)", f)
}
key := parts[0]
if key == "" {
return nil, fmt.Errorf("field key cannot be empty in %q", f)
}
if _, exists := bodyMap[key]; exists {
return nil, fmt.Errorf("duplicate field key %q", key)
}
bodyMap[key] = parts[1]
}
// Process typed fields (-F)
for _, f := range typedFields {
parts := strings.SplitN(f, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid field format: %q (expected key=value)", f)
}
key := parts[0]
if key == "" {
return nil, fmt.Errorf("field key cannot be empty in %q", f)
}
if _, exists := bodyMap[key]; exists {
return nil, fmt.Errorf("duplicate field key %q", key)
}
value := parts[1]
parsedValue, err := parseTypedValue(value)
if err != nil {
return nil, fmt.Errorf("failed to parse field %q: %w", key, err)
}
bodyMap[key] = parsedValue
}
bodyBytes, err = json.Marshal(bodyMap)
if err != nil {
return nil, fmt.Errorf("failed to encode request body: %w", err)
}
}
method := strings.ToUpper(cmd.String("method"))
if !cmd.IsSet("method") {
if bodyBytes != nil {
method = "POST"
} else {
method = "GET"
}
}
return &preparedAPIRequest{
Method: method,
Endpoint: endpoint,
Headers: headers,
Body: bodyBytes,
}, nil
}
// parseTypedValue parses a value for -F flag, handling: // parseTypedValue parses a value for -F flag, handling:
// - @filename: read content from file // - @filename: read content from file
// - @-: read content from stdin // - @-: read content from stdin
// - "quoted": literal string (prevents type parsing)
// - true/false: boolean // - true/false: boolean
// - null: nil // - null: nil
// - numbers: int or float // - numbers: int or float
// - []/{}: JSON arrays/objects
// - otherwise: string // - otherwise: string
func parseTypedValue(value string) (any, error) { func parseTypedValue(value string) (any, error) {
// Handle file references // Handle file references.
// Note: if multiple fields use @- (stdin), only the first will get data;
// subsequent reads will return empty since stdin is consumed once.
if strings.HasPrefix(value, "@") { if strings.HasPrefix(value, "@") {
filename := value[1:] filename := value[1:]
var content []byte var content []byte
@@ -215,6 +331,16 @@ func parseTypedValue(value string) (any, error) {
return strings.TrimSuffix(string(content), "\n"), nil return strings.TrimSuffix(string(content), "\n"), nil
} }
// Handle quoted strings (literal strings, no type parsing).
// Uses strconv.Unquote so escape sequences like \" are handled correctly.
if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
unquoted, err := strconv.Unquote(value)
if err != nil {
return nil, fmt.Errorf("invalid quoted string %s: %w", value, err)
}
return unquoted, nil
}
// Handle null // Handle null
if value == "null" { if value == "null" {
return nil, nil return nil, nil
@@ -238,6 +364,14 @@ func parseTypedValue(value string) (any, error) {
return f, nil return f, nil
} }
// Handle JSON arrays and objects
if len(value) > 0 && (value[0] == '[' || value[0] == '{') {
var jsonVal any
if err := json.Unmarshal([]byte(value), &jsonVal); err == nil {
return jsonVal, nil
}
}
// Default to string // Default to string
return value, nil return value, nil
} }

635
cmd/api_test.go Normal file
View File

@@ -0,0 +1,635 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
stdctx "context"
"encoding/json"
"io"
"os"
"path/filepath"
"testing"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/context"
tea_git "code.gitea.io/tea/modules/git"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
)
func TestParseTypedValue(t *testing.T) {
t.Run("null", func(t *testing.T) {
v, err := parseTypedValue("null")
require.NoError(t, err)
assert.Nil(t, v)
})
t.Run("bool true", func(t *testing.T) {
v, err := parseTypedValue("true")
require.NoError(t, err)
assert.Equal(t, true, v)
})
t.Run("bool false", func(t *testing.T) {
v, err := parseTypedValue("false")
require.NoError(t, err)
assert.Equal(t, false, v)
})
t.Run("integer", func(t *testing.T) {
v, err := parseTypedValue("42")
require.NoError(t, err)
assert.Equal(t, int64(42), v)
})
t.Run("float", func(t *testing.T) {
v, err := parseTypedValue("3.14")
require.NoError(t, err)
assert.Equal(t, 3.14, v)
})
t.Run("string", func(t *testing.T) {
v, err := parseTypedValue("hello")
require.NoError(t, err)
assert.Equal(t, "hello", v)
})
t.Run("JSON array", func(t *testing.T) {
v, err := parseTypedValue("[1,2,3]")
require.NoError(t, err)
assert.Equal(t, []any{float64(1), float64(2), float64(3)}, v)
})
t.Run("JSON object", func(t *testing.T) {
v, err := parseTypedValue(`{"key":"val"}`)
require.NoError(t, err)
assert.Equal(t, map[string]any{"key": "val"}, v)
})
t.Run("invalid JSON array falls back to string", func(t *testing.T) {
v, err := parseTypedValue("[not json")
require.NoError(t, err)
assert.Equal(t, "[not json", v)
})
t.Run("invalid JSON object falls back to string", func(t *testing.T) {
v, err := parseTypedValue("{not json")
require.NoError(t, err)
assert.Equal(t, "{not json", v)
})
t.Run("file reference", func(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "test.txt")
require.NoError(t, os.WriteFile(tmpFile, []byte("file content\n"), 0o644))
v, err := parseTypedValue("@" + tmpFile)
require.NoError(t, err)
assert.Equal(t, "file content", v)
})
t.Run("file reference without trailing newline", func(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "test.txt")
require.NoError(t, os.WriteFile(tmpFile, []byte("no newline"), 0o644))
v, err := parseTypedValue("@" + tmpFile)
require.NoError(t, err)
assert.Equal(t, "no newline", v)
})
t.Run("empty file reference", func(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "empty.txt")
require.NoError(t, os.WriteFile(tmpFile, []byte(""), 0o644))
v, err := parseTypedValue("@" + tmpFile)
require.NoError(t, err)
assert.Equal(t, "", v)
})
t.Run("nonexistent file reference", func(t *testing.T) {
_, err := parseTypedValue("@/nonexistent/file.txt")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to read")
})
t.Run("negative integer", func(t *testing.T) {
v, err := parseTypedValue("-42")
require.NoError(t, err)
assert.Equal(t, int64(-42), v)
})
t.Run("negative float", func(t *testing.T) {
v, err := parseTypedValue("-3.14")
require.NoError(t, err)
assert.Equal(t, -3.14, v)
})
t.Run("scientific notation", func(t *testing.T) {
v, err := parseTypedValue("1.5e10")
require.NoError(t, err)
assert.Equal(t, 1.5e10, v)
})
t.Run("empty string", func(t *testing.T) {
v, err := parseTypedValue("")
require.NoError(t, err)
assert.Equal(t, "", v)
})
t.Run("string starting with number", func(t *testing.T) {
v, err := parseTypedValue("123abc")
require.NoError(t, err)
assert.Equal(t, "123abc", v)
})
t.Run("nested JSON object", func(t *testing.T) {
v, err := parseTypedValue(`{"user":{"name":"alice","id":1}}`)
require.NoError(t, err)
expected := map[string]any{
"user": map[string]any{
"name": "alice",
"id": float64(1),
},
}
assert.Equal(t, expected, v)
})
t.Run("complex JSON array", func(t *testing.T) {
v, err := parseTypedValue(`[{"id":1},{"id":2}]`)
require.NoError(t, err)
expected := []any{
map[string]any{"id": float64(1)},
map[string]any{"id": float64(2)},
}
assert.Equal(t, expected, v)
})
t.Run("quoted string prevents type parsing", func(t *testing.T) {
v, err := parseTypedValue(`"null"`)
require.NoError(t, err)
assert.Equal(t, "null", v)
})
t.Run("quoted true becomes string", func(t *testing.T) {
v, err := parseTypedValue(`"true"`)
require.NoError(t, err)
assert.Equal(t, "true", v)
})
t.Run("quoted false becomes string", func(t *testing.T) {
v, err := parseTypedValue(`"false"`)
require.NoError(t, err)
assert.Equal(t, "false", v)
})
t.Run("quoted number becomes string", func(t *testing.T) {
v, err := parseTypedValue(`"123"`)
require.NoError(t, err)
assert.Equal(t, "123", v)
})
t.Run("quoted empty string", func(t *testing.T) {
v, err := parseTypedValue(`""`)
require.NoError(t, err)
assert.Equal(t, "", v)
})
t.Run("quoted string with spaces", func(t *testing.T) {
v, err := parseTypedValue(`"hello world"`)
require.NoError(t, err)
assert.Equal(t, "hello world", v)
})
t.Run("single quote not treated as quote", func(t *testing.T) {
v, err := parseTypedValue(`'hello'`)
require.NoError(t, err)
assert.Equal(t, "'hello'", v)
})
t.Run("unmatched quote at start only", func(t *testing.T) {
v, err := parseTypedValue(`"hello`)
require.NoError(t, err)
assert.Equal(t, `"hello`, v)
})
t.Run("unmatched quote at end only", func(t *testing.T) {
v, err := parseTypedValue(`hello"`)
require.NoError(t, err)
assert.Equal(t, `hello"`, v)
})
t.Run("quoted string with escaped quote", func(t *testing.T) {
v, err := parseTypedValue(`"hello \"world\""`)
require.NoError(t, err)
assert.Equal(t, `hello "world"`, v)
})
t.Run("quoted string with backslash-n", func(t *testing.T) {
v, err := parseTypedValue(`"line1\nline2"`)
require.NoError(t, err)
assert.Equal(t, "line1\nline2", v)
})
t.Run("quoted string with tab escape", func(t *testing.T) {
v, err := parseTypedValue(`"col1\tcol2"`)
require.NoError(t, err)
assert.Equal(t, "col1\tcol2", v)
})
t.Run("quoted string with backslash", func(t *testing.T) {
v, err := parseTypedValue(`"path\\to\\file"`)
require.NoError(t, err)
assert.Equal(t, `path\to\file`, v)
})
t.Run("invalid escape sequence in quoted string", func(t *testing.T) {
_, err := parseTypedValue(`"bad \z escape"`)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid quoted string")
})
}
// runApiWithArgs configures a test login, parses the command line, and captures
// the prepared request without opening sockets or making HTTP requests.
func runApiWithArgs(t *testing.T, args []string) (method string, body []byte, err error) {
t.Helper()
var capturedMethod string
var capturedBody []byte
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "testLogin",
URL: "https://gitea.example.com",
Token: "test-token",
User: "testUser",
Default: true,
}},
})
// Use the apiFlags factory to get fresh flag instances, avoiding shared
// hasBeenSet state between tests. Append minimal login/repo flags needed
// for the test harness.
cmd := cli.Command{
Name: "api",
DisableSliceFlagSeparator: true,
Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
request, err := prepareAPIRequest(cmd, ctx)
if err != nil {
return err
}
capturedMethod = request.Method
capturedBody = append([]byte(nil), request.Body...)
return nil
},
Flags: append(apiFlags(), []cli.Flag{
&cli.StringFlag{Name: "login", Aliases: []string{"l"}},
&cli.StringFlag{Name: "repo", Aliases: []string{"r"}},
&cli.StringFlag{Name: "remote", Aliases: []string{"R"}},
}...),
Writer: io.Discard,
ErrWriter: io.Discard,
}
fullArgs := append([]string{"api", "--login", "testLogin"}, args...)
runErr := cmd.Run(stdctx.Background(), fullArgs)
return capturedMethod, capturedBody, runErr
}
func TestApiCommaInFieldValue(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{"-f", "body=hello, world", "-X", "POST", "/test"})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "hello, world", parsed["body"])
}
func TestApiRawDataFlag(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{"-d", `{"title":"test","body":"hello"}`, "/test"})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "test", parsed["title"])
assert.Equal(t, "hello", parsed["body"])
}
func TestApiDataFieldMutualExclusion(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "-f", "key=val", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "--data/-d cannot be combined with --field/-f or --Field/-F")
}
func TestApiMethodAutoDefault(t *testing.T) {
t.Run("POST when body provided without explicit method", func(t *testing.T) {
method, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "/test"})
require.NoError(t, err)
assert.Equal(t, "POST", method)
})
t.Run("explicit method overrides auto-POST", func(t *testing.T) {
method, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "-X", "PATCH", "/test"})
require.NoError(t, err)
assert.Equal(t, "PATCH", method)
})
t.Run("GET when no body", func(t *testing.T) {
method, _, err := runApiWithArgs(t, []string{"/test"})
require.NoError(t, err)
assert.Equal(t, "GET", method)
})
}
func TestApiMultipleFields(t *testing.T) {
t.Run("multiple -f flags", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-f", "title=Test Issue",
"-f", "body=Description here",
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "Test Issue", parsed["title"])
assert.Equal(t, "Description here", parsed["body"])
})
t.Run("multiple -F flags with different types", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-F", "milestone=5",
"-F", "closed=true",
"-F", "title=Test",
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, float64(5), parsed["milestone"])
assert.Equal(t, true, parsed["closed"])
assert.Equal(t, "Test", parsed["title"])
})
t.Run("combining -f and -F flags", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-f", "title=Test",
"-F", "milestone=3",
"-F", "closed=false",
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "Test", parsed["title"])
assert.Equal(t, float64(3), parsed["milestone"])
assert.Equal(t, false, parsed["closed"])
})
t.Run("-F with JSON array", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-F", `labels=["bug","enhancement"]`,
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, []any{"bug", "enhancement"}, parsed["labels"])
})
t.Run("-F with JSON object", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-F", `assignee={"login":"alice","id":123}`,
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assignee, ok := parsed["assignee"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "alice", assignee["login"])
assert.Equal(t, float64(123), assignee["id"])
})
t.Run("-F with quoted string to prevent type parsing", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-F", `status="null"`,
"-F", `enabled="true"`,
"-F", `count="42"`,
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "null", parsed["status"])
assert.Equal(t, "true", parsed["enabled"])
assert.Equal(t, "42", parsed["count"])
})
}
func TestApiDataFromFile(t *testing.T) {
t.Run("read JSON from file", func(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "data.json")
jsonData := `{"title":"From File","body":"File content"}`
require.NoError(t, os.WriteFile(tmpFile, []byte(jsonData), 0o644))
_, body, err := runApiWithArgs(t, []string{"-d", "@" + tmpFile, "/test"})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "From File", parsed["title"])
assert.Equal(t, "File content", parsed["body"])
})
t.Run("invalid JSON in --data flag", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-d", `{invalid json}`, "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "not valid JSON")
})
t.Run("invalid JSON from file includes filename", func(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "bad.json")
require.NoError(t, os.WriteFile(tmpFile, []byte("not json"), 0o644))
_, _, err := runApiWithArgs(t, []string{"-d", "@" + tmpFile, "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "not valid JSON")
assert.Contains(t, err.Error(), "bad.json")
})
}
func TestApiErrorHandling(t *testing.T) {
t.Run("missing endpoint argument", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{})
require.Error(t, err)
assert.Contains(t, err.Error(), "endpoint argument required")
})
t.Run("invalid field format", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-f", "invalidformat", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid field format")
})
t.Run("invalid Field format", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-F", "noequalsign", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid field format")
})
t.Run("empty field key with -f", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-f", "=value", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "field key cannot be empty")
})
t.Run("empty field key with -F", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-F", "=123", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "field key cannot be empty")
})
t.Run("duplicate field key in -f flags", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-f", "key=first", "-f", "key=second", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "duplicate field key")
})
t.Run("duplicate field key in -F flags", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-F", "key=1", "-F", "key=2", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "duplicate field key")
})
t.Run("duplicate field key across -f and -F flags", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-f", "key=string", "-F", "key=123", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "duplicate field key")
})
}
func TestExpandPlaceholders(t *testing.T) {
t.Run("replaces owner and repo", func(t *testing.T) {
ctx := &context.TeaContext{
Owner: "myorg",
Repo: "myrepo",
}
result := expandPlaceholders("/repos/{owner}/{repo}/issues", ctx)
assert.Equal(t, "/repos/myorg/myrepo/issues", result)
})
t.Run("replaces multiple occurrences", func(t *testing.T) {
ctx := &context.TeaContext{
Owner: "alice",
Repo: "proj",
}
result := expandPlaceholders("/repos/{owner}/{repo}/branches?owner={owner}", ctx)
assert.Equal(t, "/repos/alice/proj/branches?owner=alice", result)
})
t.Run("no placeholders returns unchanged", func(t *testing.T) {
ctx := &context.TeaContext{
Owner: "alice",
Repo: "proj",
}
result := expandPlaceholders("/api/v1/version", ctx)
assert.Equal(t, "/api/v1/version", result)
})
t.Run("empty owner and repo produce empty replacements", func(t *testing.T) {
ctx := &context.TeaContext{}
result := expandPlaceholders("/repos/{owner}/{repo}", ctx)
assert.Equal(t, "/repos//", result)
})
t.Run("branch left unreplaced when no local repo", func(t *testing.T) {
ctx := &context.TeaContext{
Owner: "alice",
Repo: "proj",
}
result := expandPlaceholders("/repos/{owner}/{repo}/branches/{branch}", ctx)
assert.Equal(t, "/repos/alice/proj/branches/{branch}", result)
})
t.Run("replaces branch from local repo HEAD", func(t *testing.T) {
tmpDir := t.TempDir()
repo, err := gogit.PlainInit(tmpDir, false)
require.NoError(t, err)
// Create an initial commit so HEAD points to a branch.
wt, err := repo.Worktree()
require.NoError(t, err)
tmpFile := filepath.Join(tmpDir, "init.txt")
require.NoError(t, os.WriteFile(tmpFile, []byte("init"), 0o644))
_, err = wt.Add("init.txt")
require.NoError(t, err)
_, err = wt.Commit("initial commit", &gogit.CommitOptions{
Author: &object.Signature{Name: "test", Email: "test@test.com"},
})
require.NoError(t, err)
// Create and checkout a feature branch.
headRef, err := repo.Head()
require.NoError(t, err)
branchRef := plumbing.NewBranchReferenceName("feature/my-branch")
ref := plumbing.NewHashReference(branchRef, headRef.Hash())
require.NoError(t, repo.Storer.SetReference(ref))
require.NoError(t, wt.Checkout(&gogit.CheckoutOptions{Branch: branchRef}))
ctx := &context.TeaContext{
Owner: "alice",
Repo: "proj",
LocalRepo: &tea_git.TeaRepo{Repository: repo},
}
result := expandPlaceholders("/repos/{owner}/{repo}/branches/{branch}", ctx)
assert.Equal(t, "/repos/alice/proj/branches/feature/my-branch", result)
})
}
func TestIsTextContentType(t *testing.T) {
tests := []struct {
name string
contentType string
want bool
}{
{"empty string defaults to text", "", true},
{"plain text", "text/plain", true},
{"html", "text/html", true},
{"json", "application/json", true},
{"json with charset", "application/json; charset=utf-8", true},
{"xml", "application/xml", true},
{"javascript", "application/javascript", true},
{"yaml", "application/yaml", true},
{"toml", "application/toml", true},
{"binary", "application/octet-stream", false},
{"image", "image/png", false},
{"pdf", "application/pdf", false},
{"zip", "application/zip", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isTextContentType(tt.contentType)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -27,8 +27,13 @@ var CmdReleaseAttachmentCreate = cli.Command{
} }
func runReleaseAttachmentCreate(_ stdctx.Context, cmd *cli.Command) error { func runReleaseAttachmentCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
if ctx.Args().Len() < 2 { if ctx.Args().Len() < 2 {

View File

@@ -32,8 +32,13 @@ var CmdReleaseAttachmentDelete = cli.Command{
} }
func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error { func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
if ctx.Args().Len() < 2 { if ctx.Args().Len() < 2 {

View File

@@ -31,8 +31,13 @@ var CmdReleaseAttachmentList = cli.Command{
// RunReleaseAttachmentList list release attachments // RunReleaseAttachmentList list release attachments
func RunReleaseAttachmentList(_ stdctx.Context, cmd *cli.Command) error { func RunReleaseAttachmentList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
tag := ctx.Args().First() tag := ctx.Args().First()
@@ -46,14 +51,13 @@ 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: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err != nil { if err != nil {
return err return err
} }
print.ReleaseAttachmentsList(attachments, ctx.Output) return print.ReleaseAttachmentsList(attachments, ctx.Output)
return nil
} }
func getReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) { func getReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {

View File

@@ -38,8 +38,13 @@ var CmdBranchesList = cli.Command{
// RunBranchesList list branches // RunBranchesList list branches
func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error { func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
owner := ctx.Owner owner := ctx.Owner
if ctx.IsSet("owner") { if ctx.IsSet("owner") {
@@ -48,16 +53,15 @@ func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error {
var branches []*gitea.Branch var branches []*gitea.Branch
var protections []*gitea.BranchProtection var protections []*gitea.BranchProtection
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: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err != nil { if err != nil {
return err return err
} }
protections, _, err = ctx.Login.Client().ListBranchProtections(owner, ctx.Repo, gitea.ListBranchProtectionsOptions{ protections, _, err = ctx.Login.Client().ListBranchProtections(owner, ctx.Repo, gitea.ListBranchProtectionsOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err != nil { if err != nil {
return err return err
@@ -68,6 +72,5 @@ func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error {
return err return err
} }
print.BranchesList(branches, protections, ctx.Output, fields) return print.BranchesList(branches, protections, ctx.Output, fields)
return nil
} }

View File

@@ -45,8 +45,13 @@ var CmdBranchesUnprotect = cli.Command{
// RunBranchesProtect function to protect/unprotect a list of branches // RunBranchesProtect function to protect/unprotect a list of branches
func RunBranchesProtect(_ stdctx.Context, cmd *cli.Command) error { func RunBranchesProtect(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if !cmd.Args().Present() { if !cmd.Args().Present() {
return fmt.Errorf("must specify at least one branch") return fmt.Errorf("must specify at least one branch")

View File

@@ -48,7 +48,10 @@ When a host is specified in the repo-slug, it will override the login specified
} }
func runRepoClone(ctx stdctx.Context, cmd *cli.Command) error { func runRepoClone(ctx stdctx.Context, cmd *cli.Command) error {
teaCmd := context.InitCommand(cmd) teaCmd, err := context.InitCommand(cmd)
if err != nil {
return err
}
args := teaCmd.Args() args := teaCmd.Args()
if args.Len() < 1 { if args.Len() < 1 {

View File

@@ -10,6 +10,7 @@ import (
"io" "io"
"strings" "strings"
"code.gitea.io/sdk/gitea"
"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"
@@ -18,8 +19,7 @@ import (
"code.gitea.io/tea/modules/theme" "code.gitea.io/tea/modules/theme"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea" "charm.land/huh/v2"
"github.com/charmbracelet/huh"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@@ -36,8 +36,13 @@ var CmdAddComment = cli.Command{
} }
func runAddComment(_ stdctx.Context, cmd *cli.Command) error { func runAddComment(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
args := ctx.Args() args := ctx.Args()
if args.Len() == 0 { if args.Len() == 0 {

93
cmd/detail_json.go Normal file
View File

@@ -0,0 +1,93 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"encoding/json"
"io"
"time"
"code.gitea.io/sdk/gitea"
)
type detailLabelData struct {
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
}
type detailCommentData struct {
ID int64 `json:"id"`
Author string `json:"author"`
Created time.Time `json:"created"`
Body string `json:"body"`
}
type detailReviewData struct {
ID int64 `json:"id"`
Reviewer string `json:"reviewer"`
State gitea.ReviewStateType `json:"state"`
Body string `json:"body"`
Created time.Time `json:"created"`
}
func buildDetailLabels(labels []*gitea.Label) []detailLabelData {
labelSlice := make([]detailLabelData, 0, len(labels))
for _, label := range labels {
labelSlice = append(labelSlice, detailLabelData{
Name: label.Name,
Color: label.Color,
Description: label.Description,
})
}
return labelSlice
}
func buildDetailAssignees(assignees []*gitea.User) []string {
assigneeSlice := make([]string, 0, len(assignees))
for _, assignee := range assignees {
assigneeSlice = append(assigneeSlice, username(assignee))
}
return assigneeSlice
}
func buildDetailComments(comments []*gitea.Comment) []detailCommentData {
commentSlice := make([]detailCommentData, 0, len(comments))
for _, comment := range comments {
commentSlice = append(commentSlice, detailCommentData{
ID: comment.ID,
Author: username(comment.Poster),
Body: comment.Body,
Created: comment.Created,
})
}
return commentSlice
}
func buildDetailReviews(reviews []*gitea.PullReview) []detailReviewData {
reviewSlice := make([]detailReviewData, 0, len(reviews))
for _, review := range reviews {
reviewSlice = append(reviewSlice, detailReviewData{
ID: review.ID,
Reviewer: username(review.Reviewer),
State: review.State,
Body: review.Body,
Created: review.Submitted,
})
}
return reviewSlice
}
func username(user *gitea.User) string {
if user == nil {
return "ghost"
}
return user.UserName
}
func writeIndentedJSON(w io.Writer, data any) error {
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
return encoder.Encode(data)
}

View File

@@ -39,16 +39,33 @@ var OutputFlag = cli.StringFlag{
} }
var ( var (
paging gitea.ListOptions
// ErrPage indicates that the provided page value is invalid (less than -1 or equal to 0). // 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") ErrPage = errors.New("page cannot be smaller than 1")
// ErrLimit indicates that the provided limit value is invalid (negative). // ErrLimit indicates that the provided limit value is invalid (negative).
ErrLimit = errors.New("limit cannot be negative") ErrLimit = errors.New("limit cannot be negative")
) )
// GetListOptions returns configured paging struct const (
func GetListOptions() gitea.ListOptions { defaultPageValue = 1
return paging defaultLimitValue = 30
)
// GetListOptions returns list options derived from the active command.
func GetListOptions(cmd *cli.Command) gitea.ListOptions {
page := cmd.Int("page")
if page == 0 {
page = defaultPageValue
}
pageSize := cmd.Int("limit")
if pageSize == 0 {
pageSize = defaultLimitValue
}
return gitea.ListOptions{
Page: page,
PageSize: pageSize,
}
} }
// PaginationFlags provides all pagination related flags // PaginationFlags provides all pagination related flags
@@ -62,14 +79,13 @@ var PaginationPageFlag = cli.IntFlag{
Name: "page", Name: "page",
Aliases: []string{"p"}, Aliases: []string{"p"},
Usage: "specify page", Usage: "specify page",
Value: 1, Value: defaultPageValue,
Validator: func(i int) error { Validator: func(i int) error {
if i < 1 && i != -1 { if i < 1 && i != -1 {
return ErrPage return ErrPage
} }
return nil return nil
}, },
Destination: &paging.Page,
} }
// PaginationLimitFlag provides flag for pagination options // PaginationLimitFlag provides flag for pagination options
@@ -77,14 +93,13 @@ 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, Value: defaultLimitValue,
Validator: func(i int) error { Validator: func(i int) error {
if i < 0 { if i < 0 {
return ErrLimit return ErrLimit
} }
return nil return nil
}, },
Destination: &paging.PageSize,
} }
// LoginOutputFlags defines login and output flags that should // LoginOutputFlags defines login and output flags that should

View File

@@ -8,6 +8,7 @@ import (
"io" "io"
"testing" "testing"
"code.gitea.io/sdk/gitea"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
@@ -123,3 +124,29 @@ func TestPaginationFailures(t *testing.T) {
}) })
} }
} }
func TestGetListOptionsDoesNotLeakBetweenCommands(t *testing.T) {
var results []gitea.ListOptions
run := func(args []string) {
t.Helper()
cmd := cli.Command{
Name: "test-paging",
Action: func(_ context.Context, cmd *cli.Command) error {
results = append(results, GetListOptions(cmd))
return nil
},
Flags: PaginationFlags,
}
require.NoError(t, cmd.Run(context.Background(), args))
}
run([]string{"test", "--page", "5", "--limit", "10"})
run([]string{"test"})
require.Len(t, results, 2)
assert.Equal(t, gitea.ListOptions{Page: 5, PageSize: 10}, results[0])
assert.Equal(t, gitea.ListOptions{Page: defaultPageValue, PageSize: defaultLimitValue}, results[1])
}

View File

@@ -5,7 +5,6 @@ package cmd
import ( import (
stdctx "context" stdctx "context"
"encoding/json"
"fmt" "fmt"
"time" "time"
@@ -20,11 +19,7 @@ import (
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
type labelData struct { type labelData = detailLabelData
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
}
type issueData struct { type issueData struct {
ID int64 `json:"id"` ID int64 `json:"id"`
@@ -41,13 +36,17 @@ type issueData struct {
Comments []commentData `json:"comments"` Comments []commentData `json:"comments"`
} }
type commentData struct { type issueDetailClient interface {
ID int64 `json:"id"` GetIssue(owner, repo string, index int64) (*gitea.Issue, *gitea.Response, error)
Author string `json:"author"` GetIssueReactions(owner, repo string, index int64) ([]*gitea.Reaction, *gitea.Response, error)
Created time.Time `json:"created"`
Body string `json:"body"`
} }
type issueCommentClient interface {
ListIssueComments(owner, repo string, index int64, opt gitea.ListIssueCommentOptions) ([]*gitea.Comment, *gitea.Response, error)
}
type commentData = detailCommentData
// CmdIssues represents to login a gitea server. // CmdIssues represents to login a gitea server.
var CmdIssues = cli.Command{ var CmdIssues = cli.Command{
Name: "issues", Name: "issues",
@@ -80,17 +79,35 @@ func runIssues(ctx stdctx.Context, cmd *cli.Command) error {
} }
func runIssueDetail(_ stdctx.Context, cmd *cli.Command, index string) error { func runIssueDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
ctx := context.InitCommand(cmd) ctx, idx, err := resolveIssueDetailContext(cmd, index)
if ctx.IsSet("owner") {
ctx.Owner = ctx.String("owner")
}
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
idx, err := utils.ArgToIndex(index)
if err != nil { if err != nil {
return err return err
} }
client := ctx.Login.Client()
return runIssueDetailWithClient(ctx, idx, ctx.Login.Client())
}
func resolveIssueDetailContext(cmd *cli.Command, index string) (*context.TeaContext, int64, error) {
ctx, err := context.InitCommand(cmd)
if err != nil {
return nil, 0, err
}
if ctx.IsSet("owner") {
ctx.Owner = ctx.String("owner")
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return nil, 0, err
}
idx, err := utils.ArgToIndex(index)
if err != nil {
return nil, 0, err
}
return ctx, idx, nil
}
func runIssueDetailWithClient(ctx *context.TeaContext, idx int64, client issueDetailClient) error {
issue, _, err := client.GetIssue(ctx.Owner, ctx.Repo, idx) issue, _, err := client.GetIssue(ctx.Owner, ctx.Repo, idx)
if err != nil { if err != nil {
return err return err
@@ -120,59 +137,37 @@ func runIssueDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
} }
func runIssueDetailAsJSON(ctx *context.TeaContext, issue *gitea.Issue) error { func runIssueDetailAsJSON(ctx *context.TeaContext, issue *gitea.Issue) error {
c := ctx.Login.Client() return runIssueDetailAsJSONWithClient(ctx, issue, ctx.Login.Client())
opts := gitea.ListIssueCommentOptions{ListOptions: flags.GetListOptions()} }
labelSlice := make([]labelData, 0, len(issue.Labels)) func runIssueDetailAsJSONWithClient(ctx *context.TeaContext, issue *gitea.Issue, c issueCommentClient) error {
for _, label := range issue.Labels { opts := gitea.ListIssueCommentOptions{ListOptions: flags.GetListOptions(ctx.Command)}
labelSlice = append(labelSlice, labelData{label.Name, label.Color, label.Description}) comments := []*gitea.Comment{}
if ctx.Bool("comments") {
var err error
comments, _, err = c.ListIssueComments(ctx.Owner, ctx.Repo, issue.Index, opts)
if err != nil {
return err
}
} }
assigneesSlice := make([]string, 0, len(issue.Assignees)) return writeIndentedJSON(ctx.Writer, buildIssueData(issue, comments))
for _, assignee := range issue.Assignees { }
assigneesSlice = append(assigneesSlice, assignee.UserName)
}
issueSlice := issueData{ func buildIssueData(issue *gitea.Issue, comments []*gitea.Comment) issueData {
return issueData{
ID: issue.ID, ID: issue.ID,
Index: issue.Index, Index: issue.Index,
Title: issue.Title, Title: issue.Title,
State: issue.State, State: issue.State,
Created: issue.Created, Created: issue.Created,
User: issue.Poster.UserName, User: username(issue.Poster),
Body: issue.Body, Body: issue.Body,
Labels: labelSlice, Labels: buildDetailLabels(issue.Labels),
Assignees: assigneesSlice, Assignees: buildDetailAssignees(issue.Assignees),
URL: issue.HTMLURL, URL: issue.HTMLURL,
ClosedAt: issue.Closed, ClosedAt: issue.Closed,
Comments: make([]commentData, 0), Comments: buildDetailComments(comments),
} }
if ctx.Bool("comments") {
comments, _, err := c.ListIssueComments(ctx.Owner, ctx.Repo, issue.Index, opts)
issueSlice.Comments = make([]commentData, 0, len(comments))
if err != nil {
return err
}
for _, comment := range comments {
issueSlice.Comments = append(issueSlice.Comments, commentData{
ID: comment.ID,
Author: comment.Poster.UserName,
Body: comment.Body, // Selected Field
Created: comment.Created,
})
}
}
jsonData, err := json.MarshalIndent(issueSlice, "", "\t")
if err != nil {
return err
}
_, err = fmt.Fprintf(ctx.Writer, "%s\n", jsonData)
return err
} }

View File

@@ -31,8 +31,13 @@ var CmdIssuesClose = cli.Command{
// editIssueState abstracts the arg parsing to edit the given issue // editIssueState abstracts the arg parsing to edit the given issue
func editIssueState(_ stdctx.Context, cmd *cli.Command, opts gitea.EditIssueOption) error { func editIssueState(_ stdctx.Context, cmd *cli.Command, opts gitea.EditIssueOption) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
return fmt.Errorf("missing required argument: %s", ctx.Command.ArgsUsage) return fmt.Errorf("missing required argument: %s", ctx.Command.ArgsUsage)
} }

View File

@@ -26,8 +26,13 @@ var CmdIssuesCreate = cli.Command{
} }
func runIssuesCreate(_ stdctx.Context, cmd *cli.Command) error { func runIssuesCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.IsInteractiveMode() { if ctx.IsInteractiveMode() {
err := interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo) err := interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo)

View File

@@ -30,8 +30,13 @@ use an empty string (eg. --milestone "").`,
} }
func runIssuesEdit(_ stdctx.Context, cmd *cli.Command) error { func runIssuesEdit(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if !cmd.Args().Present() { if !cmd.Args().Present() {
return fmt.Errorf("must specify at least one issue index") return fmt.Errorf("must specify at least one issue index")

View File

@@ -33,7 +33,10 @@ var CmdIssuesList = cli.Command{
// RunIssuesList list issues // RunIssuesList list issues
func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error { func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
state, err := flags.ParseState(ctx.String("state")) state, err := flags.ParseState(ctx.String("state"))
if err != nil { if err != nil {
@@ -69,7 +72,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: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
State: state, State: state,
Type: kind, Type: kind,
KeyWord: ctx.String("keyword"), KeyWord: ctx.String("keyword"),
@@ -86,7 +89,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: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
State: state, State: state,
Type: kind, Type: kind,
KeyWord: ctx.String("keyword"), KeyWord: ctx.String("keyword"),
@@ -109,6 +112,5 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
return err return err
} }
print.IssuesPullsList(issues, ctx.Output, fields) return print.IssuesPullsList(issues, ctx.Output, fields)
return nil
} }

View File

@@ -5,11 +5,8 @@ package cmd
import ( import (
"bytes" "bytes"
stdctx "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http"
"net/http/httptest"
"testing" "testing"
"time" "time"
@@ -27,6 +24,51 @@ const (
testRepo = "testRepo" testRepo = "testRepo"
) )
type fakeIssueCommentClient struct {
owner string
repo string
index int64
comments []*gitea.Comment
}
func (f *fakeIssueCommentClient) ListIssueComments(owner, repo string, index int64, _ gitea.ListIssueCommentOptions) ([]*gitea.Comment, *gitea.Response, error) {
f.owner = owner
f.repo = repo
f.index = index
return f.comments, nil, nil
}
type fakeIssueDetailClient struct {
owner string
repo string
index int64
issue *gitea.Issue
reactions []*gitea.Reaction
}
func (f *fakeIssueDetailClient) GetIssue(owner, repo string, index int64) (*gitea.Issue, *gitea.Response, error) {
f.owner = owner
f.repo = repo
f.index = index
return f.issue, nil, nil
}
func (f *fakeIssueDetailClient) GetIssueReactions(owner, repo string, index int64) ([]*gitea.Reaction, *gitea.Response, error) {
f.owner = owner
f.repo = repo
f.index = index
return f.reactions, nil, nil
}
func toCommentPointers(comments []gitea.Comment) []*gitea.Comment {
result := make([]*gitea.Comment, 0, len(comments))
for i := range comments {
comment := comments[i]
result = append(result, &comment)
}
return result
}
func createTestIssue(comments int, isClosed bool) gitea.Issue { func createTestIssue(comments int, isClosed bool) gitea.Issue {
issue := gitea.Issue{ issue := gitea.Issue{
ID: 42, ID: 42,
@@ -160,25 +202,11 @@ func TestRunIssueDetailAsJSON(t *testing.T) {
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { client := &fakeIssueCommentClient{
path := r.URL.Path comments: toCommentPointers(testCase.comments),
if path == fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", testOwner, testRepo, testCase.issue.Index) {
jsonComments, err := json.Marshal(testCase.comments)
if err != nil {
require.NoError(t, err, "Testing setup failed: failed to marshal comments")
} }
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err = w.Write(jsonComments)
require.NoError(t, err, "Testing setup failed: failed to write out comments")
} else {
http.NotFound(w, r)
}
})
server := httptest.NewServer(handler) testContext.Login.URL = "https://gitea.example.com"
testContext.Login.URL = server.URL
testCase.issue.HTMLURL = fmt.Sprintf("%s/%s/%s/issues/%d/", testContext.Login.URL, testOwner, testRepo, testCase.issue.Index) testCase.issue.HTMLURL = fmt.Sprintf("%s/%s/%s/issues/%d/", testContext.Login.URL, testOwner, testRepo, testCase.issue.Index)
var outBuffer bytes.Buffer var outBuffer bytes.Buffer
@@ -187,16 +215,19 @@ func TestRunIssueDetailAsJSON(t *testing.T) {
testContext.ErrWriter = &errBuffer testContext.ErrWriter = &errBuffer
if testCase.flagComments { if testCase.flagComments {
_ = testContext.Command.Set("comments", "true") require.NoError(t, testContext.Set("comments", "true"))
} else { } else {
_ = testContext.Command.Set("comments", "false") require.NoError(t, testContext.Set("comments", "false"))
} }
err := runIssueDetailAsJSON(&testContext, &testCase.issue) err := runIssueDetailAsJSONWithClient(&testContext, &testCase.issue, client)
server.Close()
require.NoError(t, err, "Failed to run issue detail as JSON") require.NoError(t, err, "Failed to run issue detail as JSON")
if testCase.flagComments {
assert.Equal(t, testOwner, client.owner)
assert.Equal(t, testRepo, client.repo)
assert.Equal(t, testCase.issue.Index, client.index)
}
out := outBuffer.String() out := outBuffer.String()
@@ -269,7 +300,7 @@ func TestRunIssueDetailUsesOwnerFlag(t *testing.T) {
issueIndex := int64(12) issueIndex := int64(12)
expectedOwner := "overrideOwner" expectedOwner := "overrideOwner"
expectedRepo := "overrideRepo" expectedRepo := "overrideRepo"
issue := gitea.Issue{ issue := &gitea.Issue{
ID: 99, ID: 99,
Index: issueIndex, Index: issueIndex,
Title: "Owner override test", Title: "Owner override test",
@@ -281,34 +312,10 @@ func TestRunIssueDetailUsesOwnerFlag(t *testing.T) {
HTMLURL: "https://example.test/issues/12", HTMLURL: "https://example.test/issues/12",
} }
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", expectedOwner, expectedRepo, issueIndex):
jsonIssue, err := json.Marshal(issue)
require.NoError(t, err, "Testing setup failed: failed to marshal issue")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err = w.Write(jsonIssue)
require.NoError(t, err, "Testing setup failed: failed to write issue")
case fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions", expectedOwner, expectedRepo, issueIndex):
jsonReactions, err := json.Marshal([]gitea.Reaction{})
require.NoError(t, err, "Testing setup failed: failed to marshal reactions")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err = w.Write(jsonReactions)
require.NoError(t, err, "Testing setup failed: failed to write reactions")
default:
http.NotFound(w, r)
}
})
server := httptest.NewServer(handler)
defer server.Close()
config.SetConfigForTesting(config.LocalConfig{ config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{ Logins: []config.Login{{
Name: "testLogin", Name: "testLogin",
URL: server.URL, URL: "https://gitea.example.com",
Token: "token", Token: "token",
User: "loginUser", User: "loginUser",
Default: true, Default: true,
@@ -333,9 +340,19 @@ func TestRunIssueDetailUsesOwnerFlag(t *testing.T) {
require.NoError(t, cmd.Set("login", "testLogin")) require.NoError(t, cmd.Set("login", "testLogin"))
require.NoError(t, cmd.Set("repo", expectedRepo)) require.NoError(t, cmd.Set("repo", expectedRepo))
require.NoError(t, cmd.Set("owner", expectedOwner)) require.NoError(t, cmd.Set("owner", expectedOwner))
require.NoError(t, cmd.Set("output", "json"))
require.NoError(t, cmd.Set("comments", "false")) require.NoError(t, cmd.Set("comments", "false"))
err := runIssueDetail(stdctx.Background(), &cmd, fmt.Sprintf("%d", issueIndex)) teaCtx, idx, err := resolveIssueDetailContext(&cmd, fmt.Sprintf("%d", issueIndex))
require.NoError(t, err)
client := &fakeIssueDetailClient{
issue: issue,
reactions: []*gitea.Reaction{},
}
err = runIssueDetailWithClient(teaCtx, idx, client)
require.NoError(t, err, "Expected runIssueDetail to succeed") require.NoError(t, err, "Expected runIssueDetail to succeed")
assert.Equal(t, expectedOwner, client.owner)
assert.Equal(t, expectedRepo, client.repo)
assert.Equal(t, issueIndex, client.index)
} }

View File

@@ -46,8 +46,13 @@ var CmdLabelCreate = cli.Command{
} }
func runLabelCreate(_ stdctx.Context, cmd *cli.Command) error { func runLabelCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
labelFile := ctx.String("file") labelFile := ctx.String("file")
if len(labelFile) == 0 { if len(labelFile) == 0 {

View File

@@ -31,8 +31,13 @@ var CmdLabelDelete = cli.Command{
} }
func runLabelDelete(_ stdctx.Context, cmd *cli.Command) error { func runLabelDelete(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
labelID := ctx.Int64("id") labelID := ctx.Int64("id")
client := ctx.Login.Client() client := ctx.Login.Client()

View File

@@ -36,12 +36,17 @@ var CmdLabelsList = cli.Command{
// RunLabelsList list labels. // RunLabelsList list labels.
func RunLabelsList(_ stdctx.Context, cmd *cli.Command) error { func RunLabelsList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
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: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err != nil { if err != nil {
return err return err
@@ -51,6 +56,5 @@ func RunLabelsList(_ stdctx.Context, cmd *cli.Command) error {
return task.LabelsExport(labels, ctx.String("save")) return task.LabelsExport(labels, ctx.String("save"))
} }
print.LabelsList(labels, ctx.Output) return print.LabelsList(labels, ctx.Output)
return nil
} }

View File

@@ -41,8 +41,13 @@ var CmdLabelUpdate = cli.Command{
} }
func runLabelUpdate(_ stdctx.Context, cmd *cli.Command) error { func runLabelUpdate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
id := ctx.Int64("id") id := ctx.Int64("id")
var pName, pColor, pDescription *string var pName, pColor, pDescription *string
@@ -61,7 +66,6 @@ func runLabelUpdate(_ stdctx.Context, cmd *cli.Command) error {
pDescription = &description pDescription = &description
} }
var err error
_, _, err = ctx.Login.Client().EditLabel(ctx.Owner, ctx.Repo, id, gitea.EditLabelOption{ _, _, err = ctx.Login.Client().EditLabel(ctx.Owner, ctx.Repo, id, gitea.EditLabelOption{
Name: pName, Name: pName,
Color: pColor, Color: pColor,

View File

@@ -42,7 +42,10 @@ func runLogins(ctx context.Context, cmd *cli.Command) error {
} }
func runLoginDetail(name string) error { func runLoginDetail(name string) error {
l := config.GetLoginByName(name) l, err := config.GetLoginByName(name)
if err != nil {
return err
}
if l == nil { if l == nil {
fmt.Printf("Login '%s' do not exist\n\n", name) fmt.Printf("Login '%s' do not exist\n\n", name)
return nil return nil

View File

@@ -101,7 +101,11 @@ var CmdLoginHelper = cli.Command{
// Use --login flag if provided, otherwise fall back to host lookup // Use --login flag if provided, otherwise fall back to host lookup
var userConfig *config.Login var userConfig *config.Login
if loginName := cmd.String("login"); loginName != "" { if loginName := cmd.String("login"); loginName != "" {
userConfig = config.GetLoginByName(loginName) var lookupErr error
userConfig, lookupErr = config.GetLoginByName(loginName)
if lookupErr != nil {
log.Fatal(lookupErr)
}
if userConfig == nil { if userConfig == nil {
log.Fatalf("Login '%s' not found", loginName) log.Fatalf("Login '%s' not found", loginName)
} }
@@ -112,7 +116,7 @@ var CmdLoginHelper = cli.Command{
} }
} }
if len(userConfig.Token) == 0 { if len(userConfig.GetAccessToken()) == 0 {
log.Fatal("User not set") log.Fatal("User not set")
} }
@@ -126,7 +130,7 @@ var CmdLoginHelper = cli.Command{
return err return err
} }
_, err = fmt.Fprintf(os.Stdout, "protocol=%s\nhost=%s\nusername=%s\npassword=%s\n", host.Scheme, host.Host, userConfig.User, userConfig.Token) _, err = fmt.Fprintf(os.Stdout, "protocol=%s\nhost=%s\nusername=%s\npassword=%s\n", host.Scheme, host.Host, userConfig.User, userConfig.GetAccessToken())
if err != nil { if err != nil {
return err return err
} }

View File

@@ -30,6 +30,5 @@ func RunLoginList(_ context.Context, cmd *cli.Command) error {
if err != nil { if err != nil {
return err return err
} }
print.LoginsList(logins, cmd.String("output")) return print.LoginsList(logins, cmd.String("output"))
return nil
} }

View File

@@ -38,18 +38,21 @@ func runLoginOAuthRefresh(_ context.Context, cmd *cli.Command) error {
} }
// Get the login from config // Get the login from config
login := config.GetLoginByName(loginName) login, err := config.GetLoginByName(loginName)
if err != nil {
return err
}
if login == nil { if login == nil {
return fmt.Errorf("login '%s' not found", loginName) return fmt.Errorf("login '%s' not found", loginName)
} }
// Check if the login has a refresh token // Check if the login has a refresh token
if login.RefreshToken == "" { if login.GetRefreshToken() == "" {
return fmt.Errorf("login '%s' does not have a refresh token. It may have been created using a different authentication method", loginName) return fmt.Errorf("login '%s' does not have a refresh token. It may have been created using a different authentication method", loginName)
} }
// Try to refresh the token // Try to refresh the token
err := auth.RefreshAccessToken(login) err = auth.RefreshAccessToken(login)
if err == nil { if err == nil {
fmt.Printf("Successfully refreshed OAuth token for %s\n", loginName) fmt.Printf("Successfully refreshed OAuth token for %s\n", loginName)
return nil return nil

View File

@@ -40,8 +40,13 @@ func runMilestones(ctx stdctx.Context, cmd *cli.Command) error {
} }
func runMilestoneDetail(_ stdctx.Context, cmd *cli.Command, name string) error { func runMilestoneDetail(_ stdctx.Context, cmd *cli.Command, name string) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
milestone, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, name) milestone, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, name)

View File

@@ -50,7 +50,10 @@ var CmdMilestonesCreate = cli.Command{
} }
func runMilestonesCreate(_ stdctx.Context, cmd *cli.Command) error { func runMilestonesCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
date := ctx.String("deadline") date := ctx.String("deadline")
deadline := &time.Time{} deadline := &time.Time{}

View File

@@ -24,10 +24,15 @@ var CmdMilestonesDelete = cli.Command{
} }
func deleteMilestone(_ stdctx.Context, cmd *cli.Command) error { func deleteMilestone(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
_, err := client.DeleteMilestoneByName(ctx.Owner, ctx.Repo, ctx.Args().First()) _, err = client.DeleteMilestoneByName(ctx.Owner, ctx.Repo, ctx.Args().First())
return err return err
} }

View File

@@ -71,8 +71,13 @@ var CmdMilestoneRemoveIssue = cli.Command{
} }
func runMilestoneIssueList(_ stdctx.Context, cmd *cli.Command) error { func runMilestoneIssueList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
state, err := flags.ParseState(ctx.String("state")) state, err := flags.ParseState(ctx.String("state"))
@@ -97,7 +102,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: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
Milestones: []string{milestone}, Milestones: []string{milestone},
Type: kind, Type: kind,
State: state, State: state,
@@ -110,13 +115,17 @@ func runMilestoneIssueList(_ stdctx.Context, cmd *cli.Command) error {
if err != nil { if err != nil {
return err return err
} }
print.IssuesPullsList(issues, ctx.Output, fields) return print.IssuesPullsList(issues, ctx.Output, fields)
return nil
} }
func runMilestoneIssueAdd(_ stdctx.Context, cmd *cli.Command) error { func runMilestoneIssueAdd(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
if ctx.Args().Len() != 2 { if ctx.Args().Len() != 2 {
return fmt.Errorf("need two arguments") return fmt.Errorf("need two arguments")
@@ -145,8 +154,13 @@ func runMilestoneIssueAdd(_ stdctx.Context, cmd *cli.Command) error {
} }
func runMilestoneIssueRemove(_ stdctx.Context, cmd *cli.Command) error { func runMilestoneIssueRemove(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
if ctx.Args().Len() != 2 { if ctx.Args().Len() != 2 {
return fmt.Errorf("need two arguments") return fmt.Errorf("need two arguments")

View File

@@ -40,8 +40,13 @@ var CmdMilestonesList = cli.Command{
// RunMilestonesList list milestones // RunMilestonesList list milestones
func RunMilestonesList(_ stdctx.Context, cmd *cli.Command) error { func RunMilestonesList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
fields, err := fieldsFlag.GetValues(cmd) fields, err := fieldsFlag.GetValues(cmd)
if err != nil { if err != nil {
@@ -58,13 +63,12 @@ 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: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
State: state, State: state,
}) })
if err != nil { if err != nil {
return err return err
} }
print.MilestonesList(milestones, ctx.Output, fields) return print.MilestonesList(milestones, ctx.Output, fields)
return nil
} }

View File

@@ -29,8 +29,13 @@ var CmdMilestonesReopen = cli.Command{
} }
func editMilestoneStatus(_ stdctx.Context, cmd *cli.Command, close bool) error { func editMilestoneStatus(_ stdctx.Context, cmd *cli.Command, close bool) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
return fmt.Errorf("missing required argument: %s", ctx.Command.ArgsUsage) return fmt.Errorf("missing required argument: %s", ctx.Command.ArgsUsage)
} }
@@ -41,6 +46,13 @@ func editMilestoneStatus(_ stdctx.Context, cmd *cli.Command, close bool) error {
} }
client := ctx.Login.Client() client := ctx.Login.Client()
repoURL := ""
if ctx.Args().Len() > 1 {
repoURL, err = ctx.GetRemoteRepoHTMLURL()
if err != nil {
return err
}
}
for _, ms := range ctx.Args().Slice() { for _, ms := range ctx.Args().Slice() {
opts := gitea.EditMilestoneOption{ opts := gitea.EditMilestoneOption{
State: &state, State: &state,
@@ -52,7 +64,7 @@ func editMilestoneStatus(_ stdctx.Context, cmd *cli.Command, close bool) error {
} }
if ctx.Args().Len() > 1 { if ctx.Args().Len() > 1 {
fmt.Printf("%s/milestone/%d\n", ctx.GetRemoteRepoHTMLURL(), milestone.ID) fmt.Printf("%s/milestone/%d\n", repoURL, milestone.ID)
} else { } else {
print.MilestoneDetails(milestone) print.MilestoneDetails(milestone)
} }

View File

@@ -5,7 +5,6 @@ package notifications
import ( import (
stdctx "context" stdctx "context"
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context" "code.gitea.io/tea/modules/context"
@@ -64,12 +63,15 @@ func listNotifications(_ stdctx.Context, cmd *cli.Command, status []gitea.Notify
var news []*gitea.NotificationThread var news []*gitea.NotificationThread
var err error var err error
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
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 := flags.GetListOptions() listOpts := flags.GetListOptions(cmd)
if listOpts.Page == 0 { if listOpts.Page == 0 {
listOpts.Page = 1 listOpts.Page = 1
} }
@@ -91,7 +93,9 @@ func listNotifications(_ stdctx.Context, cmd *cli.Command, status []gitea.Notify
SubjectTypes: subjects, SubjectTypes: subjects,
}) })
} else { } else {
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
news, _, err = client.ListRepoNotifications(ctx.Owner, ctx.Repo, gitea.ListNotificationOptions{ news, _, err = client.ListRepoNotifications(ctx.Owner, ctx.Repo, gitea.ListNotificationOptions{
ListOptions: listOpts, ListOptions: listOpts,
Status: status, Status: status,
@@ -99,9 +103,8 @@ func listNotifications(_ stdctx.Context, cmd *cli.Command, status []gitea.Notify
}) })
} }
if err != nil { if err != nil {
log.Fatal(err) return err
} }
print.NotificationsList(news, ctx.Output, fields) return print.NotificationsList(news, ctx.Output, fields)
return nil
} }

View File

@@ -23,7 +23,10 @@ var CmdNotificationsMarkRead = cli.Command{
ArgsUsage: "[all | <notification id>]", ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags, Flags: flags.NotificationFlags,
Action: func(_ stdctx.Context, cmd *cli.Command) error { Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
filter, err := flags.NotificationStateFlag.GetValues(cmd) filter, err := flags.NotificationStateFlag.GetValues(cmd)
if err != nil { if err != nil {
return err return err
@@ -44,7 +47,10 @@ var CmdNotificationsMarkUnread = cli.Command{
ArgsUsage: "[all | <notification id>]", ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags, Flags: flags.NotificationFlags,
Action: func(_ stdctx.Context, cmd *cli.Command) error { Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
filter, err := flags.NotificationStateFlag.GetValues(cmd) filter, err := flags.NotificationStateFlag.GetValues(cmd)
if err != nil { if err != nil {
return err return err
@@ -65,7 +71,10 @@ var CmdNotificationsMarkPinned = cli.Command{
ArgsUsage: "[all | <notification id>]", ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags, Flags: flags.NotificationFlags,
Action: func(_ stdctx.Context, cmd *cli.Command) error { Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
filter, err := flags.NotificationStateFlag.GetValues(cmd) filter, err := flags.NotificationStateFlag.GetValues(cmd)
if err != nil { if err != nil {
return err return err
@@ -85,7 +94,10 @@ var CmdNotificationsUnpin = cli.Command{
ArgsUsage: "[all | <notification id>]", ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags, Flags: flags.NotificationFlags,
Action: func(_ stdctx.Context, cmd *cli.Command) error { Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
filter := []string{string(gitea.NotifyStatusPinned)} filter := []string{string(gitea.NotifyStatusPinned)}
// NOTE: we implicitly mark it as read, to match web UI semantics. marking as unread might be more useful? // NOTE: we implicitly mark it as read, to match web UI semantics. marking as unread might be more useful?
return markNotificationAs(ctx, filter, gitea.NotifyStatusRead) return markNotificationAs(ctx, filter, gitea.NotifyStatusRead)
@@ -109,7 +121,9 @@ func markNotificationAs(cmd *context.TeaContext, filterStates []string, targetSt
if allRepos { if allRepos {
_, _, err = client.ReadNotifications(opts) _, _, err = client.ReadNotifications(opts)
} else { } else {
cmd.Ensure(context.CtxRequirement{RemoteRepo: true}) if err := cmd.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
_, _, err = client.ReadRepoNotifications(cmd.Owner, cmd.Repo, opts) _, _, err = client.ReadRepoNotifications(cmd.Owner, cmd.Repo, opts)
} }

View File

@@ -28,8 +28,13 @@ var CmdOpen = cli.Command{
} }
func runOpen(_ stdctx.Context, cmd *cli.Command) error { func runOpen(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
var suffix string var suffix string
number := ctx.Args().Get(0) number := ctx.Args().Get(0)
@@ -74,5 +79,10 @@ func runOpen(_ stdctx.Context, cmd *cli.Command) error {
suffix = number suffix = number
} }
return open.Run(path.Join(ctx.GetRemoteRepoHTMLURL(), suffix)) repoURL, err := ctx.GetRemoteRepoHTMLURL()
if err != nil {
return err
}
return open.Run(path.Join(repoURL, suffix))
} }

View File

@@ -31,7 +31,10 @@ var CmdOrgs = cli.Command{
} }
func runOrganizations(ctx stdctx.Context, cmd *cli.Command) error { func runOrganizations(ctx stdctx.Context, cmd *cli.Command) error {
teaCtx := context.InitCommand(cmd) teaCtx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if teaCtx.Args().Len() == 1 { if teaCtx.Args().Len() == 1 {
return runOrganizationDetail(teaCtx) return runOrganizationDetail(teaCtx)
} }

View File

@@ -53,7 +53,10 @@ var CmdOrganizationCreate = cli.Command{
// RunOrganizationCreate sets up a new organization // RunOrganizationCreate sets up a new organization
func RunOrganizationCreate(_ stdctx.Context, cmd *cli.Command) error { func RunOrganizationCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if ctx.Args().Len() < 1 { if ctx.Args().Len() < 1 {
return fmt.Errorf("organization name is required") return fmt.Errorf("organization name is required")

View File

@@ -28,7 +28,10 @@ var CmdOrganizationDelete = cli.Command{
// RunOrganizationDelete delete user organization // RunOrganizationDelete delete user organization
func RunOrganizationDelete(_ stdctx.Context, cmd *cli.Command) error { func RunOrganizationDelete(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()

View File

@@ -29,17 +29,18 @@ var CmdOrganizationList = cli.Command{
// RunOrganizationList list user organizations // RunOrganizationList list user organizations
func RunOrganizationList(_ stdctx.Context, cmd *cli.Command) error { func RunOrganizationList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
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: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err != nil { if err != nil {
return err return err
} }
print.OrganizationsList(userOrganizations, ctx.Output) return print.OrganizationsList(userOrganizations, ctx.Output)
return nil
} }

View File

@@ -5,7 +5,6 @@ package cmd
import ( import (
stdctx "context" stdctx "context"
"encoding/json"
"fmt" "fmt"
"time" "time"
@@ -20,26 +19,11 @@ import (
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
type pullLabelData struct { type pullLabelData = detailLabelData
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
}
type pullReviewData struct { type pullReviewData = detailReviewData
ID int64 `json:"id"`
Reviewer string `json:"reviewer"`
State gitea.ReviewStateType `json:"state"`
Body string `json:"body"`
Created time.Time `json:"created"`
}
type pullCommentData struct { type pullCommentData = detailCommentData
ID int64 `json:"id"`
Author string `json:"author"`
Created time.Time `json:"created"`
Body string `json:"body"`
}
type pullData struct { type pullData struct {
ID int64 `json:"id"` ID int64 `json:"id"`
@@ -88,6 +72,7 @@ var CmdPulls = cli.Command{
&pulls.CmdPullsCreate, &pulls.CmdPullsCreate,
&pulls.CmdPullsClose, &pulls.CmdPullsClose,
&pulls.CmdPullsReopen, &pulls.CmdPullsReopen,
&pulls.CmdPullsEdit,
&pulls.CmdPullsReview, &pulls.CmdPullsReview,
&pulls.CmdPullsApprove, &pulls.CmdPullsApprove,
&pulls.CmdPullsReject, &pulls.CmdPullsReject,
@@ -103,8 +88,13 @@ func runPulls(ctx stdctx.Context, cmd *cli.Command) error {
} }
func runPullDetail(_ stdctx.Context, cmd *cli.Command, index string) error { func runPullDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
idx, err := utils.ArgToIndex(index) idx, err := utils.ArgToIndex(index)
if err != nil { if err != nil {
return err return err
@@ -149,28 +139,7 @@ func runPullDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
func runPullDetailAsJSON(ctx *context.TeaContext, pr *gitea.PullRequest, reviews []*gitea.PullReview) error { func runPullDetailAsJSON(ctx *context.TeaContext, pr *gitea.PullRequest, reviews []*gitea.PullReview) error {
c := ctx.Login.Client() c := ctx.Login.Client()
opts := gitea.ListIssueCommentOptions{ListOptions: flags.GetListOptions()} opts := gitea.ListIssueCommentOptions{ListOptions: flags.GetListOptions(ctx.Command)}
labelSlice := make([]pullLabelData, 0, len(pr.Labels))
for _, label := range pr.Labels {
labelSlice = append(labelSlice, pullLabelData{label.Name, label.Color, label.Description})
}
assigneesSlice := make([]string, 0, len(pr.Assignees))
for _, assignee := range pr.Assignees {
assigneesSlice = append(assigneesSlice, assignee.UserName)
}
reviewsSlice := make([]pullReviewData, 0, len(reviews))
for _, review := range reviews {
reviewsSlice = append(reviewsSlice, pullReviewData{
ID: review.ID,
Reviewer: review.Reviewer.UserName,
State: review.State,
Body: review.Body,
Created: review.Submitted,
})
}
mergedBy := "" mergedBy := ""
if pr.MergedBy != nil { if pr.MergedBy != nil {
@@ -184,10 +153,10 @@ func runPullDetailAsJSON(ctx *context.TeaContext, pr *gitea.PullRequest, reviews
State: pr.State, State: pr.State,
Created: pr.Created, Created: pr.Created,
Updated: pr.Updated, Updated: pr.Updated,
User: pr.Poster.UserName, User: username(pr.Poster),
Body: pr.Body, Body: pr.Body,
Labels: labelSlice, Labels: buildDetailLabels(pr.Labels),
Assignees: assigneesSlice, Assignees: buildDetailAssignees(pr.Assignees),
URL: pr.HTMLURL, URL: pr.HTMLURL,
Base: pr.Base.Ref, Base: pr.Base.Ref,
Head: pr.Head.Ref, Head: pr.Head.Ref,
@@ -198,7 +167,7 @@ func runPullDetailAsJSON(ctx *context.TeaContext, pr *gitea.PullRequest, reviews
MergedAt: pr.Merged, MergedAt: pr.Merged,
MergedBy: mergedBy, MergedBy: mergedBy,
ClosedAt: pr.Closed, ClosedAt: pr.Closed,
Reviews: reviewsSlice, Reviews: buildDetailReviews(reviews),
Comments: make([]pullCommentData, 0), Comments: make([]pullCommentData, 0),
} }
@@ -208,23 +177,8 @@ func runPullDetailAsJSON(ctx *context.TeaContext, pr *gitea.PullRequest, reviews
return err return err
} }
pullSlice.Comments = make([]pullCommentData, 0, len(comments)) pullSlice.Comments = buildDetailComments(comments)
for _, comment := range comments {
pullSlice.Comments = append(pullSlice.Comments, pullCommentData{
ID: comment.ID,
Author: comment.Poster.UserName,
Body: comment.Body,
Created: comment.Created,
})
}
} }
jsonData, err := json.MarshalIndent(pullSlice, "", "\t") return writeIndentedJSON(ctx.Writer, pullSlice)
if err != nil {
return err
}
_, err = fmt.Fprintf(ctx.Writer, "%s\n", jsonData)
return err
} }

View File

@@ -20,7 +20,10 @@ var CmdPullsApprove = cli.Command{
Description: "Approve a pull request", Description: "Approve a pull request",
ArgsUsage: "<pull index> [<comment>]", ArgsUsage: "<pull index> [<comment>]",
Action: func(_ stdctx.Context, cmd *cli.Command) error { Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
return runPullReview(ctx, gitea.ReviewStateApproved, false) return runPullReview(ctx, gitea.ReviewStateApproved, false)
}, },
Flags: flags.AllDefaultFlags, Flags: flags.AllDefaultFlags,

View File

@@ -34,11 +34,16 @@ var CmdPullsCheckout = cli.Command{
} }
func runPullsCheckout(_ stdctx.Context, cmd *cli.Command) error { func runPullsCheckout(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{ if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{
LocalRepo: true, LocalRepo: true,
RemoteRepo: true, RemoteRepo: true,
}) }); err != nil {
return err
}
if ctx.Args().Len() != 1 { if ctx.Args().Len() != 1 {
return fmt.Errorf("pull request index is required") return fmt.Errorf("pull request index is required")
} }

View File

@@ -32,8 +32,13 @@ var CmdPullsClean = cli.Command{
} }
func runPullsClean(_ stdctx.Context, cmd *cli.Command) error { func runPullsClean(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{LocalRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{LocalRepo: true}); err != nil {
return err
}
if ctx.Args().Len() != 1 { if ctx.Args().Len() != 1 {
return fmt.Errorf("pull request index is required") return fmt.Errorf("pull request index is required")
} }

View File

@@ -49,11 +49,16 @@ var CmdPullsCreate = cli.Command{
} }
func runPullsCreate(_ stdctx.Context, cmd *cli.Command) error { func runPullsCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{ if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{
LocalRepo: true, LocalRepo: true,
RemoteRepo: true, RemoteRepo: true,
}) }); err != nil {
return err
}
// no args -> interactive mode // no args -> interactive mode
if ctx.IsInteractiveMode() { if ctx.IsInteractiveMode() {

View File

@@ -6,19 +6,95 @@ package pulls
import ( import (
stdctx "context" stdctx "context"
"fmt" "fmt"
"strings"
"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/task"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
// CmdPullsEdit is the subcommand of pulls to edit pull requests
var CmdPullsEdit = cli.Command{
Name: "edit",
Aliases: []string{"e"},
Usage: "Edit one or more pull requests",
Description: `Edit one or more pull requests. To unset a property again,
use an empty string (eg. --milestone "").`,
ArgsUsage: "<idx> [<idx>...]",
Action: runPullsEdit,
Flags: append(flags.IssuePREditFlags,
&cli.StringFlag{
Name: "add-reviewers",
Aliases: []string{"r"},
Usage: "Comma-separated list of usernames to request review from",
},
&cli.StringFlag{
Name: "remove-reviewers",
Usage: "Comma-separated list of usernames to remove from reviewers",
},
),
}
func runPullsEdit(_ stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if !cmd.Args().Present() {
return fmt.Errorf("must specify at least one pull request index")
}
opts, err := flags.GetIssuePREditFlags(ctx)
if err != nil {
return err
}
if cmd.IsSet("add-reviewers") {
opts.AddReviewers = strings.Split(cmd.String("add-reviewers"), ",")
}
if cmd.IsSet("remove-reviewers") {
opts.RemoveReviewers = strings.Split(cmd.String("remove-reviewers"), ",")
}
indices, err := utils.ArgsToIndices(ctx.Args().Slice())
if err != nil {
return err
}
client := ctx.Login.Client()
for _, opts.Index = range indices {
pr, err := task.EditPull(ctx, client, *opts)
if err != nil {
return err
}
if ctx.Args().Len() > 1 {
fmt.Println(pr.HTMLURL)
} else {
print.PullDetails(pr, nil, nil)
}
}
return nil
}
// editPullState abstracts the arg parsing to edit the given pull request // editPullState abstracts the arg parsing to edit the given pull request
func editPullState(_ stdctx.Context, cmd *cli.Command, opts gitea.EditPullRequestOption) error { func editPullState(_ stdctx.Context, cmd *cli.Command, opts gitea.EditPullRequestOption) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
return fmt.Errorf("pull request index is required") return fmt.Errorf("pull request index is required")
} }

View File

@@ -30,8 +30,13 @@ var CmdPullsList = cli.Command{
// RunPullsList return list of pulls // RunPullsList return list of pulls
func RunPullsList(_ stdctx.Context, cmd *cli.Command) error { func RunPullsList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
state, err := flags.ParseState(ctx.String("state")) state, err := flags.ParseState(ctx.String("state"))
if err != nil { if err != nil {
@@ -39,7 +44,7 @@ func RunPullsList(_ stdctx.Context, cmd *cli.Command) error {
} }
prs, _, err := ctx.Login.Client().ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{ prs, _, err := ctx.Login.Client().ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
State: state, State: state,
}) })
if err != nil { if err != nil {
@@ -51,6 +56,5 @@ func RunPullsList(_ stdctx.Context, cmd *cli.Command) error {
return err return err
} }
print.PullsList(prs, ctx.Output, fields) return print.PullsList(prs, ctx.Output, fields)
return nil
} }

View File

@@ -41,8 +41,13 @@ var CmdPullsMerge = cli.Command{
}, },
}, flags.AllDefaultFlags...), }, flags.AllDefaultFlags...),
Action: func(_ stdctx.Context, cmd *cli.Command) error { Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
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

View File

@@ -19,7 +19,10 @@ var CmdPullsReject = cli.Command{
Description: "Request changes to a pull request", Description: "Request changes to a pull request",
ArgsUsage: "<pull index> <reason>", ArgsUsage: "<pull index> <reason>",
Action: func(_ stdctx.Context, cmd *cli.Command) error { Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
return runPullReview(ctx, gitea.ReviewStateRequestChanges, true) return runPullReview(ctx, gitea.ReviewStateRequestChanges, true)
}, },
Flags: flags.AllDefaultFlags, Flags: flags.AllDefaultFlags,

View File

@@ -22,8 +22,13 @@ var CmdPullsReview = cli.Command{
Description: "Interactively review a pull request", Description: "Interactively review a pull request",
ArgsUsage: "<pull index>", ArgsUsage: "<pull index>",
Action: func(_ stdctx.Context, cmd *cli.Command) error { Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
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")

View File

@@ -15,7 +15,9 @@ import (
// runPullReview handles the common logic for approving/rejecting pull requests // runPullReview handles the common logic for approving/rejecting pull requests
func runPullReview(ctx *context.TeaContext, state gitea.ReviewStateType, requireComment bool) error { func runPullReview(ctx *context.TeaContext, state gitea.ReviewStateType, requireComment bool) error {
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
minArgs := 1 minArgs := 1
if requireComment { if requireComment {

View File

@@ -68,8 +68,13 @@ var CmdReleaseCreate = cli.Command{
} }
func runReleaseCreate(_ stdctx.Context, cmd *cli.Command) error { func runReleaseCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
tag := ctx.String("tag") tag := ctx.String("tag")
if cmd.Args().Present() { if cmd.Args().Present() {

View File

@@ -35,8 +35,13 @@ var CmdReleaseDelete = cli.Command{
} }
func runReleaseDelete(_ stdctx.Context, cmd *cli.Command) error { func runReleaseDelete(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
if !ctx.Args().Present() { if !ctx.Args().Present() {

View File

@@ -58,8 +58,13 @@ var CmdReleaseEdit = cli.Command{
} }
func runReleaseEdit(_ stdctx.Context, cmd *cli.Command) error { func runReleaseEdit(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
var isDraft, isPre *bool var isDraft, isPre *bool

View File

@@ -31,18 +31,22 @@ var CmdReleaseList = cli.Command{
// RunReleasesList list releases // RunReleasesList list releases
func RunReleasesList(_ stdctx.Context, cmd *cli.Command) error { func RunReleasesList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
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: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err != nil { if err != nil {
return err return err
} }
print.ReleasesList(releases, ctx.Output) return print.ReleasesList(releases, ctx.Output)
return nil
} }
func getReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) { func getReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {

View File

@@ -32,6 +32,7 @@ var CmdRepos = cli.Command{
&repos.CmdRepoFork, &repos.CmdRepoFork,
&repos.CmdRepoMigrate, &repos.CmdRepoMigrate,
&repos.CmdRepoRm, &repos.CmdRepoRm,
&repos.CmdRepoEdit,
}, },
Flags: repos.CmdReposListFlags, Flags: repos.CmdReposListFlags,
} }
@@ -44,7 +45,10 @@ func runRepos(ctx stdctx.Context, cmd *cli.Command) error {
} }
func runRepoDetail(_ stdctx.Context, cmd *cli.Command, path string) error { func runRepoDetail(_ stdctx.Context, cmd *cli.Command, path string) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
repoOwner, repoName := utils.GetOwnerAndRepo(path, ctx.Owner) repoOwner, repoName := utils.GetOwnerAndRepo(path, ctx.Owner)
repo, _, err := client.GetRepo(repoOwner, repoName) repo, _, err := client.GetRepo(repoOwner, repoName)

View File

@@ -103,11 +103,13 @@ var CmdRepoCreate = cli.Command{
} }
func runRepoCreate(_ stdctx.Context, cmd *cli.Command) error { func runRepoCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
var ( var (
repo *gitea.Repository repo *gitea.Repository
err error
trustmodel gitea.TrustModel trustmodel gitea.TrustModel
) )

View File

@@ -83,7 +83,10 @@ var CmdRepoCreateFromTemplate = cli.Command{
} }
func runRepoCreateFromTemplate(_ stdctx.Context, cmd *cli.Command) error { func runRepoCreateFromTemplate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
templateOwner, templateRepo := utils.GetOwnerAndRepo(ctx.String("template"), ctx.Login.User) templateOwner, templateRepo := utils.GetOwnerAndRepo(ctx.String("template"), ctx.Login.User)

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/charmbracelet/huh" "charm.land/huh/v2"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@@ -46,7 +46,10 @@ var CmdRepoRm = cli.Command{
} }
func runRepoDelete(_ stdctx.Context, cmd *cli.Command) error { func runRepoDelete(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
@@ -76,7 +79,7 @@ func runRepoDelete(_ stdctx.Context, cmd *cli.Command) error {
} }
} }
_, err := client.DeleteRepo(owner, repoName) _, err = client.DeleteRepo(owner, repoName)
if err != nil { if err != nil {
return err return err
} }

111
cmd/repos/edit.go Normal file
View File

@@ -0,0 +1,111 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repos
import (
stdctx "context"
"strings"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v3"
)
// CmdRepoEdit represents a sub command of repos to edit one
var CmdRepoEdit = cli.Command{
Name: "edit",
Aliases: []string{"e"},
Usage: "Edit repository properties",
Description: "Edit repository properties",
ArgsUsage: " ", // command does not accept arguments
Action: runRepoEdit,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "New name of the repository",
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"desc"},
Usage: "New description of the repository",
},
&cli.StringFlag{
Name: "website",
Usage: "New website URL of the repository",
},
&cli.StringFlag{
Name: "private",
Usage: "Set private [true/false]",
DefaultText: "true",
},
&cli.StringFlag{
Name: "template",
Usage: "Set template [true/false]",
DefaultText: "true",
},
&cli.StringFlag{
Name: "archived",
Usage: "Set archived [true/false]",
DefaultText: "true",
},
&cli.StringFlag{
Name: "default-branch",
Usage: "Set default branch",
},
}, flags.AllDefaultFlags...),
}
func runRepoEdit(_ stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client()
opts := gitea.EditRepoOption{}
if ctx.IsSet("name") {
val := ctx.String("name")
opts.Name = &val
}
if ctx.IsSet("description") {
val := ctx.String("description")
opts.Description = &val
}
if ctx.IsSet("website") {
val := ctx.String("website")
opts.Website = &val
}
if ctx.IsSet("default-branch") {
val := ctx.String("default-branch")
opts.DefaultBranch = &val
}
if ctx.IsSet("private") {
opts.Private = gitea.OptionalBool(strings.ToLower(ctx.String("private"))[:1] == "t")
}
if ctx.IsSet("template") {
opts.Template = gitea.OptionalBool(strings.ToLower(ctx.String("template"))[:1] == "t")
}
if ctx.IsSet("archived") {
opts.Archived = gitea.OptionalBool(strings.ToLower(ctx.String("archived"))[:1] == "t")
}
repo, _, err := client.EditRepo(ctx.Owner, ctx.Repo, opts)
if err != nil {
return err
}
topics, _, err := client.ListRepoTopics(repo.Owner.UserName, repo.Name, gitea.ListRepoTopicsOptions{})
if err != nil {
return err
}
print.RepoDetails(repo, topics)
return nil
}

View File

@@ -33,8 +33,13 @@ var CmdRepoFork = cli.Command{
} }
func runRepoFork(_ stdctx.Context, cmd *cli.Command) error { func runRepoFork(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
opts := gitea.CreateForkOption{} opts := gitea.CreateForkOption{}

View File

@@ -5,6 +5,8 @@ package repos
import ( import (
stdctx "context" stdctx "context"
"fmt"
"net/http"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context" "code.gitea.io/tea/modules/context"
@@ -32,6 +34,12 @@ var CmdReposListFlags = append([]cli.Flag{
Required: false, Required: false,
Usage: "List your starred repos instead", Usage: "List your starred repos instead",
}, },
&cli.StringFlag{
Name: "owner",
Aliases: []string{"O"},
Required: false,
Usage: "List repos of a specific owner (org or user)",
},
repoFieldsFlag, repoFieldsFlag,
&typeFilterFlag, &typeFilterFlag,
&flags.PaginationPageFlag, &flags.PaginationPageFlag,
@@ -50,7 +58,10 @@ var CmdReposList = cli.Command{
// RunReposList list repositories // RunReposList list repositories
func RunReposList(_ stdctx.Context, cmd *cli.Command) error { func RunReposList(_ stdctx.Context, cmd *cli.Command) error {
teaCmd := context.InitCommand(cmd) teaCmd, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := teaCmd.Login.Client() client := teaCmd.Login.Client()
typeFilter, err := getTypeFilter(cmd) typeFilter, err := getTypeFilter(cmd)
@@ -59,13 +70,32 @@ func RunReposList(_ stdctx.Context, cmd *cli.Command) error {
} }
var rps []*gitea.Repository var rps []*gitea.Repository
if teaCmd.Bool("starred") { if owner := teaCmd.String("owner"); owner != "" {
var err error
_, resp, orgErr := client.GetOrg(owner)
if orgErr != nil {
if resp == nil || resp.StatusCode != http.StatusNotFound {
return fmt.Errorf("could not find owner: %w", orgErr)
}
// not an org, treat as user
rps, _, err = client.ListUserRepos(owner, gitea.ListReposOptions{
ListOptions: flags.GetListOptions(cmd),
})
} else {
rps, _, err = client.ListOrgRepos(owner, gitea.ListOrgReposOptions{
ListOptions: flags.GetListOptions(cmd),
})
}
if err != nil {
return err
}
} else if teaCmd.Bool("starred") {
user, _, err := client.GetMyUserInfo() user, _, err := client.GetMyUserInfo()
if err != nil { if err != nil {
return err return err
} }
rps, _, err = client.SearchRepos(gitea.SearchRepoOptions{ rps, _, err = client.SearchRepos(gitea.SearchRepoOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
StarredByUserID: user.ID, StarredByUserID: user.ID,
}) })
if err != nil { if err != nil {
@@ -78,11 +108,11 @@ func RunReposList(_ stdctx.Context, cmd *cli.Command) error {
if err != nil { if err != nil {
return err return err
} }
rps = paginateRepos(allRepos, flags.GetListOptions()) rps = paginateRepos(allRepos, flags.GetListOptions(cmd))
} else { } else {
var err error var err error
rps, _, err = client.ListMyRepos(gitea.ListReposOptions{ rps, _, err = client.ListMyRepos(gitea.ListReposOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
if err != nil { if err != nil {
return err return err
@@ -99,8 +129,7 @@ func RunReposList(_ stdctx.Context, cmd *cli.Command) error {
return err return err
} }
print.ReposList(reposFiltered, teaCmd.Output, fields) return print.ReposList(reposFiltered, teaCmd.Output, fields)
return nil
} }
func filterReposByType(repos []*gitea.Repository, t gitea.RepoType) []*gitea.Repository { func filterReposByType(repos []*gitea.Repository, t gitea.RepoType) []*gitea.Repository {

View File

@@ -109,11 +109,13 @@ var CmdRepoMigrate = cli.Command{
} }
func runRepoMigrate(_ stdctx.Context, cmd *cli.Command) error { func runRepoMigrate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
var ( var (
repo *gitea.Repository repo *gitea.Repository
err error
service gitea.GitServiceType service gitea.GitServiceType
) )

View File

@@ -6,6 +6,7 @@ package repos
import ( import (
stdctx "context" stdctx "context"
"fmt" "fmt"
"net/http"
"strings" "strings"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
@@ -57,17 +58,19 @@ var CmdReposSearch = cli.Command{
} }
func runReposSearch(_ stdctx.Context, cmd *cli.Command) error { func runReposSearch(_ stdctx.Context, cmd *cli.Command) error {
teaCmd := context.InitCommand(cmd) teaCmd, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := teaCmd.Login.Client() client := teaCmd.Login.Client()
var ownerID int64 var ownerID int64
if teaCmd.IsSet("owner") { if teaCmd.IsSet("owner") {
// test if owner is an organization // test if owner is an organization
org, _, err := client.GetOrg(teaCmd.String("owner")) org, resp, err := client.GetOrg(teaCmd.String("owner"))
if err != nil { if err != nil {
// HACK: the client does not return a response on 404, so we can't check res.StatusCode if resp == nil || resp.StatusCode != http.StatusNotFound {
if err.Error() != "404 Not Found" { return fmt.Errorf("Could not find owner: %w", err)
return fmt.Errorf("Could not find owner: %s", err)
} }
// if owner is no org, its a user // if owner is no org, its a user
@@ -109,7 +112,7 @@ func runReposSearch(_ stdctx.Context, cmd *cli.Command) error {
} }
rps, _, err := client.SearchRepos(gitea.SearchRepoOptions{ rps, _, err := client.SearchRepos(gitea.SearchRepoOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
OwnerID: ownerID, OwnerID: ownerID,
IsPrivate: isPrivate, IsPrivate: isPrivate,
IsArchived: isArchived, IsArchived: isArchived,
@@ -127,6 +130,5 @@ func runReposSearch(_ stdctx.Context, cmd *cli.Command) error {
if err != nil { if err != nil {
return err return err
} }
print.ReposList(rps, teaCmd.Output, fields) return print.ReposList(rps, teaCmd.Output, fields)
return nil
} }

View File

@@ -32,8 +32,13 @@ var CmdTrackedTimesAdd = cli.Command{
} }
func runTrackedTimesAdd(_ stdctx.Context, cmd *cli.Command) error { func runTrackedTimesAdd(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() < 2 { if ctx.Args().Len() < 2 {
return fmt.Errorf("No issue or duration specified.\nUsage:\t%s", ctx.Command.UsageText) return fmt.Errorf("No issue or duration specified.\nUsage:\t%s", ctx.Command.UsageText)

View File

@@ -26,8 +26,13 @@ var CmdTrackedTimesDelete = cli.Command{
} }
func runTrackedTimesDelete(_ stdctx.Context, cmd *cli.Command) error { func runTrackedTimesDelete(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
if ctx.Args().Len() < 2 { if ctx.Args().Len() < 2 {

View File

@@ -72,12 +72,16 @@ Depending on your permissions on the repository, only your own tracked times mig
// RunTimesList list repositories // RunTimesList list repositories
func RunTimesList(_ stdctx.Context, cmd *cli.Command) error { func RunTimesList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
var times []*gitea.TrackedTime var times []*gitea.TrackedTime
var err error
var from, until time.Time var from, until time.Time
var fields []string var fields []string
@@ -95,7 +99,7 @@ func RunTimesList(_ stdctx.Context, cmd *cli.Command) error {
} }
opts := gitea.ListTrackedTimesOptions{ opts := gitea.ListTrackedTimesOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
Since: from, Since: from,
Before: until, Before: until,
} }
@@ -133,6 +137,5 @@ func RunTimesList(_ stdctx.Context, cmd *cli.Command) error {
} }
} }
print.TrackedTimesList(times, ctx.Output, fields, ctx.Bool("total")) return print.TrackedTimesList(times, ctx.Output, fields, ctx.Bool("total"))
return nil
} }

View File

@@ -25,8 +25,13 @@ var CmdTrackedTimesReset = cli.Command{
} }
func runTrackedTimesReset(_ stdctx.Context, cmd *cli.Command) error { func runTrackedTimesReset(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
if ctx.Args().Len() != 1 { if ctx.Args().Len() != 1 {

View File

@@ -64,7 +64,10 @@ func runWebhooksDefault(ctx stdctx.Context, cmd *cli.Command) error {
} }
func runWebhookDetail(_ stdctx.Context, cmd *cli.Command) error { func runWebhookDetail(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
webhookID, err := utils.ArgToIndex(cmd.Args().First()) webhookID, err := utils.ArgToIndex(cmd.Args().First())

View File

@@ -59,7 +59,10 @@ func runWebhooksCreate(ctx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("webhook URL is required") return fmt.Errorf("webhook URL is required")
} }
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
webhookType := gitea.HookType(cmd.String("type")) webhookType := gitea.HookType(cmd.String("type"))
@@ -95,7 +98,6 @@ func runWebhooksCreate(ctx stdctx.Context, cmd *cli.Command) error {
} }
var hook *gitea.Hook var hook *gitea.Hook
var err error
if c.IsGlobal { if c.IsGlobal {
return fmt.Errorf("global webhooks not yet supported in this version") return fmt.Errorf("global webhooks not yet supported in this version")
} else if len(c.Org) > 0 { } else if len(c.Org) > 0 {

View File

@@ -37,7 +37,10 @@ func runWebhooksDelete(ctx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("webhook ID is required") return fmt.Errorf("webhook ID is required")
} }
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
webhookID, err := utils.ArgToIndex(cmd.Args().First()) webhookID, err := utils.ArgToIndex(cmd.Args().First())

View File

@@ -30,26 +30,27 @@ var CmdWebhooksList = cli.Command{
// RunWebhooksList list webhooks // RunWebhooksList list webhooks
func RunWebhooksList(ctx stdctx.Context, cmd *cli.Command) error { func RunWebhooksList(ctx stdctx.Context, cmd *cli.Command) error {
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
var hooks []*gitea.Hook var hooks []*gitea.Hook
var err error
if c.IsGlobal { if c.IsGlobal {
return fmt.Errorf("global webhooks not yet supported in this version") return fmt.Errorf("global webhooks not yet supported in this version")
} else if len(c.Org) > 0 { } else if len(c.Org) > 0 {
hooks, _, err = client.ListOrgHooks(c.Org, gitea.ListHooksOptions{ hooks, _, err = client.ListOrgHooks(c.Org, gitea.ListHooksOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
} else { } else {
hooks, _, err = client.ListRepoHooks(c.Owner, c.Repo, gitea.ListHooksOptions{ hooks, _, err = client.ListRepoHooks(c.Owner, c.Repo, gitea.ListHooksOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(cmd),
}) })
} }
if err != nil { if err != nil {
return err return err
} }
print.WebhooksList(hooks, c.Output) return print.WebhooksList(hooks, c.Output)
return nil
} }

View File

@@ -61,7 +61,10 @@ func runWebhooksUpdate(ctx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("webhook ID is required") return fmt.Errorf("webhook ID is required")
} }
c := context.InitCommand(cmd) c, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := c.Login.Client() client := c.Login.Client()
webhookID, err := utils.ArgToIndex(cmd.Args().First()) webhookID, err := utils.ArgToIndex(cmd.Args().First())

View File

@@ -19,7 +19,10 @@ var CmdWhoami = cli.Command{
Usage: "Show current logged in user", Usage: "Show current logged in user",
ArgsUsage: " ", // command does not accept arguments ArgsUsage: " ", // command does not accept arguments
Action: func(_ stdctx.Context, cmd *cli.Command) error { Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := ctx.Login.Client() client := ctx.Login.Client()
user, _, _ := client.GetMyUserInfo() user, _, _ := client.GetMyUserInfo()
print.UserDetails(user) print.UserDetails(user)

View File

@@ -399,6 +399,36 @@ Change state of one or more pull requests to 'open'
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
### edit, e
Edit one or more pull requests
**--add-assignees, -a**="": Comma-separated list of usernames to assign
**--add-labels, -L**="": Comma-separated list of labels to assign. Takes precedence over --remove-labels
**--add-reviewers, -r**="": Comma-separated list of usernames to request review from
**--deadline, -D**="": Deadline timestamp to assign
**--description, -d**="":
**--login, -l**="": Use a different Gitea Login. Optional
**--milestone, -m**="": Milestone to assign
**--referenced-version, -v**="": commit-hash or tag name to assign
**--remote, -R**="": Discover Gitea login from remote. Optional
**--remove-labels**="": Comma-separated list of labels to remove
**--remove-reviewers**="": Comma-separated list of usernames to remove from reviewers
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
**--title, -t**="":
### review ### review
Interactively review a pull request Interactively review a pull request
@@ -1007,6 +1037,8 @@ Show repository details
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--owner, -O**="": List repos of a specific owner (org or user)
**--page, -p**="": specify page (default: 1) **--page, -p**="": specify page (default: 1)
**--starred, -s**: List your starred repos instead **--starred, -s**: List your starred repos instead
@@ -1029,6 +1061,8 @@ List repositories you have access to
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--owner, -O**="": List repos of a specific owner (org or user)
**--page, -p**="": specify page (default: 1) **--page, -p**="": specify page (default: 1)
**--starred, -s**: List your starred repos instead **--starred, -s**: List your starred repos instead
@@ -1199,6 +1233,32 @@ Delete an existing repository
**--owner, -O**="": owner of the repo **--owner, -O**="": owner of the repo
### edit, e
Edit repository properties
**--archived**="": Set archived [true/false]
**--default-branch**="": Set default branch
**--description, --desc**="": New description of the repository
**--login, -l**="": Use a different Gitea Login. Optional
**--name**="": New name of the repository
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--private**="": Set private [true/false]
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
**--template**="": Set template [true/false]
**--website**="": New website URL of the repository
## branches, branch, b ## branches, branch, b
Consult branches Consult branches
@@ -1819,6 +1879,8 @@ Make an authenticated API request
**--Field, -F**="": Add a typed field to the request body (key=value, @file, or @- for stdin) **--Field, -F**="": Add a typed field to the request body (key=value, @file, or @- for stdin)
**--data, -d**="": Raw JSON request body (use @file to read from file, @- for stdin)
**--field, -f**="": Add a string field to the request body (key=value) **--field, -f**="": Add a string field to the request body (key=value)
**--header, -H**="": Add a custom header (key:value) **--header, -H**="": Add a custom header (key:value)

74
go.mod
View File

@@ -1,86 +1,88 @@
module code.gitea.io/tea module code.gitea.io/tea
go 1.25 go 1.26
require ( require (
charm.land/glamour/v2 v2.0.0
charm.land/huh/v2 v2.0.3
charm.land/lipgloss/v2 v2.0.2
code.gitea.io/gitea-vet v0.2.3 code.gitea.io/gitea-vet v0.2.3
code.gitea.io/sdk/gitea v0.23.2 code.gitea.io/sdk/gitea v0.24.1
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c
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/huh v0.8.0
github.com/charmbracelet/lipgloss v1.1.1-0.20260202080749-832bc9d6b9d2
github.com/enescakir/emoji v1.0.0 github.com/enescakir/emoji v1.0.0
github.com/go-git/go-git/v5 v5.16.5 github.com/go-authgate/sdk-go v0.6.1
github.com/go-git/go-git/v5 v5.17.2
github.com/muesli/termenv v0.16.0 github.com/muesli/termenv v0.16.0
github.com/olekukonko/tablewriter v1.1.3 github.com/olekukonko/tablewriter v1.1.4
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/urfave/cli-docs/v3 v3.1.0 github.com/urfave/cli-docs/v3 v3.1.0
github.com/urfave/cli/v3 v3.6.2 github.com/urfave/cli/v3 v3.8.0
golang.org/x/crypto v0.48.0 golang.org/x/crypto v0.49.0
golang.org/x/oauth2 v0.35.0 golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.41.0 golang.org/x/sys v0.42.0
golang.org/x/term v0.40.0 golang.org/x/term v0.41.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
replace github.com/muesli/termenv v0.16.0 => github.com/hramrach/termenv v0.16.1-0.20260212100405-cc30261f3059
require ( require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect
charm.land/bubbles/v2 v2.0.0 // indirect
charm.land/bubbletea/v2 v2.0.2 // indirect
dario.cat/mergo v1.0.2 // indirect dario.cat/mergo v1.0.2 // indirect
github.com/42wim/httpsig v1.2.3 // indirect github.com/42wim/httpsig v1.2.4 // 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.3.0 // indirect github.com/ProtonMail/go-crypto v1.4.0 // indirect
github.com/alecthomas/chroma/v2 v2.23.1 // indirect github.com/alecthomas/chroma/v2 v2.23.1 // indirect
github.com/atotto/clipboard v0.1.4 // 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/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20260204111555-7642919e0bee // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20260311145557-c83711a11ffa // indirect
github.com/charmbracelet/x/exp/strings v0.1.0 // indirect github.com/charmbracelet/x/exp/strings v0.1.0 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
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.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // 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
github.com/go-git/go-billy/v5 v5.7.0 // indirect github.com/go-git/go-billy/v5 v5.8.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect github.com/hashicorp/go-version v1.8.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/kevinburke/ssh_config v1.4.0 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.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.21 // indirect
github.com/mattn/go-runewidth v0.0.19 // 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/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/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.4 // indirect github.com/olekukonko/ll v0.1.7 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
@@ -91,9 +93,11 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.16 // indirect github.com/yuin/goldmark v1.7.16 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect
golang.org/x/net v0.49.0 // indirect github.com/zalando/go-keyring v0.2.6 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/net v0.52.0 // indirect
golang.org/x/tools v0.41.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.42.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
) )

173
go.sum
View File

@@ -1,20 +1,36 @@
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U=
charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w=
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
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.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg= code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg=
code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
code.gitea.io/sdk/gitea v0.24.1 h1:hpaqcdGcBmfMpV7JSbBJVwE99qo+WqGreJYKrDKEyW8=
code.gitea.io/sdk/gitea v0.24.1/go.mod h1:5/77BL3sHneCMEiZaMT9lfTvnnibsYxyO48mceCF3qA=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
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.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 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/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
@@ -33,50 +49,44 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 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.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
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 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.1 h1:nj0decPiixaZeL9diI4uzzQTkkz1kYY8+jgzCZXSmW0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/charmbracelet/bubbles v0.21.1/go.mod h1:HHvIYRCpbkCJw2yo0vNX1O5loCwSr9/mWS8GYSg50Sk= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188 h1:J8v4kWJYCaxv1SLhLunN74S+jMteZ1f7Dae99ioq4Bo=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188/go.mod h1:FzWNAbe1jEmI+GZljSnlaSA8wJjnNIZhWBLkTsAl6eg=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.1-0.20260202080749-832bc9d6b9d2 h1:jvxZhg+J/80xXR7cE07p0/aFE1BrxkUw0R2CH04CZOM=
github.com/charmbracelet/lipgloss v1.1.1-0.20260202080749-832bc9d6b9d2/go.mod h1:D4YudnJlpIa3bcKpFSigAEWd31pQMgYu3pFE94b/1mc=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= 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-20250609102027-b60490452b30 h1:lF42GCGfbMxx4SOYkjChVoUDexdM/hQ4DWnAHcJ/6K0= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250609102027-b60490452b30/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/exp/slice v0.0.0-20260204111555-7642919e0bee h1:B/JPEPNGIHyyhCPM483B+cfJQ1+9S2YBPWoTAJw3Ut0= github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
github.com/charmbracelet/x/exp/slice v0.0.0-20260204111555-7642919e0bee/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
github.com/charmbracelet/x/exp/slice v0.0.0-20260311145557-c83711a11ffa h1:bmNUSF4m+fwrzZAOhluMSZxdM4bk+SWN0Ni2DimCZm8=
github.com/charmbracelet/x/exp/slice v0.0.0-20260311145557-c83711a11ffa/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA= github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA=
github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= 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/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/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
@@ -85,6 +95,8 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
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=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -100,38 +112,48 @@ 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=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-authgate/sdk-go v0.2.0 h1:w22f+sAg/YMqnLOcS/4SAuMZXTbPurzkSQBsjb1hcbw=
github.com/go-authgate/sdk-go v0.2.0/go.mod h1:RGqvrFdrPnOumndoQQV8qzu8zP1KFUZPdhX0IkWduho=
github.com/go-authgate/sdk-go v0.6.1 h1:oQREINU63YckTRdJ+0VBmN6ewFSMXa0D862w8624/jw=
github.com/go-authgate/sdk-go v0.6.1/go.mod h1:55PLAPuu8GDK0omOwG6lx4c+9/T6dJwZd8kecUueLEk=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
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.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
github.com/go-git/go-git/v5 v5.17.1 h1:WnljyxIzSj9BRRUlnmAU35ohDsjRK0EKmL0evDqi5Jk=
github.com/go-git/go-git/v5 v5.17.1/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104=
github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.8.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/hramrach/termenv v0.16.1-0.20260212100405-cc30261f3059 h1:xxfLFNkkQNJqA7Tieg/oBg/7Wk24pbEFK1VnbkrnTo8=
github.com/hramrach/termenv v0.16.1-0.20260212100405-cc30261f3059/go.mod h1:jeqvVfGyGmpCFfP9fK4yIWvxcMb8ApE3EPBq5fCzaaU=
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/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -147,30 +169,27 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
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.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.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
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 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= 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 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 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/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.4 h1:QcDaO9quz213xqHZr0gElOcYeOSnFeq7HTQ9Wu4O1wE= github.com/olekukonko/ll v0.1.7 h1:WyK1YZwOTUKHEXZz3VydBDT5t3zDqa9yI8iJg5PHon4=
github.com/olekukonko/ll v0.1.4/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= github.com/olekukonko/ll v0.1.7/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
@@ -180,7 +199,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -198,6 +216,8 @@ github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqR
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -205,8 +225,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
@@ -216,31 +238,33 @@ github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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-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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
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=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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-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=
@@ -248,24 +272,23 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
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=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -6,10 +6,12 @@ package main // import "code.gitea.io/tea"
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"code.gitea.io/tea/cmd" "code.gitea.io/tea/cmd"
teacontext "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/debug" "code.gitea.io/tea/modules/debug"
) )
@@ -18,6 +20,9 @@ func main() {
app.Flags = append(app.Flags, debug.CliFlag()) app.Flags = append(app.Flags, debug.CliFlag())
err := app.Run(context.Background(), preprocessArgs(os.Args)) err := app.Run(context.Background(), preprocessArgs(os.Args))
if err != nil { if err != nil {
if errors.Is(err, teacontext.ErrCommandCanceled) {
os.Exit(0)
}
// app.Run already exits for errors implementing ErrorCoder, // app.Run already exits for errors implementing ErrorCoder,
// so we only handle generic errors with code 1 here. // so we only handle generic errors with code 1 here.
fmt.Fprintf(app.ErrWriter, "Error: %v\n", err) fmt.Fprintf(app.ErrWriter, "Error: %v\n", err)

Some files were not shown because too many files have changed in this diff Show More