Use the squash merge message when committing a squash merge

After "Squash merge and leave uncommitted", the commit panel opens
empty, or with the commit prefix of the branch. Users have to type a
message for the squash merge themselves. "git commit" from the terminal
suggests the message in SQUASH_MSG instead.

Show the message in SQUASH_MSG in the commit panel, unless there is a
preserved message. It takes precedence over the commit prefix, like the
message of "Squash merge and commit" does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-11-08 15:36:41 +01:00
co-authored by Claude Opus 5.5
parent c7c514fa44
commit 09780de32e
7 changed files with 121 additions and 7 deletions
+10
View File
@@ -73,6 +73,16 @@ func (self *StatusCommands) IsInSquashMerge() (bool, error) {
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "SQUASH_MSG"))
}
// SquashMergeMessage returns the message in SQUASH_MSG (see IsInSquashMerge), or
// an empty string if there is no squash merge in progress
func (self *StatusCommands) SquashMergeMessage() string {
content, err := os.ReadFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "SQUASH_MSG"))
if err != nil {
return ""
}
return strings.TrimSpace(string(content))
}
func (self *StatusCommands) IsInCherryPick() (bool, error) {
hasCherryPickHead, err := self.hasCherryPickHead()
if err != nil || hasCherryPickHead {
+4 -2
View File
@@ -131,6 +131,10 @@ func (self *CommitMessageContext) GetInitialMessage() string {
return self.viewModel.initialMessage
}
func (self *CommitMessageContext) SetInitialMessage(message string) {
self.viewModel.initialMessage = message
}
func (self *CommitMessageContext) GetHistoryMessage() string {
return self.viewModel.historyMessage
}
@@ -148,7 +152,6 @@ func (self *CommitMessageContext) SetPanelState(
summaryTitle string,
descriptionTitle string,
preserveMessage bool,
initialMessage string,
onConfirm func(string, string) error,
onSwitchToEditor func(string) error,
forceSkipHooks bool,
@@ -156,7 +159,6 @@ func (self *CommitMessageContext) SetPanelState(
) {
self.viewModel.selectedindex = index
self.viewModel.preserveMessage = preserveMessage
self.viewModel.initialMessage = initialMessage
self.viewModel.onConfirm = onConfirm
self.viewModel.onSwitchToEditor = onSwitchToEditor
self.viewModel.forceSkipHooks = forceSkipHooks
@@ -143,10 +143,7 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp
}
// 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).
// context, fall back to any previously preserved message.
initialMessage := opts.InitialMessage
initialMessageIsPreserved := false
if opts.PreserveMessage && initialMessage == "" {
@@ -159,7 +156,6 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp
opts.SummaryTitle,
opts.DescriptionTitle,
opts.PreserveMessage,
initialMessage,
onConfirm,
opts.OnSwitchToEditor,
opts.ForceSkipHooks,
@@ -172,6 +168,14 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp
self.SetMessageAndDescriptionInView(initialMessage)
}
// PreserveCommitMessage compares the message in the panel with this when the
// panel closes, so that it preserves the message only if the user changed it
// (in particular, clearing the panel then escaping will notice the
// difference and delete the preserved file). Take it from the panel, because
// a message with a blank line after the summary comes out of the panel
// without it.
self.c.Contexts().CommitMessage.SetInitialMessage(self.JoinCommitMessageAndUnwrappedDescription())
self.c.Context().Push(self.c.Contexts().CommitMessage, types.OnFocusOpts{})
}
@@ -237,6 +237,9 @@ func (self *WorkingTreeHelper) HandleCommitPress() error {
var initialMessage string
preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
if preservedMessage == "" {
initialMessage = self.c.Git().Status.SquashMergeMessage()
}
if preservedMessage == "" && initialMessage == "" {
commitPrefixConfigs := self.commitPrefixConfigsForRepo()
for _, commitPrefixConfig := range commitPrefixConfigs {
prefixPattern := commitPrefixConfig.Pattern
@@ -0,0 +1,35 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CancelCommitDuringSquashMerge = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Open the commit panel during a squash merge started outside lazygit, and close it without changing git's message",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("one").
NewBranch("feature-branch").
CreateFileAndAdd("feature-file", "content").
Commit("feature").
Checkout("master").
RunCommand([]string{"git", "merge", "--squash", "feature-branch"})
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
Focus().
Lines(
Equals("A feature-file"),
).
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
InitialText(Equals("Squashed commit of the following:")).
Cancel()
t.FileSystem().PathNotPresent(".git/LAZYGIT_PENDING_COMMIT")
},
})
@@ -0,0 +1,58 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CommitDuringSquashMerge = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Commit the changes of a squash merge that was left uncommitted, using the squash merge message",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().Git.CommitPrefix = []config.CommitPrefixConfig{{Pattern: "^(\\w+)-branch$", Replace: "[$1] "}}
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("original-branch").
EmptyCommit("one").
NewBranch("feature-branch").
CreateFileAndAdd("feature-file", "content").
Commit("feature").
Checkout("original-branch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("original-branch").IsSelected(),
Contains("feature-branch"),
).
SelectNextItem().
Press(keys.Branches.MergeIntoCurrentBranch)
t.ExpectPopup().Menu().
Title(Equals("Merge")).
Select(Contains("Squash merge and leave uncommitted")).
Confirm()
t.Views().Files().
Focus().
Lines(
Equals("A feature-file"),
).
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Title(Equals("Commit summary")).
InitialText(Equals("Squash merge feature-branch into original-branch")).
Confirm()
t.Views().Status().Content(Equals("repo → original-branch"))
t.Views().Commits().
Lines(
Contains("Squash merge feature-branch into original-branch"),
Contains("one"),
)
},
})
+2
View File
@@ -137,12 +137,14 @@ var tests = []*components.IntegrationTest{
commit.AmendWhenThereAreConflictsAndCancel,
commit.AmendWhenThereAreConflictsAndContinue,
commit.AutoWrapMessage,
commit.CancelCommitDuringSquashMerge,
commit.Checkout,
commit.CheckoutFileFromCommit,
commit.CheckoutFileFromRangeSelectionOfCommits,
commit.CheckoutFileWithLocalModifications,
commit.Commit,
commit.CommitAllWithNestedRepository,
commit.CommitDuringSquashMerge,
commit.CommitMultiline,
commit.CommitSkipHooks,
commit.CommitSwitchToEditor,