Skip to content

Commit ad3a590

Browse files
Merge pull request cli#873 from cli/the-merge-dubai
Add support for `gh pr merge`
2 parents 37fd2a7 + 43e1513 commit ad3a590

File tree

3 files changed

+240
-1
lines changed

3 files changed

+240
-1
lines changed

api/queries_pr.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ type PullRequestReviewStatus struct {
143143
ReviewRequired bool
144144
}
145145

146+
type PullRequestMergeMethod int
147+
148+
const (
149+
PullRequestMergeMethodMerge PullRequestMergeMethod = iota
150+
PullRequestMergeMethodRebase
151+
PullRequestMergeMethodSquash
152+
)
153+
146154
func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus {
147155
var status PullRequestReviewStatus
148156
switch pr.ReviewDecision {
@@ -466,6 +474,7 @@ func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, hea
466474
type response struct {
467475
Repository struct {
468476
PullRequests struct {
477+
ID githubv4.ID
469478
Nodes []PullRequest
470479
}
471480
}
@@ -915,6 +924,34 @@ func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) e
915924
return err
916925
}
917926

927+
func PullRequestMerge(client *Client, repo ghrepo.Interface, pr *PullRequest, m PullRequestMergeMethod) error {
928+
mergeMethod := githubv4.PullRequestMergeMethodMerge
929+
switch m {
930+
case PullRequestMergeMethodRebase:
931+
mergeMethod = githubv4.PullRequestMergeMethodRebase
932+
case PullRequestMergeMethodSquash:
933+
mergeMethod = githubv4.PullRequestMergeMethodSquash
934+
}
935+
936+
var mutation struct {
937+
MergePullRequest struct {
938+
PullRequest struct {
939+
ID githubv4.ID
940+
}
941+
} `graphql:"mergePullRequest(input: $input)"`
942+
}
943+
944+
input := githubv4.MergePullRequestInput{
945+
PullRequestID: pr.ID,
946+
MergeMethod: &mergeMethod,
947+
}
948+
949+
v4 := githubv4.NewClient(client.http)
950+
err := v4.Mutate(context.Background(), &mutation, input, nil)
951+
952+
return err
953+
}
954+
918955
func min(a, b int) int {
919956
if a < b {
920957
return a

command/pr.go

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ func init() {
2525
prCmd.AddCommand(prStatusCmd)
2626
prCmd.AddCommand(prCloseCmd)
2727
prCmd.AddCommand(prReopenCmd)
28+
prCmd.AddCommand(prMergeCmd)
29+
prMergeCmd.Flags().BoolP("merge", "m", true, "Merge the commits with the base branch")
30+
prMergeCmd.Flags().BoolP("rebase", "r", false, "Rebase the commits onto the base branch")
31+
prMergeCmd.Flags().BoolP("squash", "s", false, "Squash the commits into one commit and merge it into the base branch")
2832

2933
prCmd.AddCommand(prListCmd)
3034
prListCmd.Flags().IntP("limit", "L", 30, "Maximum number of items to fetch")
@@ -58,7 +62,7 @@ var prStatusCmd = &cobra.Command{
5862
RunE: prStatus,
5963
}
6064
var prViewCmd = &cobra.Command{
61-
Use: "view [{<number> | <url> | <branch>}]",
65+
Use: "view [<number> | <url> | <branch>]",
6266
Short: "View a pull request",
6367
Long: `Display the title, body, and other information about a pull request.
6468
@@ -81,6 +85,13 @@ var prReopenCmd = &cobra.Command{
8185
RunE: prReopen,
8286
}
8387

88+
var prMergeCmd = &cobra.Command{
89+
Use: "merge [<number> | <url> | <branch>]",
90+
Short: "Merge a pull request",
91+
Args: cobra.MaximumNArgs(1),
92+
RunE: prMerge,
93+
}
94+
8495
func prStatus(cmd *cobra.Command, args []string) error {
8596
ctx := contextForCommand(cmd)
8697
apiClient, err := apiClientForContext(ctx)
@@ -100,6 +111,7 @@ func prStatus(cmd *cobra.Command, args []string) error {
100111

101112
repoOverride, _ := cmd.Flags().GetString("repo")
102113
currentPRNumber, currentPRHeadRef, err := prSelectorForCurrentBranch(ctx, baseRepo)
114+
103115
if err != nil && repoOverride == "" && err.Error() != "git: not on any branch" {
104116
return fmt.Errorf("could not query for pull request for current branch: %w", err)
105117
}
@@ -419,6 +431,75 @@ func prReopen(cmd *cobra.Command, args []string) error {
419431
return nil
420432
}
421433

434+
func prMerge(cmd *cobra.Command, args []string) error {
435+
ctx := contextForCommand(cmd)
436+
apiClient, err := apiClientForContext(ctx)
437+
if err != nil {
438+
return err
439+
}
440+
441+
baseRepo, err := determineBaseRepo(cmd, ctx)
442+
if err != nil {
443+
return err
444+
}
445+
446+
var pr *api.PullRequest
447+
if len(args) > 0 {
448+
pr, err = prFromArg(apiClient, baseRepo, args[0])
449+
if err != nil {
450+
return err
451+
}
452+
} else {
453+
prNumber, branchWithOwner, err := prSelectorForCurrentBranch(ctx, baseRepo)
454+
if err != nil {
455+
return err
456+
}
457+
458+
if prNumber != 0 {
459+
pr, err = api.PullRequestByNumber(apiClient, baseRepo, prNumber)
460+
} else {
461+
pr, err = api.PullRequestForBranch(apiClient, baseRepo, "", branchWithOwner)
462+
}
463+
if err != nil {
464+
return err
465+
}
466+
}
467+
468+
if pr.State == "MERGED" {
469+
err := fmt.Errorf("%s Pull request #%d was already merged", utils.Red("!"), pr.Number)
470+
return err
471+
}
472+
473+
rebase, err := cmd.Flags().GetBool("rebase")
474+
if err != nil {
475+
return err
476+
}
477+
squash, err := cmd.Flags().GetBool("squash")
478+
if err != nil {
479+
return err
480+
}
481+
482+
var output string
483+
if rebase {
484+
output = fmt.Sprintf("%s Rebased and merged pull request #%d\n", utils.Green("✔"), pr.Number)
485+
err = api.PullRequestMerge(apiClient, baseRepo, pr, api.PullRequestMergeMethodRebase)
486+
} else if squash {
487+
output = fmt.Sprintf("%s Squashed and merged pull request #%d\n", utils.Green("✔"), pr.Number)
488+
err = api.PullRequestMerge(apiClient, baseRepo, pr, api.PullRequestMergeMethodSquash)
489+
} else {
490+
output = fmt.Sprintf("%s Merged pull request #%d\n", utils.Green("✔"), pr.Number)
491+
err = api.PullRequestMerge(apiClient, baseRepo, pr, api.PullRequestMergeMethodMerge)
492+
}
493+
494+
if err != nil {
495+
return fmt.Errorf("API call failed: %w", err)
496+
}
497+
498+
fmt.Fprint(colorableOut(cmd), output)
499+
500+
return nil
501+
}
502+
422503
func printPrPreview(out io.Writer, pr *api.PullRequest) error {
423504
// Header (Title and State)
424505
fmt.Fprintln(out, utils.Bold(pr.Title))

command/pr_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package command
33
import (
44
"bytes"
55
"encoding/json"
6+
"io"
67
"io/ioutil"
78
"os"
89
"os/exec"
@@ -970,5 +971,125 @@ func TestPRReopen_alreadyMerged(t *testing.T) {
970971
if !r.MatchString(err.Error()) {
971972
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
972973
}
974+
}
975+
976+
type stubResponse struct {
977+
ResponseCode int
978+
ResponseBody io.Reader
979+
}
980+
981+
func initWithStubs(branch string, stubs ...stubResponse) {
982+
initBlankContext("", "OWNER/REPO", branch)
983+
http := initFakeHTTP()
984+
http.StubRepoResponse("OWNER", "REPO")
973985

986+
for _, s := range stubs {
987+
http.StubResponse(s.ResponseCode, s.ResponseBody)
988+
}
989+
}
990+
991+
func TestPrMerge(t *testing.T) {
992+
initWithStubs("master",
993+
stubResponse{200, bytes.NewBufferString(`{ "data": { "repository": {
994+
"pullRequest": { "number": 1, "closed": false, "state": "OPEN"}
995+
} } }`)},
996+
stubResponse{200, bytes.NewBufferString(`{"id": "THE-ID"}`)},
997+
)
998+
999+
output, err := RunCommand("pr merge 1")
1000+
if err != nil {
1001+
t.Fatalf("error running command `pr merge`: %v", err)
1002+
}
1003+
1004+
r := regexp.MustCompile(`Merged pull request #1`)
1005+
1006+
if !r.MatchString(output.String()) {
1007+
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
1008+
}
1009+
}
1010+
1011+
func TestPrMerge_noPrNumberGiven(t *testing.T) {
1012+
cs, cmdTeardown := test.InitCmdStubber()
1013+
defer cmdTeardown()
1014+
1015+
cs.Stub("branch.blueberries.remote origin\nbranch.blueberries.merge refs/heads/blueberries") // git config --get-regexp ^branch\.master\.(remote|merge)
1016+
1017+
jsonFile, _ := os.Open("../test/fixtures/prViewPreviewWithMetadataByBranch.json")
1018+
defer jsonFile.Close()
1019+
1020+
initWithStubs("blueberries",
1021+
stubResponse{200, jsonFile},
1022+
stubResponse{200, bytes.NewBufferString(`{"id": "THE-ID"}`)},
1023+
)
1024+
1025+
output, err := RunCommand("pr merge")
1026+
if err != nil {
1027+
t.Fatalf("error running command `pr merge`: %v", err)
1028+
}
1029+
1030+
r := regexp.MustCompile(`Merged pull request #10`)
1031+
1032+
if !r.MatchString(output.String()) {
1033+
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
1034+
}
1035+
}
1036+
1037+
func TestPrMerge_rebase(t *testing.T) {
1038+
initWithStubs("master",
1039+
stubResponse{200, bytes.NewBufferString(`{ "data": { "repository": {
1040+
"pullRequest": { "number": 2, "closed": false, "state": "OPEN"}
1041+
} } }`)},
1042+
stubResponse{200, bytes.NewBufferString(`{"id": "THE-ID"}`)},
1043+
)
1044+
1045+
output, err := RunCommand("pr merge 2 --rebase")
1046+
if err != nil {
1047+
t.Fatalf("error running command `pr merge`: %v", err)
1048+
}
1049+
1050+
r := regexp.MustCompile(`Rebased and merged pull request #2`)
1051+
1052+
if !r.MatchString(output.String()) {
1053+
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
1054+
}
1055+
}
1056+
1057+
func TestPrMerge_squash(t *testing.T) {
1058+
initWithStubs("master",
1059+
stubResponse{200, bytes.NewBufferString(`{ "data": { "repository": {
1060+
"pullRequest": { "number": 3, "closed": false, "state": "OPEN"}
1061+
} } }`)},
1062+
stubResponse{200, bytes.NewBufferString(`{"id": "THE-ID"}`)},
1063+
)
1064+
1065+
output, err := RunCommand("pr merge 3 --squash")
1066+
if err != nil {
1067+
t.Fatalf("error running command `pr merge`: %v", err)
1068+
}
1069+
1070+
r := regexp.MustCompile(`Squashed and merged pull request #3`)
1071+
1072+
if !r.MatchString(output.String()) {
1073+
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
1074+
}
1075+
}
1076+
1077+
func TestPrMerge_alreadyMerged(t *testing.T) {
1078+
initWithStubs("master",
1079+
stubResponse{200, bytes.NewBufferString(`{ "data": { "repository": {
1080+
"pullRequest": { "number": 4, "closed": true, "state": "MERGED"}
1081+
} } }`)},
1082+
stubResponse{200, bytes.NewBufferString(`{"id": "THE-ID"}`)},
1083+
)
1084+
1085+
output, err := RunCommand("pr merge 4")
1086+
if err == nil {
1087+
t.Fatalf("expected an error running command `pr merge`: %v", err)
1088+
}
1089+
1090+
r := regexp.MustCompile(`Pull request #4 was already merged`)
1091+
1092+
if !r.MatchString(err.Error()) {
1093+
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
1094+
}
9741095
}

0 commit comments

Comments
 (0)