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 }