From 6a022241d2aecc852af7be4faa640dee5e9472f9 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 28 Jul 2026 17:57:24 +0200 Subject: [PATCH 1/4] Add test to demonstrate a problem with custom patches and directories sharing a prefix We had the same bug in the files panel, and fixed it in a5eec48b4b8, but forgot to make the equivalent change to the commit files panel. --- .../select_direcories_sharing_prefix.go | 59 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 2 files changed, 60 insertions(+) create mode 100644 pkg/integration/tests/patch_building/select_direcories_sharing_prefix.go diff --git a/pkg/integration/tests/patch_building/select_direcories_sharing_prefix.go b/pkg/integration/tests/patch_building/select_direcories_sharing_prefix.go new file mode 100644 index 000000000..2c0059dd0 --- /dev/null +++ b/pkg/integration/tests/patch_building/select_direcories_sharing_prefix.go @@ -0,0 +1,59 @@ +package patch_building + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var SelectDirecoriesSharingPrefix = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Select directories sharing a prefix in the commit files view and add them to a custom patch", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("foo/file", "file1 content") + shell.CreateFileAndAdd("foobar/file", "file2 content") + shell.Commit("first commit") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("first commit").IsSelected(), + ). + PressEnter() + + t.Views().CommitFiles(). + IsFocused(). + Lines( + Equals("▼ /").IsSelected(), + Equals(" ▼ foo"), + Equals(" A file"), + Equals(" ▼ foobar"), + Equals(" A file"), + ). + SelectNextItem(). + Press(keys.Universal.ToggleRangeSelect). + NavigateToLine(Contains("foobar")). + PressPrimaryAction(). + Lines( + Equals("▼ /"), + Equals(" ▼ foo").IsSelected(), + Equals(" ● file").IsSelected(), + Equals(" ▼ foobar").IsSelected(), + /* EXPECTED: + Equals(" ● file"), + ACTUAL: */ + Equals(" A file"), + ) + + t.Views().Information().Content(Contains("Building patch")) + + t.Views().Secondary().Content( + /* EXPECTED: + Contains("foo/file").Contains("foobar/file"), + ACTUAL: */ + Contains("foo/file").DoesNotContain("foobar/file"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 07a12e2be..77e54e265 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -382,6 +382,7 @@ var tests = []*components.IntegrationTest{ patch_building.RenamedFileWhole, patch_building.ResetWithEscape, patch_building.SelectAllFiles, + patch_building.SelectDirecoriesSharingPrefix, patch_building.SpecificSelection, patch_building.StartNewPatch, patch_building.ToggleDirectory, From 8fefe2b9335cb0879b24327eff4296543b474ac8 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 28 Jul 2026 17:37:30 +0200 Subject: [PATCH 2/4] Cleanup: move variable assignment out of the loop It never changes inside this function, so there's no need to recompute it with every loop iteration. Equivalent to the change that was made to isDescendentOfSelectedNodes in files_controller.go in d0c6e27fee9b4. --- pkg/gui/controllers/commits_files_controller.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index d129b3f90..5d4377fe1 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -642,9 +642,10 @@ func normalisedSelectedCommitFileNodes(selectedNodes []*filetree.CommitFileNode) } func isDescendentOfSelectedCommitFileNodes(node *filetree.CommitFileNode, selectedNodes []*filetree.CommitFileNode) bool { + nodePath := node.GetPath() + for _, selectedNode := range selectedNodes { selectedNodePath := selectedNode.GetPath() - nodePath := node.GetPath() if strings.HasPrefix(nodePath, selectedNodePath) && nodePath != selectedNodePath { return true From c7acf383990273b03cea012e4d14ab940c73333e Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 28 Jul 2026 17:40:03 +0200 Subject: [PATCH 3/4] Make isDescendentOfSelectedCommitFileNodes work for the root item The root item's path is ".", and the path of a file at top level is "./file". When using GetPath, this gives us "." and "file", respectively, and isDescendentOfSelectedCommitFileNodes would return false for these. Working with the internal paths (i.e. without stripping the leading "./") fixes this. There is no known breakage that is caused by this, that's why I'm not adding an integration test that demonstrates a bug. Equivalent to the change that was made to isDescendentOfSelectedNodes in files_controller.go in 302b621b681. --- pkg/gui/controllers/commits_files_controller.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 5d4377fe1..da24b155c 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -642,10 +642,10 @@ func normalisedSelectedCommitFileNodes(selectedNodes []*filetree.CommitFileNode) } func isDescendentOfSelectedCommitFileNodes(node *filetree.CommitFileNode, selectedNodes []*filetree.CommitFileNode) bool { - nodePath := node.GetPath() + nodePath := node.GetInternalPath() for _, selectedNode := range selectedNodes { - selectedNodePath := selectedNode.GetPath() + selectedNodePath := selectedNode.GetInternalPath() if strings.HasPrefix(nodePath, selectedNodePath) && nodePath != selectedNodePath { return true From 1d107721f2b0c063913fc80b3f1bedd00fa01e50 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 28 Jul 2026 17:45:40 +0200 Subject: [PATCH 4/4] Fix multi-selection of files with common prefix not working in commit files panel Equivalent to the change that was made to isDescendentOfSelectedNodes in files_controller.go in a5eec48b4b8. --- pkg/gui/controllers/commits_files_controller.go | 6 +++++- pkg/gui/controllers/files_controller.go | 2 ++ .../patch_building/select_direcories_sharing_prefix.go | 6 ------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index da24b155c..6c8f49e1f 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -645,9 +645,13 @@ func isDescendentOfSelectedCommitFileNodes(node *filetree.CommitFileNode, select nodePath := node.GetInternalPath() for _, selectedNode := range selectedNodes { + if selectedNode.IsFile() { + continue + } + selectedNodePath := selectedNode.GetInternalPath() - if strings.HasPrefix(nodePath, selectedNodePath) && nodePath != selectedNodePath { + if strings.HasPrefix(nodePath, selectedNodePath+"/") { return true } } diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 656da1bf5..567b0b6e5 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -1552,6 +1552,8 @@ func normalisedSelectedNodes(selectedNodes []*filetree.FileNode) []*filetree.Fil }) } +// NOTE: there's a duplicate of this function in commits_files_controller.go; if you make +// changes here, make them there, too. (We should unify them using generics.) func isDescendentOfSelectedNodes(node *filetree.FileNode, selectedNodes []*filetree.FileNode) bool { nodePath := node.GetInternalPath() diff --git a/pkg/integration/tests/patch_building/select_direcories_sharing_prefix.go b/pkg/integration/tests/patch_building/select_direcories_sharing_prefix.go index 2c0059dd0..bd20e4ff5 100644 --- a/pkg/integration/tests/patch_building/select_direcories_sharing_prefix.go +++ b/pkg/integration/tests/patch_building/select_direcories_sharing_prefix.go @@ -41,19 +41,13 @@ var SelectDirecoriesSharingPrefix = NewIntegrationTest(NewIntegrationTestArgs{ Equals(" ▼ foo").IsSelected(), Equals(" ● file").IsSelected(), Equals(" ▼ foobar").IsSelected(), - /* EXPECTED: Equals(" ● file"), - ACTUAL: */ - Equals(" A file"), ) t.Views().Information().Content(Contains("Building patch")) t.Views().Secondary().Content( - /* EXPECTED: Contains("foo/file").Contains("foobar/file"), - ACTUAL: */ - Contains("foo/file").DoesNotContain("foobar/file"), ) }, })