Repository: jesseduffield/lazygit (language: go)

Issue:
Add command to the commit message menu to paste a message from the clipboard
Pasting a commit message into the commit message panel is cumbersome, because you need to paste the subject into the upper field and the body into the lower field. Having copied a commit message using `y m`, the clipboard contains both, so there's no easy way to get the subject and body into their respective fields.

Pasting the whole thing into the subject field is especially problematic, see #3151. Pasting it into the description field is a little better, but then you still need to cut the subject line from the description and paste it into the subject field.

It would be nice if the commit message menu (the one that you open with ctrl-o) had an entry "Paste commit message from clipboard" that splits the clipboard into subject and description and pastes both into their respective fields.

A few details to consider:
- What do we do if the text in the clipboard is not in "commit message format", i.e. there's no blank line between subject and description? I'd suggest not to care; simply split at the first `\n`.
- What if the user has already started typing things into either the subject or body fields, or both? Do we append, or do we overwrite what's there? Overwriting might be annoying, but we could put up a confirmation.

I'm putting a "good first issue" label on this, it doesn't seem very difficult to do.


Discussion / hints:

Fix patch:
diff --git a/docs/Config.md b/docs/Config.md
index 67d73ebd4d0..88df40621f1 100644
--- a/docs/Config.md
+++ b/docs/Config.md
@@ -415,9 +415,13 @@ os:
   openLinkCommand: ""
 
   # CopyToClipboardCmd is the command for copying to clipboard.
-  # See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-clipboard
+  # See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard
   copyToClipboardCmd: ""
 
+  # ReadFromClipboardCmd is the command for reading the clipboard.
+  # See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard
+  readFromClipboardCmd: ""
+
 # If true, don't display introductory popups upon opening Lazygit.
 disableStartupPopups: false
 
@@ -620,7 +624,7 @@ os:
   open: 'open {{filename}}'
 ```
 
-## Custom Command for Copying to Clipboard
+## Custom Command for Copying to and Pasting from Clipboard
 ```yaml
 os:
   copyToClipboardCmd: ''
