diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2023-07-29 17:02:04 +1000 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2023-07-30 18:35:36 +1000 |
commit | 7b302d8c298ca2eb7e08d63f993186d25252f97e (patch) | |
tree | ef50114f45816c9dc16cfa6e827cbd74fce38469 /pkg/commands/git_commands | |
parent | a1fae4105116775b5c4a0c9c3b01fa822b29cfac (diff) |
Write unit tests with the help of afero
Afero is a package that lets you mock out a filesystem with an in-memory filesystem.
It allows us to easily create the files required for a given test without worrying about
a cleanup step or different tests tripping on eachother when run in parallel.
Later on I'll standardise on using afero over the vanilla os package
Diffstat (limited to 'pkg/commands/git_commands')
-rw-r--r-- | pkg/commands/git_commands/common.go | 4 | ||||
-rw-r--r-- | pkg/commands/git_commands/deps_test.go | 21 | ||||
-rw-r--r-- | pkg/commands/git_commands/file_loader.go | 2 | ||||
-rw-r--r-- | pkg/commands/git_commands/file_loader_test.go | 3 | ||||
-rw-r--r-- | pkg/commands/git_commands/paths.go | 194 | ||||
-rw-r--r-- | pkg/commands/git_commands/paths_test.go | 118 | ||||
-rw-r--r-- | pkg/commands/git_commands/worktree_loader.go | 38 | ||||
-rw-r--r-- | pkg/commands/git_commands/worktree_loader_test.go | 186 |
8 files changed, 439 insertions, 127 deletions
diff --git a/pkg/commands/git_commands/common.go b/pkg/commands/git_commands/common.go index b4c8bea32..cf8250863 100644 --- a/pkg/commands/git_commands/common.go +++ b/pkg/commands/git_commands/common.go @@ -12,7 +12,7 @@ type GitCommon struct { version *GitVersion cmd oscommands.ICmdObjBuilder os *oscommands.OSCommand - repoPaths RepoPaths + repoPaths *RepoPaths repo *gogit.Repository config *ConfigCommands // mutex for doing things like push/pull/fetch @@ -24,7 +24,7 @@ func NewGitCommon( version *GitVersion, cmd oscommands.ICmdObjBuilder, osCommand *oscommands.OSCommand, - repoPaths RepoPaths, + repoPaths *RepoPaths, repo *gogit.Repository, config *ConfigCommands, syncMutex *deadlock.Mutex, diff --git a/pkg/commands/git_commands/deps_test.go b/pkg/commands/git_commands/deps_test.go index a960098df..bb17ad0a7 100644 --- a/pkg/commands/git_commands/deps_test.go +++ b/pkg/commands/git_commands/deps_test.go @@ -11,6 +11,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/spf13/afero" ) type commonDeps struct { @@ -20,9 +21,10 @@ type commonDeps struct { gitConfig *git_config.FakeGitConfig getenv func(string) string removeFile func(string) error - dotGitDir string common *common.Common cmd *oscommands.CmdObjBuilder + fs afero.Fs + repoPaths *RepoPaths } func buildGitCommon(deps commonDeps) *GitCommon { @@ -33,6 +35,16 @@ func buildGitCommon(deps commonDeps) *GitCommon { gitCommon.Common = utils.NewDummyCommonWithUserConfig(deps.userConfig) } + if deps.fs != nil { + gitCommon.Fs = deps.fs + } + + if deps.repoPaths != nil { + gitCommon.repoPaths = deps.repoPaths + } else { + gitCommon.repoPaths = MockRepoPaths(".git") + } + runner := deps.runner if runner == nil { runner = oscommands.NewFakeRunner(nil) @@ -81,11 +93,6 @@ func buildGitCommon(deps commonDeps) *GitCommon { TempDir: os.TempDir(), }) - gitCommon.dotGitDir = deps.dotGitDir - if gitCommon.dotGitDir == "" { - gitCommon.dotGitDir = ".git" - } - return gitCommon } @@ -96,7 +103,7 @@ func buildRepo() *gogit.Repository { } func buildFileLoader(gitCommon *GitCommon) *FileLoader { - return NewFileLoader(gitCommon.Common, gitCommon.cmd, gitCommon.config) + return NewFileLoader(gitCommon, gitCommon.cmd, gitCommon.config) } func buildSubmoduleCommands(deps commonDeps) *SubmoduleCommands { diff --git a/pkg/commands/git_commands/file_loader.go b/pkg/commands/git_commands/file_loader.go index 574a8a6f0..73d7fdc64 100644 --- a/pkg/commands/git_commands/file_loader.go +++ b/pkg/commands/git_commands/file_loader.go @@ -66,7 +66,7 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File // Go through the files to see if any of these files are actually worktrees // so that we can render them correctly - worktreePaths := linkedWortkreePaths(self.repoPaths.RepoGitDirPath()) + worktreePaths := linkedWortkreePaths(self.Fs, self.repoPaths.RepoGitDirPath()) for _, file := range files { for _, worktreePath := range worktreePaths { absFilePath, err := filepath.Abs(file.Name) diff --git a/pkg/commands/git_commands/file_loader_test.go b/pkg/commands/git_commands/file_loader_test.go index ae149c2f2..f7efe68b8 100644 --- a/pkg/commands/git_commands/file_loader_test.go +++ b/pkg/commands/git_commands/file_loader_test.go @@ -5,7 +5,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" - "github.com/jesseduffield/lazygit/pkg/utils" "github.com/stretchr/testify/assert" ) @@ -178,7 +177,7 @@ func TestFileGetStatusFiles(t *testing.T) { cmd := oscommands.NewDummyCmdObjBuilder(s.runner) loader := &FileLoader{ - Common: utils.NewDummyCommon(), + GitCommon: buildGitCommon(commonDeps{}), cmd: cmd, config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"}, getFileType: func(string) string { return "file" }, diff --git a/pkg/commands/git_commands/paths.go b/pkg/commands/git_commands/paths.go index a4f5ccb74..46fcb5d10 100644 --- a/pkg/commands/git_commands/paths.go +++ b/pkg/commands/git_commands/paths.go @@ -2,7 +2,7 @@ package git_commands import ( "fmt" - "io/fs" + ioFs "io/fs" "os" "path" "path/filepath" @@ -11,33 +11,10 @@ import ( "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/env" "github.com/samber/lo" + "github.com/spf13/afero" ) -type RepoPaths interface { - // Current working directory of the program. Currently, this will always - // be the same as WorktreePath(), but in future we may support running - // lazygit from inside a subdirectory of the worktree. - CurrentPath() string - // Path to the current worktree. If we're in the main worktree, this will - // be the same as RepoPath() - WorktreePath() string - // Path of the worktree's git dir. - // If we're in the main worktree, this will be the .git dir under the RepoPath(). - // If we're in a linked worktree, it will be the directory pointed at by the worktree's .git file - WorktreeGitDirPath() string - // Path of the repo. If we're in a the main worktree, this will be the same as WorktreePath() - // If we're in a bare repo, it will be the parent folder of the bare repo - RepoPath() string - // path of the git-dir for the repo. - // If this is a bare repo, it will be the location of the bare repo - // If this is a non-bare repo, it will be the location of the .git dir in - // the main worktree. - RepoGitDirPath() string - // Name of the repo. Basename of the folder containing the repo. - RepoName() string -} - -type RepoDirsImpl struct { +type RepoPaths struct { currentPath string worktreePath string worktreeGitDirPath string @@ -46,54 +23,81 @@ type RepoDirsImpl struct { repoName string } -var _ RepoPaths = &RepoDirsImpl{} - -func (self *RepoDirsImpl) CurrentPath() string { +// Current working directory of the program. Currently, this will always +// be the same as WorktreePath(), but in future we may support running +// lazygit from inside a subdirectory of the worktree. +func (self *RepoPaths) CurrentPath() string { return self.currentPath } -func (self *RepoDirsImpl) WorktreePath() string { +// Path to the current worktree. If we're in the main worktree, this will +// be the same as RepoPath() +func (self *RepoPaths) WorktreePath() string { return self.worktreePath } -func (self *RepoDirsImpl) WorktreeGitDirPath() string { +// Path of the worktree's git dir. +// If we're in the main worktree, this will be the .git dir under the RepoPath(). +// If we're in a linked worktree, it will be the directory pointed at by the worktree's .git file +func (self *RepoPaths) WorktreeGitDirPath() string { return self.worktreeGitDirPath } -func (self *RepoDirsImpl) RepoPath() string { +// Path of the repo. If we're in a the main worktree, this will be the same as WorktreePath() +// If we're in a bare repo, it will be the parent folder of the bare repo +func (self *RepoPaths) RepoPath() string { return self.repoPath } -func (self *RepoDirsImpl) RepoGitDirPath() string { +// path of the git-dir for the repo. +// If this is a bare repo, it will be the location of the bare repo +// If this is a non-bare repo, it will be the location of the .git dir in +// the main worktree. +func (self *RepoPaths) RepoGitDirPath() string { return self.repoGitDirPath } -func (self *RepoDirsImpl) RepoName() string { +// Name of the repo. Basename of the folder containing the repo. +func (self *RepoPaths) RepoName() string { return self.repoName } -func GetRepoPaths() (RepoPaths, error) { - currentPath, err := os.Getwd() - if err != nil { - return &RepoDirsImpl{}, errors.Errorf("failed to get current path: %v", err) +// Returns the repo paths for a typical repo +func MockRepoPaths(currentPath string) *RepoPaths { + return &RepoPaths{ + currentPath: currentPath, + worktreePath: currentPath, + worktreeGitDirPath: path.Join(currentPath, ".git"), + repoPath: currentPath, + repoGitDirPath: path.Join(currentPath, ".git"), + repoName: "lazygit", } +} - // converting to forward slashes for the sake of windows (which uses backwards slashes). We want everything - // to have forward slashes internally - currentPath = filepath.ToSlash(currentPath) +func GetRepoPaths( + fs afero.Fs, + currentPath string, +) (*RepoPaths, error) { + return getRepoPathsAux(afero.NewOsFs(), resolveSymlink, currentPath) +} +func getRepoPathsAux( + fs afero.Fs, + resolveSymlinkFn func(string) (string, error), + currentPath string, +) (*RepoPaths, error) { worktreePath := currentPath - repoGitDirPath, repoPath, err := GetCurrentRepoGitDirPath(currentPath) + repoGitDirPath, repoPath, err := getCurrentRepoGitDirPath(fs, resolveSymlinkFn, currentPath) if err != nil { - return &RepoDirsImpl{}, errors.Errorf("failed to get repo git dir path: %v", err) + return nil, errors.Errorf("failed to get repo git dir path: %v", err) } - worktreeGitDirPath, err := worktreeGitDirPath(currentPath) + worktreeGitDirPath, err := worktreeGitDirPath(fs, currentPath) if err != nil { - return &RepoDirsImpl{}, errors.Errorf("failed to get worktree git dir path: %v", err) + return nil, errors.Errorf("failed to get worktree git dir path: %v", err) } repoName := path.Base(repoPath) - return &RepoDirsImpl{ + return &RepoPaths{ currentPath: currentPath, worktreePath: worktreePath, worktreeGitDirPath: worktreeGitDirPath, @@ -103,52 +107,14 @@ func GetRepoPaths() (RepoPaths, error) { }, nil } -// Returns the paths of linked worktrees -func linkedWortkreePaths(repoGitDirPath string) []string { - result := []string{} - // For each directory in this path we're going to cat the `gitdir` file and append its contents to our result - // That file points us to the `.git` file in the worktree. - worktreeGitDirsPath := path.Join(repoGitDirPath, "worktrees") - - // ensure the directory exists - _, err := os.Stat(worktreeGitDirsPath) - if err != nil { - return result - } - - _ = filepath.Walk(worktreeGitDirsPath, func(currPath string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - - if !info.IsDir() { - return nil - } - - gitDirPath := path.Join(currPath, "gitdir") - gitDirBytes, err := os.ReadFile(gitDirPath) - if err != nil { - // ignoring error - return nil - } - trimmedGitDir := strings.TrimSpace(string(gitDirBytes)) - // removing the .git part - worktreeDir := path.Dir(trimmedGitDir) - result = append(result, worktreeDir) - return nil - }) - - return result -} - // Returns the path of the git-dir for the worktree. For linked worktrees, the worktree has // a .git file that points to the git-dir (which itself lives in the git-dir // of the repo) -func worktreeGitDirPath(worktreePath string) (string, error) { +func worktreeGitDirPath(fs afero.Fs, worktreePath string) (string, error) { // if .git is a file, we're in a linked worktree, otherwise we're in // the main worktree dotGitPath := path.Join(worktreePath, ".git") - gitFileInfo, err := os.Stat(dotGitPath) + gitFileInfo, err := fs.Stat(dotGitPath) if err != nil { return "", err } @@ -157,12 +123,12 @@ func worktreeGitDirPath(worktreePath string) (string, error) { return dotGitPath, nil } - return linkedWorktreeGitDirPath(worktreePath) + return linkedWorktreeGitDirPath(fs, worktreePath) } -func linkedWorktreeGitDirPath(worktreePath string) (string, error) { +func linkedWorktreeGitDirPath(fs afero.Fs, worktreePath string) (string, error) { dotGitPath := path.Join(worktreePath, ".git") - gitFileContents, err := os.ReadFile(dotGitPath) + gitFileContents, err := afero.ReadFile(fs, dotGitPath) if err != nil { return "", err } @@ -180,7 +146,11 @@ func linkedWorktreeGitDirPath(worktreePath string) (string, error) { return gitDir, nil } -func GetCurrentRepoGitDirPath(currentPath string) (string, string, error) { +func getCurrentRepoGitDirPath( + fs afero.Fs, + resolveSymlinkFn func(string) (string, error), + currentPath string, +) (string, string, error) { var unresolvedGitPath string if env.GetGitDirEnv() != "" { unresolvedGitPath = env.GetGitDirEnv() @@ -188,13 +158,13 @@ func GetCurrentRepoGitDirPath(currentPath string) (string, string, error) { unresolvedGitPath = path.Join(currentPath, ".git") } - gitPath, err := resolveSymlink(unresolvedGitPath) + gitPath, err := resolveSymlinkFn(unresolvedGitPath) if err != nil { return "", "", err } // check if .git is a file or a directory - gitFileInfo, err := os.Stat(gitPath) + gitFileInfo, err := fs.Stat(gitPath) if err != nil { return "", "", err } @@ -205,7 +175,7 @@ func GetCurrentRepoGitDirPath(currentPath string) (string, string, error) { } // either in a submodule, or worktree - worktreeGitPath, err := linkedWorktreeGitDirPath(currentPath) + worktreeGitPath, err := linkedWorktreeGitDirPath(fs, currentPath) if err != nil { return "", "", errors.Errorf("could not find git dir for %s: %v", currentPath, err) } @@ -238,3 +208,41 @@ func resolveSymlink(path string) (string, error) { return filepath.EvalSymlinks(path) } + +// Returns the paths of linked worktrees +func linkedWortkreePaths(fs afero.Fs, repoGitDirPath string) []string { + result := []string{} + // For each directory in this path we're going to cat the `gitdir` file and append its contents to our result + // That file points us to the `.git` file in the worktree. + worktreeGitDirsPath := path.Join(repoGitDirPath, "worktrees") + + // ensure the directory exists + _, err := fs.Stat(worktreeGitDirsPath) + if err != nil { + return result + } + + _ = afero.Walk(fs, worktreeGitDirsPath, func(currPath string, info ioFs.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() { + return nil + } + + gitDirPath := path.Join(currPath, "gitdir") + gitDirBytes, err := afero.ReadFile(fs, gitDirPath) + if err != nil { + // ignoring error + return nil + } + trimmedGitDir := strings.TrimSpace(string(gitDirBytes)) + // removing the .git part + worktreeDir := path.Dir(trimmedGitDir) + result = append(result, worktreeDir) + return nil + }) + + return result +} diff --git a/pkg/commands/git_commands/paths_test.go b/pkg/commands/git_commands/paths_test.go new file mode 100644 index 000000000..52ad9bd8f --- /dev/null +++ b/pkg/commands/git_commands/paths_test.go @@ -0,0 +1,118 @@ +package git_commands + +import ( + "testing" + + "github.com/go-errors/errors" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" +) + +func mockResolveSymlinkFn(p string) (string, error) { return p, nil } + +type Scenario struct { + Name string + BeforeFunc func(fs afero.Fs) + Path string + Expected *RepoPaths + Err error +} + +func TestGetRepoPathsAux(t *testing.T) { + scenarios := []Scenario{ + { + Name: "typical case", + BeforeFunc: func(fs afero.Fs) { + // setup for main worktree + _ = fs.MkdirAll("/path/to/repo/.git", 0o755) + }, + Path: "/path/to/repo", + Expected: &RepoPaths{ + currentPath: "/path/to/repo", + worktreePath: "/path/to/repo", + worktreeGitDirPath: "/path/to/repo/.git", + repoPath: "/path/to/repo", + repoGitDirPath: "/path/to/repo/.git", + repoName: "repo", + }, + Err: nil, + }, + { + Name: "linked worktree", + BeforeFunc: func(fs afero.Fs) { + // setup for linked worktree + _ = fs.MkdirAll("/path/to/repo/.git/worktrees/worktree1", 0o755) + _ = afero.WriteFile(fs, "/path/to/repo/worktree1/.git", []byte("gitdir: /path/to/repo/.git/worktrees/worktree1"), 0o644) + }, + Path: "/path/to/repo/worktree1", + Expected: &RepoPaths{ + currentPath: "/path/to/repo/worktree1", + worktreePath: "/path/to/repo/worktree1", + worktreeGitDirPath: "/path/to/repo/.git/worktrees/worktree1", + repoPath: "/path/to/repo", + repoGitDirPath: "/path/to/repo/.git", + repoName: "repo", + }, + Err: nil, + }, + { + Name: "worktree .git file missing gitdir directive", + BeforeFunc: func(fs afero.Fs) { + _ = fs.MkdirAll("/path/to/repo/.git/worktrees/worktree2", 0o755) + _ = afero.WriteFile(fs, "/path/to/repo/worktree2/.git", []byte("blah"), 0o644) + }, + Path: "/path/to/repo/worktree2", + Expected: nil, + Err: errors.New("failed to get repo git dir path: could not find git dir for /path/to/repo/worktree2: /path/to/repo/worktree2/.git is a file which suggests we are in a submodule or a worktree but the file's contents do not contain a gitdir pointing to the actual .git directory"), + }, + { + Name: "worktree .git file gitdir directive points to a non-existing directory", + BeforeFunc: func(fs afero.Fs) { + _ = fs.MkdirAll("/path/to/repo/.git/worktrees/worktree2", 0o755) + _ = afero.WriteFile(fs, "/path/to/repo/worktree2/.git", []byte("gitdir: /nonexistant"), 0o644) + }, + Path: "/path/to/repo/worktree2", + Expected: nil, + Err: errors.New("failed to get repo git dir path: could not find git dir for /path/to/repo/worktree2"), + }, + { + Name: "submodule", + BeforeFunc: func(fs afero.Fs) { + _ = fs.MkdirAll("/path/to/repo/.git/modules/submodule1", 0o755) + _ = afero.WriteFile(fs, "/path/to/repo/submodule1/.git", []byte("gitdir: /path/to/repo/.git/modules/submodule1"), 0o644) + }, + Path: "/path/to/repo/submodule1", + Expected: &RepoPaths{ + currentPath: "/path/to/repo/submodule1", + worktreePath: "/path/to/repo/submodule1", + worktreeGitDirPath: "/path/to/repo/.git/modules/submodule1", + repoPath: "/path/to/repo/submodule1", + repoGitDirPath: "/path/to/repo/.git/modules/submodule1", + repoName: "submodule1", + }, + Err: nil, + }, + } + + for _, s := range scenarios { + s := s + t.Run(s.Name, func(t *testing.T) { + fs := afero.NewMemMapFs() + + // prepare the filesystem for the scenario + s.BeforeFunc(fs) + + // run the function with the scenario path + repoPaths, err := getRepoPathsAux(fs, mockResolveSymlinkFn, s.Path) + + // check the error and the paths + if s.Err != nil { + assert.Error(t, err) + assert.EqualError(t, err, s.Err.Error()) + } else { + assert.Nil(t, err) + assert.Equal(t, s.Expected, repoPaths) + } + }) + } +} diff --git a/pkg/commands/git_commands/worktree_loader.go b/pkg/commands/git_commands/worktree_loader.go index d81b23f4b..687e9680a 100644 --- a/pkg/commands/git_commands/worktree_loader.go +++ b/pkg/commands/git_commands/worktree_loader.go @@ -1,31 +1,23 @@ package git_commands import ( - "io/fs" - "os" + iofs "io/fs" "path/filepath" "strings" "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" + "github.com/spf13/afero" ) type WorktreeLoader struct { *GitCommon - cmd oscommands.ICmdObjBuilder } -func NewWorktreeLoader( - gitCommon *GitCommon, - cmd oscommands.ICmdObjBuilder, -) *WorktreeLoader { - return &WorktreeLoader{ - GitCommon: gitCommon, - cmd: cmd, - } +func NewWorktreeLoader(gitCommon *GitCommon) *WorktreeLoader { + return &WorktreeLoader{GitCommon: gitCommon} } func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { @@ -38,7 +30,9 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { return nil, err } - splitLines := utils.SplitLines(worktreesOutput) + splitLines := strings.Split( + utils.NormalizeLinefeeds(worktreesOutput), "\n", + ) var worktrees []*models.Worktree var current *models.Worktree @@ -64,7 +58,7 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { isPathMissing := self.pathExists(path) var gitDir string - gitDir, err := worktreeGitDirPath(path) + gitDir, err := worktreeGitDirPath(self.Fs, path) if err != nil { self.Log.Warnf("Could not find git dir for worktree %s: %v", path, err) } @@ -114,13 +108,13 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { continue } - rebasedBranch, ok := rebasedBranch(worktree) + rebasedBranch, ok := self.rebasedBranch(worktree) if ok { worktree.Branch = rebasedBranch continue } - bisectedBranch, ok := bisectedBranch(worktree) + bisectedBranch, ok := self.bisectedBranch(worktree) if ok { worktree.Branch = bisectedBranch continue @@ -131,8 +125,8 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { } func (self *WorktreeLoader) pathExists(path string) bool { - if _, err := os.Stat(path); err != nil { - if errors.Is(err, fs.ErrNotExist) { + if _, err := self.Fs.Stat(path); err != nil { + if errors.Is(err, iofs.ErrNotExist) { return true } self.Log.Errorf("failed to check if worktree path `%s` exists\n%v", path, err) @@ -141,9 +135,9 @@ func (self *WorktreeLoader) pathExists(path string) bool { return false } -func rebasedBranch(worktree *models.Worktree) (string, bool) { +func (self *WorktreeLoader) rebasedBranch(worktree *models.Worktree) (string, bool) { for _, dir := range []string{"rebase-merge", "rebase-apply"} { - if bytesContent, err := os.ReadFile(filepath.Join(worktree.GitDir, dir, "head-name")); err == nil { + if bytesContent, err := afero.ReadFile(self.Fs, filepath.Join(worktree.GitDir, dir, "head-name")); err == nil { headName := strings.TrimSpace(string(bytesContent)) shortHeadName := strings.TrimPrefix(headName, "refs/heads/") return shortHeadName, true @@ -153,9 +147,9 @@ func rebasedBranch(worktree *models.Worktree) (string, bool) { return "", false } -func bisectedBranch(worktree *models.Worktree) (string, bool) { +func (self *WorktreeLoader) bisectedBranch(worktree *models.Worktree) (string, bool) { bisectStartPath := filepath.Join(worktree.GitDir, "BISECT_START") - startContent, err := os.ReadFile(bisectStartPath) + startContent, err := afero.ReadFile(self.Fs, bisectStartPath) if err != nil { return "", false } diff --git a/pkg/commands/git_commands/worktree_loader_test.go b/pkg/commands/git_commands/worktree_loader_test.go index cf3d2a906..7f805b1b9 100644 --- a/pkg/commands/git_commands/worktree_loader_test.go +++ b/pkg/commands/git_commands/worktree_loader_test.go @@ -3,9 +3,195 @@ package git_commands import ( "testing" + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/spf13/afero" "github.com/stretchr/testify/assert" ) +func TestGetWorktrees(t *testing.T) { + type scenario struct { + testName string + repoPaths *RepoPaths + before func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) + expectedWorktrees []*models.Worktree + expectedErr string + } + + scenarios := []scenario{ + { + testName: "Single worktree (main)", + repoPaths: &RepoPaths{ + repoPath: "/path/to/repo", + worktreePath: "/path/to/repo", + }, + before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) { + runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"}, + `worktree /path/to/repo +HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d +branch refs/heads/mybranch +`, + nil) + + _ = fs.MkdirAll("/path/to/repo/.git", 0o755) + }, + expectedWorktrees: []*models.Worktree{ + { + IsMain: true, + IsCurrent: true, + Path: "/path/to/repo", + IsPathMissing: false, + GitDir: "/path/to/repo/.git", + Branch: "mybranch", + Name: "repo", + }, + }, + expectedErr: "", + }, + { + testName: "Multiple worktrees (main + linked)", + repoPaths: &RepoPaths{ + repoPath: "/path/to/repo", + worktreePath: "/path/to/repo", + }, + before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) { + runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"}, + `worktree /path/to/repo +HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d +branch refs/heads/mybranch + +worktree /path/to/repo-worktree +HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de +branch refs/heads/mybranch-worktree +`, + nil) + + _ = fs.MkdirAll("/path/to/repo/.git", 0o755) + _ = fs.MkdirAll("/path/to/repo-worktree", 0o755) + _ = fs.MkdirAll("/path/to/repo/.git/worktrees/repo-worktree", 0o755) + _ = afero.WriteFile(fs, "/path/to/repo-worktree/.git", []byte("gitdir: /path/to/repo/.git/worktrees/repo-worktree"), 0o755) + }, + expectedWorktrees: []*models.Worktree{ + { + IsMain: true, + IsCurrent: true, + Path: "/path/to/repo", + IsPathMissing: false, + GitDir: "/path/to/repo/.git", + Branch: "mybranch", + Name: "repo", + }, + { + IsMain: false, + IsCurrent: false, + Path: "/path/to/repo-worktree", + IsPathMissing: false, + GitDir: "/path/to/repo/.git/worktrees/repo-worktree", + Branch: "mybranch-worktree", + Name: "repo-worktree", + }, + }, + expectedErr: "", + }, + { + testName: "Worktree missing path", + repoPaths: &RepoPaths{ + repoPath: "/path/to/repo", + worktreePath: "/path/to/repo", + }, + before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) { + runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"}, + `worktree /path/to/worktree +HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de +branch refs/heads/missingbranch +`, + nil) + + _ = fs.MkdirAll("/path/to/repo/.git", 0o755) + }, + expectedWorktrees: []*models.Worktree{ + { + IsMain: false, + IsCurrent: false, + Path: "/path/to/worktree", + IsPathMissing: true, + GitDir: "", + Branch: "missingbranch", + Name: "worktree", + }, + }, + expectedErr: "", + }, + { + testName: "In linked worktree", + repoPaths: &RepoPaths{ + repoPath: "/path/to/repo", + worktreePath: "/path/to/repo-worktree", + }, + before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) { + runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"}, + `worktree /path/to/repo +HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d +branch refs/heads/mybranch + +worktree /path/to/repo-worktree +HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de +branch refs/heads/mybranch-worktree +`, + nil) + + _ = fs.MkdirAll("/path/to/repo/.git", 0o755) + _ = fs.MkdirAll("/path/to/repo-worktree", 0o755) + _ = fs.MkdirAll("/path/to/repo/.git/worktrees/repo-worktree", 0o755) + _ = afero.WriteFile(fs, "/path/to/repo-worktree/.git", []byte("gitdir: /path/to/repo/.git/worktrees/repo-worktree"), 0o755) + }, + expectedWorktrees: []*models.Worktree{ + { + IsMain: false, + IsCurrent: true, + Path: "/path/to/repo-worktree", + IsPathMissing: false, + GitDir: "/path/to/repo/.git/worktrees/repo-worktree", + Branch: "mybranch-worktree", + Name: "repo-worktree", + }, + { + IsMain: true, + IsCurrent: false, + Path: "/path/to/repo", + IsPathMissing: false, + GitDir: "/path/to/repo/.git", + Branch: "mybranch", + Name: "repo", + }, + }, + expectedErr: "", + }, + } + + for _, s := range scenarios { + s := s + t.Run(s.testName, func(t *testing.T) { + runner := oscommands.NewFakeRunner(t) + fs := afero.NewMemMapFs() + s.before(runner, fs) + + loader := &WorktreeLoader{ + GitCommon: buildGitCommon(commonDeps{runner: runner, fs: fs, repoPaths: s.repoPaths}), + } + + worktrees, err := loader.GetWorktrees() + if s.expectedErr != "" { + assert.EqualError(t, errors.New(s.expectedErr), err.Error()) + } else { + assert.NoError(t, err) + assert.EqualValues(t, worktrees, s.expectedWorktrees) + } + }) + } +} + func TestGetUniqueNamesFromPaths(t *testing.T) { for _, scenario := range []struct { input []string |