fix(milestone): address milestones by title or ID everywhere

milestone view v2.0 failed with "invalid resource number" although its
usage promised <id-or-name>, and issue create --milestone v1.0 failed
with a strconv error. Every milestone argument (view, edit, delete,
close, reopen, milestone issue add|remove|list) and issue create|edit
-m/--milestone now accept a title or an ID: a number is an ID first,
then a title; --by-name matches numeric titles only. --milestone-name
stays as a hidden name of --milestone, and issue edit gains
--remove-milestone (as does --milestone 0).

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-12 00:14:30 +02:00
1 parent 0d013746b5
commit 1035ff551b
12 files changed
+220 -147

No files matched your search

+15 -36
View File
@@ -5,6 +5,8 @@ package app
import (
"errors"
"slices"
"strings"
"gitea.dev/tea/internal/clierr"
"gitea.dev/tea/internal/services"
@@ -13,54 +15,31 @@ import (
func (a *application) milestoneStateCommand(verb string, service func(*cobra.Command) (services.Milestones, error)) *cobra.Command {
var byName bool
cmd := &cobra.Command{Use: verb + " <id-or-name>...", Short: titleCase(verb) + " one or more milestones", Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
ids := make(map[string]int64, len(args))
seenIDs := map[int64]bool{}
selectID := func(argument string, id int64) error {
if seenIDs[id] {
return clierr.Wrap(clierr.Usage, errors.New("selectors resolve to the same milestone"))
}
seenIDs[id], ids[argument] = true, id
return nil
}
for _, argument := range args {
if _, seen := ids[argument]; seen {
return clierr.Wrap(clierr.Usage, errors.New("milestone selectors must be unique"))
}
id, err := milestoneSelector(argument, byName)
if err != nil {
return err
}
ids[argument] = 0
if !byName {
if err := selectID(argument, id); err != nil {
return err
}
}
cmd := &cobra.Command{Use: verb + " <milestone>...", Short: titleCase(verb) + " one or more milestones", Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
if slices.ContainsFunc(args, func(arg string) bool { return strings.TrimSpace(arg) == "" }) {
return clierr.Wrap(clierr.Usage, errors.New("milestone title or ID cannot be empty"))
}
milestones, err := service(cmd)
if err != nil {
return err
}
if byName {
for _, argument := range args {
id, err := milestones.ResolveName(cmd.Context(), argument)
if err != nil {
return err
}
if err := selectID(argument, id); err != nil {
return err
}
selected, err := milestones.Find(cmd.Context(), args, byName)
if err != nil {
return err
}
for i, milestone := range selected {
if slices.ContainsFunc(selected[:i], func(other services.Milestone) bool { return other.ID == milestone.ID }) {
return clierr.Wrap(clierr.Usage, errors.New("milestone "+args[i]+" is selected more than once"))
}
}
state := "closed"
if verb == "reopen" {
state = "open"
}
return batch(a, cmd, args, func(argument string) string { return "milestone " + argument }, func(argument string) (services.Milestone, error) {
return milestones.Edit(cmd.Context(), ids[argument], services.MilestoneOptions{State: &state})
return batch(a, cmd, selected, func(milestone services.Milestone) string { return "milestone " + milestone.Title }, func(milestone services.Milestone) (services.Milestone, error) {
return milestones.Edit(cmd.Context(), milestone.ID, services.MilestoneOptions{State: &state})
}, milestoneRow)
}}
cmd.Flags().BoolVar(&byName, "by-name", false, "Select by exact milestone titles instead of IDs")
cmd.Flags().BoolVar(&byName, "by-name", false, "Match numeric arguments as milestone titles rather than IDs")
return cmd
}