diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 40d2b7319..6abf272b3 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -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:") { diff --git a/pkg/commands/git_commands/commit_test.go b/pkg/commands/git_commands/commit_test.go index 6ea914c64..25966c06f 100644 --- a/pkg/commands/git_commands/commit_test.go +++ b/pkg/commands/git_commands/commit_test.go @@ -483,6 +483,11 @@ func TestAddCoAuthorToDescription(t *testing.T) { description: "Body\n\nCo-authored-by: Jane Smith ", expectedResult: "Body\n\nCo-authored-by: Jane Smith \nCo-authored-by: John Doe ", }, + { + name: "Description with trailing newlines", + description: "Body\n\n", + expectedResult: "Body\n\nCo-authored-by: John Doe ", + }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { diff --git a/pkg/gui/context/commit_message_context.go b/pkg/gui/context/commit_message_context.go index d533e1dea..58351615e 100644 --- a/pkg/gui/context/commit_message_context.go +++ b/pkg/gui/context/commit_message_context.go @@ -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 { diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 702ed826d..51c4c3bab 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -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, diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go index 993513987..b92e8dd4c 100644 --- a/pkg/gui/controllers/commit_message_controller.go +++ b/pkg/gui/controllers/commit_message_controller.go @@ -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 } diff --git a/pkg/gui/controllers/helpers/commits_helper.go b/pkg/gui/controllers/helpers/commits_helper.go index ed97ff41b..73f1d9baf 100644 --- a/pkg/gui/controllers/helpers/commits_helper.go +++ b/pkg/gui/controllers/helpers/commits_helper.go @@ -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) } diff --git a/pkg/gui/controllers/helpers/working_tree_helper.go b/pkg/gui/controllers/helpers/working_tree_helper.go index d6289537b..b6a5407bb 100644 --- a/pkg/gui/controllers/helpers/working_tree_helper.go +++ b/pkg/gui/controllers/helpers/working_tree_helper.go @@ -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 { diff --git a/pkg/integration/tests/commit/preserve_commit_message_whitespace.go b/pkg/integration/tests/commit/preserve_commit_message_whitespace.go new file mode 100644 index 000000000..ea61d125e --- /dev/null +++ b/pkg/integration/tests/commit/preserve_commit_message_whitespace.go @@ -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() + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 04c12e600..09d487852 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -142,6 +142,7 @@ var tests = []*components.IntegrationTest{ commit.PasteCommitMessage, commit.PasteCommitMessageOverExisting, commit.PreserveCommitMessage, + commit.PreserveCommitMessageWhitespace, commit.ResetAuthor, commit.ResetAuthorRange, commit.Revert, diff --git a/scripts/check_for_fixups.sh b/scripts/check_for_fixups.sh index 3d5518360..d2e8cb08e 100755 --- a/scripts/check_for_fixups.sh +++ b/scripts/check_for_fixups.sh @@ -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."