1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-03 00:57:52 +02:00
Files
lazygit/pkg/gui/controllers/helpers/working_tree_helper.go
Stefan Haller d82852a909 Change Refresh to not return an error
Refresh is one of those functions that shouldn't require error handling (similar
to triggering a redraw of the UI, see
https://github.com/jesseduffield/lazygit/issues/3887).

As far as I see, the only reason why Refresh can currently return an error is
that the Then function returns one. The actual refresh errors, e.g. from the git
calls that are made to fetch data, are already logged and swallowed. Most of the
Then functions do only UI stuff such as selecting a list item, and always return
nil; there's only one that can return an error (updating the rebase todo file in
LocalCommitsController.startInteractiveRebaseWithEdit); it's not a critical
error if this fails, it is only used for setting rebase todo items to "edit"
when you start an interactive rebase by pressing 'e' on a range selection of
commits. We simply log this error instead of returning it.
2025-07-02 16:09:42 +02:00

250 lines
7.5 KiB
Go

package helpers
import (
"errors"
"fmt"
"regexp"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
type WorkingTreeHelper struct {
c *HelperCommon
refHelper *RefsHelper
commitsHelper *CommitsHelper
gpgHelper *GpgHelper
}
func NewWorkingTreeHelper(
c *HelperCommon,
refHelper *RefsHelper,
commitsHelper *CommitsHelper,
gpgHelper *GpgHelper,
) *WorkingTreeHelper {
return &WorkingTreeHelper{
c: c,
refHelper: refHelper,
commitsHelper: commitsHelper,
gpgHelper: gpgHelper,
}
}
func (self *WorkingTreeHelper) AnyStagedFiles() bool {
return AnyStagedFiles(self.c.Model().Files)
}
func AnyStagedFiles(files []*models.File) bool {
return lo.SomeBy(files, func(f *models.File) bool { return f.HasStagedChanges })
}
func (self *WorkingTreeHelper) AnyTrackedFiles() bool {
return AnyTrackedFiles(self.c.Model().Files)
}
func AnyTrackedFiles(files []*models.File) bool {
return lo.SomeBy(files, func(f *models.File) bool { return f.Tracked })
}
func (self *WorkingTreeHelper) IsWorkingTreeDirty() bool {
return IsWorkingTreeDirty(self.c.Model().Files)
}
func IsWorkingTreeDirty(files []*models.File) bool {
return AnyStagedFiles(files) || AnyTrackedFiles(files)
}
func (self *WorkingTreeHelper) FileForSubmodule(submodule *models.SubmoduleConfig) *models.File {
for _, file := range self.c.Model().Files {
if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) {
return file
}
}
return nil
}
func (self *WorkingTreeHelper) OpenMergeTool() error {
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.MergeToolTitle,
Prompt: self.c.Tr.MergeToolPrompt,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.OpenMergeTool)
return self.c.RunSubprocessAndRefresh(
self.c.Git().WorkingTree.OpenMergeToolCmdObj(),
)
},
})
return nil
}
func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage string, forceSkipHooks bool) error {
return self.WithEnsureCommittableFiles(func() error {
self.commitsHelper.OpenCommitMessagePanel(
&OpenCommitMessagePanelOpts{
CommitIndex: context.NoCommitIndex,
InitialMessage: initialMessage,
SummaryTitle: self.c.Tr.CommitSummaryTitle,
DescriptionTitle: self.c.Tr.CommitDescriptionTitle,
PreserveMessage: true,
OnConfirm: func(summary string, description string) error {
return self.handleCommit(summary, description, forceSkipHooks)
},
OnSwitchToEditor: func(filepath string) error {
return self.switchFromCommitMessagePanelToEditor(filepath, forceSkipHooks)
},
ForceSkipHooks: forceSkipHooks,
SkipHooksPrefix: self.c.UserConfig().Git.SkipHookPrefix,
},
)
return nil
})
}
func (self *WorkingTreeHelper) handleCommit(summary string, description string, forceSkipHooks bool) error {
cmdObj := self.c.Git().Commit.CommitCmdObj(summary, description, forceSkipHooks)
self.c.LogAction(self.c.Tr.Actions.Commit)
return self.gpgHelper.WithGpgHandling(cmdObj, git_commands.CommitGpgSign, self.c.Tr.CommittingStatus,
func() error {
self.commitsHelper.ClearPreservedCommitMessage()
return nil
}, nil)
}
func (self *WorkingTreeHelper) switchFromCommitMessagePanelToEditor(filepath string, forceSkipHooks bool) error {
// We won't be able to tell whether the commit was successful, because
// RunSubprocessAndRefresh doesn't return the error (it opens an error alert
// itself and returns nil on error). But even if we could, we wouldn't have
// access to the last message that the user typed, and it might be very
// different from what was last in the commit panel. So the best we can do
// here is to always clear the remembered commit message.
self.commitsHelper.ClearPreservedCommitMessage()
self.c.LogAction(self.c.Tr.Actions.Commit)
return self.c.RunSubprocessAndRefresh(
self.c.Git().Commit.CommitInEditorWithMessageFileCmdObj(filepath, forceSkipHooks),
)
}
// HandleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (self *WorkingTreeHelper) HandleCommitEditorPress() error {
return self.WithEnsureCommittableFiles(func() error {
// See reasoning in switchFromCommitMessagePanelToEditor for why it makes sense
// to clear this message before calling into the editor
self.commitsHelper.ClearPreservedCommitMessage()
self.c.LogAction(self.c.Tr.Actions.Commit)
return self.c.RunSubprocessAndRefresh(
self.c.Git().Commit.CommitEditorCmdObj(),
)
})
}
func (self *WorkingTreeHelper) HandleWIPCommitPress() error {
var initialMessage string
preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
if preservedMessage == "" {
// Use the skipHook prefix only if we don't have a preserved message
initialMessage = self.c.UserConfig().Git.SkipHookPrefix
}
return self.HandleCommitPressWithMessage(initialMessage, true)
}
func (self *WorkingTreeHelper) HandleCommitPress() error {
message := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
if message == "" {
commitPrefixConfigs := self.commitPrefixConfigsForRepo()
for _, commitPrefixConfig := range commitPrefixConfigs {
prefixPattern := commitPrefixConfig.Pattern
if prefixPattern == "" {
continue
}
prefixReplace := commitPrefixConfig.Replace
branchName := self.refHelper.GetCheckedOutRef().Name
rgx, err := regexp.Compile(prefixPattern)
if err != nil {
return fmt.Errorf("%s: %s", self.c.Tr.CommitPrefixPatternError, err.Error())
}
if rgx.MatchString(branchName) {
prefix := rgx.ReplaceAllString(branchName, prefixReplace)
message = prefix
break
}
}
}
return self.HandleCommitPressWithMessage(message, false)
}
func (self *WorkingTreeHelper) WithEnsureCommittableFiles(handler func() error) error {
if err := self.prepareFilesForCommit(); err != nil {
return err
}
if len(self.c.Model().Files) == 0 {
return errors.New(self.c.Tr.NoFilesStagedTitle)
}
if !self.AnyStagedFiles() {
return self.promptToStageAllAndRetry(handler)
}
return handler()
}
func (self *WorkingTreeHelper) promptToStageAllAndRetry(retry func() error) error {
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.NoFilesStagedTitle,
Prompt: self.c.Tr.NoFilesStagedPrompt,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
if err := self.c.Git().WorkingTree.StageAll(); err != nil {
return err
}
self.syncRefresh()
return retry()
},
})
return nil
}
// for when you need to refetch files before continuing an action. Runs synchronously.
func (self *WorkingTreeHelper) syncRefresh() {
self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}})
}
func (self *WorkingTreeHelper) prepareFilesForCommit() error {
noStagedFiles := !self.AnyStagedFiles()
if noStagedFiles && self.c.UserConfig().Gui.SkipNoStagedFilesWarning {
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
err := self.c.Git().WorkingTree.StageAll()
if err != nil {
return err
}
self.syncRefresh()
}
return nil
}
func (self *WorkingTreeHelper) commitPrefixConfigsForRepo() []config.CommitPrefixConfig {
cfg, ok := self.c.UserConfig().Git.CommitPrefixes[self.c.Git().RepoPaths.RepoName()]
if ok {
return append(cfg, self.c.UserConfig().Git.CommitPrefix...)
}
return self.c.UserConfig().Git.CommitPrefix
}