From 34d41b5d51b22843f053f8164212a95ee055dd31 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 7 Aug 2026 22:56:15 +0200 Subject: [PATCH] Don't let our repo answer for a different one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GIT_DIR and GIT_WORK_TREE tell git where our repo is, and every command we run inherits them — including the ones we point at a submodule or another worktree. git resolves those against our repo instead, and says nothing about it: with GIT_DIR set, `git -C mysub log -1` reports the superproject's commit. So opening lazygit with --git-dir/--work-tree quietly broke resolving submodule conflicts, stashing and resetting a submodule, and detaching another worktree; the worktree list came back claiming every worktree shared our git dir. Drop the two variables from the commands that address another repo. Co-Authored-By: Claude Opus 5 (1M context) --- .../git_commands/git_command_builder.go | 10 ++++++++ pkg/commands/git_commands/repo_paths.go | 25 ++++++++++++++++++- pkg/commands/git_commands/submodule.go | 10 ++++---- pkg/commands/git_commands/submodule_test.go | 24 ++++++++++++++++++ pkg/commands/git_commands/worktree.go | 2 +- pkg/commands/git_commands/worktree_loader.go | 2 +- pkg/env/env.go | 19 +++++++++----- 7 files changed, 78 insertions(+), 14 deletions(-) diff --git a/pkg/commands/git_commands/git_command_builder.go b/pkg/commands/git_commands/git_command_builder.go index 49e27ec1f..f1a7c87b4 100644 --- a/pkg/commands/git_commands/git_command_builder.go +++ b/pkg/commands/git_commands/git_command_builder.go @@ -6,6 +6,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/env" ) // OptionalLocksEnvVar is the name of the environment variable that tells git @@ -18,6 +19,15 @@ import ( // that opts back in is the foreground files refresh; see FileLoader.gitStatus. const OptionalLocksEnvVar = "GIT_OPTIONAL_LOCKS" +// forOtherRepo prepares a command that operates on a repo other than the one +// we have open — a submodule, or another worktree. GIT_DIR and GIT_WORK_TREE +// say where our repo is, and every command we run inherits them, so a command +// pointed at a different repo would be resolved against ours instead: `git -C +// log` would silently log the superproject's commits. +func forOtherRepo(cmdObj *oscommands.CmdObj) *oscommands.CmdObj { + return cmdObj.RemoveEnvVar(env.GitDirEnvVar).RemoveEnvVar(env.GitWorkTreeEnvVar) +} + // convenience struct for building git commands. Especially useful when // including conditional args type GitCommandBuilder struct { diff --git a/pkg/commands/git_commands/repo_paths.go b/pkg/commands/git_commands/repo_paths.go index 89a86640b..9dbb853ef 100644 --- a/pkg/commands/git_commands/repo_paths.go +++ b/pkg/commands/git_commands/repo_paths.go @@ -176,17 +176,40 @@ func getBareRepoPathsForDir( }, nil } +// Asks git about the repo at dir. This is how we find our own repo, so it has +// to be answered the way git itself would answer it there, GIT_DIR and +// GIT_WORK_TREE included. func callGitRevParseWithDir( cmd oscommands.ICmdObjBuilder, dir string, gitRevArgs ...string, ) (string, error) { + return runGitRevParse(newGitRevParseCmd(cmd, dir, gitRevArgs...)) +} + +// Asks git about a repo that isn't the one we have open; see forOtherRepo. +func callGitRevParseInOtherRepo( + cmd oscommands.ICmdObjBuilder, + dir string, + gitRevArgs ...string, +) (string, error) { + return runGitRevParse(forOtherRepo(newGitRevParseCmd(cmd, dir, gitRevArgs...))) +} + +func newGitRevParseCmd( + cmd oscommands.ICmdObjBuilder, + dir string, + gitRevArgs ...string, +) *oscommands.CmdObj { gitRevParse := NewGitCmd("rev-parse").Arg("--path-format=absolute").Arg(gitRevArgs...) if dir != "" { gitRevParse.Dir(dir) } - gitCmd := cmd.New(gitRevParse.ToArgv()).DontLog() + return cmd.New(gitRevParse.ToArgv()).DontLog() +} + +func runGitRevParse(gitCmd *oscommands.CmdObj) (string, error) { res, err := gitCmd.RunWithOutput() if err != nil { return "", errors.Errorf("'%s' failed: %v", gitCmd.ToString(), err) diff --git a/pkg/commands/git_commands/submodule.go b/pkg/commands/git_commands/submodule.go index f31bd6196..7400f0514 100644 --- a/pkg/commands/git_commands/submodule.go +++ b/pkg/commands/git_commands/submodule.go @@ -157,7 +157,7 @@ func (self *SubmoduleCommands) GetCommitSummary(path string, sha string) (string Config("log.showsignature=false"). ToArgv() - summary, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() + summary, err := forOtherRepo(self.cmd.New(cmdArgs)).DontLog().RunWithOutput() return strings.TrimSpace(summary), err } @@ -167,7 +167,7 @@ func (self *SubmoduleCommands) GetCommitSummary(path string, sha string) (string // caller then stages the submodule to record the resolution. func (self *SubmoduleCommands) CheckoutConflictCommit(path string, sha string) error { cmdArgs := NewGitCmd("checkout").Dir(path).Arg(sha).ToArgv() - return self.cmd.New(cmdArgs).Run() + return forOtherRepo(self.cmd.New(cmdArgs)).Run() } // ConflictSideLog returns a oneline log, run inside the submodule, of the commits @@ -179,7 +179,7 @@ func (self *SubmoduleCommands) ConflictSideLog(path string, side string, otherSi Arg("--oneline", "--color=always", otherSide+".."+side). ToArgv() - return self.cmd.New(cmdArgs).DontLog().RunWithOutput() + return forOtherRepo(self.cmd.New(cmdArgs)).DontLog().RunWithOutput() } func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error { @@ -195,7 +195,7 @@ func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error { Arg("--include-untracked"). ToArgv() - return self.cmd.New(cmdArgs).Run() + return forOtherRepo(self.cmd.New(cmdArgs)).Run() } func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error { @@ -229,7 +229,7 @@ func (self *SubmoduleCommands) UpdateAll() error { // need not be. func (self *SubmoduleCommands) runInParentModule(submodule *models.SubmoduleConfig, cmdObj *oscommands.CmdObj) error { if submodule.ParentModule != nil { - cmdObj.SetWd(submodule.ParentModule.FullPath()) + forOtherRepo(cmdObj.SetWd(submodule.ParentModule.FullPath())) } return cmdObj.Run() } diff --git a/pkg/commands/git_commands/submodule_test.go b/pkg/commands/git_commands/submodule_test.go index 279c963df..d449d81de 100644 --- a/pkg/commands/git_commands/submodule_test.go +++ b/pkg/commands/git_commands/submodule_test.go @@ -1,10 +1,13 @@ package git_commands import ( + "strings" "testing" "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/env" + "github.com/samber/lo" "github.com/stretchr/testify/assert" ) @@ -80,6 +83,27 @@ func TestSubmoduleCheckoutConflictCommit(t *testing.T) { runner.CheckForMissingCalls() } +// A command that runs inside a submodule mustn't inherit the GIT_DIR and +// GIT_WORK_TREE that say where the superproject is; git would answer it from +// there instead, and the answer would look perfectly plausible. +func TestSubmoduleCommandDoesntUseOurGitLocation(t *testing.T) { + t.Setenv(env.GitDirEnvVar, "/path/to/repo/.git") + t.Setenv(env.GitWorkTreeEnvVar, "/path/to/repo") + + runner := oscommands.NewFakeRunner(t). + ExpectFunc("has neither GIT_DIR nor GIT_WORK_TREE", func(cmdObj *oscommands.CmdObj) bool { + return lo.NoneBy(cmdObj.GetEnvVars(), func(envVar string) bool { + return strings.HasPrefix(envVar, env.GitDirEnvVar+"=") || + strings.HasPrefix(envVar, env.GitWorkTreeEnvVar+"=") + }) + }, "bbbbbbb the subject\n", nil) + instance := buildSubmoduleCommands(commonDeps{runner: runner}) + + _, err := instance.GetCommitSummary("mysub", "bbbbbbb") + assert.NoError(t, err) + runner.CheckForMissingCalls() +} + func TestSubmoduleConflictSideLog(t *testing.T) { runner := oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"-C", "mysub", "log", "--oneline", "--color=always", "ccccccc..bbbbbbb"}, "bbbbbbb left\n", nil) diff --git a/pkg/commands/git_commands/worktree.go b/pkg/commands/git_commands/worktree.go index 986bb6d42..64748b878 100644 --- a/pkg/commands/git_commands/worktree.go +++ b/pkg/commands/git_commands/worktree.go @@ -51,7 +51,7 @@ func (self *WorktreeCommands) Delete(worktreePath string, force bool) error { func (self *WorktreeCommands) Detach(worktreePath string) error { cmdArgs := NewGitCmd("checkout").Arg("--detach").GitDir(filepath.Join(worktreePath, ".git")).ToArgv() - return self.cmd.New(cmdArgs).Run() + return forOtherRepo(self.cmd.New(cmdArgs)).Run() } func WorktreeForBranch(branch *models.Branch, worktrees []*models.Worktree) (*models.Worktree, bool) { diff --git a/pkg/commands/git_commands/worktree_loader.go b/pkg/commands/git_commands/worktree_loader.go index adf1e48dc..f7577c870 100644 --- a/pkg/commands/git_commands/worktree_loader.go +++ b/pkg/commands/git_commands/worktree_loader.go @@ -77,7 +77,7 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { if worktree.IsPathMissing { return } - gitDir, err := callGitRevParseWithDir(self.cmd, worktree.Path, "--absolute-git-dir") + gitDir, err := callGitRevParseInOtherRepo(self.cmd, worktree.Path, "--absolute-git-dir") if err != nil { self.Log.Warnf("Could not find git dir for worktree %s: %v", worktree.Path, err) return diff --git a/pkg/env/env.go b/pkg/env/env.go index 1ade5b8c6..74d3f7072 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -6,23 +6,30 @@ import ( // This package encapsulates accessing/mutating the ENV of the program. +// The variables with which git can be told where a repo is, rather than having +// it find out from the working directory. +const ( + GitDirEnvVar = "GIT_DIR" + GitWorkTreeEnvVar = "GIT_WORK_TREE" +) + func GetGitDirEnv() string { - return os.Getenv("GIT_DIR") + return os.Getenv(GitDirEnvVar) } func SetGitDirEnv(value string) { - os.Setenv("GIT_DIR", value) + os.Setenv(GitDirEnvVar, value) } func GetWorkTreeEnv() string { - return os.Getenv("GIT_WORK_TREE") + return os.Getenv(GitWorkTreeEnvVar) } func SetWorkTreeEnv(value string) { - os.Setenv("GIT_WORK_TREE", value) + os.Setenv(GitWorkTreeEnvVar, value) } func UnsetGitLocationEnvVars() { - _ = os.Unsetenv("GIT_DIR") - _ = os.Unsetenv("GIT_WORK_TREE") + _ = os.Unsetenv(GitDirEnvVar) + _ = os.Unsetenv(GitWorkTreeEnvVar) }