github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/issue/edit/edit.go (about)

     1  package edit
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  
     8  	"github.com/MakeNowJust/heredoc"
     9  	"github.com/cli/cli/api"
    10  	"github.com/cli/cli/internal/ghrepo"
    11  	shared "github.com/cli/cli/pkg/cmd/issue/shared"
    12  	prShared "github.com/cli/cli/pkg/cmd/pr/shared"
    13  	"github.com/cli/cli/pkg/cmdutil"
    14  	"github.com/cli/cli/pkg/iostreams"
    15  	"github.com/shurcooL/githubv4"
    16  	"github.com/spf13/cobra"
    17  )
    18  
    19  type EditOptions struct {
    20  	HttpClient func() (*http.Client, error)
    21  	IO         *iostreams.IOStreams
    22  	BaseRepo   func() (ghrepo.Interface, error)
    23  
    24  	DetermineEditor    func() (string, error)
    25  	FieldsToEditSurvey func(*prShared.Editable) error
    26  	EditFieldsSurvey   func(*prShared.Editable, string) error
    27  	FetchOptions       func(*api.Client, ghrepo.Interface, *prShared.Editable) error
    28  
    29  	SelectorArg string
    30  	Interactive bool
    31  
    32  	prShared.Editable
    33  }
    34  
    35  func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command {
    36  	opts := &EditOptions{
    37  		IO:                 f.IOStreams,
    38  		HttpClient:         f.HttpClient,
    39  		DetermineEditor:    func() (string, error) { return cmdutil.DetermineEditor(f.Config) },
    40  		FieldsToEditSurvey: prShared.FieldsToEditSurvey,
    41  		EditFieldsSurvey:   prShared.EditFieldsSurvey,
    42  		FetchOptions:       prShared.FetchOptions,
    43  	}
    44  
    45  	var bodyFile string
    46  
    47  	cmd := &cobra.Command{
    48  		Use:   "edit {<number> | <url>}",
    49  		Short: "Edit an issue",
    50  		Example: heredoc.Doc(`
    51  			$ gh issue edit 23 --title "I found a bug" --body "Nothing works"
    52  			$ gh issue edit 23 --add-label "bug,help wanted" --remove-label "core"
    53  			$ gh issue edit 23 --add-assignee "@me" --remove-assignee monalisa,hubot
    54  			$ gh issue edit 23 --add-project "Roadmap" --remove-project v1,v2
    55  			$ gh issue edit 23 --milestone "Version 1"
    56  			$ gh issue edit 23 --body-file body.txt
    57  		`),
    58  		Args: cobra.ExactArgs(1),
    59  		RunE: func(cmd *cobra.Command, args []string) error {
    60  			// support `-R, --repo` override
    61  			opts.BaseRepo = f.BaseRepo
    62  
    63  			opts.SelectorArg = args[0]
    64  
    65  			flags := cmd.Flags()
    66  
    67  			bodyProvided := flags.Changed("body")
    68  			bodyFileProvided := bodyFile != ""
    69  
    70  			if err := cmdutil.MutuallyExclusive(
    71  				"specify only one of `--body` or `--body-file`",
    72  				bodyProvided,
    73  				bodyFileProvided,
    74  			); err != nil {
    75  				return err
    76  			}
    77  			if bodyProvided || bodyFileProvided {
    78  				opts.Editable.Body.Edited = true
    79  				if bodyFileProvided {
    80  					b, err := cmdutil.ReadFile(bodyFile, opts.IO.In)
    81  					if err != nil {
    82  						return err
    83  					}
    84  					opts.Editable.Body.Value = string(b)
    85  				}
    86  			}
    87  
    88  			if flags.Changed("title") {
    89  				opts.Editable.Title.Edited = true
    90  			}
    91  			if flags.Changed("add-assignee") || flags.Changed("remove-assignee") {
    92  				opts.Editable.Assignees.Edited = true
    93  			}
    94  			if flags.Changed("add-label") || flags.Changed("remove-label") {
    95  				opts.Editable.Labels.Edited = true
    96  			}
    97  			if flags.Changed("add-project") || flags.Changed("remove-project") {
    98  				opts.Editable.Projects.Edited = true
    99  			}
   100  			if flags.Changed("milestone") {
   101  				opts.Editable.Milestone.Edited = true
   102  			}
   103  
   104  			if !opts.Editable.Dirty() {
   105  				opts.Interactive = true
   106  			}
   107  
   108  			if opts.Interactive && !opts.IO.CanPrompt() {
   109  				return &cmdutil.FlagError{Err: errors.New("field to edit flag required when not running interactively")}
   110  			}
   111  
   112  			if runF != nil {
   113  				return runF(opts)
   114  			}
   115  
   116  			return editRun(opts)
   117  		},
   118  	}
   119  
   120  	cmd.Flags().StringVarP(&opts.Editable.Title.Value, "title", "t", "", "Set the new title.")
   121  	cmd.Flags().StringVarP(&opts.Editable.Body.Value, "body", "b", "", "Set the new body.")
   122  	cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file`")
   123  	cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Add, "add-assignee", nil, "Add assigned users by their `login`. Use \"@me\" to assign yourself.")
   124  	cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Remove, "remove-assignee", nil, "Remove assigned users by their `login`. Use \"@me\" to unassign yourself.")
   125  	cmd.Flags().StringSliceVar(&opts.Editable.Labels.Add, "add-label", nil, "Add labels by `name`")
   126  	cmd.Flags().StringSliceVar(&opts.Editable.Labels.Remove, "remove-label", nil, "Remove labels by `name`")
   127  	cmd.Flags().StringSliceVar(&opts.Editable.Projects.Add, "add-project", nil, "Add the issue to projects by `name`")
   128  	cmd.Flags().StringSliceVar(&opts.Editable.Projects.Remove, "remove-project", nil, "Remove the issue from projects by `name`")
   129  	cmd.Flags().StringVarP(&opts.Editable.Milestone.Value, "milestone", "m", "", "Edit the milestone the issue belongs to by `name`")
   130  
   131  	return cmd
   132  }
   133  
   134  func editRun(opts *EditOptions) error {
   135  	httpClient, err := opts.HttpClient()
   136  	if err != nil {
   137  		return err
   138  	}
   139  	apiClient := api.NewClientFromHTTP(httpClient)
   140  
   141  	issue, repo, err := shared.IssueFromArg(apiClient, opts.BaseRepo, opts.SelectorArg)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	editable := opts.Editable
   147  	editable.Title.Default = issue.Title
   148  	editable.Body.Default = issue.Body
   149  	editable.Assignees.Default = issue.Assignees.Logins()
   150  	editable.Labels.Default = issue.Labels.Names()
   151  	editable.Projects.Default = issue.ProjectCards.ProjectNames()
   152  	if issue.Milestone != nil {
   153  		editable.Milestone.Default = issue.Milestone.Title
   154  	}
   155  
   156  	if opts.Interactive {
   157  		err = opts.FieldsToEditSurvey(&editable)
   158  		if err != nil {
   159  			return err
   160  		}
   161  	}
   162  
   163  	opts.IO.StartProgressIndicator()
   164  	err = opts.FetchOptions(apiClient, repo, &editable)
   165  	opts.IO.StopProgressIndicator()
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	if opts.Interactive {
   171  		editorCommand, err := opts.DetermineEditor()
   172  		if err != nil {
   173  			return err
   174  		}
   175  		err = opts.EditFieldsSurvey(&editable, editorCommand)
   176  		if err != nil {
   177  			return err
   178  		}
   179  	}
   180  
   181  	opts.IO.StartProgressIndicator()
   182  	err = updateIssue(apiClient, repo, issue.ID, editable)
   183  	opts.IO.StopProgressIndicator()
   184  	if err != nil {
   185  		return err
   186  	}
   187  
   188  	fmt.Fprintln(opts.IO.Out, issue.URL)
   189  
   190  	return nil
   191  }
   192  
   193  func updateIssue(client *api.Client, repo ghrepo.Interface, id string, options prShared.Editable) error {
   194  	var err error
   195  	params := githubv4.UpdateIssueInput{
   196  		ID:    id,
   197  		Title: ghString(options.TitleValue()),
   198  		Body:  ghString(options.BodyValue()),
   199  	}
   200  	assigneeIds, err := options.AssigneeIds(client, repo)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	params.AssigneeIDs = ghIds(assigneeIds)
   205  	labelIds, err := options.LabelIds()
   206  	if err != nil {
   207  		return err
   208  	}
   209  	params.LabelIDs = ghIds(labelIds)
   210  	projectIds, err := options.ProjectIds()
   211  	if err != nil {
   212  		return err
   213  	}
   214  	params.ProjectIDs = ghIds(projectIds)
   215  	milestoneId, err := options.MilestoneId()
   216  	if err != nil {
   217  		return err
   218  	}
   219  	params.MilestoneID = ghId(milestoneId)
   220  	return api.IssueUpdate(client, repo, params)
   221  }
   222  
   223  func ghIds(s *[]string) *[]githubv4.ID {
   224  	if s == nil {
   225  		return nil
   226  	}
   227  	ids := make([]githubv4.ID, len(*s))
   228  	for i, v := range *s {
   229  		ids[i] = v
   230  	}
   231  	return &ids
   232  }
   233  
   234  func ghId(s *string) *githubv4.ID {
   235  	if s == nil {
   236  		return nil
   237  	}
   238  	if *s == "" {
   239  		r := githubv4.ID(nil)
   240  		return &r
   241  	}
   242  	r := githubv4.ID(*s)
   243  	return &r
   244  }
   245  
   246  func ghString(s *string) *githubv4.String {
   247  	if s == nil {
   248  		return nil
   249  	}
   250  	r := githubv4.String(*s)
   251  	return &r
   252  }