diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 65ea941fd..25093a1c7 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -401,16 +401,18 @@ func (self *FilesController) pressWithLock(selectedNodes []*filetree.FileNode) e selectedNodes = normalisedSelectedNodes(selectedNodes) - // If any node has unstaged changes, we'll stage all the selected nodes. Otherwise, - // we unstage all the selected nodes. - if someNodesHaveUnstagedChanges(selectedNodes) { + // If any node has unstaged changes, we'll stage all the selected unstaged nodes (staging already staged deleted files/folders would fail). + // Otherwise, we unstage all the selected nodes. + unstagedSelectedNodes := filterNodesHaveUnstagedChanges(selectedNodes) + + if len(unstagedSelectedNodes) > 0 { self.c.LogAction(self.c.Tr.Actions.StageFile) - if err := self.optimisticChange(selectedNodes, self.optimisticStage); err != nil { + if err := self.optimisticChange(unstagedSelectedNodes, self.optimisticStage); err != nil { return err } - if err := self.c.Git().WorkingTree.StageFiles(toPaths(selectedNodes)); err != nil { + if err := self.c.Git().WorkingTree.StageFiles(toPaths(unstagedSelectedNodes)); err != nil { return err } } else { @@ -1031,6 +1033,12 @@ func someNodesHaveStagedChanges(nodes []*filetree.FileNode) bool { return lo.SomeBy(nodes, (*filetree.FileNode).GetHasStagedChanges) } +func filterNodesHaveUnstagedChanges(nodes []*filetree.FileNode) []*filetree.FileNode { + return lo.Filter(nodes, func(node *filetree.FileNode, _ int) bool { + return node.GetHasUnstagedChanges() + }) +} + func (self *FilesController) canRemove(selectedNodes []*filetree.FileNode) *types.DisabledReason { submodules := self.c.Model().Submodules submoduleCount := lo.CountBy(selectedNodes, func(node *filetree.FileNode) bool { diff --git a/pkg/integration/tests/file/stage_deleted_range_select.go b/pkg/integration/tests/file/stage_deleted_range_select.go new file mode 100644 index 000000000..d10188756 --- /dev/null +++ b/pkg/integration/tests/file/stage_deleted_range_select.go @@ -0,0 +1,50 @@ +package file + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var StageDeletedRangeSelect = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Stage a range of deleted files using range select", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file-a", "") + shell.CreateFileAndAdd("file-b", "") + shell.Commit("first commit") + + shell.DeleteFile("file-a") + shell.DeleteFile("file-b") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains(" D").Contains("file-a").IsSelected(), + Contains(" D").Contains("file-b"), + ). + // Stage a single deleted file + PressPrimaryAction(). + Lines( + Contains("D ").Contains("file-a").IsSelected(), + Contains(" D").Contains("file-b"), + ). + Press(keys.Universal.ToggleRangeSelect). + NavigateToLine(Contains("file-b")). + // Stage both files while a deleted file is already staged + PressPrimaryAction(). + Lines( + Contains("D ").Contains("file-a").IsSelected(), + Contains("D ").Contains("file-b").IsSelected(), + ). + // Unstage; back to everything being unstaged + PressPrimaryAction(). + Lines( + Contains(" D").Contains("file-a").IsSelected(), + Contains(" D").Contains("file-b").IsSelected(), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index fc126a949..8f547078a 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -163,6 +163,7 @@ var tests = []*components.IntegrationTest{ file.RememberCommitMessageAfterFail, file.RenameSimilarityThresholdChange, file.StageChildrenRangeSelect, + file.StageDeletedRangeSelect, file.StageRangeSelect, filter_and_search.FilterCommitFiles, filter_and_search.FilterFiles,