Preserve whitespace when remembering commit message (#5528)

This is useful when cancelling out of the commit panel mid-sentence
(after having typed the space for the next word); when entering the
commit message panel again, the space was gone and you had to type it
again. Small thing, but it just seems better to resume the panel in
exactly the state that you left it in. (Which we actually don't do; we
don't remember the cursor position, or which of the subject/description
panels was active. That would be a separate improvement.)

Fixes #5519.
This commit is contained in:
Stefan Haller 2026-04-21 18:55:08 +02:00 committed by GitHub
commit 0ecf818a73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 99 additions and 35 deletions

View file

@ -61,6 +61,7 @@ func AddCoAuthorToMessage(message string, author string) string {
}
func AddCoAuthorToDescription(description string, author string) string {
description = strings.TrimRight(description, "\n")
if description != "" {
lines := strings.Split(description, "\n")
if strings.HasPrefix(lines[len(lines)-1], "Co-authored-by:") {

View file

@ -483,6 +483,11 @@ func TestAddCoAuthorToDescription(t *testing.T) {
description: "Body\n\nCo-authored-by: Jane Smith <jane@smith.com>",
expectedResult: "Body\n\nCo-authored-by: Jane Smith <jane@smith.com>\nCo-authored-by: John Doe <john@doe.com>",
},
{
name: "Description with trailing newlines",
description: "Body\n\n",
expectedResult: "Body\n\nCo-authored-by: John Doe <john@doe.com>",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {

View file

@ -129,7 +129,7 @@ func (self *CommitMessageContext) SetPreservedMessageAndLogError(message string)
}
func (self *CommitMessageContext) GetInitialMessage() string {
return strings.TrimSpace(self.viewModel.initialMessage)
return self.viewModel.initialMessage
}
func (self *CommitMessageContext) GetHistoryMessage() string {

View file

@ -1,8 +1,6 @@
package gui
import (
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
@ -35,14 +33,14 @@ func (gui *Gui) resetHelpersAndControllers() {
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
getCommitSummary := func() string {
return strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
return gui.Views.CommitMessage.TextArea.GetContent()
}
getCommitDescription := func() string {
return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetContent())
return gui.Views.CommitDescription.TextArea.GetContent()
}
getUnwrappedCommitDescription := func() string {
return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetUnwrappedContent())
return gui.Views.CommitDescription.TextArea.GetUnwrappedContent()
}
commitsHelper := helpers.NewCommitsHelper(helperCommon,
getCommitSummary,

View file

@ -143,7 +143,7 @@ func (self *CommitMessageController) handleCommitIndexChange(value int) error {
newIndex := currentIndex + value
if newIndex == context.NoCommitIndex {
self.context().SetSelectedIndex(newIndex)
self.c.Helpers().Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage())
self.c.Helpers().Commits.SetPreservedMessageInView(self.context().GetHistoryMessage())
return nil
} else if currentIndex == context.NoCommitIndex {
self.context().SetHistoryMessage(self.c.Helpers().Commits.JoinCommitMessageAndUnwrappedDescription())
@ -168,7 +168,7 @@ func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, e
if self.c.UserConfig().Git.Commit.AutoWrapCommitMessage {
commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig().Git.Commit.AutoWrapWidth)
}
self.c.Helpers().Commits.UpdateCommitPanelView(commitMessage)
self.c.Helpers().Commits.SetMessageAndDescriptionInView(commitMessage)
return true, nil
}

View file

@ -40,14 +40,34 @@ func NewCommitsHelper(
}
}
// SplitCommitMessageAndDescription splits a message in git's canonical format
// (summary and body separated by a blank line) into summary and description.
func (self *CommitsHelper) SplitCommitMessageAndDescription(message string) (string, string) {
msg, description, _ := strings.Cut(message, "\n")
return msg, strings.TrimSpace(description)
summary, description, _ := strings.Cut(message, "\n")
description = strings.TrimPrefix(description, "\n")
return summary, description
}
// SplitPreservedCommitMessage splits a message in our preservation format
// (summary and description joined by a single "\n") into summary and description.
// It is lossless: round-tripping through JoinCommitMessageAndUnwrappedDescription
// preserves the exact content.
func (self *CommitsHelper) SplitPreservedCommitMessage(message string) (string, string) {
summary, description, _ := strings.Cut(message, "\n")
return summary, description
}
func (self *CommitsHelper) SetMessageAndDescriptionInView(message string) {
summary, description := self.SplitCommitMessageAndDescription(message)
self.setSummaryAndDescriptionInView(summary, description)
}
func (self *CommitsHelper) SetPreservedMessageInView(message string) {
summary, description := self.SplitPreservedCommitMessage(message)
self.setSummaryAndDescriptionInView(summary, description)
}
func (self *CommitsHelper) setSummaryAndDescriptionInView(summary, description string) {
self.setCommitSummary(summary)
self.setCommitDescription(description)
self.c.Contexts().CommitMessage.RenderSubtitle()
@ -97,21 +117,6 @@ func (self *CommitsHelper) SwitchToEditor() error {
return self.c.Contexts().CommitMessage.SwitchToEditor(filepath)
}
func (self *CommitsHelper) UpdateCommitPanelView(message string) {
if message != "" {
self.SetMessageAndDescriptionInView(message)
return
}
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
self.SetMessageAndDescriptionInView(preservedMessage)
return
}
self.SetMessageAndDescriptionInView("")
}
type OpenCommitMessagePanelOpts struct {
CommitIndex int
SummaryTitle string
@ -137,19 +142,35 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp
return opts.OnConfirm(summary, description)
}
// When there's no explicit initial message but we're in a preservation
// context, fall back to any previously preserved message. This is stored as
// the "initial" value so the unchanged-message check on close still works
// correctly (in particular, clearing the panel then escaping will notice
// the difference and delete the preserved file).
initialMessage := opts.InitialMessage
initialMessageIsPreserved := false
if opts.PreserveMessage && initialMessage == "" {
initialMessage = self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
initialMessageIsPreserved = true
}
self.c.Contexts().CommitMessage.SetPanelState(
opts.CommitIndex,
opts.SummaryTitle,
opts.DescriptionTitle,
opts.PreserveMessage,
opts.InitialMessage,
initialMessage,
onConfirm,
opts.OnSwitchToEditor,
opts.ForceSkipHooks,
opts.SkipHooksPrefix,
)
self.UpdateCommitPanelView(opts.InitialMessage)
if initialMessageIsPreserved {
self.SetPreservedMessageInView(initialMessage)
} else {
self.SetMessageAndDescriptionInView(initialMessage)
}
self.c.Context().Push(self.c.Contexts().CommitMessage, types.OnFocusOpts{})
}
@ -161,7 +182,7 @@ func (self *CommitsHelper) ClearPreservedCommitMessage() {
func (self *CommitsHelper) HandleCommitConfirm() error {
summary, description := self.getCommitSummary(), self.getCommitDescription()
if summary == "" {
if strings.TrimSpace(summary) == "" {
return errors.New(self.c.Tr.CommitWithoutMessageErr)
}

View file

@ -195,9 +195,9 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error {
}
func (self *WorkingTreeHelper) HandleCommitPress() error {
message := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
if message == "" {
var initialMessage string
preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
if preservedMessage == "" {
commitPrefixConfigs := self.commitPrefixConfigsForRepo()
for _, commitPrefixConfig := range commitPrefixConfigs {
prefixPattern := commitPrefixConfig.Pattern
@ -212,14 +212,13 @@ func (self *WorkingTreeHelper) HandleCommitPress() error {
}
if rgx.MatchString(branchName) {
prefix := rgx.ReplaceAllString(branchName, prefixReplace)
message = prefix
initialMessage = rgx.ReplaceAllString(branchName, prefixReplace)
break
}
}
}
return self.HandleCommitPressWithMessage(message, false)
return self.HandleCommitPressWithMessage(initialMessage, false)
}
func (self *WorkingTreeHelper) WithEnsureCommittableFiles(handler func() error) error {

View file

@ -0,0 +1,39 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var PreserveCommitMessageWhitespace = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Whitespace in the description (e.g. leading blank lines, indented first line) should be preserved when canceling and reopening the commit message panel",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("myfile", "myfile content")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Type("my commit message").
SwitchToDescription().
AddNewline().
AddNewline().
Type("body ").
Cancel()
t.Views().Files().
IsFocused().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Content(Equals("my commit message")).
SwitchToDescription().
Content(Equals("\n\nbody ")).
Cancel()
},
})

View file

@ -142,6 +142,7 @@ var tests = []*components.IntegrationTest{
commit.PasteCommitMessage,
commit.PasteCommitMessageOverExisting,
commit.PreserveCommitMessage,
commit.PreserveCommitMessageWhitespace,
commit.ResetAuthor,
commit.ResetAuthorRange,
commit.Revert,

View file

@ -2,7 +2,7 @@
# We will have only done a shallow clone, so the git log will consist only of
# commits on the current PR
commits=$(git log --grep='^fixup!' --grep='^squash!' --grep='^amend!' --grep='^[^\n]*WIP' --grep='^[^\n]*DROPME' --format="%h %s")
commits=$(git log --format="%h %s" | egrep '(^fixup!|^squash!|^amend!|WIP|DROPME)')
if [ -z "$commits" ]; then
echo "No fixup commits found."