From 40cc008a5afb3c8efe647973e68098239b52bd45 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 16 Oct 2025 11:46:08 +0200 Subject: [PATCH] Avoid auto-stashing when only submodules are out of date Stashing doesn't affect submodules, so if you have a working copy that has out-of-date submodules but no other changes, and then you revert or paste a commit (or invoke one of the many other lazygit commands that auto-stash, e.g. undo), lazygit would previously try to stash changes (which did nothing, but also didn't return an error), perform the operation, and then pop the stash again. If no stashes existed before, then this would only cause a confusing error popup ("error: refs/stash@{0} is not a valid reference"), but if there were stashes, this would try to pop the newest one of these, which is very undesirable and confusing. --- .../custom_patch_options_menu_action.go | 2 +- pkg/gui/controllers/files_controller.go | 10 ++++---- .../controllers/helpers/cherry_pick_helper.go | 2 +- pkg/gui/controllers/helpers/refs_helper.go | 6 ++--- .../helpers/working_tree_helper.go | 24 +++++++++++++++---- .../controllers/local_commits_controller.go | 2 +- pkg/gui/controllers/undo_controller.go | 2 +- .../controllers/workspace_reset_controller.go | 4 ++-- 8 files changed, 34 insertions(+), 18 deletions(-) diff --git a/pkg/gui/controllers/custom_patch_options_menu_action.go b/pkg/gui/controllers/custom_patch_options_menu_action.go index d6a36c2e156..7930484291e 100644 --- a/pkg/gui/controllers/custom_patch_options_menu_action.go +++ b/pkg/gui/controllers/custom_patch_options_menu_action.go @@ -173,7 +173,7 @@ func (self *CustomPatchOptionsMenuAction) handleMovePatchIntoWorkingTree() error self.returnFocusFromPatchExplorerIfNecessary() - mustStash := self.c.Helpers().WorkingTree.IsWorkingTreeDirty() + mustStash := self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() return self.c.ConfirmIf(mustStash, types.ConfirmOpts{ Title: self.c.Tr.MustStashTitle, Prompt: self.c.Tr.MustStashWarning, diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 4a3951c4829..0289936de16 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -973,7 +973,7 @@ func (self *FilesController) createStashMenu() error { { Label: self.c.Tr.StashAllChanges, OnPress: func() error { - if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() { + if !self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() { return errors.New(self.c.Tr.NoFilesToStash) } return self.handleStashSave(self.c.Git().Stash.Push, self.c.Tr.Actions.StashAllChanges) @@ -983,7 +983,7 @@ func (self *FilesController) createStashMenu() error { { Label: self.c.Tr.StashAllChangesKeepIndex, OnPress: func() error { - if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() { + if !self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() { return errors.New(self.c.Tr.NoFilesToStash) } // if there are no staged files it behaves the same as Stash.Save @@ -1002,7 +1002,7 @@ func (self *FilesController) createStashMenu() error { Label: self.c.Tr.StashStagedChanges, OnPress: func() error { // there must be something in staging otherwise the current implementation mucks the stash up - if !self.c.Helpers().WorkingTree.AnyStagedFiles() { + if !self.c.Helpers().WorkingTree.AnyStagedFilesExceptSubmodules() { return errors.New(self.c.Tr.NoTrackedStagedFilesStash) } return self.handleStashSave(self.c.Git().Stash.SaveStagedChanges, self.c.Tr.Actions.StashStagedChanges) @@ -1012,10 +1012,10 @@ func (self *FilesController) createStashMenu() error { { Label: self.c.Tr.StashUnstagedChanges, OnPress: func() error { - if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() { + if !self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() { return errors.New(self.c.Tr.NoFilesToStash) } - if self.c.Helpers().WorkingTree.AnyStagedFiles() { + if self.c.Helpers().WorkingTree.AnyStagedFilesExceptSubmodules() { return self.handleStashSave(self.c.Git().Stash.StashUnstagedChanges, self.c.Tr.Actions.StashUnstagedChanges) } // ordinary stash diff --git a/pkg/gui/controllers/helpers/cherry_pick_helper.go b/pkg/gui/controllers/helpers/cherry_pick_helper.go index 8f5b37a6693..c4be07ceb28 100644 --- a/pkg/gui/controllers/helpers/cherry_pick_helper.go +++ b/pkg/gui/controllers/helpers/cherry_pick_helper.go @@ -77,7 +77,7 @@ func (self *CherryPickHelper) Paste() error { }), HandleConfirm: func() error { return self.c.WithWaitingStatusSync(self.c.Tr.CherryPickingStatus, func() error { - mustStash := IsWorkingTreeDirty(self.c.Model().Files) + mustStash := IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules) self.c.LogAction(self.c.Tr.Actions.CherryPick) diff --git a/pkg/gui/controllers/helpers/refs_helper.go b/pkg/gui/controllers/helpers/refs_helper.go index cfb9d3bab15..39cda5bd493 100644 --- a/pkg/gui/controllers/helpers/refs_helper.go +++ b/pkg/gui/controllers/helpers/refs_helper.go @@ -258,7 +258,7 @@ func (self *RefsHelper) CreateGitResetMenu(name string, ref string) error { style.FgRed.Sprintf("reset --%s %s", row.strength, name), }, OnPress: func() error { - return self.c.ConfirmIf(row.strength == "hard" && IsWorkingTreeDirty(self.c.Model().Files), + return self.c.ConfirmIf(row.strength == "hard" && IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules), types.ConfirmOpts{ Title: self.c.Tr.Actions.HardReset, Prompt: self.c.Tr.ResetHardConfirmation, @@ -484,7 +484,7 @@ func (self *RefsHelper) moveCommitsToNewBranchStackedOnCurrentBranch(newBranchNa return err } - mustStash := IsWorkingTreeDirty(self.c.Model().Files) + mustStash := IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules) if mustStash { if err := self.c.Git().Stash.Push(fmt.Sprintf(self.c.Tr.AutoStashForNewBranch, newBranchName)); err != nil { return err @@ -517,7 +517,7 @@ func (self *RefsHelper) moveCommitsToNewBranchOffOfMainBranch(newBranchName stri return commit.Status == models.StatusUnpushed }) - mustStash := IsWorkingTreeDirty(self.c.Model().Files) + mustStash := IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules) if mustStash { if err := self.c.Git().Stash.Push(fmt.Sprintf(self.c.Tr.AutoStashForNewBranch, newBranchName)); err != nil { return err diff --git a/pkg/gui/controllers/helpers/working_tree_helper.go b/pkg/gui/controllers/helpers/working_tree_helper.go index 9e6e362966d..6437afa3507 100644 --- a/pkg/gui/controllers/helpers/working_tree_helper.go +++ b/pkg/gui/controllers/helpers/working_tree_helper.go @@ -47,6 +47,14 @@ func AnyStagedFiles(files []*models.File) bool { return lo.SomeBy(files, func(f *models.File) bool { return f.HasStagedChanges }) } +func (self *WorkingTreeHelper) AnyStagedFilesExceptSubmodules() bool { + return AnyStagedFilesExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules) +} + +func AnyStagedFilesExceptSubmodules(files []*models.File, submoduleConfigs []*models.SubmoduleConfig) bool { + return lo.SomeBy(files, func(f *models.File) bool { return f.HasStagedChanges && !f.IsSubmodule(submoduleConfigs) }) +} + func (self *WorkingTreeHelper) AnyTrackedFiles() bool { return AnyTrackedFiles(self.c.Model().Files) } @@ -55,12 +63,20 @@ 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 (self *WorkingTreeHelper) AnyTrackedFilesExceptSubmodules() bool { + return AnyTrackedFilesExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules) +} + +func AnyTrackedFilesExceptSubmodules(files []*models.File, submoduleConfigs []*models.SubmoduleConfig) bool { + return lo.SomeBy(files, func(f *models.File) bool { return f.Tracked && !f.IsSubmodule(submoduleConfigs) }) +} + +func (self *WorkingTreeHelper) IsWorkingTreeDirtyExceptSubmodules() bool { + return IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules) } -func IsWorkingTreeDirty(files []*models.File) bool { - return AnyStagedFiles(files) || AnyTrackedFiles(files) +func IsWorkingTreeDirtyExceptSubmodules(files []*models.File, submoduleConfigs []*models.SubmoduleConfig) bool { + return AnyStagedFilesExceptSubmodules(files, submoduleConfigs) || AnyTrackedFilesExceptSubmodules(files, submoduleConfigs) } func (self *WorkingTreeHelper) FileForSubmodule(submodule *models.SubmoduleConfig) *models.File { diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index b70a4ecb984..9badaf1edcb 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -856,7 +856,7 @@ func (self *LocalCommitsController) revert(commits []*models.Commit, start, end HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.RevertCommit) return self.c.WithWaitingStatusSync(self.c.Tr.RevertingStatus, func() error { - mustStash := helpers.IsWorkingTreeDirty(self.c.Model().Files) + mustStash := helpers.IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules) if mustStash { if err := self.c.Git().Stash.Push(self.c.Tr.AutoStashForReverting); err != nil { diff --git a/pkg/gui/controllers/undo_controller.go b/pkg/gui/controllers/undo_controller.go index cd3b04e4f58..cdc8a128025 100644 --- a/pkg/gui/controllers/undo_controller.go +++ b/pkg/gui/controllers/undo_controller.go @@ -257,7 +257,7 @@ func (self *UndoController) hardResetWithAutoStash(commitHash string, options ha } // if we have any modified tracked files we need to auto-stash - dirtyWorkingTree := self.c.Helpers().WorkingTree.IsWorkingTreeDirty() + dirtyWorkingTree := self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() if dirtyWorkingTree { return self.c.WithWaitingStatus(options.WaitingStatus, func(gocui.Task) error { if err := self.c.Git().Stash.Push(fmt.Sprintf(self.c.Tr.AutoStashForUndo, utils.ShortHash(commitHash))); err != nil { diff --git a/pkg/gui/controllers/workspace_reset_controller.go b/pkg/gui/controllers/workspace_reset_controller.go index de6fc457755..82357922f4a 100644 --- a/pkg/gui/controllers/workspace_reset_controller.go +++ b/pkg/gui/controllers/workspace_reset_controller.go @@ -100,7 +100,7 @@ func (self *FilesController) createResetMenu() error { Tooltip: self.c.Tr.DiscardStagedChangesDescription, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.RemoveStagedFiles) - if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() { + if !self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() { return errors.New(self.c.Tr.NoTrackedStagedFilesStash) } if err := self.c.Git().Stash.SaveStagedChanges("[lazygit] tmp stash"); err != nil { @@ -159,7 +159,7 @@ func (self *FilesController) createResetMenu() error { red.Sprint("git reset --hard HEAD"), }, OnPress: func() error { - return self.c.ConfirmIf(helpers.IsWorkingTreeDirty(self.c.Model().Files), + return self.c.ConfirmIf(helpers.IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules), types.ConfirmOpts{ Title: self.c.Tr.Actions.HardReset, Prompt: self.c.Tr.ResetHardConfirmation,