cli/pkg/cmd/pr/edit/edit.go
2021-02-12 10:08:44 -08:00

259 lines
6.9 KiB
Go

package edit
import (
"errors"
"fmt"
"net/http"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/api"
"github.com/cli/cli/context"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghrepo"
shared "github.com/cli/cli/pkg/cmd/pr/shared"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/shurcooL/githubv4"
"github.com/spf13/cobra"
)
type EditOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
Remotes func() (context.Remotes, error)
Branch func() (string, error)
Surveyor Surveyor
Fetcher EditableOptionsFetcher
EditorRetriever EditorRetriever
SelectorArg string
Interactive bool
shared.Editable
}
func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command {
opts := &EditOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
Remotes: f.Remotes,
Branch: f.Branch,
Surveyor: surveyor{},
Fetcher: fetcher{},
EditorRetriever: editorRetriever{config: f.Config},
}
cmd := &cobra.Command{
Use: "edit {<number> | <url>}",
Short: "Edit a pull request",
Example: heredoc.Doc(`
$ gh pr edit 23 --title "I found a bug" --body "Nothing works"
$ gh pr edit 23 --label "bug,help wanted"
$ gh pr edit 23 --label bug --label "help wanted"
$ gh pr edit 23 --reviewer monalisa,hubot --reviewer myorg/team-name
$ gh pr edit 23 --assignee monalisa,hubot
$ gh pr edit 23 --assignee @me
$ gh pr edit 23 --project "Roadmap"
`),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo
opts.SelectorArg = args[0]
flags := cmd.Flags()
if flags.Changed("title") {
opts.Editable.TitleEdited = true
}
if flags.Changed("body") {
opts.Editable.BodyEdited = true
}
if flags.Changed("reviewer") {
opts.Editable.ReviewersEdited = true
}
if flags.Changed("assignee") {
opts.Editable.AssigneesEdited = true
}
if flags.Changed("label") {
opts.Editable.LabelsEdited = true
}
if flags.Changed("project") {
opts.Editable.ProjectsEdited = true
}
if flags.Changed("milestone") {
opts.Editable.MilestoneEdited = true
}
if !opts.Editable.Dirty() {
opts.Interactive = true
}
if opts.Interactive && !opts.IO.CanPrompt() {
return &cmdutil.FlagError{Err: errors.New("--tile, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively")}
}
if runF != nil {
return runF(opts)
}
return editRun(opts)
},
}
cmd.Flags().StringVarP(&opts.Editable.Title, "title", "t", "", "Revise the pr title.")
cmd.Flags().StringVarP(&opts.Editable.Body, "body", "b", "", "Revise the pr body.")
cmd.Flags().StringSliceVarP(&opts.Editable.Reviewers, "reviewer", "r", nil, "Request reviews from people or teams by their `handle`")
cmd.Flags().StringSliceVarP(&opts.Editable.Assignees, "assignee", "a", nil, "Set assigned people by their `login`. Use \"@me\" to self-assign.")
cmd.Flags().StringSliceVarP(&opts.Editable.Labels, "label", "l", nil, "Set the pr labels by `name`")
cmd.Flags().StringSliceVarP(&opts.Editable.Projects, "project", "p", nil, "Set the projects the pr belongs to by `name`")
cmd.Flags().StringVarP(&opts.Editable.Milestone, "milestone", "m", "", "Set the milestone the pr belongs to by `name`")
return cmd
}
func editRun(opts *EditOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
apiClient := api.NewClientFromHTTP(httpClient)
pr, repo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
if err != nil {
return err
}
editable := opts.Editable
editable.ReviewersAllowed = true
editable.TitleDefault = pr.Title
editable.BodyDefault = pr.Body
editable.ReviewersDefault = pr.ReviewRequests
editable.AssigneesDefault = pr.Assignees
editable.LabelsDefault = pr.Labels
editable.ProjectsDefault = pr.ProjectCards
editable.MilestoneDefault = pr.Milestone
if opts.Interactive {
err = opts.Surveyor.FieldsToEdit(&editable)
if err != nil {
return err
}
}
opts.IO.StartProgressIndicator()
err = opts.Fetcher.EditableOptionsFetch(apiClient, repo, &editable)
opts.IO.StopProgressIndicator()
if err != nil {
return err
}
if opts.Interactive {
editorCommand, err := opts.EditorRetriever.Retrieve()
if err != nil {
return err
}
err = opts.Surveyor.EditFields(&editable, editorCommand)
if err != nil {
return err
}
}
opts.IO.StartProgressIndicator()
err = updatePullRequest(apiClient, repo, pr.ID, editable)
opts.IO.StopProgressIndicator()
if err != nil {
return err
}
fmt.Fprintln(opts.IO.Out, pr.URL)
return nil
}
func updatePullRequest(client *api.Client, repo ghrepo.Interface, id string, editable shared.Editable) error {
var err error
params := githubv4.UpdatePullRequestInput{
PullRequestID: id,
Title: editable.TitleParam(),
Body: editable.BodyParam(),
}
params.AssigneeIDs, err = editable.AssigneesParam(client, repo)
if err != nil {
return err
}
params.LabelIDs, err = editable.LabelsParam()
if err != nil {
return err
}
params.ProjectIDs, err = editable.ProjectsParam()
if err != nil {
return err
}
params.MilestoneID, err = editable.MilestoneParam()
if err != nil {
return err
}
err = api.UpdatePullRequest(client, repo, params)
if err != nil {
return err
}
return updatePullRequestReviews(client, repo, id, editable)
}
func updatePullRequestReviews(client *api.Client, repo ghrepo.Interface, id string, editable shared.Editable) error {
if !editable.ReviewersEdited {
return nil
}
userIds, teamIds, err := editable.ReviewersParams()
if err != nil {
return err
}
union := githubv4.Boolean(false)
reviewsRequestParams := githubv4.RequestReviewsInput{
PullRequestID: id,
Union: &union,
UserIDs: userIds,
TeamIDs: teamIds,
}
return api.UpdatePullRequestReviews(client, repo, reviewsRequestParams)
}
type Surveyor interface {
FieldsToEdit(*shared.Editable) error
EditFields(*shared.Editable, string) error
}
type surveyor struct{}
func (s surveyor) FieldsToEdit(editable *shared.Editable) error {
return shared.FieldsToEditSurvey(editable)
}
func (s surveyor) EditFields(editable *shared.Editable, editorCmd string) error {
return shared.EditFieldsSurvey(editable, editorCmd)
}
type EditableOptionsFetcher interface {
EditableOptionsFetch(*api.Client, ghrepo.Interface, *shared.Editable) error
}
type fetcher struct{}
func (f fetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable) error {
return shared.FetchOptions(client, repo, opts)
}
type EditorRetriever interface {
Retrieve() (string, error)
}
type editorRetriever struct {
config func() (config.Config, error)
}
func (e editorRetriever) Retrieve() (string, error) {
return cmdutil.DetermineEditor(e.config)
}