1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-21 12:16:54 +02:00
lazygit/pkg/gui/controllers/commit_message_controller.go
Stefan Haller ba6cfc1f85 Handle pasting multi-line commit messages
When pasting a multi-line commit message into the subject field of the commit
editor, we would interpret the first newline as the confirmation for closing the
editor, and then all remaining characters as whatever command they are bound to,
resulting in executing all sorts of arbitrary commands.

Now we recognize this being a paste, and interpret the first newline as moving
to the description.

Also, prevent tabs in the pasted content from switching to the respective other
panel; simply insert four spaces instead, which should be good enough for the
leading indentation in pasted code snippets, for example.
2025-02-10 13:40:25 +01:00

204 lines
6.5 KiB
Go

package controllers
import (
"errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type CommitMessageController struct {
baseController
c *ControllerCommon
}
var _ types.IController = &CommitMessageController{}
func NewCommitMessageController(
c *ControllerCommon,
) *CommitMessageController {
return &CommitMessageController{
baseController: baseController{},
c: c,
}
}
func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.SubmitEditorText),
Handler: self.confirm,
Description: self.c.Tr.Confirm,
},
{
Key: opts.GetKey(opts.Config.Universal.Return),
Handler: self.close,
Description: self.c.Tr.Close,
},
{
Key: opts.GetKey(opts.Config.Universal.PrevItem),
Handler: self.handlePreviousCommit,
},
{
Key: opts.GetKey(opts.Config.Universal.NextItem),
Handler: self.handleNextCommit,
},
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: self.handleTogglePanel,
},
{
Key: opts.GetKey(opts.Config.CommitMessage.CommitMenu),
Handler: self.openCommitMenu,
},
}
return bindings
}
func (self *CommitMessageController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{
{
ViewName: self.Context().GetViewName(),
Key: gocui.MouseLeft,
Handler: self.onClick,
},
}
}
func (self *CommitMessageController) GetOnFocus() func(types.OnFocusOpts) {
return func(types.OnFocusOpts) {
self.c.Views().CommitDescription.Footer = ""
}
}
func (self *CommitMessageController) GetOnFocusLost() func(types.OnFocusLostOpts) {
return func(types.OnFocusLostOpts) {
self.context().RenderCommitLength()
}
}
func (self *CommitMessageController) Context() types.Context {
return self.context()
}
func (self *CommitMessageController) context() *context.CommitMessageContext {
return self.c.Contexts().CommitMessage
}
func (self *CommitMessageController) handlePreviousCommit() error {
return self.handleCommitIndexChange(1)
}
func (self *CommitMessageController) handleNextCommit() error {
if self.context().GetSelectedIndex() == context.NoCommitIndex {
return nil
}
return self.handleCommitIndexChange(-1)
}
func (self *CommitMessageController) switchToCommitDescription() error {
self.c.Context().Replace(self.c.Contexts().CommitDescription)
return nil
}
func (self *CommitMessageController) handleTogglePanel() error {
// The default keybinding for this action is "<tab>", which means that we
// also get here when pasting multi-line text that contains tabs. In that
// case we don't want to toggle the panel, but insert the tab as a character
// (somehow, see below).
//
// Only do this if the TogglePanel command is actually mapped to "<tab>"
// (the default). If it's not, we can only hope that it's mapped to some
// ctrl key or fn key, which is unlikely to occur in pasted text. And if
// they mapped some *other* command to "<tab>", then we're totally out of
// luck.
if self.c.GocuiGui().IsPasting && self.c.UserConfig().Keybinding.Universal.TogglePanel == "<tab>" {
// It is unlikely that a pasted commit message contains a tab in the
// subject line, so it shouldn't matter too much how we handle it.
// Simply insert 4 spaces instead; all that matters is that we don't
// switch to the description panel.
view := self.context().GetView()
for range 4 {
view.Editor.Edit(view, gocui.KeySpace, ' ', 0)
}
return nil
}
return self.switchToCommitDescription()
}
func (self *CommitMessageController) handleCommitIndexChange(value int) error {
currentIndex := self.context().GetSelectedIndex()
newIndex := currentIndex + value
if newIndex == context.NoCommitIndex {
self.context().SetSelectedIndex(newIndex)
self.c.Helpers().Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage())
return nil
} else if currentIndex == context.NoCommitIndex {
self.context().SetHistoryMessage(self.c.Helpers().Commits.JoinCommitMessageAndUnwrappedDescription())
}
validCommit, err := self.setCommitMessageAtIndex(newIndex)
if validCommit {
self.context().SetSelectedIndex(newIndex)
}
return err
}
// returns true if the given index is for a valid commit
func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, error) {
commitMessage, err := self.c.Git().Commit.GetCommitMessageFromHistory(index)
if err != nil {
if err == git_commands.ErrInvalidCommitIndex {
return false, nil
}
return false, errors.New(self.c.Tr.CommitWithoutMessageErr)
}
if self.c.UserConfig().Git.Commit.AutoWrapCommitMessage {
commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig().Git.Commit.AutoWrapWidth)
}
self.c.Helpers().Commits.UpdateCommitPanelView(commitMessage)
return true, nil
}
func (self *CommitMessageController) confirm() error {
// The default keybinding for this action is "<enter>", which means that we
// also get here when pasting multi-line text that contains newlines. In
// that case we don't want to confirm the commit, but switch to the
// description panel instead so that the rest of the pasted text goes there.
//
// Only do this if the SubmitEditorText command is actually mapped to
// "<enter>" (the default). If it's not, we can only hope that it's mapped
// to some ctrl key or fn key, which is unlikely to occur in pasted text.
// And if they mapped some *other* command to "<enter>", then we're totally
// out of luck.
if self.c.GocuiGui().IsPasting && self.c.UserConfig().Keybinding.Universal.SubmitEditorText == "<enter>" {
return self.switchToCommitDescription()
}
return self.c.Helpers().Commits.HandleCommitConfirm()
}
func (self *CommitMessageController) close() error {
self.c.Helpers().Commits.CloseCommitMessagePanel()
return nil
}
func (self *CommitMessageController) openCommitMenu() error {
authorSuggestion := self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc()
return self.c.Helpers().Commits.OpenCommitMenu(authorSuggestion)
}
func (self *CommitMessageController) onClick(opts gocui.ViewMouseBindingOpts) error {
// Activate the commit message panel when the commit description panel is currently active
if self.c.Context().Current().GetKey() == context.COMMIT_DESCRIPTION_CONTEXT_KEY {
self.c.Context().Replace(self.c.Contexts().CommitMessage)
}
return nil
}