jira-cli/internal/cmd/issue/edit/edit.go

451 lines
12 KiB
Go

package edit
import (
"fmt"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/ankitpokhrel/jira-cli/api"
"github.com/ankitpokhrel/jira-cli/internal/cmdcommon"
"github.com/ankitpokhrel/jira-cli/internal/cmdutil"
"github.com/ankitpokhrel/jira-cli/internal/query"
"github.com/ankitpokhrel/jira-cli/pkg/adf"
"github.com/ankitpokhrel/jira-cli/pkg/jira"
"github.com/ankitpokhrel/jira-cli/pkg/md"
"github.com/ankitpokhrel/jira-cli/pkg/surveyext"
)
const (
helpText = `Edit an issue in a given project with minimal information.`
examples = `$ jira issue edit ISSUE-1
# Edit issue in the configured project
$ jira issue edit ISSUE-1 -s"New Bug" -yHigh -lbug -lurgent -CBackend -b"Bug description"
# Use --no-input option to disable interactive prompt
$ jira issue edit ISSUE-1 -s"New updated summary" --no-input
# Use pipe to read the description body directly from standard input
$ echo "Description from stdin" | jira issue edit ISSUE-1 -s"New updated summary" --no-input
# Use minus (-) to remove label, component or fixVersion
$ jira issue edit ISSUE-1 --label -urgent --component -BE --fix-version -v1.0`
)
// NewCmdEdit is an edit command.
func NewCmdEdit() *cobra.Command {
cmd := cobra.Command{
Use: "edit ISSUE-KEY",
Short: "Edit an issue in a project",
Long: helpText,
Example: examples,
Aliases: []string{"update", "modify"},
Annotations: map[string]string{
"help:args": `ISSUE-KEY Issue key, eg: ISSUE-1`,
},
Args: cobra.MinimumNArgs(1),
Run: edit,
}
setFlags(&cmd)
return &cmd
}
func edit(cmd *cobra.Command, args []string) {
server := viper.GetString("server")
project := viper.GetString("project.key")
params := parseArgsAndFlags(cmd.Flags(), args, project)
client := api.DefaultClient(params.debug)
ec := editCmd{
client: client,
params: params,
}
issue, err := func() (*jira.Issue, error) {
s := cmdutil.Info(fmt.Sprintf("Fetching issue %s...", params.issueKey))
defer s.Stop()
issue, err := api.ProxyGetIssue(client, params.issueKey)
if err != nil {
return nil, err
}
return issue, nil
}()
cmdutil.ExitIfError(err)
var (
isADF bool
originalBody string
)
if issue.Fields.Description != nil {
if adfBody, ok := issue.Fields.Description.(*adf.ADF); ok {
isADF = true
originalBody = adf.NewTranslator(adfBody, adf.NewJiraMarkdownTranslator()).Translate()
} else {
originalBody = issue.Fields.Description.(string)
}
}
cmdutil.ExitIfError(ec.askQuestions(issue, originalBody))
if !params.noInput {
getAnswers(params, issue)
}
// Use stdin only if nothing is passed to --body
if params.body == "" && cmdutil.StdinHasData() {
b, err := cmdutil.ReadFile("-")
if err != nil {
cmdutil.Failed("Error: %s", err)
}
params.body = string(b)
}
// Keep body as is if there were no changes.
if params.body != "" && params.body == originalBody {
params.body = ""
}
labels := params.labels
labels = append(labels, issue.Fields.Labels...)
components := make([]string, 0, len(issue.Fields.Components)+len(params.components))
for _, c := range issue.Fields.Components {
components = append(components, c.Name)
}
components = append(components, params.components...)
fixVersions := make([]string, 0, len(issue.Fields.FixVersions)+len(params.fixVersions))
for _, fv := range issue.Fields.FixVersions {
fixVersions = append(fixVersions, fv.Name)
}
fixVersions = append(fixVersions, params.fixVersions...)
affectsVersions := make([]string, 0, len(issue.Fields.AffectsVersions)+len(params.affectsVersions))
for _, fv := range issue.Fields.AffectsVersions {
affectsVersions = append(affectsVersions, fv.Name)
}
affectsVersions = append(affectsVersions, params.affectsVersions...)
err = func() error {
s := cmdutil.Info("Updating an issue...")
defer s.Stop()
body := params.body
if isADF {
body = md.ToJiraMD(body)
}
parent := cmdutil.GetJiraIssueKey(project, params.parentIssueKey)
if parent == "" && issue.Fields.Parent != nil {
parent = issue.Fields.Parent.Key
}
edr := jira.EditRequest{
ParentIssueKey: parent,
Summary: params.summary,
Body: body,
Priority: params.priority,
Labels: labels,
Components: components,
FixVersions: fixVersions,
AffectsVersions: affectsVersions,
CustomFields: params.customFields,
}
if configuredCustomFields, err := cmdcommon.GetConfiguredCustomFields(); err == nil {
cmdcommon.ValidateCustomFields(edr.CustomFields, configuredCustomFields)
edr.WithCustomFields(configuredCustomFields)
}
return client.Edit(params.issueKey, &edr)
}()
cmdutil.ExitIfError(err)
cmdutil.Success("Issue updated\n%s", cmdutil.GenerateServerBrowseURL(server, params.issueKey))
handleUserAssign(project, params.issueKey, params.assignee, client)
if web, _ := cmd.Flags().GetBool("web"); web {
err := cmdutil.Navigate(server, params.issueKey)
cmdutil.ExitIfError(err)
}
}
func getAnswers(params *editParams, issue *jira.Issue) {
answer := struct{ Action string }{}
for answer.Action != cmdcommon.ActionSubmit {
err := survey.Ask([]*survey.Question{cmdcommon.GetNextAction()}, &answer)
cmdutil.ExitIfError(err)
switch answer.Action {
case cmdcommon.ActionCancel:
cmdutil.Failed("Action aborted")
case cmdcommon.ActionMetadata:
ans := struct{ Metadata []string }{}
err := survey.Ask(cmdcommon.GetMetadata(), &ans)
cmdutil.ExitIfError(err)
if len(ans.Metadata) > 0 {
qs := getMetadataQuestions(ans.Metadata, issue)
ans := struct {
Priority string
Labels string
Components string
FixVersions string
AffectsVersions string
}{}
err := survey.Ask(qs, &ans)
cmdutil.ExitIfError(err)
if ans.Priority != "" {
params.priority = ans.Priority
}
if len(ans.Labels) > 0 {
params.labels = strings.Split(ans.Labels, ",")
}
if len(ans.Components) > 0 {
params.components = strings.Split(ans.Components, ",")
}
if len(ans.FixVersions) > 0 {
params.fixVersions = strings.Split(ans.FixVersions, ",")
}
if len(ans.AffectsVersions) > 0 {
params.affectsVersions = strings.Split(ans.AffectsVersions, ",")
}
}
}
}
}
func handleUserAssign(project, key, assignee string, client *jira.Client) {
if assignee == "" {
return
}
if assignee == "x" {
if err := api.ProxyAssignIssue(client, key, nil, jira.AssigneeNone); err != nil {
cmdutil.Failed("Unable to unassign user: %s", err.Error())
}
return
}
user, err := api.ProxyUserSearch(client, &jira.UserSearchOptions{
Query: assignee,
Project: project,
})
if err != nil || len(user) == 0 {
cmdutil.Failed("Unable to find assignee")
}
if err = api.ProxyAssignIssue(client, key, user[0], assignee); err != nil {
cmdutil.Failed("Unable to set assignee: %s", err.Error())
}
}
type editCmd struct {
client *jira.Client
params *editParams
}
func (ec *editCmd) askQuestions(issue *jira.Issue, originalBody string) error {
if ec.params.noInput {
return nil
}
var qs []*survey.Question
if ec.params.summary == "" {
qs = append(qs, &survey.Question{
Name: "summary",
Prompt: &survey.Input{
Message: "Summary",
Default: issue.Fields.Summary,
},
Validate: survey.Required,
})
}
if ec.params.body == "" {
qs = append(qs, &survey.Question{
Name: "body",
Prompt: &surveyext.JiraEditor{
Editor: &survey.Editor{
Message: "Description",
Default: originalBody,
HideDefault: true,
AppendDefault: true,
},
BlankAllowed: true,
},
})
}
ans := struct{ Summary, Body string }{}
err := survey.Ask(qs, &ans)
if err != nil {
return err
}
if ec.params.summary == "" {
ec.params.summary = ans.Summary
}
if ec.params.body == "" {
ec.params.body = ans.Body
}
return nil
}
type editParams struct {
issueKey string
parentIssueKey string
summary string
body string
priority string
assignee string
labels []string
components []string
fixVersions []string
affectsVersions []string
customFields map[string]string
noInput bool
debug bool
}
func parseArgsAndFlags(flags query.FlagParser, args []string, project string) *editParams {
parentIssueKey, err := flags.GetString("parent")
cmdutil.ExitIfError(err)
summary, err := flags.GetString("summary")
cmdutil.ExitIfError(err)
body, err := flags.GetString("body")
cmdutil.ExitIfError(err)
priority, err := flags.GetString("priority")
cmdutil.ExitIfError(err)
assignee, err := flags.GetString("assignee")
cmdutil.ExitIfError(err)
labels, err := flags.GetStringArray("label")
cmdutil.ExitIfError(err)
components, err := flags.GetStringArray("component")
cmdutil.ExitIfError(err)
fixVersions, err := flags.GetStringArray("fix-version")
cmdutil.ExitIfError(err)
affectsVersions, err := flags.GetStringArray("affects-version")
cmdutil.ExitIfError(err)
custom, err := flags.GetStringToString("custom")
cmdutil.ExitIfError(err)
noInput, err := flags.GetBool("no-input")
cmdutil.ExitIfError(err)
debug, err := flags.GetBool("debug")
cmdutil.ExitIfError(err)
return &editParams{
issueKey: cmdutil.GetJiraIssueKey(project, args[0]),
parentIssueKey: parentIssueKey,
summary: summary,
body: body,
priority: priority,
assignee: assignee,
labels: labels,
components: components,
fixVersions: fixVersions,
affectsVersions: affectsVersions,
customFields: custom,
noInput: noInput,
debug: debug,
}
}
func getMetadataQuestions(meta []string, issue *jira.Issue) []*survey.Question {
var qs []*survey.Question
fixVersions := make([]string, 0, len(issue.Fields.FixVersions))
for _, fv := range issue.Fields.FixVersions {
fixVersions = append(fixVersions, fv.Name)
}
affectsVersions := make([]string, 0, len(issue.Fields.AffectsVersions))
for _, fv := range issue.Fields.AffectsVersions {
affectsVersions = append(affectsVersions, fv.Name)
}
for _, m := range meta {
switch m {
case "Priority":
qs = append(qs, &survey.Question{
Name: "priority",
Prompt: &survey.Input{Message: "Priority", Default: issue.Fields.Priority.Name},
})
case "Components":
qs = append(qs, &survey.Question{
Name: "components",
Prompt: &survey.Input{
Message: "Components",
Help: "Comma separated list of valid components. For eg: BE,FE",
},
})
case "Labels":
qs = append(qs, &survey.Question{
Name: "labels",
Prompt: &survey.Input{
Message: "Labels",
Help: "Comma separated list of labels. For eg: backend,urgent",
Default: strings.Join(issue.Fields.Labels, ","),
},
})
case "FixVersions":
qs = append(qs, &survey.Question{
Name: "fixversions",
Prompt: &survey.Input{
Message: "Fix Versions",
Help: "Comma separated list of fixVersions. For eg: v1.0-beta,v2.0",
Default: strings.Join(fixVersions, ","),
},
})
case "AffectsVersions":
qs = append(qs, &survey.Question{
Name: "affectsversions",
Prompt: &survey.Input{
Message: "Affects Versions",
Help: "Comma separated list of affectsVersions. For eg: v1.0-beta,v2.0",
Default: strings.Join(affectsVersions, ","),
},
})
}
}
return qs
}
func setFlags(cmd *cobra.Command) {
custom := make(map[string]string)
cmd.Flags().SortFlags = false
cmd.Flags().StringP("parent", "P", "", `Link to a parent key`)
cmd.Flags().StringP("summary", "s", "", "Edit summary or title")
cmd.Flags().StringP("body", "b", "", "Edit description")
cmd.Flags().StringP("priority", "y", "", "Edit priority")
cmd.Flags().StringP("assignee", "a", "", "Edit assignee (email or display name)")
cmd.Flags().StringArrayP("label", "l", []string{}, "Append labels")
cmd.Flags().StringArrayP("component", "C", []string{}, "Replace components")
cmd.Flags().StringArray("fix-version", []string{}, "Add/Append release info (fixVersions)")
cmd.Flags().StringArray("affects-version", []string{}, "Add/Append release info (affectsVersions)")
cmd.Flags().StringToString("custom", custom, "Edit custom fields")
cmd.Flags().Bool("web", false, "Open in web browser after successful update")
cmd.Flags().Bool("no-input", false, "Disable prompt for non-required fields")
}