diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 16702f9be..ec27d66cb 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -68,6 +68,19 @@ func (self *CommitCommands) RewordLastCommitInEditorCmdObj() oscommands.ICmdObj return self.cmd.New(NewGitCmd("commit").Arg("--allow-empty", "--amend", "--only").ToArgv()) } +func (self *CommitCommands) RewordLastCommitInEditorWithMessageFileCmdObj(tmpMessageFile string) oscommands.ICmdObj { + return self.cmd.New(NewGitCmd("commit"). + Arg("--allow-empty", "--amend", "--only", "--edit", "--file="+tmpMessageFile).ToArgv()) +} + +func (self *CommitCommands) CommitInEditorWithMessageFileCmdObj(tmpMessageFile string) oscommands.ICmdObj { + return self.cmd.New(NewGitCmd("commit"). + Arg("--edit"). + Arg("--file="+tmpMessageFile). + ArgIf(self.signoffFlag() != "", self.signoffFlag()). + ToArgv()) +} + // RewordLastCommit rewords the topmost commit with the given message func (self *CommitCommands) RewordLastCommit(summary string, description string) error { messageArgs := self.commitMessageArgs(summary, description) diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 495e76df8..09631374f 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -132,16 +132,17 @@ type UpdateConfig struct { } type KeybindingConfig struct { - Universal KeybindingUniversalConfig `yaml:"universal"` - Status KeybindingStatusConfig `yaml:"status"` - Files KeybindingFilesConfig `yaml:"files"` - Branches KeybindingBranchesConfig `yaml:"branches"` - Worktrees KeybindingWorktreesConfig `yaml:"worktrees"` - Commits KeybindingCommitsConfig `yaml:"commits"` - Stash KeybindingStashConfig `yaml:"stash"` - CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"` - Main KeybindingMainConfig `yaml:"main"` - Submodules KeybindingSubmodulesConfig `yaml:"submodules"` + Universal KeybindingUniversalConfig `yaml:"universal"` + Status KeybindingStatusConfig `yaml:"status"` + Files KeybindingFilesConfig `yaml:"files"` + Branches KeybindingBranchesConfig `yaml:"branches"` + Worktrees KeybindingWorktreesConfig `yaml:"worktrees"` + Commits KeybindingCommitsConfig `yaml:"commits"` + Stash KeybindingStashConfig `yaml:"stash"` + CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"` + Main KeybindingMainConfig `yaml:"main"` + Submodules KeybindingSubmodulesConfig `yaml:"submodules"` + CommitMessage KeybindingCommitMessageConfig `yaml:"commitMessage"` } // damn looks like we have some inconsistencies here with -alt and -alt1 @@ -305,6 +306,10 @@ type KeybindingSubmodulesConfig struct { BulkMenu string `yaml:"bulkMenu"` } +type KeybindingCommitMessageConfig struct { + SwitchToEditor string `yaml:"switchToEditor"` +} + // OSConfig contains config on the level of the os type OSConfig struct { // Command for editing a file. Should contain "{{filename}}". @@ -652,6 +657,9 @@ func GetDefaultConfig() *UserConfig { Update: "u", BulkMenu: "b", }, + CommitMessage: KeybindingCommitMessageConfig{ + SwitchToEditor: "", + }, }, OS: OSConfig{}, DisableStartupPopups: false, diff --git a/pkg/gui/context/commit_message_context.go b/pkg/gui/context/commit_message_context.go index 4ad99e12c..1ac158839 100644 --- a/pkg/gui/context/commit_message_context.go +++ b/pkg/gui/context/commit_message_context.go @@ -5,7 +5,10 @@ import ( "strings" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" ) type CommitMessageContext struct { @@ -32,6 +35,8 @@ type CommitMessageViewModel struct { preservedMessage string // invoked when pressing enter in the commit message panel onConfirm func(string, string) error + // invoked when pressing the switch-to-editor key binding + onSwitchToEditor func(string) error // The message typed in before cycling through history // We store this separately to 'preservedMessage' because 'preservedMessage' @@ -98,12 +103,21 @@ func (self *CommitMessageContext) SetPanelState( descriptionTitle string, preserveMessage bool, onConfirm func(string, string) error, + onSwitchToEditor func(string) error, ) { self.viewModel.selectedindex = index self.viewModel.preserveMessage = preserveMessage self.viewModel.onConfirm = onConfirm + self.viewModel.onSwitchToEditor = onSwitchToEditor self.GetView().Title = summaryTitle self.c.Views().CommitDescription.Title = descriptionTitle + + subtitleTemplate := lo.Ternary(onSwitchToEditor != nil, self.c.Tr.CommitDescriptionSubTitle, self.c.Tr.CommitDescriptionSubTitleNoSwitch) + self.c.Views().CommitDescription.Subtitle = utils.ResolvePlaceholderString(subtitleTemplate, + map[string]string{ + "togglePanelKeyBinding": keybindings.Label(self.c.UserConfig.Keybinding.Universal.TogglePanel), + "switchToEditorKeyBinding": keybindings.Label(self.c.UserConfig.Keybinding.CommitMessage.SwitchToEditor), + }) } func (self *CommitMessageContext) RenderCommitLength() { @@ -117,3 +131,11 @@ func (self *CommitMessageContext) RenderCommitLength() { func getBufferLength(view *gocui.View) string { return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " " } + +func (self *CommitMessageContext) SwitchToEditor(message string) error { + return self.viewModel.onSwitchToEditor(message) +} + +func (self *CommitMessageContext) CanSwitchToEditor() bool { + return self.viewModel.onSwitchToEditor != nil +} diff --git a/pkg/gui/controllers/commit_description_controller.go b/pkg/gui/controllers/commit_description_controller.go index 78d275184..13bb5949f 100644 --- a/pkg/gui/controllers/commit_description_controller.go +++ b/pkg/gui/controllers/commit_description_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -34,6 +35,10 @@ func (self *CommitDescriptionController) GetKeybindings(opts types.KeybindingsOp Key: opts.GetKey(opts.Config.Universal.ConfirmInEditor), Handler: self.confirm, }, + { + Key: opts.GetKey(opts.Config.CommitMessage.SwitchToEditor), + Handler: self.switchToEditor, + }, } return bindings @@ -43,7 +48,7 @@ func (self *CommitDescriptionController) Context() types.Context { return self.context() } -func (self *CommitDescriptionController) context() types.Context { +func (self *CommitDescriptionController) context() *context.CommitMessageContext { return self.c.Contexts().CommitMessage } @@ -58,3 +63,7 @@ func (self *CommitDescriptionController) close() error { func (self *CommitDescriptionController) confirm() error { return self.c.Helpers().Commits.HandleCommitConfirm() } + +func (self *CommitDescriptionController) switchToEditor() error { + return self.c.Helpers().Commits.SwitchToEditor() +} diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go index 141f24618..fc5aca970 100644 --- a/pkg/gui/controllers/commit_message_controller.go +++ b/pkg/gui/controllers/commit_message_controller.go @@ -22,8 +22,6 @@ func NewCommitMessageController( } } -// TODO: merge that commit panel PR because we're not currently showing how to add a newline as it's -// handled by the editor func rather than by the controller here. func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { @@ -48,6 +46,10 @@ func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts) Key: opts.GetKey(opts.Config.Universal.TogglePanel), Handler: self.switchToCommitDescription, }, + { + Key: opts.GetKey(opts.Config.CommitMessage.SwitchToEditor), + Handler: self.switchToEditor, + }, } return bindings @@ -86,6 +88,10 @@ func (self *CommitMessageController) switchToCommitDescription() error { return nil } +func (self *CommitMessageController) switchToEditor() error { + return self.c.Helpers().Commits.SwitchToEditor() +} + func (self *CommitMessageController) handleCommitIndexChange(value int) error { currentIndex := self.context().GetSelectedIndex() newIndex := currentIndex + value diff --git a/pkg/gui/controllers/helpers/commits_helper.go b/pkg/gui/controllers/helpers/commits_helper.go index 10a28ad5f..5d388c319 100644 --- a/pkg/gui/controllers/helpers/commits_helper.go +++ b/pkg/gui/controllers/helpers/commits_helper.go @@ -1,9 +1,12 @@ package helpers import ( + "path/filepath" "strings" + "time" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) type ICommitsHelper interface { @@ -62,6 +65,28 @@ func (self *CommitsHelper) JoinCommitMessageAndDescription() string { return self.getCommitSummary() + "\n" + self.getCommitDescription() } +func (self *CommitsHelper) SwitchToEditor() error { + if !self.c.Contexts().CommitMessage.CanSwitchToEditor() { + return nil + } + + message := lo.Ternary(len(self.getCommitDescription()) == 0, + self.getCommitSummary(), + self.getCommitSummary()+"\n\n"+self.getCommitDescription()) + filepath := filepath.Join(self.c.OS().GetTempDir(), self.c.Git().RepoPaths.RepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".msg") + err := self.c.OS().CreateFileWithContent(filepath, message) + if err != nil { + return err + } + + err = self.CloseCommitMessagePanel() + if err != nil { + return err + } + + return self.c.Contexts().CommitMessage.SwitchToEditor(filepath) +} + func (self *CommitsHelper) UpdateCommitPanelView(message string) { if message != "" { self.SetMessageAndDescriptionInView(message) @@ -83,6 +108,7 @@ type OpenCommitMessagePanelOpts struct { DescriptionTitle string PreserveMessage bool OnConfirm func(summary string, description string) error + OnSwitchToEditor func(string) error InitialMessage string } @@ -101,6 +127,7 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp opts.DescriptionTitle, opts.PreserveMessage, onConfirm, + opts.OnSwitchToEditor, ) self.UpdateCommitPanelView(opts.InitialMessage) diff --git a/pkg/gui/controllers/helpers/working_tree_helper.go b/pkg/gui/controllers/helpers/working_tree_helper.go index dba9d2a6a..4f7a6bd83 100644 --- a/pkg/gui/controllers/helpers/working_tree_helper.go +++ b/pkg/gui/controllers/helpers/working_tree_helper.go @@ -104,6 +104,7 @@ func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage strin DescriptionTitle: self.c.Tr.CommitDescriptionTitle, PreserveMessage: true, OnConfirm: self.handleCommit, + OnSwitchToEditor: self.switchFromCommitMessagePanelToEditor, }, ) } @@ -117,6 +118,21 @@ func (self *WorkingTreeHelper) handleCommit(summary string, description string) }) } +func (self *WorkingTreeHelper) switchFromCommitMessagePanelToEditor(filepath string) error { + // We won't be able to tell whether the commit was successful, because + // RunSubprocessAndRefresh doesn't return the error (it opens an error alert + // itself and returns nil on error). But even if we could, we wouldn't have + // access to the last message that the user typed, and it might be very + // different from what was last in the commit panel. So the best we can do + // here is to always clear the remembered commit message. + self.commitsHelper.OnCommitSuccess() + + self.c.LogAction(self.c.Tr.Actions.Commit) + return self.c.RunSubprocessAndRefresh( + self.c.Git().Commit.CommitInEditorWithMessageFileCmdObj(filepath), + ) +} + // HandleCommitEditorPress - handle when the user wants to commit changes via // their editor rather than via the popup panel func (self *WorkingTreeHelper) HandleCommitEditorPress() error { diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 4a2627cdc..38ddaa515 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -279,10 +279,37 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error { DescriptionTitle: self.c.Tr.CommitDescriptionTitle, PreserveMessage: false, OnConfirm: self.handleReword, + OnSwitchToEditor: self.switchFromCommitMessagePanelToEditor, }, ) } +func (self *LocalCommitsController) switchFromCommitMessagePanelToEditor(filepath string) error { + if self.isHeadCommit() { + return self.c.RunSubprocessAndRefresh( + self.c.Git().Commit.RewordLastCommitInEditorWithMessageFileCmdObj(filepath)) + } + + err := self.c.Git().Rebase.BeginInteractiveRebaseForCommit(self.c.Model().Commits, self.context().GetSelectedLineIdx(), false) + if err != nil { + return err + } + + // now the selected commit should be our head so we'll amend it with the new message + err = self.c.RunSubprocessAndRefresh( + self.c.Git().Commit.RewordLastCommitInEditorWithMessageFileCmdObj(filepath)) + if err != nil { + return err + } + + err = self.c.Git().Rebase.ContinueRebase() + if err != nil { + return err + } + + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) +} + func (self *LocalCommitsController) handleReword(summary string, description string) error { err := self.c.Git().Rebase.RewordCommit(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), summary, description) if err != nil { diff --git a/pkg/gui/views.go b/pkg/gui/views.go index d9cf15ffe..327feb1bc 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -2,9 +2,7 @@ package gui import ( "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/theme" - "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -164,10 +162,6 @@ func (gui *Gui) createAllViews() error { gui.Views.CommitDescription.Visible = false gui.Views.CommitDescription.Title = gui.c.Tr.CommitDescriptionTitle - gui.Views.CommitDescription.Subtitle = utils.ResolvePlaceholderString(gui.Tr.CommitDescriptionSubTitle, - map[string]string{ - "togglePanelKeyBinding": keybindings.Label(gui.UserConfig.Keybinding.Universal.TogglePanel), - }) gui.Views.CommitDescription.FgColor = theme.GocuiDefaultTextColor gui.Views.CommitDescription.Editable = true gui.Views.CommitDescription.Editor = gocui.EditorFunc(gui.commitDescriptionEditor) diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 109f13e42..50b62549f 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -199,6 +199,7 @@ type TranslationSet struct { CommitSummaryTitle string CommitDescriptionTitle string CommitDescriptionSubTitle string + CommitDescriptionSubTitleNoSwitch string LocalBranchesTitle string SearchTitle string TagsTitle string @@ -969,7 +970,8 @@ func EnglishTranslationSet() TranslationSet { RebaseOptionsTitle: "Rebase options", CommitSummaryTitle: "Commit summary", CommitDescriptionTitle: "Commit description", - CommitDescriptionSubTitle: "Press {{.togglePanelKeyBinding}} to toggle focus", + CommitDescriptionSubTitle: "Press {{.togglePanelKeyBinding}} to toggle focus, {{.switchToEditorKeyBinding}} to switch to editor", + CommitDescriptionSubTitleNoSwitch: "Press {{.togglePanelKeyBinding}} to toggle focus", LocalBranchesTitle: "Local branches", SearchTitle: "Search", TagsTitle: "Tags", diff --git a/pkg/integration/components/commit_message_panel_driver.go b/pkg/integration/components/commit_message_panel_driver.go index f9089ee2e..52ad60815 100644 --- a/pkg/integration/components/commit_message_panel_driver.go +++ b/pkg/integration/components/commit_message_panel_driver.go @@ -74,6 +74,10 @@ func (self *CommitMessagePanelDriver) Cancel() { self.getViewDriver().PressEscape() } +func (self *CommitMessagePanelDriver) SwitchToEditor() { + self.getViewDriver().Press(self.t.keys.CommitMessage.SwitchToEditor) +} + func (self *CommitMessagePanelDriver) SelectPreviousMessage() *CommitMessagePanelDriver { self.getViewDriver().SelectPreviousItem() return self diff --git a/pkg/integration/tests/commit/commit_switch_to_editor.go b/pkg/integration/tests/commit/commit_switch_to_editor.go new file mode 100644 index 000000000..069384ab1 --- /dev/null +++ b/pkg/integration/tests/commit/commit_switch_to_editor.go @@ -0,0 +1,54 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var CommitSwitchToEditor = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Commit, then switch from built-in commit message panel to editor", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFile("file1", "file1 content") + shell.CreateFile("file2", "file2 content") + + // Set an editor that appends a line to the existing message. Since + // git adds all this "# Please enter the commit message for your changes" + // stuff, this will result in an extra blank line before the added line. + shell.SetConfig("core.editor", "sh -c 'echo third line >>.git/COMMIT_EDITMSG'") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + IsEmpty() + + t.Views().Files(). + IsFocused(). + PressPrimaryAction(). // stage one of the files + Press(keys.Files.CommitChanges) + + t.ExpectPopup().CommitMessagePanel(). + Type("first line"). + SwitchToDescription(). + Type("second line"). + SwitchToSummary(). + SwitchToEditor() + t.Views().Commits(). + Lines( + Contains("first line"), + ) + + t.Views().Commits().Focus() + t.Views().Main().Content(MatchesRegexp(`first line\n\s*\n\s*second line\n\s*\n\s*third line`)) + + // Now check that the preserved commit message was cleared: + t.Views().Files(). + Focus(). + PressPrimaryAction(). // stage the other file + Press(keys.Files.CommitChanges) + + t.ExpectPopup().CommitMessagePanel(). + InitialText(Equals("")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index a8b22413c..779286fbc 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -55,6 +55,7 @@ var tests = []*components.IntegrationTest{ commit.Amend, commit.Commit, commit.CommitMultiline, + commit.CommitSwitchToEditor, commit.CommitWipWithPrefix, commit.CommitWithPrefix, commit.CreateTag,