Run the stash operations with a waiting status

Creating and applying a stash both touch every changed file, so in a
large repo they can take long enough to be noticeable — and running them
on the UI thread meant the confirmation popup stayed on screen, frozen,
for the whole operation. Run them on a worker instead, with a spinner,
and keep blocking input for their duration so that the type-ahead
guarantee the refresh used to provide still holds.

Dropping stays on the UI thread: it only rewrites the stash reflog, so
it's fast no matter how big the stashes are.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-08-07 06:54:55 +02:00
parent ed22322ec8
commit 4c39b0b903
3 changed files with 44 additions and 23 deletions

View file

@ -1508,16 +1508,20 @@ func (self *FilesController) handleStashSave(stashFunc func(message string) erro
self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.StashChanges,
HandleConfirm: func(stashComment string) error {
self.c.LogAction(action)
return self.c.WithWaitingStatusBlockingInput(
types.WaitingStatusOpts{Message: self.c.Tr.StashingStatus},
func(gocui.Task) error {
self.c.LogAction(action)
if err := stashFunc(stashComment); err != nil {
return err
}
self.c.Refresh(types.RefreshOptions{
BatchUIUpdates: true,
Scope: []types.RefreshableView{types.STASH, types.FILES},
})
return nil
if err := stashFunc(stashComment); err != nil {
return err
}
self.c.RefreshFromWorker(types.RefreshOptions{
BatchUIUpdates: true,
Scope: []types.RefreshableView{types.STASH, types.FILES},
})
return nil
})
},
AllowEmptyInput: true,
})

View file

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
@ -120,21 +121,29 @@ func (self *StashController) handleStashApply(stashEntry *models.StashEntry) err
Title: self.c.Tr.StashApply,
Prompt: self.c.Tr.SureApplyStashEntry,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.ApplyStash)
err := self.c.Git().Stash.Apply(stashEntry.Index)
self.postStashRefresh(err == nil && self.c.UserConfig().Gui.SwitchToFilesAfterStashApply)
return err
return self.c.WithWaitingStatusBlockingInput(
types.WaitingStatusOpts{Message: self.c.Tr.ApplyingStashStatus},
func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.ApplyStash)
err := self.c.Git().Stash.Apply(stashEntry.Index)
self.postStashRefresh(err == nil && self.c.UserConfig().Gui.SwitchToFilesAfterStashApply)
return err
})
},
})
}
func (self *StashController) handleStashPop(stashEntry *models.StashEntry) error {
pop := func() error {
self.c.LogAction(self.c.Tr.Actions.PopStash)
self.c.LogCommand(fmt.Sprintf(self.c.Tr.Log.PoppingStash, stashEntry.Hash), false)
err := self.c.Git().Stash.Pop(stashEntry.Index)
self.postStashRefresh(err == nil && self.c.UserConfig().Gui.SwitchToFilesAfterStashPop)
return err
return self.c.WithWaitingStatusBlockingInput(
types.WaitingStatusOpts{Message: self.c.Tr.PoppingStashStatus},
func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.PopStash)
self.c.LogCommand(fmt.Sprintf(self.c.Tr.Log.PoppingStash, stashEntry.Hash), false)
err := self.c.Git().Stash.Pop(stashEntry.Index)
self.postStashRefresh(err == nil && self.c.UserConfig().Gui.SwitchToFilesAfterStashPop)
return err
})
}
if self.c.UserConfig().Gui.SkipStashWarning {
@ -198,12 +207,14 @@ func (self *StashController) handleStashDrop(stashEntries []*models.StashEntry)
// postStashRefresh refreshes the panels that applying or popping a stash
// affects, moving the focus to the files panel if switchToFiles is set.
//
// Call it from the worker that ran the stash command, from inside a
// WithWaitingStatusBlockingInput: popping shifts the indices of the remaining
// stash entries, so acting on the next entry in quick succession (confirming
// the popup and pressing the key again right away) has to be held off until
// the refreshed list is in place, or it would target the wrong stash.
func (self *StashController) postStashRefresh(switchToFiles bool) {
// Block input until the refresh has landed: popping shifts the indices of
// the remaining stash entries, and acting on the next entry in quick
// succession (confirming the popup and pressing the key again right away)
// must see the refreshed list, or it would target the wrong stash.
self.c.RefreshBlockingInput(types.RefreshOptions{
self.c.RefreshFromWorker(types.RefreshOptions{
BatchUIUpdates: true,
Scope: []types.RefreshableView{types.STASH, types.FILES},
Then: func() error {

View file

@ -443,6 +443,9 @@ type TranslationSet struct {
MovingCommitsToNewBranchStatus string
ApplyingFilterStatus string
RemovingFilterStatus string
StashingStatus string
ApplyingStashStatus string
PoppingStashStatus string
CommitFiles string
SubCommitsDynamicTitle string
CommitFilesDynamicTitle string
@ -1603,6 +1606,9 @@ func EnglishTranslationSet() *TranslationSet {
MovingCommitsToNewBranchStatus: "Moving commits to new branch",
ApplyingFilterStatus: "Applying filter",
RemovingFilterStatus: "Removing filter",
StashingStatus: "Stashing",
ApplyingStashStatus: "Applying stash",
PoppingStashStatus: "Popping stash",
CommitFiles: "Commit files",
SubCommitsDynamicTitle: "Commits (%s)",
CommitFilesDynamicTitle: "Diff files (%s)",