From 6905d62af6c3edd8a48bb4c68476cb4acc3469c3 Mon Sep 17 00:00:00 2001 From: phanium <91544758+phanen@users.noreply.github.com> Date: Sun, 26 Jul 2026 00:23:43 +0800 Subject: [PATCH] Fix multi-select ignore/exclude for tracked ranges A range select that crosses a tracked directory boundary breaks because `git rm -r --cached -- dir` removes its descendants from the index, so a subsequent child-targeted call fails with pathspec errors. Untracked nodes in a mixed range hit the same problem since they aren't in the index at all. Filter the selection through `normalisedSelectedNodes` and only unstage/rm the nodes that are actually tracked. --- pkg/gui/controllers/files_controller.go | 18 ++++--- .../file/gitignore_tracked_range_select.go | 50 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 3 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 pkg/integration/tests/file/gitignore_tracked_range_select.go diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 94c0a8132..3c88701c9 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -974,15 +974,19 @@ func (self *FilesController) unstageFiles(node *filetree.FileNode) error { func (self *FilesController) ignoreOrExcludeTracked(nodes []*filetree.FileNode, trAction string, f func([]string) error) error { self.c.LogAction(trAction) + nodes = normalisedSelectedNodes(nodes) + paths := make([]string, 0, len(nodes)) for _, node := range nodes { - // not 100% sure if this is necessary but I'll assume it is - if err := self.unstageFiles(node); err != nil { - return err - } + if node.GetIsTracked() { + // not 100% sure if this is necessary but I'll assume it is + if err := self.unstageFiles(node); err != nil { + return err + } - if err := self.c.Git().WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil { - return err + if err := self.c.Git().WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil { + return err + } } paths = append(paths, node.GetPath()) @@ -999,6 +1003,8 @@ func (self *FilesController) ignoreOrExcludeTracked(nodes []*filetree.FileNode, func (self *FilesController) ignoreOrExcludeUntracked(nodes []*filetree.FileNode, trAction string, f func([]string) error) error { self.c.LogAction(trAction) + nodes = normalisedSelectedNodes(nodes) + paths := make([]string, 0, len(nodes)) for _, node := range nodes { paths = append(paths, node.GetPath()) diff --git a/pkg/integration/tests/file/gitignore_tracked_range_select.go b/pkg/integration/tests/file/gitignore_tracked_range_select.go new file mode 100644 index 000000000..1e754cb86 --- /dev/null +++ b/pkg/integration/tests/file/gitignore_tracked_range_select.go @@ -0,0 +1,50 @@ +package file + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var GitignoreTrackedRangeSelect = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Range-select across a tracked directory and its children (parent+child case)", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("dir-tracked/file-a", "x") + shell.CreateFileAndAdd("dir-tracked/file-b", "x") + shell.CreateFileAndAdd("tracked1", "x") + shell.Commit("initial") + shell.UpdateFile("dir-tracked/file-a", "y") + shell.UpdateFile("dir-tracked/file-b", "y") + shell.UpdateFile("tracked1", "y") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Equals("▼ /").IsSelected(), + Equals(" ▼ dir-tracked"), + Equals(" M file-a"), + Equals(" M file-b"), + Equals(" M tracked1"), + ). + NavigateToLine(Contains("dir-tracked")). + Press(keys.Universal.ToggleRangeSelect). + NavigateToLine(Contains("tracked1")). + Press(keys.Files.IgnoreFile). + Tap(func() { + t.ExpectPopup().Menu().Title(Equals("Ignore or exclude file")). + Select(Contains("Add to .gitignore")). + Confirm() + + t.ExpectPopup().Confirmation(). + Title(Equals("Ignore tracked file")). + Content(Contains("tracked file")). + Confirm() + + t.FileSystem().FileContent(".gitignore", Equals("/dir-tracked\n/tracked1\n")) + }) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index a6e9771e0..78438c48b 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -242,6 +242,7 @@ var tests = []*components.IntegrationTest{ file.Gitignore, file.GitignoreRangeSelect, file.GitignoreSpecialCharacters, + file.GitignoreTrackedRangeSelect, file.RememberCommitMessageAfterFail, file.RenameSimilarityThresholdChange, file.RenamedFiles,