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

+34 -10
View File
@@ -80,7 +80,31 @@ func issueTemplate(ctx context.Context, service services.Issues, name string) (s
return services.IssueTemplate{}, fmt.Errorf("issue template %q does not exist; available: %s", name, strings.Join(names, ", "))
}
// validateMilestoneFlags checks --milestone and --milestone-name before any request.
// milestoneFlag reads -m/--milestone, a milestone title or ID, before any request: value is
// "" when the flag is absent, and remove reports 0 or --remove-milestone, which clear it.
func milestoneFlag(cmd *cobra.Command, value string) (selected string, remove bool, err error) {
remove, _ = cmd.Flags().GetBool("remove-milestone")
switch {
case !cmd.Flags().Changed("milestone"):
return "", remove, nil
case remove:
return "", false, clierr.Wrap(clierr.Usage, errors.New("--milestone and --remove-milestone cannot be combined"))
case strings.TrimSpace(value) == "":
return "", false, clierr.Wrap(clierr.Usage, errors.New("--milestone needs a title or ID; use --remove-milestone to clear it"))
case strings.TrimSpace(value) == "0":
return "", true, nil
}
return strings.TrimSpace(value), false, nil
}
// milestoneID resolves a milestone title or ID in the target repository.
func milestoneID(cmd *cobra.Command, t target, raw string) (int64, error) {
milestone, err := (services.Milestones{Client: t.client, Repository: t.Repo}).FindOne(cmd.Context(), raw, false)
return milestone.ID, err
}
// validateMilestoneFlags checks the --milestone ID and --milestone-name flags of pr create and pr
// edit before any request.
func validateMilestoneFlags(cmd *cobra.Command, name string, id int64) error {
if cmd.Flags().Changed("milestone-name") {
if cmd.Flags().Changed("milestone") {
@@ -301,15 +325,15 @@ func (a *application) issueCommand() *cobra.Command {
var useEditor bool
var ref string
var assignees, labelNames, attachmentPaths []string
var milestone int64
var milestoneName string
var milestone string
create := &cobra.Command{Use: "create", Short: "Create a repository issue", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) (runErr error) {
var draft terminal.Draft
defer finishDraft(&draft, &runErr)
if err := validateBodySources(cmd); err != nil {
return err
}
if err := validateMilestoneFlags(cmd, milestoneName, milestone); err != nil {
milestone, _, err := milestoneFlag(cmd, milestone)
if err != nil {
return err
}
if err := requireNames("label", "", labelNames); err != nil {
@@ -381,9 +405,9 @@ func (a *application) issueCommand() *cobra.Command {
return err
}
}
if cmd.Flags().Changed("milestone-name") {
milestone, err = (services.Milestones{Client: target.client, Repository: target.Repo}).ResolveName(cmd.Context(), milestoneName)
if err != nil {
var milestoneValue int64
if milestone != "" {
if milestoneValue, err = milestoneID(cmd, target, milestone); err != nil {
return err
}
}
@@ -397,7 +421,7 @@ func (a *application) issueCommand() *cobra.Command {
}
}
service := services.Issues{Client: target.client, Repository: target.Repo}
item, err := service.Create(cmd.Context(), services.IssueOptions{Title: title, Body: body, Deadline: due, Ref: ref, Assignees: assignees, Milestone: milestone, Labels: labelIDs})
item, err := service.Create(cmd.Context(), services.IssueOptions{Title: title, Body: body, Deadline: due, Ref: ref, Assignees: assignees, Milestone: milestoneValue, Labels: labelIDs})
if err != nil {
return err
}
@@ -416,8 +440,8 @@ func (a *application) issueCommand() *cobra.Command {
create.Flags().StringVar(&ref, "ref", "", "Referenced branch or tag")
create.Flags().StringSliceVar(&labelNames, "label", nil, "Apply labels by exact name; use repo: or org: to disambiguate")
create.Flags().StringSliceVar(&assignees, "assignee", nil, "Assign users by username")
create.Flags().StringVar(&milestoneName, "milestone-name", "", "Milestone by exact title; conflicts with --milestone")
create.Flags().Int64Var(&milestone, "milestone", 0, "Milestone ID")
create.Flags().StringVarP(&milestone, "milestone", "m", "", "Add the issue to a milestone by `title` or ID")
flagAliases(create, map[string]string{"milestone-name": "milestone"})
create.Flags().StringVar(&deadline, "deadline", "", "Due date: YYYY-MM-DD, RFC3339, or Unix timestamp")
create.Flags().StringVar(&body, "body", "", "Issue body")
create.Flags().StringVar(&bodyFile, "body-file", "", "Read body from a file, or - for stdin")