diff --git a/pkg/cmd/pr/update/update.go b/pkg/cmd/pr/update/update.go new file mode 100644 index 000000000..5ce98c88f --- /dev/null +++ b/pkg/cmd/pr/update/update.go @@ -0,0 +1,120 @@ +package update + +import ( + "fmt" + "net/http" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghrepo" + shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/shurcooL/githubv4" + "github.com/spf13/cobra" +) + +type UpdateOptions struct { + HttpClient func() (*http.Client, error) + IO *iostreams.IOStreams + GitClient *git.Client + + Finder shared.PRFinder + + SelectorArg string + Rebase bool +} + +func NewCmdUpdate(f *cmdutil.Factory, runF func(*UpdateOptions) error) *cobra.Command { + opts := &UpdateOptions{ + IO: f.IOStreams, + HttpClient: f.HttpClient, + GitClient: f.GitClient, + } + + cmd := &cobra.Command{ + Use: "update [ | | ]", + Short: "Update a pull request branch", + Long: heredoc.Docf(` + Update a pull request branch with latest changes of the base branch. + + Without an argument, the pull request that belongs to the current branch is selected. + + The default behavior is to update with a merge commit (i.e., merging the base branch + into the the PR's branch). To reconcile the changes with rebasing on top of the base + branch, the %[1]s--rebase%[1]s option should be provided. + `, "`"), + Example: heredoc.Doc(` + $ gh pr update 23 + $ gh pr update 23 --rebase + `), + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.Finder = shared.NewFinder(f) + + if len(args) > 0 { + opts.SelectorArg = args[0] + } + + if runF != nil { + return runF(opts) + } + + return updateRun(opts) + }, + } + + cmd.Flags().BoolVar(&opts.Rebase, "rebase", false, "Update PR branch by rebasing on top of latest base branch") + + _ = cmdutil.RegisterBranchCompletionFlags(f.GitClient, cmd, "base") + + return cmd +} + +func updateRun(opts *UpdateOptions) error { + findOptions := shared.FindOptions{ + Selector: opts.SelectorArg, + Fields: []string{"id", "headRefOid"}, + } + + pr, repo, err := opts.Finder.Find(findOptions) + if err != nil { + return err + } + + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + apiClient := api.NewClientFromHTTP(httpClient) + + opts.IO.StartProgressIndicator() + err = updatePullRequestBranch(apiClient, repo, pr.ID, pr.HeadRefOid, opts.Rebase) + opts.IO.StopProgressIndicator() + + if err != nil { + return err + } + + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.ErrOut, "%s PR branch updated\n", cs.SuccessIcon()) + return nil +} + +// updatePullRequestBranch calls the GraphQL API endpoint to update the given PR +// branch with latest changes of its base. +func updatePullRequestBranch(apiClient *api.Client, repo ghrepo.Interface, pullRequestID string, expectedHeadOid string, rebase bool) error { + updateMethod := githubv4.PullRequestBranchUpdateMethodMerge + if rebase { + updateMethod = githubv4.PullRequestBranchUpdateMethodRebase + } + + params := githubv4.UpdatePullRequestBranchInput{ + PullRequestID: pullRequestID, + ExpectedHeadOid: (*githubv4.GitObjectID)(&expectedHeadOid), + UpdateMethod: &updateMethod, + } + + return api.UpdatePullRequestBranch(apiClient, repo, params) +}