Skip to content

Commit a0aa7a1

Browse files
authored
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.
2 parents fcf30ca + 6065908 commit a0aa7a1

File tree

7 files changed

+91
-25
lines changed

7 files changed

+91
-25
lines changed

pkg/gui/context/commit_message_context.go

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package context
22

33
import (
4+
"os"
5+
"path/filepath"
46
"strconv"
57
"strings"
68

79
"github.com/jesseduffield/gocui"
810
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
911
"github.com/jesseduffield/lazygit/pkg/gui/types"
1012
"github.com/jesseduffield/lazygit/pkg/utils"
13+
"github.com/spf13/afero"
1114
)
1215

16+
const PreservedCommitMessageFileName = "LAZYGIT_PENDING_COMMIT"
17+
1318
type CommitMessageContext struct {
1419
c *ContextCommon
1520
types.Context
@@ -33,8 +38,6 @@ type CommitMessageViewModel struct {
3338
// we remember the initial message so that we can tell whether we should preserve
3439
// the message; if it's still identical to the initial message, we don't
3540
initialMessage string
36-
// the full preserved message (combined summary and description)
37-
preservedMessage string
3841
// invoked when pressing enter in the commit message panel
3942
onConfirm func(string, string) error
4043
// invoked when pressing the switch-to-editor key binding
@@ -75,16 +78,51 @@ func (self *CommitMessageContext) GetSelectedIndex() int {
7578
return self.viewModel.selectedindex
7679
}
7780

81+
func (self *CommitMessageContext) GetPreservedMessagePath() string {
82+
return filepath.Join(self.c.Git().RepoPaths.WorktreeGitDirPath(), PreservedCommitMessageFileName)
83+
}
84+
7885
func (self *CommitMessageContext) GetPreserveMessage() bool {
7986
return self.viewModel.preserveMessage
8087
}
8188

82-
func (self *CommitMessageContext) GetPreservedMessage() string {
83-
return self.viewModel.preservedMessage
89+
func (self *CommitMessageContext) getPreservedMessage() (string, error) {
90+
buf, err := afero.ReadFile(self.c.Fs, self.GetPreservedMessagePath())
91+
if os.IsNotExist(err) {
92+
return "", nil
93+
}
94+
if err != nil {
95+
return "", err
96+
}
97+
return string(buf), nil
98+
}
99+
100+
func (self *CommitMessageContext) GetPreservedMessageAndLogError() string {
101+
msg, err := self.getPreservedMessage()
102+
if err != nil {
103+
self.c.Log.Errorf("error when retrieving persisted commit message: %v", err)
104+
}
105+
return msg
84106
}
85107

86-
func (self *CommitMessageContext) SetPreservedMessage(message string) {
87-
self.viewModel.preservedMessage = message
108+
func (self *CommitMessageContext) setPreservedMessage(message string) error {
109+
preservedFilePath := self.GetPreservedMessagePath()
110+
111+
if len(message) == 0 {
112+
err := self.c.Fs.Remove(preservedFilePath)
113+
if os.IsNotExist(err) {
114+
return nil
115+
}
116+
return err
117+
}
118+
119+
return afero.WriteFile(self.c.Fs, preservedFilePath, []byte(message), 0o644)
120+
}
121+
122+
func (self *CommitMessageContext) SetPreservedMessageAndLogError(message string) {
123+
if err := self.setPreservedMessage(message); err != nil {
124+
self.c.Log.Errorf("error when persisting commit message: %v", err)
125+
}
88126
}
89127

90128
func (self *CommitMessageContext) GetInitialMessage() string {

pkg/gui/controllers/helpers/commits_helper.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func (self *CommitsHelper) UpdateCommitPanelView(message string) {
113113
}
114114

115115
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
116-
preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessage()
116+
preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
117117
self.SetMessageAndDescriptionInView(preservedMessage)
118118
return
119119
}
@@ -156,7 +156,7 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp
156156
func (self *CommitsHelper) OnCommitSuccess() {
157157
// if we have a preserved message we want to clear it on success
158158
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
159-
self.c.Contexts().CommitMessage.SetPreservedMessage("")
159+
self.c.Contexts().CommitMessage.SetPreservedMessageAndLogError("")
160160
}
161161
}
162162

@@ -179,7 +179,7 @@ func (self *CommitsHelper) CloseCommitMessagePanel() {
179179
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
180180
message := self.JoinCommitMessageAndUnwrappedDescription()
181181
if message != self.c.Contexts().CommitMessage.GetInitialMessage() {
182-
self.c.Contexts().CommitMessage.SetPreservedMessage(message)
182+
self.c.Contexts().CommitMessage.SetPreservedMessageAndLogError(message)
183183
}
184184
} else {
185185
self.SetMessageAndDescriptionInView("")

pkg/gui/controllers/helpers/working_tree_helper.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error {
149149
}
150150

151151
func (self *WorkingTreeHelper) HandleCommitPress() error {
152-
message := self.c.Contexts().CommitMessage.GetPreservedMessage()
152+
message := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
153153

154154
if message == "" {
155155
commitPrefixConfig := self.commitPrefixConfigForRepo()

pkg/integration/components/commit_description_panel_driver.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ func (self *CommitDescriptionPanelDriver) AddCoAuthor(author string) *CommitDesc
5252
return self
5353
}
5454

55+
func (self *CommitDescriptionPanelDriver) Clear() *CommitDescriptionPanelDriver {
56+
self.getViewDriver().Clear()
57+
return self
58+
}
59+
5560
func (self *CommitDescriptionPanelDriver) Title(expected *TextMatcher) *CommitDescriptionPanelDriver {
5661
self.getViewDriver().Title(expected)
5762

pkg/integration/components/commit_message_panel_driver.go

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,7 @@ func (self *CommitMessagePanelDriver) SwitchToDescription() *CommitDescriptionPa
3939
}
4040

4141
func (self *CommitMessagePanelDriver) Clear() *CommitMessagePanelDriver {
42-
// clearing multiple times in case there's multiple lines
43-
// (the clear button only clears a single line at a time)
44-
maxAttempts := 100
45-
for i := 0; i < maxAttempts+1; i++ {
46-
if self.getViewDriver().getView().Buffer() == "" {
47-
break
48-
}
49-
50-
self.t.press(ClearKey)
51-
if i == maxAttempts {
52-
panic("failed to clear commit message panel")
53-
}
54-
}
55-
42+
self.getViewDriver().Clear()
5643
return self
5744
}
5845

pkg/integration/components/view_driver.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ func (self *ViewDriver) Title(expected *TextMatcher) *ViewDriver {
4040
return self
4141
}
4242

43+
func (self *ViewDriver) Clear() *ViewDriver {
44+
// clearing multiple times in case there's multiple lines
45+
// (the clear button only clears a single line at a time)
46+
maxAttempts := 100
47+
for i := 0; i < maxAttempts+1; i++ {
48+
if self.getView().Buffer() == "" {
49+
break
50+
}
51+
52+
self.t.press(ClearKey)
53+
if i == maxAttempts {
54+
panic("failed to clear view buffer")
55+
}
56+
}
57+
58+
return self
59+
}
60+
4361
// asserts that the view has lines matching the given matchers. One matcher must be passed for each line.
4462
// If you only care about the top n lines, use the TopLines method instead.
4563
// If you only care about a subset of lines, use the ContainsLines method instead.

pkg/integration/tests/commit/preserve_commit_message.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,31 @@ var PreserveCommitMessage = NewIntegrationTest(NewIntegrationTestArgs{
2828
Type("second paragraph").
2929
Cancel()
3030

31+
t.FileSystem().PathPresent(".git/LAZYGIT_PENDING_COMMIT")
32+
3133
t.Views().Files().
3234
IsFocused().
3335
Press(keys.Files.CommitChanges)
3436

3537
t.ExpectPopup().CommitMessagePanel().
3638
Content(Equals("my commit message")).
3739
SwitchToDescription().
38-
Content(Equals("first paragraph\n\nsecond paragraph"))
40+
Content(Equals("first paragraph\n\nsecond paragraph")).
41+
Clear().
42+
SwitchToSummary().
43+
Clear().
44+
Cancel()
45+
46+
t.FileSystem().PathNotPresent(".git/LAZYGIT_PENDING_COMMIT")
47+
48+
t.Views().Files().
49+
IsFocused().
50+
Press(keys.Files.CommitChanges)
51+
52+
t.ExpectPopup().CommitMessagePanel().
53+
Type("my new commit message").
54+
Confirm()
55+
56+
t.FileSystem().PathNotPresent(".git/LAZYGIT_PENDING_COMMIT")
3957
},
4058
})

0 commit comments

Comments
 (0)