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 }