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:
bircni committed 2026-10-12 00:16:09 +02:00
1 parent 10417049a1
commit 2ba02cf915
10 files changed
+583 -194

No files matched your search

+24 -6
View File
@@ -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) {
+18 -5
View File
@@ -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.