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 }