github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/pr/merge/merge_test.go (about) 1 package merge 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "os" 10 "path/filepath" 11 "regexp" 12 "strings" 13 "testing" 14 15 "github.com/MakeNowJust/heredoc" 16 "github.com/ungtb10d/cli/v2/api" 17 "github.com/ungtb10d/cli/v2/context" 18 "github.com/ungtb10d/cli/v2/git" 19 "github.com/ungtb10d/cli/v2/internal/ghrepo" 20 "github.com/ungtb10d/cli/v2/internal/run" 21 "github.com/ungtb10d/cli/v2/pkg/cmd/pr/shared" 22 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 23 "github.com/ungtb10d/cli/v2/pkg/httpmock" 24 "github.com/ungtb10d/cli/v2/pkg/iostreams" 25 "github.com/ungtb10d/cli/v2/pkg/prompt" 26 "github.com/ungtb10d/cli/v2/test" 27 "github.com/google/shlex" 28 "github.com/stretchr/testify/assert" 29 "github.com/stretchr/testify/require" 30 ) 31 32 func Test_NewCmdMerge(t *testing.T) { 33 tmpFile := filepath.Join(t.TempDir(), "my-body.md") 34 err := os.WriteFile(tmpFile, []byte("a body from file"), 0600) 35 require.NoError(t, err) 36 37 tests := []struct { 38 name string 39 args string 40 stdin string 41 isTTY bool 42 want MergeOptions 43 wantErr string 44 }{ 45 { 46 name: "number argument", 47 args: "123", 48 isTTY: true, 49 want: MergeOptions{ 50 SelectorArg: "123", 51 DeleteBranch: false, 52 IsDeleteBranchIndicated: false, 53 CanDeleteLocalBranch: true, 54 MergeMethod: PullRequestMergeMethodMerge, 55 MergeStrategyEmpty: true, 56 Body: "", 57 BodySet: false, 58 AuthorEmail: "", 59 }, 60 }, 61 { 62 name: "delete-branch specified", 63 args: "--delete-branch=false", 64 isTTY: true, 65 want: MergeOptions{ 66 SelectorArg: "", 67 DeleteBranch: false, 68 IsDeleteBranchIndicated: true, 69 CanDeleteLocalBranch: true, 70 MergeMethod: PullRequestMergeMethodMerge, 71 MergeStrategyEmpty: true, 72 Body: "", 73 BodySet: false, 74 AuthorEmail: "", 75 }, 76 }, 77 { 78 name: "body from file", 79 args: fmt.Sprintf("123 --body-file '%s'", tmpFile), 80 isTTY: true, 81 want: MergeOptions{ 82 SelectorArg: "123", 83 DeleteBranch: false, 84 IsDeleteBranchIndicated: false, 85 CanDeleteLocalBranch: true, 86 MergeMethod: PullRequestMergeMethodMerge, 87 MergeStrategyEmpty: true, 88 Body: "a body from file", 89 BodySet: true, 90 AuthorEmail: "", 91 }, 92 }, 93 { 94 name: "body from stdin", 95 args: "123 --body-file -", 96 stdin: "this is on standard input", 97 isTTY: true, 98 want: MergeOptions{ 99 SelectorArg: "123", 100 DeleteBranch: false, 101 IsDeleteBranchIndicated: false, 102 CanDeleteLocalBranch: true, 103 MergeMethod: PullRequestMergeMethodMerge, 104 MergeStrategyEmpty: true, 105 Body: "this is on standard input", 106 BodySet: true, 107 AuthorEmail: "", 108 }, 109 }, 110 { 111 name: "body", 112 args: "123 -bcool", 113 isTTY: true, 114 want: MergeOptions{ 115 SelectorArg: "123", 116 DeleteBranch: false, 117 IsDeleteBranchIndicated: false, 118 CanDeleteLocalBranch: true, 119 MergeMethod: PullRequestMergeMethodMerge, 120 MergeStrategyEmpty: true, 121 Body: "cool", 122 BodySet: true, 123 AuthorEmail: "", 124 }, 125 }, 126 { 127 name: "match-head-commit specified", 128 args: "123 --match-head-commit 555", 129 isTTY: true, 130 want: MergeOptions{ 131 SelectorArg: "123", 132 DeleteBranch: false, 133 IsDeleteBranchIndicated: false, 134 CanDeleteLocalBranch: true, 135 MergeMethod: PullRequestMergeMethodMerge, 136 MergeStrategyEmpty: true, 137 Body: "", 138 BodySet: false, 139 MatchHeadCommit: "555", 140 AuthorEmail: "", 141 }, 142 }, 143 { 144 name: "author email", 145 args: "123 --author-email octocat@github.com", 146 isTTY: true, 147 want: MergeOptions{ 148 SelectorArg: "123", 149 DeleteBranch: false, 150 IsDeleteBranchIndicated: false, 151 CanDeleteLocalBranch: true, 152 MergeMethod: PullRequestMergeMethodMerge, 153 MergeStrategyEmpty: true, 154 Body: "", 155 BodySet: false, 156 AuthorEmail: "octocat@github.com", 157 }, 158 }, 159 { 160 name: "body and body-file flags", 161 args: "123 --body 'test' --body-file 'test-file.txt'", 162 isTTY: true, 163 wantErr: "specify only one of `--body` or `--body-file`", 164 }, 165 { 166 name: "no argument with --repo override", 167 args: "-R owner/repo", 168 isTTY: true, 169 wantErr: "argument required when using the --repo flag", 170 }, 171 { 172 name: "multiple merge methods", 173 args: "123 --merge --rebase", 174 isTTY: true, 175 wantErr: "only one of --merge, --rebase, or --squash can be enabled", 176 }, 177 { 178 name: "multiple merge methods, non-tty", 179 args: "123 --merge --rebase", 180 isTTY: false, 181 wantErr: "only one of --merge, --rebase, or --squash can be enabled", 182 }, 183 } 184 for _, tt := range tests { 185 t.Run(tt.name, func(t *testing.T) { 186 ios, stdin, _, _ := iostreams.Test() 187 ios.SetStdoutTTY(tt.isTTY) 188 ios.SetStdinTTY(tt.isTTY) 189 ios.SetStderrTTY(tt.isTTY) 190 191 if tt.stdin != "" { 192 _, _ = stdin.WriteString(tt.stdin) 193 } 194 195 f := &cmdutil.Factory{ 196 IOStreams: ios, 197 } 198 199 var opts *MergeOptions 200 cmd := NewCmdMerge(f, func(o *MergeOptions) error { 201 opts = o 202 return nil 203 }) 204 cmd.PersistentFlags().StringP("repo", "R", "", "") 205 206 argv, err := shlex.Split(tt.args) 207 require.NoError(t, err) 208 cmd.SetArgs(argv) 209 210 cmd.SetIn(&bytes.Buffer{}) 211 cmd.SetOut(io.Discard) 212 cmd.SetErr(io.Discard) 213 214 _, err = cmd.ExecuteC() 215 if tt.wantErr != "" { 216 require.EqualError(t, err, tt.wantErr) 217 return 218 } else { 219 require.NoError(t, err) 220 } 221 222 assert.Equal(t, tt.want.SelectorArg, opts.SelectorArg) 223 assert.Equal(t, tt.want.DeleteBranch, opts.DeleteBranch) 224 assert.Equal(t, tt.want.CanDeleteLocalBranch, opts.CanDeleteLocalBranch) 225 assert.Equal(t, tt.want.MergeMethod, opts.MergeMethod) 226 assert.Equal(t, tt.want.MergeStrategyEmpty, opts.MergeStrategyEmpty) 227 assert.Equal(t, tt.want.Body, opts.Body) 228 assert.Equal(t, tt.want.BodySet, opts.BodySet) 229 assert.Equal(t, tt.want.MatchHeadCommit, opts.MatchHeadCommit) 230 assert.Equal(t, tt.want.AuthorEmail, opts.AuthorEmail) 231 }) 232 } 233 } 234 235 func baseRepo(owner, repo, branch string) ghrepo.Interface { 236 return api.InitRepoHostname(&api.Repository{ 237 Name: repo, 238 Owner: api.RepositoryOwner{Login: owner}, 239 DefaultBranchRef: api.BranchRef{Name: branch}, 240 }, "github.com") 241 } 242 243 func stubCommit(pr *api.PullRequest, oid string) { 244 pr.Commits.Nodes = append(pr.Commits.Nodes, api.PullRequestCommit{ 245 Commit: api.PullRequestCommitCommit{OID: oid}, 246 }) 247 } 248 249 func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*test.CmdOut, error) { 250 ios, _, stdout, stderr := iostreams.Test() 251 ios.SetStdoutTTY(isTTY) 252 ios.SetStdinTTY(isTTY) 253 ios.SetStderrTTY(isTTY) 254 255 factory := &cmdutil.Factory{ 256 IOStreams: ios, 257 HttpClient: func() (*http.Client, error) { 258 return &http.Client{Transport: rt}, nil 259 }, 260 Branch: func() (string, error) { 261 return branch, nil 262 }, 263 Remotes: func() (context.Remotes, error) { 264 return []*context.Remote{ 265 { 266 Remote: &git.Remote{ 267 Name: "origin", 268 }, 269 Repo: ghrepo.New("OWNER", "REPO"), 270 }, 271 }, nil 272 }, 273 GitClient: &git.Client{GitPath: "some/path/git"}, 274 } 275 276 cmd := NewCmdMerge(factory, nil) 277 cmd.PersistentFlags().StringP("repo", "R", "", "") 278 279 cli = strings.TrimPrefix(cli, "pr merge") 280 argv, err := shlex.Split(cli) 281 if err != nil { 282 return nil, err 283 } 284 cmd.SetArgs(argv) 285 286 cmd.SetIn(&bytes.Buffer{}) 287 cmd.SetOut(io.Discard) 288 cmd.SetErr(io.Discard) 289 290 _, err = cmd.ExecuteC() 291 return &test.CmdOut{ 292 OutBuf: stdout, 293 ErrBuf: stderr, 294 }, err 295 } 296 297 func initFakeHTTP() *httpmock.Registry { 298 return &httpmock.Registry{} 299 } 300 301 func TestPrMerge(t *testing.T) { 302 http := initFakeHTTP() 303 defer http.Verify(t) 304 305 shared.RunCommandFinder( 306 "1", 307 &api.PullRequest{ 308 ID: "THE-ID", 309 Number: 1, 310 State: "OPEN", 311 Title: "The title of the PR", 312 MergeStateStatus: "CLEAN", 313 }, 314 baseRepo("OWNER", "REPO", "main"), 315 ) 316 317 http.Register( 318 httpmock.GraphQL(`mutation PullRequestMerge\b`), 319 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 320 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 321 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 322 assert.NotContains(t, input, "commitHeadline") 323 }), 324 ) 325 326 cs, cmdTeardown := run.Stub() 327 defer cmdTeardown(t) 328 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 329 330 output, err := runCommand(http, "main", true, "pr merge 1 --merge") 331 if err != nil { 332 t.Fatalf("error running command `pr merge`: %v", err) 333 } 334 335 r := regexp.MustCompile(`Merged pull request #1 \(The title of the PR\)`) 336 337 if !r.MatchString(output.Stderr()) { 338 t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) 339 } 340 } 341 342 func TestPrMerge_blocked(t *testing.T) { 343 http := initFakeHTTP() 344 defer http.Verify(t) 345 346 shared.RunCommandFinder( 347 "1", 348 &api.PullRequest{ 349 ID: "THE-ID", 350 Number: 1, 351 State: "OPEN", 352 Title: "The title of the PR", 353 MergeStateStatus: "BLOCKED", 354 }, 355 baseRepo("OWNER", "REPO", "main"), 356 ) 357 358 cs, cmdTeardown := run.Stub() 359 defer cmdTeardown(t) 360 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 361 362 output, err := runCommand(http, "main", true, "pr merge 1 --merge") 363 assert.EqualError(t, err, "SilentError") 364 365 assert.Equal(t, "", output.String()) 366 assert.Equal(t, heredoc.Docf(` 367 X Pull request #1 is not mergeable: the base branch policy prohibits the merge. 368 To have the pull request merged after all the requirements have been met, add the %[1]s--auto%[1]s flag. 369 To use administrator privileges to immediately merge the pull request, add the %[1]s--admin%[1]s flag. 370 `, "`"), output.Stderr()) 371 } 372 373 func TestPrMerge_dirty(t *testing.T) { 374 http := initFakeHTTP() 375 defer http.Verify(t) 376 377 shared.RunCommandFinder( 378 "1", 379 &api.PullRequest{ 380 ID: "THE-ID", 381 Number: 123, 382 State: "OPEN", 383 Title: "The title of the PR", 384 MergeStateStatus: "DIRTY", 385 BaseRefName: "trunk", 386 HeadRefName: "feature", 387 }, 388 baseRepo("OWNER", "REPO", "main"), 389 ) 390 391 cs, cmdTeardown := run.Stub() 392 defer cmdTeardown(t) 393 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 394 395 output, err := runCommand(http, "main", true, "pr merge 1 --merge") 396 assert.EqualError(t, err, "SilentError") 397 398 assert.Equal(t, "", output.String()) 399 assert.Equal(t, heredoc.Docf(` 400 X Pull request #123 is not mergeable: the merge commit cannot be cleanly created. 401 To have the pull request merged after all the requirements have been met, add the %[1]s--auto%[1]s flag. 402 Run the following to resolve the merge conflicts locally: 403 gh pr checkout 123 && git fetch origin trunk && git merge origin/trunk 404 `, "`"), output.Stderr()) 405 } 406 407 func TestPrMerge_nontty(t *testing.T) { 408 http := initFakeHTTP() 409 defer http.Verify(t) 410 411 shared.RunCommandFinder( 412 "1", 413 &api.PullRequest{ 414 ID: "THE-ID", 415 Number: 1, 416 State: "OPEN", 417 Title: "The title of the PR", 418 MergeStateStatus: "CLEAN", 419 }, 420 baseRepo("OWNER", "REPO", "main"), 421 ) 422 423 http.Register( 424 httpmock.GraphQL(`mutation PullRequestMerge\b`), 425 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 426 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 427 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 428 assert.NotContains(t, input, "commitHeadline") 429 })) 430 431 cs, cmdTeardown := run.Stub() 432 defer cmdTeardown(t) 433 434 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 435 436 output, err := runCommand(http, "main", false, "pr merge 1 --merge") 437 if err != nil { 438 t.Fatalf("error running command `pr merge`: %v", err) 439 } 440 441 assert.Equal(t, "", output.String()) 442 assert.Equal(t, "", output.Stderr()) 443 } 444 445 func TestPrMerge_editMessage_nontty(t *testing.T) { 446 http := initFakeHTTP() 447 defer http.Verify(t) 448 449 shared.RunCommandFinder( 450 "1", 451 &api.PullRequest{ 452 ID: "THE-ID", 453 Number: 1, 454 State: "OPEN", 455 Title: "The title of the PR", 456 MergeStateStatus: "CLEAN", 457 }, 458 baseRepo("OWNER", "REPO", "main"), 459 ) 460 461 http.Register( 462 httpmock.GraphQL(`mutation PullRequestMerge\b`), 463 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 464 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 465 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 466 assert.Equal(t, "mytitle", input["commitHeadline"].(string)) 467 assert.Equal(t, "mybody", input["commitBody"].(string)) 468 })) 469 470 cs, cmdTeardown := run.Stub() 471 defer cmdTeardown(t) 472 473 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 474 475 output, err := runCommand(http, "main", false, "pr merge 1 --merge -t mytitle -b mybody") 476 if err != nil { 477 t.Fatalf("error running command `pr merge`: %v", err) 478 } 479 480 assert.Equal(t, "", output.String()) 481 assert.Equal(t, "", output.Stderr()) 482 } 483 484 func TestPrMerge_withRepoFlag(t *testing.T) { 485 http := initFakeHTTP() 486 defer http.Verify(t) 487 488 shared.RunCommandFinder( 489 "1", 490 &api.PullRequest{ 491 ID: "THE-ID", 492 Number: 1, 493 State: "OPEN", 494 Title: "The title of the PR", 495 MergeStateStatus: "CLEAN", 496 }, 497 baseRepo("OWNER", "REPO", "main"), 498 ) 499 500 http.Register( 501 httpmock.GraphQL(`mutation PullRequestMerge\b`), 502 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 503 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 504 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 505 assert.NotContains(t, input, "commitHeadline") 506 })) 507 508 _, cmdTeardown := run.Stub() 509 defer cmdTeardown(t) 510 511 output, err := runCommand(http, "main", true, "pr merge 1 --merge -R OWNER/REPO") 512 if err != nil { 513 t.Fatalf("error running command `pr merge`: %v", err) 514 } 515 516 r := regexp.MustCompile(`Merged pull request #1 \(The title of the PR\)`) 517 518 if !r.MatchString(output.Stderr()) { 519 t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) 520 } 521 } 522 523 func TestPrMerge_withMatchCommitHeadFlag(t *testing.T) { 524 http := initFakeHTTP() 525 defer http.Verify(t) 526 527 shared.RunCommandFinder( 528 "1", 529 &api.PullRequest{ 530 ID: "THE-ID", 531 Number: 1, 532 State: "OPEN", 533 Title: "The title of the PR", 534 MergeStateStatus: "CLEAN", 535 }, 536 baseRepo("OWNER", "REPO", "main"), 537 ) 538 539 http.Register( 540 httpmock.GraphQL(`mutation PullRequestMerge\b`), 541 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 542 assert.Equal(t, 3, len(input)) 543 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 544 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 545 assert.Equal(t, "285ed5ab740f53ff6b0b4b629c59a9df23b9c6db", input["expectedHeadOid"].(string)) 546 })) 547 548 cs, cmdTeardown := run.Stub() 549 defer cmdTeardown(t) 550 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 551 552 output, err := runCommand(http, "main", true, "pr merge 1 --merge --match-head-commit 285ed5ab740f53ff6b0b4b629c59a9df23b9c6db") 553 if err != nil { 554 t.Fatalf("error running command `pr merge`: %v", err) 555 } 556 557 r := regexp.MustCompile(`Merged pull request #1 \(The title of the PR\)`) 558 559 if !r.MatchString(output.Stderr()) { 560 t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) 561 } 562 } 563 564 func TestPrMerge_withAuthorFlag(t *testing.T) { 565 http := initFakeHTTP() 566 defer http.Verify(t) 567 568 shared.RunCommandFinder( 569 "1", 570 &api.PullRequest{ 571 ID: "THE-ID", 572 Number: 1, 573 State: "OPEN", 574 Title: "The title of the PR", 575 MergeStateStatus: "CLEAN", 576 }, 577 baseRepo("OWNER", "REPO", "main"), 578 ) 579 580 http.Register( 581 httpmock.GraphQL(`mutation PullRequestMerge\b`), 582 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 583 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 584 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 585 assert.Equal(t, "octocat@github.com", input["authorEmail"].(string)) 586 assert.NotContains(t, input, "commitHeadline") 587 }), 588 ) 589 590 cs, cmdTeardown := run.Stub() 591 defer cmdTeardown(t) 592 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 593 594 output, err := runCommand(http, "main", true, "pr merge 1 --merge --author-email octocat@github.com") 595 if err != nil { 596 t.Fatalf("error running command `pr merge`: %v", err) 597 } 598 599 r := regexp.MustCompile(`Merged pull request #1 \(The title of the PR\)`) 600 601 if !r.MatchString(output.Stderr()) { 602 t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) 603 } 604 } 605 606 func TestPrMerge_deleteBranch(t *testing.T) { 607 http := initFakeHTTP() 608 defer http.Verify(t) 609 610 shared.RunCommandFinder( 611 "", 612 &api.PullRequest{ 613 ID: "PR_10", 614 Number: 10, 615 State: "OPEN", 616 Title: "Blueberries are a good fruit", 617 HeadRefName: "blueberries", 618 BaseRefName: "main", 619 MergeStateStatus: "CLEAN", 620 }, 621 baseRepo("OWNER", "REPO", "main"), 622 ) 623 624 http.Register( 625 httpmock.GraphQL(`mutation PullRequestMerge\b`), 626 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 627 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 628 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 629 assert.NotContains(t, input, "commitHeadline") 630 })) 631 http.Register( 632 httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), 633 httpmock.StringResponse(`{}`)) 634 635 cs, cmdTeardown := run.Stub() 636 defer cmdTeardown(t) 637 638 cs.Register(`git rev-parse --verify refs/heads/main`, 0, "") 639 cs.Register(`git checkout main`, 0, "") 640 cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") 641 cs.Register(`git branch -D blueberries`, 0, "") 642 cs.Register(`git pull --ff-only`, 0, "") 643 644 output, err := runCommand(http, "blueberries", true, `pr merge --merge --delete-branch`) 645 if err != nil { 646 t.Fatalf("Got unexpected error running `pr merge` %s", err) 647 } 648 649 assert.Equal(t, "", output.String()) 650 assert.Equal(t, heredoc.Doc(` 651 ✓ Merged pull request #10 (Blueberries are a good fruit) 652 ✓ Deleted branch blueberries and switched to branch main 653 `), output.Stderr()) 654 } 655 656 func TestPrMerge_deleteBranch_nonDefault(t *testing.T) { 657 http := initFakeHTTP() 658 defer http.Verify(t) 659 660 shared.RunCommandFinder( 661 "", 662 &api.PullRequest{ 663 ID: "PR_10", 664 Number: 10, 665 State: "OPEN", 666 Title: "Blueberries are a good fruit", 667 HeadRefName: "blueberries", 668 MergeStateStatus: "CLEAN", 669 BaseRefName: "fruit", 670 }, 671 baseRepo("OWNER", "REPO", "main"), 672 ) 673 674 http.Register( 675 httpmock.GraphQL(`mutation PullRequestMerge\b`), 676 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 677 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 678 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 679 assert.NotContains(t, input, "commitHeadline") 680 })) 681 http.Register( 682 httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), 683 httpmock.StringResponse(`{}`)) 684 685 cs, cmdTeardown := run.Stub() 686 defer cmdTeardown(t) 687 688 cs.Register(`git rev-parse --verify refs/heads/fruit`, 0, "") 689 cs.Register(`git checkout fruit`, 0, "") 690 cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") 691 cs.Register(`git branch -D blueberries`, 0, "") 692 cs.Register(`git pull --ff-only`, 0, "") 693 694 output, err := runCommand(http, "blueberries", true, `pr merge --merge --delete-branch`) 695 if err != nil { 696 t.Fatalf("Got unexpected error running `pr merge` %s", err) 697 } 698 699 assert.Equal(t, "", output.String()) 700 assert.Equal(t, heredoc.Doc(` 701 ✓ Merged pull request #10 (Blueberries are a good fruit) 702 ✓ Deleted branch blueberries and switched to branch fruit 703 `), output.Stderr()) 704 } 705 706 func TestPrMerge_deleteBranch_checkoutNewBranch(t *testing.T) { 707 http := initFakeHTTP() 708 defer http.Verify(t) 709 710 shared.RunCommandFinder( 711 "", 712 &api.PullRequest{ 713 ID: "PR_10", 714 Number: 10, 715 State: "OPEN", 716 Title: "Blueberries are a good fruit", 717 HeadRefName: "blueberries", 718 MergeStateStatus: "CLEAN", 719 BaseRefName: "fruit", 720 }, 721 baseRepo("OWNER", "REPO", "main"), 722 ) 723 724 http.Register( 725 httpmock.GraphQL(`mutation PullRequestMerge\b`), 726 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 727 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 728 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 729 assert.NotContains(t, input, "commitHeadline") 730 })) 731 http.Register( 732 httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), 733 httpmock.StringResponse(`{}`)) 734 735 cs, cmdTeardown := run.Stub() 736 defer cmdTeardown(t) 737 738 cs.Register(`git rev-parse --verify refs/heads/fruit`, 1, "") 739 cs.Register(`git checkout -b fruit --track origin/fruit`, 0, "") 740 cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") 741 cs.Register(`git branch -D blueberries`, 0, "") 742 cs.Register(`git pull --ff-only`, 0, "") 743 744 output, err := runCommand(http, "blueberries", true, `pr merge --merge --delete-branch`) 745 if err != nil { 746 t.Fatalf("Got unexpected error running `pr merge` %s", err) 747 } 748 749 assert.Equal(t, "", output.String()) 750 assert.Equal(t, heredoc.Doc(` 751 ✓ Merged pull request #10 (Blueberries are a good fruit) 752 ✓ Deleted branch blueberries and switched to branch fruit 753 `), output.Stderr()) 754 } 755 756 func TestPrMerge_deleteNonCurrentBranch(t *testing.T) { 757 http := initFakeHTTP() 758 defer http.Verify(t) 759 760 shared.RunCommandFinder( 761 "blueberries", 762 &api.PullRequest{ 763 ID: "PR_10", 764 Number: 10, 765 State: "OPEN", 766 Title: "Blueberries are a good fruit", 767 HeadRefName: "blueberries", 768 MergeStateStatus: "CLEAN", 769 }, 770 baseRepo("OWNER", "REPO", "main"), 771 ) 772 773 http.Register( 774 httpmock.GraphQL(`mutation PullRequestMerge\b`), 775 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 776 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 777 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 778 assert.NotContains(t, input, "commitHeadline") 779 })) 780 http.Register( 781 httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), 782 httpmock.StringResponse(`{}`)) 783 784 cs, cmdTeardown := run.Stub() 785 defer cmdTeardown(t) 786 787 cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") 788 cs.Register(`git branch -D blueberries`, 0, "") 789 790 output, err := runCommand(http, "main", true, `pr merge --merge --delete-branch blueberries`) 791 if err != nil { 792 t.Fatalf("Got unexpected error running `pr merge` %s", err) 793 } 794 795 assert.Equal(t, "", output.String()) 796 assert.Equal(t, heredoc.Doc(` 797 ✓ Merged pull request #10 (Blueberries are a good fruit) 798 ✓ Deleted branch blueberries 799 `), output.Stderr()) 800 } 801 802 func Test_nonDivergingPullRequest(t *testing.T) { 803 http := initFakeHTTP() 804 defer http.Verify(t) 805 806 pr := &api.PullRequest{ 807 ID: "PR_10", 808 Number: 10, 809 Title: "Blueberries are a good fruit", 810 State: "OPEN", 811 MergeStateStatus: "CLEAN", 812 BaseRefName: "main", 813 } 814 stubCommit(pr, "COMMITSHA1") 815 816 shared.RunCommandFinder("", pr, baseRepo("OWNER", "REPO", "main")) 817 818 http.Register( 819 httpmock.GraphQL(`mutation PullRequestMerge\b`), 820 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 821 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 822 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 823 assert.NotContains(t, input, "commitHeadline") 824 })) 825 826 cs, cmdTeardown := run.Stub() 827 defer cmdTeardown(t) 828 829 cs.Register(`git .+ show .+ HEAD`, 0, "COMMITSHA1,title") 830 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 831 832 output, err := runCommand(http, "blueberries", true, "pr merge --merge") 833 if err != nil { 834 t.Fatalf("error running command `pr merge`: %v", err) 835 } 836 837 assert.Equal(t, heredoc.Doc(` 838 ✓ Merged pull request #10 (Blueberries are a good fruit) 839 `), output.Stderr()) 840 } 841 842 func Test_divergingPullRequestWarning(t *testing.T) { 843 http := initFakeHTTP() 844 defer http.Verify(t) 845 846 pr := &api.PullRequest{ 847 ID: "PR_10", 848 Number: 10, 849 Title: "Blueberries are a good fruit", 850 State: "OPEN", 851 MergeStateStatus: "CLEAN", 852 BaseRefName: "main", 853 } 854 stubCommit(pr, "COMMITSHA1") 855 856 shared.RunCommandFinder("", pr, baseRepo("OWNER", "REPO", "main")) 857 858 http.Register( 859 httpmock.GraphQL(`mutation PullRequestMerge\b`), 860 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 861 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 862 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 863 assert.NotContains(t, input, "commitHeadline") 864 })) 865 866 cs, cmdTeardown := run.Stub() 867 defer cmdTeardown(t) 868 869 cs.Register(`git .+ show .+ HEAD`, 0, "COMMITSHA2,title") 870 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 871 872 output, err := runCommand(http, "blueberries", true, "pr merge --merge") 873 if err != nil { 874 t.Fatalf("error running command `pr merge`: %v", err) 875 } 876 877 assert.Equal(t, heredoc.Doc(` 878 ! Pull request #10 (Blueberries are a good fruit) has diverged from local branch 879 ✓ Merged pull request #10 (Blueberries are a good fruit) 880 `), output.Stderr()) 881 } 882 883 func Test_pullRequestWithoutCommits(t *testing.T) { 884 http := initFakeHTTP() 885 defer http.Verify(t) 886 887 shared.RunCommandFinder( 888 "", 889 &api.PullRequest{ 890 ID: "PR_10", 891 Number: 10, 892 Title: "Blueberries are a good fruit", 893 State: "OPEN", 894 MergeStateStatus: "CLEAN", 895 }, 896 baseRepo("OWNER", "REPO", "main"), 897 ) 898 899 http.Register( 900 httpmock.GraphQL(`mutation PullRequestMerge\b`), 901 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 902 assert.Equal(t, "PR_10", input["pullRequestId"].(string)) 903 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 904 assert.NotContains(t, input, "commitHeadline") 905 })) 906 907 cs, cmdTeardown := run.Stub() 908 defer cmdTeardown(t) 909 910 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 911 912 output, err := runCommand(http, "blueberries", true, "pr merge --merge") 913 if err != nil { 914 t.Fatalf("error running command `pr merge`: %v", err) 915 } 916 917 assert.Equal(t, heredoc.Doc(` 918 ✓ Merged pull request #10 (Blueberries are a good fruit) 919 `), output.Stderr()) 920 } 921 922 func TestPrMerge_rebase(t *testing.T) { 923 http := initFakeHTTP() 924 defer http.Verify(t) 925 926 shared.RunCommandFinder( 927 "2", 928 &api.PullRequest{ 929 ID: "THE-ID", 930 Number: 2, 931 Title: "The title of the PR", 932 State: "OPEN", 933 MergeStateStatus: "CLEAN", 934 }, 935 baseRepo("OWNER", "REPO", "main"), 936 ) 937 938 http.Register( 939 httpmock.GraphQL(`mutation PullRequestMerge\b`), 940 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 941 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 942 assert.Equal(t, "REBASE", input["mergeMethod"].(string)) 943 assert.NotContains(t, input, "commitHeadline") 944 })) 945 946 cs, cmdTeardown := run.Stub() 947 defer cmdTeardown(t) 948 949 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 950 951 output, err := runCommand(http, "main", true, "pr merge 2 --rebase") 952 if err != nil { 953 t.Fatalf("error running command `pr merge`: %v", err) 954 } 955 956 r := regexp.MustCompile(`Rebased and merged pull request #2 \(The title of the PR\)`) 957 958 if !r.MatchString(output.Stderr()) { 959 t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) 960 } 961 } 962 963 func TestPrMerge_squash(t *testing.T) { 964 http := initFakeHTTP() 965 defer http.Verify(t) 966 967 shared.RunCommandFinder( 968 "3", 969 &api.PullRequest{ 970 ID: "THE-ID", 971 Number: 3, 972 Title: "The title of the PR", 973 State: "OPEN", 974 MergeStateStatus: "CLEAN", 975 }, 976 baseRepo("OWNER", "REPO", "main"), 977 ) 978 979 http.Register( 980 httpmock.GraphQL(`mutation PullRequestMerge\b`), 981 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 982 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 983 assert.Equal(t, "SQUASH", input["mergeMethod"].(string)) 984 assert.NotContains(t, input, "commitHeadline") 985 })) 986 987 cs, cmdTeardown := run.Stub() 988 defer cmdTeardown(t) 989 990 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 991 992 output, err := runCommand(http, "main", true, "pr merge 3 --squash") 993 if err != nil { 994 t.Fatalf("error running command `pr merge`: %v", err) 995 } 996 997 assert.Equal(t, "", output.String()) 998 assert.Equal(t, heredoc.Doc(` 999 ✓ Squashed and merged pull request #3 (The title of the PR) 1000 `), output.Stderr()) 1001 } 1002 1003 func TestPrMerge_alreadyMerged(t *testing.T) { 1004 http := initFakeHTTP() 1005 defer http.Verify(t) 1006 1007 shared.RunCommandFinder( 1008 "4", 1009 &api.PullRequest{ 1010 ID: "THE-ID", 1011 Number: 4, 1012 State: "MERGED", 1013 HeadRefName: "blueberries", 1014 BaseRefName: "main", 1015 MergeStateStatus: "CLEAN", 1016 }, 1017 baseRepo("OWNER", "REPO", "main"), 1018 ) 1019 1020 cs, cmdTeardown := run.Stub() 1021 defer cmdTeardown(t) 1022 1023 cs.Register(`git rev-parse --verify refs/heads/main`, 0, "") 1024 cs.Register(`git checkout main`, 0, "") 1025 cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") 1026 cs.Register(`git branch -D blueberries`, 0, "") 1027 cs.Register(`git pull --ff-only`, 0, "") 1028 1029 //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock 1030 as := prompt.NewAskStubber(t) 1031 as.StubPrompt("Pull request #4 was already merged. Delete the branch locally?").AnswerWith(true) 1032 1033 output, err := runCommand(http, "blueberries", true, "pr merge 4") 1034 assert.NoError(t, err) 1035 assert.Equal(t, "", output.String()) 1036 assert.Equal(t, "✓ Deleted branch blueberries and switched to branch main\n", output.Stderr()) 1037 } 1038 1039 func TestPrMerge_alreadyMerged_withMergeStrategy(t *testing.T) { 1040 http := initFakeHTTP() 1041 defer http.Verify(t) 1042 1043 shared.RunCommandFinder( 1044 "4", 1045 &api.PullRequest{ 1046 ID: "THE-ID", 1047 Number: 4, 1048 State: "MERGED", 1049 HeadRepositoryOwner: api.Owner{Login: "OWNER"}, 1050 MergeStateStatus: "CLEAN", 1051 }, 1052 baseRepo("OWNER", "REPO", "main"), 1053 ) 1054 1055 cs, cmdTeardown := run.Stub() 1056 defer cmdTeardown(t) 1057 1058 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 1059 1060 output, err := runCommand(http, "blueberries", false, "pr merge 4 --merge") 1061 if err != nil { 1062 t.Fatalf("Got unexpected error running `pr merge` %s", err) 1063 } 1064 1065 assert.Equal(t, "", output.String()) 1066 assert.Equal(t, "! Pull request #4 was already merged\n", output.Stderr()) 1067 } 1068 1069 func TestPrMerge_alreadyMerged_withMergeStrategy_TTY(t *testing.T) { 1070 http := initFakeHTTP() 1071 defer http.Verify(t) 1072 1073 shared.RunCommandFinder( 1074 "4", 1075 &api.PullRequest{ 1076 ID: "THE-ID", 1077 Number: 4, 1078 State: "MERGED", 1079 HeadRepositoryOwner: api.Owner{Login: "OWNER"}, 1080 MergeStateStatus: "CLEAN", 1081 }, 1082 baseRepo("OWNER", "REPO", "main"), 1083 ) 1084 1085 cs, cmdTeardown := run.Stub() 1086 defer cmdTeardown(t) 1087 1088 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 1089 cs.Register(`git branch -D `, 0, "") 1090 1091 //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock 1092 as := prompt.NewAskStubber(t) 1093 as.StubPrompt("Pull request #4 was already merged. Delete the branch locally?").AnswerWith(true) 1094 1095 output, err := runCommand(http, "blueberries", true, "pr merge 4 --merge") 1096 if err != nil { 1097 t.Fatalf("Got unexpected error running `pr merge` %s", err) 1098 } 1099 1100 assert.Equal(t, "", output.String()) 1101 assert.Equal(t, "✓ Deleted branch \n", output.Stderr()) 1102 } 1103 1104 func TestPrMerge_alreadyMerged_withMergeStrategy_crossRepo(t *testing.T) { 1105 http := initFakeHTTP() 1106 defer http.Verify(t) 1107 1108 shared.RunCommandFinder( 1109 "4", 1110 &api.PullRequest{ 1111 ID: "THE-ID", 1112 Number: 4, 1113 State: "MERGED", 1114 HeadRepositoryOwner: api.Owner{Login: "monalisa"}, 1115 MergeStateStatus: "CLEAN", 1116 }, 1117 baseRepo("OWNER", "REPO", "main"), 1118 ) 1119 1120 cs, cmdTeardown := run.Stub() 1121 defer cmdTeardown(t) 1122 1123 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 1124 1125 output, err := runCommand(http, "blueberries", true, "pr merge 4 --merge") 1126 if err != nil { 1127 t.Fatalf("Got unexpected error running `pr merge` %s", err) 1128 } 1129 1130 assert.Equal(t, "", output.String()) 1131 assert.Equal(t, "", output.Stderr()) 1132 } 1133 func TestPRMergeTTY(t *testing.T) { 1134 http := initFakeHTTP() 1135 defer http.Verify(t) 1136 1137 shared.RunCommandFinder( 1138 "", 1139 &api.PullRequest{ 1140 ID: "THE-ID", 1141 Number: 3, 1142 Title: "It was the best of times", 1143 HeadRefName: "blueberries", 1144 MergeStateStatus: "CLEAN", 1145 }, 1146 baseRepo("OWNER", "REPO", "main"), 1147 ) 1148 1149 http.Register( 1150 httpmock.GraphQL(`query RepositoryInfo\b`), 1151 httpmock.StringResponse(` 1152 { "data": { "repository": { 1153 "mergeCommitAllowed": true, 1154 "rebaseMergeAllowed": true, 1155 "squashMergeAllowed": true 1156 } } }`)) 1157 1158 http.Register( 1159 httpmock.GraphQL(`mutation PullRequestMerge\b`), 1160 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 1161 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 1162 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 1163 assert.NotContains(t, input, "commitHeadline") 1164 })) 1165 1166 cs, cmdTeardown := run.Stub() 1167 defer cmdTeardown(t) 1168 1169 cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") 1170 1171 //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock 1172 as := prompt.NewAskStubber(t) 1173 as.StubPrompt("What merge method would you like to use?").AnswerDefault() 1174 as.StubPrompt("Delete the branch locally and on GitHub?").AnswerDefault() 1175 as.StubPrompt("What's next?").AnswerWith("Submit") 1176 1177 output, err := runCommand(http, "blueberries", true, "") 1178 if err != nil { 1179 t.Fatalf("Got unexpected error running `pr merge` %s", err) 1180 } 1181 1182 assert.Equal(t, "✓ Merged pull request #3 (It was the best of times)\n", output.Stderr()) 1183 } 1184 1185 func TestPRMergeTTY_withDeleteBranch(t *testing.T) { 1186 http := initFakeHTTP() 1187 defer http.Verify(t) 1188 1189 shared.RunCommandFinder( 1190 "", 1191 &api.PullRequest{ 1192 ID: "THE-ID", 1193 Number: 3, 1194 Title: "It was the best of times", 1195 HeadRefName: "blueberries", 1196 MergeStateStatus: "CLEAN", 1197 BaseRefName: "main", 1198 }, 1199 baseRepo("OWNER", "REPO", "main"), 1200 ) 1201 1202 http.Register( 1203 httpmock.GraphQL(`query RepositoryInfo\b`), 1204 httpmock.StringResponse(` 1205 { "data": { "repository": { 1206 "mergeCommitAllowed": true, 1207 "rebaseMergeAllowed": true, 1208 "squashMergeAllowed": true, 1209 "mergeQueue": { 1210 "mergeMethod": "" 1211 } 1212 } } }`)) 1213 http.Register( 1214 httpmock.GraphQL(`mutation PullRequestMerge\b`), 1215 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 1216 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 1217 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 1218 assert.NotContains(t, input, "commitHeadline") 1219 })) 1220 http.Register( 1221 httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), 1222 httpmock.StringResponse(`{}`)) 1223 1224 cs, cmdTeardown := run.Stub() 1225 defer cmdTeardown(t) 1226 1227 cs.Register(`git rev-parse --verify refs/heads/main`, 0, "") 1228 cs.Register(`git checkout main`, 0, "") 1229 cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") 1230 cs.Register(`git branch -D blueberries`, 0, "") 1231 cs.Register(`git pull --ff-only`, 0, "") 1232 1233 //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock 1234 as := prompt.NewAskStubber(t) 1235 as.StubPrompt("What merge method would you like to use?").AnswerDefault() 1236 as.StubPrompt("What's next?").AnswerWith("Submit") 1237 1238 output, err := runCommand(http, "blueberries", true, "-d") 1239 if err != nil { 1240 t.Fatalf("Got unexpected error running `pr merge` %s", err) 1241 } 1242 1243 assert.Equal(t, "", output.String()) 1244 assert.Equal(t, heredoc.Doc(` 1245 ✓ Merged pull request #3 (It was the best of times) 1246 ✓ Deleted branch blueberries and switched to branch main 1247 `), output.Stderr()) 1248 } 1249 1250 func TestPRMergeTTY_squashEditCommitMsgAndSubject(t *testing.T) { 1251 ios, _, stdout, stderr := iostreams.Test() 1252 ios.SetStdinTTY(true) 1253 ios.SetStdoutTTY(true) 1254 ios.SetStderrTTY(true) 1255 1256 tr := initFakeHTTP() 1257 defer tr.Verify(t) 1258 1259 tr.Register( 1260 httpmock.GraphQL(`query RepositoryInfo\b`), 1261 httpmock.StringResponse(` 1262 { "data": { "repository": { 1263 "mergeCommitAllowed": true, 1264 "rebaseMergeAllowed": true, 1265 "squashMergeAllowed": true 1266 } } }`)) 1267 tr.Register( 1268 httpmock.GraphQL(`query PullRequestMergeText\b`), 1269 httpmock.StringResponse(` 1270 { "data": { "node": { 1271 "viewerMergeHeadlineText": "default headline text", 1272 "viewerMergeBodyText": "default body text" 1273 } } }`)) 1274 tr.Register( 1275 httpmock.GraphQL(`query PullRequestMergeText\b`), 1276 httpmock.StringResponse(` 1277 { "data": { "node": { 1278 "viewerMergeHeadlineText": "default headline text", 1279 "viewerMergeBodyText": "default body text" 1280 } } }`)) 1281 tr.Register( 1282 httpmock.GraphQL(`mutation PullRequestMerge\b`), 1283 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 1284 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 1285 assert.Equal(t, "SQUASH", input["mergeMethod"].(string)) 1286 assert.Equal(t, "DEFAULT HEADLINE TEXT", input["commitHeadline"].(string)) 1287 assert.Equal(t, "DEFAULT BODY TEXT", input["commitBody"].(string)) 1288 })) 1289 1290 _, cmdTeardown := run.Stub() 1291 defer cmdTeardown(t) 1292 1293 //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock 1294 as := prompt.NewAskStubber(t) 1295 as.StubPrompt("What merge method would you like to use?").AnswerWith("Squash and merge") 1296 as.StubPrompt("Delete the branch on GitHub?").AnswerDefault() 1297 as.StubPrompt("What's next?").AnswerWith("Edit commit message") 1298 as.StubPrompt("What's next?").AnswerWith("Edit commit subject") 1299 as.StubPrompt("What's next?").AnswerWith("Submit") 1300 1301 err := mergeRun(&MergeOptions{ 1302 IO: ios, 1303 Editor: testEditor{}, 1304 HttpClient: func() (*http.Client, error) { 1305 return &http.Client{Transport: tr}, nil 1306 }, 1307 SelectorArg: "https://github.com/OWNER/REPO/pull/123", 1308 MergeStrategyEmpty: true, 1309 Finder: shared.NewMockFinder( 1310 "https://github.com/OWNER/REPO/pull/123", 1311 &api.PullRequest{ID: "THE-ID", Number: 123, Title: "title", MergeStateStatus: "CLEAN"}, 1312 ghrepo.New("OWNER", "REPO"), 1313 ), 1314 }) 1315 assert.NoError(t, err) 1316 1317 assert.Equal(t, "", stdout.String()) 1318 assert.Equal(t, "✓ Squashed and merged pull request #123 (title)\n", stderr.String()) 1319 } 1320 1321 func TestPRMergeEmptyStrategyNonTTY(t *testing.T) { 1322 http := initFakeHTTP() 1323 defer http.Verify(t) 1324 1325 shared.RunCommandFinder( 1326 "1", 1327 &api.PullRequest{ 1328 ID: "THE-ID", 1329 Number: 1, 1330 State: "OPEN", 1331 Title: "The title of the PR", 1332 MergeStateStatus: "CLEAN", 1333 BaseRefName: "main", 1334 }, 1335 baseRepo("OWNER", "REPO", "main"), 1336 ) 1337 1338 cs, cmdTeardown := run.Stub() 1339 defer cmdTeardown(t) 1340 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 1341 1342 output, err := runCommand(http, "blueberries", false, "pr merge 1") 1343 assert.EqualError(t, err, "--merge, --rebase, or --squash required when not running interactively") 1344 assert.Equal(t, "", output.String()) 1345 assert.Equal(t, "", output.Stderr()) 1346 } 1347 1348 func TestPRTTY_cancelled(t *testing.T) { 1349 http := initFakeHTTP() 1350 defer http.Verify(t) 1351 1352 shared.RunCommandFinder( 1353 "", 1354 &api.PullRequest{ID: "THE-ID", Number: 123, MergeStateStatus: "CLEAN"}, 1355 ghrepo.New("OWNER", "REPO"), 1356 ) 1357 1358 http.Register( 1359 httpmock.GraphQL(`query RepositoryInfo\b`), 1360 httpmock.StringResponse(` 1361 { "data": { "repository": { 1362 "mergeCommitAllowed": true, 1363 "rebaseMergeAllowed": true, 1364 "squashMergeAllowed": true 1365 } } }`)) 1366 1367 cs, cmdTeardown := run.Stub() 1368 defer cmdTeardown(t) 1369 1370 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 1371 1372 //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock 1373 as := prompt.NewAskStubber(t) 1374 as.StubPrompt("What merge method would you like to use?").AnswerDefault() 1375 as.StubPrompt("Delete the branch locally and on GitHub?").AnswerDefault() 1376 as.StubPrompt("What's next?").AnswerWith("Cancel") 1377 1378 output, err := runCommand(http, "blueberries", true, "") 1379 if !errors.Is(err, cmdutil.CancelError) { 1380 t.Fatalf("got error %v", err) 1381 } 1382 1383 assert.Equal(t, "Cancelled.\n", output.Stderr()) 1384 } 1385 1386 func Test_mergeMethodSurvey(t *testing.T) { 1387 repo := &api.Repository{ 1388 MergeCommitAllowed: false, 1389 RebaseMergeAllowed: true, 1390 SquashMergeAllowed: true, 1391 } 1392 //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock 1393 as := prompt.NewAskStubber(t) 1394 as.StubPrompt("What merge method would you like to use?").AnswerWith("Rebase and merge") 1395 1396 method, err := mergeMethodSurvey(repo) 1397 assert.Nil(t, err) 1398 assert.Equal(t, PullRequestMergeMethodRebase, method) 1399 } 1400 1401 func TestMergeRun_autoMerge(t *testing.T) { 1402 ios, _, stdout, stderr := iostreams.Test() 1403 ios.SetStdoutTTY(true) 1404 ios.SetStderrTTY(true) 1405 1406 tr := initFakeHTTP() 1407 defer tr.Verify(t) 1408 tr.Register( 1409 httpmock.GraphQL(`mutation PullRequestAutoMerge\b`), 1410 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 1411 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 1412 assert.Equal(t, "SQUASH", input["mergeMethod"].(string)) 1413 })) 1414 1415 _, cmdTeardown := run.Stub() 1416 defer cmdTeardown(t) 1417 1418 err := mergeRun(&MergeOptions{ 1419 IO: ios, 1420 HttpClient: func() (*http.Client, error) { 1421 return &http.Client{Transport: tr}, nil 1422 }, 1423 SelectorArg: "https://github.com/OWNER/REPO/pull/123", 1424 AutoMergeEnable: true, 1425 MergeMethod: PullRequestMergeMethodSquash, 1426 Finder: shared.NewMockFinder( 1427 "https://github.com/OWNER/REPO/pull/123", 1428 &api.PullRequest{ID: "THE-ID", Number: 123, MergeStateStatus: "BLOCKED"}, 1429 ghrepo.New("OWNER", "REPO"), 1430 ), 1431 }) 1432 assert.NoError(t, err) 1433 1434 assert.Equal(t, "", stdout.String()) 1435 assert.Equal(t, "✓ Pull request #123 will be automatically merged via squash when all requirements are met\n", stderr.String()) 1436 } 1437 1438 func TestMergeRun_autoMerge_directMerge(t *testing.T) { 1439 ios, _, stdout, stderr := iostreams.Test() 1440 ios.SetStdoutTTY(true) 1441 ios.SetStderrTTY(true) 1442 1443 tr := initFakeHTTP() 1444 defer tr.Verify(t) 1445 tr.Register( 1446 httpmock.GraphQL(`mutation PullRequestMerge\b`), 1447 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 1448 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 1449 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 1450 assert.NotContains(t, input, "commitHeadline") 1451 })) 1452 1453 _, cmdTeardown := run.Stub() 1454 defer cmdTeardown(t) 1455 1456 err := mergeRun(&MergeOptions{ 1457 IO: ios, 1458 HttpClient: func() (*http.Client, error) { 1459 return &http.Client{Transport: tr}, nil 1460 }, 1461 SelectorArg: "https://github.com/OWNER/REPO/pull/123", 1462 AutoMergeEnable: true, 1463 MergeMethod: PullRequestMergeMethodMerge, 1464 Finder: shared.NewMockFinder( 1465 "https://github.com/OWNER/REPO/pull/123", 1466 &api.PullRequest{ID: "THE-ID", Number: 123, MergeStateStatus: "CLEAN"}, 1467 ghrepo.New("OWNER", "REPO"), 1468 ), 1469 }) 1470 assert.NoError(t, err) 1471 1472 assert.Equal(t, "", stdout.String()) 1473 assert.Equal(t, "✓ Merged pull request #123 ()\n", stderr.String()) 1474 } 1475 1476 func TestMergeRun_disableAutoMerge(t *testing.T) { 1477 ios, _, stdout, stderr := iostreams.Test() 1478 ios.SetStdoutTTY(true) 1479 ios.SetStderrTTY(true) 1480 1481 tr := initFakeHTTP() 1482 defer tr.Verify(t) 1483 tr.Register( 1484 httpmock.GraphQL(`mutation PullRequestAutoMergeDisable\b`), 1485 httpmock.GraphQLQuery(`{}`, func(s string, m map[string]interface{}) { 1486 assert.Equal(t, map[string]interface{}{"prID": "THE-ID"}, m) 1487 })) 1488 1489 _, cmdTeardown := run.Stub() 1490 defer cmdTeardown(t) 1491 1492 err := mergeRun(&MergeOptions{ 1493 IO: ios, 1494 HttpClient: func() (*http.Client, error) { 1495 return &http.Client{Transport: tr}, nil 1496 }, 1497 SelectorArg: "https://github.com/OWNER/REPO/pull/123", 1498 AutoMergeDisable: true, 1499 Finder: shared.NewMockFinder( 1500 "https://github.com/OWNER/REPO/pull/123", 1501 &api.PullRequest{ID: "THE-ID", Number: 123}, 1502 ghrepo.New("OWNER", "REPO"), 1503 ), 1504 }) 1505 assert.NoError(t, err) 1506 1507 assert.Equal(t, "", stdout.String()) 1508 assert.Equal(t, "✓ Auto-merge disabled for pull request #123\n", stderr.String()) 1509 } 1510 1511 func TestPrInMergeQueue(t *testing.T) { 1512 http := initFakeHTTP() 1513 defer http.Verify(t) 1514 1515 shared.RunCommandFinder( 1516 "1", 1517 &api.PullRequest{ 1518 ID: "THE-ID", 1519 Number: 1, 1520 State: "OPEN", 1521 Title: "The title of the PR", 1522 MergeStateStatus: "CLEAN", 1523 IsInMergeQueue: true, 1524 IsMergeQueueEnabled: true, 1525 }, 1526 baseRepo("OWNER", "REPO", "main"), 1527 ) 1528 1529 cs, cmdTeardown := run.Stub() 1530 defer cmdTeardown(t) 1531 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 1532 1533 output, err := runCommand(http, "blueberries", true, "pr merge 1") 1534 if err != nil { 1535 t.Fatalf("error running command `pr merge`: %v", err) 1536 } 1537 1538 assert.Equal(t, "", output.String()) 1539 assert.Equal(t, "! Pull request #1 is already queued to merge\n", output.Stderr()) 1540 } 1541 1542 func TestPrAddToMergeQueueWithMergeMethod(t *testing.T) { 1543 http := initFakeHTTP() 1544 defer http.Verify(t) 1545 1546 shared.RunCommandFinder( 1547 "1", 1548 &api.PullRequest{ 1549 ID: "THE-ID", 1550 Number: 1, 1551 State: "OPEN", 1552 Title: "The title of the PR", 1553 MergeStateStatus: "CLEAN", 1554 IsInMergeQueue: false, 1555 IsMergeQueueEnabled: true, 1556 BaseRefName: "main", 1557 }, 1558 baseRepo("OWNER", "REPO", "main"), 1559 ) 1560 http.Register( 1561 httpmock.GraphQL(`mutation PullRequestAutoMerge\b`), 1562 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 1563 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 1564 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 1565 }), 1566 ) 1567 1568 cs, cmdTeardown := run.Stub() 1569 defer cmdTeardown(t) 1570 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 1571 1572 output, err := runCommand(http, "blueberries", true, "pr merge 1 --merge") 1573 if err != nil { 1574 t.Fatalf("error running command `pr merge`: %v", err) 1575 } 1576 assert.Equal(t, "", output.String()) 1577 assert.Equal(t, "! The merge strategy for main is set by the merge queue\n✓ Pull request #1 will be added to the merge queue for main when ready\n", output.Stderr()) 1578 } 1579 1580 func TestPrAddToMergeQueueClean(t *testing.T) { 1581 http := initFakeHTTP() 1582 defer http.Verify(t) 1583 1584 shared.RunCommandFinder( 1585 "1", 1586 &api.PullRequest{ 1587 ID: "THE-ID", 1588 Number: 1, 1589 State: "OPEN", 1590 Title: "The title of the PR", 1591 MergeStateStatus: "CLEAN", 1592 IsInMergeQueue: false, 1593 IsMergeQueueEnabled: true, 1594 BaseRefName: "main", 1595 }, 1596 baseRepo("OWNER", "REPO", "main"), 1597 ) 1598 1599 http.Register( 1600 httpmock.GraphQL(`mutation PullRequestAutoMerge\b`), 1601 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 1602 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 1603 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 1604 }), 1605 ) 1606 1607 cs, cmdTeardown := run.Stub() 1608 defer cmdTeardown(t) 1609 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 1610 1611 output, err := runCommand(http, "blueberries", true, "pr merge 1") 1612 if err != nil { 1613 t.Fatalf("error running command `pr merge`: %v", err) 1614 } 1615 1616 assert.Equal(t, "", output.String()) 1617 assert.Equal(t, "✓ Pull request #1 will be added to the merge queue for main when ready\n", output.Stderr()) 1618 } 1619 1620 func TestPrAddToMergeQueueBlocked(t *testing.T) { 1621 http := initFakeHTTP() 1622 defer http.Verify(t) 1623 1624 shared.RunCommandFinder( 1625 "1", 1626 &api.PullRequest{ 1627 ID: "THE-ID", 1628 Number: 1, 1629 State: "OPEN", 1630 Title: "The title of the PR", 1631 MergeStateStatus: "BLOCKED", 1632 IsInMergeQueue: false, 1633 IsMergeQueueEnabled: true, 1634 BaseRefName: "main", 1635 }, 1636 baseRepo("OWNER", "REPO", "main"), 1637 ) 1638 1639 http.Register( 1640 httpmock.GraphQL(`mutation PullRequestAutoMerge\b`), 1641 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 1642 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 1643 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 1644 }), 1645 ) 1646 1647 cs, cmdTeardown := run.Stub() 1648 defer cmdTeardown(t) 1649 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 1650 1651 output, err := runCommand(http, "blueberries", true, "pr merge 1") 1652 if err != nil { 1653 t.Fatalf("error running command `pr merge`: %v", err) 1654 } 1655 1656 assert.Equal(t, "", output.String()) 1657 assert.Equal(t, "✓ Pull request #1 will be added to the merge queue for main when ready\n", output.Stderr()) 1658 } 1659 1660 func TestPrAddToMergeQueueAdmin(t *testing.T) { 1661 http := initFakeHTTP() 1662 defer http.Verify(t) 1663 1664 shared.RunCommandFinder( 1665 "1", 1666 &api.PullRequest{ 1667 ID: "THE-ID", 1668 Number: 1, 1669 State: "OPEN", 1670 Title: "The title of the PR", 1671 MergeStateStatus: "CLEAN", 1672 IsInMergeQueue: false, 1673 IsMergeQueueEnabled: true, 1674 }, 1675 baseRepo("OWNER", "REPO", "main"), 1676 ) 1677 1678 http.Register( 1679 httpmock.GraphQL(`query RepositoryInfo\b`), 1680 httpmock.StringResponse(` 1681 { "data": { "repository": { 1682 "mergeCommitAllowed": true, 1683 "rebaseMergeAllowed": true, 1684 "squashMergeAllowed": true 1685 } } }`)) 1686 1687 http.Register( 1688 httpmock.GraphQL(`mutation PullRequestMerge\b`), 1689 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 1690 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 1691 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 1692 assert.NotContains(t, input, "commitHeadline") 1693 }), 1694 ) 1695 1696 cs, cmdTeardown := run.Stub() 1697 defer cmdTeardown(t) 1698 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 1699 1700 //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock 1701 as := prompt.NewAskStubber(t) 1702 as.StubPrompt("What merge method would you like to use?").AnswerDefault() 1703 as.StubPrompt("Delete the branch locally and on GitHub?").AnswerDefault() 1704 as.StubPrompt("What's next?").AnswerDefault() 1705 1706 output, err := runCommand(http, "blueberries", true, "pr merge 1 --admin") 1707 if err != nil { 1708 t.Fatalf("error running command `pr merge`: %v", err) 1709 } 1710 1711 assert.Equal(t, "", output.String()) 1712 assert.Equal(t, "✓ Merged pull request #1 (The title of the PR)\n", output.Stderr()) 1713 } 1714 1715 func TestPrAddToMergeQueueAdminWithMergeStrategy(t *testing.T) { 1716 http := initFakeHTTP() 1717 defer http.Verify(t) 1718 1719 shared.RunCommandFinder( 1720 "1", 1721 &api.PullRequest{ 1722 ID: "THE-ID", 1723 Number: 1, 1724 State: "OPEN", 1725 Title: "The title of the PR", 1726 MergeStateStatus: "CLEAN", 1727 IsInMergeQueue: false, 1728 }, 1729 baseRepo("OWNER", "REPO", "main"), 1730 ) 1731 1732 http.Register( 1733 httpmock.GraphQL(`mutation PullRequestMerge\b`), 1734 httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { 1735 assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) 1736 assert.Equal(t, "MERGE", input["mergeMethod"].(string)) 1737 assert.NotContains(t, input, "commitHeadline") 1738 }), 1739 ) 1740 1741 cs, cmdTeardown := run.Stub() 1742 defer cmdTeardown(t) 1743 cs.Register(`git rev-parse --verify refs/heads/`, 0, "") 1744 1745 output, err := runCommand(http, "blueberries", true, "pr merge 1 --admin --merge") 1746 if err != nil { 1747 t.Fatalf("error running command `pr merge`: %v", err) 1748 } 1749 1750 assert.Equal(t, "", output.String()) 1751 assert.Equal(t, "✓ Merged pull request #1 (The title of the PR)\n", output.Stderr()) 1752 } 1753 1754 type testEditor struct{} 1755 1756 func (e testEditor) Edit(filename, text string) (string, error) { 1757 return strings.ToUpper(text), nil 1758 }