mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 02:24:25 -05:00
Remember how to get back to a repo we entered a submodule from
Entering a submodule clears GIT_DIR and GIT_WORK_TREE, as it must: they say where the superproject is. But the stack we push the superproject onto so that escape brings us back only held its path, and for a repo opened with --git-dir/--work-tree the path leads nowhere — git can't find a repo there. Escaping out of a submodule of a dotfile repo failed with "not a git repository", or, if some unrelated repo happened to lie above the work tree, quietly switched to that one instead. Push the environment onto the stack along with the path, taken from the repo paths rather than from the process env, so that it also covers a repo we worked the location out for ourselves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9b1078a2ca
commit
06b421ad0c
Vendored
+12
@@ -35,6 +35,18 @@ func UnsetGitLocationEnvVars() {
|
||||
_ = os.Unsetenv(GitWorkTreeEnvVar)
|
||||
}
|
||||
|
||||
// GetGitLocationEnvVars returns the location variables that are set, as
|
||||
// "NAME=value" entries.
|
||||
func GetGitLocationEnvVars() []string {
|
||||
envVars := []string{}
|
||||
for _, name := range []string{GitDirEnvVar, GitWorkTreeEnvVar} {
|
||||
if value := os.Getenv(name); value != "" {
|
||||
envVars = append(envVars, name+"="+value)
|
||||
}
|
||||
}
|
||||
return envVars
|
||||
}
|
||||
|
||||
// SetGitLocationEnvVars sets the location variables from "NAME=value" entries,
|
||||
// clearing both first so that only what is given remains. Passing nothing is
|
||||
// how you say the repo is to be found from the working directory.
|
||||
|
||||
@@ -54,7 +54,10 @@ func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self.c.State().GetRepoPathStack().Push(wd)
|
||||
self.c.State().GetRepoPathStack().Push(types.RepoLocation{
|
||||
Path: wd,
|
||||
GitLocationEnvVars: self.c.Git().RepoPaths.GitLocationEnvVars(),
|
||||
})
|
||||
|
||||
return self.switchTo(submodule.FullPath(), self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT)
|
||||
}
|
||||
@@ -164,7 +167,7 @@ func (self *ReposHelper) SwitchToParentRepo() error {
|
||||
if self.switchRefusedBecauseBusy() {
|
||||
return nil
|
||||
}
|
||||
return self.switchTo(self.c.State().GetRepoPathStack().Pop(), self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT)
|
||||
return self.switchToLocation(self.c.State().GetRepoPathStack().Pop(), self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT)
|
||||
}
|
||||
|
||||
func (self *ReposHelper) DispatchSwitchTo(path string, errMsg string, contextKey types.ContextKey) error {
|
||||
@@ -189,23 +192,41 @@ func (self *ReposHelper) switchRefusedBecauseBusy() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// switchTo switches lazygit to the repository (or worktree) at the given path.
|
||||
// It runs synchronously on the UI thread: the switch swaps gui.State (in
|
||||
// resetState) and reassigns gui.git and the process cwd, all of which the UI
|
||||
// thread also reads, so doing it here rather than on a worker avoids racing
|
||||
// those reads. The heavy data loading is still dispatched asynchronously by the
|
||||
// refresh that onNewRepo kicks off.
|
||||
// switchTo switches lazygit to the repository (or worktree) at the given path,
|
||||
// which git is expected to find from that path alone. That's true of every repo
|
||||
// we switch to without having been there before.
|
||||
func (self *ReposHelper) switchTo(path string, errMsg string, contextKey types.ContextKey) error {
|
||||
env.UnsetGitLocationEnvVars()
|
||||
return self.switchToLocation(types.RepoLocation{Path: path}, errMsg, contextKey)
|
||||
}
|
||||
|
||||
// switchToLocation switches lazygit to the repository (or worktree) at the
|
||||
// given location. It runs synchronously on the UI thread: the switch swaps
|
||||
// gui.State (in resetState) and reassigns gui.git and the process cwd, all of
|
||||
// which the UI thread also reads, so doing it here rather than on a worker
|
||||
// avoids racing those reads. The heavy data loading is still dispatched
|
||||
// asynchronously by the refresh that onNewRepo kicks off.
|
||||
//
|
||||
// Everything from here on has to find the repo the way git does, from the
|
||||
// directory we're about to change to, so the location's environment goes into
|
||||
// the process env before we do. Usually that just clears whatever the repo
|
||||
// we're leaving needed, but going back to a repo whose git dir isn't in its
|
||||
// work tree (a dotfile repo opened with --git-dir/--work-tree, say) is the
|
||||
// reason we remember the environment at all: nothing in the path leads to its
|
||||
// git dir. On failure we put back what the repo we're staying in needs.
|
||||
func (self *ReposHelper) switchToLocation(location types.RepoLocation, errMsg string, contextKey types.ContextKey) error {
|
||||
originalPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
originalGitLocationEnvVars := env.GetGitLocationEnvVars()
|
||||
|
||||
msg := utils.ResolvePlaceholderString(self.c.Tr.ChangingDirectoryTo, map[string]string{"path": path})
|
||||
env.SetGitLocationEnvVars(location.GitLocationEnvVars)
|
||||
|
||||
msg := utils.ResolvePlaceholderString(self.c.Tr.ChangingDirectoryTo, map[string]string{"path": location.Path})
|
||||
self.c.LogCommand(msg, false)
|
||||
|
||||
if err := os.Chdir(path); err != nil {
|
||||
if err := os.Chdir(location.Path); err != nil {
|
||||
env.SetGitLocationEnvVars(originalGitLocationEnvVars)
|
||||
if os.IsNotExist(err) {
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
@@ -213,6 +234,7 @@ func (self *ReposHelper) switchTo(path string, errMsg string, contextKey types.C
|
||||
}
|
||||
|
||||
if err := commands.VerifyInGitRepo(self.c.OS()); err != nil {
|
||||
env.SetGitLocationEnvVars(originalGitLocationEnvVars)
|
||||
if err := os.Chdir(originalPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+5
-5
@@ -94,9 +94,9 @@ type Gui struct {
|
||||
|
||||
Mutexes types.Mutexes
|
||||
|
||||
// when you enter into a submodule we'll append the superproject's path to this array
|
||||
// so that you can return to the superproject
|
||||
RepoPathStack *utils.Stack[string]
|
||||
// when you enter into a submodule we'll append the superproject's location to
|
||||
// this array so that you can return to the superproject
|
||||
RepoPathStack *utils.Stack[types.RepoLocation]
|
||||
|
||||
// this tells us whether our views have been initially set up
|
||||
ViewsSetup bool
|
||||
@@ -158,7 +158,7 @@ type StateAccessor struct {
|
||||
|
||||
var _ types.IStateAccessor = new(StateAccessor)
|
||||
|
||||
func (self *StateAccessor) GetRepoPathStack() *utils.Stack[string] {
|
||||
func (self *StateAccessor) GetRepoPathStack() *utils.Stack[types.RepoLocation] {
|
||||
return self.gui.RepoPathStack
|
||||
}
|
||||
|
||||
@@ -799,7 +799,7 @@ func NewGui(
|
||||
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
|
||||
viewPtmxMap: map[string]oscommands.Pty{},
|
||||
showRecentRepos: showRecentRepos,
|
||||
RepoPathStack: &utils.Stack[string]{},
|
||||
RepoPathStack: &utils.Stack[types.RepoLocation]{},
|
||||
RepoStateMap: map[Repo]*GuiRepoState{},
|
||||
GuiLog: []string{},
|
||||
|
||||
|
||||
+10
-1
@@ -403,8 +403,17 @@ type HasUrn interface {
|
||||
URN() string
|
||||
}
|
||||
|
||||
// RepoLocation is everything it takes to open a repo again: the directory to
|
||||
// change to, plus the environment telling git where the repo is for the repos
|
||||
// git can't find from that directory (see RepoPaths.GitLocationEnvVars), which
|
||||
// is empty for all the others.
|
||||
type RepoLocation struct {
|
||||
Path string
|
||||
GitLocationEnvVars []string
|
||||
}
|
||||
|
||||
type IStateAccessor interface {
|
||||
GetRepoPathStack() *utils.Stack[string]
|
||||
GetRepoPathStack() *utils.Stack[RepoLocation]
|
||||
GetRepoState() IRepoStateAccessor
|
||||
GetDiffRendererConfigManager() *config.DiffRendererConfigManager
|
||||
// tells us whether we're currently updating lazygit
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package submodule
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
// Entering a submodule and escaping back out again, in a repo that git can only
|
||||
// find because we were told where it is (--git-dir/--work-tree). Entering the
|
||||
// submodule has to leave that behind, since it says where the superproject is,
|
||||
// so coming back out has to bring it along again.
|
||||
|
||||
var EnterFromDotfileBareRepo = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Enter a submodule of a dotfile bare repo and escape back out again",
|
||||
ExtraCmdArgs: []string{"--git-dir={{.actualPath}}/.bare", "--work-tree={{.actualPath}}/repo"},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
// we're going to have a directory structure like this:
|
||||
// project
|
||||
// - .bare (the git dir)
|
||||
// - repo (the work tree, with no .git of its own)
|
||||
// - my_submodule_name (the submodule's remote)
|
||||
//
|
||||
// The work tree is called 'repo' because that's the directory that all
|
||||
// lazygit tests start in
|
||||
|
||||
// make a repo for the submodule to be cloned from, using the .git dir
|
||||
// that every test starts with
|
||||
shell.EmptyCommit("initial submodule commit")
|
||||
shell.Clone("my_submodule_name")
|
||||
|
||||
// now turn the test repo into a dotfile-style bare repo
|
||||
shell.DeleteFile(".git")
|
||||
shell.RunCommand([]string{"git", "init", "--bare", "../.bare"})
|
||||
gitInBareRepo := []string{"git", "--git-dir=../.bare", "--work-tree=."}
|
||||
shell.RunCommand(append(gitInBareRepo, "checkout", "-b", "mybranch"))
|
||||
shell.CreateFile("blah", "blah\n")
|
||||
shell.RunCommand(append(gitInBareRepo, "add", "blah"))
|
||||
shell.RunCommand(append(gitInBareRepo, "commit", "-m", "initial commit"))
|
||||
shell.RunCommand(append(gitInBareRepo, "-c", "protocol.file.allow=always", "submodule",
|
||||
"add", "--name", "my_submodule_name", "../my_submodule_name", "my_submodule_path"))
|
||||
shell.RunCommand(append(gitInBareRepo, "commit", "-m", "add submodule"))
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
assertInParentRepo := func() {
|
||||
t.Views().Status().Content(Contains("repo"))
|
||||
t.Views().Commits().Lines(
|
||||
Contains("add submodule"),
|
||||
Contains("initial commit"),
|
||||
)
|
||||
}
|
||||
|
||||
assertInParentRepo()
|
||||
|
||||
t.Views().Submodules().Focus().
|
||||
Lines(
|
||||
Contains("my_submodule_name").IsSelected(),
|
||||
).
|
||||
PressEnter()
|
||||
|
||||
t.Views().Status().Content(Contains("my_submodule_path"))
|
||||
t.Views().Commits().Lines(
|
||||
Contains("initial submodule commit"),
|
||||
)
|
||||
|
||||
t.Views().Files().IsFocused().PressEscape()
|
||||
|
||||
assertInParentRepo()
|
||||
t.Views().Submodules().IsFocused()
|
||||
},
|
||||
})
|
||||
@@ -441,6 +441,7 @@ var tests = []*components.IntegrationTest{
|
||||
status.LogCmdStatusPanelAllBranchesLog,
|
||||
submodule.Add,
|
||||
submodule.Enter,
|
||||
submodule.EnterFromDotfileBareRepo,
|
||||
submodule.EnterNested,
|
||||
submodule.Remove,
|
||||
submodule.RemoveNested,
|
||||
|
||||
Reference in New Issue
Block a user