From 7568f5bf05739a5dedea1bf3d8c37c157dcf36dc Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 13 Oct 2025 15:15:14 +0200 Subject: [PATCH 1/2] Add test demonstrating the problem For a worktree that contains submodules, trying to delete it shows an error and doesn't offer to force-remove it. --- pkg/integration/tests/test_list.go | 1 + .../force_remove_worktree_with_submodules.go | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 pkg/integration/tests/worktree/force_remove_worktree_with_submodules.go diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 1abba20f6bb..a7a4f31c799 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -455,6 +455,7 @@ var tests = []*components.IntegrationTest{ worktree.FastForwardWorktreeBranch, worktree.FastForwardWorktreeBranchShouldNotPolluteCurrentWorktree, worktree.ForceRemoveWorktree, + worktree.ForceRemoveWorktreeWithSubmodules, worktree.RemoveWorktreeFromBranch, worktree.ResetWindowTabs, worktree.SymlinkIntoRepoSubdir, diff --git a/pkg/integration/tests/worktree/force_remove_worktree_with_submodules.go b/pkg/integration/tests/worktree/force_remove_worktree_with_submodules.go new file mode 100644 index 00000000000..02b8fced230 --- /dev/null +++ b/pkg/integration/tests/worktree/force_remove_worktree_with_submodules.go @@ -0,0 +1,58 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ForceRemoveWorktreeWithSubmodules = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Force remove a worktree that contains submodules", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.NewBranch("mybranch") + shell.CreateFileAndAdd("README.md", "hello world") + shell.Commit("initial commit") + shell.CloneIntoSubmodule("submodule", "submodule") + shell.Commit("Add submodule") + shell.AddWorktree("mybranch", "../linked-worktree", "newbranch") + shell.RunCommand([]string{"git", "-C", "../linked-worktree", "submodule", "update", "--init"}) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Worktrees(). + Focus(). + Lines( + Contains("repo (main)").IsSelected(), + Contains("linked-worktree"), + ). + NavigateToLine(Contains("linked-worktree")). + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Remove worktree")). + Content(Equals("Are you sure you want to remove worktree 'linked-worktree'?")). + Confirm() + + /* EXPECTED: + t.ExpectPopup().Confirmation(). + Title(Equals("Remove worktree")). + Content(Equals("'linked-worktree' contains modified or untracked files, or submodules (or all of these). Are you sure you want to remove it?")). + Confirm() + ACTUAL: */ + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Equals("fatal: working trees containing submodules cannot be moved or removed")). + Confirm() + }). + /* EXPECTED: + Lines( + Contains("repo (main)").IsSelected(), + ) + ACTUAL: */ + Lines( + Contains("repo (main)"), + Contains("linked-worktree").IsSelected(), + ) + }, +}) From 3415ed975b15671dbeb68194e08099091cc13b81 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 13 Oct 2025 15:00:09 +0200 Subject: [PATCH 2/2] Offer to force-delete a worktree if it contains submodules --- pkg/gui/controllers/helpers/worktree_helper.go | 3 ++- pkg/i18n/english.go | 2 +- .../tests/worktree/force_remove_worktree.go | 2 +- .../force_remove_worktree_with_submodules.go | 12 ------------ .../tests/worktree/remove_worktree_from_branch.go | 2 +- 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/pkg/gui/controllers/helpers/worktree_helper.go b/pkg/gui/controllers/helpers/worktree_helper.go index a54adcc490c..ea889781e88 100644 --- a/pkg/gui/controllers/helpers/worktree_helper.go +++ b/pkg/gui/controllers/helpers/worktree_helper.go @@ -189,7 +189,8 @@ func (self *WorktreeHelper) Remove(worktree *models.Worktree, force bool) error self.c.LogAction(self.c.Tr.RemoveWorktree) if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil { errMessage := err.Error() - if !strings.Contains(errMessage, "--force") { + if !strings.Contains(errMessage, "--force") && + !strings.Contains(errMessage, "fatal: working trees containing submodules cannot be moved or removed") { return err } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 86ba163aadb..6c474c0b049 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -1933,7 +1933,7 @@ func EnglishTranslationSet() *TranslationSet { RemoveWorktree: "Remove worktree", RemoveWorktreeTitle: "Remove worktree", RemoveWorktreePrompt: "Are you sure you want to remove worktree '{{.worktreeName}}'?", - ForceRemoveWorktreePrompt: "'{{.worktreeName}}' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?", + ForceRemoveWorktreePrompt: "'{{.worktreeName}}' contains modified or untracked files, or submodules (or all of these). Are you sure you want to remove it?", RemovingWorktree: "Deleting worktree", DetachWorktree: "Detach worktree", DetachingWorktree: "Detaching worktree", diff --git a/pkg/integration/tests/worktree/force_remove_worktree.go b/pkg/integration/tests/worktree/force_remove_worktree.go index 0d3bf90cfc6..e716b4e2ea6 100644 --- a/pkg/integration/tests/worktree/force_remove_worktree.go +++ b/pkg/integration/tests/worktree/force_remove_worktree.go @@ -36,7 +36,7 @@ var ForceRemoveWorktree = NewIntegrationTest(NewIntegrationTestArgs{ t.ExpectPopup().Confirmation(). Title(Equals("Remove worktree")). - Content(Equals("'linked-worktree' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?")). + Content(Equals("'linked-worktree' contains modified or untracked files, or submodules (or all of these). Are you sure you want to remove it?")). Confirm() }). Lines( diff --git a/pkg/integration/tests/worktree/force_remove_worktree_with_submodules.go b/pkg/integration/tests/worktree/force_remove_worktree_with_submodules.go index 02b8fced230..dd54eb97c2a 100644 --- a/pkg/integration/tests/worktree/force_remove_worktree_with_submodules.go +++ b/pkg/integration/tests/worktree/force_remove_worktree_with_submodules.go @@ -34,25 +34,13 @@ var ForceRemoveWorktreeWithSubmodules = NewIntegrationTest(NewIntegrationTestArg Content(Equals("Are you sure you want to remove worktree 'linked-worktree'?")). Confirm() - /* EXPECTED: t.ExpectPopup().Confirmation(). Title(Equals("Remove worktree")). Content(Equals("'linked-worktree' contains modified or untracked files, or submodules (or all of these). Are you sure you want to remove it?")). Confirm() - ACTUAL: */ - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Equals("fatal: working trees containing submodules cannot be moved or removed")). - Confirm() }). - /* EXPECTED: Lines( Contains("repo (main)").IsSelected(), ) - ACTUAL: */ - Lines( - Contains("repo (main)"), - Contains("linked-worktree").IsSelected(), - ) }, }) diff --git a/pkg/integration/tests/worktree/remove_worktree_from_branch.go b/pkg/integration/tests/worktree/remove_worktree_from_branch.go index f0b8de4e64c..b1e8cc1cec8 100644 --- a/pkg/integration/tests/worktree/remove_worktree_from_branch.go +++ b/pkg/integration/tests/worktree/remove_worktree_from_branch.go @@ -48,7 +48,7 @@ var RemoveWorktreeFromBranch = NewIntegrationTest(NewIntegrationTestArgs{ t.ExpectPopup().Confirmation(). Title(Equals("Remove worktree")). - Content(Equals("'linked-worktree' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?")). + Content(Equals("'linked-worktree' contains modified or untracked files, or submodules (or all of these). Are you sure you want to remove it?")). Confirm() }). Lines(