mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
- **switch tea to the Cobra command tree:** `internal/app` wires the packages from parts 1–4 into explicit noun/verb commands with `--repo`, `--json` / `--jq`, `--sudo` and `--debug`; `main.go` points at it. Build, release and docs tooling (`tools/cli-docs`, Makefile, goreleaser, snap, CI) and README / CONTRIBUTING are updated. - **remove the urfave/cli command tree:** deletes `cmd/`, `modules/`, the old integration tests, contrib completion scripts and the generated `docs/CLI.md`, and drops their dependencies. This commit only deletes. ## Breaking changes This is a breaking release. It requires Gitea 28 or newer; configuration moves to separate `config.yml` and `hosts.yml` files and requires fresh authentication, and command names, aliases, flags and output formats change. `docs/migration-1.0.md` maps every old command, flag and environment variable to its replacement. ## Issues fixes #1118 fixes #1116 fixes #1115 fixes #1094 fixes #1091 fixes #1073 fixes #1052 fixes #1032 fixes #1023 fixes #966 fixes #908 fixes #833 fixes #831 fixes #817 fixes #804 fixes #779 fixes #770 fixes #737 fixes #697 fixes #726 fixes #677 fixes #672 fixes #633 fixes #632 fixes #560 fixes #556 fixes #523 fixes #516 fixes #510 fixes #504 fixes #500 fixes #482 fixes #406 fixes #403 fixes #373 fixes #351 fixes #344 fixes #326 fixes #325 fixes #323 fixes #246 fixes #240 fixes #232 fixes #197 fixes #172 fixes #141 fixes #69 fixes #1110 fixes #569 fixes #480 fixes #666 fixes #670 fixes #457 fixes #643 fixes #498 fixes #490 fixes #1069 fixes #834 fixes #602 fixes #328 fixes #417 fixes #571 fixes #1029 fixes #489 fixes #1050 fixes #162 fixes #430 fixes #1087 fixes #683 Reviewed-on: https://gitea.com/gitea/tea/pulls/1126 Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
213 lines
9.8 KiB
Go
213 lines
9.8 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dev/tea/internal/terminal"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type recordingPager struct {
|
|
command string
|
|
input string
|
|
calls int
|
|
}
|
|
|
|
func (p *recordingPager) Page(_ context.Context, command string, input io.Reader, streams terminal.Streams) error {
|
|
p.calls++
|
|
p.command = command
|
|
data, err := io.ReadAll(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.input = string(data)
|
|
_, err = io.WriteString(streams.Out, "PAGED:"+string(data))
|
|
return err
|
|
}
|
|
|
|
// pagedDeps returns serverDeps on a terminal stdout with a recording pager,
|
|
// extra environment variables and the given config settings.
|
|
func pagedDeps(t *testing.T, serverURL string, env map[string]string, config ...string) (Dependencies, *bytes.Buffer, *bytes.Buffer, *recordingPager) {
|
|
t.Helper()
|
|
deps, out, diagnostics := testDeps(t)
|
|
pager := &recordingPager{}
|
|
deps.Pager = pager
|
|
deps.Streams.OutTTY = true
|
|
deps.Getenv = serverEnv(serverURL, env)
|
|
for i := 0; i < len(config); i += 2 {
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "set", config[i], config[i+1]}))
|
|
}
|
|
return deps, out, diagnostics, pager
|
|
}
|
|
|
|
func TestLabelListAppliesColorAndPagesWithNoInput(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "/api/v1/repos/alice/project/labels", r.URL.Path)
|
|
_, _ = w.Write([]byte(`[{"id":1,"name":"bug","color":"ff0000","description":"Defect"},{"id":2,"name":"docs","color":"00ff00","description":"Docs"}]`))
|
|
}))
|
|
defer server.Close()
|
|
deps, out, diagnostics, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "fake-pager"}, "color", "always")
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, 1, pager.calls)
|
|
require.Equal(t, "fake-pager", pager.command)
|
|
require.Contains(t, pager.input, "\x1b[38;2;255;0;0mbug\x1b[0m")
|
|
require.Contains(t, pager.input, "\x1b[38;2;0;255;0mdocs\x1b[0m")
|
|
require.Equal(t, "PAGED:"+pager.input, out.String())
|
|
require.Empty(t, diagnostics.String())
|
|
}
|
|
|
|
func TestIssueListRespectsColorNeverAndStillPages(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`[{"number":1,"title":"One","state":"open"},{"number":2,"title":"Two","state":"closed"}]`))
|
|
}))
|
|
defer server.Close()
|
|
deps, out, _, pager := pagedDeps(t, server.URL, nil, "color", "never", "pager", "configured-pager")
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"issue", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, 1, pager.calls)
|
|
require.Equal(t, "configured-pager", pager.command)
|
|
require.NotContains(t, pager.input, "\x1b[")
|
|
require.Contains(t, pager.input, "open")
|
|
require.Contains(t, pager.input, "closed")
|
|
require.Equal(t, "PAGED:"+pager.input, out.String())
|
|
}
|
|
|
|
func TestIssueViewJSONDoesNotPage(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = fmt.Fprint(w, `{"number":7,"title":"Title","html_url":"https://example.org/alice/project/issues/7","body":"line1\nline2\nline3"}`)
|
|
}))
|
|
defer server.Close()
|
|
deps, out, _, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "fake-pager"})
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"issue", "view", "7", "-R", "alice/project", "--json=number,title", "--no-input"}))
|
|
require.Zero(t, pager.calls)
|
|
require.JSONEq(t, `{"number":7,"title":"Title"}`, out.String())
|
|
}
|
|
|
|
func TestConfiguredPagerOverridesPagerEnvironment(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`[{"number":1,"title":"One"},{"number":2,"title":"Two"}]`))
|
|
}))
|
|
defer server.Close()
|
|
deps, _, _, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "env-pager"})
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, "env-pager", pager.command)
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "set", "pager", "configured-pager"}))
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, "configured-pager", pager.command)
|
|
}
|
|
|
|
func TestTeaPagerAndColorEnvironmentOverrides(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`[{"id":1,"name":"bug","color":"ff0000","description":""},{"id":2,"name":"docs","color":"00ff00","description":""}]`))
|
|
}))
|
|
defer server.Close()
|
|
env := map[string]string{"TEA_PAGER": "tea-pager", "PAGER": "generic-pager", "TEA_COLOR": "always"}
|
|
deps, out, _, pager := pagedDeps(t, server.URL, env, "pager", "configured-pager", "color", "never")
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, "tea-pager", pager.command)
|
|
require.Contains(t, pager.input, "\x1b[38;2;255;0;0mbug\x1b[0m")
|
|
out.Reset()
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "get", "pager", "--show-source", "--json"}))
|
|
require.JSONEq(t, `{"pager":{"value":"tea-pager","source":"TEA_PAGER"}}`, out.String())
|
|
out.Reset()
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "get", "color", "--show-source", "--json"}))
|
|
require.JSONEq(t, `{"color":{"value":"always","source":"TEA_COLOR"}}`, out.String())
|
|
}
|
|
|
|
func TestColorAutoUsesStdoutTTY(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`[{"id":1,"name":"bug","color":"ff0000","description":""}]`))
|
|
}))
|
|
defer server.Close()
|
|
deps, out, _ := serverDeps(t, server.URL)
|
|
deps.Streams.OutTTY = false
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, "1 bug ff0000\n", out.String())
|
|
require.NotContains(t, out.String(), "\x1b[")
|
|
|
|
out.Reset()
|
|
deps.Streams.OutTTY = true
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Contains(t, out.String(), "\x1b[38;2;255;0;0mbug\x1b[0m")
|
|
}
|
|
|
|
func TestMultiItemHumanOutputPagesOnce(t *testing.T) {
|
|
issue := func(n int) string {
|
|
return fmt.Sprintf(`{"number":%d,"title":"Issue %d","state":"closed","html_url":"https://example.org/i/%d"}`, n, n, n)
|
|
}
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/v1/repos/alice/project/milestones/3":
|
|
_, _ = w.Write([]byte(`{"id":3,"title":"v1"}`))
|
|
case "/api/v1/repos/alice/project/issues":
|
|
_, _ = w.Write([]byte("[" + issue(1) + "," + issue(2) + "]"))
|
|
case "/api/v1/repos/alice/project/issues/1", "/api/v1/repos/alice/project/issues/2":
|
|
_, _ = w.Write([]byte(issue(int(r.URL.Path[len(r.URL.Path)-1] - '0'))))
|
|
case "/api/v1/repos/alice/project/branches":
|
|
_, _ = w.Write([]byte(`[{"name":"main","commit":{"id":"abc"}},{"name":"dev","commit":{"id":"def"}}]`))
|
|
case "/api/v1/repos/alice/project/issues/5/comments":
|
|
_, _ = w.Write([]byte(`[{"id":1,"body":"one","user":{"login":"alice"}},{"id":2,"body":"two","user":{"login":"bob"}}]`))
|
|
default:
|
|
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
for _, tc := range []struct {
|
|
args []string
|
|
want string
|
|
}{
|
|
{[]string{"milestone", "issue", "list", "3"}, "1 Issue 1 closed\n2 Issue 2 closed\n"},
|
|
{[]string{"issue", "close", "1", "2"}, "1 Issue 1 closed\n2 Issue 2 closed\n"},
|
|
{[]string{"branch", "list"}, "main abc false\ndev def false\n"},
|
|
{[]string{"issue", "comment", "list", "5"}, "1 alice\none\n\n2 bob\ntwo\n\n"},
|
|
} {
|
|
t.Run(tc.args[0]+" "+tc.args[1], func(t *testing.T) {
|
|
deps, out, _, pager := pagedDeps(t, server.URL, nil, "pager", "fake-pager", "color", "never")
|
|
require.NoError(t, Execute(context.Background(), deps, append(tc.args, "-R", "alice/project", "--yes")))
|
|
require.Equal(t, 1, pager.calls)
|
|
require.Equal(t, tc.want, pager.input)
|
|
require.Equal(t, "PAGED:"+tc.want, out.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListsHintWhenMorePagesExist(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
|
size, total := min(limit, 50), 120
|
|
items := []string{}
|
|
for id := (page-1)*size + 1; id <= min(page*size, total); id++ {
|
|
items = append(items, fmt.Sprintf(`{"id":%d,"username":"org%d","full_name":"owner/repo%d"}`, id, id, id))
|
|
}
|
|
w.Header().Set("X-Total-Count", strconv.Itoa(total))
|
|
_, _ = w.Write([]byte("[" + strings.Join(items, ",") + "]"))
|
|
}))
|
|
defer server.Close()
|
|
deps, out, diagnostics := serverDeps(t, server.URL)
|
|
for _, args := range [][]string{{"org", "list"}, {"repo", "list"}, {"issue", "list", "-R", "alice/project"}, {"pr", "list", "-R", "alice/project"}} {
|
|
out.Reset()
|
|
diagnostics.Reset()
|
|
require.NoError(t, Execute(context.Background(), deps, append(args, "--limit", "100")))
|
|
require.Len(t, strings.Split(strings.TrimSpace(out.String()), "\n"), 100, args)
|
|
require.Equal(t, "more results available; use --page 2\n", diagnostics.String(), args)
|
|
out.Reset()
|
|
diagnostics.Reset()
|
|
require.NoError(t, Execute(context.Background(), deps, append(args, "--limit", "100", "--page", "2")))
|
|
require.Len(t, strings.Split(strings.TrimSpace(out.String()), "\n"), 20, args)
|
|
require.Empty(t, diagnostics.String(), args)
|
|
diagnostics.Reset()
|
|
require.NoError(t, Execute(context.Background(), deps, append(args, "--json=id")))
|
|
require.Empty(t, diagnostics.String(), args)
|
|
}
|
|
}
|