From b61ca21a8419c05fe5876bc020a40408321634c6 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Thu, 13 Jul 2023 18:36:39 +1000 Subject: [PATCH] Allow checking for merge conflicts after running a custom command We have a use-case to rebind 'm' to the merge action in the branches panel. There's three ways to handle this: 1) For all global keybindings, define a per-panel key that invokes it 2) Give a name to all controller actions and allow them to be invoked in custom commands 3) Allow checking for merge conflicts after running a custom command so that users can add their own 'git merge' custom command that matches the in-built action Option 1 is hairy, Option 2 though good for users introduces new backwards compatibility issues that I don't want to do right now, and option 3 is trivially easy to implement so that's what I'm doing. I've put this under an 'after' key so that we can add more things later. I'm imagining other things like being able to move the cursor to a newly added item etc. I considered always running this hook by default but I'd rather not: it's matching on the output text and I'd rather something like that be explicitly opted-into to avoid cases where we erroneously believe that there are conflicts. --- docs/Custom_Command_Keybindings.md | 6 ++ pkg/config/user_config.go | 23 +++++--- .../helpers/merge_and_rebase_helper.go | 58 ++++++++++++------- pkg/gui/services/custom_commands/client.go | 7 ++- .../custom_commands/handler_creator.go | 40 ++++++++----- .../custom_commands/check_for_conflicts.go | 40 +++++++++++++ pkg/integration/tests/test_list.go | 1 + 7 files changed, 128 insertions(+), 47 deletions(-) create mode 100644 pkg/integration/tests/custom_commands/check_for_conflicts.go diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md index 7cc3d035b..6b0a090ed 100644 --- a/docs/Custom_Command_Keybindings.md +++ b/docs/Custom_Command_Keybindings.md @@ -59,6 +59,12 @@ For a given custom command, here are the allowed fields: | description | Label for the custom command when displayed in the keybindings menu | no | | stream | Whether you want to stream the command's output to the Command Log panel | no | | showOutput | Whether you want to show the command's output in a popup within Lazygit | no | +| after | Actions to take after the command has completed | no | + +Here are the options for the `after` key: +| _field_ | _description_ | required | +|-----------------|----------------------|-| +| checkForConflicts | true/false. If true, check for merge conflicts | no | ## Contexts diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 8faff4326..5b7edc9e4 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -349,16 +349,21 @@ type OSConfig struct { OpenLinkCommand string `yaml:"openLinkCommand,omitempty"` } +type CustomCommandAfterHook struct { + CheckForConflicts bool `yaml:"checkForConflicts"` +} + type CustomCommand struct { - Key string `yaml:"key"` - Context string `yaml:"context"` - Command string `yaml:"command"` - Subprocess bool `yaml:"subprocess"` - Prompts []CustomCommandPrompt `yaml:"prompts"` - LoadingText string `yaml:"loadingText"` - Description string `yaml:"description"` - Stream bool `yaml:"stream"` - ShowOutput bool `yaml:"showOutput"` + Key string `yaml:"key"` + Context string `yaml:"context"` + Command string `yaml:"command"` + Subprocess bool `yaml:"subprocess"` + Prompts []CustomCommandPrompt `yaml:"prompts"` + LoadingText string `yaml:"loadingText"` + Description string `yaml:"description"` + Stream bool `yaml:"stream"` + ShowOutput bool `yaml:"showOutput"` + After CustomCommandAfterHook `yaml:"after"` } type CustomCommandPrompt struct { diff --git a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go index 21ab44201..0e83ccf25 100644 --- a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go +++ b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go @@ -137,33 +137,47 @@ func (self *MergeAndRebaseHelper) CheckMergeOrRebase(result error) error { } else if strings.Contains(result.Error(), "No rebase in progress?") { // assume in this case that we're already done return nil - } else if isMergeConflictErr(result.Error()) { - mode := self.workingTreeStateNoun() - return self.c.Menu(types.CreateMenuOptions{ - Title: self.c.Tr.FoundConflictsTitle, - Items: []*types.MenuItem{ - { - Label: self.c.Tr.ViewConflictsMenuItem, - OnPress: func() error { - return self.c.PushContext(self.c.Contexts().Files) - }, - Key: 'v', - }, - { - Label: fmt.Sprintf(self.c.Tr.AbortMenuItem, mode), - OnPress: func() error { - return self.genericMergeCommand(REBASE_OPTION_ABORT) - }, - Key: 'a', - }, - }, - HideCancel: true, - }) + } else { + return self.CheckForConflicts(result) + } +} + +func (self *MergeAndRebaseHelper) CheckForConflicts(result error) error { + if result == nil { + return nil + } + + if isMergeConflictErr(result.Error()) { + return self.PromptForConflictHandling() } else { return self.c.ErrorMsg(result.Error()) } } +func (self *MergeAndRebaseHelper) PromptForConflictHandling() error { + mode := self.workingTreeStateNoun() + return self.c.Menu(types.CreateMenuOptions{ + Title: self.c.Tr.FoundConflictsTitle, + Items: []*types.MenuItem{ + { + Label: self.c.Tr.ViewConflictsMenuItem, + OnPress: func() error { + return self.c.PushContext(self.c.Contexts().Files) + }, + Key: 'v', + }, + { + Label: fmt.Sprintf(self.c.Tr.AbortMenuItem, mode), + OnPress: func() error { + return self.genericMergeCommand(REBASE_OPTION_ABORT) + }, + Key: 'a', + }, + }, + HideCancel: true, + }) +} + func (self *MergeAndRebaseHelper) AbortMergeOrRebaseWithConfirm() error { // prompt user to confirm that they want to abort, then do it mode := self.workingTreeStateNoun() diff --git a/pkg/gui/services/custom_commands/client.go b/pkg/gui/services/custom_commands/client.go index 5e456ff6e..c746f0579 100644 --- a/pkg/gui/services/custom_commands/client.go +++ b/pkg/gui/services/custom_commands/client.go @@ -19,7 +19,12 @@ func NewClient( helpers *helpers.Helpers, ) *Client { sessionStateLoader := NewSessionStateLoader(c, helpers.Refs) - handlerCreator := NewHandlerCreator(c, sessionStateLoader, helpers.Suggestions) + handlerCreator := NewHandlerCreator( + c, + sessionStateLoader, + helpers.Suggestions, + helpers.MergeAndRebase, + ) keybindingCreator := NewKeybindingCreator(c) customCommands := c.UserConfig.CustomCommands diff --git a/pkg/gui/services/custom_commands/handler_creator.go b/pkg/gui/services/custom_commands/handler_creator.go index 4d6580b03..eea596ab8 100644 --- a/pkg/gui/services/custom_commands/handler_creator.go +++ b/pkg/gui/services/custom_commands/handler_creator.go @@ -17,27 +17,30 @@ import ( // takes a custom command and returns a function that will be called when the corresponding user-defined keybinding is pressed type HandlerCreator struct { - c *helpers.HelperCommon - sessionStateLoader *SessionStateLoader - resolver *Resolver - menuGenerator *MenuGenerator - suggestionsHelper *helpers.SuggestionsHelper + c *helpers.HelperCommon + sessionStateLoader *SessionStateLoader + resolver *Resolver + menuGenerator *MenuGenerator + suggestionsHelper *helpers.SuggestionsHelper + mergeAndRebaseHelper *helpers.MergeAndRebaseHelper } func NewHandlerCreator( c *helpers.HelperCommon, sessionStateLoader *SessionStateLoader, suggestionsHelper *helpers.SuggestionsHelper, + mergeAndRebaseHelper *helpers.MergeAndRebaseHelper, ) *HandlerCreator { resolver := NewResolver(c.Common) menuGenerator := NewMenuGenerator(c.Common) return &HandlerCreator{ - c: c, - sessionStateLoader: sessionStateLoader, - resolver: resolver, - menuGenerator: menuGenerator, - suggestionsHelper: suggestionsHelper, + c: c, + sessionStateLoader: sessionStateLoader, + resolver: resolver, + menuGenerator: menuGenerator, + suggestionsHelper: suggestionsHelper, + mergeAndRebaseHelper: mergeAndRebaseHelper, } } @@ -272,7 +275,16 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses cmdObj.StreamOutput() } output, err := cmdObj.RunWithOutput() + + if refreshErr := self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil { + self.c.Log.Error(refreshErr) + } + if err != nil { + if customCommand.After.CheckForConflicts { + return self.mergeAndRebaseHelper.CheckForConflicts(err) + } + return self.c.Error(err) } @@ -280,11 +292,9 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses if strings.TrimSpace(output) == "" { output = self.c.Tr.EmptyOutput } - if err = self.c.Alert(cmdStr, output); err != nil { - return self.c.Error(err) - } - return self.c.Refresh(types.RefreshOptions{}) + return self.c.Alert(cmdStr, output) } - return self.c.Refresh(types.RefreshOptions{}) + + return nil }) } diff --git a/pkg/integration/tests/custom_commands/check_for_conflicts.go b/pkg/integration/tests/custom_commands/check_for_conflicts.go new file mode 100644 index 000000000..cb8ac7c77 --- /dev/null +++ b/pkg/integration/tests/custom_commands/check_for_conflicts.go @@ -0,0 +1,40 @@ +package custom_commands + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" + "github.com/jesseduffield/lazygit/pkg/integration/tests/shared" +) + +var CheckForConflicts = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Run a command and check for conflicts after", + ExtraCmdArgs: []string{}, + Skip: false, + SetupRepo: func(shell *Shell) { + shared.MergeConflictsSetup(shell) + }, + SetupConfig: func(cfg *config.AppConfig) { + cfg.UserConfig.CustomCommands = []config.CustomCommand{ + { + Key: "m", + Context: "localBranches", + Command: "git merge {{ .SelectedLocalBranch.Name | quote }}", + After: config.CustomCommandAfterHook{ + CheckForConflicts: true, + }, + }, + } + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + TopLines( + Contains("first-change-branch"), + Contains("second-change-branch"), + ). + NavigateToLine(Contains("second-change-branch")). + Press("m") + + t.Common().AcknowledgeConflicts() + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 55085f989..af0ac1c5b 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -75,6 +75,7 @@ var tests = []*components.IntegrationTest{ conflicts.UndoChooseHunk, custom_commands.BasicCmdAtRuntime, custom_commands.BasicCmdFromConfig, + custom_commands.CheckForConflicts, custom_commands.ComplexCmdAtRuntime, custom_commands.FormPrompts, custom_commands.MenuFromCommand,