diff --git a/pkg/commands/git.go b/pkg/commands/git.go index c2c53948a..3f7e823e7 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -1,6 +1,7 @@ package commands import ( + "fmt" "os" "path/filepath" "strings" @@ -66,13 +67,16 @@ func NewGitCommand( return nil, err } - repo, err := setupRepository(gogit.PlainOpenWithOptions, gogit.PlainOpenOptions{DetectDotGit: false, EnableDotGitCommonDir: true}, cmn.Tr.GitconfigParseErr) + dotGitDir, err := findDotGitDir(os.Stat, os.ReadFile) if err != nil { return nil, err } - dotGitDir, err := findDotGitDir(os.Stat, os.ReadFile) + repository, err := gogit.PlainOpenWithOptions(dotGitDir, &gogit.PlainOpenOptions{DetectDotGit: false, EnableDotGitCommonDir: true}) if err != nil { + if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) { + return nil, errors.New(cmn.Tr.GitconfigParseErr) + } return nil, err } @@ -82,7 +86,7 @@ func NewGitCommand( osCommand, gitConfig, dotGitDir, - repo, + repository, syncMutex, ), nil } @@ -218,8 +222,8 @@ func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir f } } -// resolvePath takes a path containing a symlink and returns the true path -func resolvePath(path string) (string, error) { +// takes a path containing a symlink and returns the true path +func resolveSymlink(path string) (string, error) { l, err := os.Lstat(path) if err != nil { return "", err @@ -232,27 +236,17 @@ func resolvePath(path string) (string, error) { return filepath.EvalSymlinks(path) } -func setupRepository(openGitRepository func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error), options gogit.PlainOpenOptions, gitConfigParseErrorStr string) (*gogit.Repository, error) { - unresolvedPath := env.GetGitDirEnv() - if unresolvedPath == "" { - var err error - unresolvedPath, err = os.Getwd() - if err != nil { - return nil, err - } - } - - path, err := resolvePath(unresolvedPath) - if err != nil { - return nil, err - } - +func setupRepository( + openGitRepository func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error), + options gogit.PlainOpenOptions, + gitConfigParseErrorStr string, + path string, +) (*gogit.Repository, error) { repository, err := openGitRepository(path, &options) if err != nil { if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) { return nil, errors.New(gitConfigParseErrorStr) } - return nil, err } @@ -260,26 +254,38 @@ func setupRepository(openGitRepository func(string, *gogit.PlainOpenOptions) (*g } func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filename string) ([]byte, error)) (string, error) { - if env.GetGitDirEnv() != "" { - return env.GetGitDirEnv(), nil + unresolvedPath := env.GetGitDirEnv() + if unresolvedPath == "" { + var err error + unresolvedPath, err = os.Getwd() + if err != nil { + return "", err + } + unresolvedPath = filepath.Join(unresolvedPath, ".git") } - f, err := stat(".git") + path, err := resolveSymlink(unresolvedPath) + if err != nil { + return "", err + } + + f, err := stat(path) if err != nil { return "", err } if f.IsDir() { - return ".git", nil + return path, nil } - fileBytes, err := readFile(".git") + fileBytes, err := readFile(path) if err != nil { return "", err } + fileContent := string(fileBytes) if !strings.HasPrefix(fileContent, "gitdir: ") { - return "", errors.New(".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") + return "", errors.New(fmt.Sprintf("%s 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", path)) } return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil } diff --git a/pkg/commands/git_commands/worktree_loader.go b/pkg/commands/git_commands/worktree_loader.go index 6c73eaa13..bbd8367f3 100644 --- a/pkg/commands/git_commands/worktree_loader.go +++ b/pkg/commands/git_commands/worktree_loader.go @@ -46,6 +46,11 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { current = nil continue } + if splitLine == "bare" { + current = nil + continue + } + if strings.HasPrefix(splitLine, "worktree ") { path := strings.SplitN(splitLine, " ", 2)[1] isMain := path == currentRepoPath diff --git a/pkg/integration/components/shell.go b/pkg/integration/components/shell.go index 741135e2f..ce98e575a 100644 --- a/pkg/integration/components/shell.go +++ b/pkg/integration/components/shell.go @@ -85,7 +85,7 @@ func (self *Shell) CreateFile(path string, content string) *Shell { func (self *Shell) DeleteFile(path string) *Shell { fullPath := filepath.Join(self.dir, path) - err := os.Remove(fullPath) + err := os.RemoveAll(fullPath) if err != nil { self.fail(fmt.Sprintf("error deleting file: %s\n%s", fullPath, err)) } @@ -328,3 +328,11 @@ func (self *Shell) CopyFile(source string, destination string) *Shell { return self } + +// NOTE: this only takes effect before running the test; +// the test will still run in the original directory +func (self *Shell) Chdir(path string) *Shell { + self.dir = filepath.Join(self.dir, path) + + return self +} diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 361fa2064..62a4fae29 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -223,6 +223,7 @@ var tests = []*components.IntegrationTest{ worktree.AddFromBranch, worktree.AddFromBranchDetached, worktree.AddFromCommit, + worktree.BareRepo, worktree.Bisect, worktree.Crud, worktree.CustomCommand, diff --git a/pkg/integration/tests/worktree/bare_repo.go b/pkg/integration/tests/worktree/bare_repo.go new file mode 100644 index 000000000..af0133227 --- /dev/null +++ b/pkg/integration/tests/worktree/bare_repo.go @@ -0,0 +1,61 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var BareRepo = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Open lazygit in the worktree of a bare repo", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + // we're going to have a directory structure like this: + // project + // - .bare + // - repo (a worktree) + // - worktree2 (another worktree) + // + // The first repo is called 'repo' because that's the + // directory that all lazygit tests start in + + shell.NewBranch("mybranch") + shell.CreateFileAndAdd("blah", "blah") + shell.Commit("initial commit") + + shell.RunCommand([]string{"git", "clone", "--bare", ".", "../.bare"}) + + shell.DeleteFile(".git") + + shell.Chdir("..") + + // This is the dir we were just in (and the dir that lazygit starts in when the test runs) + // We're going to replace it with a worktree + shell.DeleteFile("repo") + + shell.RunCommand([]string{"git", "--git-dir", ".bare", "worktree", "add", "-b", "repo", "repo", "mybranch"}) + shell.RunCommand([]string{"git", "--git-dir", ".bare", "worktree", "add", "-b", "worktree2", "worktree2", "mybranch"}) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Lines( + Contains("repo"), + Contains("mybranch"), + Contains("worktree2 (worktree)"), + ) + + t.Views().Worktrees(). + Focus(). + Lines( + Contains("repo").IsSelected(), + Contains("worktree2"), + ). + NavigateToLine(Contains("worktree2")). + Press(keys.Universal.Select). + Lines( + Contains("worktree2").IsSelected(), + Contains("repo"), + ) + }, +})