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

     1  package checkout
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/cli/cli/api"
    12  	"github.com/cli/cli/context"
    13  	"github.com/cli/cli/git"
    14  	"github.com/cli/cli/internal/config"
    15  	"github.com/cli/cli/internal/ghrepo"
    16  	"github.com/cli/cli/internal/run"
    17  	"github.com/cli/cli/pkg/cmd/pr/shared"
    18  	"github.com/cli/cli/pkg/cmdutil"
    19  	"github.com/cli/cli/pkg/httpmock"
    20  	"github.com/cli/cli/pkg/iostreams"
    21  	"github.com/cli/cli/test"
    22  	"github.com/google/shlex"
    23  	"github.com/stretchr/testify/assert"
    24  )
    25  
    26  // repo: either "baseOwner/baseRepo" or "baseOwner/baseRepo:defaultBranch"
    27  // prHead: "headOwner/headRepo:headBranch"
    28  func stubPR(repo, prHead string) (ghrepo.Interface, *api.PullRequest) {
    29  	defaultBranch := ""
    30  	if idx := strings.IndexRune(repo, ':'); idx >= 0 {
    31  		defaultBranch = repo[idx+1:]
    32  		repo = repo[:idx]
    33  	}
    34  	baseRepo, err := ghrepo.FromFullName(repo)
    35  	if err != nil {
    36  		panic(err)
    37  	}
    38  	if defaultBranch != "" {
    39  		baseRepo = api.InitRepoHostname(&api.Repository{
    40  			Name:             baseRepo.RepoName(),
    41  			Owner:            api.RepositoryOwner{Login: baseRepo.RepoOwner()},
    42  			DefaultBranchRef: api.BranchRef{Name: defaultBranch},
    43  		}, baseRepo.RepoHost())
    44  	}
    45  
    46  	idx := strings.IndexRune(prHead, ':')
    47  	headRefName := prHead[idx+1:]
    48  	headRepo, err := ghrepo.FromFullName(prHead[:idx])
    49  	if err != nil {
    50  		panic(err)
    51  	}
    52  
    53  	return baseRepo, &api.PullRequest{
    54  		Number:              123,
    55  		HeadRefName:         headRefName,
    56  		HeadRepositoryOwner: api.Owner{Login: headRepo.RepoOwner()},
    57  		HeadRepository:      &api.PRRepository{Name: headRepo.RepoName()},
    58  		IsCrossRepository:   !ghrepo.IsSame(baseRepo, headRepo),
    59  		MaintainerCanModify: false,
    60  	}
    61  }
    62  
    63  func Test_checkoutRun(t *testing.T) {
    64  	tests := []struct {
    65  		name       string
    66  		opts       *CheckoutOptions
    67  		httpStubs  func(*httpmock.Registry)
    68  		runStubs   func(*run.CommandStubber)
    69  		remotes    map[string]string
    70  		wantStdout string
    71  		wantStderr string
    72  		wantErr    bool
    73  	}{
    74  		{
    75  			name: "fork repo was deleted",
    76  			opts: &CheckoutOptions{
    77  				SelectorArg: "123",
    78  				Finder: func() shared.PRFinder {
    79  					baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
    80  					pr.MaintainerCanModify = true
    81  					pr.HeadRepository = nil
    82  					finder := shared.NewMockFinder("123", pr, baseRepo)
    83  					return finder
    84  				}(),
    85  				Config: func() (config.Config, error) {
    86  					return config.NewBlankConfig(), nil
    87  				},
    88  				Branch: func() (string, error) {
    89  					return "main", nil
    90  				},
    91  			},
    92  			remotes: map[string]string{
    93  				"origin": "OWNER/REPO",
    94  			},
    95  			runStubs: func(cs *run.CommandStubber) {
    96  				cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "")
    97  				cs.Register(`git config branch\.feature\.merge`, 1, "")
    98  				cs.Register(`git checkout feature`, 0, "")
    99  				cs.Register(`git config branch\.feature\.remote origin`, 0, "")
   100  				cs.Register(`git config branch\.feature\.merge refs/pull/123/head`, 0, "")
   101  			},
   102  		},
   103  		{
   104  			name: "with local branch rename and existing git remote",
   105  			opts: &CheckoutOptions{
   106  				SelectorArg: "123",
   107  				BranchName:  "foobar",
   108  				Finder: func() shared.PRFinder {
   109  					baseRepo, pr := stubPR("OWNER/REPO:master", "OWNER/REPO:feature")
   110  					finder := shared.NewMockFinder("123", pr, baseRepo)
   111  					return finder
   112  				}(),
   113  				Config: func() (config.Config, error) {
   114  					return config.NewBlankConfig(), nil
   115  				},
   116  				Branch: func() (string, error) {
   117  					return "main", nil
   118  				},
   119  			},
   120  			remotes: map[string]string{
   121  				"origin": "OWNER/REPO",
   122  			},
   123  			runStubs: func(cs *run.CommandStubber) {
   124  				cs.Register(`git show-ref --verify -- refs/heads/foobar`, 1, "")
   125  				cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
   126  				cs.Register(`git checkout -b foobar --track origin/feature`, 0, "")
   127  			},
   128  		},
   129  		{
   130  			name: "with local branch name, no existing git remote",
   131  			opts: &CheckoutOptions{
   132  				SelectorArg: "123",
   133  				BranchName:  "foobar",
   134  				Finder: func() shared.PRFinder {
   135  					baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
   136  					pr.MaintainerCanModify = true
   137  					finder := shared.NewMockFinder("123", pr, baseRepo)
   138  					return finder
   139  				}(),
   140  				Config: func() (config.Config, error) {
   141  					return config.NewBlankConfig(), nil
   142  				},
   143  				Branch: func() (string, error) {
   144  					return "main", nil
   145  				},
   146  			},
   147  			remotes: map[string]string{
   148  				"origin": "OWNER/REPO",
   149  			},
   150  			runStubs: func(cs *run.CommandStubber) {
   151  				cs.Register(`git config branch\.foobar\.merge`, 1, "")
   152  				cs.Register(`git fetch origin refs/pull/123/head:foobar`, 0, "")
   153  				cs.Register(`git checkout foobar`, 0, "")
   154  				cs.Register(`git config branch\.foobar\.remote https://github.com/hubot/REPO.git`, 0, "")
   155  				cs.Register(`git config branch\.foobar\.merge refs/heads/feature`, 0, "")
   156  			},
   157  		},
   158  	}
   159  	for _, tt := range tests {
   160  		t.Run(tt.name, func(t *testing.T) {
   161  			opts := tt.opts
   162  
   163  			io, _, stdout, stderr := iostreams.Test()
   164  			opts.IO = io
   165  
   166  			httpReg := &httpmock.Registry{}
   167  			defer httpReg.Verify(t)
   168  			if tt.httpStubs != nil {
   169  				tt.httpStubs(httpReg)
   170  			}
   171  			opts.HttpClient = func() (*http.Client, error) {
   172  				return &http.Client{Transport: httpReg}, nil
   173  			}
   174  
   175  			cmdStubs, cmdTeardown := run.Stub()
   176  			defer cmdTeardown(t)
   177  			if tt.runStubs != nil {
   178  				tt.runStubs(cmdStubs)
   179  			}
   180  
   181  			opts.Remotes = func() (context.Remotes, error) {
   182  				if len(tt.remotes) == 0 {
   183  					return nil, errors.New("no remotes")
   184  				}
   185  				var remotes context.Remotes
   186  				for name, repo := range tt.remotes {
   187  					r, err := ghrepo.FromFullName(repo)
   188  					if err != nil {
   189  						return remotes, err
   190  					}
   191  					remotes = append(remotes, &context.Remote{
   192  						Remote: &git.Remote{Name: name},
   193  						Repo:   r,
   194  					})
   195  				}
   196  				return remotes, nil
   197  			}
   198  
   199  			err := checkoutRun(opts)
   200  			if (err != nil) != tt.wantErr {
   201  				t.Errorf("want error: %v, got: %v", tt.wantErr, err)
   202  			}
   203  			assert.Equal(t, tt.wantStdout, stdout.String())
   204  			assert.Equal(t, tt.wantStderr, stderr.String())
   205  		})
   206  	}
   207  }
   208  
   209  /** LEGACY TESTS **/
   210  
   211  func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cli string) (*test.CmdOut, error) {
   212  	io, _, stdout, stderr := iostreams.Test()
   213  
   214  	factory := &cmdutil.Factory{
   215  		IOStreams: io,
   216  		HttpClient: func() (*http.Client, error) {
   217  			return &http.Client{Transport: rt}, nil
   218  		},
   219  		Config: func() (config.Config, error) {
   220  			return config.NewBlankConfig(), nil
   221  		},
   222  		Remotes: func() (context.Remotes, error) {
   223  			if remotes == nil {
   224  				return context.Remotes{
   225  					{
   226  						Remote: &git.Remote{Name: "origin"},
   227  						Repo:   ghrepo.New("OWNER", "REPO"),
   228  					},
   229  				}, nil
   230  			}
   231  			return remotes, nil
   232  		},
   233  		Branch: func() (string, error) {
   234  			return branch, nil
   235  		},
   236  	}
   237  
   238  	cmd := NewCmdCheckout(factory, nil)
   239  
   240  	argv, err := shlex.Split(cli)
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  	cmd.SetArgs(argv)
   245  
   246  	cmd.SetIn(&bytes.Buffer{})
   247  	cmd.SetOut(ioutil.Discard)
   248  	cmd.SetErr(ioutil.Discard)
   249  
   250  	_, err = cmd.ExecuteC()
   251  	return &test.CmdOut{
   252  		OutBuf: stdout,
   253  		ErrBuf: stderr,
   254  	}, err
   255  }
   256  
   257  func TestPRCheckout_sameRepo(t *testing.T) {
   258  	http := &httpmock.Registry{}
   259  	defer http.Verify(t)
   260  
   261  	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
   262  	finder := shared.RunCommandFinder("123", pr, baseRepo)
   263  	finder.ExpectFields([]string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"})
   264  
   265  	cs, cmdTeardown := run.Stub()
   266  	defer cmdTeardown(t)
   267  
   268  	cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
   269  	cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "")
   270  	cs.Register(`git checkout -b feature --track origin/feature`, 0, "")
   271  
   272  	output, err := runCommand(http, nil, "master", `123`)
   273  	assert.NoError(t, err)
   274  	assert.Equal(t, "", output.String())
   275  	assert.Equal(t, "", output.Stderr())
   276  }
   277  
   278  func TestPRCheckout_existingBranch(t *testing.T) {
   279  	http := &httpmock.Registry{}
   280  	defer http.Verify(t)
   281  
   282  	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
   283  	shared.RunCommandFinder("123", pr, baseRepo)
   284  
   285  	cs, cmdTeardown := run.Stub()
   286  	defer cmdTeardown(t)
   287  
   288  	cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
   289  	cs.Register(`git show-ref --verify -- refs/heads/feature`, 0, "")
   290  	cs.Register(`git checkout feature`, 0, "")
   291  	cs.Register(`git merge --ff-only refs/remotes/origin/feature`, 0, "")
   292  
   293  	output, err := runCommand(http, nil, "master", `123`)
   294  	assert.NoError(t, err)
   295  	assert.Equal(t, "", output.String())
   296  	assert.Equal(t, "", output.Stderr())
   297  }
   298  
   299  func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
   300  	remotes := context.Remotes{
   301  		{
   302  			Remote: &git.Remote{Name: "origin"},
   303  			Repo:   ghrepo.New("OWNER", "REPO"),
   304  		},
   305  		{
   306  			Remote: &git.Remote{Name: "robot-fork"},
   307  			Repo:   ghrepo.New("hubot", "REPO"),
   308  		},
   309  	}
   310  
   311  	http := &httpmock.Registry{}
   312  	defer http.Verify(t)
   313  
   314  	baseRepo, pr := stubPR("OWNER/REPO", "hubot/REPO:feature")
   315  	finder := shared.RunCommandFinder("123", pr, baseRepo)
   316  	finder.ExpectFields([]string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"})
   317  
   318  	cs, cmdTeardown := run.Stub()
   319  	defer cmdTeardown(t)
   320  
   321  	cs.Register(`git fetch robot-fork \+refs/heads/feature:refs/remotes/robot-fork/feature`, 0, "")
   322  	cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "")
   323  	cs.Register(`git checkout -b feature --track robot-fork/feature`, 0, "")
   324  
   325  	output, err := runCommand(http, remotes, "master", `123`)
   326  	assert.NoError(t, err)
   327  	assert.Equal(t, "", output.String())
   328  	assert.Equal(t, "", output.Stderr())
   329  }
   330  
   331  func TestPRCheckout_differentRepo(t *testing.T) {
   332  	http := &httpmock.Registry{}
   333  	defer http.Verify(t)
   334  
   335  	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
   336  	finder := shared.RunCommandFinder("123", pr, baseRepo)
   337  	finder.ExpectFields([]string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"})
   338  
   339  	cs, cmdTeardown := run.Stub()
   340  	defer cmdTeardown(t)
   341  
   342  	cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "")
   343  	cs.Register(`git config branch\.feature\.merge`, 1, "")
   344  	cs.Register(`git checkout feature`, 0, "")
   345  	cs.Register(`git config branch\.feature\.remote origin`, 0, "")
   346  	cs.Register(`git config branch\.feature\.merge refs/pull/123/head`, 0, "")
   347  
   348  	output, err := runCommand(http, nil, "master", `123`)
   349  	assert.NoError(t, err)
   350  	assert.Equal(t, "", output.String())
   351  	assert.Equal(t, "", output.Stderr())
   352  }
   353  
   354  func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
   355  	http := &httpmock.Registry{}
   356  	defer http.Verify(t)
   357  
   358  	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
   359  	shared.RunCommandFinder("123", pr, baseRepo)
   360  
   361  	cs, cmdTeardown := run.Stub()
   362  	defer cmdTeardown(t)
   363  
   364  	cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "")
   365  	cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n")
   366  	cs.Register(`git checkout feature`, 0, "")
   367  
   368  	output, err := runCommand(http, nil, "master", `123`)
   369  	assert.NoError(t, err)
   370  	assert.Equal(t, "", output.String())
   371  	assert.Equal(t, "", output.Stderr())
   372  }
   373  
   374  func TestPRCheckout_detachedHead(t *testing.T) {
   375  	http := &httpmock.Registry{}
   376  	defer http.Verify(t)
   377  
   378  	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
   379  	shared.RunCommandFinder("123", pr, baseRepo)
   380  
   381  	cs, cmdTeardown := run.Stub()
   382  	defer cmdTeardown(t)
   383  
   384  	cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "")
   385  	cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n")
   386  	cs.Register(`git checkout feature`, 0, "")
   387  
   388  	output, err := runCommand(http, nil, "", `123`)
   389  	assert.NoError(t, err)
   390  	assert.Equal(t, "", output.String())
   391  	assert.Equal(t, "", output.Stderr())
   392  }
   393  
   394  func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
   395  	http := &httpmock.Registry{}
   396  	defer http.Verify(t)
   397  
   398  	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
   399  	shared.RunCommandFinder("123", pr, baseRepo)
   400  
   401  	cs, cmdTeardown := run.Stub()
   402  	defer cmdTeardown(t)
   403  
   404  	cs.Register(`git fetch origin refs/pull/123/head`, 0, "")
   405  	cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n")
   406  	cs.Register(`git merge --ff-only FETCH_HEAD`, 0, "")
   407  
   408  	output, err := runCommand(http, nil, "feature", `123`)
   409  	assert.NoError(t, err)
   410  	assert.Equal(t, "", output.String())
   411  	assert.Equal(t, "", output.Stderr())
   412  }
   413  
   414  func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) {
   415  	http := &httpmock.Registry{}
   416  	defer http.Verify(t)
   417  
   418  	baseRepo, pr := stubPR("OWNER/REPO", "hubot/REPO:-foo")
   419  	shared.RunCommandFinder("123", pr, baseRepo)
   420  
   421  	_, cmdTeardown := run.Stub()
   422  	defer cmdTeardown(t)
   423  
   424  	output, err := runCommand(http, nil, "master", `123`)
   425  	assert.EqualError(t, err, `invalid branch name: "-foo"`)
   426  	assert.Equal(t, "", output.Stderr())
   427  	assert.Equal(t, "", output.Stderr())
   428  }
   429  
   430  func TestPRCheckout_maintainerCanModify(t *testing.T) {
   431  	http := &httpmock.Registry{}
   432  	defer http.Verify(t)
   433  
   434  	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
   435  	pr.MaintainerCanModify = true
   436  	shared.RunCommandFinder("123", pr, baseRepo)
   437  
   438  	cs, cmdTeardown := run.Stub()
   439  	defer cmdTeardown(t)
   440  
   441  	cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "")
   442  	cs.Register(`git config branch\.feature\.merge`, 1, "")
   443  	cs.Register(`git checkout feature`, 0, "")
   444  	cs.Register(`git config branch\.feature\.remote https://github\.com/hubot/REPO\.git`, 0, "")
   445  	cs.Register(`git config branch\.feature\.merge refs/heads/feature`, 0, "")
   446  
   447  	output, err := runCommand(http, nil, "master", `123`)
   448  	assert.NoError(t, err)
   449  	assert.Equal(t, "", output.String())
   450  	assert.Equal(t, "", output.Stderr())
   451  }
   452  
   453  func TestPRCheckout_recurseSubmodules(t *testing.T) {
   454  	http := &httpmock.Registry{}
   455  
   456  	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
   457  	shared.RunCommandFinder("123", pr, baseRepo)
   458  
   459  	cs, cmdTeardown := run.Stub()
   460  	defer cmdTeardown(t)
   461  
   462  	cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
   463  	cs.Register(`git show-ref --verify -- refs/heads/feature`, 0, "")
   464  	cs.Register(`git checkout feature`, 0, "")
   465  	cs.Register(`git merge --ff-only refs/remotes/origin/feature`, 0, "")
   466  	cs.Register(`git submodule sync --recursive`, 0, "")
   467  	cs.Register(`git submodule update --init --recursive`, 0, "")
   468  
   469  	output, err := runCommand(http, nil, "master", `123 --recurse-submodules`)
   470  	assert.NoError(t, err)
   471  	assert.Equal(t, "", output.String())
   472  	assert.Equal(t, "", output.Stderr())
   473  }
   474  
   475  func TestPRCheckout_force(t *testing.T) {
   476  	http := &httpmock.Registry{}
   477  
   478  	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
   479  	shared.RunCommandFinder("123", pr, baseRepo)
   480  
   481  	cs, cmdTeardown := run.Stub()
   482  	defer cmdTeardown(t)
   483  
   484  	cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
   485  	cs.Register(`git show-ref --verify -- refs/heads/feature`, 0, "")
   486  	cs.Register(`git checkout feature`, 0, "")
   487  	cs.Register(`git reset --hard refs/remotes/origin/feature`, 0, "")
   488  
   489  	output, err := runCommand(http, nil, "master", `123 --force`)
   490  
   491  	assert.NoError(t, err)
   492  	assert.Equal(t, "", output.String())
   493  	assert.Equal(t, "", output.Stderr())
   494  }
   495  
   496  func TestPRCheckout_detach(t *testing.T) {
   497  	http := &httpmock.Registry{}
   498  	defer http.Verify(t)
   499  
   500  	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
   501  	shared.RunCommandFinder("123", pr, baseRepo)
   502  
   503  	cs, cmdTeardown := run.Stub()
   504  	defer cmdTeardown(t)
   505  
   506  	cs.Register(`git checkout --detach FETCH_HEAD`, 0, "")
   507  	cs.Register(`git fetch origin refs/pull/123/head`, 0, "")
   508  
   509  	output, err := runCommand(http, nil, "", `123 --detach`)
   510  	assert.NoError(t, err)
   511  	assert.Equal(t, "", output.String())
   512  	assert.Equal(t, "", output.Stderr())
   513  }