Skip to content

Commit

Permalink
Preserve pending commit message when closing/re-opening (#4191)
Browse files Browse the repository at this point in the history
- **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.
  • Loading branch information
stefanhaller authored Feb 7, 2025
2 parents fcf30ca + 6065908 commit a0aa7a1
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 25 deletions.
50 changes: 44 additions & 6 deletions pkg/gui/context/commit_message_context.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package context

import (
"os"
"path/filepath"
"strconv"
"strings"

"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spf13/afero"
)

const PreservedCommitMessageFileName = "LAZYGIT_PENDING_COMMIT"

type CommitMessageContext struct {
c *ContextCommon
types.Context
Expand All @@ -33,8 +38,6 @@ type CommitMessageViewModel struct {
// 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
initialMessage string
// the full preserved message (combined summary and description)
preservedMessage string
// invoked when pressing enter in the commit message panel
onConfirm func(string, string) error
// invoked when pressing the switch-to-editor key binding
Expand Down Expand Up @@ -75,16 +78,51 @@ func (self *CommitMessageContext) GetSelectedIndex() int {
return self.viewModel.selectedindex
}

func (self *CommitMessageContext) GetPreservedMessagePath() string {
return filepath.Join(self.c.Git().RepoPaths.WorktreeGitDirPath(), PreservedCommitMessageFileName)
}

func (self *CommitMessageContext) GetPreserveMessage() bool {
return self.viewModel.preserveMessage
}

func (self *CommitMessageContext) GetPreservedMessage() string {
return self.viewModel.preservedMessage
func (self *CommitMessageContext) getPreservedMessage() (string, error) {
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) GetPreservedMessageAndLogError() string {
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) {
self.viewModel.preservedMessage = message
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 {
Expand Down
6 changes: 3 additions & 3 deletions pkg/gui/controllers/helpers/commits_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (self *CommitsHelper) UpdateCommitPanelView(message string) {
}

if self.c.Contexts().CommitMessage.GetPreserveMessage() {
preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessage()
preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
self.SetMessageAndDescriptionInView(preservedMessage)
return
}
Expand Down Expand Up @@ -156,7 +156,7 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp
func (self *CommitsHelper) OnCommitSuccess() {
// if we have a preserved message we want to clear it on success
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
self.c.Contexts().CommitMessage.SetPreservedMessage("")
self.c.Contexts().CommitMessage.SetPreservedMessageAndLogError("")
}
}

Expand All @@ -179,7 +179,7 @@ func (self *CommitsHelper) CloseCommitMessagePanel() {
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
message := self.JoinCommitMessageAndUnwrappedDescription()
if message != self.c.Contexts().CommitMessage.GetInitialMessage() {
self.c.Contexts().CommitMessage.SetPreservedMessage(message)
self.c.Contexts().CommitMessage.SetPreservedMessageAndLogError(message)
}
} else {
self.SetMessageAndDescriptionInView("")
Expand Down
2 changes: 1 addition & 1 deletion pkg/gui/controllers/helpers/working_tree_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error {
}

func (self *WorkingTreeHelper) HandleCommitPress() error {
message := self.c.Contexts().CommitMessage.GetPreservedMessage()
message := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()

if message == "" {
commitPrefixConfig := self.commitPrefixConfigForRepo()
Expand Down
5 changes: 5 additions & 0 deletions pkg/integration/components/commit_description_panel_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ func (self *CommitDescriptionPanelDriver) AddCoAuthor(author string) *CommitDesc
return self
}

func (self *CommitDescriptionPanelDriver) Clear() *CommitDescriptionPanelDriver {
self.getViewDriver().Clear()
return self
}

func (self *CommitDescriptionPanelDriver) Title(expected *TextMatcher) *CommitDescriptionPanelDriver {
self.getViewDriver().Title(expected)

Expand Down
15 changes: 1 addition & 14 deletions pkg/integration/components/commit_message_panel_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,7 @@ func (self *CommitMessagePanelDriver) SwitchToDescription() *CommitDescriptionPa
}

func (self *CommitMessagePanelDriver) Clear() *CommitMessagePanelDriver {
// 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.getViewDriver().getView().Buffer() == "" {
break
}

self.t.press(ClearKey)
if i == maxAttempts {
panic("failed to clear commit message panel")
}
}

self.getViewDriver().Clear()
return self
}

Expand Down
18 changes: 18 additions & 0 deletions pkg/integration/components/view_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ func (self *ViewDriver) Title(expected *TextMatcher) *ViewDriver {
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.
// 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.
Expand Down
20 changes: 19 additions & 1 deletion pkg/integration/tests/commit/preserve_commit_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,31 @@ var PreserveCommitMessage = NewIntegrationTest(NewIntegrationTestArgs{
Type("second paragraph").
Cancel()

t.FileSystem().PathPresent(".git/LAZYGIT_PENDING_COMMIT")

t.Views().Files().
IsFocused().
Press(keys.Files.CommitChanges)

t.ExpectPopup().CommitMessagePanel().
Content(Equals("my commit message")).
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")
},
})

0 comments on commit a0aa7a1

Please sign in to comment.