From 4c5b1574f147fe2005bcd30bbb5dc106c4838b92 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 28 Jul 2023 18:27:14 +1000 Subject: [PATCH] Centralise logic for obtaining repo paths There are quite a few paths you might want to get e.g. the repo's path, the worktree's path, the repo's git dir path, the worktree's git dir path. I want these all obtained once and then used when needed rather than having to have IO whenever we need them. This is not so much about reducing time spent on IO as it is about not having to care about errors every time we want a path. --- pkg/commands/git.go | 75 ++---- pkg/commands/git_commands/bisect.go | 2 +- pkg/commands/git_commands/commit_loader.go | 12 +- pkg/commands/git_commands/common.go | 6 +- pkg/commands/git_commands/file_loader.go | 9 +- pkg/commands/git_commands/patch.go | 2 +- pkg/commands/git_commands/paths.go | 235 ++++++++++++++++++ pkg/commands/git_commands/rebase.go | 6 +- pkg/commands/git_commands/status.go | 6 +- pkg/commands/git_commands/submodule.go | 4 +- pkg/commands/git_commands/worktree.go | 90 ------- pkg/commands/git_commands/worktree_loader.go | 67 ++--- pkg/env/env.go | 11 +- pkg/gui/controllers/helpers/refresh_helper.go | 2 +- pkg/gui/controllers/helpers/repos_helper.go | 2 +- .../helpers/working_tree_helper.go | 3 +- pkg/gui/controllers/status_controller.go | 4 +- pkg/gui/gui.go | 10 +- pkg/integration/tests/test_list.go | 4 +- .../{bisect.go => associate_branch_bisect.go} | 2 +- .../{rebase.go => associate_branch_rebase.go} | 2 +- pkg/integration/tests/worktree/bare_repo.go | 43 +++- 22 files changed, 347 insertions(+), 250 deletions(-) create mode 100644 pkg/commands/git_commands/paths.go rename pkg/integration/tests/worktree/{bisect.go => associate_branch_bisect.go} (97%) rename pkg/integration/tests/worktree/{rebase.go => associate_branch_rebase.go} (97%) 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(