diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index ce53ef65b..64ebb8d15 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -1691,24 +1691,41 @@ func findSubmoduleNode(nodes []*filetree.FileNode, submodules []*models.Submodul return nil } +func findWorktreeNode(nodes []*filetree.FileNode) *models.File { + for _, node := range nodes { + if worktreeFile := node.FindFirstFileBy(func(f *models.File) bool { return f.IsWorktree }); worktreeFile != nil { + return worktreeFile + } + } + return nil +} + func (self *FilesController) canRemove(selectedNodes []*filetree.FileNode) *types.DisabledReason { - // Return disabled if the selection contains multiple changed items and includes a submodule change. + // Return disabled if the selection contains multiple changed items and + // includes a submodule change or a worktree. These have menus of their own. submodules := self.c.Model().Submodules hasFiles := false uniqueSelectedSubmodules := set.New[*models.SubmoduleConfig]() + uniqueSelectedWorktreePaths := set.New[string]() for _, node := range selectedNodes { _ = node.ForEachFile(func(f *models.File) error { if submodule := f.SubmoduleConfig(submodules); submodule != nil { uniqueSelectedSubmodules.Add(submodule) + } else if f.IsWorktree { + uniqueSelectedWorktreePaths.Add(f.Path) } else { hasFiles = true } return nil }) - if uniqueSelectedSubmodules.Len() > 0 && (hasFiles || uniqueSelectedSubmodules.Len() > 1) { + if uniqueSelectedSubmodules.Len() > 0 && + (hasFiles || uniqueSelectedWorktreePaths.Len() > 0 || uniqueSelectedSubmodules.Len() > 1) { return &types.DisabledReason{Text: self.c.Tr.MultiSelectNotSupportedForSubmodules} } + if uniqueSelectedWorktreePaths.Len() > 0 && (hasFiles || uniqueSelectedWorktreePaths.Len() > 1) { + return &types.DisabledReason{Text: self.c.Tr.MultiSelectNotSupportedForWorktrees} + } } return nil @@ -1737,6 +1754,13 @@ func (self *FilesController) remove(selectedNodes []*filetree.FileNode) error { return self.c.Menu(types.CreateMenuOptions{Title: submoduleNode.GetPath(), Items: menuItems}) } + // Likewise, if we have a worktree, it is the only thing selected + if worktreeFile := findWorktreeNode(selectedNodes); worktreeFile != nil { + if worktree, ok := self.c.Helpers().WorkingTree.WorktreeForFile(worktreeFile); ok { + return self.c.Helpers().BranchesHelper.RemoveWorktreeMenu(worktree) + } + } + discardAllChangesItem := types.MenuItem{ Label: self.c.Tr.DiscardAllChanges, OnPress: func() error { diff --git a/pkg/gui/controllers/helpers/branches_helper.go b/pkg/gui/controllers/helpers/branches_helper.go index 849226e03..8117f4bae 100644 --- a/pkg/gui/controllers/helpers/branches_helper.go +++ b/pkg/gui/controllers/helpers/branches_helper.go @@ -293,8 +293,8 @@ func (self *BranchesHelper) RemoveWorktreeMenu(worktree *models.Worktree) error // RemoveWorktreeAndDeleteBranch removes the worktree and deletes the local branch // it has checked out, force-warning first if the branch isn't fully merged. It's -// the worktrees-panel counterpart to deleting a worktree-checked-out branch from -// the branches panel. +// the remove-worktree menu's counterpart to deleting a worktree-checked-out +// branch from the branches panel. func (self *BranchesHelper) RemoveWorktreeAndDeleteBranch( worktree *models.Worktree, branch *models.Branch, ) error { diff --git a/pkg/gui/controllers/helpers/working_tree_helper.go b/pkg/gui/controllers/helpers/working_tree_helper.go index 5f68cb822..1cdc99606 100644 --- a/pkg/gui/controllers/helpers/working_tree_helper.go +++ b/pkg/gui/controllers/helpers/working_tree_helper.go @@ -113,6 +113,12 @@ func (self *WorkingTreeHelper) FileForSubmodule(submodule *models.SubmoduleConfi return nil } +// WorktreeForFile returns the worktree that an entry of the files panel is, if +// it is one +func (self *WorkingTreeHelper) WorktreeForFile(file *models.File) (*models.Worktree, bool) { + return worktreeOfFile(file, self.c.Model().Worktrees, self.c.Git().RepoPaths.WorktreePath()) +} + func (self *WorkingTreeHelper) OpenMergeTool() error { self.c.LogAction(self.c.Tr.Actions.OpenMergeTool) return self.c.RunSubprocessAndRefresh( diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index e961638d3..81dd2c335 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -1003,6 +1003,7 @@ type TranslationSet struct { SelectedItemIsNotABranch string SelectedItemDoesNotHaveFiles string MultiSelectNotSupportedForSubmodules string + MultiSelectNotSupportedForWorktrees string NothingToStageForSubmodule string CommandDoesNotSupportOpeningInEditor string CustomCommands string @@ -2199,6 +2200,7 @@ func EnglishTranslationSet() *TranslationSet { SelectedItemIsNotABranch: "Selected item is not a branch", SelectedItemDoesNotHaveFiles: "Selected item does not have files to view", MultiSelectNotSupportedForSubmodules: "Multiselection not supported for submodules", + MultiSelectNotSupportedForWorktrees: "Multiselection not supported for worktrees", NothingToStageForSubmodule: "Nothing to stage: the parent repo can only stage a new submodule commit, not the uncommitted changes inside a submodule. Commit inside the submodule first.", CommandDoesNotSupportOpeningInEditor: "This command doesn't support switching to the editor", CustomCommands: "Custom commands", diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 9461fc2d3..8e8e27aec 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -696,6 +696,7 @@ var tests = []*components.IntegrationTest{ worktree.CustomCommand, worktree.DefaultPathTilde, worktree.DetachWorktreeFromBranch, + worktree.DiscardRangeWithWorktree, worktree.DotfileBareRepo, worktree.DoubleNestedLinkedSubmodule, worktree.EditFileOfBranchInOtherWorktree, diff --git a/pkg/integration/tests/worktree/discard_range_with_worktree.go b/pkg/integration/tests/worktree/discard_range_with_worktree.go new file mode 100644 index 000000000..064918479 --- /dev/null +++ b/pkg/integration/tests/worktree/discard_range_with_worktree.go @@ -0,0 +1,35 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DiscardRangeWithWorktree = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Discarding a range of files that includes a worktree inside the repo is not supported, because a worktree has its own menu for removing it", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Gui.ShowFileTree = false + }, + SetupRepo: func(shell *Shell) { + shell.NewBranch("mybranch") + shell.CreateFileAndAdd("README.md", "hello world") + shell.Commit("initial commit") + shell.AddWorktree("mybranch", "nested-worktree", "newbranch") + shell.CreateFile("untracked-file", "content") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Equals("?? nested-worktree (worktree)").IsSelected(), + Equals("?? untracked-file"), + ). + Press(keys.Universal.RangeSelectDown). + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectToast(Equals("Disabled: Multiselection not supported for worktrees")) + }) + }, +}) diff --git a/pkg/integration/tests/worktree/worktree_in_repo.go b/pkg/integration/tests/worktree/worktree_in_repo.go index f6b5b44a3..d05fc9136 100644 --- a/pkg/integration/tests/worktree/worktree_in_repo.go +++ b/pkg/integration/tests/worktree/worktree_in_repo.go @@ -64,18 +64,18 @@ var WorktreeInRepo = NewIntegrationTest(NewIntegrationTestArgs{ Press(keys.Universal.Remove). Tap(func() { t.ExpectPopup().Menu(). - Title(Equals("Discard changes")). - Select(Contains("Discard all changes")). + Title(Equals("Remove worktree 'linked-worktree'?")). + Select(MatchesRegexp("Remove worktree$")). Confirm() }). IsEmpty() - // confirm worktree appears as missing + t.FileSystem().PathNotPresent("linked-worktree") + t.Views().Worktrees(). Focus(). Lines( Contains("(main worktree)").IsSelected(), - Contains("linked-worktree (missing)"), ) }, })