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..0da0211a8 100644 --- a/pkg/gui/context/commit_message_context.go +++ b/pkg/gui/context/commit_message_context.go @@ -32,6 +32,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,10 +100,12 @@ 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 } @@ -117,3 +121,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 9e180db5a..fc5aca970 100644 --- a/pkg/gui/controllers/commit_message_controller.go +++ b/pkg/gui/controllers/commit_message_controller.go @@ -46,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 @@ -84,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 {