diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 9ac14f3e5..c9034ec9c 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -44,7 +44,7 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types { Keys: opts.GetKeys(opts.Config.Universal.Select), Handler: self.withItems(self.press), - GetDisabledReason: self.require(self.withFileTreeViewModelMutex(self.itemsSelected())), + GetDisabledReason: self.require(self.withFileTreeViewModelMutex(self.itemsSelected(self.canStageSelection))), Description: self.c.Tr.Stage, Tooltip: self.c.Tr.StageTooltip, DisplayOnScreen: true, @@ -583,6 +583,12 @@ func (self *FilesController) pressWithLock(selectedNodes []*filetree.FileNode) e } func (self *FilesController) press(nodes []*filetree.FileNode) error { + // A single file with a conflict that can only be resolved through a dialog + // can't be staged; route it to the same picker that `enter` uses instead. + if len(nodes) == 1 && self.conflictNeedsResolutionDialog(nodes[0].File) { + return self.openConflictResolutionMenu(nodes[0].File) + } + if err := self.pressWithLock(nodes); err != nil { return err } @@ -716,6 +722,27 @@ func (self *FilesController) conflictNeedsResolutionDialog(file *models.File) bo return !file.HasInlineMergeConflicts } +// canStageSelection disables staging when a multiple selection includes a file +// with a conflict that must be resolved through a dialog; those have to be +// resolved one at a time. +func (self *FilesController) canStageSelection(nodes []*filetree.FileNode) *types.DisabledReason { + if len(nodes) > 1 { + for _, node := range nodes { + if node.SomeFile(self.conflictNeedsResolutionDialog) { + return &types.DisabledReason{ + Text: utils.ResolvePlaceholderString( + self.c.Tr.StageConflictsRangeDisabled, map[string]string{ + "goIntoKey": self.c.UserConfig().Keybinding.Universal.GoInto.String(), + }, + ), + } + } + } + } + + return nil +} + func (self *FilesController) openConflictResolutionMenu(file *models.File) error { handle := func(command func(command string) error, logText string) error { self.c.LogAction(logText) diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 8dd40016a..3928aac38 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -101,6 +101,7 @@ type TranslationSet struct { MergeConflictPressEnterToResolve string MergeConflictKeepFile string MergeConflictDeleteFile string + StageConflictsRangeDisabled string Checkout string CheckoutTooltip string CantCheckoutBranchWhilePulling string @@ -1200,6 +1201,7 @@ func EnglishTranslationSet() *TranslationSet { MergeConflictPressEnterToResolve: "Press %s to resolve.", MergeConflictKeepFile: "Keep file", MergeConflictDeleteFile: "Delete file", + StageConflictsRangeDisabled: "Cannot stage a selection that includes files with merge conflicts; resolve them individually with {{.goIntoKey}} first.", Checkout: "Checkout", CheckoutTooltip: "Checkout selected item.", CantCheckoutBranchWhilePulling: "You cannot checkout another branch while pulling the current branch", diff --git a/pkg/integration/tests/conflicts/space_on_non_textual_conflict.go b/pkg/integration/tests/conflicts/space_on_non_textual_conflict.go new file mode 100644 index 000000000..2899fadff --- /dev/null +++ b/pkg/integration/tests/conflicts/space_on_non_textual_conflict.go @@ -0,0 +1,56 @@ +package conflicts + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var SpaceOnNonTextualConflict = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Pressing space on a non-textual conflict opens the resolution menu; staging is disabled for a range that includes one", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Gui.ShowFileTree = false + }, + SetupRepo: func(shell *Shell) { + shell.RunShellCommand(`echo 1 > foo && echo 1 > bar`) + shell.RunShellCommand(`git checkout -b base && git add . && git commit -m base`) + + // theirs: delete foo, modify bar + shell.RunShellCommand(`git checkout -b theirs`) + shell.RunShellCommand(`git rm foo && echo 2 > bar && git add bar && git commit -m theirs`) + + // ours: modify foo, delete bar + shell.RunShellCommand(`git checkout base && git checkout -b ours`) + shell.RunShellCommand(`echo 2 > foo && git add foo && git rm bar && git commit -m ours`) + + shell.RunCommandExpectError([]string{"git", "merge", "theirs"}) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains("DU bar"), + Contains("UD foo"), + ). + // Pressing space on a single non-textual conflict opens the + // resolution menu rather than trying to stage it. + NavigateToLine(Contains("bar")). + PressPrimaryAction(). + Tap(func() { + t.ExpectPopup().Menu().Title(Equals("Merge conflicts")).Cancel() + }). + // Staging is disabled for a range selection that includes a conflict. + Press(keys.Universal.ToggleRangeSelect). + NavigateToLine(Contains("foo")). + PressPrimaryAction(). + Tap(func() { + t.ExpectToast(Contains("Cannot stage a selection that includes files with merge conflicts")) + }). + // Entering a range selection is disabled too, with the usual toast. + Press(keys.Universal.GoInto). + Tap(func() { + t.ExpectToast(Contains("does not support range selection")) + }) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 3cadf6843..b27577362 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -172,6 +172,7 @@ var tests = []*components.IntegrationTest{ conflicts.ResolveNoAutoStage, conflicts.ResolveNonTextualConflicts, conflicts.ResolveWithoutTrailingLf, + conflicts.SpaceOnNonTextualConflict, conflicts.UndoChooseHunk, custom_commands.AccessCommitProperties, custom_commands.BasicCommand,