Skip to content

Commit 67bfedd

Browse files
committed
Add pr merge --auto
1 parent f75bd72 commit 67bfedd

File tree

4 files changed

+173
-86
lines changed

4 files changed

+173
-86
lines changed

api/queries_pr.go

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,6 @@ type PullRequestReviewStatus struct {
140140
ReviewRequired bool
141141
}
142142

143-
type PullRequestMergeMethod int
144-
145-
const (
146-
PullRequestMergeMethodMerge PullRequestMergeMethod = iota
147-
PullRequestMergeMethodRebase
148-
PullRequestMergeMethodSquash
149-
)
150-
151143
func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus {
152144
var status PullRequestReviewStatus
153145
switch pr.ReviewDecision {
@@ -1074,47 +1066,6 @@ func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) e
10741066
return err
10751067
}
10761068

1077-
func PullRequestMerge(client *Client, repo ghrepo.Interface, pr *PullRequest, m PullRequestMergeMethod, body *string) error {
1078-
mergeMethod := githubv4.PullRequestMergeMethodMerge
1079-
switch m {
1080-
case PullRequestMergeMethodRebase:
1081-
mergeMethod = githubv4.PullRequestMergeMethodRebase
1082-
case PullRequestMergeMethodSquash:
1083-
mergeMethod = githubv4.PullRequestMergeMethodSquash
1084-
}
1085-
1086-
var mutation struct {
1087-
MergePullRequest struct {
1088-
PullRequest struct {
1089-
ID githubv4.ID
1090-
}
1091-
} `graphql:"mergePullRequest(input: $input)"`
1092-
}
1093-
1094-
input := githubv4.MergePullRequestInput{
1095-
PullRequestID: pr.ID,
1096-
MergeMethod: &mergeMethod,
1097-
}
1098-
1099-
if m == PullRequestMergeMethodSquash {
1100-
commitHeadline := githubv4.String(fmt.Sprintf("%s (#%d)", pr.Title, pr.Number))
1101-
input.CommitHeadline = &commitHeadline
1102-
}
1103-
if body != nil {
1104-
commitBody := githubv4.String(*body)
1105-
input.CommitBody = &commitBody
1106-
}
1107-
1108-
variables := map[string]interface{}{
1109-
"input": input,
1110-
}
1111-
1112-
gql := graphQLClient(client.http, repo.RepoHost())
1113-
err := gql.MutateNamed(context.Background(), "PullRequestMerge", &mutation, variables)
1114-
1115-
return err
1116-
}
1117-
11181069
func PullRequestReady(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
11191070
var mutation struct {
11201071
MarkPullRequestReadyForReview struct {

pkg/cmd/pr/merge/http.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package merge
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/cli/cli/internal/ghinstance"
8+
"github.com/cli/cli/internal/ghrepo"
9+
"github.com/shurcooL/githubv4"
10+
"github.com/shurcooL/graphql"
11+
)
12+
13+
type PullRequestMergeMethod int
14+
15+
const (
16+
PullRequestMergeMethodMerge PullRequestMergeMethod = iota
17+
PullRequestMergeMethodRebase
18+
PullRequestMergeMethodSquash
19+
)
20+
21+
type mergePayload struct {
22+
repo ghrepo.Interface
23+
pullRequestID string
24+
method PullRequestMergeMethod
25+
auto bool
26+
commitSubject string
27+
setCommitSubject bool
28+
commitBody string
29+
setCommitBody bool
30+
}
31+
32+
// TODO: drop after githubv4 gets updated
33+
type EnablePullRequestAutoMergeInput struct {
34+
githubv4.MergePullRequestInput
35+
}
36+
37+
func mergePullRequest(client *http.Client, payload mergePayload) error {
38+
input := githubv4.MergePullRequestInput{
39+
PullRequestID: githubv4.ID(payload.pullRequestID),
40+
}
41+
42+
switch payload.method {
43+
case PullRequestMergeMethodMerge:
44+
m := githubv4.PullRequestMergeMethodMerge
45+
input.MergeMethod = &m
46+
case PullRequestMergeMethodRebase:
47+
m := githubv4.PullRequestMergeMethodRebase
48+
input.MergeMethod = &m
49+
case PullRequestMergeMethodSquash:
50+
m := githubv4.PullRequestMergeMethodSquash
51+
input.MergeMethod = &m
52+
}
53+
54+
if payload.setCommitSubject {
55+
commitHeadline := githubv4.String(payload.commitSubject)
56+
input.CommitHeadline = &commitHeadline
57+
}
58+
if payload.setCommitBody {
59+
commitBody := githubv4.String(payload.commitBody)
60+
input.CommitBody = &commitBody
61+
}
62+
63+
variables := map[string]interface{}{
64+
"input": input,
65+
}
66+
67+
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(payload.repo.RepoHost()), client)
68+
69+
if payload.auto {
70+
var mutation struct {
71+
AnablePullRequestAutoMerge struct {
72+
ClientMutationId string
73+
} `graphql:"enablePullRequestAutoMerge(input: $input)"`
74+
}
75+
variables["input"] = EnablePullRequestAutoMergeInput{input}
76+
return gql.MutateNamed(context.Background(), "PullRequestAutoMerge", &mutation, variables)
77+
}
78+
79+
var mutation struct {
80+
MergePullRequest struct {
81+
ClientMutationId string
82+
} `graphql:"mergePullRequest(input: $input)"`
83+
}
84+
return gql.MutateNamed(context.Background(), "PullRequestMerge", &mutation, variables)
85+
}
86+
87+
func disableAutoMerge(client *http.Client, repo ghrepo.Interface, prID string) error {
88+
var mutation struct {
89+
DisablePullRequestAutoMerge struct {
90+
ClientMutationId string
91+
} `graphql:"disablePullRequestAutoMerge(input: {pullRequestId: $prID})"`
92+
}
93+
94+
variables := map[string]interface{}{
95+
"prID": githubv4.ID(prID),
96+
}
97+
98+
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), client)
99+
return gql.MutateNamed(context.Background(), "PullRequestAutoMergeDisable", &mutation, variables)
100+
}

