github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/git/client_test.go (about) 1 package git 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "strconv" 12 "strings" 13 "testing" 14 15 "github.com/stretchr/testify/assert" 16 ) 17 18 func TestClientCommand(t *testing.T) { 19 tests := []struct { 20 name string 21 repoDir string 22 gitPath string 23 wantExe string 24 wantArgs []string 25 }{ 26 { 27 name: "creates command", 28 gitPath: "path/to/git", 29 wantExe: "path/to/git", 30 wantArgs: []string{"path/to/git", "ref-log"}, 31 }, 32 { 33 name: "adds repo directory configuration", 34 repoDir: "path/to/repo", 35 gitPath: "path/to/git", 36 wantExe: "path/to/git", 37 wantArgs: []string{"path/to/git", "-C", "path/to/repo", "ref-log"}, 38 }, 39 } 40 for _, tt := range tests { 41 t.Run(tt.name, func(t *testing.T) { 42 in, out, errOut := &bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{} 43 client := Client{ 44 Stdin: in, 45 Stdout: out, 46 Stderr: errOut, 47 RepoDir: tt.repoDir, 48 GitPath: tt.gitPath, 49 } 50 cmd, err := client.Command(context.Background(), "ref-log") 51 assert.NoError(t, err) 52 assert.Equal(t, tt.wantExe, cmd.Path) 53 assert.Equal(t, tt.wantArgs, cmd.Args) 54 assert.Equal(t, in, cmd.Stdin) 55 assert.Equal(t, out, cmd.Stdout) 56 assert.Equal(t, errOut, cmd.Stderr) 57 }) 58 } 59 } 60 61 func TestClientAuthenticatedCommand(t *testing.T) { 62 tests := []struct { 63 name string 64 path string 65 wantArgs []string 66 }{ 67 { 68 name: "adds credential helper config options", 69 path: "path/to/gh", 70 wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", "credential.helper=!\"path/to/gh\" auth git-credential", "fetch"}, 71 }, 72 { 73 name: "fallback when GhPath is not set", 74 wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", "credential.helper=!\"gh\" auth git-credential", "fetch"}, 75 }, 76 } 77 for _, tt := range tests { 78 t.Run(tt.name, func(t *testing.T) { 79 client := Client{ 80 GhPath: tt.path, 81 GitPath: "path/to/git", 82 } 83 cmd, err := client.AuthenticatedCommand(context.Background(), "fetch") 84 assert.NoError(t, err) 85 assert.Equal(t, tt.wantArgs, cmd.Args) 86 }) 87 } 88 } 89 90 func TestClientRemotes(t *testing.T) { 91 tempDir := t.TempDir() 92 initRepo(t, tempDir) 93 gitDir := filepath.Join(tempDir, ".git") 94 remoteFile := filepath.Join(gitDir, "config") 95 remotes := ` 96 [remote "origin"] 97 url = git@example.com:monalisa/origin.git 98 [remote "test"] 99 url = git://github.com/hubot/test.git 100 gh-resolved = other 101 [remote "upstream"] 102 url = https://github.com/monalisa/upstream.git 103 gh-resolved = base 104 [remote "github"] 105 url = git@github.com:hubot/github.git 106 ` 107 f, err := os.OpenFile(remoteFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755) 108 assert.NoError(t, err) 109 _, err = f.Write([]byte(remotes)) 110 assert.NoError(t, err) 111 err = f.Close() 112 assert.NoError(t, err) 113 client := Client{ 114 RepoDir: tempDir, 115 } 116 rs, err := client.Remotes(context.Background()) 117 assert.NoError(t, err) 118 assert.Equal(t, 4, len(rs)) 119 assert.Equal(t, "upstream", rs[0].Name) 120 assert.Equal(t, "base", rs[0].Resolved) 121 assert.Equal(t, "github", rs[1].Name) 122 assert.Equal(t, "", rs[1].Resolved) 123 assert.Equal(t, "origin", rs[2].Name) 124 assert.Equal(t, "", rs[2].Resolved) 125 assert.Equal(t, "test", rs[3].Name) 126 assert.Equal(t, "other", rs[3].Resolved) 127 } 128 129 func TestClientRemotes_no_resolved_remote(t *testing.T) { 130 tempDir := t.TempDir() 131 initRepo(t, tempDir) 132 gitDir := filepath.Join(tempDir, ".git") 133 remoteFile := filepath.Join(gitDir, "config") 134 remotes := ` 135 [remote "origin"] 136 url = git@example.com:monalisa/origin.git 137 [remote "test"] 138 url = git://github.com/hubot/test.git 139 [remote "upstream"] 140 url = https://github.com/monalisa/upstream.git 141 [remote "github"] 142 url = git@github.com:hubot/github.git 143 ` 144 f, err := os.OpenFile(remoteFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755) 145 assert.NoError(t, err) 146 _, err = f.Write([]byte(remotes)) 147 assert.NoError(t, err) 148 err = f.Close() 149 assert.NoError(t, err) 150 client := Client{ 151 RepoDir: tempDir, 152 } 153 rs, err := client.Remotes(context.Background()) 154 assert.NoError(t, err) 155 assert.Equal(t, 4, len(rs)) 156 assert.Equal(t, "upstream", rs[0].Name) 157 assert.Equal(t, "github", rs[1].Name) 158 assert.Equal(t, "origin", rs[2].Name) 159 assert.Equal(t, "", rs[2].Resolved) 160 assert.Equal(t, "test", rs[3].Name) 161 } 162 163 func TestParseRemotes(t *testing.T) { 164 remoteList := []string{ 165 "mona\tgit@github.com:monalisa/myfork.git (fetch)", 166 "origin\thttps://github.com/monalisa/octo-cat.git (fetch)", 167 "origin\thttps://github.com/monalisa/octo-cat-push.git (push)", 168 "upstream\thttps://example.com/nowhere.git (fetch)", 169 "upstream\thttps://github.com/hubot/tools (push)", 170 "zardoz\thttps://example.com/zed.git (push)", 171 "koke\tgit://github.com/koke/grit.git (fetch)", 172 "koke\tgit://github.com/koke/grit.git (push)", 173 } 174 175 r := parseRemotes(remoteList) 176 assert.Equal(t, 5, len(r)) 177 178 assert.Equal(t, "mona", r[0].Name) 179 assert.Equal(t, "ssh://git@github.com/monalisa/myfork.git", r[0].FetchURL.String()) 180 assert.Nil(t, r[0].PushURL) 181 182 assert.Equal(t, "origin", r[1].Name) 183 assert.Equal(t, "/monalisa/octo-cat.git", r[1].FetchURL.Path) 184 assert.Equal(t, "/monalisa/octo-cat-push.git", r[1].PushURL.Path) 185 186 assert.Equal(t, "upstream", r[2].Name) 187 assert.Equal(t, "example.com", r[2].FetchURL.Host) 188 assert.Equal(t, "github.com", r[2].PushURL.Host) 189 190 assert.Equal(t, "zardoz", r[3].Name) 191 assert.Nil(t, r[3].FetchURL) 192 assert.Equal(t, "https://example.com/zed.git", r[3].PushURL.String()) 193 194 assert.Equal(t, "koke", r[4].Name) 195 assert.Equal(t, "/koke/grit.git", r[4].FetchURL.Path) 196 assert.Equal(t, "/koke/grit.git", r[4].PushURL.Path) 197 } 198 199 func TestClientUpdateRemoteURL(t *testing.T) { 200 tests := []struct { 201 name string 202 cmdExitStatus int 203 cmdStdout string 204 cmdStderr string 205 wantCmdArgs string 206 wantErrorMsg string 207 }{ 208 { 209 name: "update remote url", 210 wantCmdArgs: `path/to/git remote set-url test https://test.com`, 211 }, 212 { 213 name: "git error", 214 cmdExitStatus: 1, 215 cmdStderr: "git error message", 216 wantCmdArgs: `path/to/git remote set-url test https://test.com`, 217 wantErrorMsg: "failed to run git: git error message", 218 }, 219 } 220 for _, tt := range tests { 221 t.Run(tt.name, func(t *testing.T) { 222 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 223 client := Client{ 224 GitPath: "path/to/git", 225 commandContext: cmdCtx, 226 } 227 err := client.UpdateRemoteURL(context.Background(), "test", "https://test.com") 228 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 229 if tt.wantErrorMsg == "" { 230 assert.NoError(t, err) 231 } else { 232 assert.EqualError(t, err, tt.wantErrorMsg) 233 } 234 }) 235 } 236 } 237 238 func TestClientSetRemoteResolution(t *testing.T) { 239 tests := []struct { 240 name string 241 cmdExitStatus int 242 cmdStdout string 243 cmdStderr string 244 wantCmdArgs string 245 wantErrorMsg string 246 }{ 247 { 248 name: "set remote resolution", 249 wantCmdArgs: `path/to/git config --add remote.origin.gh-resolved base`, 250 }, 251 { 252 name: "git error", 253 cmdExitStatus: 1, 254 cmdStderr: "git error message", 255 wantCmdArgs: `path/to/git config --add remote.origin.gh-resolved base`, 256 wantErrorMsg: "failed to run git: git error message", 257 }, 258 } 259 for _, tt := range tests { 260 t.Run(tt.name, func(t *testing.T) { 261 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 262 client := Client{ 263 GitPath: "path/to/git", 264 commandContext: cmdCtx, 265 } 266 err := client.SetRemoteResolution(context.Background(), "origin", "base") 267 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 268 if tt.wantErrorMsg == "" { 269 assert.NoError(t, err) 270 } else { 271 assert.EqualError(t, err, tt.wantErrorMsg) 272 } 273 }) 274 } 275 } 276 277 func TestClientCurrentBranch(t *testing.T) { 278 tests := []struct { 279 name string 280 cmdExitStatus int 281 cmdStdout string 282 cmdStderr string 283 wantCmdArgs string 284 wantErrorMsg string 285 wantBranch string 286 }{ 287 { 288 name: "branch name", 289 cmdStdout: "branch-name\n", 290 wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`, 291 wantBranch: "branch-name", 292 }, 293 { 294 name: "ref", 295 cmdStdout: "refs/heads/branch-name\n", 296 wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`, 297 wantBranch: "branch-name", 298 }, 299 { 300 name: "escaped ref", 301 cmdStdout: "refs/heads/branch\u00A0with\u00A0non\u00A0breaking\u00A0space\n", 302 wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`, 303 wantBranch: "branch\u00A0with\u00A0non\u00A0breaking\u00A0space", 304 }, 305 { 306 name: "detatched head", 307 cmdExitStatus: 1, 308 wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`, 309 wantErrorMsg: "failed to run git: not on any branch", 310 }, 311 } 312 for _, tt := range tests { 313 t.Run(tt.name, func(t *testing.T) { 314 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 315 client := Client{ 316 GitPath: "path/to/git", 317 commandContext: cmdCtx, 318 } 319 branch, err := client.CurrentBranch(context.Background()) 320 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 321 if tt.wantErrorMsg == "" { 322 assert.NoError(t, err) 323 } else { 324 assert.EqualError(t, err, tt.wantErrorMsg) 325 } 326 assert.Equal(t, tt.wantBranch, branch) 327 }) 328 } 329 } 330 331 func TestClientShowRefs(t *testing.T) { 332 tests := []struct { 333 name string 334 cmdExitStatus int 335 cmdStdout string 336 cmdStderr string 337 wantCmdArgs string 338 wantRefs []Ref 339 wantErrorMsg string 340 }{ 341 { 342 name: "show refs with one vaid ref and one invalid ref", 343 cmdExitStatus: 128, 344 cmdStdout: "9ea76237a557015e73446d33268569a114c0649c refs/heads/valid", 345 cmdStderr: "fatal: 'refs/heads/invalid' - not a valid ref", 346 wantCmdArgs: `path/to/git show-ref --verify -- refs/heads/valid refs/heads/invalid`, 347 wantRefs: []Ref{{ 348 Hash: "9ea76237a557015e73446d33268569a114c0649c", 349 Name: "refs/heads/valid", 350 }}, 351 wantErrorMsg: "failed to run git: fatal: 'refs/heads/invalid' - not a valid ref", 352 }, 353 } 354 for _, tt := range tests { 355 t.Run(tt.name, func(t *testing.T) { 356 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 357 client := Client{ 358 GitPath: "path/to/git", 359 commandContext: cmdCtx, 360 } 361 refs, err := client.ShowRefs(context.Background(), []string{"refs/heads/valid", "refs/heads/invalid"}) 362 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 363 assert.EqualError(t, err, tt.wantErrorMsg) 364 assert.Equal(t, tt.wantRefs, refs) 365 }) 366 } 367 } 368 369 func TestClientConfig(t *testing.T) { 370 tests := []struct { 371 name string 372 cmdExitStatus int 373 cmdStdout string 374 cmdStderr string 375 wantCmdArgs string 376 wantOut string 377 wantErrorMsg string 378 }{ 379 { 380 name: "get config key", 381 cmdStdout: "test", 382 wantCmdArgs: `path/to/git config credential.helper`, 383 wantOut: "test", 384 }, 385 { 386 name: "get unknown config key", 387 cmdExitStatus: 1, 388 cmdStderr: "git error message", 389 wantCmdArgs: `path/to/git config credential.helper`, 390 wantErrorMsg: "failed to run git: unknown config key credential.helper", 391 }, 392 { 393 name: "git error", 394 cmdExitStatus: 2, 395 cmdStderr: "git error message", 396 wantCmdArgs: `path/to/git config credential.helper`, 397 wantErrorMsg: "failed to run git: git error message", 398 }, 399 } 400 for _, tt := range tests { 401 t.Run(tt.name, func(t *testing.T) { 402 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 403 client := Client{ 404 GitPath: "path/to/git", 405 commandContext: cmdCtx, 406 } 407 out, err := client.Config(context.Background(), "credential.helper") 408 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 409 if tt.wantErrorMsg == "" { 410 assert.NoError(t, err) 411 } else { 412 assert.EqualError(t, err, tt.wantErrorMsg) 413 } 414 assert.Equal(t, tt.wantOut, out) 415 }) 416 } 417 } 418 419 func TestClientUncommittedChangeCount(t *testing.T) { 420 tests := []struct { 421 name string 422 cmdExitStatus int 423 cmdStdout string 424 cmdStderr string 425 wantCmdArgs string 426 wantChangeCount int 427 }{ 428 { 429 name: "no changes", 430 wantCmdArgs: `path/to/git status --porcelain`, 431 wantChangeCount: 0, 432 }, 433 { 434 name: "one change", 435 cmdStdout: " M poem.txt", 436 wantCmdArgs: `path/to/git status --porcelain`, 437 wantChangeCount: 1, 438 }, 439 { 440 name: "untracked file", 441 cmdStdout: " M poem.txt\n?? new.txt", 442 wantCmdArgs: `path/to/git status --porcelain`, 443 wantChangeCount: 2, 444 }, 445 } 446 for _, tt := range tests { 447 t.Run(tt.name, func(t *testing.T) { 448 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 449 client := Client{ 450 GitPath: "path/to/git", 451 commandContext: cmdCtx, 452 } 453 ucc, err := client.UncommittedChangeCount(context.Background()) 454 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 455 assert.NoError(t, err) 456 assert.Equal(t, tt.wantChangeCount, ucc) 457 }) 458 } 459 } 460 461 func TestClientCommits(t *testing.T) { 462 tests := []struct { 463 name string 464 cmdExitStatus int 465 cmdStdout string 466 cmdStderr string 467 wantCmdArgs string 468 wantCommits []*Commit 469 wantErrorMsg string 470 }{ 471 { 472 name: "get commits", 473 cmdStdout: "6a6872b918c601a0e730710ad8473938a7516d30,testing testability test", 474 wantCmdArgs: `path/to/git -c log.ShowSignature=false log --pretty=format:%H,%s --cherry SHA1...SHA2`, 475 wantCommits: []*Commit{{ 476 Sha: "6a6872b918c601a0e730710ad8473938a7516d30", 477 Title: "testing testability test", 478 }}, 479 }, 480 { 481 name: "no commits between SHAs", 482 wantCmdArgs: `path/to/git -c log.ShowSignature=false log --pretty=format:%H,%s --cherry SHA1...SHA2`, 483 wantErrorMsg: "could not find any commits between SHA1 and SHA2", 484 }, 485 { 486 name: "git error", 487 cmdExitStatus: 1, 488 cmdStderr: "git error message", 489 wantCmdArgs: `path/to/git -c log.ShowSignature=false log --pretty=format:%H,%s --cherry SHA1...SHA2`, 490 wantErrorMsg: "failed to run git: git error message", 491 }, 492 } 493 for _, tt := range tests { 494 t.Run(tt.name, func(t *testing.T) { 495 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 496 client := Client{ 497 GitPath: "path/to/git", 498 commandContext: cmdCtx, 499 } 500 commits, err := client.Commits(context.Background(), "SHA1", "SHA2") 501 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 502 if tt.wantErrorMsg != "" { 503 assert.EqualError(t, err, tt.wantErrorMsg) 504 } else { 505 assert.NoError(t, err) 506 } 507 assert.Equal(t, tt.wantCommits, commits) 508 }) 509 } 510 } 511 512 func TestClientLastCommit(t *testing.T) { 513 client := Client{ 514 RepoDir: "./fixtures/simple.git", 515 } 516 c, err := client.LastCommit(context.Background()) 517 assert.NoError(t, err) 518 assert.Equal(t, "6f1a2405cace1633d89a79c74c65f22fe78f9659", c.Sha) 519 assert.Equal(t, "Second commit", c.Title) 520 } 521 522 func TestClientCommitBody(t *testing.T) { 523 client := Client{ 524 RepoDir: "./fixtures/simple.git", 525 } 526 body, err := client.CommitBody(context.Background(), "6f1a2405cace1633d89a79c74c65f22fe78f9659") 527 assert.NoError(t, err) 528 assert.Equal(t, "I'm starting to get the hang of things\n", body) 529 } 530 531 func TestClientReadBranchConfig(t *testing.T) { 532 tests := []struct { 533 name string 534 cmdExitStatus int 535 cmdStdout string 536 cmdStderr string 537 wantCmdArgs string 538 wantBranchConfig BranchConfig 539 }{ 540 { 541 name: "read branch config", 542 cmdStdout: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk", 543 wantCmdArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge)$`, 544 wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk"}, 545 }, 546 } 547 for _, tt := range tests { 548 t.Run(tt.name, func(t *testing.T) { 549 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 550 client := Client{ 551 GitPath: "path/to/git", 552 commandContext: cmdCtx, 553 } 554 branchConfig := client.ReadBranchConfig(context.Background(), "trunk") 555 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 556 assert.Equal(t, tt.wantBranchConfig, branchConfig) 557 }) 558 } 559 } 560 561 func TestClientDeleteLocalBranch(t *testing.T) { 562 tests := []struct { 563 name string 564 cmdExitStatus int 565 cmdStdout string 566 cmdStderr string 567 wantCmdArgs string 568 wantErrorMsg string 569 }{ 570 { 571 name: "delete local branch", 572 wantCmdArgs: `path/to/git branch -D trunk`, 573 }, 574 { 575 name: "git error", 576 cmdExitStatus: 1, 577 cmdStderr: "git error message", 578 wantCmdArgs: `path/to/git branch -D trunk`, 579 wantErrorMsg: "failed to run git: git error message", 580 }, 581 } 582 for _, tt := range tests { 583 t.Run(tt.name, func(t *testing.T) { 584 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 585 client := Client{ 586 GitPath: "path/to/git", 587 commandContext: cmdCtx, 588 } 589 err := client.DeleteLocalBranch(context.Background(), "trunk") 590 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 591 if tt.wantErrorMsg == "" { 592 assert.NoError(t, err) 593 } else { 594 assert.EqualError(t, err, tt.wantErrorMsg) 595 } 596 }) 597 } 598 } 599 600 func TestClientHasLocalBranch(t *testing.T) { 601 tests := []struct { 602 name string 603 cmdExitStatus int 604 cmdStdout string 605 cmdStderr string 606 wantCmdArgs string 607 wantOut bool 608 }{ 609 { 610 name: "has local branch", 611 wantCmdArgs: `path/to/git rev-parse --verify refs/heads/trunk`, 612 wantOut: true, 613 }, 614 { 615 name: "does not have local branch", 616 cmdExitStatus: 1, 617 wantCmdArgs: `path/to/git rev-parse --verify refs/heads/trunk`, 618 wantOut: false, 619 }, 620 } 621 for _, tt := range tests { 622 t.Run(tt.name, func(t *testing.T) { 623 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 624 client := Client{ 625 GitPath: "path/to/git", 626 commandContext: cmdCtx, 627 } 628 out := client.HasLocalBranch(context.Background(), "trunk") 629 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 630 assert.Equal(t, out, tt.wantOut) 631 }) 632 } 633 } 634 635 func TestClientCheckoutBranch(t *testing.T) { 636 tests := []struct { 637 name string 638 cmdExitStatus int 639 cmdStdout string 640 cmdStderr string 641 wantCmdArgs string 642 wantErrorMsg string 643 }{ 644 { 645 name: "checkout branch", 646 wantCmdArgs: `path/to/git checkout trunk`, 647 }, 648 { 649 name: "git error", 650 cmdExitStatus: 1, 651 cmdStderr: "git error message", 652 wantCmdArgs: `path/to/git checkout trunk`, 653 wantErrorMsg: "failed to run git: git error message", 654 }, 655 } 656 for _, tt := range tests { 657 t.Run(tt.name, func(t *testing.T) { 658 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 659 client := Client{ 660 GitPath: "path/to/git", 661 commandContext: cmdCtx, 662 } 663 err := client.CheckoutBranch(context.Background(), "trunk") 664 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 665 if tt.wantErrorMsg == "" { 666 assert.NoError(t, err) 667 } else { 668 assert.EqualError(t, err, tt.wantErrorMsg) 669 } 670 }) 671 } 672 } 673 674 func TestClientCheckoutNewBranch(t *testing.T) { 675 tests := []struct { 676 name string 677 cmdExitStatus int 678 cmdStdout string 679 cmdStderr string 680 wantCmdArgs string 681 wantErrorMsg string 682 }{ 683 { 684 name: "checkout new branch", 685 wantCmdArgs: `path/to/git checkout -b trunk --track origin/trunk`, 686 }, 687 { 688 name: "git error", 689 cmdExitStatus: 1, 690 cmdStderr: "git error message", 691 wantCmdArgs: `path/to/git checkout -b trunk --track origin/trunk`, 692 wantErrorMsg: "failed to run git: git error message", 693 }, 694 } 695 for _, tt := range tests { 696 t.Run(tt.name, func(t *testing.T) { 697 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 698 client := Client{ 699 GitPath: "path/to/git", 700 commandContext: cmdCtx, 701 } 702 err := client.CheckoutNewBranch(context.Background(), "origin", "trunk") 703 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 704 if tt.wantErrorMsg == "" { 705 assert.NoError(t, err) 706 } else { 707 assert.EqualError(t, err, tt.wantErrorMsg) 708 } 709 }) 710 } 711 } 712 713 func TestClientToplevelDir(t *testing.T) { 714 tests := []struct { 715 name string 716 cmdExitStatus int 717 cmdStdout string 718 cmdStderr string 719 wantCmdArgs string 720 wantDir string 721 wantErrorMsg string 722 }{ 723 { 724 name: "top level dir", 725 cmdStdout: "/path/to/repo", 726 wantCmdArgs: `path/to/git rev-parse --show-toplevel`, 727 wantDir: "/path/to/repo", 728 }, 729 { 730 name: "git error", 731 cmdExitStatus: 1, 732 cmdStderr: "git error message", 733 wantCmdArgs: `path/to/git rev-parse --show-toplevel`, 734 wantErrorMsg: "failed to run git: git error message", 735 }, 736 } 737 for _, tt := range tests { 738 t.Run(tt.name, func(t *testing.T) { 739 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 740 client := Client{ 741 GitPath: "path/to/git", 742 commandContext: cmdCtx, 743 } 744 dir, err := client.ToplevelDir(context.Background()) 745 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 746 if tt.wantErrorMsg == "" { 747 assert.NoError(t, err) 748 } else { 749 assert.EqualError(t, err, tt.wantErrorMsg) 750 } 751 assert.Equal(t, tt.wantDir, dir) 752 }) 753 } 754 } 755 756 func TestClientGitDir(t *testing.T) { 757 tests := []struct { 758 name string 759 cmdExitStatus int 760 cmdStdout string 761 cmdStderr string 762 wantCmdArgs string 763 wantDir string 764 wantErrorMsg string 765 }{ 766 { 767 name: "git dir", 768 cmdStdout: "/path/to/repo/.git", 769 wantCmdArgs: `path/to/git rev-parse --git-dir`, 770 wantDir: "/path/to/repo/.git", 771 }, 772 { 773 name: "git error", 774 cmdExitStatus: 1, 775 cmdStderr: "git error message", 776 wantCmdArgs: `path/to/git rev-parse --git-dir`, 777 wantErrorMsg: "failed to run git: git error message", 778 }, 779 } 780 for _, tt := range tests { 781 t.Run(tt.name, func(t *testing.T) { 782 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 783 client := Client{ 784 GitPath: "path/to/git", 785 commandContext: cmdCtx, 786 } 787 dir, err := client.GitDir(context.Background()) 788 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 789 if tt.wantErrorMsg == "" { 790 assert.NoError(t, err) 791 } else { 792 assert.EqualError(t, err, tt.wantErrorMsg) 793 } 794 assert.Equal(t, tt.wantDir, dir) 795 }) 796 } 797 } 798 799 func TestClientPathFromRoot(t *testing.T) { 800 tests := []struct { 801 name string 802 cmdExitStatus int 803 cmdStdout string 804 cmdStderr string 805 wantCmdArgs string 806 wantErrorMsg string 807 wantDir string 808 }{ 809 { 810 name: "current path from root", 811 cmdStdout: "some/path/", 812 wantCmdArgs: `path/to/git rev-parse --show-prefix`, 813 wantDir: "some/path", 814 }, 815 { 816 name: "git error", 817 cmdExitStatus: 1, 818 cmdStderr: "git error message", 819 wantCmdArgs: `path/to/git rev-parse --show-prefix`, 820 wantDir: "", 821 }, 822 } 823 for _, tt := range tests { 824 t.Run(tt.name, func(t *testing.T) { 825 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 826 client := Client{ 827 GitPath: "path/to/git", 828 commandContext: cmdCtx, 829 } 830 dir := client.PathFromRoot(context.Background()) 831 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 832 assert.Equal(t, tt.wantDir, dir) 833 }) 834 } 835 } 836 837 func TestClientFetch(t *testing.T) { 838 tests := []struct { 839 name string 840 mods []CommandModifier 841 cmdExitStatus int 842 cmdStdout string 843 cmdStderr string 844 wantCmdArgs string 845 wantErrorMsg string 846 }{ 847 { 848 name: "fetch", 849 wantCmdArgs: `path/to/git fetch origin trunk`, 850 }, 851 { 852 name: "accepts command modifiers", 853 mods: []CommandModifier{WithRepoDir("/path/to/repo")}, 854 wantCmdArgs: `path/to/git -C /path/to/repo fetch origin trunk`, 855 }, 856 { 857 name: "git error", 858 cmdExitStatus: 1, 859 cmdStderr: "git error message", 860 wantCmdArgs: `path/to/git fetch origin trunk`, 861 wantErrorMsg: "failed to run git: git error message", 862 }, 863 } 864 for _, tt := range tests { 865 t.Run(tt.name, func(t *testing.T) { 866 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 867 client := Client{ 868 GitPath: "path/to/git", 869 commandContext: cmdCtx, 870 } 871 err := client.Fetch(context.Background(), "origin", "trunk", tt.mods...) 872 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 873 if tt.wantErrorMsg == "" { 874 assert.NoError(t, err) 875 } else { 876 assert.EqualError(t, err, tt.wantErrorMsg) 877 } 878 }) 879 } 880 } 881 882 func TestClientPull(t *testing.T) { 883 tests := []struct { 884 name string 885 mods []CommandModifier 886 cmdExitStatus int 887 cmdStdout string 888 cmdStderr string 889 wantCmdArgs string 890 wantErrorMsg string 891 }{ 892 { 893 name: "pull", 894 wantCmdArgs: `path/to/git pull --ff-only origin trunk`, 895 }, 896 { 897 name: "accepts command modifiers", 898 mods: []CommandModifier{WithRepoDir("/path/to/repo")}, 899 wantCmdArgs: `path/to/git -C /path/to/repo pull --ff-only origin trunk`, 900 }, 901 { 902 name: "git error", 903 cmdExitStatus: 1, 904 cmdStderr: "git error message", 905 wantCmdArgs: `path/to/git pull --ff-only origin trunk`, 906 wantErrorMsg: "failed to run git: git error message", 907 }, 908 } 909 for _, tt := range tests { 910 t.Run(tt.name, func(t *testing.T) { 911 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 912 client := Client{ 913 GitPath: "path/to/git", 914 commandContext: cmdCtx, 915 } 916 err := client.Pull(context.Background(), "origin", "trunk", tt.mods...) 917 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 918 if tt.wantErrorMsg == "" { 919 assert.NoError(t, err) 920 } else { 921 assert.EqualError(t, err, tt.wantErrorMsg) 922 } 923 }) 924 } 925 } 926 927 func TestClientPush(t *testing.T) { 928 tests := []struct { 929 name string 930 mods []CommandModifier 931 cmdExitStatus int 932 cmdStdout string 933 cmdStderr string 934 wantCmdArgs string 935 wantErrorMsg string 936 }{ 937 { 938 name: "push", 939 wantCmdArgs: `path/to/git push --set-upstream origin trunk`, 940 }, 941 { 942 name: "accepts command modifiers", 943 mods: []CommandModifier{WithRepoDir("/path/to/repo")}, 944 wantCmdArgs: `path/to/git -C /path/to/repo push --set-upstream origin trunk`, 945 }, 946 { 947 name: "git error", 948 cmdExitStatus: 1, 949 cmdStderr: "git error message", 950 wantCmdArgs: `path/to/git push --set-upstream origin trunk`, 951 wantErrorMsg: "failed to run git: git error message", 952 }, 953 } 954 for _, tt := range tests { 955 t.Run(tt.name, func(t *testing.T) { 956 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 957 client := Client{ 958 GitPath: "path/to/git", 959 commandContext: cmdCtx, 960 } 961 err := client.Push(context.Background(), "origin", "trunk", tt.mods...) 962 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 963 if tt.wantErrorMsg == "" { 964 assert.NoError(t, err) 965 } else { 966 assert.EqualError(t, err, tt.wantErrorMsg) 967 } 968 }) 969 } 970 } 971 972 func TestClientClone(t *testing.T) { 973 tests := []struct { 974 name string 975 mods []CommandModifier 976 cmdExitStatus int 977 cmdStdout string 978 cmdStderr string 979 wantCmdArgs string 980 wantTarget string 981 wantErrorMsg string 982 }{ 983 { 984 name: "clone", 985 wantCmdArgs: `path/to/git clone github.com/ungtb10d/cli`, 986 wantTarget: "cli", 987 }, 988 { 989 name: "accepts command modifiers", 990 mods: []CommandModifier{WithRepoDir("/path/to/repo")}, 991 wantCmdArgs: `path/to/git -C /path/to/repo clone github.com/ungtb10d/cli`, 992 wantTarget: "cli", 993 }, 994 { 995 name: "git error", 996 cmdExitStatus: 1, 997 cmdStderr: "git error message", 998 wantCmdArgs: `path/to/git clone github.com/ungtb10d/cli`, 999 wantErrorMsg: "failed to run git: git error message", 1000 }, 1001 } 1002 for _, tt := range tests { 1003 t.Run(tt.name, func(t *testing.T) { 1004 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 1005 client := Client{ 1006 GitPath: "path/to/git", 1007 commandContext: cmdCtx, 1008 } 1009 target, err := client.Clone(context.Background(), "github.com/ungtb10d/cli", []string{}, tt.mods...) 1010 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 1011 if tt.wantErrorMsg == "" { 1012 assert.NoError(t, err) 1013 } else { 1014 assert.EqualError(t, err, tt.wantErrorMsg) 1015 } 1016 assert.Equal(t, tt.wantTarget, target) 1017 }) 1018 } 1019 } 1020 1021 func TestParseCloneArgs(t *testing.T) { 1022 type wanted struct { 1023 args []string 1024 dir string 1025 } 1026 tests := []struct { 1027 name string 1028 args []string 1029 want wanted 1030 }{ 1031 { 1032 name: "args and target", 1033 args: []string{"target_directory", "-o", "upstream", "--depth", "1"}, 1034 want: wanted{ 1035 args: []string{"-o", "upstream", "--depth", "1"}, 1036 dir: "target_directory", 1037 }, 1038 }, 1039 { 1040 name: "only args", 1041 args: []string{"-o", "upstream", "--depth", "1"}, 1042 want: wanted{ 1043 args: []string{"-o", "upstream", "--depth", "1"}, 1044 dir: "", 1045 }, 1046 }, 1047 { 1048 name: "only target", 1049 args: []string{"target_directory"}, 1050 want: wanted{ 1051 args: []string{}, 1052 dir: "target_directory", 1053 }, 1054 }, 1055 { 1056 name: "no args", 1057 args: []string{}, 1058 want: wanted{ 1059 args: []string{}, 1060 dir: "", 1061 }, 1062 }, 1063 } 1064 for _, tt := range tests { 1065 t.Run(tt.name, func(t *testing.T) { 1066 args, dir := parseCloneArgs(tt.args) 1067 got := wanted{args: args, dir: dir} 1068 assert.Equal(t, got, tt.want) 1069 }) 1070 } 1071 } 1072 1073 func TestClientAddRemote(t *testing.T) { 1074 tests := []struct { 1075 title string 1076 name string 1077 url string 1078 branches []string 1079 dir string 1080 cmdExitStatus int 1081 cmdStdout string 1082 cmdStderr string 1083 wantCmdArgs string 1084 wantErrorMsg string 1085 }{ 1086 { 1087 title: "fetch all", 1088 name: "test", 1089 url: "URL", 1090 dir: "DIRECTORY", 1091 branches: []string{}, 1092 wantCmdArgs: `path/to/git -C DIRECTORY remote add -f test URL`, 1093 }, 1094 { 1095 title: "fetch specific branches only", 1096 name: "test", 1097 url: "URL", 1098 dir: "DIRECTORY", 1099 branches: []string{"trunk", "dev"}, 1100 wantCmdArgs: `path/to/git -C DIRECTORY remote add -t trunk -t dev -f test URL`, 1101 }, 1102 } 1103 for _, tt := range tests { 1104 t.Run(tt.title, func(t *testing.T) { 1105 cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) 1106 client := Client{ 1107 GitPath: "path/to/git", 1108 RepoDir: tt.dir, 1109 commandContext: cmdCtx, 1110 } 1111 _, err := client.AddRemote(context.Background(), tt.name, tt.url, tt.branches) 1112 assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) 1113 assert.NoError(t, err) 1114 }) 1115 } 1116 } 1117 1118 func initRepo(t *testing.T, dir string) { 1119 errBuf := &bytes.Buffer{} 1120 inBuf := &bytes.Buffer{} 1121 outBuf := &bytes.Buffer{} 1122 client := Client{ 1123 RepoDir: dir, 1124 Stderr: errBuf, 1125 Stdin: inBuf, 1126 Stdout: outBuf, 1127 } 1128 cmd, err := client.Command(context.Background(), []string{"init", "--quiet"}...) 1129 assert.NoError(t, err) 1130 _, err = cmd.Output() 1131 assert.NoError(t, err) 1132 } 1133 1134 func TestHelperProcess(t *testing.T) { 1135 if os.Getenv("GH_WANT_HELPER_PROCESS") != "1" { 1136 return 1137 } 1138 if err := func(args []string) error { 1139 fmt.Fprint(os.Stdout, os.Getenv("GH_HELPER_PROCESS_STDOUT")) 1140 exitStatus := os.Getenv("GH_HELPER_PROCESS_EXIT_STATUS") 1141 if exitStatus != "0" { 1142 return errors.New("error") 1143 } 1144 return nil 1145 }(os.Args[3:]); err != nil { 1146 fmt.Fprint(os.Stderr, os.Getenv("GH_HELPER_PROCESS_STDERR")) 1147 exitStatus := os.Getenv("GH_HELPER_PROCESS_EXIT_STATUS") 1148 i, err := strconv.Atoi(exitStatus) 1149 if err != nil { 1150 os.Exit(1) 1151 } 1152 os.Exit(i) 1153 } 1154 os.Exit(0) 1155 } 1156 1157 func createCommandContext(t *testing.T, exitStatus int, stdout, stderr string) (*exec.Cmd, commandCtx) { 1158 cmd := exec.CommandContext(context.Background(), os.Args[0], "-test.run=TestHelperProcess", "--") 1159 cmd.Env = []string{ 1160 "GH_WANT_HELPER_PROCESS=1", 1161 fmt.Sprintf("GH_HELPER_PROCESS_STDOUT=%s", stdout), 1162 fmt.Sprintf("GH_HELPER_PROCESS_STDERR=%s", stderr), 1163 fmt.Sprintf("GH_HELPER_PROCESS_EXIT_STATUS=%v", exitStatus), 1164 } 1165 return cmd, func(ctx context.Context, exe string, args ...string) *exec.Cmd { 1166 cmd.Args = append(cmd.Args, exe) 1167 cmd.Args = append(cmd.Args, args...) 1168 return cmd 1169 } 1170 }