summaryrefslogtreecommitdiffstats
path: root/pkg/commands/git_commands
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2023-07-29 17:02:04 +1000
committerJesse Duffield <jessedduffield@gmail.com>2023-07-30 18:35:36 +1000
commit7b302d8c298ca2eb7e08d63f993186d25252f97e (patch)
treeef50114f45816c9dc16cfa6e827cbd74fce38469 /pkg/commands/git_commands
parenta1fae4105116775b5c4a0c9c3b01fa822b29cfac (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.go4
-rw-r--r--pkg/commands/git_commands/deps_test.go21
-rw-r--r--pkg/commands/git_commands/file_loader.go2
-rw-r--r--pkg/commands/git_commands/file_loader_test.go3
-rw-r--r--pkg/commands/git_commands/paths.go194
-rw-r--r--pkg/commands/git_commands/paths_test.go118
-rw-r--r--pkg/commands/git_commands/worktree_loader.go38
-rw-r--r--pkg/commands/git_commands/worktree_loader_test.go186
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