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
+583
-194
No files matched your search
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user