// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package app import ( "errors" "strconv" "strings" "time" "gitea.dev/tea/internal/clierr" "gitea.dev/tea/internal/services" "gitea.dev/tea/internal/terminal" "github.com/spf13/cobra" ) func (a *application) milestoneCommand() *cobra.Command { group := &cobra.Command{Use: "milestone", Short: "Manage repository milestones"} service := func(cmd *cobra.Command) (services.Milestones, error) { t, err := a.resolve(cmd.Context(), true) return services.Milestones{Client: t.client, Repository: t.Repo}, err } var limit, page int var state string list := &cobra.Command{Use: "list", Short: "List milestones", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error { if err := oneOf("state", state, "open", "closed", "all"); err != nil { return err } s, err := service(cmd) if err != nil { return err } items, err := s.List(cmd.Context(), state, limit, page) if err != nil { return err } return writeList(a, cmd, items, milestoneRow) }} pageFlags(list, &limit, &page) list.Flags().StringVar(&state, "state", "open", "open, closed, all") group.AddCommand(list) for _, verb := range []string{"view", "create", "edit", "delete"} { var title, description, descriptionFile, deadline, createState string var byName, useEditor bool command := &cobra.Command{Use: verb, Short: titleCase(verb) + " a milestone"} if verb == "create" { command.Args = argsExact(0) } else { command.Use += " " command.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID") command.Args = argsExact(1) } if verb == "create" || verb == "edit" { command.Flags().StringVar(&title, "title", "", "Milestone title") command.Flags().StringVar(&description, "description", "", "Milestone description") command.Flags().StringVar(&descriptionFile, "description-file", "", "Read description from file, or - for stdin") command.Flags().BoolVar(&useEditor, "editor", false, "Edit the milestone description in the configured editor") command.Flags().StringVar(&deadline, "deadline", "", "Due date: YYYY-MM-DD, RFC3339, or Unix timestamp; empty clears") } if verb == "create" { command.Flags().StringVar(&createState, "state", "open", "Milestone state: open or closed") } command.RunE = func(cmd *cobra.Command, args []string) (runErr error) { var editorDraft terminal.Draft defer finishDraft(&editorDraft, &runErr) if verb == "create" || verb == "edit" { if err := validateTextSources(cmd, "description", "description-file"); err != nil { return err } } opts := services.MilestoneOptions{} if verb != "create" && strings.TrimSpace(args[0]) == "" { return clierr.Wrap(clierr.Usage, errors.New("milestone title or ID cannot be empty")) } var interactiveDeadline *time.Time descriptionChanged := false if verb == "create" { if err := oneOf("state", createState, "open", "closed"); err != nil { return err } opts.State = &createState snapshot, err := a.deps.Settings.Load() if err != nil { return err } policy := a.policy(snapshot) if !localFlagsChanged(cmd) && policy.CanPrompt(a.deps.Streams) && a.deps.MilestoneForm != nil { values, err := a.deps.MilestoneForm.CreateMilestone(cmd.Context()) if err != nil { return err } title, description = values.Title, values.Description interactiveDeadline = values.Deadline descriptionChanged = true } else if _, err := a.require(cmd, &title, "title"); err != nil { return err } } if (verb == "create" || verb == "edit") && !useEditor { var err error description, err = a.readTextSource(cmd, description, descriptionFile, "description", "description-file") if err != nil { return err } if cmd.Flags().Changed("description") || cmd.Flags().Changed("description-file") { descriptionChanged = true } } if cmd.Flags().Changed("title") && strings.TrimSpace(title) == "" { return clierr.Wrap(clierr.Usage, errors.New("milestone title cannot be empty")) } if verb == "create" || cmd.Flags().Changed("title") { opts.Title = &title } if cmd.Flags().Changed("deadline") || interactiveDeadline != nil { date := time.Time{} if interactiveDeadline != nil { date = *interactiveDeadline } else if deadline != "" { parsed, _, err := parseDeadline(deadline) if err != nil { return err } date = *parsed } normalized := date.Format(time.RFC3339) opts.Deadline = &normalized } if verb == "edit" && opts.Title == nil && !descriptionChanged && !useEditor && opts.Deadline == nil { return clierr.Wrap(clierr.Usage, errors.New("supply milestone fields to edit")) } s, err := service(cmd) if err != nil { return err } var current services.Milestone if verb != "create" { if current, err = s.FindOne(cmd.Context(), args[0], byName); err != nil { return err } } id := current.ID if verb == "delete" { if err := a.confirm(cmd, "Delete milestone "+args[0]); err != nil { return err } if err := s.Delete(cmd.Context(), id); err != nil { return err } if a.options.json != "" { return writeItem(a, cmd, current, milestoneRow) } return nil } if useEditor { editorDraft, err = a.editDraft(cmd, current.Description) if err != nil { return err } description = editorDraft.Body descriptionChanged = true } if verb == "create" || descriptionChanged { opts.Description = &description } var item services.Milestone if verb == "view" { item = current } else if verb == "create" { item, err = s.Create(cmd.Context(), opts) } else { item, err = s.Edit(cmd.Context(), id, opts) } if err != nil { return err } return writeItem(a, cmd, item, milestoneRow) } group.AddCommand(command) } group.AddCommand(a.milestoneStateCommand("close", service), a.milestoneStateCommand("reopen", service)) group.AddCommand(a.milestoneIssueCommand()) return group } 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]}} }