Files
gitea-tea/internal/app/issue_edit.go
T
bircni 2826726557 refactor(issue,pr): trim help, restating comments and single-use helpers
Cut issue, pr, label, milestone and comment examples to at most three, drop
doc comments that only restate the name, fold the comment and attachment
argument lookups into one closure each, and inline label helpers with one
caller.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-12 20:48:00 +02:00

183 lines
6.9 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package app
import (
"errors"
"strings"
"time"
"gitea.dev/tea/internal/clierr"
"gitea.dev/tea/internal/services"
"gitea.dev/tea/internal/terminal"
"github.com/spf13/cobra"
)
var errNothingToEdit = clierr.Wrap(clierr.Usage, errors.New("nothing to edit; pass a flag such as --title (see --help)"))
func (a *application) issueEditCommand() *cobra.Command {
var title, body, file, deadline, ref, milestone string
var useEditor, removeMilestone bool
var metadata metadataFlags
cmd := &cobra.Command{Use: "edit {<number> | <url>}...", Short: "Edit issues: title, body, labels, assignees, milestone, deadline", Long: `Edit one or more issues. Without flags on a terminal, a form edits one issue at a time,
starting from its current values. Label and assignee flags add to or remove from each issue's
current values; the --set flags replace them.`, Example: `$ tea issue edit 12 --title "Crash on start" --add-label bug
$ tea issue edit 12 13 --add-assignee @me --milestone v1.0
$ tea issue edit 12 --remove-label wontfix --remove-milestone`, Annotations: map[string]string{"tea.arguments": issueArguments}, Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) (runErr error) {
var draft terminal.Draft
defer finishDraft(&draft, &runErr)
if err := validateBodySources(cmd); err != nil {
return err
}
if !localFlagsChanged(cmd) {
if !a.canPrompt() || a.deps.IssueEditForm == nil {
return errNothingToEdit
}
target, numbers, err := a.resolveRefs(cmd, args, issueRef)
if err != nil {
return err
}
return a.editIssuesInteractively(cmd, target, numbers)
}
if useEditor && len(args) != 1 {
return clierr.Wrap(clierr.Usage, errors.New("--editor requires one issue"))
}
milestone, removeMilestone, err := milestoneFlag(cmd, milestone)
if err != nil {
return err
}
opts := services.IssueEdit{}
if removeMilestone {
opts.Milestone = new(int64)
}
if cmd.Flags().Changed("ref") {
opts.Ref = &ref
}
if cmd.Flags().Changed("deadline") {
if opts.Deadline, opts.RemoveDeadline, err = parseDeadline(deadline); err != nil {
return err
}
}
if cmd.Flags().Changed("title") {
if strings.TrimSpace(title) == "" {
return clierr.Wrap(clierr.Usage, errors.New("issue title cannot be empty"))
}
opts.Title = &title
}
if cmd.Flags().Changed("body") || cmd.Flags().Changed("body-file") {
if body, err = a.readBody(cmd, body, file); err != nil {
return err
}
opts.Body = &body
}
if err := metadata.validate(cmd); err != nil {
return err
}
if opts == (services.IssueEdit{}) && !useEditor && !metadata.changed(cmd) && milestone == "" {
return errNothingToEdit
}
target, numbers, err := a.resolveRefs(cmd, args, issueRef)
if err != nil {
return err
}
if milestone != "" {
id, err := milestoneID(cmd, target, milestone)
if err != nil {
return err
}
opts.Milestone = &id
}
if opts.Labels, opts.Assignees, opts.Changes, err = metadata.resolve(cmd, target); err != nil {
return err
}
service := services.Issues{Client: target.client, Repository: target.Repo}
if useEditor {
current, err := service.View(cmd.Context(), numbers[0])
if err != nil {
return err
}
if draft, err = a.editDraft(cmd, current.Body); err != nil {
return err
}
opts.Body = &draft.Body
}
return batchDone(a, cmd, numbers, issueLabel, func(n int64) (services.Issue, error) {
item, err := service.Edit(cmd.Context(), n, opts)
return item, itemError(err, target.Repo)
}, editedIssue(target))
}}
flags := cmd.Flags()
flags.StringVarP(&title, "title", "t", "", "New title")
flags.StringVarP(&body, "body", "b", "", "New body; empty clears it")
flags.StringVarP(&file, "body-file", "F", "", "Read the body from `file`, or - for stdin")
flags.BoolVarP(&useEditor, "editor", "e", false, "Edit the current body in the configured editor")
metadata.register(cmd)
flags.StringVarP(&milestone, "milestone", "m", "", "Move the issue to a milestone by `title` or ID")
flags.BoolVar(&removeMilestone, "remove-milestone", false, "Remove the issue from its milestone")
flagAliases(cmd, map[string]string{"milestone-name": "milestone"})
flags.StringVar(&ref, "ref", "", "Branch or tag the issue refers to; empty clears it")
flags.StringVar(&deadline, "deadline", "", "Due `date`: YYYY-MM-DD, RFC3339, or Unix timestamp; empty clears it")
a.completeMetadataFlags(cmd)
_ = cmd.RegisterFlagCompletionFunc("milestone", a.completeMilestones())
_ = cmd.RegisterFlagCompletionFunc("ref", a.completeBranches())
cmd.ValidArgsFunction = a.completeIssues("open")
return cmd
}
func editedIssue(t target) func(services.Issue) string {
return func(item services.Issue) string {
return "Edited " + discussionNoun(item.Kind) + " " + refLabel(t.Repo, item.Number, item.Title)
}
}
func (a *application) editIssuesInteractively(cmd *cobra.Command, target target, numbers []int64) error {
service := services.Issues{Client: target.client, Repository: target.Repo}
selectables, err := service.CreateSelectables(cmd.Context())
if err != nil {
return err
}
milestones, labels := milestoneChoices(selectables.Milestones), labelChoices(selectables.Labels)
return batchDone(a, cmd, numbers, issueLabel, func(n int64) (item services.Issue, err error) {
current, err := service.View(cmd.Context(), n)
if err != nil {
return item, itemError(err, target.Repo)
}
choices := terminal.IssueEditFormChoices{
Heading: "Editing " + refLabel(target.Repo, n, current.Title),
Title: current.Title, Body: current.Body, Assignees: current.Assignees,
CanPush: selectables.CanPush, AvailableAssignees: selectables.Assignees, Milestones: milestones, AvailableLabels: labels,
EditBody: a.bodyEditor(cmd),
}
if current.Milestone != nil {
choices.Milestone = &terminal.IssueMilestoneChoice{Title: current.Milestone.Title, ID: current.Milestone.ID}
}
choices.Labels = labelChoices(current.Labels)
if current.Deadline != nil {
if parsed, err := time.Parse(time.RFC3339, *current.Deadline); err == nil {
choices.Deadline = &parsed
}
}
values, err := a.deps.IssueEditForm.EditIssue(cmd.Context(), choices)
defer finishDraft(&values.Draft, &err)
if err != nil {
return item, abortBatch{err}
}
opts := services.IssueEdit{Title: &values.Title, Body: &values.Body}
if selectables.CanPush {
// Non-nil slices: an empty selection must clear rather than be omitted.
assignees, labels := append([]string{}, values.Assignees...), append([]int64{}, values.Labels...)
opts.Assignees, opts.Labels = &assignees, &labels
if values.ClearMilestone || values.Milestone != 0 {
opts.Milestone = &values.Milestone
}
opts.Deadline = values.Deadline
if values.ClearDeadline {
opts.RemoveDeadline = &values.ClearDeadline
}
}
item, err = service.Edit(cmd.Context(), n, opts)
return item, itemError(err, target.Repo)
}, editedIssue(target))
}