github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/pr/checkout/checkout_test.go (about)

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