@@ -633,6 +637,12 @@ os:
   copyToClipboardCmd: printf "\033]52;c;$(printf {{text}} | base64)\a" > /dev/tty
 ```
 
+A custom command for reading from the clipboard can be set using
+```yaml
+os:
+  readFromClipboardCmd: ''
+```
+It is used, for example, when pasting a commit message into the commit message panel. The command is supposed to output the clipboard content to stdout.
 
 ## Configuring File Editing
 
diff --git a/pkg/commands/oscommands/os.go b/pkg/commands/oscommands/os.go
index 0a6bf7397c9..7771dffba5f 100644
--- a/pkg/commands/oscommands/os.go
+++ b/pkg/commands/oscommands/os.go
@@ -302,6 +302,23 @@ func (c *OSCommand) CopyToClipboard(str string) error {
 	return clipboard.WriteAll(str)
 }
 
+func (c *OSCommand) PasteFromClipboard() (string, error) {
+	var s string
+	var err error
+	if c.UserConfig.OS.CopyToClipboardCmd != "" {
+		cmdStr := c.UserConfig.OS.ReadFromClipboardCmd
+		s, err = c.Cmd.NewShell(cmdStr).RunWithOutput()
+	} else {
+		s, err = clipboard.ReadAll()
+	}
+
+	if err != nil {
+		return "", err
+	}
+
+	return strings.ReplaceAll(s, "\r\n", "\n"), nil
+}
+
 func (c *OSCommand) RemoveFile(path string) error {
 	msg := utils.ResolvePlaceholderString(
 		c.Tr.Log.RemoveFile,
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go
index 7ab567fbe25..26d10f73a90 100644
--- a/pkg/config/user_config.go
+++ b/pkg/config/user_config.go
@@ -565,8 +565,12 @@ type OSConfig struct {
 	OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
 
 	// CopyToClipboardCmd is the command for copying to clipboard.
-	// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-clipboard
+	// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard
 	CopyToClipboardCmd string `yaml:"copyToClipboardCmd,omitempty"`
+
+	// ReadFromClipboardCmd is the command for reading the clipboard.
+	// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard
+	ReadFromClipboardCmd string `yaml:"readFromClipboardCmd,omitempty"`
 }
 
 type CustomCommandAfterHook struct {
diff --git a/pkg/gui/controllers/helpers/commits_helper.go b/pkg/gui/controllers/helpers/commits_helper.go
index 6e1a181c77d..216f55f8e8b 100644
--- a/pkg/gui/controllers/helpers/commits_helper.go
+++ b/pkg/gui/controllers/helpers/commits_helper.go
@@ -238,6 +238,13 @@ func (self *CommitsHelper) OpenCommitMenu(suggestionFunc func(string) []*types.S
 			},
 			Key: 'c',
 		},
+		{
+			Label: self.c.Tr.PasteCommitMessageFromClipboard,
+			OnPress: func() error {
+				return self.pasteCommitMessageFromClipboard()
+			},
+			Key: 'p',
+		},
 	}
 	return self.c.Menu(types.CreateMenuOptions{
 		Title: self.c.Tr.CommitMenuTitle,
@@ -257,3 +264,28 @@ func (self *CommitsHelper) addCoAuthor(suggestionFunc func(string) []*types.Sugg
 		},
 	})
 }
+
+func (self *CommitsHelper) pasteCommitMessageFromClipboard() error {
+	message, err := self.c.OS().PasteFromClipboard()
+	if err != nil {
+		return err
+	}
+	if message == "" {
+		return nil
+	}
+
+	if currentMessage := self.JoinCommitMessageAndUnwrappedDescription(); currentMessage == "" {
+		self.SetMessageAndDescriptionInView(message)
+		return nil
+	}
+
+	// Confirm before overwriting the commit message
+	return self.c.Confirm(types.ConfirmOpts{
+		Title:  self.c.Tr.PasteCommitMessageFromClipboard,
+		Prompt: self.c.Tr.SurePasteCommitMessage,
+		HandleConfirm: func() error {
+			self.SetMessageAndDescriptionInView(message)
+			return nil
+		},
+	})
+}
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 0c8f0047903..3665ae8b1ae 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -582,6 +582,8 @@ type TranslationSet struct {
 	CommitHash                            string
 	CommitURL                             string
 	CopyCommitMessageToClipboard          string
+	PasteCommitMessageFromClipboard       string
+	SurePasteCommitMessage                string
 	CommitMessage                         string
 	CommitSubject                         string
 	CommitAuthor                          string
@@ -1553,6 +1555,8 @@ func EnglishTranslationSet() *TranslationSet {
 		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?",
 		CommitMessage:                         "Commit message",
 		CommitSubject:                         "Commit subject",
 		CommitAuthor:                          "Commit author",
diff --git a/schema/config.json b/schema/config.json
index 802069bed66..daaf4ada66c 100644
--- a/schema/config.json
+++ b/schema/config.json
@@ -796,7 +796,11 @@
         },
         "copyToClipboardCmd": {
           "type": "string",
-          "description": "CopyToClipboardCmd is the command for copying to clipboard.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-clipboard"
+          "description": "CopyToClipboardCmd is the command for copying to clipboard.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard"
+        },
+        "readFromClipboardCmd": {
+          "type": "string",
+          "description": "ReadFromClipboardCmd is the command for reading the clipboard.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard"
         }
       },
       "additionalProperties": false,


Test patch:
diff --git a/pkg/integration/tests/commit/paste_commit_message.go b/pkg/integration/tests/commit/paste_commit_message.go
new file mode 100644
index 00000000000..130d1885f7b
--- /dev/null
+++ b/pkg/integration/tests/commit/paste_commit_message.go
@@ -0,0 +1,49 @@
+package commit
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var PasteCommitMessage = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Paste a commit message into the commit message panel",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig: func(config *config.AppConfig) {
+		config.UserConfig.OS.CopyToClipboardCmd = "echo {{text}} > ../clipboard"
+		config.UserConfig.OS.ReadFromClipboardCmd = "cat ../clipboard"
+	},
+	SetupRepo: func(shell *Shell) {
+		shell.EmptyCommit("subject\n\nbody 1st line\nbody 2nd line")
+		shell.CreateFileAndAdd("file", "file content")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			ContainsLines(
+				Contains("subject").IsSelected(),
+			).
+			Press(keys.Commits.CopyCommitAttributeToClipboard)
+
+		t.ExpectPopup().Menu().Title(Equals("Copy to clipboard")).
+			Select(Contains("Commit message")).Confirm()
+
+		t.ExpectToast(Equals("Commit message copied to clipboard"))
+
+		t.Views().Files().
+			Focus().
+			Press(keys.Files.CommitChanges)
+
+		t.ExpectPopup().CommitMessagePanel().
+			OpenCommitMenu()
+
+		t.ExpectPopup().Menu().Title(Equals("Commit Menu")).
+			Select(Contains("Paste commit message from clipboard")).
+			Confirm()
+
+		t.ExpectPopup().CommitMessagePanel().
+			Content(Equals("subject")).
+			SwitchToDescription().
+			Content(Equals("body 1st line\nbody 2nd line"))
+	},
+})
diff --git a/pkg/integration/tests/commit/paste_commit_message_over_existing.go b/pkg/integration/tests/commit/paste_commit_message_over_existing.go
new file mode 100644
index 00000000000..52f6d44f506
--- /dev/null
+++ b/pkg/integration/tests/commit/paste_commit_message_over_existing.go
@@ -0,0 +1,54 @@
+package commit
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var PasteCommitMessageOverExisting = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Paste a commit message into the commit message panel when there is already text in the panel, causing a confirmation",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig: func(config *config.AppConfig) {
+		config.UserConfig.OS.CopyToClipboardCmd = "echo {{text}} > ../clipboard"
+		config.UserConfig.OS.ReadFromClipboardCmd = "cat ../clipboard"
+	},
+	SetupRepo: func(shell *Shell) {
+		shell.EmptyCommit("subject\n\nbody 1st line\nbody 2nd line")
+		shell.CreateFileAndAdd("file", "file content")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			ContainsLines(
+				Contains("subject").IsSelected(),
+			).
+			Press(keys.Commits.CopyCommitAttributeToClipboard)
+
+		t.ExpectPopup().Menu().Title(Equals("Copy to clipboard")).
+			Select(Contains("Commit message")).Confirm()
+
+		t.ExpectToast(Equals("Commit message copied to clipboard"))
+
+		t.Views().Files().
+			Focus().
+			Press(keys.Files.CommitChanges)
+
+		t.ExpectPopup().CommitMessagePanel().
+			Type("existing message").
+			OpenCommitMenu()
+
+		t.ExpectPopup().Menu().Title(Equals("Commit Menu")).
+			Select(Contains("Paste commit message from clipboard")).
+			Confirm()
+
+		t.ExpectPopup().Alert().Title(Equals("Paste commit message from clipboard")).
+			Content(Equals("Pasting will overwrite the current commit message, continue?")).
+			Confirm()
+
+		t.ExpectPopup().CommitMessagePanel().
+			Content(Equals("subject")).
+			SwitchToDescription().
+			Content(Equals("body 1st line\nbody 2nd line"))
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index c879b8638ec..b6ebad021e9 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -89,6 +89,8 @@ var tests = []*components.IntegrationTest{
 	commit.History,
 	commit.HistoryComplex,
 	commit.NewBranch,
+	commit.PasteCommitMessage,
+	commit.PasteCommitMessageOverExisting,
 	commit.PreserveCommitMessage,
 	commit.ResetAuthor,
 	commit.ResetAuthorRange,