package merge import ( "bytes" "errors" "fmt" "io" "net/http" "os" "path/filepath" "regexp" "strings" "testing" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/internal/run" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/test" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_NewCmdMerge(t *testing.T) { tmpFile := filepath.Join(t.TempDir(), "my-body.md") err := os.WriteFile(tmpFile, []byte("a body from file"), 0600) require.NoError(t, err) tests := []struct { name string args string stdin string isTTY bool want MergeOptions wantErr string }{ { name: "number argument", args: "123", isTTY: true, want: MergeOptions{ SelectorArg: "123", DeleteBranch: false, IsDeleteBranchIndicated: false, CanDeleteLocalBranch: true, MergeMethod: PullRequestMergeMethodMerge, MergeStrategyEmpty: true, Body: "", BodySet: false, AuthorEmail: "", }, }, { name: "delete-branch specified", args: "--delete-branch=false", isTTY: true, want: MergeOptions{ SelectorArg: "", DeleteBranch: false, IsDeleteBranchIndicated: true, CanDeleteLocalBranch: true, MergeMethod: PullRequestMergeMethodMerge, MergeStrategyEmpty: true, Body: "", BodySet: false, AuthorEmail: "", }, }, { name: "body from file", args: fmt.Sprintf("123 --body-file '%s'", tmpFile), isTTY: true, want: MergeOptions{ SelectorArg: "123", DeleteBranch: false, IsDeleteBranchIndicated: false, CanDeleteLocalBranch: true, MergeMethod: PullRequestMergeMethodMerge, MergeStrategyEmpty: true, Body: "a body from file", BodySet: true, AuthorEmail: "", }, }, { name: "body from stdin", args: "123 --body-file -", stdin: "this is on standard input", isTTY: true, want: MergeOptions{ SelectorArg: "123", DeleteBranch: false, IsDeleteBranchIndicated: false, CanDeleteLocalBranch: true, MergeMethod: PullRequestMergeMethodMerge, MergeStrategyEmpty: true, Body: "this is on standard input", BodySet: true, AuthorEmail: "", }, }, { name: "body", args: "123 -bcool", isTTY: true, want: MergeOptions{ SelectorArg: "123", DeleteBranch: false, IsDeleteBranchIndicated: false, CanDeleteLocalBranch: true, MergeMethod: PullRequestMergeMethodMerge, MergeStrategyEmpty: true, Body: "cool", BodySet: true, AuthorEmail: "", }, }, { name: "match-head-commit specified", args: "123 --match-head-commit 555", isTTY: true, want: MergeOptions{ SelectorArg: "123", DeleteBranch: false, IsDeleteBranchIndicated: false, CanDeleteLocalBranch: true, MergeMethod: PullRequestMergeMethodMerge, MergeStrategyEmpty: true, Body: "", BodySet: false, MatchHeadCommit: "555", AuthorEmail: "", }, }, { name: "author email", args: "123 --author-email octocat@github.com", isTTY: true, want: MergeOptions{ SelectorArg: "123", DeleteBranch: false, IsDeleteBranchIndicated: false, CanDeleteLocalBranch: true, MergeMethod: PullRequestMergeMethodMerge, MergeStrategyEmpty: true, Body: "", BodySet: false, AuthorEmail: "octocat@github.com", }, }, { name: "body and body-file flags", args: "123 --body 'test' --body-file 'test-file.txt'", isTTY: true, wantErr: "specify only one of `--body` or `--body-file`", }, { name: "no argument with --repo override", args: "-R owner/repo", isTTY: true, wantErr: "argument required when using the --repo flag", }, { name: "multiple merge methods", args: "123 --merge --rebase", isTTY: true, wantErr: "only one of --merge, --rebase, or --squash can be enabled", }, { name: "multiple merge methods, non-tty", args: "123 --merge --rebase", isTTY: false, wantErr: "only one of --merge, --rebase, or --squash can be enabled", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, stdin, _, _ := iostreams.Test() ios.SetStdoutTTY(tt.isTTY) ios.SetStdinTTY(tt.isTTY) ios.SetStderrTTY(tt.isTTY) if tt.stdin != "" { _, _ = stdin.WriteString(tt.stdin) } f := &cmdutil.Factory{ IOStreams: ios, } var opts *MergeOptions cmd := NewCmdMerge(f, func(o *MergeOptions) error { opts = o return nil }) cmd.PersistentFlags().StringP("repo", "R", "", "") argv, err := shlex.Split(tt.args) require.NoError(t, err) cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) _, err = cmd.ExecuteC() if tt.wantErr != "" { require.EqualError(t, err, tt.wantErr) return } else { require.NoError(t, err) } assert.Equal(t, tt.want.SelectorArg, opts.SelectorArg) assert.Equal(t, tt.want.DeleteBranch, opts.DeleteBranch) assert.Equal(t, tt.want.CanDeleteLocalBranch, opts.CanDeleteLocalBranch) assert.Equal(t, tt.want.MergeMethod, opts.MergeMethod) assert.Equal(t, tt.want.MergeStrategyEmpty, opts.MergeStrategyEmpty) assert.Equal(t, tt.want.Body, opts.Body) assert.Equal(t, tt.want.BodySet, opts.BodySet) assert.Equal(t, tt.want.MatchHeadCommit, opts.MatchHeadCommit) assert.Equal(t, tt.want.AuthorEmail, opts.AuthorEmail) }) } } func baseRepo(owner, repo, branch string) ghrepo.Interface { return api.InitRepoHostname(&api.Repository{ Name: repo, Owner: api.RepositoryOwner{Login: owner}, DefaultBranchRef: api.BranchRef{Name: branch}, }, "github.com") } func stubCommit(pr *api.PullRequest, oid string) { pr.Commits.Nodes = append(pr.Commits.Nodes, api.PullRequestCommit{ Commit: api.PullRequestCommitCommit{OID: oid}, }) } // TODO port to new style tests func runCommand(rt http.RoundTripper, pm *prompter.PrompterMock, branch string, isTTY bool, cli string) (*test.CmdOut, error) { ios, _, stdout, stderr := iostreams.Test() ios.SetStdoutTTY(isTTY) ios.SetStdinTTY(isTTY) ios.SetStderrTTY(isTTY) factory := &cmdutil.Factory{ IOStreams: ios, HttpClient: func() (*http.Client, error) { return &http.Client{Transport: rt}, nil }, Branch: func() (string, error) { return branch, nil }, Remotes: func() (context.Remotes, error) { return []*context.Remote{ { Remote: &git.Remote{ Name: "origin", }, Repo: ghrepo.New("OWNER", "REPO"), }, }, nil }, GitClient: &git.Client{ GhPath: "some/path/gh", GitPath: "some/path/git", }, Prompter: pm, } cmd := NewCmdMerge(factory, nil) cmd.PersistentFlags().StringP("repo", "R", "", "") cli = strings.TrimPrefix(cli, "pr merge") argv, err := shlex.Split(cli) if err != nil { return nil, err } cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) _, err = cmd.ExecuteC() return &test.CmdOut{ OutBuf: stdout, ErrBuf: stderr, }, err } func initFakeHTTP() *httpmock.Registry { return &httpmock.Registry{} } func TestPrMerge(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") }), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "main", true, "pr merge 1 --merge") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } r := regexp.MustCompile(`Merged pull request OWNER/REPO#1 \(The title of the PR\)`) if !r.MatchString(output.Stderr()) { t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) } } func TestPrMerge_blocked(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "BLOCKED", }, baseRepo("OWNER", "REPO", "main"), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "main", true, "pr merge 1 --merge") assert.EqualError(t, err, "SilentError") assert.Equal(t, "", output.String()) assert.Equal(t, heredoc.Docf(` X Pull request OWNER/REPO#1 is not mergeable: the base branch policy prohibits the merge. To have the pull request merged after all the requirements have been met, add the %[1]s--auto%[1]s flag. To use administrator privileges to immediately merge the pull request, add the %[1]s--admin%[1]s flag. `, "`"), output.Stderr()) } func TestPrMerge_dirty(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 123, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "DIRTY", BaseRefName: "trunk", HeadRefName: "feature", }, baseRepo("OWNER", "REPO", "main"), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "main", true, "pr merge 1 --merge") assert.EqualError(t, err, "SilentError") assert.Equal(t, "", output.String()) assert.Equal(t, heredoc.Docf(` X Pull request OWNER/REPO#123 is not mergeable: the merge commit cannot be cleanly created. To have the pull request merged after all the requirements have been met, add the %[1]s--auto%[1]s flag. Run the following to resolve the merge conflicts locally: gh pr checkout 123 && git fetch origin trunk && git merge origin/trunk `, "`"), output.Stderr()) } func TestPrMerge_nontty(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "main", false, "pr merge 1 --merge") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) } func TestPrMerge_editMessage_nontty(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.Equal(t, "mytitle", input["commitHeadline"].(string)) assert.Equal(t, "mybody", input["commitBody"].(string)) })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "main", false, "pr merge 1 --merge -t mytitle -b mybody") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) } func TestPrMerge_withRepoFlag(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) _, cmdTeardown := run.Stub() defer cmdTeardown(t) output, err := runCommand(http, nil, "main", true, "pr merge 1 --merge -R OWNER/REPO") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } r := regexp.MustCompile(`Merged pull request OWNER/REPO#1 \(The title of the PR\)`) if !r.MatchString(output.Stderr()) { t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) } } func TestPrMerge_withMatchCommitHeadFlag(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, 3, len(input)) assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.Equal(t, "285ed5ab740f53ff6b0b4b629c59a9df23b9c6db", input["expectedHeadOid"].(string)) })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "main", true, "pr merge 1 --merge --match-head-commit 285ed5ab740f53ff6b0b4b629c59a9df23b9c6db") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } r := regexp.MustCompile(`Merged pull request OWNER/REPO#1 \(The title of the PR\)`) if !r.MatchString(output.Stderr()) { t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) } } func TestPrMerge_withAuthorFlag(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.Equal(t, "octocat@github.com", input["authorEmail"].(string)) assert.NotContains(t, input, "commitHeadline") }), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "main", true, "pr merge 1 --merge --author-email octocat@github.com") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } r := regexp.MustCompile(`Merged pull request OWNER/REPO#1 \(The title of the PR\)`) if !r.MatchString(output.Stderr()) { t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) } } func TestPrMerge_deleteBranch(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "", &api.PullRequest{ ID: "PR_10", Number: 10, State: "OPEN", Title: "Blueberries are a good fruit", HeadRefName: "blueberries", BaseRefName: "main", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "PR_10", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) http.Register( httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), httpmock.StringResponse(`{}`)) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/main`, 0, "") cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) if err != nil { t.Fatalf("Got unexpected error running `pr merge` %s", err) } assert.Equal(t, "", output.String()) assert.Equal(t, heredoc.Doc(` ✓ Merged pull request OWNER/REPO#10 (Blueberries are a good fruit) ✓ Deleted local branch blueberries and switched to branch main ✓ Deleted remote branch blueberries `), output.Stderr()) } func TestPrMerge_deleteBranch_nonDefault(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "", &api.PullRequest{ ID: "PR_10", Number: 10, State: "OPEN", Title: "Blueberries are a good fruit", HeadRefName: "blueberries", MergeStateStatus: "CLEAN", BaseRefName: "fruit", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "PR_10", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) http.Register( httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), httpmock.StringResponse(`{}`)) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/fruit`, 0, "") cs.Register(`git checkout fruit`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) if err != nil { t.Fatalf("Got unexpected error running `pr merge` %s", err) } assert.Equal(t, "", output.String()) assert.Equal(t, heredoc.Doc(` ✓ Merged pull request OWNER/REPO#10 (Blueberries are a good fruit) ✓ Deleted local branch blueberries and switched to branch fruit ✓ Deleted remote branch blueberries `), output.Stderr()) } func TestPrMerge_deleteBranch_onlyLocally(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "", &api.PullRequest{ ID: "PR_10", Number: 10, State: "OPEN", Title: "Blueberries are a good fruit", HeadRefName: "blueberries", BaseRefName: "main", MergeStateStatus: "CLEAN", HeadRepositoryOwner: api.Owner{Login: "HEAD"}, // Not the same owner as the base repo }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "PR_10", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/main`, 0, "") cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) if err != nil { t.Fatalf("Got unexpected error running `pr merge` %s", err) } assert.Equal(t, "", output.String()) assert.Equal(t, heredoc.Doc(` ✓ Merged pull request OWNER/REPO#10 (Blueberries are a good fruit) ✓ Deleted local branch blueberries and switched to branch main `), output.Stderr()) } func TestPrMerge_deleteBranch_checkoutNewBranch(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "", &api.PullRequest{ ID: "PR_10", Number: 10, State: "OPEN", Title: "Blueberries are a good fruit", HeadRefName: "blueberries", MergeStateStatus: "CLEAN", BaseRefName: "fruit", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "PR_10", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) http.Register( httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), httpmock.StringResponse(`{}`)) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/fruit`, 1, "") cs.Register(`git checkout -b fruit --track origin/fruit`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) if err != nil { t.Fatalf("Got unexpected error running `pr merge` %s", err) } assert.Equal(t, "", output.String()) assert.Equal(t, heredoc.Doc(` ✓ Merged pull request OWNER/REPO#10 (Blueberries are a good fruit) ✓ Deleted local branch blueberries and switched to branch fruit ✓ Deleted remote branch blueberries `), output.Stderr()) } func TestPrMerge_deleteNonCurrentBranch(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "blueberries", &api.PullRequest{ ID: "PR_10", Number: 10, State: "OPEN", Title: "Blueberries are a good fruit", HeadRefName: "blueberries", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "PR_10", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) http.Register( httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), httpmock.StringResponse(`{}`)) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") output, err := runCommand(http, nil, "main", true, `pr merge --merge --delete-branch blueberries`) if err != nil { t.Fatalf("Got unexpected error running `pr merge` %s", err) } assert.Equal(t, "", output.String()) assert.Equal(t, heredoc.Doc(` ✓ Merged pull request OWNER/REPO#10 (Blueberries are a good fruit) ✓ Deleted local branch blueberries ✓ Deleted remote branch blueberries `), output.Stderr()) } func Test_nonDivergingPullRequest(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) pr := &api.PullRequest{ ID: "PR_10", Number: 10, Title: "Blueberries are a good fruit", State: "OPEN", MergeStateStatus: "CLEAN", BaseRefName: "main", } stubCommit(pr, "COMMITSHA1") shared.RunCommandFinder("", pr, baseRepo("OWNER", "REPO", "main")) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "PR_10", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git .+ show .+ HEAD`, 0, "COMMITSHA1,title") cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "blueberries", true, "pr merge --merge") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, heredoc.Doc(` ✓ Merged pull request OWNER/REPO#10 (Blueberries are a good fruit) `), output.Stderr()) } func Test_divergingPullRequestWarning(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) pr := &api.PullRequest{ ID: "PR_10", Number: 10, Title: "Blueberries are a good fruit", State: "OPEN", MergeStateStatus: "CLEAN", BaseRefName: "main", } stubCommit(pr, "COMMITSHA1") shared.RunCommandFinder("", pr, baseRepo("OWNER", "REPO", "main")) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "PR_10", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git .+ show .+ HEAD`, 0, "COMMITSHA2,title") cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "blueberries", true, "pr merge --merge") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, heredoc.Doc(` ! Pull request OWNER/REPO#10 (Blueberries are a good fruit) has diverged from local branch ✓ Merged pull request OWNER/REPO#10 (Blueberries are a good fruit) `), output.Stderr()) } func Test_pullRequestWithoutCommits(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "", &api.PullRequest{ ID: "PR_10", Number: 10, Title: "Blueberries are a good fruit", State: "OPEN", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "PR_10", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "blueberries", true, "pr merge --merge") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, heredoc.Doc(` ✓ Merged pull request OWNER/REPO#10 (Blueberries are a good fruit) `), output.Stderr()) } func TestPrMerge_rebase(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "2", &api.PullRequest{ ID: "THE-ID", Number: 2, Title: "The title of the PR", State: "OPEN", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "REBASE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "main", true, "pr merge 2 --rebase") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } r := regexp.MustCompile(`Rebased and merged pull request OWNER/REPO#2 \(The title of the PR\)`) if !r.MatchString(output.Stderr()) { t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr()) } } func TestPrMerge_squash(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "3", &api.PullRequest{ ID: "THE-ID", Number: 3, Title: "The title of the PR", State: "OPEN", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "SQUASH", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "main", true, "pr merge 3 --squash") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, "", output.String()) assert.Equal(t, heredoc.Doc(` ✓ Squashed and merged pull request OWNER/REPO#3 (The title of the PR) `), output.Stderr()) } func TestPrMerge_alreadyMerged(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "4", &api.PullRequest{ ID: "THE-ID", Number: 4, State: "MERGED", HeadRefName: "blueberries", BaseRefName: "main", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/main`, 0, "") cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") cs.Register(`git pull --ff-only`, 0, "") pm := &prompter.PrompterMock{ ConfirmFunc: func(p string, d bool) (bool, error) { if p == "Pull request OWNER/REPO#4 was already merged. Delete the branch locally?" { return true, nil } else { return false, prompter.NoSuchPromptErr(p) } }, } output, err := runCommand(http, pm, "blueberries", true, "pr merge 4") assert.NoError(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, heredoc.Doc(` ✓ Deleted local branch blueberries and switched to branch main ✓ Deleted remote branch blueberries `), output.Stderr()) } func TestPrMerge_alreadyMerged_withMergeStrategy(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "4", &api.PullRequest{ ID: "THE-ID", Number: 4, State: "MERGED", HeadRepositoryOwner: api.Owner{Login: "OWNER"}, MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "blueberries", false, "pr merge 4 --merge") if err != nil { t.Fatalf("Got unexpected error running `pr merge` %s", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "! Pull request OWNER/REPO#4 was already merged\n", output.Stderr()) } func TestPrMerge_alreadyMerged_withMergeStrategy_TTY(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "4", &api.PullRequest{ ID: "THE-ID", Number: 4, State: "MERGED", HeadRepositoryOwner: api.Owner{Login: "OWNER"}, MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") cs.Register(`git branch -D `, 0, "") pm := &prompter.PrompterMock{ ConfirmFunc: func(p string, d bool) (bool, error) { if p == "Pull request OWNER/REPO#4 was already merged. Delete the branch locally?" { return true, nil } else { return false, prompter.NoSuchPromptErr(p) } }, } output, err := runCommand(http, pm, "blueberries", true, "pr merge 4 --merge") if err != nil { t.Fatalf("Got unexpected error running `pr merge` %s", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "✓ Deleted local branch \n✓ Deleted remote branch \n", output.Stderr()) } func TestPrMerge_alreadyMerged_withMergeStrategy_crossRepo(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "4", &api.PullRequest{ ID: "THE-ID", Number: 4, State: "MERGED", HeadRepositoryOwner: api.Owner{Login: "monalisa"}, MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") pm := &prompter.PrompterMock{ ConfirmFunc: func(p string, d bool) (bool, error) { if p == "Pull request OWNER/REPO#4 was already merged. Delete the branch locally?" { return d, nil } else { return false, prompter.NoSuchPromptErr(p) } }, } output, err := runCommand(http, pm, "blueberries", true, "pr merge 4 --merge") if err != nil { t.Fatalf("Got unexpected error running `pr merge` %s", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) } func TestPRMergeTTY(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "", &api.PullRequest{ ID: "THE-ID", Number: 3, Title: "It was the best of times", HeadRefName: "blueberries", MergeStateStatus: "CLEAN", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "mergeCommitAllowed": true, "rebaseMergeAllowed": true, "squashMergeAllowed": true } } }`)) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") pm := &prompter.PrompterMock{ ConfirmFunc: func(p string, d bool) (bool, error) { if p == "Delete the branch locally and on GitHub?" { return d, nil } else { return false, prompter.NoSuchPromptErr(p) } }, SelectFunc: func(p, d string, opts []string) (int, error) { switch p { case "What's next?": return prompter.IndexFor(opts, "Submit") case "What merge method would you like to use?": return 0, nil default: return -1, prompter.NoSuchPromptErr(p) } }, } output, err := runCommand(http, pm, "blueberries", true, "") if err != nil { t.Fatalf("Got unexpected error running `pr merge` %s", err) } assert.Equal(t, "Merging pull request OWNER/REPO#3 (It was the best of times)\n✓ Merged pull request OWNER/REPO#3 (It was the best of times)\n", output.Stderr()) } func TestPRMergeTTY_withDeleteBranch(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "", &api.PullRequest{ ID: "THE-ID", Number: 3, Title: "It was the best of times", HeadRefName: "blueberries", MergeStateStatus: "CLEAN", BaseRefName: "main", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "mergeCommitAllowed": true, "rebaseMergeAllowed": true, "squashMergeAllowed": true, "mergeQueue": { "mergeMethod": "" } } } }`)) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) http.Register( httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), httpmock.StringResponse(`{}`)) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/main`, 0, "") cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") cs.Register(`git pull --ff-only`, 0, "") pm := &prompter.PrompterMock{ SelectFunc: func(p, d string, opts []string) (int, error) { switch p { case "What's next?": return prompter.IndexFor(opts, "Submit") case "What merge method would you like to use?": return 0, nil default: return -1, prompter.NoSuchPromptErr(p) } }, } output, err := runCommand(http, pm, "blueberries", true, "-d") if err != nil { t.Fatalf("Got unexpected error running `pr merge` %s", err) } assert.Equal(t, "", output.String()) assert.Equal(t, heredoc.Doc(` Merging pull request OWNER/REPO#3 (It was the best of times) ✓ Merged pull request OWNER/REPO#3 (It was the best of times) ✓ Deleted local branch blueberries and switched to branch main ✓ Deleted remote branch blueberries `), output.Stderr()) } func TestPRMergeTTY_squashEditCommitMsgAndSubject(t *testing.T) { ios, _, stdout, stderr := iostreams.Test() ios.SetStdinTTY(true) ios.SetStdoutTTY(true) ios.SetStderrTTY(true) tr := initFakeHTTP() defer tr.Verify(t) tr.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "mergeCommitAllowed": true, "rebaseMergeAllowed": true, "squashMergeAllowed": true } } }`)) tr.Register( httpmock.GraphQL(`query PullRequestMergeText\b`), httpmock.StringResponse(` { "data": { "node": { "viewerMergeHeadlineText": "default headline text", "viewerMergeBodyText": "default body text" } } }`)) tr.Register( httpmock.GraphQL(`query PullRequestMergeText\b`), httpmock.StringResponse(` { "data": { "node": { "viewerMergeHeadlineText": "default headline text", "viewerMergeBodyText": "default body text" } } }`)) tr.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "SQUASH", input["mergeMethod"].(string)) assert.Equal(t, "DEFAULT HEADLINE TEXT", input["commitHeadline"].(string)) assert.Equal(t, "DEFAULT BODY TEXT", input["commitBody"].(string)) })) _, cmdTeardown := run.Stub() defer cmdTeardown(t) selectCount := -1 answers := []string{"Edit commit message", "Edit commit subject", "Submit"} pm := &prompter.PrompterMock{ ConfirmFunc: func(p string, d bool) (bool, error) { if p == "Delete the branch on GitHub?" { return d, nil } else { return false, prompter.NoSuchPromptErr(p) } }, SelectFunc: func(p, d string, opts []string) (int, error) { switch p { case "What's next?": selectCount++ return prompter.IndexFor(opts, answers[selectCount]) case "What merge method would you like to use?": return prompter.IndexFor(opts, "Squash and merge") default: return -1, prompter.NoSuchPromptErr(p) } }, } err := mergeRun(&MergeOptions{ IO: ios, Editor: testEditor{}, HttpClient: func() (*http.Client, error) { return &http.Client{Transport: tr}, nil }, Prompter: pm, SelectorArg: "https://github.com/OWNER/REPO/pull/123", MergeStrategyEmpty: true, Finder: shared.NewMockFinder( "https://github.com/OWNER/REPO/pull/123", &api.PullRequest{ID: "THE-ID", Number: 123, Title: "title", MergeStateStatus: "CLEAN"}, ghrepo.New("OWNER", "REPO"), ), }) assert.NoError(t, err) assert.Equal(t, "", stdout.String()) assert.Equal(t, "Merging pull request OWNER/REPO#123 (title)\n✓ Squashed and merged pull request OWNER/REPO#123 (title)\n", stderr.String()) } func TestPRMergeEmptyStrategyNonTTY(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", BaseRefName: "main", }, baseRepo("OWNER", "REPO", "main"), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "blueberries", false, "pr merge 1") assert.EqualError(t, err, "--merge, --rebase, or --squash required when not running interactively") assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) } func TestPRTTY_cancelled(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "", &api.PullRequest{ID: "THE-ID", Number: 123, Title: "title", MergeStateStatus: "CLEAN"}, ghrepo.New("OWNER", "REPO"), ) http.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "mergeCommitAllowed": true, "rebaseMergeAllowed": true, "squashMergeAllowed": true } } }`)) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") pm := &prompter.PrompterMock{ ConfirmFunc: func(p string, d bool) (bool, error) { if p == "Delete the branch locally and on GitHub?" { return d, nil } else { return false, prompter.NoSuchPromptErr(p) } }, SelectFunc: func(p, d string, opts []string) (int, error) { switch p { case "What's next?": return prompter.IndexFor(opts, "Cancel") case "What merge method would you like to use?": return 0, nil default: return -1, prompter.NoSuchPromptErr(p) } }, } output, err := runCommand(http, pm, "blueberries", true, "") if !errors.Is(err, cmdutil.CancelError) { t.Fatalf("got error %v", err) } assert.Equal(t, "Merging pull request OWNER/REPO#123 (title)\nCancelled.\n", output.Stderr()) } func Test_mergeMethodSurvey(t *testing.T) { repo := &api.Repository{ MergeCommitAllowed: false, RebaseMergeAllowed: true, SquashMergeAllowed: true, } pm := &prompter.PrompterMock{ SelectFunc: func(p, d string, opts []string) (int, error) { if p == "What merge method would you like to use?" { return prompter.IndexFor(opts, "Rebase and merge") } else { return -1, prompter.NoSuchPromptErr(p) } }, } method, err := mergeMethodSurvey(pm, repo) assert.Nil(t, err) assert.Equal(t, PullRequestMergeMethodRebase, method) } func TestMergeRun_autoMerge(t *testing.T) { ios, _, stdout, stderr := iostreams.Test() ios.SetStdoutTTY(true) ios.SetStderrTTY(true) tr := initFakeHTTP() defer tr.Verify(t) tr.Register( httpmock.GraphQL(`mutation PullRequestAutoMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "SQUASH", input["mergeMethod"].(string)) })) _, cmdTeardown := run.Stub() defer cmdTeardown(t) err := mergeRun(&MergeOptions{ IO: ios, HttpClient: func() (*http.Client, error) { return &http.Client{Transport: tr}, nil }, SelectorArg: "https://github.com/OWNER/REPO/pull/123", AutoMergeEnable: true, MergeMethod: PullRequestMergeMethodSquash, Finder: shared.NewMockFinder( "https://github.com/OWNER/REPO/pull/123", &api.PullRequest{ID: "THE-ID", Number: 123, MergeStateStatus: "BLOCKED"}, ghrepo.New("OWNER", "REPO"), ), }) assert.NoError(t, err) assert.Equal(t, "", stdout.String()) assert.Equal(t, "✓ Pull request OWNER/REPO#123 will be automatically merged via squash when all requirements are met\n", stderr.String()) } func TestMergeRun_autoMerge_directMerge(t *testing.T) { ios, _, stdout, stderr := iostreams.Test() ios.SetStdoutTTY(true) ios.SetStderrTTY(true) tr := initFakeHTTP() defer tr.Verify(t) tr.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") })) _, cmdTeardown := run.Stub() defer cmdTeardown(t) err := mergeRun(&MergeOptions{ IO: ios, HttpClient: func() (*http.Client, error) { return &http.Client{Transport: tr}, nil }, SelectorArg: "https://github.com/OWNER/REPO/pull/123", AutoMergeEnable: true, MergeMethod: PullRequestMergeMethodMerge, Finder: shared.NewMockFinder( "https://github.com/OWNER/REPO/pull/123", &api.PullRequest{ID: "THE-ID", Number: 123, MergeStateStatus: "CLEAN"}, ghrepo.New("OWNER", "REPO"), ), }) assert.NoError(t, err) assert.Equal(t, "", stdout.String()) assert.Equal(t, "✓ Merged pull request OWNER/REPO#123 ()\n", stderr.String()) } func TestMergeRun_disableAutoMerge(t *testing.T) { ios, _, stdout, stderr := iostreams.Test() ios.SetStdoutTTY(true) ios.SetStderrTTY(true) tr := initFakeHTTP() defer tr.Verify(t) tr.Register( httpmock.GraphQL(`mutation PullRequestAutoMergeDisable\b`), httpmock.GraphQLQuery(`{}`, func(s string, m map[string]interface{}) { assert.Equal(t, map[string]interface{}{"prID": "THE-ID"}, m) })) _, cmdTeardown := run.Stub() defer cmdTeardown(t) err := mergeRun(&MergeOptions{ IO: ios, HttpClient: func() (*http.Client, error) { return &http.Client{Transport: tr}, nil }, SelectorArg: "https://github.com/OWNER/REPO/pull/123", AutoMergeDisable: true, Finder: shared.NewMockFinder( "https://github.com/OWNER/REPO/pull/123", &api.PullRequest{ID: "THE-ID", Number: 123}, ghrepo.New("OWNER", "REPO"), ), }) assert.NoError(t, err) assert.Equal(t, "", stdout.String()) assert.Equal(t, "✓ Auto-merge disabled for pull request OWNER/REPO#123\n", stderr.String()) } func TestPrInMergeQueue(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", IsInMergeQueue: true, IsMergeQueueEnabled: true, }, baseRepo("OWNER", "REPO", "main"), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "blueberries", true, "pr merge 1") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "! Pull request OWNER/REPO#1 is already queued to merge\n", output.Stderr()) } func TestPrAddToMergeQueueWithMergeMethod(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", IsInMergeQueue: false, IsMergeQueueEnabled: true, BaseRefName: "main", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestAutoMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) }), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "blueberries", true, "pr merge 1 --merge") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "! The merge strategy for main is set by the merge queue\n✓ Pull request OWNER/REPO#1 will be added to the merge queue for main when ready\n", output.Stderr()) } func TestPrAddToMergeQueueClean(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", IsInMergeQueue: false, IsMergeQueueEnabled: true, BaseRefName: "main", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestAutoMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) }), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "blueberries", true, "pr merge 1") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "✓ Pull request OWNER/REPO#1 will be added to the merge queue for main when ready\n", output.Stderr()) } func TestPrAddToMergeQueueBlocked(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "BLOCKED", IsInMergeQueue: false, IsMergeQueueEnabled: true, BaseRefName: "main", }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestAutoMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) }), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "blueberries", true, "pr merge 1") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "✓ Pull request OWNER/REPO#1 will be added to the merge queue for main when ready\n", output.Stderr()) } func TestPrAddToMergeQueueAdmin(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", IsInMergeQueue: false, IsMergeQueueEnabled: true, }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "mergeCommitAllowed": true, "rebaseMergeAllowed": true, "squashMergeAllowed": true } } }`)) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") }), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") pm := &prompter.PrompterMock{ ConfirmFunc: func(p string, d bool) (bool, error) { if p == "Delete the branch locally and on GitHub?" { return d, nil } else { return false, prompter.NoSuchPromptErr(p) } }, SelectFunc: func(p, d string, opts []string) (int, error) { switch p { case "What's next?": return 0, nil case "What merge method would you like to use?": return 0, nil default: return -1, prompter.NoSuchPromptErr(p) } }, } output, err := runCommand(http, pm, "blueberries", true, "pr merge 1 --admin") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "Merging pull request OWNER/REPO#1 (The title of the PR)\n✓ Merged pull request OWNER/REPO#1 (The title of the PR)\n", output.Stderr()) } func TestPrAddToMergeQueueAdminWithMergeStrategy(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) shared.RunCommandFinder( "1", &api.PullRequest{ ID: "THE-ID", Number: 1, State: "OPEN", Title: "The title of the PR", MergeStateStatus: "CLEAN", IsInMergeQueue: false, }, baseRepo("OWNER", "REPO", "main"), ) http.Register( httpmock.GraphQL(`mutation PullRequestMerge\b`), httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { assert.Equal(t, "THE-ID", input["pullRequestId"].(string)) assert.Equal(t, "MERGE", input["mergeMethod"].(string)) assert.NotContains(t, input, "commitHeadline") }), ) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git rev-parse --verify refs/heads/`, 0, "") output, err := runCommand(http, nil, "blueberries", true, "pr merge 1 --admin --merge") if err != nil { t.Fatalf("error running command `pr merge`: %v", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "✓ Merged pull request OWNER/REPO#1 (The title of the PR)\n", output.Stderr()) } type testEditor struct{} func (e testEditor) Edit(filename, text string) (string, error) { return strings.ToUpper(text), nil }