github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/pr/merge/merge_test.go (about) 1 package merge 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "path/filepath" 10 "regexp" 11 "strings" 12 "testing" 13 14 "github.com/MakeNowJust/heredoc" 15 "github.com/cli/cli/api" 16 "github.com/cli/cli/internal/ghrepo" 17 "github.com/cli/cli/internal/run" 18 "github.com/cli/cli/pkg/cmd/pr/shared" 19 "github.com/cli/cli/pkg/cmdutil" 20 "github.com/cli/cli/pkg/httpmock" 21 "github.com/cli/cli/pkg/iostreams" 22 "github.com/cli/cli/pkg/prompt" 23 "github.com/cli/cli/test" 24 "github.com/google/shlex" 25 "github.com/stretchr/testify/assert" 26 "github.com/stretchr/testify/require" 27 ) 28 29 func Test_NewCmdMerge(t *testing.T) { 30 tmpFile := filepath.Join(t.TempDir(), "my-body.md") 31 err := ioutil.WriteFile(tmpFile, []byte("a body from file"), 0600) 32 require.NoError(t, err) 33 34 tests := []struct { 35 name string 36 args string 37 stdin string 38 isTTY bool 39 want MergeOptions 40 wantErr string 41 }{ 42 { 43 name: "number argument", 44 args: "123", 45 isTTY: true, 46 want: MergeOptions{ 47 SelectorArg: "123", 48 DeleteBranch: false, 49 IsDeleteBranchIndicated: false, 50 CanDeleteLocalBranch: true, 51 MergeMethod: PullRequestMergeMethodMerge, 52 InteractiveMode: true, 53 Body: "", 54 BodySet: false, 55 }, 56 }, 57 { 58 name: "delete-branch specified", 59 args: "--delete-branch=false", 60 isTTY: true, 61 want: MergeOptions{ 62 SelectorArg: "", 63 DeleteBranch: false, 64 IsDeleteBranchIndicated: true, 65 CanDeleteLocalBranch: true, 66 MergeMethod: PullRequestMergeMethodMerge, 67 InteractiveMode: true, 68 Body: "", 69 BodySet: false, 70 }, 71 }, 72 { 73 name: "body from file", 74 args: fmt.Sprintf("123 --body-file '%s'", tmpFile), 75 isTTY: true, 76 want: MergeOptions{ 77 SelectorArg: "123", 78 DeleteBranch: false, 79 IsDeleteBranchIndicated: false, 80 CanDeleteLocalBranch: true, 81 MergeMethod: PullRequestMergeMethodMerge, 82 InteractiveMode: true, 83 Body: "a body from file", 84 BodySet: true, 85 }, 86 }, 87 { 88 name: "body from stdin", 89 args: "123 --body-file -", 90 stdin: "this is on standard input", 91 isTTY: true, 92 want: MergeOptions{ 93 SelectorArg: "123", 94 DeleteBranch: false, 95 IsDeleteBranchIndicated: false, 96 CanDeleteLocalBranch: true, 97 MergeMethod: PullRequestMergeMethodMerge, 98 InteractiveMode: true, 99 Body: "this is on standard input", 100 BodySet: true, 101 }, 102 }, 103 { 104 name: "body", 105 args: "123 -bcool", 106 isTTY: true, 107 want: MergeOptions{ 108 SelectorArg: "123", 109 DeleteBranch: false, 110 IsDeleteBranchIndicated: false, 111 CanDeleteLocalBranch: true, 112 MergeMethod: PullRequestMergeMethodMerge, 113 InteractiveMode: true, 114 Body: "cool", 115 BodySet: true, 116 }, 117 }, 118 { 119 name: "body and body-file flags", 120 args: "123 --body 'test' --body-file 'test-file.txt'", 121 isTTY: true, 122 wantErr: "specify only one of `--body` or `--body-file`", 123 }, 124 { 125 name: "no argument with --repo override", 126 args: "-R owner/repo", 127 isTTY: true, 128 wantErr: "argument required when using the --repo flag", 129 }, 130 { 131 name: "insufficient flags in non-interactive mode", 132 args: "123", 133 isTTY: false, 134 wantErr: "--merge, --rebase, or --squash required when not running interactively", 135 }, 136 { 137 name: "multiple merge methods", 138 args: "123 --merge --rebase", 139 isTTY: true, 140 wantErr: "only one of --merge, --rebase, or --squash can be enabled", 141 }, 142 { 143 name: "multiple merge methods, non-tty", 144 args: "123 --merge --rebase", 145 isTTY: false, 146 wantErr: "only one of --merge, --rebase, or --squash can be enabled", 147 }, 148 } 149 for _, tt := range tests { 150 t.Run(tt.name, func(t *testing.T) { 151 io, stdin, _, _ := iostreams.Test() 152 io.SetStdoutTTY(tt.isTTY) 153 io.SetStdinTTY(tt.isTTY) 154 io.SetStderrTTY(tt.isTTY) 155 156 if tt.stdin != "" { 157 _, _ = stdin.WriteString(tt.stdin) 158 } 159 160 f := &cmdutil.Factory{ 161 IOStreams: io, 162 } 163 164 var opts *MergeOptions 165 cmd := NewCmdMerge(f, func(o *MergeOptions) error { 166 opts = o 167 return nil 168 }) 169 cmd.PersistentFlags().StringP("repo", "R", "", "") 170 171 argv, err := shlex.Split(tt.args) 172 require.NoError(t, err) 173 cmd.SetArgs(argv) 174 175 cmd.SetIn(&bytes.Buffer{}) 176 cmd.SetOut(ioutil.Discard) 177 cmd.SetErr(ioutil.Discard) 178 179 _, err = cmd.ExecuteC() 180 if tt.wantErr != "" { 181 require.EqualError(t, err, tt.wantErr) 182 return 183 } else { 184 require.NoError(t, err) 185 } 186 187 assert.Equal(t, tt.want.SelectorArg, opts.SelectorArg) 188 assert.Equal(t, tt.want.DeleteBranch, opts.DeleteBranch) 189 assert.Equal(t, tt.want.CanDeleteLocalBranch, opts.CanDeleteLocalBranch) 190 assert.Equal(t, tt.want.MergeMethod, opts.MergeMethod) 191 assert.Equal(t, tt.want.InteractiveMode, opts.InteractiveMode) 192 assert.Equal(t, tt.want.Body, opts.Body) 193 assert.Equal(t, tt.want.BodySet, opts.BodySet) 194 }) 195 } 196 } 197 198 func baseRepo(owner, repo, branch string) ghrepo.Interface { 199 return api.InitRepoHostname(&api.Repository{ 200 Name: repo, 201 Owner: api.RepositoryOwner{Login: owner}, 202 DefaultBranchRef: api.BranchRef{Name: branch}, 203 }, "github.com") 204 } 205 206 func stubCommit(pr *api.PullRequest, oid string) { 207 pr.Commits.Nodes = append(pr.Commits.Nodes, api.PullRequestCommit{ 208 Commit: api.PullRequestCommitCommit{OID: oid}, 209 }) 210 } 211 212 func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*test.CmdOut, error) { 213 io, _, stdout, stderr := iostreams.Test() 214 io.SetStdoutTTY(isTTY) 215 io.SetStdinTTY(isTTY) 216 io.SetStderrTTY(isTTY) 217 218 factory := &cmdutil.Factory{ 219 IOStreams: io, 220 HttpClient: func() (*http.Client, error) { 221 return &http.Client{Transport: rt}, nil 222 }, 223 Branch: func() (string, error) { 224 return branch, nil 225 }, 226 } 227 228 cmd := NewCmdMerge(factory, nil) 229 cmd.PersistentFlags().StringP("repo", "R", "", "") 230 231 cli = strings.TrimPrefix(cli, "pr merge") 232 argv, err := shlex.Split(cli) 233 if err != nil { 234 return nil, err 235 } 236 cmd.SetArgs(argv) 237 238 cmd.SetIn(&bytes.Buffer{}) 239 cmd.SetOut(ioutil.Discard) 240 cmd.SetErr(ioutil.Discard) 241 242 _, err = cmd.ExecuteC() 243 return &test.CmdOut{ 244 OutBuf: stdout, 245 ErrBuf: stderr, 246 }, err 247 } 248 249 func initFakeHTTP() *httpmock.Registry { 250 return &httpmock.Registry{} 251 } 252 253 func TestPrMerge(t *testing.T) { 254 http := initFakeHTTP() 255 defer http.Verify(t) 256 257 shared.RunCommandFinder( 258 "1", 259 &api.PullRequest{ 260 ID: "THE-ID", 261 Number: 1, 262 State: "OPEN", 263 Title: "The title of the PR", 264 MergeStateStatus: "CLEAN", 265 }, 266 baseRepo("OWNER", "REPO", "master"), 267 ) 268 269 http.Register( 270 httpmock.GraphQL(`mutation PullRequestMerge\b`), 271 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 272 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 273 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 274 assert.NotContains(t, input, "commitHeadline") 275 })) 276 277 _, cmdTeardown := run.Stub() 278 defer cmdTeardown(t) 279 280 output, err := runCommand(http, "master", true, "pr merge 1 --merge") 281 if err != nil { 282 t.Fatalf("error running command `pr merge`: %v", err) 283 } 284 285 r := regexp.MustCompile(`Merged pull request #1 \(The title of the PR\)`) 286 287 if !r.MatchString(output.Stderr()) { 288 t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) 289 } 290 } 291 292 func TestPrMerge_blocked(t *testing.T) { 293 http := initFakeHTTP() 294 defer http.Verify(t) 295 296 shared.RunCommandFinder( 297 "1", 298 &api.PullRequest{ 299 ID: "THE-ID", 300 Number: 1, 301 State: "OPEN", 302 Title: "The title of the PR", 303 MergeStateStatus: "BLOCKED", 304 }, 305 baseRepo("OWNER", "REPO", "master"), 306 ) 307 308 _, cmdTeardown := run.Stub() 309 defer cmdTeardown(t) 310 311 output, err := runCommand(http, "master", true, "pr merge 1 --merge") 312 assert.EqualError(t, err, "SilentError") 313 314 assert.Equal(t, "", output.String()) 315 assert.Equal(t, heredoc.Docf(` 316 X Pull request #1 is not mergeable: the base branch policy prohibits the merge. 317 To have the pull request merged after all the requirements have been met, add the %[1]s--auto%[1]s flag. 318 To use administrator privileges to immediately merge the pull request, add the %[1]s--admin%[1]s flag. 319 `, "`"), output.Stderr()) 320 } 321 322 func TestPrMerge_nontty(t *testing.T) { 323 http := initFakeHTTP() 324 defer http.Verify(t) 325 326 shared.RunCommandFinder( 327 "1", 328 &api.PullRequest{ 329 ID: "THE-ID", 330 Number: 1, 331 State: "OPEN", 332 Title: "The title of the PR", 333 MergeStateStatus: "CLEAN", 334 }, 335 baseRepo("OWNER", "REPO", "master"), 336 ) 337 338 http.Register( 339 httpmock.GraphQL(`mutation PullRequestMerge\b`), 340 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 341 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 342 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 343 assert.NotContains(t, input, "commitHeadline") 344 })) 345 346 _, cmdTeardown := run.Stub() 347 defer cmdTeardown(t) 348 349 output, err := runCommand(http, "master", false, "pr merge 1 --merge") 350 if err != nil { 351 t.Fatalf("error running command `pr merge`: %v", err) 352 } 353 354 assert.Equal(t, "", output.String()) 355 assert.Equal(t, "", output.Stderr()) 356 } 357 358 func TestPrMerge_withRepoFlag(t *testing.T) { 359 http := initFakeHTTP() 360 defer http.Verify(t) 361 362 shared.RunCommandFinder( 363 "1", 364 &api.PullRequest{ 365 ID: "THE-ID", 366 Number: 1, 367 State: "OPEN", 368 Title: "The title of the PR", 369 MergeStateStatus: "CLEAN", 370 }, 371 baseRepo("OWNER", "REPO", "master"), 372 ) 373 374 http.Register( 375 httpmock.GraphQL(`mutation PullRequestMerge\b`), 376 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 377 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 378 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 379 assert.NotContains(t, input, "commitHeadline") 380 })) 381 382 _, cmdTeardown := run.Stub() 383 defer cmdTeardown(t) 384 385 output, err := runCommand(http, "master", true, "pr merge 1 --merge -R OWNER/REPO") 386 if err != nil { 387 t.Fatalf("error running command `pr merge`: %v", err) 388 } 389 390 r := regexp.MustCompile(`Merged pull request #1 \(The title of the PR\)`) 391 392 if !r.MatchString(output.Stderr()) { 393 t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) 394 } 395 } 396 397 func TestPrMerge_deleteBranch(t *testing.T) { 398 http := initFakeHTTP() 399 defer http.Verify(t) 400 401 shared.RunCommandFinder( 402 "", 403 &api.PullRequest{ 404 ID: "PR_10", 405 Number: 10, 406 State: "OPEN", 407 Title: "Blueberries are a good fruit", 408 HeadRefName: "blueberries", 409 MergeStateStatus: "CLEAN", 410 }, 411 baseRepo("OWNER", "REPO", "master"), 412 ) 413 414 http.Register( 415 httpmock.GraphQL(`mutation PullRequestMerge\b`), 416 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 417 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 418 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 419 assert.NotContains(t, input, "commitHeadline") 420 })) 421 http.Register( 422 httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), 423 httpmock.StringResponse(`{}`)) 424 425 cs, cmdTeardown := run.Stub() 426 defer cmdTeardown(t) 427 428 cs.Register(`git checkout master`, 0, "") 429 cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") 430 cs.Register(`git branch -D blueberries`, 0, "") 431 432 output, err := runCommand(http, "blueberries", true, `pr merge --merge --delete-branch`) 433 if err != nil { 434 t.Fatalf("Got unexpected error running `pr merge` %s", err) 435 } 436 437 assert.Equal(t, "", output.String()) 438 assert.Equal(t, heredoc.Doc(` 439 ✓ Merged pull request #10 (Blueberries are a good fruit) 440 ✓ Deleted branch blueberries and switched to branch master 441 `), output.Stderr()) 442 } 443 444 func TestPrMerge_deleteNonCurrentBranch(t *testing.T) { 445 http := initFakeHTTP() 446 defer http.Verify(t) 447 448 shared.RunCommandFinder( 449 "blueberries", 450 &api.PullRequest{ 451 ID: "PR_10", 452 Number: 10, 453 State: "OPEN", 454 Title: "Blueberries are a good fruit", 455 HeadRefName: "blueberries", 456 MergeStateStatus: "CLEAN", 457 }, 458 baseRepo("OWNER", "REPO", "master"), 459 ) 460 461 http.Register( 462 httpmock.GraphQL(`mutation PullRequestMerge\b`), 463 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 464 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 465 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 466 assert.NotContains(t, input, "commitHeadline") 467 })) 468 http.Register( 469 httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), 470 httpmock.StringResponse(`{}`)) 471 472 cs, cmdTeardown := run.Stub() 473 defer cmdTeardown(t) 474 475 cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") 476 cs.Register(`git branch -D blueberries`, 0, "") 477 478 output, err := runCommand(http, "master", true, `pr merge --merge --delete-branch blueberries`) 479 if err != nil { 480 t.Fatalf("Got unexpected error running `pr merge` %s", err) 481 } 482 483 assert.Equal(t, "", output.String()) 484 assert.Equal(t, heredoc.Doc(` 485 ✓ Merged pull request #10 (Blueberries are a good fruit) 486 ✓ Deleted branch blueberries 487 `), output.Stderr()) 488 } 489 490 func Test_nonDivergingPullRequest(t *testing.T) { 491 http := initFakeHTTP() 492 defer http.Verify(t) 493 494 pr := &api.PullRequest{ 495 ID: "PR_10", 496 Number: 10, 497 Title: "Blueberries are a good fruit", 498 State: "OPEN", 499 MergeStateStatus: "CLEAN", 500 } 501 stubCommit(pr, "COMMITSHA1") 502 503 prFinder := shared.RunCommandFinder("", pr, baseRepo("OWNER", "REPO", "master")) 504 prFinder.ExpectFields([]string{"id", "number", "state", "title", "lastCommit", "mergeStateStatus", "headRepositoryOwner", "headRefName"}) 505 506 http.Register( 507 httpmock.GraphQL(`mutation PullRequestMerge\b`), 508 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 509 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 510 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 511 assert.NotContains(t, input, "commitHeadline") 512 })) 513 514 cs, cmdTeardown := run.Stub() 515 defer cmdTeardown(t) 516 517 cs.Register(`git .+ show .+ HEAD`, 0, "COMMITSHA1,title") 518 519 output, err := runCommand(http, "blueberries", true, "pr merge --merge") 520 if err != nil { 521 t.Fatalf("error running command `pr merge`: %v", err) 522 } 523 524 assert.Equal(t, heredoc.Doc(` 525 ✓ Merged pull request #10 (Blueberries are a good fruit) 526 `), output.Stderr()) 527 } 528 529 func Test_divergingPullRequestWarning(t *testing.T) { 530 http := initFakeHTTP() 531 defer http.Verify(t) 532 533 pr := &api.PullRequest{ 534 ID: "PR_10", 535 Number: 10, 536 Title: "Blueberries are a good fruit", 537 State: "OPEN", 538 MergeStateStatus: "CLEAN", 539 } 540 stubCommit(pr, "COMMITSHA1") 541 542 prFinder := shared.RunCommandFinder("", pr, baseRepo("OWNER", "REPO", "master")) 543 prFinder.ExpectFields([]string{"id", "number", "state", "title", "lastCommit", "mergeStateStatus", "headRepositoryOwner", "headRefName"}) 544 545 http.Register( 546 httpmock.GraphQL(`mutation PullRequestMerge\b`), 547 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 548 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 549 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 550 assert.NotContains(t, input, "commitHeadline") 551 })) 552 553 cs, cmdTeardown := run.Stub() 554 defer cmdTeardown(t) 555 556 cs.Register(`git .+ show .+ HEAD`, 0, "COMMITSHA2,title") 557 558 output, err := runCommand(http, "blueberries", true, "pr merge --merge") 559 if err != nil { 560 t.Fatalf("error running command `pr merge`: %v", err) 561 } 562 563 assert.Equal(t, heredoc.Doc(` 564 ! Pull request #10 (Blueberries are a good fruit) has diverged from local branch 565 ✓ Merged pull request #10 (Blueberries are a good fruit) 566 `), output.Stderr()) 567 } 568 569 func Test_pullRequestWithoutCommits(t *testing.T) { 570 http := initFakeHTTP() 571 defer http.Verify(t) 572 573 shared.RunCommandFinder( 574 "", 575 &api.PullRequest{ 576 ID: "PR_10", 577 Number: 10, 578 Title: "Blueberries are a good fruit", 579 State: "OPEN", 580 MergeStateStatus: "CLEAN", 581 }, 582 baseRepo("OWNER", "REPO", "master"), 583 ) 584 585 http.Register( 586 httpmock.GraphQL(`mutation PullRequestMerge\b`), 587 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 588 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 589 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 590 assert.NotContains(t, input, "commitHeadline") 591 })) 592 593 _, cmdTeardown := run.Stub() 594 defer cmdTeardown(t) 595 596 output, err := runCommand(http, "blueberries", true, "pr merge --merge") 597 if err != nil { 598 t.Fatalf("error running command `pr merge`: %v", err) 599 } 600 601 assert.Equal(t, heredoc.Doc(` 602 ✓ Merged pull request #10 (Blueberries are a good fruit) 603 `), output.Stderr()) 604 } 605 606 func TestPrMerge_rebase(t *testing.T) { 607 http := initFakeHTTP() 608 defer http.Verify(t) 609 610 shared.RunCommandFinder( 611 "2", 612 &api.PullRequest{ 613 ID: "THE-ID", 614 Number: 2, 615 Title: "The title of the PR", 616 State: "OPEN", 617 MergeStateStatus: "CLEAN", 618 }, 619 baseRepo("OWNER", "REPO", "master"), 620 ) 621 622 http.Register( 623 httpmock.GraphQL(`mutation PullRequestMerge\b`), 624 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 625 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 626 assert.Equal(t, "REBASE", input["mergeMethod"].(string)) 627 assert.NotContains(t, input, "commitHeadline") 628 })) 629 630 _, cmdTeardown := run.Stub() 631 defer cmdTeardown(t) 632 633 output, err := runCommand(http, "master", true, "pr merge 2 --rebase") 634 if err != nil { 635 t.Fatalf("error running command `pr merge`: %v", err) 636 } 637 638 r := regexp.MustCompile(`Rebased and merged pull request #2 \(The title of the PR\)`) 639 640 if !r.MatchString(output.Stderr()) { 641 t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) 642 } 643 } 644 645 func TestPrMerge_squash(t *testing.T) { 646 http := initFakeHTTP() 647 defer http.Verify(t) 648 649 shared.RunCommandFinder( 650 "3", 651 &api.PullRequest{ 652 ID: "THE-ID", 653 Number: 3, 654 Title: "The title of the PR", 655 State: "OPEN", 656 MergeStateStatus: "CLEAN", 657 }, 658 baseRepo("OWNER", "REPO", "master"), 659 ) 660 661 http.Register( 662 httpmock.GraphQL(`mutation PullRequestMerge\b`), 663 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 664 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 665 assert.Equal(t, "SQUASH", input["mergeMethod"].(string)) 666 assert.NotContains(t, input, "commitHeadline") 667 })) 668 669 _, cmdTeardown := run.Stub() 670 defer cmdTeardown(t) 671 672 output, err := runCommand(http, "master", true, "pr merge 3 --squash") 673 if err != nil { 674 t.Fatalf("error running command `pr merge`: %v", err) 675 } 676 677 assert.Equal(t, "", output.String()) 678 assert.Equal(t, heredoc.Doc(` 679 ✓ Squashed and merged pull request #3 (The title of the PR) 680 `), output.Stderr()) 681 } 682 683 func TestPrMerge_alreadyMerged(t *testing.T) { 684 http := initFakeHTTP() 685 defer http.Verify(t) 686 687 shared.RunCommandFinder( 688 "4", 689 &api.PullRequest{ 690 ID: "THE-ID", 691 Number: 4, 692 State: "MERGED", 693 HeadRefName: "blueberries", 694 BaseRefName: "master", 695 MergeStateStatus: "CLEAN", 696 }, 697 baseRepo("OWNER", "REPO", "master"), 698 ) 699 700 cs, cmdTeardown := run.Stub() 701 defer cmdTeardown(t) 702 703 cs.Register(`git checkout master`, 0, "") 704 cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") 705 cs.Register(`git branch -D blueberries`, 0, "") 706 707 as, surveyTeardown := prompt.InitAskStubber() 708 defer surveyTeardown() 709 as.StubOne(true) 710 711 output, err := runCommand(http, "blueberries", true, "pr merge 4") 712 assert.NoError(t, err) 713 assert.Equal(t, "", output.String()) 714 assert.Equal(t, "✓ Deleted branch blueberries and switched to branch master\n", output.Stderr()) 715 } 716 717 func TestPrMerge_alreadyMerged_nonInteractive(t *testing.T) { 718 http := initFakeHTTP() 719 defer http.Verify(t) 720 721 shared.RunCommandFinder( 722 "4", 723 &api.PullRequest{ 724 ID: "THE-ID", 725 Number: 4, 726 State: "MERGED", 727 HeadRepositoryOwner: api.Owner{Login: "monalisa"}, 728 MergeStateStatus: "CLEAN", 729 }, 730 baseRepo("OWNER", "REPO", "master"), 731 ) 732 733 _, cmdTeardown := run.Stub() 734 defer cmdTeardown(t) 735 736 output, err := runCommand(http, "blueberries", true, "pr merge 4 --merge") 737 if err != nil { 738 t.Fatalf("Got unexpected error running `pr merge` %s", err) 739 } 740 741 assert.Equal(t, "", output.String()) 742 assert.Equal(t, "! Pull request #4 was already merged\n", output.Stderr()) 743 } 744 745 func TestPRMerge_interactive(t *testing.T) { 746 http := initFakeHTTP() 747 defer http.Verify(t) 748 749 shared.RunCommandFinder( 750 "", 751 &api.PullRequest{ 752 ID: "THE-ID", 753 Number: 3, 754 Title: "It was the best of times", 755 HeadRefName: "blueberries", 756 MergeStateStatus: "CLEAN", 757 }, 758 baseRepo("OWNER", "REPO", "master"), 759 ) 760 761 http.Register( 762 httpmock.GraphQL(`query RepositoryInfo\b`), 763 httpmock.StringResponse(` 764 { "data": { "repository": { 765 "mergeCommitAllowed": true, 766 "rebaseMergeAllowed": true, 767 "squashMergeAllowed": true 768 } } }`)) 769 http.Register( 770 httpmock.GraphQL(`mutation PullRequestMerge\b`), 771 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 772 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 773 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 774 assert.NotContains(t, input, "commitHeadline") 775 })) 776 777 _, cmdTeardown := run.Stub() 778 defer cmdTeardown(t) 779 780 as, surveyTeardown := prompt.InitAskStubber() 781 defer surveyTeardown() 782 783 as.StubOne(0) // Merge method survey 784 as.StubOne(false) // Delete branch survey 785 as.StubOne("Submit") // Confirm submit survey 786 787 output, err := runCommand(http, "blueberries", true, "") 788 if err != nil { 789 t.Fatalf("Got unexpected error running `pr merge` %s", err) 790 } 791 792 //nolint:staticcheck // prefer exact matchers over ExpectLines 793 test.ExpectLines(t, output.Stderr(), "Merged pull request #3") 794 } 795 796 func TestPRMerge_interactiveWithDeleteBranch(t *testing.T) { 797 http := initFakeHTTP() 798 defer http.Verify(t) 799 800 shared.RunCommandFinder( 801 "", 802 &api.PullRequest{ 803 ID: "THE-ID", 804 Number: 3, 805 Title: "It was the best of times", 806 HeadRefName: "blueberries", 807 MergeStateStatus: "CLEAN", 808 }, 809 baseRepo("OWNER", "REPO", "master"), 810 ) 811 812 http.Register( 813 httpmock.GraphQL(`query RepositoryInfo\b`), 814 httpmock.StringResponse(` 815 { "data": { "repository": { 816 "mergeCommitAllowed": true, 817 "rebaseMergeAllowed": true, 818 "squashMergeAllowed": true 819 } } }`)) 820 http.Register( 821 httpmock.GraphQL(`mutation PullRequestMerge\b`), 822 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 823 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 824 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 825 assert.NotContains(t, input, "commitHeadline") 826 })) 827 http.Register( 828 httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), 829 httpmock.StringResponse(`{}`)) 830 831 cs, cmdTeardown := run.Stub() 832 defer cmdTeardown(t) 833 834 cs.Register(`git checkout master`, 0, "") 835 cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") 836 cs.Register(`git branch -D blueberries`, 0, "") 837 838 as, surveyTeardown := prompt.InitAskStubber() 839 defer surveyTeardown() 840 841 as.StubOne(0) // Merge method survey 842 as.StubOne("Submit") // Confirm submit survey 843 844 output, err := runCommand(http, "blueberries", true, "-d") 845 if err != nil { 846 t.Fatalf("Got unexpected error running `pr merge` %s", err) 847 } 848 849 assert.Equal(t, "", output.String()) 850 assert.Equal(t, heredoc.Doc(` 851 ✓ Merged pull request #3 (It was the best of times) 852 ✓ Deleted branch blueberries and switched to branch master 853 `), output.Stderr()) 854 } 855 856 func TestPRMerge_interactiveSquashEditCommitMsg(t *testing.T) { 857 io, _, stdout, stderr := iostreams.Test() 858 io.SetStdoutTTY(true) 859 io.SetStderrTTY(true) 860 861 tr := initFakeHTTP() 862 defer tr.Verify(t) 863 tr.Register( 864 httpmock.GraphQL(`query RepositoryInfo\b`), 865 httpmock.StringResponse(` 866 { "data": { "repository": { 867 "mergeCommitAllowed": true, 868 "rebaseMergeAllowed": true, 869 "squashMergeAllowed": true 870 } } }`)) 871 tr.Register( 872 httpmock.GraphQL(`query PullRequestMergeText\b`), 873 httpmock.StringResponse(` 874 { "data": { "node": { 875 "viewerMergeBodyText": "default body text" 876 } } }`)) 877 tr.Register( 878 httpmock.GraphQL(`mutation PullRequestMerge\b`), 879 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 880 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 881 assert.Equal(t, "SQUASH", input["mergeMethod"].(string)) 882 assert.Equal(t, "DEFAULT BODY TEXT", input["commitBody"].(string)) 883 })) 884 885 _, cmdTeardown := run.Stub() 886 defer cmdTeardown(t) 887 888 as, surveyTeardown := prompt.InitAskStubber() 889 defer surveyTeardown() 890 891 as.StubOne(2) // Merge method survey 892 as.StubOne(false) // Delete branch survey 893 as.StubOne("Edit commit message") // Confirm submit survey 894 as.StubOne("Submit") // Confirm submit survey 895 896 err := mergeRun(&MergeOptions{ 897 IO: io, 898 Editor: testEditor{}, 899 HttpClient: func() (*http.Client, error) { 900 return &http.Client{Transport: tr}, nil 901 }, 902 SelectorArg: "https://github.com/OWNER/REPO/pull/123", 903 InteractiveMode: true, 904 Finder: shared.NewMockFinder( 905 "https://github.com/OWNER/REPO/pull/123", 906 &api.PullRequest{ID: "THE-ID", Number: 123, Title: "title", MergeStateStatus: "CLEAN"}, 907 ghrepo.New("OWNER", "REPO"), 908 ), 909 }) 910 assert.NoError(t, err) 911 912 assert.Equal(t, "", stdout.String()) 913 assert.Equal(t, "✓ Squashed and merged pull request #123 (title)\n", stderr.String()) 914 } 915 916 func TestPRMerge_interactiveCancelled(t *testing.T) { 917 http := initFakeHTTP() 918 defer http.Verify(t) 919 920 shared.RunCommandFinder( 921 "", 922 &api.PullRequest{ID: "THE-ID", Number: 123, MergeStateStatus: "CLEAN"}, 923 ghrepo.New("OWNER", "REPO"), 924 ) 925 926 http.Register( 927 httpmock.GraphQL(`query RepositoryInfo\b`), 928 httpmock.StringResponse(` 929 { "data": { "repository": { 930 "mergeCommitAllowed": true, 931 "rebaseMergeAllowed": true, 932 "squashMergeAllowed": true 933 } } }`)) 934 935 _, cmdTeardown := run.Stub() 936 defer cmdTeardown(t) 937 938 as, surveyTeardown := prompt.InitAskStubber() 939 defer surveyTeardown() 940 941 as.StubOne(0) // Merge method survey 942 as.StubOne(true) // Delete branch survey 943 as.StubOne("Cancel") // Confirm submit survey 944 945 output, err := runCommand(http, "blueberries", true, "") 946 if !errors.Is(err, cmdutil.CancelError) { 947 t.Fatalf("got error %v", err) 948 } 949 950 assert.Equal(t, "Cancelled.\n", output.Stderr()) 951 } 952 953 func Test_mergeMethodSurvey(t *testing.T) { 954 repo := &api.Repository{ 955 MergeCommitAllowed: false, 956 RebaseMergeAllowed: true, 957 SquashMergeAllowed: true, 958 } 959 as, surveyTeardown := prompt.InitAskStubber() 960 defer surveyTeardown() 961 as.StubOne(0) // Select first option which is rebase merge 962 method, err := mergeMethodSurvey(repo) 963 assert.Nil(t, err) 964 assert.Equal(t, PullRequestMergeMethodRebase, method) 965 } 966 967 func TestMergeRun_autoMerge(t *testing.T) { 968 io, _, stdout, stderr := iostreams.Test() 969 io.SetStdoutTTY(true) 970 io.SetStderrTTY(true) 971 972 tr := initFakeHTTP() 973 defer tr.Verify(t) 974 tr.Register( 975 httpmock.GraphQL(`mutation PullRequestAutoMerge\b`), 976 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 977 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 978 assert.Equal(t, "SQUASH", input["mergeMethod"].(string)) 979 })) 980 981 _, cmdTeardown := run.Stub() 982 defer cmdTeardown(t) 983 984 err := mergeRun(&MergeOptions{ 985 IO: io, 986 HttpClient: func() (*http.Client, error) { 987 return &http.Client{Transport: tr}, nil 988 }, 989 SelectorArg: "https://github.com/OWNER/REPO/pull/123", 990 AutoMergeEnable: true, 991 MergeMethod: PullRequestMergeMethodSquash, 992 Finder: shared.NewMockFinder( 993 "https://github.com/OWNER/REPO/pull/123", 994 &api.PullRequest{ID: "THE-ID", Number: 123, MergeStateStatus: "BLOCKED"}, 995 ghrepo.New("OWNER", "REPO"), 996 ), 997 }) 998 assert.NoError(t, err) 999 1000 assert.Equal(t, "", stdout.String()) 1001 assert.Equal(t, "✓ Pull request #123 will be automatically merged via squash when all requirements are met\n", stderr.String()) 1002 } 1003 1004 func TestMergeRun_autoMerge_directMerge(t *testing.T) { 1005 io, _, stdout, stderr := iostreams.Test() 1006 io.SetStdoutTTY(true) 1007 io.SetStderrTTY(true) 1008 1009 tr := initFakeHTTP() 1010 defer tr.Verify(t) 1011 tr.Register( 1012 httpmock.GraphQL(`mutation PullRequestMerge\b`), 1013 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 1014 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 1015 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 1016 assert.NotContains(t, input, "commitHeadline") 1017 })) 1018 1019 _, cmdTeardown := run.Stub() 1020 defer cmdTeardown(t) 1021 1022 err := mergeRun(&MergeOptions{ 1023 IO: io, 1024 HttpClient: func() (*http.Client, error) { 1025 return &http.Client{Transport: tr}, nil 1026 }, 1027 SelectorArg: "https://github.com/OWNER/REPO/pull/123", 1028 AutoMergeEnable: true, 1029 MergeMethod: PullRequestMergeMethodMerge, 1030 Finder: shared.NewMockFinder( 1031 "https://github.com/OWNER/REPO/pull/123", 1032 &api.PullRequest{ID: "THE-ID", Number: 123, MergeStateStatus: "CLEAN"}, 1033 ghrepo.New("OWNER", "REPO"), 1034 ), 1035 }) 1036 assert.NoError(t, err) 1037 1038 assert.Equal(t, "", stdout.String()) 1039 assert.Equal(t, "✓ Merged pull request #123 ()\n", stderr.String()) 1040 } 1041 1042 func TestMergeRun_disableAutoMerge(t *testing.T) { 1043 io, _, stdout, stderr := iostreams.Test() 1044 io.SetStdoutTTY(true) 1045 io.SetStderrTTY(true) 1046 1047 tr := initFakeHTTP() 1048 defer tr.Verify(t) 1049 tr.Register( 1050 httpmock.GraphQL(`mutation PullRequestAutoMergeDisable\b`), 1051 httpmock.GraphQLQuery(`{}`, func(s string, m map[string]interface{}) { 1052 assert.Equal(t, map[string]interface{}{"prID": "THE-ID"}, m) 1053 })) 1054 1055 _, cmdTeardown := run.Stub() 1056 defer cmdTeardown(t) 1057 1058 err := mergeRun(&MergeOptions{ 1059 IO: io, 1060 HttpClient: func() (*http.Client, error) { 1061 return &http.Client{Transport: tr}, nil 1062 }, 1063 SelectorArg: "https://github.com/OWNER/REPO/pull/123", 1064 AutoMergeDisable: true, 1065 Finder: shared.NewMockFinder( 1066 "https://github.com/OWNER/REPO/pull/123", 1067 &api.PullRequest{ID: "THE-ID", Number: 123}, 1068 ghrepo.New("OWNER", "REPO"), 1069 ), 1070 }) 1071 assert.NoError(t, err) 1072 1073 assert.Equal(t, "", stdout.String()) 1074 assert.Equal(t, "✓ Auto-merge disabled for pull request #123\n", stderr.String()) 1075 } 1076 1077 type testEditor struct{} 1078 1079 func (e testEditor) Edit(filename, text string) (string, error) { 1080 return strings.ToUpper(text), nil 1081 }