github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/pr/merge/merge_test.go (about)

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