github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/issue/edit/edit.go (about)

     1  package edit
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  
     7  	"github.com/MakeNowJust/heredoc"
     8  	"github.com/ungtb10d/cli/v2/api"
     9  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    10  	shared "github.com/ungtb10d/cli/v2/pkg/cmd/issue/shared"
    11  	prShared "github.com/ungtb10d/cli/v2/pkg/cmd/pr/shared"
    12  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    13  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    14  	"github.com/spf13/cobra"
    15  )
    16  
    17  type EditOptions struct {
    18  	HttpClient func() (*http.Client, error)
    19  	IO         *iostreams.IOStreams
    20  	BaseRepo   func() (ghrepo.Interface, error)
    21  
    22  	DetermineEditor    func() (string, error)
    23  	FieldsToEditSurvey func(*prShared.Editable) error
    24  	EditFieldsSurvey   func(*prShared.Editable, string) error
    25  	FetchOptions       func(*api.Client, ghrepo.Interface, *prShared.Editable) error
    26  
    27  	SelectorArg string
    28  	Interactive bool
    29  
    30  	prShared.Editable
    31  }
    32  
    33  func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command {
    34  	opts := &EditOptions{
    35  		IO:                 f.IOStreams,
    36  		HttpClient:         f.HttpClient,
    37  		DetermineEditor:    func() (string, error) { return cmdutil.DetermineEditor(f.Config) },
    38  		FieldsToEditSurvey: prShared.FieldsToEditSurvey,
    39  		EditFieldsSurvey:   prShared.EditFieldsSurvey,
    40  		FetchOptions:       prShared.FetchOptions,
    41  	}
    42  
    43  	var bodyFile string
    44  
    45  	cmd := &cobra.Command{
    46  		Use:   "edit {<number> | <url>}",
    47  		Short: "Edit an issue",
    48  		Example: heredoc.Doc(`
    49  			$ gh issue edit 23 --title "I found a bug" --body "Nothing works"
    50  			$ gh issue edit 23 --add-label "bug,help wanted" --remove-label "core"
    51  			$ gh issue edit 23 --add-assignee "@me" --remove-assignee monalisa,hubot
    52  			$ gh issue edit 23 --add-project "Roadmap" --remove-project v1,v2
    53  			$ gh issue edit 23 --milestone "Version 1"
    54  			$ gh issue edit 23 --body-file body.txt
    55  		`),
    56  		Args: cobra.ExactArgs(1),
    57  		RunE: func(cmd *cobra.Command, args []string) error {
    58  			// support `-R, --repo` override
    59  			opts.BaseRepo = f.BaseRepo
    60  
    61  			opts.SelectorArg = args[0]
    62  
    63  			flags := cmd.Flags()
    64  
    65  			bodyProvided := flags.Changed("body")
    66  			bodyFileProvided := bodyFile != ""
    67  
    68  			if err := cmdutil.MutuallyExclusive(
    69  				"specify only one of `--body` or `--body-file`",
    70  				bodyProvided,
    71  				bodyFileProvided,
    72  			); err != nil {
    73  				return err
    74  			}
    75  			if bodyProvided || bodyFileProvided {
    76  				opts.Editable.Body.Edited = true
    77  				if bodyFileProvided {
    78  					b, err := cmdutil.ReadFile(bodyFile, opts.IO.In)
    79  					if err != nil {
    80  						return err
    81  					}
    82  					opts.Editable.Body.Value = string(b)
    83  				}
    84  			}
    85  
    86  			if flags.Changed("title") {
    87  				opts.Editable.Title.Edited = true
    88  			}
    89  			if flags.Changed("add-assignee") || flags.Changed("remove-assignee") {
    90  				opts.Editable.Assignees.Edited = true
    91  			}
    92  			if flags.Changed("add-label") || flags.Changed("remove-label") {
    93  				opts.Editable.Labels.Edited = true
    94  			}
    95  			if flags.Changed("add-project") || flags.Changed("remove-project") {
    96  				opts.Editable.Projects.Edited = true
    97  			}
    98  			if flags.Changed("milestone") {
    99  				opts.Editable.Milestone.Edited = true
   100  			}
   101  
   102  			if !opts.Editable.Dirty() {
   103  				opts.Interactive = true
   104  			}
   105  
   106  			if opts.Interactive && !opts.IO.CanPrompt() {
   107  				return cmdutil.FlagErrorf("field to edit flag required when not running interactively")
   108  			}
   109  
   110  			if runF != nil {
   111  				return runF(opts)
   112  			}
   113  
   114  			return editRun(opts)
   115  		},
   116  	}
   117  
   118  	cmd.Flags().StringVarP(&opts.Editable.Title.Value, "title", "t", "", "Set the new title.")
   119  	cmd.Flags().StringVarP(&opts.Editable.Body.Value, "body", "b", "", "Set the new body.")
   120  	cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)")
   121  	cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Add, "add-assignee", nil, "Add assigned users by their `login`. Use \"@me\" to assign yourself.")
   122  	cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Remove, "remove-assignee", nil, "Remove assigned users by their `login`. Use \"@me\" to unassign yourself.")
   123  	cmd.Flags().StringSliceVar(&opts.Editable.Labels.Add, "add-label", nil, "Add labels by `name`")
   124  	cmd.Flags().StringSliceVar(&opts.Editable.Labels.Remove, "remove-label", nil, "Remove labels by `name`")
   125  	cmd.Flags().StringSliceVar(&opts.Editable.Projects.Add, "add-project", nil, "Add the issue to projects by `name`")
   126  	cmd.Flags().StringSliceVar(&opts.Editable.Projects.Remove, "remove-project", nil, "Remove the issue from projects by `name`")
   127  	cmd.Flags().StringVarP(&opts.Editable.Milestone.Value, "milestone", "m", "", "Edit the milestone the issue belongs to by `name`")
   128  
   129  	return cmd
   130  }
   131  
   132  func editRun(opts *EditOptions) error {
   133  	httpClient, err := opts.HttpClient()
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	editable := opts.Editable
   139  	lookupFields := []string{"id", "number", "title", "body", "url"}
   140  	if opts.Interactive || editable.Assignees.Edited {
   141  		lookupFields = append(lookupFields, "assignees")
   142  	}
   143  	if opts.Interactive || editable.Labels.Edited {
   144  		lookupFields = append(lookupFields, "labels")
   145  	}
   146  	if opts.Interactive || editable.Projects.Edited {
   147  		lookupFields = append(lookupFields, "projectCards")
   148  	}
   149  	if opts.Interactive || editable.Milestone.Edited {
   150  		lookupFields = append(lookupFields, "milestone")
   151  	}
   152  
   153  	issue, repo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, lookupFields)
   154  	if err != nil {
   155  		return err
   156  	}
   157  
   158  	editable.Title.Default = issue.Title
   159  	editable.Body.Default = issue.Body
   160  	editable.Assignees.Default = issue.Assignees.Logins()
   161  	editable.Labels.Default = issue.Labels.Names()
   162  	editable.Projects.Default = issue.ProjectCards.ProjectNames()
   163  	if issue.Milestone != nil {
   164  		editable.Milestone.Default = issue.Milestone.Title
   165  	}
   166  
   167  	if opts.Interactive {
   168  		err = opts.FieldsToEditSurvey(&editable)
   169  		if err != nil {
   170  			return err
   171  		}
   172  	}
   173  
   174  	apiClient := api.NewClientFromHTTP(httpClient)
   175  	opts.IO.StartProgressIndicator()
   176  	err = opts.FetchOptions(apiClient, repo, &editable)
   177  	opts.IO.StopProgressIndicator()
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	if opts.Interactive {
   183  		editorCommand, err := opts.DetermineEditor()
   184  		if err != nil {
   185  			return err
   186  		}
   187  		err = opts.EditFieldsSurvey(&editable, editorCommand)
   188  		if err != nil {
   189  			return err
   190  		}
   191  	}
   192  
   193  	opts.IO.StartProgressIndicator()
   194  	err = prShared.UpdateIssue(httpClient, repo, issue.ID, issue.IsPullRequest(), editable)
   195  	opts.IO.StopProgressIndicator()
   196  	if err != nil {
   197  		return err
   198  	}
   199  
   200  	fmt.Fprintln(opts.IO.Out, issue.URL)
   201  
   202  	return nil
   203  }