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

     1  package create
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"path/filepath"
    10  	"testing"
    11  
    12  	"github.com/MakeNowJust/heredoc"
    13  	"github.com/cli/cli/api"
    14  	"github.com/cli/cli/context"
    15  	"github.com/cli/cli/git"
    16  	"github.com/cli/cli/internal/config"
    17  	"github.com/cli/cli/internal/ghrepo"
    18  	"github.com/cli/cli/internal/run"
    19  	"github.com/cli/cli/pkg/cmd/pr/shared"
    20  	prShared "github.com/cli/cli/pkg/cmd/pr/shared"
    21  	"github.com/cli/cli/pkg/cmdutil"
    22  	"github.com/cli/cli/pkg/httpmock"
    23  	"github.com/cli/cli/pkg/iostreams"
    24  	"github.com/cli/cli/pkg/prompt"
    25  	"github.com/cli/cli/test"
    26  	"github.com/google/shlex"
    27  	"github.com/stretchr/testify/assert"
    28  	"github.com/stretchr/testify/require"
    29  )
    30  
    31  func TestNewCmdCreate(t *testing.T) {
    32  	tmpFile := filepath.Join(t.TempDir(), "my-body.md")
    33  	err := ioutil.WriteFile(tmpFile, []byte("a body from file"), 0600)
    34  	require.NoError(t, err)
    35  
    36  	tests := []struct {
    37  		name      string
    38  		tty       bool
    39  		stdin     string
    40  		cli       string
    41  		wantsErr  bool
    42  		wantsOpts CreateOptions
    43  	}{
    44  		{
    45  			name:     "empty non-tty",
    46  			tty:      false,
    47  			cli:      "",
    48  			wantsErr: true,
    49  		},
    50  		{
    51  			name:     "empty tty",
    52  			tty:      true,
    53  			cli:      "",
    54  			wantsErr: false,
    55  			wantsOpts: CreateOptions{
    56  				Title:               "",
    57  				TitleProvided:       false,
    58  				Body:                "",
    59  				BodyProvided:        false,
    60  				Autofill:            false,
    61  				RecoverFile:         "",
    62  				WebMode:             false,
    63  				IsDraft:             false,
    64  				BaseBranch:          "",
    65  				HeadBranch:          "",
    66  				MaintainerCanModify: true,
    67  			},
    68  		},
    69  		{
    70  			name:     "body from stdin",
    71  			tty:      false,
    72  			stdin:    "this is on standard input",
    73  			cli:      "-t mytitle -F -",
    74  			wantsErr: false,
    75  			wantsOpts: CreateOptions{
    76  				Title:               "mytitle",
    77  				TitleProvided:       true,
    78  				Body:                "this is on standard input",
    79  				BodyProvided:        true,
    80  				Autofill:            false,
    81  				RecoverFile:         "",
    82  				WebMode:             false,
    83  				IsDraft:             false,
    84  				BaseBranch:          "",
    85  				HeadBranch:          "",
    86  				MaintainerCanModify: true,
    87  			},
    88  		},
    89  		{
    90  			name:     "body from file",
    91  			tty:      false,
    92  			cli:      fmt.Sprintf("-t mytitle -F '%s'", tmpFile),
    93  			wantsErr: false,
    94  			wantsOpts: CreateOptions{
    95  				Title:               "mytitle",
    96  				TitleProvided:       true,
    97  				Body:                "a body from file",
    98  				BodyProvided:        true,
    99  				Autofill:            false,
   100  				RecoverFile:         "",
   101  				WebMode:             false,
   102  				IsDraft:             false,
   103  				BaseBranch:          "",
   104  				HeadBranch:          "",
   105  				MaintainerCanModify: true,
   106  			},
   107  		},
   108  	}
   109  	for _, tt := range tests {
   110  		t.Run(tt.name, func(t *testing.T) {
   111  			io, stdin, stdout, stderr := iostreams.Test()
   112  			if tt.stdin != "" {
   113  				_, _ = stdin.WriteString(tt.stdin)
   114  			} else if tt.tty {
   115  				io.SetStdinTTY(true)
   116  				io.SetStdoutTTY(true)
   117  			}
   118  
   119  			f := &cmdutil.Factory{
   120  				IOStreams: io,
   121  			}
   122  
   123  			var opts *CreateOptions
   124  			cmd := NewCmdCreate(f, func(o *CreateOptions) error {
   125  				opts = o
   126  				return nil
   127  			})
   128  
   129  			args, err := shlex.Split(tt.cli)
   130  			require.NoError(t, err)
   131  			cmd.SetArgs(args)
   132  			_, err = cmd.ExecuteC()
   133  			if tt.wantsErr {
   134  				assert.Error(t, err)
   135  				return
   136  			} else {
   137  				require.NoError(t, err)
   138  			}
   139  
   140  			assert.Equal(t, "", stdout.String())
   141  			assert.Equal(t, "", stderr.String())
   142  
   143  			assert.Equal(t, tt.wantsOpts.Body, opts.Body)
   144  			assert.Equal(t, tt.wantsOpts.BodyProvided, opts.BodyProvided)
   145  			assert.Equal(t, tt.wantsOpts.Title, opts.Title)
   146  			assert.Equal(t, tt.wantsOpts.TitleProvided, opts.TitleProvided)
   147  			assert.Equal(t, tt.wantsOpts.Autofill, opts.Autofill)
   148  			assert.Equal(t, tt.wantsOpts.WebMode, opts.WebMode)
   149  			assert.Equal(t, tt.wantsOpts.RecoverFile, opts.RecoverFile)
   150  			assert.Equal(t, tt.wantsOpts.IsDraft, opts.IsDraft)
   151  			assert.Equal(t, tt.wantsOpts.MaintainerCanModify, opts.MaintainerCanModify)
   152  			assert.Equal(t, tt.wantsOpts.BaseBranch, opts.BaseBranch)
   153  			assert.Equal(t, tt.wantsOpts.HeadBranch, opts.HeadBranch)
   154  		})
   155  	}
   156  }
   157  
   158  func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, isTTY bool, cli string) (*test.CmdOut, error) {
   159  	return runCommandWithRootDirOverridden(rt, remotes, branch, isTTY, cli, "")
   160  }
   161  
   162  func runCommandWithRootDirOverridden(rt http.RoundTripper, remotes context.Remotes, branch string, isTTY bool, cli string, rootDir string) (*test.CmdOut, error) {
   163  	io, _, stdout, stderr := iostreams.Test()
   164  	io.SetStdoutTTY(isTTY)
   165  	io.SetStdinTTY(isTTY)
   166  	io.SetStderrTTY(isTTY)
   167  
   168  	browser := &cmdutil.TestBrowser{}
   169  	factory := &cmdutil.Factory{
   170  		IOStreams: io,
   171  		Browser:   browser,
   172  		HttpClient: func() (*http.Client, error) {
   173  			return &http.Client{Transport: rt}, nil
   174  		},
   175  		Config: func() (config.Config, error) {
   176  			return config.NewBlankConfig(), nil
   177  		},
   178  		Remotes: func() (context.Remotes, error) {
   179  			if remotes != nil {
   180  				return remotes, nil
   181  			}
   182  			return context.Remotes{
   183  				{
   184  					Remote: &git.Remote{
   185  						Name:     "origin",
   186  						Resolved: "base",
   187  					},
   188  					Repo: ghrepo.New("OWNER", "REPO"),
   189  				},
   190  			}, nil
   191  		},
   192  		Branch: func() (string, error) {
   193  			return branch, nil
   194  		},
   195  	}
   196  
   197  	cmd := NewCmdCreate(factory, func(opts *CreateOptions) error {
   198  		opts.RootDirOverride = rootDir
   199  		return createRun(opts)
   200  	})
   201  	cmd.PersistentFlags().StringP("repo", "R", "", "")
   202  
   203  	argv, err := shlex.Split(cli)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	cmd.SetArgs(argv)
   208  
   209  	cmd.SetIn(&bytes.Buffer{})
   210  	cmd.SetOut(ioutil.Discard)
   211  	cmd.SetErr(ioutil.Discard)
   212  
   213  	_, err = cmd.ExecuteC()
   214  	return &test.CmdOut{
   215  		OutBuf:     stdout,
   216  		ErrBuf:     stderr,
   217  		BrowsedURL: browser.BrowsedURL(),
   218  	}, err
   219  }
   220  
   221  func initFakeHTTP() *httpmock.Registry {
   222  	return &httpmock.Registry{}
   223  }
   224  
   225  func TestPRCreate_nontty_web(t *testing.T) {
   226  	http := initFakeHTTP()
   227  	defer http.Verify(t)
   228  
   229  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   230  
   231  	cs, cmdTeardown := run.Stub()
   232  	defer cmdTeardown(t)
   233  
   234  	cs.Register(`git status --porcelain`, 0, "")
   235  	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
   236  
   237  	output, err := runCommand(http, nil, "feature", false, `--web --head=feature`)
   238  	require.NoError(t, err)
   239  
   240  	assert.Equal(t, "", output.String())
   241  	assert.Equal(t, "", output.Stderr())
   242  	assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", output.BrowsedURL)
   243  }
   244  
   245  func TestPRCreate_recover(t *testing.T) {
   246  	http := initFakeHTTP()
   247  	defer http.Verify(t)
   248  
   249  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   250  	shared.RunCommandFinder("feature", nil, nil)
   251  	http.Register(
   252  		httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
   253  		httpmock.StringResponse(`
   254  		{ "data": {
   255  			"u000": { "login": "jillValentine", "id": "JILLID" },
   256  			"repository": {},
   257  			"organization": {}
   258  		} }
   259  		`))
   260  	http.Register(
   261  		httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`),
   262  		httpmock.GraphQLMutation(`
   263  		{ "data": { "requestReviews": {
   264  			"clientMutationId": ""
   265  		} } }
   266  	`, func(inputs map[string]interface{}) {
   267  			assert.Equal(t, []interface{}{"JILLID"}, inputs["userIds"])
   268  		}))
   269  	http.Register(
   270  		httpmock.GraphQL(`mutation PullRequestCreate\b`),
   271  		httpmock.GraphQLMutation(`
   272  		{ "data": { "createPullRequest": { "pullRequest": {
   273  			"URL": "https://github.com/OWNER/REPO/pull/12"
   274  		} } } }
   275  		`, func(input map[string]interface{}) {
   276  			assert.Equal(t, "recovered title", input["title"].(string))
   277  			assert.Equal(t, "recovered body", input["body"].(string))
   278  		}))
   279  
   280  	cs, cmdTeardown := run.Stub()
   281  	defer cmdTeardown(t)
   282  
   283  	cs.Register(`git status --porcelain`, 0, "")
   284  	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
   285  
   286  	as, teardown := prompt.InitAskStubber()
   287  	defer teardown()
   288  	as.Stub([]*prompt.QuestionStub{
   289  		{
   290  			Name:    "Title",
   291  			Default: true,
   292  		},
   293  	})
   294  	as.Stub([]*prompt.QuestionStub{
   295  		{
   296  			Name:    "Body",
   297  			Default: true,
   298  		},
   299  	})
   300  	as.Stub([]*prompt.QuestionStub{
   301  		{
   302  			Name:  "confirmation",
   303  			Value: 0,
   304  		},
   305  	})
   306  
   307  	tmpfile, err := ioutil.TempFile(t.TempDir(), "testrecover*")
   308  	assert.NoError(t, err)
   309  	defer tmpfile.Close()
   310  
   311  	state := prShared.IssueMetadataState{
   312  		Title:     "recovered title",
   313  		Body:      "recovered body",
   314  		Reviewers: []string{"jillValentine"},
   315  	}
   316  
   317  	data, err := json.Marshal(state)
   318  	assert.NoError(t, err)
   319  
   320  	_, err = tmpfile.Write(data)
   321  	assert.NoError(t, err)
   322  
   323  	args := fmt.Sprintf("--recover '%s' -Hfeature", tmpfile.Name())
   324  
   325  	output, err := runCommandWithRootDirOverridden(http, nil, "feature", true, args, "")
   326  	assert.NoError(t, err)
   327  
   328  	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
   329  }
   330  
   331  func TestPRCreate_nontty(t *testing.T) {
   332  	http := initFakeHTTP()
   333  	defer http.Verify(t)
   334  
   335  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   336  	shared.RunCommandFinder("feature", nil, nil)
   337  	http.Register(
   338  		httpmock.GraphQL(`mutation PullRequestCreate\b`),
   339  		httpmock.GraphQLMutation(`
   340  			{ "data": { "createPullRequest": { "pullRequest": {
   341  				"URL": "https://github.com/OWNER/REPO/pull/12"
   342  			} } } }`,
   343  			func(input map[string]interface{}) {
   344  				assert.Equal(t, "REPOID", input["repositoryId"])
   345  				assert.Equal(t, "my title", input["title"])
   346  				assert.Equal(t, "my body", input["body"])
   347  				assert.Equal(t, "master", input["baseRefName"])
   348  				assert.Equal(t, "feature", input["headRefName"])
   349  			}),
   350  	)
   351  
   352  	cs, cmdTeardown := run.Stub()
   353  	defer cmdTeardown(t)
   354  
   355  	cs.Register(`git status --porcelain`, 0, "")
   356  
   357  	output, err := runCommand(http, nil, "feature", false, `-t "my title" -b "my body" -H feature`)
   358  	require.NoError(t, err)
   359  
   360  	assert.Equal(t, "", output.Stderr())
   361  	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
   362  }
   363  
   364  func TestPRCreate(t *testing.T) {
   365  	http := initFakeHTTP()
   366  	defer http.Verify(t)
   367  
   368  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   369  	http.StubRepoResponse("OWNER", "REPO")
   370  	http.Register(
   371  		httpmock.GraphQL(`query UserCurrent\b`),
   372  		httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
   373  	shared.RunCommandFinder("feature", nil, nil)
   374  	http.Register(
   375  		httpmock.GraphQL(`mutation PullRequestCreate\b`),
   376  		httpmock.GraphQLMutation(`
   377  		{ "data": { "createPullRequest": { "pullRequest": {
   378  			"URL": "https://github.com/OWNER/REPO/pull/12"
   379  		} } } }
   380  		`, func(input map[string]interface{}) {
   381  			assert.Equal(t, "REPOID", input["repositoryId"].(string))
   382  			assert.Equal(t, "my title", input["title"].(string))
   383  			assert.Equal(t, "my body", input["body"].(string))
   384  			assert.Equal(t, "master", input["baseRefName"].(string))
   385  			assert.Equal(t, "feature", input["headRefName"].(string))
   386  		}))
   387  
   388  	cs, cmdTeardown := run.Stub()
   389  	defer cmdTeardown(t)
   390  
   391  	cs.Register(`git status --porcelain`, 0, "")
   392  	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
   393  	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
   394  	cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "")
   395  
   396  	ask, cleanupAsk := prompt.InitAskStubber()
   397  	defer cleanupAsk()
   398  	ask.StubOne(0)
   399  
   400  	output, err := runCommand(http, nil, "feature", true, `-t "my title" -b "my body"`)
   401  	require.NoError(t, err)
   402  
   403  	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
   404  	assert.Equal(t, "\nCreating pull request for feature into master in OWNER/REPO\n\n", output.Stderr())
   405  }
   406  
   407  func TestPRCreate_NoMaintainerModify(t *testing.T) {
   408  	// TODO update this copypasta
   409  	http := initFakeHTTP()
   410  	defer http.Verify(t)
   411  
   412  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   413  	http.StubRepoResponse("OWNER", "REPO")
   414  	http.Register(
   415  		httpmock.GraphQL(`query UserCurrent\b`),
   416  		httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
   417  	shared.RunCommandFinder("feature", nil, nil)
   418  	http.Register(
   419  		httpmock.GraphQL(`mutation PullRequestCreate\b`),
   420  		httpmock.GraphQLMutation(`
   421  		{ "data": { "createPullRequest": { "pullRequest": {
   422  			"URL": "https://github.com/OWNER/REPO/pull/12"
   423  		} } } }
   424  		`, func(input map[string]interface{}) {
   425  			assert.Equal(t, false, input["maintainerCanModify"].(bool))
   426  			assert.Equal(t, "REPOID", input["repositoryId"].(string))
   427  			assert.Equal(t, "my title", input["title"].(string))
   428  			assert.Equal(t, "my body", input["body"].(string))
   429  			assert.Equal(t, "master", input["baseRefName"].(string))
   430  			assert.Equal(t, "feature", input["headRefName"].(string))
   431  		}))
   432  
   433  	cs, cmdTeardown := run.Stub()
   434  	defer cmdTeardown(t)
   435  
   436  	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
   437  	cs.Register(`git status --porcelain`, 0, "")
   438  	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
   439  	cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "")
   440  
   441  	ask, cleanupAsk := prompt.InitAskStubber()
   442  	defer cleanupAsk()
   443  	ask.StubOne(0)
   444  
   445  	output, err := runCommand(http, nil, "feature", true, `-t "my title" -b "my body" --no-maintainer-edit`)
   446  	require.NoError(t, err)
   447  
   448  	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
   449  	assert.Equal(t, "\nCreating pull request for feature into master in OWNER/REPO\n\n", output.Stderr())
   450  }
   451  
   452  func TestPRCreate_createFork(t *testing.T) {
   453  	http := initFakeHTTP()
   454  	defer http.Verify(t)
   455  
   456  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   457  	http.StubRepoResponse("OWNER", "REPO")
   458  	http.Register(
   459  		httpmock.GraphQL(`query UserCurrent\b`),
   460  		httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`))
   461  	shared.RunCommandFinder("feature", nil, nil)
   462  	http.Register(
   463  		httpmock.REST("POST", "repos/OWNER/REPO/forks"),
   464  		httpmock.StatusStringResponse(201, `
   465  		{ "node_id": "NODEID",
   466  		  "name": "REPO",
   467  		  "owner": {"login": "monalisa"}
   468  		}
   469  		`))
   470  	http.Register(
   471  		httpmock.GraphQL(`mutation PullRequestCreate\b`),
   472  		httpmock.GraphQLMutation(`
   473  		{ "data": { "createPullRequest": { "pullRequest": {
   474  			"URL": "https://github.com/OWNER/REPO/pull/12"
   475  		} } } }
   476  		`, func(input map[string]interface{}) {
   477  			assert.Equal(t, "REPOID", input["repositoryId"].(string))
   478  			assert.Equal(t, "master", input["baseRefName"].(string))
   479  			assert.Equal(t, "monalisa:feature", input["headRefName"].(string))
   480  		}))
   481  
   482  	cs, cmdTeardown := run.Stub()
   483  	defer cmdTeardown(t)
   484  
   485  	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
   486  	cs.Register(`git status --porcelain`, 0, "")
   487  	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
   488  	cs.Register(`git remote add -f fork https://github.com/monalisa/REPO.git`, 0, "")
   489  	cs.Register(`git push --set-upstream fork HEAD:feature`, 0, "")
   490  
   491  	ask, cleanupAsk := prompt.InitAskStubber()
   492  	defer cleanupAsk()
   493  	ask.StubOne(1)
   494  
   495  	output, err := runCommand(http, nil, "feature", true, `-t title -b body`)
   496  	require.NoError(t, err)
   497  
   498  	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
   499  }
   500  
   501  func TestPRCreate_pushedToNonBaseRepo(t *testing.T) {
   502  	remotes := context.Remotes{
   503  		{
   504  			Remote: &git.Remote{
   505  				Name:     "upstream",
   506  				Resolved: "base",
   507  			},
   508  			Repo: ghrepo.New("OWNER", "REPO"),
   509  		},
   510  		{
   511  			Remote: &git.Remote{
   512  				Name:     "origin",
   513  				Resolved: "base",
   514  			},
   515  			Repo: ghrepo.New("monalisa", "REPO"),
   516  		},
   517  	}
   518  
   519  	http := initFakeHTTP()
   520  	defer http.Verify(t)
   521  
   522  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   523  	shared.RunCommandFinder("feature", nil, nil)
   524  	http.Register(
   525  		httpmock.GraphQL(`mutation PullRequestCreate\b`),
   526  		httpmock.GraphQLMutation(`
   527  		{ "data": { "createPullRequest": { "pullRequest": {
   528  			"URL": "https://github.com/OWNER/REPO/pull/12"
   529  		} } } }
   530  		`, func(input map[string]interface{}) {
   531  			assert.Equal(t, "REPOID", input["repositoryId"].(string))
   532  			assert.Equal(t, "master", input["baseRefName"].(string))
   533  			assert.Equal(t, "monalisa:feature", input["headRefName"].(string))
   534  		}))
   535  
   536  	cs, cmdTeardown := run.Stub()
   537  	defer cmdTeardown(t)
   538  
   539  	cs.Register("git status", 0, "")
   540  	cs.Register(`git config --get-regexp \^branch\\\.feature\\\.`, 1, "") // determineTrackingBranch
   541  	cs.Register("git show-ref --verify", 0, heredoc.Doc(`
   542  		deadbeef HEAD
   543  		deadb00f refs/remotes/upstream/feature
   544  		deadbeef refs/remotes/origin/feature
   545  	`)) // determineTrackingBranch
   546  
   547  	_, cleanupAsk := prompt.InitAskStubber()
   548  	defer cleanupAsk()
   549  
   550  	output, err := runCommand(http, remotes, "feature", true, `-t title -b body`)
   551  	require.NoError(t, err)
   552  
   553  	assert.Equal(t, "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\n", output.Stderr())
   554  	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
   555  }
   556  
   557  func TestPRCreate_pushedToDifferentBranchName(t *testing.T) {
   558  	http := initFakeHTTP()
   559  	defer http.Verify(t)
   560  
   561  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   562  	shared.RunCommandFinder("feature", nil, nil)
   563  	http.Register(
   564  		httpmock.GraphQL(`mutation PullRequestCreate\b`),
   565  		httpmock.GraphQLMutation(`
   566  		{ "data": { "createPullRequest": { "pullRequest": {
   567  			"URL": "https://github.com/OWNER/REPO/pull/12"
   568  		} } } }
   569  		`, func(input map[string]interface{}) {
   570  			assert.Equal(t, "REPOID", input["repositoryId"].(string))
   571  			assert.Equal(t, "master", input["baseRefName"].(string))
   572  			assert.Equal(t, "my-feat2", input["headRefName"].(string))
   573  		}))
   574  
   575  	cs, cmdTeardown := run.Stub()
   576  	defer cmdTeardown(t)
   577  
   578  	cs.Register("git status", 0, "")
   579  	cs.Register(`git config --get-regexp \^branch\\\.feature\\\.`, 0, heredoc.Doc(`
   580  		branch.feature.remote origin
   581  		branch.feature.merge refs/heads/my-feat2
   582  	`)) // determineTrackingBranch
   583  	cs.Register("git show-ref --verify", 0, heredoc.Doc(`
   584  		deadbeef HEAD
   585  		deadbeef refs/remotes/origin/my-feat2
   586  	`)) // determineTrackingBranch
   587  
   588  	_, cleanupAsk := prompt.InitAskStubber()
   589  	defer cleanupAsk()
   590  
   591  	output, err := runCommand(http, nil, "feature", true, `-t title -b body`)
   592  	require.NoError(t, err)
   593  
   594  	assert.Equal(t, "\nCreating pull request for my-feat2 into master in OWNER/REPO\n\n", output.Stderr())
   595  	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
   596  }
   597  
   598  func TestPRCreate_nonLegacyTemplate(t *testing.T) {
   599  	http := initFakeHTTP()
   600  	defer http.Verify(t)
   601  
   602  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   603  	shared.RunCommandFinder("feature", nil, nil)
   604  	http.Register(
   605  		httpmock.GraphQL(`mutation PullRequestCreate\b`),
   606  		httpmock.GraphQLMutation(`
   607  		{ "data": { "createPullRequest": { "pullRequest": {
   608  			"URL": "https://github.com/OWNER/REPO/pull/12"
   609  		} } } }
   610  		`, func(input map[string]interface{}) {
   611  			assert.Equal(t, "my title", input["title"].(string))
   612  			assert.Equal(t, "- commit 1\n- commit 0\n\nFixes a bug and Closes an issue", input["body"].(string))
   613  		}))
   614  
   615  	cs, cmdTeardown := run.Stub()
   616  	defer cmdTeardown(t)
   617  
   618  	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "1234567890,commit 0\n2345678901,commit 1")
   619  	cs.Register(`git status --porcelain`, 0, "")
   620  
   621  	as, teardown := prompt.InitAskStubber()
   622  	defer teardown()
   623  	as.StubOne(0) // template
   624  	as.Stub([]*prompt.QuestionStub{
   625  		{
   626  			Name:    "Body",
   627  			Default: true,
   628  		},
   629  	}) // body
   630  	as.Stub([]*prompt.QuestionStub{
   631  		{
   632  			Name:  "confirmation",
   633  			Value: 0,
   634  		},
   635  	}) // confirm
   636  
   637  	output, err := runCommandWithRootDirOverridden(http, nil, "feature", true, `-t "my title" -H feature`, "./fixtures/repoWithNonLegacyPRTemplates")
   638  	require.NoError(t, err)
   639  
   640  	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
   641  }
   642  
   643  func TestPRCreate_metadata(t *testing.T) {
   644  	http := initFakeHTTP()
   645  	defer http.Verify(t)
   646  
   647  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   648  	shared.RunCommandFinder("feature", nil, nil)
   649  	http.Register(
   650  		httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
   651  		httpmock.StringResponse(`
   652  		{ "data": {
   653  			"u000": { "login": "MonaLisa", "id": "MONAID" },
   654  			"u001": { "login": "hubot", "id": "HUBOTID" },
   655  			"repository": {
   656  				"l000": { "name": "bug", "id": "BUGID" },
   657  				"l001": { "name": "TODO", "id": "TODOID" }
   658  			},
   659  			"organization": {
   660  				"t000": { "slug": "core", "id": "COREID" },
   661  				"t001": { "slug": "robots", "id": "ROBOTID" }
   662  			}
   663  		} }
   664  		`))
   665  	http.Register(
   666  		httpmock.GraphQL(`query RepositoryMilestoneList\b`),
   667  		httpmock.StringResponse(`
   668  		{ "data": { "repository": { "milestones": {
   669  			"nodes": [
   670  				{ "title": "GA", "id": "GAID" },
   671  				{ "title": "Big One.oh", "id": "BIGONEID" }
   672  			],
   673  			"pageInfo": { "hasNextPage": false }
   674  		} } } }
   675  		`))
   676  	http.Register(
   677  		httpmock.GraphQL(`query RepositoryProjectList\b`),
   678  		httpmock.StringResponse(`
   679  		{ "data": { "repository": { "projects": {
   680  			"nodes": [
   681  				{ "name": "Cleanup", "id": "CLEANUPID" },
   682  				{ "name": "Roadmap", "id": "ROADMAPID" }
   683  			],
   684  			"pageInfo": { "hasNextPage": false }
   685  		} } } }
   686  		`))
   687  	http.Register(
   688  		httpmock.GraphQL(`query OrganizationProjectList\b`),
   689  		httpmock.StringResponse(`
   690  		{ "data": { "organization": { "projects": {
   691  			"nodes": [],
   692  			"pageInfo": { "hasNextPage": false }
   693  		} } } }
   694  		`))
   695  	http.Register(
   696  		httpmock.GraphQL(`mutation PullRequestCreate\b`),
   697  		httpmock.GraphQLMutation(`
   698  		{ "data": { "createPullRequest": { "pullRequest": {
   699  			"id": "NEWPULLID",
   700  			"URL": "https://github.com/OWNER/REPO/pull/12"
   701  		} } } }
   702  	`, func(inputs map[string]interface{}) {
   703  			assert.Equal(t, "TITLE", inputs["title"])
   704  			assert.Equal(t, "BODY", inputs["body"])
   705  			if v, ok := inputs["assigneeIds"]; ok {
   706  				t.Errorf("did not expect assigneeIds: %v", v)
   707  			}
   708  			if v, ok := inputs["userIds"]; ok {
   709  				t.Errorf("did not expect userIds: %v", v)
   710  			}
   711  		}))
   712  	http.Register(
   713  		httpmock.GraphQL(`mutation PullRequestCreateMetadata\b`),
   714  		httpmock.GraphQLMutation(`
   715  		{ "data": { "updatePullRequest": {
   716  			"clientMutationId": ""
   717  		} } }
   718  	`, func(inputs map[string]interface{}) {
   719  			assert.Equal(t, "NEWPULLID", inputs["pullRequestId"])
   720  			assert.Equal(t, []interface{}{"MONAID"}, inputs["assigneeIds"])
   721  			assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
   722  			assert.Equal(t, []interface{}{"ROADMAPID"}, inputs["projectIds"])
   723  			assert.Equal(t, "BIGONEID", inputs["milestoneId"])
   724  		}))
   725  	http.Register(
   726  		httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`),
   727  		httpmock.GraphQLMutation(`
   728  		{ "data": { "requestReviews": {
   729  			"clientMutationId": ""
   730  		} } }
   731  	`, func(inputs map[string]interface{}) {
   732  			assert.Equal(t, "NEWPULLID", inputs["pullRequestId"])
   733  			assert.Equal(t, []interface{}{"HUBOTID", "MONAID"}, inputs["userIds"])
   734  			assert.Equal(t, []interface{}{"COREID", "ROBOTID"}, inputs["teamIds"])
   735  			assert.Equal(t, true, inputs["union"])
   736  		}))
   737  
   738  	output, err := runCommand(http, nil, "feature", true, `-t TITLE -b BODY -H feature -a monalisa -l bug -l todo -p roadmap -m 'big one.oh' -r hubot -r monalisa -r /core -r /robots`)
   739  	assert.NoError(t, err)
   740  
   741  	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
   742  }
   743  
   744  func TestPRCreate_alreadyExists(t *testing.T) {
   745  	http := initFakeHTTP()
   746  	defer http.Verify(t)
   747  
   748  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   749  	shared.RunCommandFinder("feature", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO"))
   750  
   751  	_, err := runCommand(http, nil, "feature", true, `-t title -b body -H feature`)
   752  	assert.EqualError(t, err, "a pull request for branch \"feature\" into branch \"master\" already exists:\nhttps://github.com/OWNER/REPO/pull/123")
   753  }
   754  
   755  func TestPRCreate_web(t *testing.T) {
   756  	http := initFakeHTTP()
   757  	defer http.Verify(t)
   758  
   759  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   760  	http.StubRepoResponse("OWNER", "REPO")
   761  	http.Register(
   762  		httpmock.GraphQL(`query UserCurrent\b`),
   763  		httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
   764  
   765  	cs, cmdTeardown := run.Stub()
   766  	defer cmdTeardown(t)
   767  
   768  	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
   769  	cs.Register(`git status --porcelain`, 0, "")
   770  	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
   771  	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
   772  	cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "")
   773  
   774  	ask, cleanupAsk := prompt.InitAskStubber()
   775  	defer cleanupAsk()
   776  	ask.StubOne(0)
   777  
   778  	output, err := runCommand(http, nil, "feature", true, `--web`)
   779  	require.NoError(t, err)
   780  
   781  	assert.Equal(t, "", output.String())
   782  	assert.Equal(t, "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", output.Stderr())
   783  	assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", output.BrowsedURL)
   784  }
   785  
   786  func TestPRCreate_webLongURL(t *testing.T) {
   787  	longBodyFile := filepath.Join(t.TempDir(), "long-body.txt")
   788  	err := ioutil.WriteFile(longBodyFile, make([]byte, 9216), 0600)
   789  	require.NoError(t, err)
   790  
   791  	http := initFakeHTTP()
   792  	defer http.Verify(t)
   793  
   794  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   795  
   796  	cs, cmdTeardown := run.Stub()
   797  	defer cmdTeardown(t)
   798  
   799  	cs.Register(`git status --porcelain`, 0, "")
   800  	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
   801  
   802  	_, err = runCommand(http, nil, "feature", false, fmt.Sprintf("--body-file '%s' --web --head=feature", longBodyFile))
   803  	require.EqualError(t, err, "cannot open in browser: maximum URL length exceeded")
   804  }
   805  
   806  func TestPRCreate_webProject(t *testing.T) {
   807  	http := initFakeHTTP()
   808  	defer http.Verify(t)
   809  
   810  	http.StubRepoInfoResponse("OWNER", "REPO", "master")
   811  	http.StubRepoResponse("OWNER", "REPO")
   812  	http.Register(
   813  		httpmock.GraphQL(`query UserCurrent\b`),
   814  		httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
   815  	http.Register(
   816  		httpmock.GraphQL(`query RepositoryProjectList\b`),
   817  		httpmock.StringResponse(`
   818  			{ "data": { "repository": { "projects": {
   819  				"nodes": [
   820  					{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" }
   821  				],
   822  				"pageInfo": { "hasNextPage": false }
   823  			} } } }
   824  			`))
   825  	http.Register(
   826  		httpmock.GraphQL(`query OrganizationProjectList\b`),
   827  		httpmock.StringResponse(`
   828  			{ "data": { "organization": { "projects": {
   829  				"nodes": [
   830  					{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1"  }
   831  				],
   832  				"pageInfo": { "hasNextPage": false }
   833  			} } } }
   834  			`))
   835  
   836  	cs, cmdTeardown := run.Stub()
   837  	defer cmdTeardown(t)
   838  
   839  	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
   840  	cs.Register(`git status --porcelain`, 0, "")
   841  	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
   842  	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
   843  	cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "")
   844  
   845  	ask, cleanupAsk := prompt.InitAskStubber()
   846  	defer cleanupAsk()
   847  	ask.StubOne(0)
   848  
   849  	output, err := runCommand(http, nil, "feature", true, `--web -p Triage`)
   850  	require.NoError(t, err)
   851  
   852  	assert.Equal(t, "", output.String())
   853  	assert.Equal(t, "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", output.Stderr())
   854  	assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1&projects=ORG%2F1", output.BrowsedURL)
   855  }
   856  
   857  func Test_determineTrackingBranch_empty(t *testing.T) {
   858  	cs, cmdTeardown := run.Stub()
   859  	defer cmdTeardown(t)
   860  
   861  	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
   862  	cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD")
   863  
   864  	remotes := context.Remotes{}
   865  
   866  	ref := determineTrackingBranch(remotes, "feature")
   867  	if ref != nil {
   868  		t.Errorf("expected nil result, got %v", ref)
   869  	}
   870  }
   871  
   872  func Test_determineTrackingBranch_noMatch(t *testing.T) {
   873  	cs, cmdTeardown := run.Stub()
   874  	defer cmdTeardown(t)
   875  
   876  	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
   877  	cs.Register("git show-ref --verify -- HEAD refs/remotes/origin/feature refs/remotes/upstream/feature", 0, "abc HEAD\nbca refs/remotes/origin/feature")
   878  
   879  	remotes := context.Remotes{
   880  		&context.Remote{
   881  			Remote: &git.Remote{Name: "origin"},
   882  			Repo:   ghrepo.New("hubot", "Spoon-Knife"),
   883  		},
   884  		&context.Remote{
   885  			Remote: &git.Remote{Name: "upstream"},
   886  			Repo:   ghrepo.New("octocat", "Spoon-Knife"),
   887  		},
   888  	}
   889  
   890  	ref := determineTrackingBranch(remotes, "feature")
   891  	if ref != nil {
   892  		t.Errorf("expected nil result, got %v", ref)
   893  	}
   894  }
   895  
   896  func Test_determineTrackingBranch_hasMatch(t *testing.T) {
   897  	cs, cmdTeardown := run.Stub()
   898  	defer cmdTeardown(t)
   899  
   900  	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
   901  	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature refs/remotes/upstream/feature$`, 0, heredoc.Doc(`
   902  		deadbeef HEAD
   903  		deadb00f refs/remotes/origin/feature
   904  		deadbeef refs/remotes/upstream/feature
   905  	`))
   906  
   907  	remotes := context.Remotes{
   908  		&context.Remote{
   909  			Remote: &git.Remote{Name: "origin"},
   910  			Repo:   ghrepo.New("hubot", "Spoon-Knife"),
   911  		},
   912  		&context.Remote{
   913  			Remote: &git.Remote{Name: "upstream"},
   914  			Repo:   ghrepo.New("octocat", "Spoon-Knife"),
   915  		},
   916  	}
   917  
   918  	ref := determineTrackingBranch(remotes, "feature")
   919  	if ref == nil {
   920  		t.Fatal("expected result, got nil")
   921  	}
   922  
   923  	assert.Equal(t, "upstream", ref.RemoteName)
   924  	assert.Equal(t, "feature", ref.BranchName)
   925  }
   926  
   927  func Test_determineTrackingBranch_respectTrackingConfig(t *testing.T) {
   928  	cs, cmdTeardown := run.Stub()
   929  	defer cmdTeardown(t)
   930  
   931  	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, heredoc.Doc(`
   932  		branch.feature.remote origin
   933  		branch.feature.merge refs/heads/great-feat
   934  	`))
   935  	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/great-feat refs/remotes/origin/feature$`, 0, heredoc.Doc(`
   936  		deadbeef HEAD
   937  		deadb00f refs/remotes/origin/feature
   938  	`))
   939  
   940  	remotes := context.Remotes{
   941  		&context.Remote{
   942  			Remote: &git.Remote{Name: "origin"},
   943  			Repo:   ghrepo.New("hubot", "Spoon-Knife"),
   944  		},
   945  	}
   946  
   947  	ref := determineTrackingBranch(remotes, "feature")
   948  	if ref != nil {
   949  		t.Errorf("expected nil result, got %v", ref)
   950  	}
   951  }
   952  
   953  func Test_generateCompareURL(t *testing.T) {
   954  	tests := []struct {
   955  		name    string
   956  		ctx     CreateContext
   957  		state   prShared.IssueMetadataState
   958  		want    string
   959  		wantErr bool
   960  	}{
   961  		{
   962  			name: "basic",
   963  			ctx: CreateContext{
   964  				BaseRepo:        api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
   965  				BaseBranch:      "main",
   966  				HeadBranchLabel: "feature",
   967  			},
   968  			want:    "https://github.com/OWNER/REPO/compare/main...feature?body=&expand=1",
   969  			wantErr: false,
   970  		},
   971  		{
   972  			name: "with labels",
   973  			ctx: CreateContext{
   974  				BaseRepo:        api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
   975  				BaseBranch:      "a",
   976  				HeadBranchLabel: "b",
   977  			},
   978  			state: prShared.IssueMetadataState{
   979  				Labels: []string{"one", "two three"},
   980  			},
   981  			want:    "https://github.com/OWNER/REPO/compare/a...b?body=&expand=1&labels=one%2Ctwo+three",
   982  			wantErr: false,
   983  		},
   984  		{
   985  			name: "complex branch names",
   986  			ctx: CreateContext{
   987  				BaseRepo:        api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
   988  				BaseBranch:      "main/trunk",
   989  				HeadBranchLabel: "owner:feature",
   990  			},
   991  			want:    "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner%3Afeature?body=&expand=1",
   992  			wantErr: false,
   993  		},
   994  	}
   995  	for _, tt := range tests {
   996  		t.Run(tt.name, func(t *testing.T) {
   997  			got, err := generateCompareURL(tt.ctx, tt.state)
   998  			if (err != nil) != tt.wantErr {
   999  				t.Errorf("generateCompareURL() error = %v, wantErr %v", err, tt.wantErr)
  1000  				return
  1001  			}
  1002  			if got != tt.want {
  1003  				t.Errorf("generateCompareURL() = %v, want %v", got, tt.want)
  1004  			}
  1005  		})
  1006  	}
  1007  }