Fix removing a submodule in a linked worktree breaking the submodule in the main worktree (#6186)

Removing a submodule in a linked worktree deletes the git dir of that
submodule in the main worktree. This breaks the submodule there, and any
unpushed commits, local branches and stashes in it are lost. The git dir
of the submodule in the linked worktree stays behind.

Git keeps the git dirs of submodules separately for each worktree. For a
linked worktree, they are in `.git/worktrees/<worktree>/modules`.
Lazygit used the worktree's git dir until #3341 switched it to the
repo's git dir, so this has been broken since v0.41.0. This PR goes back
to using the worktree's git dir.
This commit is contained in:
Stefan Haller
2026-11-07 23:59:05 +01:00
committed by GitHub
4 changed files with 54 additions and 6 deletions
+2 -3
View File
@@ -269,9 +269,8 @@ func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
self.Log.Error(err)
}
// 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(submodule.GitDirPath(self.repoPaths.repoGitDirPath))
// Git keeps the git dirs of submodules separately for each worktree
return os.RemoveAll(submodule.GitDirPath(self.repoPaths.WorktreeGitDirPath()))
}
func (self *SubmoduleCommands) Add(name string, path string, url string) error {
+3 -3
View File
@@ -38,10 +38,10 @@ func (r *SubmoduleConfig) Description() string {
return r.RefName()
}
func (r *SubmoduleConfig) GitDirPath(repoGitDirPath string) string {
parentPath := repoGitDirPath
func (r *SubmoduleConfig) GitDirPath(worktreeGitDirPath string) string {
parentPath := worktreeGitDirPath
if r.ParentModule != nil {
parentPath = r.ParentModule.GitDirPath(repoGitDirPath)
parentPath = r.ParentModule.GitDirPath(worktreeGitDirPath)
}
return filepath.Join(parentPath, "modules", r.Name)
@@ -0,0 +1,48 @@
package submodule
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var RemoveInLinkedWorktree = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Remove a submodule in a linked worktree",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("first commit")
shell.CloneIntoSubmodule("my_submodule_name", "my_submodule_path")
shell.GitAddAll()
shell.Commit("add submodule")
shell.AddWorktree("HEAD", "../linked-worktree", "mybranch")
shell.Chdir("../linked-worktree")
shell.RunCommand([]string{"git", "submodule", "update", "--init"})
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
// Git keeps the git dirs of submodules separately for each worktree
mainGitDirSubmodulePath := "../repo/.git/modules/my_submodule_name"
linkedGitDirSubmodulePath := "../repo/.git/worktrees/linked-worktree/modules/my_submodule_name"
t.FileSystem().
PathPresent(mainGitDirSubmodulePath).
PathPresent(linkedGitDirSubmodulePath)
t.Views().Submodules().Focus().
Lines(
Contains("my_submodule_name").IsSelected(),
).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Remove submodule")).
Content(Equals("Are you sure you want to remove submodule 'my_submodule_name' and its corresponding directory? This is irreversible.")).
Confirm()
}).
IsEmpty()
t.FileSystem().
PathPresent(mainGitDirSubmodulePath).
PathNotPresent(linkedGitDirSubmodulePath)
},
})
+1
View File
@@ -579,6 +579,7 @@ var tests = []*components.IntegrationTest{
submodule.EnterFromDotfileBareRepo,
submodule.EnterNested,
submodule.Remove,
submodule.RemoveInLinkedWorktree,
submodule.RemoveNested,
submodule.Reset,
submodule.ResetFolder,