mirror of
https://gitea.com/gitea/tea.git
synced 2026-04-26 02:03:30 +02:00
Compare commits
38 Commits
v0.12.0
...
lunny/fix_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
465a6f01ee | ||
|
|
a58c35c3e2 | ||
|
|
b43f36abd4 | ||
|
|
783ac7684a | ||
|
|
ab7dc97518 | ||
|
|
d0b7ea09e8 | ||
|
|
469a6d3466 | ||
|
|
20914a1375 | ||
|
|
3c1c9b2904 | ||
|
|
63bc90ea52 | ||
|
|
9e0a6203ae | ||
|
|
84ecd16f9c | ||
|
|
53e53e1067 | ||
|
|
0489d8c275 | ||
|
|
f538c05282 | ||
|
|
662e339bf9 | ||
|
|
5bb73667d1 | ||
|
|
f329f6fab2 | ||
|
|
366069315f | ||
|
|
1e13681663 | ||
|
|
bfbec3fc00 | ||
|
|
e31a167e54 | ||
|
|
6a7c3e4efa | ||
|
|
b05e03416b | ||
|
|
21881525a8 | ||
|
|
9a462247bd | ||
|
|
5f74fb37df | ||
|
|
ec658cfc33 | ||
|
|
cb9824b451 | ||
|
|
a531faa626 | ||
|
|
302c946cb8 | ||
|
|
0346e1cbb5 | ||
|
|
cd4051ed38 | ||
|
|
c797624fcf | ||
|
|
3372c9ec59 | ||
|
|
1ac8492ac7 | ||
|
|
d019f0dd72 | ||
|
|
c031db2413 |
@@ -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": {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.26.0
|
||||||
cmd:
|
cmd:
|
||||||
- bash
|
- bash
|
||||||
- -c
|
- -c
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,3 +17,5 @@ dist/
|
|||||||
.direnv/
|
.direnv/
|
||||||
result
|
result
|
||||||
result-*
|
result-*
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -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
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 != "" {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ var CmdActionsWorkflows = cli.Command{
|
|||||||
Action: runWorkflowsDefault,
|
Action: runWorkflowsDefault,
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
&workflows.CmdWorkflowsList,
|
&workflows.CmdWorkflowsList,
|
||||||
|
&workflows.CmdWorkflowsView,
|
||||||
|
&workflows.CmdWorkflowsDispatch,
|
||||||
|
&workflows.CmdWorkflowsEnable,
|
||||||
|
&workflows.CmdWorkflowsDisable,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
cmd/actions/workflows/disable.go
Normal file
65
cmd/actions/workflows/disable.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsDisable represents a sub command to disable a workflow
|
||||||
|
var CmdWorkflowsDisable = cli.Command{
|
||||||
|
Name: "disable",
|
||||||
|
Usage: "Disable a workflow",
|
||||||
|
Description: "Disable a workflow in the repository",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsDisable,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "confirm",
|
||||||
|
Aliases: []string{"y"},
|
||||||
|
Usage: "confirm disable without prompting",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsDisable(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
|
||||||
|
if !cmd.Bool("confirm") {
|
||||||
|
fmt.Printf("Are you sure you want to disable workflow %s? [y/N] ", workflowID)
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
if response != "y" && response != "Y" && response != "yes" {
|
||||||
|
fmt.Println("Disable canceled.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.DisableRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to disable workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Workflow %s disabled successfully\n", workflowID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
174
cmd/actions/workflows/dispatch.go
Normal file
174
cmd/actions/workflows/dispatch.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsDispatch represents a sub command to dispatch a workflow
|
||||||
|
var CmdWorkflowsDispatch = cli.Command{
|
||||||
|
Name: "dispatch",
|
||||||
|
Aliases: []string{"trigger", "run"},
|
||||||
|
Usage: "Dispatch a workflow run",
|
||||||
|
Description: "Trigger a workflow_dispatch event for a workflow",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsDispatch,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "ref",
|
||||||
|
Aliases: []string{"r"},
|
||||||
|
Usage: "branch or tag to dispatch on (default: current branch)",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "input",
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
Usage: "workflow input in key=value format (can be specified multiple times)",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "follow",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "follow log output after dispatching",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsDispatch(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
|
||||||
|
ref := cmd.String("ref")
|
||||||
|
if ref == "" {
|
||||||
|
if c.LocalRepo != nil {
|
||||||
|
branchName, _, localErr := c.LocalRepo.TeaGetCurrentBranchNameAndSHA()
|
||||||
|
if localErr == nil && branchName != "" {
|
||||||
|
ref = branchName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ref == "" {
|
||||||
|
return fmt.Errorf("--ref is required (no local branch detected)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs := make(map[string]string)
|
||||||
|
for _, input := range cmd.StringSlice("input") {
|
||||||
|
key, value, ok := strings.Cut(input, "=")
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid input format %q, expected key=value", input)
|
||||||
|
}
|
||||||
|
inputs[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitea.CreateActionWorkflowDispatchOption{
|
||||||
|
Ref: ref,
|
||||||
|
Inputs: inputs,
|
||||||
|
}
|
||||||
|
|
||||||
|
details, _, err := client.DispatchRepoActionWorkflow(c.Owner, c.Repo, workflowID, opt, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to dispatch workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
print.ActionWorkflowDispatchResult(details)
|
||||||
|
|
||||||
|
if cmd.Bool("follow") && details != nil && details.WorkflowRunID > 0 {
|
||||||
|
return followDispatchedRun(client, c, details.WorkflowRunID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
followPollInterval = 2 * time.Second
|
||||||
|
followMaxDuration = 30 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// followDispatchedRun waits for the dispatched run to start, then follows its logs
|
||||||
|
func followDispatchedRun(client *gitea.Client, c *context.TeaContext, runID int64) error {
|
||||||
|
fmt.Printf("\nWaiting for run %d to start...\n", runID)
|
||||||
|
|
||||||
|
var jobs *gitea.ActionWorkflowJobsResponse
|
||||||
|
for range 30 {
|
||||||
|
time.Sleep(followPollInterval)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
jobs, _, err = client.ListRepoActionRunJobs(c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get jobs: %w", err)
|
||||||
|
}
|
||||||
|
if len(jobs.Jobs) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if jobs == nil || len(jobs.Jobs) == 0 {
|
||||||
|
return fmt.Errorf("timed out waiting for jobs to appear")
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID := jobs.Jobs[0].ID
|
||||||
|
jobName := jobs.Jobs[0].Name
|
||||||
|
fmt.Printf("Following logs for job '%s' (ID: %d) - press Ctrl+C to stop...\n", jobName, jobID)
|
||||||
|
fmt.Println("---")
|
||||||
|
|
||||||
|
deadline := time.Now().Add(followMaxDuration)
|
||||||
|
var lastLogLength int
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
job, _, err := client.GetRepoActionJob(c.Owner, c.Repo, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning := job.Status == "in_progress" || job.Status == "queued" || job.Status == "pending"
|
||||||
|
|
||||||
|
logs, _, logErr := client.GetRepoActionJobLogs(c.Owner, c.Repo, jobID)
|
||||||
|
if logErr != nil && isRunning {
|
||||||
|
time.Sleep(followPollInterval)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if logErr == nil && len(logs) > lastLogLength {
|
||||||
|
fmt.Print(string(logs[lastLogLength:]))
|
||||||
|
lastLogLength = len(logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isRunning {
|
||||||
|
if logErr != nil {
|
||||||
|
fmt.Printf("\n---\nJob completed with status: %s (failed to fetch final logs: %v)\n", job.Status, logErr)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\n---\nJob completed with status: %s\n", job.Status)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(followPollInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return fmt.Errorf("timed out after %s following logs", followMaxDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
cmd/actions/workflows/enable.go
Normal file
48
cmd/actions/workflows/enable.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsEnable represents a sub command to enable a workflow
|
||||||
|
var CmdWorkflowsEnable = cli.Command{
|
||||||
|
Name: "enable",
|
||||||
|
Usage: "Enable a workflow",
|
||||||
|
Description: "Enable a disabled workflow in the repository",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsEnable,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsEnable(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
_, err = client.EnableRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to enable workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Workflow %s enabled successfully\n", workflowID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,8 +6,6 @@ package workflows
|
|||||||
import (
|
import (
|
||||||
stdctx "context"
|
stdctx "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/tea/cmd/flags"
|
"code.gitea.io/tea/cmd/flags"
|
||||||
"code.gitea.io/tea/modules/context"
|
"code.gitea.io/tea/modules/context"
|
||||||
@@ -22,65 +20,31 @@ var CmdWorkflowsList = cli.Command{
|
|||||||
Name: "list",
|
Name: "list",
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
Usage: "List repository workflows",
|
Usage: "List repository workflows",
|
||||||
Description: "List workflow files in the repository with active/inactive status",
|
Description: "List workflows in the repository with their status",
|
||||||
Action: RunWorkflowsList,
|
Action: RunWorkflowsList,
|
||||||
Flags: append([]cli.Flag{
|
Flags: flags.AllDefaultFlags,
|
||||||
&flags.PaginationPageFlag,
|
|
||||||
&flags.PaginationLimitFlag,
|
|
||||||
}, flags.AllDefaultFlags...),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunWorkflowsList lists workflow files in the repository
|
// RunWorkflowsList lists workflows in the repository using the workflow API
|
||||||
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
|
resp, _, err := client.ListRepoActionWorkflows(c.Owner, c.Repo)
|
||||||
var workflows []*gitea.ContentsResponse
|
|
||||||
|
|
||||||
// Try .gitea/workflows first, then .github/workflows
|
|
||||||
workflowDir := ".gitea/workflows"
|
|
||||||
contents, _, err := client.ListContents(c.Owner, c.Repo, "", workflowDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
workflowDir = ".github/workflows"
|
return fmt.Errorf("failed to list workflows: %w", err)
|
||||||
contents, _, err = client.ListContents(c.Owner, c.Repo, "", workflowDir)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("No workflow files found\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter for workflow files (.yml and .yaml)
|
var workflows []*gitea.ActionWorkflow
|
||||||
for _, content := range contents {
|
if resp != nil {
|
||||||
if content.Type == "file" {
|
workflows = resp.Workflows
|
||||||
ext := strings.ToLower(filepath.Ext(content.Name))
|
|
||||||
if ext == ".yml" || ext == ".yaml" {
|
|
||||||
content.Path = workflowDir + "/" + content.Name
|
|
||||||
workflows = append(workflows, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(workflows) == 0 {
|
return print.ActionWorkflowsList(workflows, c.Output)
|
||||||
fmt.Printf("No workflow files found\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check which workflows have runs to determine active status
|
|
||||||
workflowStatus := make(map[string]bool)
|
|
||||||
|
|
||||||
// Get recent runs to check activity
|
|
||||||
runs, _, err := client.ListRepoActionRuns(c.Owner, c.Repo, gitea.ListRepoActionRunsOptions{
|
|
||||||
ListOptions: flags.GetListOptions(),
|
|
||||||
})
|
|
||||||
if err == nil && runs != nil {
|
|
||||||
for _, run := range runs.WorkflowRuns {
|
|
||||||
// Extract workflow file name from path
|
|
||||||
workflowFile := filepath.Base(run.Path)
|
|
||||||
workflowStatus[workflowFile] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print.WorkflowsList(workflows, workflowStatus, c.Output)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
50
cmd/actions/workflows/view.go
Normal file
50
cmd/actions/workflows/view.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsView represents a sub command to view workflow details
|
||||||
|
var CmdWorkflowsView = cli.Command{
|
||||||
|
Name: "view",
|
||||||
|
Aliases: []string{"show", "get"},
|
||||||
|
Usage: "View workflow details",
|
||||||
|
Description: "View details of a specific workflow",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsView,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsView(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
wf, _, err := client.GetRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
print.ActionWorkflowDetails(wf)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
292
cmd/api.go
292
cmd/api.go
@@ -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
635
cmd/api_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"code.gitea.io/tea/cmd/flags"
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/cmd/releases"
|
||||||
"code.gitea.io/tea/modules/context"
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
@@ -27,20 +28,25 @@ 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 {
|
||||||
return fmt.Errorf("No release tag or assets specified.\nUsage:\t%s", ctx.Command.UsageText)
|
return fmt.Errorf("no release tag or assets specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||||
}
|
}
|
||||||
|
|
||||||
tag := ctx.Args().First()
|
tag := ctx.Args().First()
|
||||||
if len(tag) == 0 {
|
if len(tag) == 0 {
|
||||||
return fmt.Errorf("Release tag needed to create attachment")
|
return fmt.Errorf("release tag needed to create attachment")
|
||||||
}
|
}
|
||||||
|
|
||||||
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
release, err := releases.GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.gitea.io/tea/cmd/flags"
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/cmd/releases"
|
||||||
"code.gitea.io/tea/modules/context"
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
@@ -32,17 +33,22 @@ 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 {
|
||||||
return fmt.Errorf("No release tag or attachment names specified.\nUsage:\t%s", ctx.Command.UsageText)
|
return fmt.Errorf("no release tag or attachment names specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||||
}
|
}
|
||||||
|
|
||||||
tag := ctx.Args().First()
|
tag := ctx.Args().First()
|
||||||
if len(tag) == 0 {
|
if len(tag) == 0 {
|
||||||
return fmt.Errorf("Release tag needed to delete attachment")
|
return fmt.Errorf("release tag needed to delete attachment")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.Bool("confirm") {
|
if !ctx.Bool("confirm") {
|
||||||
@@ -50,7 +56,7 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
release, err := releases.GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -70,7 +76,7 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if attachment == nil {
|
if attachment == nil {
|
||||||
return fmt.Errorf("Release does not have attachment named '%s'", name)
|
return fmt.Errorf("release does not have attachment named '%s'", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = client.DeleteReleaseAttachment(ctx.Owner, ctx.Repo, release.ID, attachment.ID)
|
_, err = client.DeleteReleaseAttachment(ctx.Owner, ctx.Repo, release.ID, attachment.ID)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.gitea.io/tea/cmd/flags"
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/cmd/releases"
|
||||||
"code.gitea.io/tea/modules/context"
|
"code.gitea.io/tea/modules/context"
|
||||||
"code.gitea.io/tea/modules/print"
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
@@ -31,45 +32,31 @@ 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()
|
||||||
if len(tag) == 0 {
|
if len(tag) == 0 {
|
||||||
return fmt.Errorf("Release tag needed to list attachments")
|
return fmt.Errorf("release tag needed to list attachments")
|
||||||
}
|
}
|
||||||
|
|
||||||
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
release, err := releases.GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
|
||||||
rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{
|
|
||||||
ListOptions: gitea.ListOptions{Page: -1},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(rl) == 0 {
|
|
||||||
return nil, fmt.Errorf("Repo does not have any release")
|
|
||||||
}
|
|
||||||
for _, r := range rl {
|
|
||||||
if r.TagName == tag {
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("Release tag does not exist")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ var CmdBranches = cli.Command{
|
|||||||
&branches.CmdBranchesList,
|
&branches.CmdBranchesList,
|
||||||
&branches.CmdBranchesProtect,
|
&branches.CmdBranchesProtect,
|
||||||
&branches.CmdBranchesUnprotect,
|
&branches.CmdBranchesUnprotect,
|
||||||
|
&branches.CmdBranchesRename,
|
||||||
},
|
},
|
||||||
Flags: append([]cli.Flag{
|
Flags: append([]cli.Flag{
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
78
cmd/branches/rename.go
Normal file
78
cmd/branches/rename.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package branches
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdBranchesRenameFlags Flags for command rename
|
||||||
|
var CmdBranchesRenameFlags = append([]cli.Flag{
|
||||||
|
branchFieldsFlag,
|
||||||
|
&flags.PaginationPageFlag,
|
||||||
|
&flags.PaginationLimitFlag,
|
||||||
|
}, flags.AllDefaultFlags...)
|
||||||
|
|
||||||
|
// CmdBranchesRename represents a sub command of branches to rename a branch
|
||||||
|
var CmdBranchesRename = cli.Command{
|
||||||
|
Name: "rename",
|
||||||
|
Aliases: []string{"rn"},
|
||||||
|
Usage: "Rename a branch",
|
||||||
|
Description: `Rename a branch in a repository`,
|
||||||
|
ArgsUsage: "<old_branch_name> <new_branch_name>",
|
||||||
|
Action: RunBranchesRename,
|
||||||
|
Flags: CmdBranchesRenameFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunBranchesRename function to rename a branch
|
||||||
|
func RunBranchesRename(_ 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 err := ValidateRenameArgs(ctx.Args().Slice()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldBranchName := ctx.Args().Get(0)
|
||||||
|
newBranchName := ctx.Args().Get(1)
|
||||||
|
|
||||||
|
owner := ctx.Owner
|
||||||
|
if ctx.IsSet("owner") {
|
||||||
|
owner = ctx.String("owner")
|
||||||
|
}
|
||||||
|
|
||||||
|
successful, _, err := ctx.Login.Client().RenameRepoBranch(owner, ctx.Repo, oldBranchName, gitea.RenameRepoBranchOption{
|
||||||
|
Name: newBranchName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to rename branch: %w", err)
|
||||||
|
}
|
||||||
|
if !successful {
|
||||||
|
return fmt.Errorf("failed to rename branch")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully renamed branch '%s' to '%s'\n", oldBranchName, newBranchName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRenameArgs validates arguments for the rename command
|
||||||
|
func ValidateRenameArgs(args []string) error {
|
||||||
|
if len(args) != 2 {
|
||||||
|
return fmt.Errorf("must specify exactly two arguments: <old_branch_name> <new_branch_name>")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
46
cmd/branches/rename_test.go
Normal file
46
cmd/branches/rename_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package branches
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBranchesRenameArgs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid args",
|
||||||
|
args: []string{"main", "develop"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing both args",
|
||||||
|
args: []string{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing new branch name",
|
||||||
|
args: []string{"main"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many args",
|
||||||
|
args: []string{"main", "develop", "extra"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := ValidateRenameArgs(tt.args)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ValidateRenameArgs() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
13
cmd/clone.go
13
cmd/clone.go
@@ -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 {
|
||||||
@@ -73,9 +76,13 @@ func runRepoClone(ctx stdctx.Context, cmd *cli.Command) error {
|
|||||||
|
|
||||||
owner, repo = utils.GetOwnerAndRepo(url.Path, login.User)
|
owner, repo = utils.GetOwnerAndRepo(url.Path, login.User)
|
||||||
if url.Host != "" {
|
if url.Host != "" {
|
||||||
login = config.GetLoginByHost(url.Host)
|
var lookupErr error
|
||||||
|
login, lookupErr = config.GetLoginByHost(url.Host)
|
||||||
|
if lookupErr != nil {
|
||||||
|
return lookupErr
|
||||||
|
}
|
||||||
if login == nil {
|
if login == nil {
|
||||||
return fmt.Errorf("No login configured matching host '%s', run `tea login add` first", url.Host)
|
return fmt.Errorf("no login configured matching host '%s', run 'tea login add' first", url.Host)
|
||||||
}
|
}
|
||||||
debug.Printf("Matched login '%s' for host '%s'", login.Name, url.Host)
|
debug.Printf("Matched login '%s' for host '%s'", login.Name, url.Host)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +36,17 @@ 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 {
|
||||||
return fmt.Errorf("Please specify issue / pr index")
|
return fmt.Errorf("please specify issue / pr index")
|
||||||
}
|
}
|
||||||
|
|
||||||
idx, err := utils.ArgToIndex(ctx.Args().First())
|
idx, err := utils.ArgToIndex(ctx.Args().First())
|
||||||
|
|||||||
93
cmd/detail_json.go
Normal file
93
cmd/detail_json.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ func (f CsvFlag) GetValues(cmd *cli.Command) ([]string, error) {
|
|||||||
if f.AvailableFields != nil && val != "" {
|
if f.AvailableFields != nil && val != "" {
|
||||||
for _, field := range selection {
|
for _, field := range selection {
|
||||||
if !utils.Contains(f.AvailableFields, field) {
|
if !utils.Contains(f.AvailableFields, field) {
|
||||||
return nil, fmt.Errorf("Invalid field '%s'", field)
|
return nil, fmt.Errorf("invalid field '%s'", field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package flags
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
@@ -39,16 +40,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 +80,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 +94,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
|
||||||
@@ -152,7 +168,7 @@ func ParseState(stateStr string) (gitea.StateType, error) {
|
|||||||
case "closed":
|
case "closed":
|
||||||
return gitea.StateClosed, nil
|
return gitea.StateClosed, nil
|
||||||
default:
|
default:
|
||||||
return "", errors.New("unknown state '" + stateStr + "'")
|
return "", fmt.Errorf("unknown state '%s'", stateStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +185,6 @@ func ParseIssueKind(kindStr string, defaultKind gitea.IssueType) (gitea.IssueTyp
|
|||||||
case "pull", "pulls", "pr":
|
case "pull", "pulls", "pr":
|
||||||
return gitea.IssueTypePull, nil
|
return gitea.IssueTypePull, nil
|
||||||
default:
|
default:
|
||||||
return "", errors.New("unknown kind '" + kindStr + "'")
|
return "", fmt.Errorf("unknown kind '%s'", kindStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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])
|
||||||
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ func GetIssuePRCreateFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, e
|
|||||||
}
|
}
|
||||||
ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestoneName)
|
ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestoneName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Milestone '%s' not found", milestoneName)
|
return nil, fmt.Errorf("milestone '%s' not found", milestoneName)
|
||||||
}
|
}
|
||||||
opts.Milestone = ms.ID
|
opts.Milestone = ms.ID
|
||||||
}
|
}
|
||||||
|
|||||||
117
cmd/issues.go
117
cmd/issues.go
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,5 +37,5 @@ func runLabels(ctx context.Context, cmd *cli.Command) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runLabelsDetails(cmd *cli.Command) error {
|
func runLabelsDetails(cmd *cli.Command) error {
|
||||||
return fmt.Errorf("Not yet implemented")
|
return fmt.Errorf("not yet implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ package login
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
|
||||||
"code.gitea.io/tea/modules/config"
|
"code.gitea.io/tea/modules/config"
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ var CmdLoginDelete = cli.Command{
|
|||||||
func RunLoginDelete(_ context.Context, cmd *cli.Command) error {
|
func RunLoginDelete(_ context.Context, cmd *cli.Command) error {
|
||||||
logins, err := config.GetLogins()
|
logins, err := config.GetLogins()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var name string
|
var name string
|
||||||
@@ -37,7 +36,7 @@ func RunLoginDelete(_ context.Context, cmd *cli.Command) error {
|
|||||||
} else if len(logins) == 1 {
|
} else if len(logins) == 1 {
|
||||||
name = logins[0].Name
|
name = logins[0].Name
|
||||||
} else {
|
} else {
|
||||||
return errors.New("Please specify a login name")
|
return fmt.Errorf("please specify a login name")
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.DeleteLogin(name)
|
return config.DeleteLogin(name)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package login
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ func runLoginEdit(_ context.Context, _ *cli.Command) error {
|
|||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
log.Fatal(err.Error())
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return open.Start(config.GetConfigPath())
|
return open.Start(config.GetConfigPath())
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -93,7 +92,7 @@ var CmdLoginHelper = cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(wants["host"]) == 0 {
|
if len(wants["host"]) == 0 {
|
||||||
log.Fatal("Hostname is required")
|
return fmt.Errorf("hostname is required")
|
||||||
} else if len(wants["protocol"]) == 0 {
|
} else if len(wants["protocol"]) == 0 {
|
||||||
wants["protocol"] = "http"
|
wants["protocol"] = "http"
|
||||||
}
|
}
|
||||||
@@ -101,19 +100,27 @@ 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 {
|
||||||
|
return lookupErr
|
||||||
|
}
|
||||||
if userConfig == nil {
|
if userConfig == nil {
|
||||||
log.Fatalf("Login '%s' not found", loginName)
|
return fmt.Errorf("login '%s' not found", loginName)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
userConfig = config.GetLoginByHost(wants["host"])
|
var lookupErr error
|
||||||
|
userConfig, lookupErr = config.GetLoginByHost(wants["host"])
|
||||||
|
if lookupErr != nil {
|
||||||
|
return lookupErr
|
||||||
|
}
|
||||||
if userConfig == nil {
|
if userConfig == nil {
|
||||||
log.Fatalf("No login found for host '%s'", wants["host"])
|
return fmt.Errorf("no login found for host '%s'", wants["host"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(userConfig.Token) == 0 {
|
if len(userConfig.GetAccessToken()) == 0 {
|
||||||
log.Fatal("User not set")
|
return fmt.Errorf("user not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
host, err := url.Parse(userConfig.URL)
|
host, err := url.Parse(userConfig.URL)
|
||||||
@@ -126,7 +133,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
cmd/open.go
16
cmd/open.go
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
85
cmd/pulls.go
85
cmd/pulls.go
@@ -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,10 +72,14 @@ 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,
|
||||||
&pulls.CmdPullsMerge,
|
&pulls.CmdPullsMerge,
|
||||||
|
&pulls.CmdPullsReviewComments,
|
||||||
|
&pulls.CmdPullsResolve,
|
||||||
|
&pulls.CmdPullsUnresolve,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,8 +91,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 +142,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 +156,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 +170,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 +180,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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ package pulls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
stdctx "context"
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
"code.gitea.io/tea/cmd/flags"
|
"code.gitea.io/tea/cmd/flags"
|
||||||
@@ -30,16 +32,22 @@ 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 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
prs, _, err := ctx.Login.Client().ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{
|
client := ctx.Login.Client()
|
||||||
ListOptions: flags.GetListOptions(),
|
prs, _, err := client.ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{
|
||||||
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
State: state,
|
State: state,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,6 +59,21 @@ func RunPullsList(_ stdctx.Context, cmd *cli.Command) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
print.PullsList(prs, ctx.Output, fields)
|
var ciStatuses map[int64]*gitea.CombinedStatus
|
||||||
return nil
|
if slices.Contains(fields, "ci") {
|
||||||
|
ciStatuses = map[int64]*gitea.CombinedStatus{}
|
||||||
|
for _, pr := range prs {
|
||||||
|
if pr.Head == nil || pr.Head.Sha == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ci, _, err := client.GetCombinedStatus(ctx.Owner, ctx.Repo, pr.Head.Sha)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error fetching CI status for PR #%d: %v\n", pr.Index, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ciStatuses[pr.Index] = ci
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return print.PullsList(prs, ctx.Output, fields, ciStatuses)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
30
cmd/pulls/resolve.go
Normal file
30
cmd/pulls/resolve.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package pulls
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdPullsResolve resolves a review comment on a pull request
|
||||||
|
var CmdPullsResolve = cli.Command{
|
||||||
|
Name: "resolve",
|
||||||
|
Usage: "Resolve a review comment on a pull request",
|
||||||
|
Description: "Resolve a review comment on a pull request",
|
||||||
|
ArgsUsage: "<comment id>",
|
||||||
|
Action: func(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return runResolveComment(ctx, task.ResolvePullReviewComment)
|
||||||
|
},
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
63
cmd/pulls/review_comments.go
Normal file
63
cmd/pulls/review_comments.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package pulls
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reviewCommentFieldsFlag = flags.FieldsFlag(print.PullReviewCommentFields, []string{
|
||||||
|
"id", "path", "line", "body", "reviewer", "resolver",
|
||||||
|
})
|
||||||
|
|
||||||
|
// CmdPullsReviewComments lists review comments on a pull request
|
||||||
|
var CmdPullsReviewComments = cli.Command{
|
||||||
|
Name: "review-comments",
|
||||||
|
Aliases: []string{"rc"},
|
||||||
|
Usage: "List review comments on a pull request",
|
||||||
|
Description: "List review comments on a pull request",
|
||||||
|
ArgsUsage: "<pull index>",
|
||||||
|
Action: runPullsReviewComments,
|
||||||
|
Flags: append([]cli.Flag{reviewCommentFieldsFlag}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPullsReviewComments(_ 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 ctx.Args().Len() < 1 {
|
||||||
|
return fmt.Errorf("pull request index is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, err := utils.ArgToIndex(ctx.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
comments, err := task.ListPullReviewComments(ctx, idx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fields, err := reviewCommentFieldsFlag.GetValues(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return print.PullReviewCommentsList(comments, ctx.Output, fields)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -38,3 +40,21 @@ func runPullReview(ctx *context.TeaContext, state gitea.ReviewStateType, require
|
|||||||
|
|
||||||
return task.CreatePullReview(ctx, idx, state, comment, nil)
|
return task.CreatePullReview(ctx, idx, state, comment, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runResolveComment handles the common logic for resolving/unresolving review comments
|
||||||
|
func runResolveComment(ctx *context.TeaContext, action func(*context.TeaContext, int64) error) error {
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Args().Len() < 1 {
|
||||||
|
return fmt.Errorf("comment ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
commentID, err := utils.ArgToIndex(ctx.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return action(ctx, commentID)
|
||||||
|
}
|
||||||
|
|||||||
30
cmd/pulls/unresolve.go
Normal file
30
cmd/pulls/unresolve.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package pulls
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdPullsUnresolve unresolves a review comment on a pull request
|
||||||
|
var CmdPullsUnresolve = cli.Command{
|
||||||
|
Name: "unresolve",
|
||||||
|
Usage: "Unresolve a review comment on a pull request",
|
||||||
|
Description: "Unresolve a review comment on a pull request",
|
||||||
|
ArgsUsage: "<comment id>",
|
||||||
|
Action: func(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return runResolveComment(ctx, task.UnresolvePullReviewComment)
|
||||||
|
},
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
@@ -99,7 +104,7 @@ func runReleaseCreate(_ stdctx.Context, cmd *cli.Command) error {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil && resp.StatusCode == http.StatusConflict {
|
if resp != nil && resp.StatusCode == http.StatusConflict {
|
||||||
return fmt.Errorf("There already is a release for this tag")
|
return fmt.Errorf("there is already a release for this tag")
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
@@ -50,7 +55,7 @@ func runReleaseDelete(_ stdctx.Context, cmd *cli.Command) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tag := range ctx.Args().Slice() {
|
for _, tag := range ctx.Args().Slice() {
|
||||||
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
release, err := GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -76,7 +81,7 @@ func runReleaseEdit(_ stdctx.Context, cmd *cli.Command) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tag := range ctx.Args().Slice() {
|
for _, tag := range ctx.Args().Slice() {
|
||||||
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
release, err := GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package releases
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
stdctx "context"
|
stdctx "context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"code.gitea.io/tea/cmd/flags"
|
"code.gitea.io/tea/cmd/flags"
|
||||||
"code.gitea.io/tea/modules/context"
|
"code.gitea.io/tea/modules/context"
|
||||||
@@ -31,34 +30,20 @@ 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) {
|
|
||||||
rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{
|
|
||||||
ListOptions: gitea.ListOptions{Page: -1},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(rl) == 0 {
|
|
||||||
return nil, fmt.Errorf("Repo does not have any release")
|
|
||||||
}
|
|
||||||
for _, r := range rl {
|
|
||||||
if r.TagName == tag {
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("Release tag does not exist")
|
|
||||||
}
|
}
|
||||||
|
|||||||
29
cmd/releases/utils.go
Normal file
29
cmd/releases/utils.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package releases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetReleaseByTag finds a release by its tag name.
|
||||||
|
func GetReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {
|
||||||
|
rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{
|
||||||
|
ListOptions: gitea.ListOptions{Page: -1},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(rl) == 0 {
|
||||||
|
return nil, fmt.Errorf("repo does not have any release")
|
||||||
|
}
|
||||||
|
for _, r := range rl {
|
||||||
|
if r.TagName == tag {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("release tag does not exist")
|
||||||
|
}
|
||||||
12
cmd/repos.go
12
cmd/repos.go
@@ -15,13 +15,13 @@ import (
|
|||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CmdRepos represents to login a gitea server.
|
// CmdRepos represents the command to manage repositories.
|
||||||
var CmdRepos = cli.Command{
|
var CmdRepos = cli.Command{
|
||||||
Name: "repos",
|
Name: "repos",
|
||||||
Aliases: []string{"repo"},
|
Aliases: []string{"repo"},
|
||||||
Category: catEntities,
|
Category: catEntities,
|
||||||
Usage: "Show repository details",
|
Usage: "Manage repositories",
|
||||||
Description: "Show repository details",
|
Description: "Manage repositories",
|
||||||
ArgsUsage: "[<repo owner>/<repo name>]",
|
ArgsUsage: "[<repo owner>/<repo name>]",
|
||||||
Action: runRepos,
|
Action: runRepos,
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
111
cmd/repos/edit.go
Normal 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
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user