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  }