github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/pr/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/config"
    11  	"github.com/cli/cli/internal/ghrepo"
    12  	shared "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  
    23  	Finder          shared.PRFinder
    24  	Surveyor        Surveyor
    25  	Fetcher         EditableOptionsFetcher
    26  	EditorRetriever EditorRetriever
    27  
    28  	SelectorArg string
    29  	Interactive bool
    30  
    31  	shared.Editable
    32  }
    33  
    34  func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command {
    35  	opts := &EditOptions{
    36  		IO:              f.IOStreams,
    37  		HttpClient:      f.HttpClient,
    38  		Surveyor:        surveyor{},
    39  		Fetcher:         fetcher{},
    40  		EditorRetriever: editorRetriever{config: f.Config},
    41  	}
    42  
    43  	var bodyFile string
    44  
    45  	cmd := &cobra.Command{
    46  		Use:   "edit [<number> | <url> | <branch>]",
    47  		Short: "Edit a pull request",
    48  		Long: heredoc.Doc(`
    49  			Edit a pull request.
    50  
    51  			Without an argument, the pull request that belongs to the current branch
    52  			is selected.
    53  		`),
    54  		Example: heredoc.Doc(`
    55  			$ gh pr edit 23 --title "I found a bug" --body "Nothing works"
    56  			$ gh pr edit 23 --add-label "bug,help wanted" --remove-label "core"
    57  			$ gh pr edit 23 --add-reviewer monalisa,hubot  --remove-reviewer myorg/team-name
    58  			$ gh pr edit 23 --add-assignee "@me" --remove-assignee monalisa,hubot
    59  			$ gh pr edit 23 --add-project "Roadmap" --remove-project v1,v2
    60  			$ gh pr edit 23 --milestone "Version 1"
    61  		`),
    62  		Args: cobra.MaximumNArgs(1),
    63  		RunE: func(cmd *cobra.Command, args []string) error {
    64  			opts.Finder = shared.NewFinder(f)
    65  
    66  			if len(args) > 0 {
    67  				opts.SelectorArg = args[0]
    68  			}
    69  
    70  			flags := cmd.Flags()
    71  
    72  			bodyProvided := flags.Changed("body")
    73  			bodyFileProvided := bodyFile != ""
    74  
    75  			if err := cmdutil.MutuallyExclusive(
    76  				"specify only one of `--body` or `--body-file`",
    77  				bodyProvided,
    78  				bodyFileProvided,
    79  			); err != nil {
    80  				return err
    81  			}
    82  			if bodyProvided || bodyFileProvided {
    83  				opts.Editable.Body.Edited = true
    84  				if bodyFileProvided {
    85  					b, err := cmdutil.ReadFile(bodyFile, opts.IO.In)
    86  					if err != nil {
    87  						return err
    88  					}
    89  					opts.Editable.Body.Value = string(b)
    90  				}
    91  			}
    92  
    93  			if flags.Changed("title") {
    94  				opts.Editable.Title.Edited = true
    95  			}
    96  			if flags.Changed("body") {
    97  				opts.Editable.Body.Edited = true
    98  			}
    99  			if flags.Changed("base") {
   100  				opts.Editable.Base.Edited = true
   101  			}
   102  			if flags.Changed("add-reviewer") || flags.Changed("remove-reviewer") {
   103  				opts.Editable.Reviewers.Edited = true
   104  			}
   105  			if flags.Changed("add-assignee") || flags.Changed("remove-assignee") {
   106  				opts.Editable.Assignees.Edited = true
   107  			}
   108  			if flags.Changed("add-label") || flags.Changed("remove-label") {
   109  				opts.Editable.Labels.Edited = true
   110  			}
   111  			if flags.Changed("add-project") || flags.Changed("remove-project") {
   112  				opts.Editable.Projects.Edited = true
   113  			}
   114  			if flags.Changed("milestone") {
   115  				opts.Editable.Milestone.Edited = true
   116  			}
   117  
   118  			if !opts.Editable.Dirty() {
   119  				opts.Interactive = true
   120  			}
   121  
   122  			if opts.Interactive && !opts.IO.CanPrompt() {
   123  				return &cmdutil.FlagError{Err: errors.New("--tile, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively")}
   124  			}
   125  
   126  			if runF != nil {
   127  				return runF(opts)
   128  			}
   129  
   130  			return editRun(opts)
   131  		},
   132  	}
   133  
   134  	cmd.Flags().StringVarP(&opts.Editable.Title.Value, "title", "t", "", "Set the new title.")
   135  	cmd.Flags().StringVarP(&opts.Editable.Body.Value, "body", "b", "", "Set the new body.")
   136  	cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file`")
   137  	cmd.Flags().StringVarP(&opts.Editable.Base.Value, "base", "B", "", "Change the base `branch` for this pull request")
   138  	cmd.Flags().StringSliceVar(&opts.Editable.Reviewers.Add, "add-reviewer", nil, "Add reviewers by their `login`.")
   139  	cmd.Flags().StringSliceVar(&opts.Editable.Reviewers.Remove, "remove-reviewer", nil, "Remove reviewers by their `login`.")
   140  	cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Add, "add-assignee", nil, "Add assigned users by their `login`. Use \"@me\" to assign yourself.")
   141  	cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Remove, "remove-assignee", nil, "Remove assigned users by their `login`. Use \"@me\" to unassign yourself.")
   142  	cmd.Flags().StringSliceVar(&opts.Editable.Labels.Add, "add-label", nil, "Add labels by `name`")
   143  	cmd.Flags().StringSliceVar(&opts.Editable.Labels.Remove, "remove-label", nil, "Remove labels by `name`")
   144  	cmd.Flags().StringSliceVar(&opts.Editable.Projects.Add, "add-project", nil, "Add the pull request to projects by `name`")
   145  	cmd.Flags().StringSliceVar(&opts.Editable.Projects.Remove, "remove-project", nil, "Remove the pull request from projects by `name`")
   146  	cmd.Flags().StringVarP(&opts.Editable.Milestone.Value, "milestone", "m", "", "Edit the milestone the pull request belongs to by `name`")
   147  
   148  	return cmd
   149  }
   150  
   151  func editRun(opts *EditOptions) error {
   152  	findOptions := shared.FindOptions{
   153  		Selector: opts.SelectorArg,
   154  		Fields:   []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "assignees", "labels", "projectCards", "milestone"},
   155  	}
   156  	pr, repo, err := opts.Finder.Find(findOptions)
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	editable := opts.Editable
   162  	editable.Reviewers.Allowed = true
   163  	editable.Title.Default = pr.Title
   164  	editable.Body.Default = pr.Body
   165  	editable.Base.Default = pr.BaseRefName
   166  	editable.Reviewers.Default = pr.ReviewRequests.Logins()
   167  	editable.Assignees.Default = pr.Assignees.Logins()
   168  	editable.Labels.Default = pr.Labels.Names()
   169  	editable.Projects.Default = pr.ProjectCards.ProjectNames()
   170  	if pr.Milestone != nil {
   171  		editable.Milestone.Default = pr.Milestone.Title
   172  	}
   173  
   174  	if opts.Interactive {
   175  		err = opts.Surveyor.FieldsToEdit(&editable)
   176  		if err != nil {
   177  			return err
   178  		}
   179  	}
   180  
   181  	httpClient, err := opts.HttpClient()
   182  	if err != nil {
   183  		return err
   184  	}
   185  	apiClient := api.NewClientFromHTTP(httpClient)
   186  
   187  	opts.IO.StartProgressIndicator()
   188  	err = opts.Fetcher.EditableOptionsFetch(apiClient, repo, &editable)
   189  	opts.IO.StopProgressIndicator()
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	if opts.Interactive {
   195  		editorCommand, err := opts.EditorRetriever.Retrieve()
   196  		if err != nil {
   197  			return err
   198  		}
   199  		err = opts.Surveyor.EditFields(&editable, editorCommand)
   200  		if err != nil {
   201  			return err
   202  		}
   203  	}
   204  
   205  	opts.IO.StartProgressIndicator()
   206  	err = updatePullRequest(apiClient, repo, pr.ID, editable)
   207  	opts.IO.StopProgressIndicator()
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	fmt.Fprintln(opts.IO.Out, pr.URL)
   213  
   214  	return nil
   215  }
   216  
   217  func updatePullRequest(client *api.Client, repo ghrepo.Interface, id string, editable shared.Editable) error {
   218  	var err error
   219  	params := githubv4.UpdatePullRequestInput{
   220  		PullRequestID: id,
   221  		Title:         ghString(editable.TitleValue()),
   222  		Body:          ghString(editable.BodyValue()),
   223  	}
   224  	assigneeIds, err := editable.AssigneeIds(client, repo)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	params.AssigneeIDs = ghIds(assigneeIds)
   229  	labelIds, err := editable.LabelIds()
   230  	if err != nil {
   231  		return err
   232  	}
   233  	params.LabelIDs = ghIds(labelIds)
   234  	projectIds, err := editable.ProjectIds()
   235  	if err != nil {
   236  		return err
   237  	}
   238  	params.ProjectIDs = ghIds(projectIds)
   239  	milestoneId, err := editable.MilestoneId()
   240  	if err != nil {
   241  		return err
   242  	}
   243  	params.MilestoneID = ghId(milestoneId)
   244  	if editable.Base.Edited {
   245  		params.BaseRefName = ghString(&editable.Base.Value)
   246  	}
   247  	err = api.UpdatePullRequest(client, repo, params)
   248  	if err != nil {
   249  		return err
   250  	}
   251  	return updatePullRequestReviews(client, repo, id, editable)
   252  }
   253  
   254  func updatePullRequestReviews(client *api.Client, repo ghrepo.Interface, id string, editable shared.Editable) error {
   255  	if !editable.Reviewers.Edited {
   256  		return nil
   257  	}
   258  	userIds, teamIds, err := editable.ReviewerIds()
   259  	if err != nil {
   260  		return err
   261  	}
   262  	union := githubv4.Boolean(false)
   263  	reviewsRequestParams := githubv4.RequestReviewsInput{
   264  		PullRequestID: id,
   265  		Union:         &union,
   266  		UserIDs:       ghIds(userIds),
   267  		TeamIDs:       ghIds(teamIds),
   268  	}
   269  	return api.UpdatePullRequestReviews(client, repo, reviewsRequestParams)
   270  }
   271  
   272  type Surveyor interface {
   273  	FieldsToEdit(*shared.Editable) error
   274  	EditFields(*shared.Editable, string) error
   275  }
   276  
   277  type surveyor struct{}
   278  
   279  func (s surveyor) FieldsToEdit(editable *shared.Editable) error {
   280  	return shared.FieldsToEditSurvey(editable)
   281  }
   282  
   283  func (s surveyor) EditFields(editable *shared.Editable, editorCmd string) error {
   284  	return shared.EditFieldsSurvey(editable, editorCmd)
   285  }
   286  
   287  type EditableOptionsFetcher interface {
   288  	EditableOptionsFetch(*api.Client, ghrepo.Interface, *shared.Editable) error
   289  }
   290  
   291  type fetcher struct{}
   292  
   293  func (f fetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable) error {
   294  	return shared.FetchOptions(client, repo, opts)
   295  }
   296  
   297  type EditorRetriever interface {
   298  	Retrieve() (string, error)
   299  }
   300  
   301  type editorRetriever struct {
   302  	config func() (config.Config, error)
   303  }
   304  
   305  func (e editorRetriever) Retrieve() (string, error) {
   306  	return cmdutil.DetermineEditor(e.config)
   307  }
   308  
   309  func ghIds(s *[]string) *[]githubv4.ID {
   310  	if s == nil {
   311  		return nil
   312  	}
   313  	ids := make([]githubv4.ID, len(*s))
   314  	for i, v := range *s {
   315  		ids[i] = v
   316  	}
   317  	return &ids
   318  }
   319  
   320  func ghId(s *string) *githubv4.ID {
   321  	if s == nil {
   322  		return nil
   323  	}
   324  	if *s == "" {
   325  		r := githubv4.ID(nil)
   326  		return &r
   327  	}
   328  	r := githubv4.ID(*s)
   329  	return &r
   330  }
   331  
   332  func ghString(s *string) *githubv4.String {
   333  	if s == nil {
   334  		return nil
   335  	}
   336  	r := githubv4.String(*s)
   337  	return &r
   338  }