diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 3f7e823e7..510b26c11 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -1,7 +1,6 @@ package commands import ( - "fmt" "os" "path/filepath" "strings" @@ -40,6 +39,7 @@ type GitCommand struct { Bisect *git_commands.BisectCommands Worktree *git_commands.WorktreeCommands Version *git_commands.GitVersion + RepoPaths git_commands.RepoPaths Loaders Loaders } @@ -67,12 +67,15 @@ func NewGitCommand( return nil, err } - dotGitDir, err := findDotGitDir(os.Stat, os.ReadFile) + repoPaths, err := git_commands.GetRepoPaths() if err != nil { - return nil, err + return nil, errors.Errorf("Error getting repo paths: %v", err) } - repository, err := gogit.PlainOpenWithOptions(dotGitDir, &gogit.PlainOpenOptions{DetectDotGit: false, EnableDotGitCommonDir: true}) + repository, err := gogit.PlainOpenWithOptions( + repoPaths.WorktreeGitDirPath(), + &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) @@ -85,7 +88,7 @@ func NewGitCommand( version, osCommand, gitConfig, - dotGitDir, + repoPaths, repository, syncMutex, ), nil @@ -96,7 +99,7 @@ func NewGitCommandAux( version *git_commands.GitVersion, osCommand *oscommands.OSCommand, gitConfig git_config.IGitConfig, - dotGitDir string, + repoPaths git_commands.RepoPaths, repo *gogit.Repository, syncMutex *deadlock.Mutex, ) *GitCommand { @@ -109,9 +112,9 @@ func NewGitCommandAux( // common ones are: cmn, osCommand, dotGitDir, configCommands configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo) - fileLoader := git_commands.NewFileLoader(cmn, cmd, configCommands) + gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, repoPaths, repo, configCommands, syncMutex) - gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, dotGitDir, repo, configCommands, syncMutex) + fileLoader := git_commands.NewFileLoader(gitCommon, cmd, configCommands) statusCommands := git_commands.NewStatusCommands(gitCommon) flowCommands := git_commands.NewFlowCommands(gitCommon) remoteCommands := git_commands.NewRemoteCommands(gitCommon) @@ -138,10 +141,10 @@ func NewGitCommandAux( branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands) commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd) - commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode, gitCommon) + commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon) reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd) remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes) - worktreeLoader := git_commands.NewWorktreeLoader(cmn, cmd) + worktreeLoader := git_commands.NewWorktreeLoader(gitCommon, cmd) stashLoader := git_commands.NewStashLoader(cmn, cmd) tagLoader := git_commands.NewTagLoader(cmn, cmd) @@ -176,6 +179,7 @@ func NewGitCommandAux( StashLoader: stashLoader, TagLoader: tagLoader, }, + RepoPaths: repoPaths, } } @@ -222,20 +226,6 @@ func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir f } } -// 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 - } - - if l.Mode()&os.ModeSymlink == 0 { - return path, nil - } - - return filepath.EvalSymlinks(path) -} - func setupRepository( openGitRepository func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error), options gogit.PlainOpenOptions, @@ -253,43 +243,6 @@ func setupRepository( return repository, err } -func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filename string) ([]byte, error)) (string, error) { - unresolvedPath := env.GetGitDirEnv() - if unresolvedPath == "" { - var err error - unresolvedPath, err = os.Getwd() - if err != nil { - return "", err - } - unresolvedPath = filepath.Join(unresolvedPath, ".git") - } - - path, err := resolveSymlink(unresolvedPath) - if err != nil { - return "", err - } - - f, err := stat(path) - if err != nil { - return "", err - } - - if f.IsDir() { - return path, nil - } - - fileBytes, err := readFile(path) - if err != nil { - return "", err - } - - fileContent := string(fileBytes) - if !strings.HasPrefix(fileContent, "gitdir: ") { - 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 -} - func VerifyInGitRepo(osCommand *oscommands.OSCommand) error { return osCommand.Cmd.New(git_commands.NewGitCmd("rev-parse").Arg("--git-dir").ToArgv()).DontLog().Run() } diff --git a/pkg/commands/git_commands/bisect.go b/pkg/commands/git_commands/bisect.go index bd4b3ead2..41e99bd3c 100644 --- a/pkg/commands/git_commands/bisect.go +++ b/pkg/commands/git_commands/bisect.go @@ -19,7 +19,7 @@ func NewBisectCommands(gitCommon *GitCommon) *BisectCommands { // This command is pretty cheap to run so we're not storing the result anywhere. // But if it becomes problematic we can chang that. func (self *BisectCommands) GetInfo() *BisectInfo { - return self.GetInfoForGitDir(self.dotGitDir) + return self.GetInfoForGitDir(self.repoPaths.WorktreeGitDirPath()) } func (self *BisectCommands) GetInfoForGitDir(gitDir string) *BisectInfo { diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index 7e304ea44..ac0f19363 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -47,7 +47,6 @@ type CommitLoader struct { func NewCommitLoader( cmn *common.Common, cmd oscommands.ICmdObjBuilder, - dotGitDir string, getRebaseMode func() (enums.RebaseMode, error), gitCommon *GitCommon, ) *CommitLoader { @@ -57,7 +56,6 @@ func NewCommitLoader( getRebaseMode: getRebaseMode, readFile: os.ReadFile, walkFiles: filepath.Walk, - dotGitDir: dotGitDir, mainBranches: nil, GitCommon: gitCommon, } @@ -299,7 +297,7 @@ func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*mo func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) { rewrittenCount := 0 - bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-apply/rewritten")) + bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply/rewritten")) if err == nil { content := string(bytesContent) rewrittenCount = len(strings.Split(content, "\n")) @@ -307,7 +305,7 @@ func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) { // we know we're rebasing, so lets get all the files whose names have numbers commits := []*models.Commit{} - err = self.walkFiles(filepath.Join(self.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error { + err = self.walkFiles(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply"), func(path string, f os.FileInfo, err error) error { if rewrittenCount > 0 { rewrittenCount-- return nil @@ -348,7 +346,7 @@ func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) { // and extracts out the sha and names of commits that we still have to go // in the rebase: func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) { - bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")) + bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo")) if err != nil { self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error())) // we assume an error means the file doesn't exist so we just return @@ -393,7 +391,7 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err } func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string { - bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/done")) + bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/done")) if err != nil { self.Log.Error(fmt.Sprintf("error occurred reading rebase-merge/done: %s", err.Error())) return "" @@ -406,7 +404,7 @@ func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string { } amendFileExists := false - if _, err := os.Stat(filepath.Join(self.dotGitDir, "rebase-merge/amend")); err == nil { + if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend")); err == nil { amendFileExists = true } diff --git a/pkg/commands/git_commands/common.go b/pkg/commands/git_commands/common.go index f2f187bef..b4c8bea32 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 - dotGitDir string + 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, - dotGitDir string, + repoPaths RepoPaths, repo *gogit.Repository, config *ConfigCommands, syncMutex *deadlock.Mutex, @@ -34,7 +34,7 @@ func NewGitCommon( version: version, cmd: cmd, os: osCommand, - dotGitDir: dotGitDir, + repoPaths: repoPaths, repo: repo, config: config, syncMutex: syncMutex, diff --git a/pkg/commands/git_commands/file_loader.go b/pkg/commands/git_commands/file_loader.go index 1585fced6..574a8a6f0 100644 --- a/pkg/commands/git_commands/file_loader.go +++ b/pkg/commands/git_commands/file_loader.go @@ -7,7 +7,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" - "github.com/jesseduffield/lazygit/pkg/common" ) type FileLoaderConfig interface { @@ -15,15 +14,15 @@ type FileLoaderConfig interface { } type FileLoader struct { - *common.Common + *GitCommon cmd oscommands.ICmdObjBuilder config FileLoaderConfig getFileType func(string) string } -func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader { +func NewFileLoader(gitCommon *GitCommon, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader { return &FileLoader{ - Common: cmn, + GitCommon: gitCommon, cmd: cmd, getFileType: oscommands.FileType, config: config, @@ -67,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() + worktreePaths := linkedWortkreePaths(self.repoPaths.RepoGitDirPath()) for _, file := range files { for _, worktreePath := range worktreePaths { absFilePath, err := filepath.Abs(file.Name) diff --git a/pkg/commands/git_commands/patch.go b/pkg/commands/git_commands/patch.go index 871dadc05..749e5dc22 100644 --- a/pkg/commands/git_commands/patch.go +++ b/pkg/commands/git_commands/patch.go @@ -79,7 +79,7 @@ func (self *PatchCommands) applyPatchFile(filepath string, opts ApplyPatchOpts) } func (self *PatchCommands) SaveTemporaryPatch(patch string) (string, error) { - filepath := filepath.Join(self.os.GetTempDir(), GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch") + filepath := filepath.Join(self.os.GetTempDir(), self.repoPaths.RepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch") self.Log.Infof("saving temporary patch to %s", filepath) if err := self.os.CreateFileWithContent(filepath, patch); err != nil { return "", err diff --git a/pkg/commands/git_commands/paths.go b/pkg/commands/git_commands/paths.go new file mode 100644 index 000000000..de83346ce --- /dev/null +++ b/pkg/commands/git_commands/paths.go @@ -0,0 +1,235 @@ +package git_commands + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/env" + "github.com/samber/lo" +) + +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 { + currentPath string + worktreePath string + worktreeGitDirPath string + repoPath string + repoGitDirPath string + repoName string +} + +var _ RepoPaths = &RepoDirsImpl{} + +func (self *RepoDirsImpl) CurrentPath() string { + return self.currentPath +} + +func (self *RepoDirsImpl) WorktreePath() string { + return self.worktreePath +} + +func (self *RepoDirsImpl) WorktreeGitDirPath() string { + return self.worktreeGitDirPath +} + +func (self *RepoDirsImpl) RepoPath() string { + return self.repoPath +} + +func (self *RepoDirsImpl) RepoGitDirPath() string { + return self.repoGitDirPath +} + +func (self *RepoDirsImpl) 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) + } + + worktreePath := currentPath + repoGitDirPath, repoPath, err := GetCurrentRepoGitDirPath(currentPath) + if err != nil { + return &RepoDirsImpl{}, errors.Errorf("failed to get repo git dir path: %v", err) + } + worktreeGitDirPath, err := worktreeGitDirPath(currentPath) + if err != nil { + return &RepoDirsImpl{}, errors.Errorf("failed to get worktree git dir path: %v", err) + } + repoName := filepath.Base(repoPath) + + return &RepoDirsImpl{ + currentPath: currentPath, + worktreePath: worktreePath, + worktreeGitDirPath: worktreeGitDirPath, + repoPath: repoPath, + repoGitDirPath: repoGitDirPath, + repoName: repoName, + }, 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 := filepath.Join(repoGitDirPath, "worktrees") + + // ensure the directory exists + _, err := os.Stat(worktreeGitDirsPath) + if err != nil { + return result + } + + _ = filepath.Walk(worktreeGitDirsPath, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() { + return nil + } + + gitDirPath := filepath.Join(path, "gitdir") + gitDirBytes, err := os.ReadFile(gitDirPath) + if err != nil { + // ignoring error + return nil + } + trimmedGitDir := strings.TrimSpace(string(gitDirBytes)) + // removing the .git part + worktreeDir := filepath.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) { + // if .git is a file, we're in a linked worktree, otherwise we're in + // the main worktree + dotGitPath := filepath.Join(worktreePath, ".git") + gitFileInfo, err := os.Stat(dotGitPath) + if err != nil { + return "", err + } + + if gitFileInfo.IsDir() { + return dotGitPath, nil + } + + return linkedWorktreeGitDirPath(worktreePath) +} + +func linkedWorktreeGitDirPath(worktreePath string) (string, error) { + dotGitPath := filepath.Join(worktreePath, ".git") + gitFileContents, err := os.ReadFile(dotGitPath) + if err != nil { + return "", err + } + + // The file will have `gitdir: /path/to/.git/worktrees/` + gitDirLine := lo.Filter(strings.Split(string(gitFileContents), "\n"), func(line string, _ int) bool { + return strings.HasPrefix(line, "gitdir: ") + }) + + if len(gitDirLine) == 0 { + 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", dotGitPath)) + } + + gitDir := strings.TrimPrefix(gitDirLine[0], "gitdir: ") + return gitDir, nil +} + +func GetCurrentRepoGitDirPath(currentPath string) (string, string, error) { + var unresolvedGitPath string + if env.GetGitDirEnv() != "" { + unresolvedGitPath = env.GetGitDirEnv() + } else { + unresolvedGitPath = filepath.Join(currentPath, ".git") + } + + gitPath, err := resolveSymlink(unresolvedGitPath) + if err != nil { + return "", "", err + } + + // check if .git is a file or a directory + gitFileInfo, err := os.Stat(gitPath) + if err != nil { + return "", "", err + } + + if gitFileInfo.IsDir() { + // must be in the main worktree + return gitPath, filepath.Dir(gitPath), nil + } + + // either in a submodule, or worktree + worktreeGitPath, err := linkedWorktreeGitDirPath(currentPath) + if err != nil { + return "", "", errors.Errorf("could not find git dir for %s: %v", currentPath, err) + } + + // confirm whether the next directory up is the worktrees/submodules directory + parent := filepath.Dir(worktreeGitPath) + if filepath.Base(parent) != "worktrees" && filepath.Base(parent) != "modules" { + return "", "", errors.Errorf("could not find git dir for %s", currentPath) + } + + // if it's a submodule, we treat it as its own repo + if filepath.Base(parent) == "modules" { + return worktreeGitPath, currentPath, nil + } + + gitDirPath := filepath.Dir(parent) + return gitDirPath, filepath.Dir(gitDirPath), nil +} + +// 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 + } + + if l.Mode()&os.ModeSymlink == 0 { + return path, nil + } + + return filepath.EvalSymlinks(path) +} diff --git a/pkg/commands/git_commands/rebase.go b/pkg/commands/git_commands/rebase.go index 74bb3d464..66bfbf82d 100644 --- a/pkg/commands/git_commands/rebase.go +++ b/pkg/commands/git_commands/rebase.go @@ -243,18 +243,18 @@ func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) e // EditRebaseTodo sets the action for a given rebase commit in the git-rebase-todo file func (self *RebaseCommands) EditRebaseTodo(commit *models.Commit, action todo.TodoCommand) error { return utils.EditRebaseTodo( - filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action, self.config.GetCoreCommentChar()) + filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action, self.config.GetCoreCommentChar()) } // MoveTodoDown moves a rebase todo item down by one position func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error { - fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo") + fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo") return utils.MoveTodoDown(fileName, commit.Sha, commit.Action, self.config.GetCoreCommentChar()) } // MoveTodoDown moves a rebase todo item down by one position func (self *RebaseCommands) MoveTodoUp(commit *models.Commit) error { - fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo") + fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo") return utils.MoveTodoUp(fileName, commit.Sha, commit.Action, self.config.GetCoreCommentChar()) } diff --git a/pkg/commands/git_commands/status.go b/pkg/commands/git_commands/status.go index 7f03c698e..13ff02cc0 100644 --- a/pkg/commands/git_commands/status.go +++ b/pkg/commands/git_commands/status.go @@ -24,14 +24,14 @@ func NewStatusCommands( // RebaseMode returns "" for non-rebase mode, "normal" for normal rebase // and "interactive" for interactive rebase func (self *StatusCommands) RebaseMode() (enums.RebaseMode, error) { - exists, err := self.os.FileExists(filepath.Join(self.dotGitDir, "rebase-apply")) + exists, err := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply")) if err != nil { return enums.REBASE_MODE_NONE, err } if exists { return enums.REBASE_MODE_NORMAL, nil } - exists, err = self.os.FileExists(filepath.Join(self.dotGitDir, "rebase-merge")) + exists, err = self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge")) if exists { return enums.REBASE_MODE_INTERACTIVE, err } else { @@ -69,5 +69,5 @@ func IsBareRepo(osCommand *oscommands.OSCommand) (bool, error) { // IsInMergeState states whether we are still mid-merge func (self *StatusCommands) IsInMergeState() (bool, error) { - return self.os.FileExists(filepath.Join(self.dotGitDir, "MERGE_HEAD")) + return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "MERGE_HEAD")) } diff --git a/pkg/commands/git_commands/submodule.go b/pkg/commands/git_commands/submodule.go index 3d8602b9a..d9d1ccd20 100644 --- a/pkg/commands/git_commands/submodule.go +++ b/pkg/commands/git_commands/submodule.go @@ -139,7 +139,9 @@ func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error { self.Log.Error(err) } - return os.RemoveAll(filepath.Join(self.dotGitDir, "modules", submodule.Path)) + // We may in fact want to use the repo's git dir path but git docs say not to + // mix submodules and worktrees anyway. + return os.RemoveAll(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "modules", submodule.Path)) } func (self *SubmoduleCommands) Add(name string, path string, url string) error { diff --git a/pkg/commands/git_commands/worktree.go b/pkg/commands/git_commands/worktree.go index 1b57ab122..986bb6d42 100644 --- a/pkg/commands/git_commands/worktree.go +++ b/pkg/commands/git_commands/worktree.go @@ -1,11 +1,7 @@ package git_commands import ( - "io/fs" - "log" - "os" "path/filepath" - "strings" "github.com/jesseduffield/lazygit/pkg/commands/models" ) @@ -76,89 +72,3 @@ func CheckedOutByOtherWorktree(branch *models.Branch, worktrees []*models.Worktr return !worktree.IsCurrent } - -// If in a non-bare repo, this returns the path of the main worktree -// TODO: see if this works with a bare repo. -func GetCurrentRepoPath() string { - pwd, err := os.Getwd() - if err != nil { - log.Fatalln(err.Error()) - } - - // check if .git is a file or a directory - gitPath := filepath.Join(pwd, ".git") - gitFileInfo, err := os.Stat(gitPath) - if err != nil { - // fallback - return currentPath() - } - - if gitFileInfo.IsDir() { - // must be in the main worktree - return currentPath() - } - - // either in a submodule, a worktree, or a bare repo - worktreeGitPath, ok := LinkedWorktreeGitPath(pwd) - if !ok { - // fallback - return currentPath() - } - - // confirm whether the next directory up is the 'worktrees' directory - parent := filepath.Dir(worktreeGitPath) - if filepath.Base(parent) != "worktrees" { - // fallback - return currentPath() - } - - // now we just jump up two more directories to get the repo name - return filepath.Dir(filepath.Dir(parent)) -} - -func GetCurrentRepoName() string { - return filepath.Base(GetCurrentRepoPath()) -} - -func currentPath() string { - pwd, err := os.Getwd() - if err != nil { - log.Fatalln(err.Error()) - } - return pwd -} - -func linkedWortkreePaths() []string { - // first we need to get the repo dir - repoPath := GetCurrentRepoPath() - result := []string{} - worktreePath := filepath.Join(repoPath, ".git", "worktrees") - // for each directory in this path we're going to cat the `gitdir` file and append its contents to our result - - // ensure the directory exists - _, err := os.Stat(worktreePath) - if err != nil { - return result - } - - err = filepath.Walk(worktreePath, func(path string, info fs.FileInfo, err error) error { - if info.IsDir() { - gitDirPath := filepath.Join(path, "gitdir") - gitDirBytes, err := os.ReadFile(gitDirPath) - if err != nil { - // ignoring error - return nil - } - trimmedGitDir := strings.TrimSpace(string(gitDirBytes)) - // removing the .git part - worktreeDir := filepath.Dir(trimmedGitDir) - result = append(result, worktreeDir) - } - return nil - }) - if err != nil { - return result - } - - return result -} diff --git a/pkg/commands/git_commands/worktree_loader.go b/pkg/commands/git_commands/worktree_loader.go index 42f889156..06a22d2b5 100644 --- a/pkg/commands/git_commands/worktree_loader.go +++ b/pkg/commands/git_commands/worktree_loader.go @@ -9,33 +9,28 @@ import ( "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" - "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) type WorktreeLoader struct { - *common.Common + *GitCommon cmd oscommands.ICmdObjBuilder } func NewWorktreeLoader( - common *common.Common, + gitCommon *GitCommon, cmd oscommands.ICmdObjBuilder, ) *WorktreeLoader { return &WorktreeLoader{ - Common: common, - cmd: cmd, + GitCommon: gitCommon, + cmd: cmd, } } func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { - currentRepoPath := GetCurrentRepoPath() - - pwd, err := os.Getwd() - if err != nil { - return nil, err - } + currentRepoPath := self.repoPaths.RepoPath() + worktreePath := self.repoPaths.WorktreePath() cmdArgs := NewGitCmd("worktree").Arg("list", "--porcelain").ToArgv() worktreesOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() @@ -48,11 +43,15 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { var worktrees []*models.Worktree var current *models.Worktree for _, splitLine := range splitLines { + // worktrees are defined over multiple lines and are separated by blank lines + // so if we reach a blank line we're done with the current worktree if len(splitLine) == 0 && current != nil { worktrees = append(worktrees, current) current = nil continue } + + // ignore bare repo (not sure why it's even appearing in this list: it's not a worktree) if splitLine == "bare" { current = nil continue @@ -61,18 +60,13 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { if strings.HasPrefix(splitLine, "worktree ") { path := strings.SplitN(splitLine, " ", 2)[1] isMain := path == currentRepoPath - isCurrent := path == pwd + isCurrent := path == worktreePath isPathMissing := self.pathExists(path) var gitDir string - if isMain { - gitDir = filepath.Join(path, ".git") - } else { - var ok bool - gitDir, ok = LinkedWorktreeGitPath(path) - if !ok { - self.Log.Warnf("Could not find git dir for worktree %s", path) - } + gitDir, err := worktreeGitDirPath(path) + if err != nil { + self.Log.Warnf("Could not find git dir for worktree %s: %v", path, err) } current = &models.Worktree{ @@ -120,15 +114,15 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { continue } - rebaseBranch, ok := rebaseBranch(worktree) + rebasedBranch, ok := rebasedBranch(worktree) if ok { - worktree.Branch = rebaseBranch + worktree.Branch = rebasedBranch continue } - bisectBranch, ok := bisectBranch(worktree) + bisectedBranch, ok := bisectedBranch(worktree) if ok { - worktree.Branch = bisectBranch + worktree.Branch = bisectedBranch continue } } @@ -147,7 +141,7 @@ func (self *WorktreeLoader) pathExists(path string) bool { return false } -func rebaseBranch(worktree *models.Worktree) (string, bool) { +func 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 { headName := strings.TrimSpace(string(bytesContent)) @@ -159,7 +153,7 @@ func rebaseBranch(worktree *models.Worktree) (string, bool) { return "", false } -func bisectBranch(worktree *models.Worktree) (string, bool) { +func bisectedBranch(worktree *models.Worktree) (string, bool) { bisectStartPath := filepath.Join(worktree.GitDir, "BISECT_START") startContent, err := os.ReadFile(bisectStartPath) if err != nil { @@ -169,27 +163,6 @@ func bisectBranch(worktree *models.Worktree) (string, bool) { return strings.TrimSpace(string(startContent)), true } -func LinkedWorktreeGitPath(worktreePath string) (string, bool) { - // first we get the path of the worktree, then we look at the contents of the `.git` file in that path - // then we look for the line that says `gitdir: /path/to/.git/worktrees/` - // then we return that path - gitFileContents, err := os.ReadFile(filepath.Join(worktreePath, ".git")) - if err != nil { - return "", false - } - - gitDirLine := lo.Filter(strings.Split(string(gitFileContents), "\n"), func(line string, _ int) bool { - return strings.HasPrefix(line, "gitdir: ") - }) - - if len(gitDirLine) == 0 { - return "", false - } - - gitDir := strings.TrimPrefix(gitDirLine[0], "gitdir: ") - return gitDir, true -} - type pathWithIndexT struct { path string index int diff --git a/pkg/env/env.go b/pkg/env/env.go index 8d7993a9a..c96bd18bd 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -10,19 +10,10 @@ func GetGitDirEnv() string { return os.Getenv("GIT_DIR") } -func GetGitWorkTreeEnv() string { - return os.Getenv("GIT_WORK_TREE") -} - func SetGitDirEnv(value string) { os.Setenv("GIT_DIR", value) } -func SetGitWorkTreeEnv(value string) { - os.Setenv("GIT_WORK_TREE", value) -} - -func UnsetGitDirEnvs() { +func UnsetGitDirEnv() { _ = os.Unsetenv("GIT_DIR") - _ = os.Unsetenv("GIT_WORK_TREE") } diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 0dc6924f9..46130231f 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -631,7 +631,7 @@ func (self *RefreshHelper) refreshStatus() { workingTreeState := self.c.Git().Status.WorkingTreeState() linkedWorktreeName := self.worktreeHelper.GetLinkedWorktreeName() - repoName := git_commands.GetCurrentRepoName() + repoName := self.c.Git().RepoPaths.RepoName() status := presentation.FormatStatus(repoName, currentBranch, linkedWorktreeName, workingTreeState, self.c.Tr) diff --git a/pkg/gui/controllers/helpers/repos_helper.go b/pkg/gui/controllers/helpers/repos_helper.go index c39997c5b..0faa0e16b 100644 --- a/pkg/gui/controllers/helpers/repos_helper.go +++ b/pkg/gui/controllers/helpers/repos_helper.go @@ -145,7 +145,7 @@ func (self *ReposHelper) DispatchSwitchToRepo(path string, contextKey types.Cont func (self *ReposHelper) DispatchSwitchTo(path string, errMsg string, contextKey types.ContextKey) error { return self.c.WithWaitingStatus(self.c.Tr.Switching, func(gocui.Task) error { - env.UnsetGitDirEnvs() + env.UnsetGitDirEnv() originalPath, err := os.Getwd() if err != nil { return nil diff --git a/pkg/gui/controllers/helpers/working_tree_helper.go b/pkg/gui/controllers/helpers/working_tree_helper.go index a0a0447d7..dba9d2a6a 100644 --- a/pkg/gui/controllers/helpers/working_tree_helper.go +++ b/pkg/gui/controllers/helpers/working_tree_helper.go @@ -4,7 +4,6 @@ import ( "fmt" "regexp" - "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/context" @@ -203,7 +202,7 @@ func (self *WorkingTreeHelper) prepareFilesForCommit() error { } func (self *WorkingTreeHelper) commitPrefixConfigForRepo() *config.CommitPrefixConfig { - cfg, ok := self.c.UserConfig.Git.CommitPrefixes[git_commands.GetCurrentRepoName()] + cfg, ok := self.c.UserConfig.Git.CommitPrefixes[self.c.Git().RepoPaths.RepoName()] if !ok { return nil } diff --git a/pkg/gui/controllers/status_controller.go b/pkg/gui/controllers/status_controller.go index d8e906f3b..af1cb9984 100644 --- a/pkg/gui/controllers/status_controller.go +++ b/pkg/gui/controllers/status_controller.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/constants" "github.com/jesseduffield/lazygit/pkg/gui/presentation" @@ -108,8 +107,7 @@ func (self *StatusController) onClick() error { cx, _ := self.c.Views().Status.Cursor() upstreamStatus := presentation.BranchStatus(currentBranch, self.c.Tr) - // TODO: support worktrees here - repoName := git_commands.GetCurrentRepoName() + repoName := self.c.Git().RepoPaths.RepoName() workingTreeState := self.c.Git().Status.WorkingTreeState() switch workingTreeState { case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING: diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index ec4ab77c5..42a29d500 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -64,7 +64,8 @@ type Gui struct { CustomCommandsClient *custom_commands.Client // this is a mapping of repos to gui states, so that we can restore the original - // gui state when returning from a subrepo + // gui state when returning from a subrepo. + // In repos with multiple worktrees, we store a separate repo state per worktree. RepoStateMap map[Repo]*GuiRepoState Config config.AppConfigurer Updater *updates.Updater @@ -325,12 +326,9 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context // you've already switched from. There's no doubt some easy way to make the UX // optimal for all cases but I'm too lazy to think about what that is right now func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context { - currentDir, err := os.Getwd() - if err != nil { - gui.c.Log.Error(err) - } + worktreePath := gui.git.RepoPaths.WorktreePath() - if state := gui.RepoStateMap[Repo(currentDir)]; state != nil { + if state := gui.RepoStateMap[Repo(worktreePath)]; state != nil { gui.State = state gui.State.ViewsSetup = false diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 62a4fae29..bdd2a2bfd 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -223,14 +223,14 @@ var tests = []*components.IntegrationTest{ worktree.AddFromBranch, worktree.AddFromBranchDetached, worktree.AddFromCommit, + worktree.AssociateBranchBisect, + worktree.AssociateBranchRebase, worktree.BareRepo, - worktree.Bisect, worktree.Crud, worktree.CustomCommand, worktree.DetachWorktreeFromBranch, worktree.FastForwardWorktreeBranch, worktree.ForceRemoveWorktree, - worktree.Rebase, worktree.RemoveWorktreeFromBranch, worktree.ResetWindowTabs, worktree.WorktreeInRepo, diff --git a/pkg/integration/tests/worktree/bisect.go b/pkg/integration/tests/worktree/associate_branch_bisect.go similarity index 97% rename from pkg/integration/tests/worktree/bisect.go rename to pkg/integration/tests/worktree/associate_branch_bisect.go index 143f8114f..77d46e176 100644 --- a/pkg/integration/tests/worktree/bisect.go +++ b/pkg/integration/tests/worktree/associate_branch_bisect.go @@ -12,7 +12,7 @@ import ( // not bothering to test the linked worktree here because it's the same logic as the rebase test -var Bisect = NewIntegrationTest(NewIntegrationTestArgs{ +var AssociateBranchBisect = NewIntegrationTest(NewIntegrationTestArgs{ Description: "Verify that when you start a bisect in a linked worktree, Lazygit still associates the worktree with the branch", ExtraCmdArgs: []string{}, Skip: false, diff --git a/pkg/integration/tests/worktree/rebase.go b/pkg/integration/tests/worktree/associate_branch_rebase.go similarity index 97% rename from pkg/integration/tests/worktree/rebase.go rename to pkg/integration/tests/worktree/associate_branch_rebase.go index 8b91702b5..b0b04b805 100644 --- a/pkg/integration/tests/worktree/rebase.go +++ b/pkg/integration/tests/worktree/associate_branch_rebase.go @@ -13,7 +13,7 @@ import ( // We need different logic for associated the branch depending on whether it's a main worktree or // linked worktree, so this test handles both. -var Rebase = NewIntegrationTest(NewIntegrationTestArgs{ +var AssociateBranchRebase = NewIntegrationTest(NewIntegrationTestArgs{ Description: "Verify that when you start a rebase in a linked or main worktree, Lazygit still associates the worktree with the branch", ExtraCmdArgs: []string{}, Skip: false, diff --git a/pkg/integration/tests/worktree/bare_repo.go b/pkg/integration/tests/worktree/bare_repo.go index af0133227..2343b112e 100644 --- a/pkg/integration/tests/worktree/bare_repo.go +++ b/pkg/integration/tests/worktree/bare_repo.go @@ -6,7 +6,7 @@ import ( ) var BareRepo = NewIntegrationTest(NewIntegrationTestArgs{ - Description: "Open lazygit in the worktree of a bare repo", + Description: "Open lazygit in the worktree of a bare repo and do a rebase/bisect", ExtraCmdArgs: []string{}, Skip: false, SetupConfig: func(config *config.AppConfig) {}, @@ -23,6 +23,8 @@ var BareRepo = NewIntegrationTest(NewIntegrationTestArgs{ shell.NewBranch("mybranch") shell.CreateFileAndAdd("blah", "blah") shell.Commit("initial commit") + shell.EmptyCommit("commit two") + shell.EmptyCommit("commit three") shell.RunCommand([]string{"git", "clone", "--bare", ".", "../.bare"}) @@ -45,6 +47,45 @@ var BareRepo = NewIntegrationTest(NewIntegrationTestArgs{ Contains("worktree2 (worktree)"), ) + // test that a rebase works fine + // (rebase uses the git dir of the worktree so we're confirming that it points + // to the right git dir) + t.Views().Commits(). + Focus(). + Lines( + Contains("commit three").IsSelected(), + Contains("commit two"), + Contains("initial commit"), + ). + Press(keys.Commits.MoveDownCommit). + Lines( + Contains("commit two"), + Contains("commit three").IsSelected(), + Contains("initial commit"), + ). + // test that bisect works fine (same logic as above) + NavigateToLine(Contains("commit two")). + Press(keys.Commits.ViewBisectOptions). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Bisect")). + Select(MatchesRegexp(`Mark .* as bad`)). + Confirm() + + t.Views().Information().Content(Contains("Bisecting")) + }). + NavigateToLine(Contains("initial commit")). + Press(keys.Commits.ViewBisectOptions). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Bisect")). + Select(MatchesRegexp(`Mark .* as good`)). + Confirm() + + t.Views().Information().Content(Contains("Bisecting")) + }) + + // switch to other worktree t.Views().Worktrees(). Focus(). Lines(