mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
revert: tea 1.0 rewrite (#1122–#1126) (#1146)
Reverts #1126, #1125, #1124, #1123 and #1122. `go.mod` keeps the Go 1.26 line with toolchain `go1.26.9` (instead of the original `go1.26.6`) so the security fix from #1131 isn't lost. reopens #69 reopens #141 reopens #162 reopens #172 reopens #197 reopens #232 reopens #240 reopens #246 reopens #323 reopens #325 reopens #326 reopens #328 reopens #344 reopens #351 reopens #373 reopens #403 reopens #406 reopens #417 reopens #430 reopens #457 reopens #480 reopens #482 reopens #489 reopens #490 reopens #498 reopens #500 reopens #504 reopens #510 reopens #516 reopens #523 reopens #556 reopens #560 reopens #569 reopens #571 reopens #602 reopens #632 reopens #633 reopens #643 reopens #666 reopens #670 reopens #672 reopens #677 reopens #683 reopens #697 reopens #726 reopens #737 reopens #770 reopens #779 reopens #804 reopens #817 reopens #831 reopens #833 reopens #834 reopens #908 reopens #966 reopens #1023 reopens #1029 reopens #1032 reopens #1050 reopens #1052 reopens #1069 reopens #1073 reopens #1087 reopens #1091 reopens #1094 reopens #1110 reopens #1115 reopens #1116 reopens #1118 Reviewed-on: https://gitea.com/gitea/tea/pulls/1146 Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
This commit is contained in:
1 parent
33bb12729e
commit
c5f674111a
604 files changed
+33711
-35545
No files matched your search
@@ -0,0 +1,73 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package flags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// stdinPiped reports whether stdin is not a terminal, e.g. when a description
|
||||
// is piped from a file, command substitution, or a CI harness.
|
||||
func stdinPiped() bool {
|
||||
return !term.IsTerminal(int(os.Stdin.Fd()))
|
||||
}
|
||||
|
||||
// resolveCreateBody returns the issue/PR description for create commands.
|
||||
//
|
||||
// Precedence:
|
||||
// 1. --description-file (read from the file, or stdin when the path is "-")
|
||||
// 2. --description
|
||||
// 3. piped stdin
|
||||
func resolveCreateBody(description, descriptionFile string, descriptionFileSet, stdinPiped bool, stdin io.Reader) (string, error) {
|
||||
if descriptionFileSet {
|
||||
return readDescriptionSource(descriptionFile, stdin)
|
||||
}
|
||||
if description != "" {
|
||||
return description, nil
|
||||
}
|
||||
if stdinPiped {
|
||||
return readDescriptionStdin(stdin)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// resolveEditBody returns the new issue/PR body when a description flag was
|
||||
// provided, or nil when the caller should leave the body unchanged.
|
||||
func resolveEditBody(description string, descriptionSet bool, descriptionFile string, descriptionFileSet bool, stdin io.Reader) (*string, error) {
|
||||
if descriptionFileSet {
|
||||
body, err := readDescriptionSource(descriptionFile, stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &body, nil
|
||||
}
|
||||
if descriptionSet {
|
||||
body := description
|
||||
return &body, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func readDescriptionSource(source string, stdin io.Reader) (string, error) {
|
||||
if source == "-" {
|
||||
return readDescriptionStdin(stdin)
|
||||
}
|
||||
data, err := os.ReadFile(source)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read description file %q: %w", source, err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func readDescriptionStdin(stdin io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(stdin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read description from stdin: %w", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package flags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolveCreateBody(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), "body.md")
|
||||
require.NoError(t, os.WriteFile(file, []byte("from file"), 0o600))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
description string
|
||||
descriptionFile string
|
||||
descriptionFileSet bool
|
||||
stdinPiped bool
|
||||
stdin string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "description flag",
|
||||
description: "from -d",
|
||||
want: "from -d",
|
||||
},
|
||||
{
|
||||
name: "description file",
|
||||
descriptionFile: file,
|
||||
descriptionFileSet: true,
|
||||
want: "from file",
|
||||
},
|
||||
{
|
||||
name: "description file wins over description",
|
||||
description: "from -d",
|
||||
descriptionFile: file,
|
||||
descriptionFileSet: true,
|
||||
want: "from file",
|
||||
},
|
||||
{
|
||||
name: "dash reads stdin",
|
||||
descriptionFile: "-",
|
||||
descriptionFileSet: true,
|
||||
stdin: "from stdin",
|
||||
want: "from stdin",
|
||||
},
|
||||
{
|
||||
name: "description wins over piped stdin",
|
||||
description: "from -d",
|
||||
stdinPiped: true,
|
||||
stdin: "from stdin",
|
||||
want: "from -d",
|
||||
},
|
||||
{
|
||||
name: "piped stdin",
|
||||
stdinPiped: true,
|
||||
stdin: "from stdin",
|
||||
want: "from stdin",
|
||||
},
|
||||
{
|
||||
name: "empty description falls back to piped stdin",
|
||||
description: "",
|
||||
stdinPiped: true,
|
||||
stdin: "from stdin",
|
||||
want: "from stdin",
|
||||
},
|
||||
{
|
||||
name: "empty when no source provided",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := resolveCreateBody(tt.description, tt.descriptionFile, tt.descriptionFileSet, tt.stdinPiped, strings.NewReader(tt.stdin))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEditBody(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), "body.md")
|
||||
require.NoError(t, os.WriteFile(file, []byte("from file"), 0o600))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
description string
|
||||
descriptionSet bool
|
||||
descriptionFile string
|
||||
descriptionFileSet bool
|
||||
stdin string
|
||||
wantBody string
|
||||
wantSet bool
|
||||
}{
|
||||
{
|
||||
name: "no description flag",
|
||||
},
|
||||
{
|
||||
name: "description flag",
|
||||
description: "from -d",
|
||||
descriptionSet: true,
|
||||
wantBody: "from -d",
|
||||
wantSet: true,
|
||||
},
|
||||
{
|
||||
name: "empty description clears body",
|
||||
descriptionSet: true,
|
||||
wantSet: true,
|
||||
},
|
||||
{
|
||||
name: "description file",
|
||||
descriptionFile: file,
|
||||
descriptionFileSet: true,
|
||||
wantBody: "from file",
|
||||
wantSet: true,
|
||||
},
|
||||
{
|
||||
name: "description file wins over description",
|
||||
description: "from -d",
|
||||
descriptionSet: true,
|
||||
descriptionFile: file,
|
||||
descriptionFileSet: true,
|
||||
wantBody: "from file",
|
||||
wantSet: true,
|
||||
},
|
||||
{
|
||||
name: "dash reads stdin",
|
||||
descriptionFile: "-",
|
||||
descriptionFileSet: true,
|
||||
stdin: "from stdin",
|
||||
wantBody: "from stdin",
|
||||
wantSet: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := resolveEditBody(tt.description, tt.descriptionSet, tt.descriptionFile, tt.descriptionFileSet, strings.NewReader(tt.stdin))
|
||||
require.NoError(t, err)
|
||||
if !tt.wantSet {
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, tt.wantBody, *got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDescriptionSourceError(t *testing.T) {
|
||||
_, err := resolveCreateBody("", filepath.Join(t.TempDir(), "missing.md"), true, false, strings.NewReader(""))
|
||||
require.ErrorContains(t, err, "could not read description file")
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package flags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"gitea.dev/tea/modules/utils"
|
||||
)
|
||||
|
||||
// CsvFlag is a wrapper around cli.StringFlag, with an added GetValues() method
|
||||
// to retrieve comma separated string values as a slice.
|
||||
type CsvFlag struct {
|
||||
cli.StringFlag
|
||||
AvailableFields []string
|
||||
}
|
||||
|
||||
// NewCsvFlag creates a CsvFlag, while setting its usage string and default values
|
||||
func NewCsvFlag(name, usage string, aliases, availableValues, defaults []string) *CsvFlag {
|
||||
var availableDesc string
|
||||
if len(availableValues) != 0 {
|
||||
availableDesc = " Available values:"
|
||||
}
|
||||
return &CsvFlag{
|
||||
AvailableFields: availableValues,
|
||||
StringFlag: cli.StringFlag{
|
||||
Name: name,
|
||||
Aliases: aliases,
|
||||
Value: strings.Join(defaults, ","),
|
||||
Usage: fmt.Sprintf(`Comma-separated list of %s.%s
|
||||
%s
|
||||
`, usage, availableDesc, strings.Join(availableValues, ",")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetValues returns the value of the flag, parsed as a commaseparated list
|
||||
func (f CsvFlag) GetValues(cmd *cli.Command) ([]string, error) {
|
||||
val := cmd.String(f.Name)
|
||||
selection := strings.Split(val, ",")
|
||||
if f.AvailableFields != nil && val != "" {
|
||||
for _, field := range selection {
|
||||
if !utils.Contains(f.AvailableFields, field) {
|
||||
return nil, fmt.Errorf("invalid field '%s'", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
return selection, nil
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package flags
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
gitea "gitea.dev/sdk"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// LoginFlag provides flag to specify tea login profile
|
||||
var LoginFlag = cli.StringFlag{
|
||||
Name: "login",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "Use a different Gitea Login. Optional",
|
||||
}
|
||||
|
||||
// RepoFlag provides flag to specify repository
|
||||
var RepoFlag = cli.StringFlag{
|
||||
Name: "repo",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "Override local repository path or gitea repository slug to interact with. Optional",
|
||||
}
|
||||
|
||||
// RemoteFlag provides flag to specify remote repository
|
||||
var RemoteFlag = cli.StringFlag{
|
||||
Name: "remote",
|
||||
Aliases: []string{"R"},
|
||||
Usage: "Discover Gitea login from remote. Optional",
|
||||
}
|
||||
|
||||
// OutputFlag provides flag to specify output type
|
||||
var OutputFlag = cli.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "Output format. (simple, table, csv, tsv, yaml, json)",
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrPage indicates that the provided page value is invalid (less than -1 or equal to 0).
|
||||
ErrPage = errors.New("page cannot be smaller than 1")
|
||||
// ErrLimit indicates that the provided limit value is invalid (negative).
|
||||
ErrLimit = errors.New("limit cannot be negative")
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPageValue = 1
|
||||
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
|
||||
var PaginationFlags = []cli.Flag{
|
||||
&PaginationPageFlag,
|
||||
&PaginationLimitFlag,
|
||||
}
|
||||
|
||||
// PaginationPageFlag provides flag for pagination options
|
||||
var PaginationPageFlag = cli.IntFlag{
|
||||
Name: "page",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "specify page",
|
||||
Value: defaultPageValue,
|
||||
Validator: func(i int) error {
|
||||
if i < 1 && i != -1 {
|
||||
return ErrPage
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// PaginationLimitFlag provides flag for pagination options
|
||||
var PaginationLimitFlag = cli.IntFlag{
|
||||
Name: "limit",
|
||||
Aliases: []string{"lm"},
|
||||
Usage: "specify limit of items per page",
|
||||
Value: defaultLimitValue,
|
||||
Validator: func(i int) error {
|
||||
if i < 0 {
|
||||
return ErrLimit
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// LoginOutputFlags defines login and output flags that should
|
||||
// added to all subcommands and appended to the flags of the
|
||||
// subcommand to work around issue and provide --login and --output:
|
||||
// https://github.com/urfave/cli/issues/585
|
||||
var LoginOutputFlags = []cli.Flag{
|
||||
&LoginFlag,
|
||||
&OutputFlag,
|
||||
}
|
||||
|
||||
// LoginRepoFlags defines login and repo flags that should
|
||||
// be used for all subcommands and appended to the flags of
|
||||
// the subcommand to work around issue and provide --login and --repo:
|
||||
// https://github.com/urfave/cli/issues/585
|
||||
var LoginRepoFlags = []cli.Flag{
|
||||
&LoginFlag,
|
||||
&RepoFlag,
|
||||
&RemoteFlag,
|
||||
}
|
||||
|
||||
// AllDefaultFlags defines flags that should be available
|
||||
// for all subcommands working with dedicated repositories
|
||||
// to work around issue and provide --login, --repo and --output:
|
||||
// https://github.com/urfave/cli/issues/585
|
||||
var AllDefaultFlags = append([]cli.Flag{
|
||||
&RepoFlag,
|
||||
&RemoteFlag,
|
||||
}, LoginOutputFlags...)
|
||||
|
||||
// NotificationFlags defines flags that should be available on notifications.
|
||||
var NotificationFlags = append([]cli.Flag{
|
||||
NotificationStateFlag,
|
||||
&cli.BoolFlag{
|
||||
Name: "mine",
|
||||
Aliases: []string{"m"},
|
||||
Usage: "Show notifications across all your repositories instead of the current repository only",
|
||||
},
|
||||
&PaginationPageFlag,
|
||||
&PaginationLimitFlag,
|
||||
}, AllDefaultFlags...)
|
||||
|
||||
// NotificationStateFlag is a csv flag applied to all notification subcommands as filter
|
||||
var NotificationStateFlag = NewCsvFlag(
|
||||
"states",
|
||||
"notification states to filter by",
|
||||
[]string{"s"},
|
||||
[]string{"pinned", "unread", "read"},
|
||||
[]string{"unread", "pinned"},
|
||||
)
|
||||
|
||||
// FieldsFlag generates a flag selecting printable fields.
|
||||
// To retrieve the value, use f.GetValues()
|
||||
func FieldsFlag(availableFields, defaultFields []string) *CsvFlag {
|
||||
return NewCsvFlag("fields", "fields to print", []string{"f"}, availableFields, defaultFields)
|
||||
}
|
||||
|
||||
// ParseState parses a state string and returns the corresponding gitea.StateType
|
||||
func ParseState(stateStr string) (gitea.StateType, error) {
|
||||
switch stateStr {
|
||||
case "all":
|
||||
return gitea.StateAll, nil
|
||||
case "", "open":
|
||||
return gitea.StateOpen, nil
|
||||
case "closed":
|
||||
return gitea.StateClosed, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown state '%s'", stateStr)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseIssueKind parses a kind string and returns the corresponding gitea.IssueType.
|
||||
// If kindStr is empty, returns the provided defaultKind.
|
||||
func ParseIssueKind(kindStr string, defaultKind gitea.IssueType) (gitea.IssueType, error) {
|
||||
switch kindStr {
|
||||
case "":
|
||||
return defaultKind, nil
|
||||
case "all":
|
||||
return gitea.IssueTypeAll, nil
|
||||
case "issue", "issues":
|
||||
return gitea.IssueTypeIssue, nil
|
||||
case "pull", "pulls", "pr":
|
||||
return gitea.IssueTypePull, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown kind '%s'", kindStr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package flags
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
gitea "gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func TestPaginationFlags(t *testing.T) {
|
||||
var (
|
||||
defaultPage = PaginationPageFlag.Value
|
||||
defaultLimit = PaginationLimitFlag.Value
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedPage int
|
||||
expectedLimit int
|
||||
}{
|
||||
{
|
||||
name: "no flags",
|
||||
args: []string{"test"},
|
||||
expectedPage: defaultPage,
|
||||
expectedLimit: defaultLimit,
|
||||
},
|
||||
{
|
||||
name: "only paging",
|
||||
args: []string{"test", "--page", "5"},
|
||||
expectedPage: 5,
|
||||
expectedLimit: defaultLimit,
|
||||
},
|
||||
{
|
||||
name: "only limit",
|
||||
args: []string{"test", "--limit", "10"},
|
||||
expectedPage: defaultPage,
|
||||
expectedLimit: 10,
|
||||
},
|
||||
{
|
||||
name: "only limit",
|
||||
args: []string{"test", "--limit", "10"},
|
||||
expectedPage: defaultPage,
|
||||
expectedLimit: 10,
|
||||
},
|
||||
{
|
||||
name: "both flags",
|
||||
args: []string{"test", "--page", "2", "--limit", "20"},
|
||||
expectedPage: 2,
|
||||
expectedLimit: 20,
|
||||
},
|
||||
{ // TODO: Should no paging be applied as -1 or a separate flag? It's not obvious that page=-1 turns off paging and limit is ignored
|
||||
name: "no paging",
|
||||
args: []string{"test", "--limit", "20", "--page", "-1"},
|
||||
expectedPage: -1,
|
||||
expectedLimit: 20,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := cli.Command{
|
||||
Name: "test-paging",
|
||||
Action: func(_ context.Context, cmd *cli.Command) error {
|
||||
assert.Equal(t, tc.expectedPage, cmd.Int("page"))
|
||||
assert.Equal(t, tc.expectedLimit, cmd.Int("limit"))
|
||||
return nil
|
||||
},
|
||||
Flags: PaginationFlags,
|
||||
}
|
||||
err := cmd.Run(t.Context(), tc.args)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaginationFailures(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "negative limit",
|
||||
args: []string{"test", "--limit", "-10"},
|
||||
expectedError: ErrLimit,
|
||||
},
|
||||
{
|
||||
name: "negative paging",
|
||||
args: []string{"test", "--page", "-2"},
|
||||
expectedError: ErrPage,
|
||||
},
|
||||
{
|
||||
name: "zero paging",
|
||||
args: []string{"test", "--page", "0"},
|
||||
expectedError: ErrPage,
|
||||
},
|
||||
{
|
||||
// urfave does not validate all flags in one pass
|
||||
name: "negative paging and paging",
|
||||
args: []string{"test", "--page", "-2", "--limit", "-10"},
|
||||
expectedError: ErrPage,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
cmd := cli.Command{
|
||||
Name: "test-paging",
|
||||
Flags: PaginationFlags,
|
||||
Writer: io.Discard,
|
||||
ErrWriter: io.Discard,
|
||||
}
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := cmd.Run(t.Context(), tc.args)
|
||||
require.ErrorContains(t, err, tc.expectedError.Error())
|
||||
// require.ErrorIs(t, err, tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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(t.Context(), 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])
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package flags
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/task"
|
||||
|
||||
"github.com/araddon/dateparse"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// StateFlag provides flag to specify issue/pr state, defaulting to "open"
|
||||
var StateFlag = cli.StringFlag{
|
||||
Name: "state",
|
||||
Usage: "Filter by state (all|open|closed)",
|
||||
DefaultText: "open",
|
||||
}
|
||||
|
||||
// MilestoneFilterFlag is a CSV flag used to filter issues by milestones
|
||||
var MilestoneFilterFlag = NewCsvFlag(
|
||||
"milestones",
|
||||
"milestones to match issues against",
|
||||
[]string{"m"}, nil, nil)
|
||||
|
||||
// LabelFilterFlag is a CSV flag used to filter issues by labels
|
||||
var LabelFilterFlag = NewCsvFlag(
|
||||
"labels",
|
||||
"labels to match issues against",
|
||||
[]string{"L"}, nil, nil)
|
||||
|
||||
// PRListingFlags defines flags that should be available on pr listing flags.
|
||||
var PRListingFlags = append([]cli.Flag{
|
||||
&StateFlag,
|
||||
&PaginationPageFlag,
|
||||
&PaginationLimitFlag,
|
||||
}, AllDefaultFlags...)
|
||||
|
||||
// IssueListingFlags defines flags that should be available on issue listing flags.
|
||||
var IssueListingFlags = append([]cli.Flag{
|
||||
&StateFlag,
|
||||
&cli.StringFlag{
|
||||
Name: "kind",
|
||||
Aliases: []string{"K"},
|
||||
Usage: "Whether to return `issues`, `pulls`, or `all` (you can use this to apply advanced search filters to PRs)",
|
||||
DefaultText: "issues",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "keyword",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "Filter by search string",
|
||||
},
|
||||
LabelFilterFlag,
|
||||
MilestoneFilterFlag,
|
||||
&cli.StringFlag{
|
||||
Name: "author",
|
||||
Aliases: []string{"A"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "assignee",
|
||||
Aliases: []string{"a"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mentions",
|
||||
Aliases: []string{"M"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "owner",
|
||||
Aliases: []string{"org"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "from",
|
||||
Aliases: []string{"F"},
|
||||
Usage: "Filter by activity after this date",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "until",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Filter by activity before this date",
|
||||
},
|
||||
&PaginationPageFlag,
|
||||
&PaginationLimitFlag,
|
||||
}, AllDefaultFlags...)
|
||||
|
||||
// issuePRFlags defines shared flags between flags IssuePRCreateFlags and IssuePREditFlags
|
||||
var issuePRFlags = append([]cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "title",
|
||||
Aliases: []string{"t"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "description",
|
||||
Aliases: []string{"d"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "description-file",
|
||||
Usage: "Read description from file ('-' for stdin)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "referenced-version",
|
||||
Aliases: []string{"v"},
|
||||
Usage: "commit-hash or tag name to assign",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "milestone",
|
||||
Aliases: []string{"m"},
|
||||
Usage: "Milestone to assign",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "deadline",
|
||||
Aliases: []string{"D"},
|
||||
Usage: "Deadline timestamp to assign",
|
||||
},
|
||||
}, LoginRepoFlags...)
|
||||
|
||||
// IssuePRCreateFlags defines flags for creation of issues and PRs
|
||||
var IssuePRCreateFlags = append([]cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "assignees",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Comma-separated list of usernames to assign",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "labels",
|
||||
Aliases: []string{"L"},
|
||||
Usage: "Comma-separated list of labels to assign",
|
||||
},
|
||||
}, issuePRFlags...)
|
||||
|
||||
// GetIssuePRCreateFlags parses all IssuePREditFlags
|
||||
func GetIssuePRCreateFlags(requestCtx stdctx.Context, ctx *context.TeaContext) (*gitea.CreateIssueOption, error) {
|
||||
body, err := resolveCreateBody(
|
||||
ctx.String("description"),
|
||||
ctx.String("description-file"),
|
||||
ctx.IsSet("description-file"),
|
||||
stdinPiped(),
|
||||
ctx.Reader,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := gitea.CreateIssueOption{
|
||||
Title: ctx.String("title"),
|
||||
Body: body,
|
||||
Assignees: strings.Split(ctx.String("assignees"), ","),
|
||||
}
|
||||
|
||||
date := ctx.String("deadline")
|
||||
if date != "" {
|
||||
t, err := dateparse.ParseAny(date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.Deadline = &t
|
||||
}
|
||||
|
||||
client := ctx.Login.Client()
|
||||
|
||||
labelNames := strings.Split(ctx.String("labels"), ",")
|
||||
if len(labelNames) != 0 {
|
||||
if client == nil {
|
||||
client = ctx.Login.Client()
|
||||
}
|
||||
if opts.Labels, err = task.ResolveLabelNames(requestCtx, client, ctx.Owner, ctx.Repo, labelNames); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if milestoneName := ctx.String("milestone"); len(milestoneName) != 0 {
|
||||
if client == nil {
|
||||
client = ctx.Login.Client()
|
||||
}
|
||||
ms, _, err := client.Repositories.GetMilestoneByName(requestCtx, ctx.Owner, ctx.Repo, milestoneName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("milestone '%s' not found", milestoneName)
|
||||
}
|
||||
opts.Milestone = ms.ID
|
||||
}
|
||||
|
||||
return &opts, nil
|
||||
}
|
||||
|
||||
// IssuePREditFlags defines flags for editing properties of issues and PRs
|
||||
var IssuePREditFlags = append([]cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "set-assignees",
|
||||
Usage: "Clear all existing assignees and assign comma-separated list of usernames. Takes precedence over --add-assignees and --remove-assignees",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "add-assignees",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Comma-separated list of usernames to assign. Takes precedence over --remove-assignees",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "remove-assignees",
|
||||
Usage: "Comma-separated list of usernames to remove",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "add-labels",
|
||||
Aliases: []string{"L"},
|
||||
Usage: "Comma-separated list of labels to assign. Takes precedence over --remove-labels",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "remove-labels",
|
||||
Usage: "Comma-separated list of labels to remove",
|
||||
},
|
||||
}, issuePRFlags...)
|
||||
|
||||
// GetIssuePREditFlags parses all IssuePREditFlags
|
||||
func GetIssuePREditFlags(ctx *context.TeaContext) (*task.EditIssueOption, error) {
|
||||
opts := task.EditIssueOption{}
|
||||
if ctx.IsSet("title") {
|
||||
val := ctx.String("title")
|
||||
opts.Title = &val
|
||||
}
|
||||
body, err := resolveEditBody(
|
||||
ctx.String("description"),
|
||||
ctx.IsSet("description"),
|
||||
ctx.String("description-file"),
|
||||
ctx.IsSet("description-file"),
|
||||
ctx.Reader,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body != nil {
|
||||
opts.Body = body
|
||||
}
|
||||
if ctx.IsSet("referenced-version") {
|
||||
val := ctx.String("referenced-version")
|
||||
opts.Ref = &val
|
||||
}
|
||||
if ctx.IsSet("milestone") {
|
||||
val := ctx.String("milestone")
|
||||
opts.Milestone = &val
|
||||
}
|
||||
if ctx.IsSet("deadline") {
|
||||
date := ctx.String("deadline")
|
||||
if date == "" {
|
||||
opts.Deadline = &time.Time{}
|
||||
} else {
|
||||
t, err := dateparse.ParseAny(date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.Deadline = &t
|
||||
}
|
||||
}
|
||||
if ctx.IsSet("set-assignees") {
|
||||
val := ctx.String("set-assignees")
|
||||
opts.SetAssignees = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||
}
|
||||
if ctx.IsSet("add-assignees") {
|
||||
val := ctx.String("add-assignees")
|
||||
opts.AddAssignees = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||
}
|
||||
if ctx.IsSet("remove-assignees") {
|
||||
val := ctx.String("remove-assignees")
|
||||
opts.RemoveAssignees = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||
}
|
||||
if ctx.IsSet("add-labels") {
|
||||
val := ctx.String("add-labels")
|
||||
opts.AddLabels = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||
}
|
||||
if ctx.IsSet("remove-labels") {
|
||||
val := ctx.String("remove-labels")
|
||||
opts.RemoveLabels = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||
}
|
||||
return &opts, nil
|
||||
}
|
||||
Reference in new issue
Block a user