mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Stashing and popping change both the stash list and the files list. With each scope updating the UI as soon as its own refresh is done, the two panels visibly change at different times; with gui.shrinkSidePanelsToContent that also means their sizes change at different times than their contents. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
243 lines
7.7 KiB
Go
243 lines
7.7 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
)
|
|
|
|
type StashController struct {
|
|
baseController
|
|
*ListControllerTrait[*models.StashEntry]
|
|
c *ControllerCommon
|
|
}
|
|
|
|
var _ types.IController = &StashController{}
|
|
|
|
func NewStashController(
|
|
c *ControllerCommon,
|
|
) *StashController {
|
|
return &StashController{
|
|
baseController: baseController{},
|
|
ListControllerTrait: NewListControllerTrait(
|
|
c,
|
|
c.Contexts().Stash,
|
|
c.Contexts().Stash.GetSelected,
|
|
c.Contexts().Stash.GetSelectedItems,
|
|
),
|
|
c: c,
|
|
}
|
|
}
|
|
|
|
func (self *StashController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
|
bindings := []*types.Binding{
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Select),
|
|
Handler: self.withItem(self.handleStashApply),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.Apply,
|
|
Tooltip: self.c.Tr.StashApplyTooltip,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Stash.PopStash),
|
|
Handler: self.withItem(self.handleStashPop),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.Pop,
|
|
Tooltip: self.c.Tr.StashPopTooltip,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Remove),
|
|
Handler: self.withItems(self.handleStashDrop),
|
|
GetDisabledReason: self.require(self.itemRangeSelected()),
|
|
Description: self.c.Tr.Drop,
|
|
Tooltip: self.c.Tr.StashDropTooltip,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.New),
|
|
Handler: self.withItem(self.handleNewBranchOffStashEntry),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.NewBranch,
|
|
Tooltip: self.c.Tr.NewBranchFromStashTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.NewWorktree),
|
|
Handler: self.withItem(self.c.Helpers().Worktree.NewWorktreeMenuForStash),
|
|
Description: self.c.Tr.NewWorktree,
|
|
OpensMenu: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Stash.RenameStash),
|
|
Handler: self.withItem(self.handleRenameStashEntry),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.RenameStash,
|
|
},
|
|
}
|
|
|
|
return bindings
|
|
}
|
|
|
|
func (self *StashController) GetOnRenderToMain() func() {
|
|
return func() {
|
|
self.c.Helpers().Diff.WithDiffModeCheck(func() {
|
|
var task types.UpdateTask
|
|
stashEntry := self.context().GetSelected()
|
|
if stashEntry == nil {
|
|
task = types.NewRenderStringTask(self.c.Tr.NoStashEntries)
|
|
} else {
|
|
prefix := style.FgYellow.Sprintf("%s\n\n", stashEntry.Description())
|
|
task = types.NewRunPtyTaskWithPrefix(
|
|
self.c.Git().Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd(),
|
|
prefix,
|
|
)
|
|
}
|
|
|
|
self.c.RenderToMainViews(types.RefreshMainOpts{
|
|
Pair: self.c.MainViewPairs().Normal,
|
|
Main: &types.ViewUpdateOpts{
|
|
Title: "Stash",
|
|
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
|
|
Task: task,
|
|
},
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func (self *StashController) context() *context.StashContext {
|
|
return self.c.Contexts().Stash
|
|
}
|
|
|
|
func (self *StashController) handleStashApply(stashEntry *models.StashEntry) error {
|
|
return self.c.ConfirmIf(!self.c.UserConfig().Gui.SkipStashWarning,
|
|
types.ConfirmOpts{
|
|
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()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if self.c.UserConfig().Gui.SwitchToFilesAfterStashApply {
|
|
self.c.Context().Push(self.c.Contexts().Files, types.OnFocusOpts{})
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
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()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if self.c.UserConfig().Gui.SwitchToFilesAfterStashPop {
|
|
self.c.Context().Push(self.c.Contexts().Files, types.OnFocusOpts{})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if self.c.UserConfig().Gui.SkipStashWarning {
|
|
return pop()
|
|
}
|
|
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.StashPop,
|
|
Prompt: self.c.Tr.SurePopStashEntry,
|
|
HandleConfirm: func() error {
|
|
return pop()
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *StashController) handleStashDrop(stashEntries []*models.StashEntry) error {
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.StashDrop,
|
|
Prompt: self.c.Tr.SureDropStashEntry,
|
|
HandleConfirm: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.DropStash)
|
|
// Refresh once at the end rather than after each drop: a refresh
|
|
// from the UI thread finishes in the background, so firing one per
|
|
// iteration lets the workers race and an earlier, stale result can
|
|
// land last. The indices are captured up front and we drop
|
|
// highest-first, so the remaining lower indices stay valid without
|
|
// an intervening refresh. Block input until the refresh has
|
|
// landed, so that dropping the next entry in quick succession
|
|
// (confirming and pressing the key again right away) sees the
|
|
// refreshed list and not the stale, pre-drop indices.
|
|
defer self.c.RefreshBlockingInput(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH}})
|
|
for i := len(stashEntries) - 1; i >= 0; i-- {
|
|
self.c.LogCommand(fmt.Sprintf(self.c.Tr.Log.DroppingStash, stashEntries[i].Hash), false)
|
|
if err := self.c.Git().Stash.Drop(stashEntries[i].Index); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
self.context().CollapseRangeSelectionToTop()
|
|
return nil
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *StashController) postStashRefresh() {
|
|
// 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{
|
|
BatchUIUpdates: true,
|
|
Scope: []types.RefreshableView{types.STASH, types.FILES},
|
|
})
|
|
}
|
|
|
|
func (self *StashController) handleNewBranchOffStashEntry(stashEntry *models.StashEntry) error {
|
|
return self.c.Helpers().Refs.NewBranch(stashEntry.FullRefName(), stashEntry.Description(), "")
|
|
}
|
|
|
|
func (self *StashController) handleRenameStashEntry(stashEntry *models.StashEntry) error {
|
|
message := utils.ResolvePlaceholderString(
|
|
self.c.Tr.RenameStashPrompt,
|
|
map[string]string{
|
|
"stashName": stashEntry.RefName(),
|
|
},
|
|
)
|
|
|
|
self.c.Prompt(types.PromptOpts{
|
|
Title: message,
|
|
InitialContent: stashEntry.Name,
|
|
HandleConfirm: func(response string) error {
|
|
self.c.LogAction(self.c.Tr.Actions.RenameStash)
|
|
err := self.c.Git().Stash.Rename(stashEntry.Index, response)
|
|
if err != nil {
|
|
self.c.RefreshBlockingInput(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH}})
|
|
return err
|
|
}
|
|
self.context().SetSelection(0) // Select the renamed stash
|
|
self.context().FocusLine(true)
|
|
// Renaming re-creates the stash at the top, shifting the other
|
|
// entries' indices; block input so that a quick next action sees
|
|
// the refreshed list rather than the stale indices.
|
|
self.c.RefreshBlockingInput(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH}})
|
|
return nil
|
|
},
|
|
AllowEmptyInput: true,
|
|
})
|
|
|
|
return nil
|
|
}
|