mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-06-29 00:51:35 +02:00
Preserve pending commit message when closing/re-opening (#4191)
- **PR Description** This PR allows lazygit to preserve the commit messages when the commit popup gets closed. While discussing the feature as part of its issue, two approaches were taken into consideration: - to store the commit content as part of the global state file - to store the commit content in a special file to place inside the `.git` folder I opted for the second approach to avoid worrying about associating each preserved message to the worktree it belongs to. I am happy to reconsider this and opt for the alternative approach, but I wanted to discuss this with the maintainers before deciding. Note: The preserving file (`.git/LAZYGIT_PENDING_COMMIT`) is deleted when the commit is finalized or when the commit content becomes empty.
This commit is contained in:
@ -1,6 +1,8 @@
|
|||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -8,8 +10,11 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const PreservedCommitMessageFileName = "LAZYGIT_PENDING_COMMIT"
|
||||||
|
|
||||||
type CommitMessageContext struct {
|
type CommitMessageContext struct {
|
||||||
c *ContextCommon
|
c *ContextCommon
|
||||||
types.Context
|
types.Context
|
||||||
@ -33,8 +38,6 @@ type CommitMessageViewModel struct {
|
|||||||
// we remember the initial message so that we can tell whether we should preserve
|
// we remember the initial message so that we can tell whether we should preserve
|
||||||
// the message; if it's still identical to the initial message, we don't
|
// the message; if it's still identical to the initial message, we don't
|
||||||
initialMessage string
|
initialMessage string
|
||||||
// the full preserved message (combined summary and description)
|
|
||||||
preservedMessage string
|
|
||||||
// invoked when pressing enter in the commit message panel
|
// invoked when pressing enter in the commit message panel
|
||||||
onConfirm func(string, string) error
|
onConfirm func(string, string) error
|
||||||
// invoked when pressing the switch-to-editor key binding
|
// invoked when pressing the switch-to-editor key binding
|
||||||
@ -75,16 +78,51 @@ func (self *CommitMessageContext) GetSelectedIndex() int {
|
|||||||
return self.viewModel.selectedindex
|
return self.viewModel.selectedindex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *CommitMessageContext) GetPreservedMessagePath() string {
|
||||||
|
return filepath.Join(self.c.Git().RepoPaths.WorktreeGitDirPath(), PreservedCommitMessageFileName)
|
||||||
|
}
|
||||||
|
|
||||||
func (self *CommitMessageContext) GetPreserveMessage() bool {
|
func (self *CommitMessageContext) GetPreserveMessage() bool {
|
||||||
return self.viewModel.preserveMessage
|
return self.viewModel.preserveMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *CommitMessageContext) GetPreservedMessage() string {
|
func (self *CommitMessageContext) getPreservedMessage() (string, error) {
|
||||||
return self.viewModel.preservedMessage
|
buf, err := afero.ReadFile(self.c.Fs, self.GetPreservedMessagePath())
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(buf), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *CommitMessageContext) SetPreservedMessage(message string) {
|
func (self *CommitMessageContext) GetPreservedMessageAndLogError() string {
|
||||||
self.viewModel.preservedMessage = message
|
msg, err := self.getPreservedMessage()
|
||||||
|
if err != nil {
|
||||||
|
self.c.Log.Errorf("error when retrieving persisted commit message: %v", err)
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *CommitMessageContext) setPreservedMessage(message string) error {
|
||||||
|
preservedFilePath := self.GetPreservedMessagePath()
|
||||||
|
|
||||||
|
if len(message) == 0 {
|
||||||
|
err := self.c.Fs.Remove(preservedFilePath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return afero.WriteFile(self.c.Fs, preservedFilePath, []byte(message), 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *CommitMessageContext) SetPreservedMessageAndLogError(message string) {
|
||||||
|
if err := self.setPreservedMessage(message); err != nil {
|
||||||
|
self.c.Log.Errorf("error when persisting commit message: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *CommitMessageContext) GetInitialMessage() string {
|
func (self *CommitMessageContext) GetInitialMessage() string {
|
||||||
|
@ -113,7 +113,7 @@ func (self *CommitsHelper) UpdateCommitPanelView(message string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
|
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
|
||||||
preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessage()
|
preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
|
||||||
self.SetMessageAndDescriptionInView(preservedMessage)
|
self.SetMessageAndDescriptionInView(preservedMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -156,7 +156,7 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp
|
|||||||
func (self *CommitsHelper) OnCommitSuccess() {
|
func (self *CommitsHelper) OnCommitSuccess() {
|
||||||
// if we have a preserved message we want to clear it on success
|
// if we have a preserved message we want to clear it on success
|
||||||
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
|
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
|
||||||
self.c.Contexts().CommitMessage.SetPreservedMessage("")
|
self.c.Contexts().CommitMessage.SetPreservedMessageAndLogError("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ func (self *CommitsHelper) CloseCommitMessagePanel() {
|
|||||||
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
|
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
|
||||||
message := self.JoinCommitMessageAndUnwrappedDescription()
|
message := self.JoinCommitMessageAndUnwrappedDescription()
|
||||||
if message != self.c.Contexts().CommitMessage.GetInitialMessage() {
|
if message != self.c.Contexts().CommitMessage.GetInitialMessage() {
|
||||||
self.c.Contexts().CommitMessage.SetPreservedMessage(message)
|
self.c.Contexts().CommitMessage.SetPreservedMessageAndLogError(message)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.SetMessageAndDescriptionInView("")
|
self.SetMessageAndDescriptionInView("")
|
||||||
|
@ -149,7 +149,7 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *WorkingTreeHelper) HandleCommitPress() error {
|
func (self *WorkingTreeHelper) HandleCommitPress() error {
|
||||||
message := self.c.Contexts().CommitMessage.GetPreservedMessage()
|
message := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
|
||||||
|
|
||||||
if message == "" {
|
if message == "" {
|
||||||
commitPrefixConfig := self.commitPrefixConfigForRepo()
|
commitPrefixConfig := self.commitPrefixConfigForRepo()
|
||||||
|
@ -52,6 +52,11 @@ func (self *CommitDescriptionPanelDriver) AddCoAuthor(author string) *CommitDesc
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *CommitDescriptionPanelDriver) Clear() *CommitDescriptionPanelDriver {
|
||||||
|
self.getViewDriver().Clear()
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
func (self *CommitDescriptionPanelDriver) Title(expected *TextMatcher) *CommitDescriptionPanelDriver {
|
func (self *CommitDescriptionPanelDriver) Title(expected *TextMatcher) *CommitDescriptionPanelDriver {
|
||||||
self.getViewDriver().Title(expected)
|
self.getViewDriver().Title(expected)
|
||||||
|
|
||||||
|
@ -39,20 +39,7 @@ func (self *CommitMessagePanelDriver) SwitchToDescription() *CommitDescriptionPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *CommitMessagePanelDriver) Clear() *CommitMessagePanelDriver {
|
func (self *CommitMessagePanelDriver) Clear() *CommitMessagePanelDriver {
|
||||||
// clearing multiple times in case there's multiple lines
|
self.getViewDriver().Clear()
|
||||||
// (the clear button only clears a single line at a time)
|
|
||||||
maxAttempts := 100
|
|
||||||
for i := 0; i < maxAttempts+1; i++ {
|
|
||||||
if self.getViewDriver().getView().Buffer() == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
self.t.press(ClearKey)
|
|
||||||
if i == maxAttempts {
|
|
||||||
panic("failed to clear commit message panel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,24 @@ func (self *ViewDriver) Title(expected *TextMatcher) *ViewDriver {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *ViewDriver) Clear() *ViewDriver {
|
||||||
|
// clearing multiple times in case there's multiple lines
|
||||||
|
// (the clear button only clears a single line at a time)
|
||||||
|
maxAttempts := 100
|
||||||
|
for i := 0; i < maxAttempts+1; i++ {
|
||||||
|
if self.getView().Buffer() == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
self.t.press(ClearKey)
|
||||||
|
if i == maxAttempts {
|
||||||
|
panic("failed to clear view buffer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
// asserts that the view has lines matching the given matchers. One matcher must be passed for each line.
|
// asserts that the view has lines matching the given matchers. One matcher must be passed for each line.
|
||||||
// If you only care about the top n lines, use the TopLines method instead.
|
// If you only care about the top n lines, use the TopLines method instead.
|
||||||
// If you only care about a subset of lines, use the ContainsLines method instead.
|
// If you only care about a subset of lines, use the ContainsLines method instead.
|
||||||
|
@ -28,6 +28,8 @@ var PreserveCommitMessage = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Type("second paragraph").
|
Type("second paragraph").
|
||||||
Cancel()
|
Cancel()
|
||||||
|
|
||||||
|
t.FileSystem().PathPresent(".git/LAZYGIT_PENDING_COMMIT")
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
Press(keys.Files.CommitChanges)
|
Press(keys.Files.CommitChanges)
|
||||||
@ -35,6 +37,22 @@ var PreserveCommitMessage = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
t.ExpectPopup().CommitMessagePanel().
|
t.ExpectPopup().CommitMessagePanel().
|
||||||
Content(Equals("my commit message")).
|
Content(Equals("my commit message")).
|
||||||
SwitchToDescription().
|
SwitchToDescription().
|
||||||
Content(Equals("first paragraph\n\nsecond paragraph"))
|
Content(Equals("first paragraph\n\nsecond paragraph")).
|
||||||
|
Clear().
|
||||||
|
SwitchToSummary().
|
||||||
|
Clear().
|
||||||
|
Cancel()
|
||||||
|
|
||||||
|
t.FileSystem().PathNotPresent(".git/LAZYGIT_PENDING_COMMIT")
|
||||||
|
|
||||||
|
t.Views().Files().
|
||||||
|
IsFocused().
|
||||||
|
Press(keys.Files.CommitChanges)
|
||||||
|
|
||||||
|
t.ExpectPopup().CommitMessagePanel().
|
||||||
|
Type("my new commit message").
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
t.FileSystem().PathNotPresent(".git/LAZYGIT_PENDING_COMMIT")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user