mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
feat(label): address labels by name and add import and clone
label edit bug and label delete kind/feature failed with "invalid resource number", and label create took its name as --name and required --color. Like gh, label create takes the name as an argument and picks a random color without -c; label edit and delete take names or IDs (delete several at once, after a confirmation that names them), and -c/-d/-n/-f are the gh short flags. Without a name on a terminal, label create asks for the name, color and description. label list shows ID NAME COLOR DESCRIPTION FLAGS with a colored swatch and exclusive and archived labels flagged. Mutations report ✓ lines. label import reads a label file or stdin (label create --file still works), label clone copies another repository's labels, skipping those that exist unless --force, and label export writes to stdout by default or to -O/--output. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
10 files changed
+581
-192
No files matched your search
@@ -265,7 +265,7 @@ func TestCommandHelpShowsRelevantFlagsOnly(t *testing.T) {
|
||||
}
|
||||
out.Reset()
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "--help"}))
|
||||
require.Contains(t, out.String(), "\nFLAGS\n -L, --limit int Maximum number of results (1..1000) (default 30)\n --org string ")
|
||||
require.Contains(t, out.String(), "\nFLAGS\n -L, --limit int Maximum number of results (1..1000) (default 30)\n --org organization ")
|
||||
for _, args := range [][]string{{"version", "--help"}, {"alias", "list", "--help"}, {"auth", "status", "--help"}} {
|
||||
out.Reset()
|
||||
require.NoError(t, Execute(context.Background(), deps, args))
|
||||
@@ -424,11 +424,6 @@ const (
|
||||
issue attachment list
|
||||
issue attachment upload
|
||||
issue attachment view
|
||||
label create
|
||||
label delete
|
||||
label edit
|
||||
label export
|
||||
label list
|
||||
milestone close
|
||||
milestone create
|
||||
milestone delete
|
||||
@@ -454,7 +449,6 @@ const (
|
||||
`
|
||||
longsToWrite = `
|
||||
issue attachment
|
||||
label
|
||||
milestone
|
||||
milestone issue
|
||||
pr attachment
|
||||
|
||||
@@ -66,8 +66,8 @@ func TestLabelListAppliesColorAndPagesWithNoInput(t *testing.T) {
|
||||
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.Contains(t, pager.input, "\x1b[38;2;255;0;0m● ff0000\x1b[0m")
|
||||
require.Contains(t, pager.input, "\x1b[38;2;0;255;0m● 00ff00\x1b[0m")
|
||||
require.Equal(t, "PAGED:"+pager.input, out.String())
|
||||
require.Empty(t, diagnostics.String())
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func TestTeaPagerAndColorEnvironmentOverrides(t *testing.T) {
|
||||
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;5;196mbug\x1b[0m")
|
||||
require.Contains(t, pager.input, "\x1b[38;5;196m● ff0000\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"}}`, withoutANSI(out.String()))
|
||||
@@ -137,12 +137,12 @@ func TestColorAutoUsesStdoutTTY(t *testing.T) {
|
||||
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\tbug\tff0000\t\n", out.String())
|
||||
require.Equal(t, "1\tbug\tff0000\t\t\n", out.String())
|
||||
|
||||
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;5;196mbug\x1b[0m")
|
||||
require.Contains(t, out.String(), "\x1b[38;5;196m● ff0000\x1b[0m")
|
||||
}
|
||||
|
||||
func TestMultiItemHumanOutputPagesOnce(t *testing.T) {
|
||||
|
||||
@@ -7,18 +7,23 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/tea/internal/services"
|
||||
"gitea.dev/tea/internal/terminal"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (a *application) labelExportCommand(service func(*cobra.Command) (services.Labels, error)) *cobra.Command {
|
||||
func (a *application) labelExportCommand(scope labelScope) *cobra.Command {
|
||||
var output string
|
||||
command := &cobra.Command{Use: "export", Short: "Export all labels in the selected scope", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
command := &cobra.Command{Use: "export", Short: "Export labels to a label file", Long: `Write every label as a row of a label file, the format tea label import reads, to standard
|
||||
output or to a new file.`, Example: `$ tea label export > labels.txt
|
||||
$ tea label export --org acme --output labels.txt
|
||||
# Copy the labels of one repository to another
|
||||
$ tea label export -R alice/old | tea label import - -R alice/new`, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if output != "" {
|
||||
if err := validateDownloadDestination(output); err != nil {
|
||||
return err
|
||||
}
|
||||
labels, err := service(cmd)
|
||||
}
|
||||
labels, err := scope(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -42,6 +47,7 @@ func (a *application) labelExportCommand(service func(*cobra.Command) (services.
|
||||
return err
|
||||
})
|
||||
}}
|
||||
command.Flags().StringVar(&output, "output-file", "", "New export file, or - for stdout")
|
||||
command.Flags().StringVarP(&output, "output", "O", "", "Write to a new `file` instead of standard output")
|
||||
flagAliases(command, map[string]string{"output-file": "output"})
|
||||
return command
|
||||
}
|
||||
@@ -6,7 +6,9 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -16,6 +18,10 @@ func TestLabelCreateAndEditExclusiveArchived(t *testing.T) {
|
||||
calls := 0
|
||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
if calls > 0 && r.URL.Query().Get("page") == "1" {
|
||||
_, _ = w.Write([]byte(`[{"id":3,"name":"scoped","color":"00aa00"}]`))
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
return
|
||||
}
|
||||
@@ -83,3 +89,107 @@ func TestLabelCreateRefusesDuplicateNames(t *testing.T) {
|
||||
require.JSONEq(t, `{"id":11,"color":"000000"}`, out.String())
|
||||
require.Equal(t, []string{"PATCH /api/v1/repos/alice/project/labels/11"}, changed)
|
||||
}
|
||||
|
||||
func TestLabelsByName(t *testing.T) {
|
||||
var changed []string
|
||||
labels := `[{"id":11,"name":"bug","color":"ee0701"},{"id":12,"name":"kind/feature","color":"84b6eb","exclusive":true},{"id":13,"name":"2026","color":"cccccc"}]`
|
||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method + " " + r.URL.Path {
|
||||
case "GET /api/v1/repos/alice/project/labels", "GET /api/v1/repos/bob/source/labels":
|
||||
if r.URL.Query().Get("page") != "1" {
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api/v1/repos/bob/source/labels" {
|
||||
_, _ = w.Write([]byte(`[{"id":1,"name":"bug","color":"000000"},{"id":2,"name":"docs","color":"0000ff","description":"Docs"}]`))
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(labels))
|
||||
default:
|
||||
changed = append(changed, r.Method+" "+r.URL.Path+" "+requestFields(t, r, "name", "color", "description"))
|
||||
if r.Method == http.MethodDelete {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"id":20,"name":"docs","color":"0000ff"}`))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
deps, out, diagnostics := serverDeps(t, server.URL)
|
||||
deps.Getenv = serverEnv(server.URL, map[string]string{"NO_COLOR": "1"})
|
||||
deps.Streams.OutTTY = true
|
||||
run := func(args ...string) error {
|
||||
out.Reset()
|
||||
diagnostics.Reset()
|
||||
return Execute(context.Background(), deps, append(args, "-R", "alice/project", "--no-input"))
|
||||
}
|
||||
require.NoError(t, run("label", "create", "docs", "-c", "#0000FF", "-d", "Docs"))
|
||||
require.Equal(t, "✓ Created label \"docs\" in alice/project\n", diagnostics.String())
|
||||
require.NoError(t, run("label", "edit", "kind/feature", "-n", "kind/enhancement"))
|
||||
require.Equal(t, "✓ Edited label \"docs\" in alice/project\n", diagnostics.String())
|
||||
require.NoError(t, run("label", "edit", "2026", "--by-name", "-d", "Year"))
|
||||
require.NoError(t, run("label", "delete", "BUG", "12", "--yes"))
|
||||
require.Equal(t, "✓ Deleted label \"bug\" from alice/project\n✓ Deleted label \"kind/feature\" from alice/project\n", diagnostics.String())
|
||||
require.NoError(t, run("label", "clone", "bob/source"))
|
||||
require.Equal(t, "! Skipped label \"bug\" in alice/project: it exists already\n✓ Created label \"docs\" in alice/project\n", diagnostics.String())
|
||||
require.Equal(t, []string{
|
||||
"POST /api/v1/repos/alice/project/labels name=docs color=0000FF description=Docs",
|
||||
"PATCH /api/v1/repos/alice/project/labels/12 name=kind/enhancement color=<nil> description=<nil>",
|
||||
"PATCH /api/v1/repos/alice/project/labels/13 name=<nil> color=<nil> description=Year",
|
||||
"DELETE /api/v1/repos/alice/project/labels/11 ",
|
||||
"DELETE /api/v1/repos/alice/project/labels/12 ",
|
||||
"POST /api/v1/repos/alice/project/labels name=docs color=0000ff description=Docs",
|
||||
}, changed)
|
||||
for _, test := range []struct {
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{[]string{"label", "edit", "nope", "-c", "000000"}, `label "nope" not found in alice/project`},
|
||||
{[]string{"label", "edit", "bug"}, "nothing to edit"},
|
||||
{[]string{"label", "delete", "bug"}, `confirmation required to delete label "bug" from alice/project; rerun with --yes`},
|
||||
{[]string{"label", "create", "x", "-c", "red"}, "--color requires six hexadecimal digits"},
|
||||
} {
|
||||
require.ErrorContains(t, run(test.args...), test.want, test.args)
|
||||
}
|
||||
}
|
||||
|
||||
// requestFields lists the JSON request body's named fields as name=value, empty without a body.
|
||||
func requestFields(t *testing.T, r *http.Request, names ...string) string {
|
||||
t.Helper()
|
||||
if r.ContentLength == 0 {
|
||||
return ""
|
||||
}
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&payload))
|
||||
fields := make([]string, len(names))
|
||||
for i, name := range names {
|
||||
fields[i] = fmt.Sprintf("%s=%v", name, payload[name])
|
||||
}
|
||||
return strings.Join(fields, " ")
|
||||
}
|
||||
|
||||
func TestLabelCreatePicksAColorAndPrompts(t *testing.T) {
|
||||
var created []map[string]any
|
||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
return
|
||||
}
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&payload))
|
||||
created = append(created, payload)
|
||||
_, _ = w.Write([]byte(`{"id":1,"name":"x"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
deps, _, _ := serverDeps(t, server.URL)
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"label", "create", "triage", "-R", "alice/project"}))
|
||||
require.Contains(t, labelColors, created[0]["color"])
|
||||
prompt := &askingPrompter{scriptedPrompter: scriptedPrompter{inputs: map[string]string{"Label name": "prompted", "Color (hex, e.g. d73a4a; empty = random):": "#00AA00", "Description (optional):": "From a prompt"}}}
|
||||
deps.Prompter = prompt
|
||||
deps.Streams.InTTY, deps.Streams.ErrTTY = true, true
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"label", "create", "-R", "alice/project"}))
|
||||
require.Equal(t, "prompted", created[1]["name"])
|
||||
require.Equal(t, "00AA00", created[1]["color"])
|
||||
require.Equal(t, "From a prompt", created[1]["description"])
|
||||
require.Equal(t, []string{"Label name", "Color (hex, e.g. d73a4a; empty = random):", "Description (optional):"}, prompt.asked)
|
||||
}
|
||||
@@ -86,5 +86,5 @@ func TestLabelImportAcceptsColorPrefixedRows(t *testing.T) {
|
||||
require.Len(t, created, 2)
|
||||
requirePayload(t, `{"name":"bug","color":"ee0701","description":"Something is not working"}`, strings.NewReader(created[0]))
|
||||
requirePayload(t, `{"name":"enhancement","color":"84b6eb"}`, strings.NewReader(created[1]))
|
||||
require.Equal(t, "1\tbug\t\t\n2\tenhancement\t\t\n", out.String())
|
||||
require.Empty(t, out.String())
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/tea/internal/clierr"
|
||||
"gitea.dev/tea/internal/services"
|
||||
"gitea.dev/tea/internal/terminal"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// labelScope returns the labels a label command manages: the repository's, or those of --org.
|
||||
type labelScope func(*cobra.Command) (services.Labels, error)
|
||||
|
||||
// labelColors are the colors of new labels created without --color, from Gitea's default labels.
|
||||
var labelColors = []string{"ee0701", "fbca04", "84b6eb", "cc317c", "c2e0c6", "0e8a16", "1d76db", "5319e7", "d4c5f9", "e99695", "bfd4f2", "006b75"}
|
||||
|
||||
// labelArguments is the ARGUMENTS help of commands that take labels.
|
||||
const labelArguments = `A label can be supplied by its name, such as "bug" or "kind/feature", or by its ID. A
|
||||
number is an ID first, then a name.`
|
||||
|
||||
func (a *application) labelCommand() *cobra.Command {
|
||||
group := &cobra.Command{Use: "label", Short: "Manage labels", Long: `Work with the labels of a repository or, with --org, of an organization. Labels are
|
||||
addressed by name or ID.`}
|
||||
var org string
|
||||
group.PersistentFlags().StringVar(&org, "org", "", "Manage the labels of an `organization` instead of a repository")
|
||||
_ = group.RegisterFlagCompletionFunc("org", a.completeOrgs())
|
||||
scope := func(cmd *cobra.Command) (services.Labels, error) {
|
||||
if org != "" && a.options.repo != "" {
|
||||
return services.Labels{}, clierr.Wrap(clierr.Usage, errors.New("--org and --repo are mutually exclusive"))
|
||||
}
|
||||
t, err := a.resolve(cmd.Context(), org == "")
|
||||
return services.Labels{Client: t.client, Repository: t.Repo, Organization: org}, err
|
||||
}
|
||||
complete := a.completeScopedLabels(&org)
|
||||
group.AddCommand(a.labelListCommand(scope), a.labelCreateCommand(scope), a.labelEditCommand(scope, complete), a.labelDeleteCommand(scope, complete),
|
||||
a.labelImportCommand(scope), a.labelCloneCommand(scope), a.labelExportCommand(scope))
|
||||
return group
|
||||
}
|
||||
|
||||
func (a *application) labelListCommand(scope labelScope) *cobra.Command {
|
||||
var limit, page int
|
||||
cmd := &cobra.Command{Use: "list", Short: "List labels", Example: `$ tea label list
|
||||
$ tea label list --org acme
|
||||
$ tea label list --json name,color`, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
s, err := scope(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := s.ListPage(cmd.Context(), limit, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeTable(a, cmd, items.Items, listSpec[services.Label]{
|
||||
Columns: []terminal.Column{{Header: "ID"}, {Header: "Name"}, {Header: "Color", Prefix: "● "}, {Header: "Description", Flex: true}, {Header: "Flags"}},
|
||||
Row: labelRow, Empty: "no labels in " + s.Scope(), Context: "labels in " + s.Scope(),
|
||||
}, servicePage(items, page))
|
||||
}}
|
||||
pageFlags(cmd, &limit, &page)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func labelRow(item services.Label) []terminal.Cell {
|
||||
var flags []string
|
||||
if item.Exclusive {
|
||||
flags = append(flags, "exclusive")
|
||||
}
|
||||
if item.Archived {
|
||||
flags = append(flags, "archived")
|
||||
}
|
||||
return []terminal.Cell{
|
||||
{Text: strconv.FormatInt(item.ID, 10)},
|
||||
{Text: item.Name},
|
||||
{Text: item.Color, Color: item.Color},
|
||||
{Text: item.Description},
|
||||
{Text: strings.Join(flags, ", ")},
|
||||
}
|
||||
}
|
||||
|
||||
// labelName names a label in messages: `label "bug" in alice/x`.
|
||||
func labelName(s services.Labels, item services.Label) string {
|
||||
return fmt.Sprintf("label %q in %s", item.Name, s.Scope())
|
||||
}
|
||||
|
||||
// labelFields are the fields of label create and edit.
|
||||
type labelFields struct {
|
||||
name, color, description string
|
||||
exclusive, archived bool
|
||||
}
|
||||
|
||||
func (f *labelFields) register(cmd *cobra.Command, nameUsage string) {
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&f.color, "color", "c", "", "Color as six hexadecimal `digits`, such as d73a4a")
|
||||
flags.StringVarP(&f.description, "description", "d", "", "Description of the label")
|
||||
flags.BoolVar(&f.exclusive, "exclusive", false, "Allow only one label of its scope, such as priority/high, per issue")
|
||||
flags.BoolVar(&f.archived, "archived", false, "Archive the label so that it cannot be applied anymore")
|
||||
flags.StringVarP(&f.name, "name", "n", "", nameUsage)
|
||||
}
|
||||
|
||||
// options returns the fields that were passed, checking them before any request.
|
||||
func (f *labelFields) options(cmd *cobra.Command) (services.LabelOptions, error) {
|
||||
var opts services.LabelOptions
|
||||
if cmd.Flags().Changed("name") {
|
||||
if strings.TrimSpace(f.name) == "" {
|
||||
return opts, clierr.Wrap(clierr.Usage, errors.New("label name cannot be empty"))
|
||||
}
|
||||
opts.Name = &f.name
|
||||
}
|
||||
if cmd.Flags().Changed("color") {
|
||||
f.color = strings.TrimPrefix(f.color, "#")
|
||||
if !labelColor.MatchString(f.color) {
|
||||
return opts, clierr.Wrap(clierr.Usage, errors.New("--color requires six hexadecimal digits, such as d73a4a"))
|
||||
}
|
||||
opts.Color = &f.color
|
||||
}
|
||||
if cmd.Flags().Changed("description") {
|
||||
opts.Description = &f.description
|
||||
}
|
||||
if cmd.Flags().Changed("exclusive") {
|
||||
opts.Exclusive = &f.exclusive
|
||||
}
|
||||
if cmd.Flags().Changed("archived") {
|
||||
opts.Archived = &f.archived
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func (a *application) labelCreateCommand(scope labelScope) *cobra.Command {
|
||||
var fields labelFields
|
||||
var file string
|
||||
var force bool
|
||||
cmd := &cobra.Command{Use: "create <name>", Aliases: []string{"new"}, Short: "Create a label", Long: `Create a label. Without --color, the label gets a random color. A label with the same
|
||||
name, ignoring case, is refused unless --force updates it. Without a name on a terminal,
|
||||
prompts ask for the name, color and description.`, Example: `$ tea label create bug --color d73a4a --description "Something is broken"
|
||||
$ tea label create "priority/high" --exclusive
|
||||
# Update the label if it exists
|
||||
$ tea label create bug -c ee0701 --force`, Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if file != "" {
|
||||
return a.importLabels(cmd, scope, file, force)
|
||||
}
|
||||
if len(args) == 1 {
|
||||
if cmd.Flags().Changed("name") {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("pass the label name as an argument or --name, not both"))
|
||||
}
|
||||
if err := cmd.Flags().Set("name", args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
opts, err := fields.options(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prompted, err := a.requireInput(cmd, &fields.name, "name", "Label name", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Name = &fields.name
|
||||
if prompted {
|
||||
if err := a.askLabelDetails(cmd, &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if opts.Color == nil {
|
||||
color := labelColors[rand.IntN(len(labelColors))]
|
||||
opts.Color = &color
|
||||
}
|
||||
s, err := scope(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existing, err := s.All(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item, updated, err := s.CreateUnique(cmd.Context(), existing, opts, force)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.reportLabel(cmd, s, item, updated)
|
||||
}}
|
||||
fields.register(cmd, "Label `name`")
|
||||
_ = cmd.Flags().MarkHidden("name")
|
||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "Update the label if one with the same name exists")
|
||||
cmd.Flags().StringVar(&file, "file", "", "Import labels from a `file`, like tea label import")
|
||||
_ = cmd.Flags().MarkHidden("file")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// askLabelDetails asks for the optional color and description of a label whose name was prompted.
|
||||
func (a *application) askLabelDetails(cmd *cobra.Command, opts *services.LabelOptions) error {
|
||||
asker, ok := a.deps.Prompter.(terminal.Asker)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if opts.Color == nil {
|
||||
color, err := asker.Ask(cmd.Context(), "Color (hex, e.g. d73a4a; empty = random):", func(s string) error {
|
||||
if s != "" && !labelColor.MatchString(strings.TrimPrefix(s, "#")) {
|
||||
return errors.New("use six hexadecimal digits")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if color = strings.TrimPrefix(color, "#"); color != "" {
|
||||
opts.Color = &color
|
||||
}
|
||||
}
|
||||
if opts.Description == nil {
|
||||
description, err := asker.Ask(cmd.Context(), "Description (optional):", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Description = &description
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// reportLabel reports a created or updated label as JSON or with a ✓ line.
|
||||
func (a *application) reportLabel(cmd *cobra.Command, s services.Labels, item services.Label, updated bool) error {
|
||||
if a.options.json != "" {
|
||||
return a.writeJSON(cmd, item)
|
||||
}
|
||||
verb := "Created"
|
||||
if updated {
|
||||
verb = "Updated"
|
||||
}
|
||||
a.done(cmd, "%s %s", verb, labelName(s, item))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *application) labelEditCommand(scope labelScope, complete cobra.CompletionFunc) *cobra.Command {
|
||||
var fields labelFields
|
||||
var byName bool
|
||||
cmd := &cobra.Command{Use: "edit <label>", Short: "Edit a label", Example: `$ tea label edit bug --name defect --color b60205
|
||||
$ tea label edit "kind/question" --description "Further information is requested"
|
||||
$ tea label edit old-stuff --archived`, Annotations: map[string]string{"tea.arguments": labelArguments}, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts, err := fields.options(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts == (services.LabelOptions{}) {
|
||||
return errNothingToEdit
|
||||
}
|
||||
s, err := scope(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found, err := s.Find(cmd.Context(), args, byName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item, err := s.Edit(cmd.Context(), found[0].ID, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a.options.json != "" {
|
||||
return a.writeJSON(cmd, item)
|
||||
}
|
||||
a.done(cmd, "Edited %s", labelName(s, item))
|
||||
return nil
|
||||
}}
|
||||
fields.register(cmd, "Rename the label")
|
||||
cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a label name rather than an ID")
|
||||
cmd.ValidArgsFunction = complete
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a *application) labelDeleteCommand(scope labelScope, complete cobra.CompletionFunc) *cobra.Command {
|
||||
var byName bool
|
||||
cmd := &cobra.Command{Use: "delete <label>...", Short: "Delete labels", Example: `$ tea label delete bug
|
||||
$ tea label delete wontfix duplicate --yes`, Annotations: map[string]string{"tea.text": "label deletion does not return a resource", "tea.confirm": "true", "tea.arguments": labelArguments}, Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
s, err := scope(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found, err := s.Find(cmd.Context(), args, byName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
names := make([]string, len(found))
|
||||
for i, item := range found {
|
||||
names[i] = strconv.Quote(item.Name)
|
||||
}
|
||||
subject := "label " + names[0]
|
||||
if len(found) > 1 {
|
||||
subject = fmt.Sprintf("%d labels (%s)", len(found), strings.Join(names, ", "))
|
||||
}
|
||||
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Delete " + subject + " from " + s.Scope() + "?", NonTTY: "delete " + subject + " from " + s.Scope()}); err != nil {
|
||||
return err
|
||||
}
|
||||
return batchDone(a, cmd, found, func(item services.Label) string { return "label " + strconv.Quote(item.Name) }, func(item services.Label) (services.Label, error) {
|
||||
return item, s.Delete(cmd.Context(), item.ID)
|
||||
}, func(item services.Label) string { return fmt.Sprintf("Deleted label %q from %s", item.Name, s.Scope()) })
|
||||
}}
|
||||
cmd.Flags().BoolVar(&byName, "by-name", false, "Match numeric arguments as label names rather than IDs")
|
||||
cmd.ValidArgsFunction = complete
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a *application) labelImportCommand(scope labelScope) *cobra.Command {
|
||||
var force bool
|
||||
cmd := &cobra.Command{Use: "import <file>", Short: "Create labels from a file", Long: `Create the labels of a label file, one per line: "#color name", optionally followed by
|
||||
" ;description" and " ;exclusive", " ;archived" or both. tea label export writes this
|
||||
format, and Gitea's label templates use it. Existing labels are refused unless --force
|
||||
updates them.`, Example: `$ tea label import labels.txt
|
||||
$ tea label export --repo alice/old | tea label import - --repo alice/new`, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return a.importLabels(cmd, scope, args[0], force)
|
||||
}}
|
||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "Update labels that exist already")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// importLabels creates the labels of a label file, or of stdin for "-".
|
||||
func (a *application) importLabels(cmd *cobra.Command, scope labelScope, file string, force bool) error {
|
||||
data, err := readFileOrStdin(cmd.InOrStdin(), file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := parseLabelFile(strings.NewReader(string(data)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, err := scope(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.createLabels(cmd, s, rows, force, false)
|
||||
}
|
||||
|
||||
// createLabels creates each label in rows, continuing past failures. A label that exists already
|
||||
// is updated with force, skipped with skip, and fails otherwise.
|
||||
func (a *application) createLabels(cmd *cobra.Command, s services.Labels, rows []services.LabelOptions, force, skip bool) error {
|
||||
existing, err := s.All(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outcome := map[string]string{}
|
||||
results, failures := applyBatch(cmd, rows, func(row services.LabelOptions) string { return "label " + strconv.Quote(*row.Name) }, func(row services.LabelOptions) (services.Label, error) {
|
||||
item, updated, err := s.CreateUnique(cmd.Context(), existing, row, force)
|
||||
var conflict *services.ConflictError
|
||||
switch {
|
||||
case skip && errors.As(err, &conflict) && conflict.Reason == services.ReasonExists:
|
||||
outcome[*row.Name] = "Skipped"
|
||||
return services.Label{Name: *row.Name}, nil
|
||||
case err != nil:
|
||||
return item, err
|
||||
case updated:
|
||||
outcome[item.Name] = "Updated"
|
||||
default:
|
||||
outcome[item.Name] = "Created"
|
||||
existing = append(existing, item)
|
||||
}
|
||||
return item, nil
|
||||
}, func(item services.Label) {
|
||||
if outcome[item.Name] == "Skipped" {
|
||||
a.warn(cmd, "Skipped %s: it exists already", labelName(s, item))
|
||||
return
|
||||
}
|
||||
a.done(cmd, "%s %s", outcome[item.Name], labelName(s, item))
|
||||
})
|
||||
if a.options.json != "" {
|
||||
failures = append(failures, writeBatchJSON(a, cmd, len(rows), results))
|
||||
}
|
||||
return errors.Join(failures...)
|
||||
}
|
||||
|
||||
func (a *application) labelCloneCommand(scope labelScope) *cobra.Command {
|
||||
var force bool
|
||||
cmd := &cobra.Command{Use: "clone <source-repository>", Short: "Copy the labels of another repository", Long: `Create the labels of the source repository in the selected repository or organization.
|
||||
Labels that exist already are skipped unless --force updates them.`, Example: `$ tea label clone alice/project
|
||||
$ tea label clone alice/project --repo alice/new --force`, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
s, err := scope(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
source, err := (services.Labels{Client: s.Client, Repository: args[0]}).All(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows := make([]services.LabelOptions, len(source))
|
||||
for i, item := range source {
|
||||
rows[i] = services.LabelOptions{Name: &item.Name, Color: &item.Color, Description: &item.Description, Exclusive: &item.Exclusive}
|
||||
}
|
||||
return a.createLabels(cmd, s, rows, force, true)
|
||||
}}
|
||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "Update labels that exist already")
|
||||
cmd.ValidArgsFunction = a.completeRepos()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// completeScopedLabels completes the label names of the repository or, with --org, of the
|
||||
// organization that *org names.
|
||||
func (a *application) completeScopedLabels(org *string) cobra.CompletionFunc {
|
||||
return a.apiCompletion(false, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
|
||||
if *org == "" && t.Repo == "" {
|
||||
return nil, errRepositoryRequired
|
||||
}
|
||||
items, err := (services.Labels{Client: t.client, Repository: t.Repo, Organization: *org}).List(ctx, completionLimit, 1)
|
||||
return describe(items, func(item services.Label) (string, string) { return item.Name, item.Description }), err
|
||||
})
|
||||
}
|
||||
@@ -5,8 +5,6 @@ package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -17,162 +15,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (a *application) labelCommand() *cobra.Command {
|
||||
group := &cobra.Command{Use: "label", Short: "Manage repository or organization labels"}
|
||||
var org string
|
||||
group.PersistentFlags().StringVar(&org, "org", "", "Manage organization labels instead of repository labels")
|
||||
service := func(cmd *cobra.Command) (services.Labels, error) {
|
||||
if org != "" && a.options.repo != "" {
|
||||
return services.Labels{}, clierr.Wrap(clierr.Usage, errors.New("--org and --repo are mutually exclusive"))
|
||||
}
|
||||
t, err := a.resolve(cmd.Context(), org == "")
|
||||
return services.Labels{Client: t.client, Repository: t.Repo, Organization: org}, err
|
||||
}
|
||||
group.AddCommand(a.labelExportCommand(service))
|
||||
var limit, page int
|
||||
list := &cobra.Command{Use: "list", Short: "List labels in the selected scope", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
s, err := service(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := s.List(cmd.Context(), limit, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeList(a, cmd, items, labelRow)
|
||||
}}
|
||||
pageFlags(list, &limit, &page)
|
||||
group.AddCommand(list)
|
||||
for _, verb := range []string{"create", "edit"} {
|
||||
var name, color, description, file string
|
||||
var exclusive, archived, force bool
|
||||
command := &cobra.Command{Use: verb, Short: titleCase(verb) + " a label"}
|
||||
if verb == "edit" {
|
||||
command.Use += " <id>"
|
||||
command.Args = argsExact(1)
|
||||
} else {
|
||||
command.Args = argsExact(0)
|
||||
}
|
||||
command.Flags().StringVar(&name, "name", "", "Label name")
|
||||
command.Flags().StringVar(&color, "color", "", "Six-digit RGB color")
|
||||
command.Flags().StringVar(&description, "description", "", "Label description")
|
||||
command.Flags().BoolVar(&exclusive, "exclusive", false, "Only one label from this exclusive group may be applied")
|
||||
command.Flags().BoolVar(&archived, "archived", false, "Archive the label so it cannot be newly applied")
|
||||
if verb == "create" {
|
||||
command.Flags().StringVar(&file, "file", "", "Import color/name rows with optional ;description")
|
||||
command.Flags().BoolVar(&force, "force", false, "Update the label if one with the same name exists")
|
||||
}
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
opts := services.LabelOptions{}
|
||||
var imported []services.LabelOptions
|
||||
var id int64
|
||||
if verb == "edit" {
|
||||
var err error
|
||||
id, err = number(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if file != "" {
|
||||
if cmd.Flags().Changed("name") || cmd.Flags().Changed("color") || cmd.Flags().Changed("description") || cmd.Flags().Changed("exclusive") || cmd.Flags().Changed("archived") {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("--file cannot be combined with label fields"))
|
||||
}
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if imported, err = parseLabelFile(f); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if verb == "create" {
|
||||
if _, err := a.require(cmd, &name, "name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := a.require(cmd, &color, "color"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if verb == "create" || cmd.Flags().Changed("name") {
|
||||
if name == "" {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("label name cannot be empty"))
|
||||
}
|
||||
opts.Name = &name
|
||||
}
|
||||
if verb == "create" || cmd.Flags().Changed("color") {
|
||||
color = strings.TrimPrefix(color, "#")
|
||||
if !labelColor.MatchString(color) {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("--color requires six hexadecimal digits"))
|
||||
}
|
||||
opts.Color = &color
|
||||
}
|
||||
if cmd.Flags().Changed("description") {
|
||||
opts.Description = &description
|
||||
}
|
||||
if cmd.Flags().Changed("exclusive") {
|
||||
opts.Exclusive = &exclusive
|
||||
}
|
||||
if cmd.Flags().Changed("archived") {
|
||||
opts.Archived = &archived
|
||||
}
|
||||
if opts.Name == nil && opts.Color == nil && opts.Description == nil && opts.Exclusive == nil && opts.Archived == nil {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("supply label fields to edit"))
|
||||
}
|
||||
}
|
||||
s, err := service(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var existing []services.Label
|
||||
if verb == "create" {
|
||||
if existing, err = s.All(cmd.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if file != "" {
|
||||
results := []services.Label{}
|
||||
var failures []error
|
||||
for _, row := range imported {
|
||||
item, err := s.CreateUnique(cmd.Context(), existing, row, force)
|
||||
if err != nil {
|
||||
failures = append(failures, fmt.Errorf("label %s: %w", *row.Name, err))
|
||||
continue
|
||||
}
|
||||
results, existing = append(results, item), append(existing, item)
|
||||
}
|
||||
return errors.Join(append(failures, writeList(a, cmd, results, labelRow))...)
|
||||
}
|
||||
var item services.Label
|
||||
if verb == "create" {
|
||||
item, err = s.CreateUnique(cmd.Context(), existing, opts, force)
|
||||
} else {
|
||||
item, err = s.Edit(cmd.Context(), id, opts)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeItem(a, cmd, item, labelRow)
|
||||
}
|
||||
group.AddCommand(command)
|
||||
}
|
||||
group.AddCommand(&cobra.Command{Use: "delete <id>", Short: "Delete a label", Annotations: textOutput("label deletion does not return a resource"), Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
id, err := number(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, err := service(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.confirm(cmd, "Delete label "+args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Delete(cmd.Context(), id)
|
||||
}})
|
||||
return group
|
||||
}
|
||||
|
||||
func (a *application) milestoneCommand() *cobra.Command {
|
||||
group := &cobra.Command{Use: "milestone", Short: "Manage repository milestones"}
|
||||
service := func(cmd *cobra.Command) (services.Milestones, error) {
|
||||
@@ -343,10 +185,6 @@ func (a *application) milestoneCommand() *cobra.Command {
|
||||
return group
|
||||
}
|
||||
|
||||
func labelRow(item services.Label) []terminal.Cell {
|
||||
return []terminal.Cell{{Text: strconv.FormatInt(item.ID, 10)}, {Text: item.Name, Color: item.Color}, {Text: item.Color, Color: item.Color}, {Text: item.Description}}
|
||||
}
|
||||
|
||||
func milestoneRow(item services.Milestone) []terminal.Cell {
|
||||
return []terminal.Cell{{Text: strconv.FormatInt(item.ID, 10)}, {Text: item.Title}, {Text: item.State, Color: stateColors[item.State]}}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func TestUsageErrorsPrecedeRequests(t *testing.T) {
|
||||
{[]string{"issue", "list", "--page", "0"}, `"--page" flag: must be a positive integer`},
|
||||
{[]string{"issue", "list", "--label", "bug, "}, "label names cannot be empty"},
|
||||
{[]string{"label", "create", "--name", "bug", "--color", "wrong"}, "--color requires six hexadecimal digits"},
|
||||
{[]string{"label", "edit", "1"}, "supply label fields to edit"},
|
||||
{[]string{"label", "edit", "1"}, "nothing to edit; pass a flag such as --title (see --help)"},
|
||||
{[]string{"label", "list", "--org", "team", "-R", "a/b"}, "--org and --repo are mutually exclusive"},
|
||||
{[]string{"milestone", "close", " ", "--by-name"}, "milestone title or ID cannot be empty"},
|
||||
{[]string{"milestone", "create", "--title", "v1", "--deadline", "invalid"}, "invalid --deadline"},
|
||||
|
||||
@@ -25,23 +25,41 @@ func (s Labels) finder(byName bool) NameOrID[Label] {
|
||||
return NameOrID[Label]{Kind: "label", Scope: s.Scope(), ID: func(l Label) int64 { return l.ID }, Name: func(l Label) string { return l.Name }, ByName: byName}
|
||||
}
|
||||
|
||||
// Find returns the labels of the scope that raws address, each by ID or name (D10); byName matches
|
||||
// numeric values as names only.
|
||||
func (s Labels) Find(ctx context.Context, raws []string, byName bool) ([]Label, error) {
|
||||
items, err := s.All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]Label, len(raws))
|
||||
for i, raw := range raws {
|
||||
if result[i], err = s.finder(byName).Find(items, raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateUnique creates a label unless existing, the labels of the scope, has one with its name,
|
||||
// ignoring case. Then update edits that label with the supplied fields, keeping its name, and
|
||||
// otherwise CreateUnique fails: Gitea would create a second label that name filters cannot tell
|
||||
// apart.
|
||||
func (s Labels) CreateUnique(ctx context.Context, existing []Label, opts LabelOptions, update bool) (Label, error) {
|
||||
// apart. It reports whether it updated a label.
|
||||
func (s Labels) CreateUnique(ctx context.Context, existing []Label, opts LabelOptions, update bool) (Label, bool, error) {
|
||||
current, err := s.finder(true).Find(existing, deref(opts.Name))
|
||||
if errors.As(err, new(*NotFoundError)) {
|
||||
return s.Create(ctx, opts)
|
||||
label, err := s.Create(ctx, opts)
|
||||
return label, false, err
|
||||
}
|
||||
if err != nil {
|
||||
return Label{}, err
|
||||
return Label{}, false, err
|
||||
}
|
||||
if !update {
|
||||
return Label{}, &ConflictError{Reason: ReasonExists, Kind: "label", Key: strconv.Quote(current.Name), Scope: s.Scope(), Hint: "pass --force to update it"}
|
||||
return Label{}, false, &ConflictError{Reason: ReasonExists, Kind: "label", Key: strconv.Quote(current.Name), Scope: s.Scope(), Hint: "pass --force to update it"}
|
||||
}
|
||||
opts.Name = nil
|
||||
return s.Edit(ctx, current.ID, opts)
|
||||
label, err := s.Edit(ctx, current.ID, opts)
|
||||
return label, true, err
|
||||
}
|
||||
|
||||
func selectLabelNames(scope string, names []string, matches map[string][]int64) ([]int64, error) {
|
||||
|
||||
@@ -45,7 +45,24 @@ func (s Labels) orgEndpoint() string { return "orgs/" + url.PathEscape(s.Organiz
|
||||
|
||||
// List returns scoped labels without merging unrelated organization labels.
|
||||
func (s Labels) List(ctx context.Context, limit, page int) ([]Label, error) {
|
||||
labels, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Label, *gitea.Response, error) {
|
||||
labels, _, err := s.list(ctx, limit, page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertAll(labels, labelFrom), nil
|
||||
}
|
||||
|
||||
// ListPage is List with the number of labels in the scope, as the server counts them.
|
||||
func (s Labels) ListPage(ctx context.Context, limit, page int) (Page[Label], error) {
|
||||
labels, err := listPage(limit, page, func(limit, page int) ([]*gitea.Label, http.Header, error) {
|
||||
labels, resp, err := s.list(ctx, limit, page)
|
||||
return labels, headerOf(resp), err
|
||||
})
|
||||
return Page[Label]{Items: convertAll(labels.Items, labelFrom), More: labels.More, Total: labels.Total, Counted: labels.Counted}, err
|
||||
}
|
||||
|
||||
func (s Labels) list(ctx context.Context, limit, page int) ([]*gitea.Label, *gitea.Response, error) {
|
||||
return call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Label, *gitea.Response, error) {
|
||||
if s.Organization != "" {
|
||||
return c.Organizations.ListOrgLabels(ctx, s.Organization, gitea.ListOrgLabelsOptions{ListOptions: listOptions(limit, page)})
|
||||
}
|
||||
@@ -55,10 +72,6 @@ func (s Labels) List(ctx context.Context, limit, page int) ([]Label, error) {
|
||||
}
|
||||
return c.Repositories.ListRepoLabels(ctx, owner, repo, gitea.ListLabelsOptions{ListOptions: listOptions(limit, page)})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertAll(labels, labelFrom), nil
|
||||
}
|
||||
|
||||
// All returns every label in the scope.
|
||||
|
||||
Reference in new issue
Block a user