pkg/cmd/pr/merge/merge.go

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ type MergeOptions struct {
3030

3131
SelectorArg string
3232
DeleteBranch bool
33-
MergeMethod api.PullRequestMergeMethod
33+
MergeMethod PullRequestMergeMethod
34+
35+
AutoMerge bool
36+
AutoMergeDisable bool
3437

3538
Body string
3639

@@ -75,15 +78,15 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm
7578

7679
methodFlags := 0
7780
if flagMerge {
78-
opts.MergeMethod = api.PullRequestMergeMethodMerge
81+
opts.MergeMethod = PullRequestMergeMethodMerge
7982
methodFlags++
8083
}
8184
if flagRebase {
82-
opts.MergeMethod = api.PullRequestMergeMethodRebase
85+
opts.MergeMethod = PullRequestMergeMethodRebase
8386
methodFlags++
8487
}
8588
if flagSquash {
86-
opts.MergeMethod = api.PullRequestMergeMethodSquash
89+
opts.MergeMethod = PullRequestMergeMethodSquash
8790
methodFlags++
8891
}
8992
if methodFlags == 0 {
@@ -110,6 +113,8 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm
110113
cmd.Flags().BoolVarP(&flagMerge, "merge", "m", false, "Merge the commits with the base branch")
111114
cmd.Flags().BoolVarP(&flagRebase, "rebase", "r", false, "Rebase the commits onto the base branch")
112115
cmd.Flags().BoolVarP(&flagSquash, "squash", "s", false, "Squash the commits into one commit and merge it into the base branch")
116+
cmd.Flags().BoolVar(&opts.AutoMerge, "auto", false, "Automatically merge only after necessary requirements are met")
117+
cmd.Flags().BoolVar(&opts.AutoMergeDisable, "disable-auto", false, "Disable auto-merge for this pull request")
113118
return cmd
114119
}
115120

@@ -127,6 +132,19 @@ func mergeRun(opts *MergeOptions) error {
127132
return err
128133
}
129134

135+
isTerminal := opts.IO.IsStdoutTTY()
136+
137+
if opts.AutoMergeDisable {
138+
err := disableAutoMerge(httpClient, baseRepo, pr.ID)
139+
if err != nil {
140+
return err
141+
}
142+
if isTerminal {
143+
fmt.Fprintf(opts.IO.ErrOut, "%s Auto-merge disabled for pull request #%d\n", cs.SuccessIconWithColor(cs.Green), pr.Number)
144+
}
145+
return nil
146+
}
147+
130148
if opts.SelectorArg == "" {
131149
localBranchLastCommit, err := git.LastCommit()
132150
if err == nil {
@@ -144,18 +162,26 @@ func mergeRun(opts *MergeOptions) error {
144162

145163
deleteBranch := opts.DeleteBranch
146164
crossRepoPR := pr.HeadRepositoryOwner.Login != baseRepo.RepoOwner()
147-
isTerminal := opts.IO.IsStdoutTTY()
148165

149166
isPRAlreadyMerged := pr.State == "MERGED"
150167
if !isPRAlreadyMerged {
151-
mergeMethod := opts.MergeMethod
168+
payload := mergePayload{
169+
repo: baseRepo,
170+
pullRequestID: pr.ID,
171+
method: opts.MergeMethod,
172+
auto: opts.AutoMerge,
173+
commitBody: opts.Body,
174+
}
175+
if opts.Body != "" {
176+
payload.setCommitBody = true
177+
}
152178

153179
if opts.InteractiveMode {
154180
r, err := api.GitHubRepo(apiClient, baseRepo)
155181
if err != nil {
156182
return err
157183
}
158-
mergeMethod, err = mergeMethodSurvey(r)
184+
payload.method, err = mergeMethodSurvey(r)
159185
if err != nil {
160186
return err
161187
}
@@ -164,7 +190,7 @@ func mergeRun(opts *MergeOptions) error {
164190
return err
165191
}
166192

167-
allowEditMsg := mergeMethod != api.PullRequestMergeMethodRebase
193+
allowEditMsg := payload.method != PullRequestMergeMethodRebase
168194

169195
action, err := confirmSurvey(allowEditMsg)
170196
if err != nil {
@@ -178,19 +204,19 @@ func mergeRun(opts *MergeOptions) error {
178204
return err
179205
}
180206

181-
if opts.Body == "" {
182-
if mergeMethod == api.PullRequestMergeMethodMerge {
183-
opts.Body = pr.Title
207+
if payload.commitBody == "" {
208+
if payload.method == PullRequestMergeMethodMerge {
209+
payload.commitBody = pr.Title
184210
} else {
185-
opts.Body = pr.ViewerMergeBodyText
211+
payload.commitBody = pr.ViewerMergeBodyText
186212
}
187213
}
188214

189-
msg, err := commitMsgSurvey(opts.Body, editorCommand)
215+
payload.commitBody, err = commitMsgSurvey(payload.commitBody, editorCommand)
190216
if err != nil {
191217
return err
192218
}
193-
opts.Body = msg
219+
payload.setCommitBody = true
194220

195221
action, err = confirmSurvey(false)
196222
if err != nil {
@@ -203,27 +229,38 @@ func mergeRun(opts *MergeOptions) error {
203229
}
204230
}
205231

206-
var body *string
207-
if opts.Body != "" {
208-
body = &opts.Body
232+
if !payload.setCommitSubject && payload.method == PullRequestMergeMethodSquash {
233+
payload.commitSubject = fmt.Sprintf("%s (#%d)", pr.Title, pr.Number)
234+
payload.setCommitSubject = true
209235
}
210236

211-
err = api.PullRequestMerge(apiClient, baseRepo, pr, mergeMethod, body)
237+
err = mergePullRequest(httpClient, payload)
212238
if err != nil {
213239
return err
214240
}
215241

216242
if isTerminal {
217-
action := "Merged"
218-
switch mergeMethod {
219-
case api.PullRequestMergeMethodRebase:
220-
action = "Rebased and merged"
221-
case api.PullRequestMergeMethodSquash:
222-
action = "Squashed and merged"
243+
if payload.auto {
244+
method := ""
245+
switch payload.method {
246+
case PullRequestMergeMethodRebase:
247+
method = " via rebase"
248+
case PullRequestMergeMethodSquash:
249+
method = " via squash"
250+
}
251+
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d will automatically get merged%s when all requiremenets are met\n", cs.SuccessIconWithColor(cs.Green), pr.Number, method)
252+
} else {
253+
action := "Merged"
254+
switch payload.method {
255+
case PullRequestMergeMethodRebase:
256+
action = "Rebased and merged"
257+
case PullRequestMergeMethodSquash:
258+
action = "Squashed and merged"
259+
}
260+
fmt.Fprintf(opts.IO.ErrOut, "%s %s pull request #%d (%s)\n", cs.SuccessIconWithColor(cs.Magenta), action, pr.Number, pr.Title)
223261
}
224-
fmt.Fprintf(opts.IO.ErrOut, "%s %s pull request #%d (%s)\n", cs.SuccessIconWithColor(cs.Magenta), action, pr.Number, pr.Title)
225262
}
226-
} else if !opts.IsDeleteBranchIndicated && opts.InteractiveMode && !crossRepoPR {
263+
} else if !opts.IsDeleteBranchIndicated && opts.InteractiveMode && !crossRepoPR && !opts.AutoMerge {
227264
err := prompt.SurveyAskOne(&survey.Confirm{
228265
Message: fmt.Sprintf("Pull request #%d was already merged. Delete the branch locally?", pr.Number),
229266
Default: false,
@@ -235,7 +272,7 @@ func mergeRun(opts *MergeOptions) error {
235272
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d was already merged\n", cs.WarningIcon(), pr.Number)
236273
}
237274

238-
if !deleteBranch || crossRepoPR {
275+
if !deleteBranch || crossRepoPR || opts.AutoMerge {
239276
return nil
240277
}
241278

@@ -290,23 +327,23 @@ func mergeRun(opts *MergeOptions) error {
290327
return nil
291328
}
292329

293-
func mergeMethodSurvey(baseRepo *api.Repository) (api.PullRequestMergeMethod, error) {
330+
func mergeMethodSurvey(baseRepo *api.Repository) (PullRequestMergeMethod, error) {
294331
type mergeOption struct {
295332
title string
296-
method api.PullRequestMergeMethod
333+
method PullRequestMergeMethod
297334
}
298335

299336
var mergeOpts []mergeOption
300337
if baseRepo.MergeCommitAllowed {
301-
opt := mergeOption{title: "Create a merge commit", method: api.PullRequestMergeMethodMerge}
338+
opt := mergeOption{title: "Create a merge commit", method: PullRequestMergeMethodMerge}
302339
mergeOpts = append(mergeOpts, opt)
303340
}
304341
if baseRepo.RebaseMergeAllowed {
305-
opt := mergeOption{title: "Rebase and merge", method: api.PullRequestMergeMethodRebase}
342+
opt := mergeOption{title: "Rebase and merge", method: PullRequestMergeMethodRebase}
306343
mergeOpts = append(mergeOpts, opt)
307344
}
308345
if baseRepo.SquashMergeAllowed {
309-
opt := mergeOption{title: "Squash and merge", method: api.PullRequestMergeMethodSquash}
346+
opt := mergeOption{title: "Squash and merge", method: PullRequestMergeMethodSquash}
310347
mergeOpts = append(mergeOpts, opt)
311348
}
312349

@@ -318,7 +355,6 @@ func mergeMethodSurvey(baseRepo *api.Repository) (api.PullRequestMergeMethod, er
318355
mergeQuestion := &survey.Select{
319356
Message: "What merge method would you like to use?",
320357
Options: surveyOpts,
321-
Default: "Create a merge commit",
322358
}
323359

324360
var result int

0 commit comments

Comments
 (0)