Add "Copy commit message to clipboard" to the commit menu (#6209)

The commit menu (`ctrl+o` in the commit message panel) can paste a
message from the clipboard, but it can't copy the message to it. Copying
is useful for keeping a message around while committing something else
first. For example, if a pre-commit hook fails, users may want to commit
a fix for it before they come back to the original change. Copying the
preserved commit message gives them a chance to park it in an untitled
editor buffer and reuse it later.

Also, add "Clear commit message" to the menu; this is for those users
who don't know that you can quickly clear the message by holding down
`ctrl+u` with auto-repeat. I've seen users hold down backspace with
auto-repeat instead, which takes forever.

This changes the keybinding for the "Paste commit message from
clipboard" menu item from `p` to `V`, so that we can use `C` for
copying. I hope this isn't a problem for anybody's muscle memory, but
the new keys are consistent with copying and pasting commits in the
commits panel.
This commit is contained in:
Stefan Haller
2026-11-12 10:22:47 +01:00
committed by GitHub
5 changed files with 140 additions and 5 deletions
+51 -5
View File
@@ -5,11 +5,11 @@ import (
"path/filepath"
"strings"
"time"
"unicode"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
type CommitsHelper struct {
@@ -48,6 +48,15 @@ func (self *CommitsHelper) SplitCommitMessageAndDescription(message string) (str
return summary, description
}
// joinCommitMessageAndDescription is the inverse of
// SplitCommitMessageAndDescription.
func (self *CommitsHelper) joinCommitMessageAndDescription(summary string, description string) string {
if len(description) == 0 {
return summary
}
return summary + "\n\n" + 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
@@ -103,9 +112,7 @@ func TryRemoveHardLineBreaks(message string, autoWrapWidth int) string {
}
func (self *CommitsHelper) SwitchToEditor() error {
message := lo.Ternary(len(self.getCommitDescription()) == 0,
self.getCommitSummary(),
self.getCommitSummary()+"\n\n"+self.getCommitDescription())
message := self.joinCommitMessageAndDescription(self.getCommitSummary(), 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 {
@@ -242,12 +249,27 @@ func (self *CommitsHelper) OpenCommitMenu(suggestionFunc func(string) []*types.S
},
Keys: menuKey('c'),
},
{
Label: self.c.Tr.CopyCommitMessageToClipboard,
OnPress: func() error {
return self.copyCommitMessageToClipboard()
},
Keys: menuKey('C'),
},
{
Label: self.c.Tr.PasteCommitMessageFromClipboard,
OnPress: func() error {
return self.pasteCommitMessageFromClipboard()
},
Keys: menuKey('p'),
Keys: menuKey('V'),
},
{
Label: self.c.Tr.ClearCommitMessage,
OnPress: func() error {
self.setSummaryAndDescriptionInView("", "")
return nil
},
Keys: menuKey('d'),
},
}
return self.c.Menu(types.CreateMenuOptions{
@@ -271,6 +293,30 @@ func (self *CommitsHelper) addCoAuthor(suggestionFunc func(string) []*types.Sugg
return nil
}
func (self *CommitsHelper) copyCommitMessageToClipboard() error {
// The panel keeps the space at the end of each line that it wraps. Git
// strips trailing whitespace when it commits, so do the same here; this
// makes the copied message look like the committed one.
message := stripTrailingWhitespace(
self.joinCommitMessageAndDescription(self.getCommitSummary(), self.getCommitDescription()))
self.c.LogAction(self.c.Tr.Actions.CopyCommitMessageToClipboard)
if err := self.c.OS().CopyToClipboard(message); err != nil {
return err
}
self.c.Toast(self.c.Tr.CommitMessageCopiedToClipboard)
return nil
}
func stripTrailingWhitespace(message string) string {
lines := strings.Split(message, "\n")
for i, line := range lines {
lines[i] = strings.TrimRightFunc(line, unicode.IsSpace)
}
return strings.Join(lines, "\n")
}
func (self *CommitsHelper) pasteCommitMessageFromClipboard() error {
message, err := self.c.OS().PasteFromClipboard()
if err != nil {
+4
View File
@@ -765,8 +765,10 @@ type TranslationSet struct {
CopyCommitHashToClipboard string
CommitHash string
CommitURL string
CopyCommitMessageToClipboard string
PasteCommitMessageFromClipboard string
SurePasteCommitMessage string
ClearCommitMessage string
CommitMessage string
CommitMessageBody string
CommitSubject string
@@ -2002,8 +2004,10 @@ func EnglishTranslationSet() *TranslationSet {
CopyCommitHashToClipboard: "Copy abbreviated commit hash to clipboard",
CommitHash: "Commit hash",
CommitURL: "Commit URL",
CopyCommitMessageToClipboard: "Copy commit message to clipboard",
PasteCommitMessageFromClipboard: "Paste commit message from clipboard",
SurePasteCommitMessage: "Pasting will overwrite the current commit message, continue?",
ClearCommitMessage: "Clear commit message",
CommitMessage: "Commit message (subject and body)",
CommitMessageBody: "Commit message body",
CommitSubject: "Commit subject",
@@ -0,0 +1,39 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ClearCommitMessage = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Clear the commit message in the commit message panel",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file", "file content")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
Focus().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Type("subject").
SwitchToDescription().
Type("first line").
AddNewline().
Type("second line").
SwitchToSummary().
OpenCommitMenu()
t.ExpectPopup().Menu().Title(Equals("Commit Menu")).
Select(Contains("Clear commit message")).
Confirm()
t.ExpectPopup().CommitMessagePanel().
Content(Equals("")).
SwitchToDescription().
Content(Equals(""))
},
})
@@ -0,0 +1,44 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CopyCommitMessageFromPanel = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Copy the commit message from the commit message panel to the clipboard",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > ../clipboard"
},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file", "file content")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
Focus().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Type("subject").
SwitchToDescription().
Type("This line is longer than the auto-wrap width, so the clipboard gets it with a line break").
AddNewline().
Type("second line").
SwitchToSummary().
OpenCommitMenu()
t.ExpectPopup().Menu().Title(Equals("Commit Menu")).
Select(Contains("Copy commit message to clipboard")).
Confirm()
t.ExpectToast(Equals("Commit message copied to clipboard"))
t.FileSystem().FileContent("../clipboard", Equals(
"subject\n\nThis line is longer than the auto-wrap width, so the clipboard gets it\nwith a line break\nsecond line"))
t.ExpectPopup().CommitMessagePanel().
Content(Equals("subject"))
},
})
+2
View File
@@ -142,6 +142,7 @@ var tests = []*components.IntegrationTest{
commit.CheckoutFileFromCommit,
commit.CheckoutFileFromRangeSelectionOfCommits,
commit.CheckoutFileWithLocalModifications,
commit.ClearCommitMessage,
commit.Commit,
commit.CommitAllWithNestedRepository,
commit.CommitDuringSquashMerge,
@@ -156,6 +157,7 @@ var tests = []*components.IntegrationTest{
commit.CommitWithNonMatchingBranchName,
commit.CommitWithPrefix,
commit.CopyAuthorToClipboard,
commit.CopyCommitMessageFromPanel,
commit.CopyMessageBodyToClipboard,
commit.CopyTagToClipboard,
commit.CreateAmendCommit,