mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
Refuse a repo switch while a foreground operation is in flight
Switching repos reassigns gui.git and the process cwd; doing it while a foreground git operation (rebase/commit/push/…) is mid-flight would run that operation's remaining commands against the wrong repo. The same applies while the refresh an operation triggers is still settling: its model writes are generation-guarded, but the client-side Then/OnUIThread callbacks that run after it aren't, and shouldn't run against a repo that changed underneath them. Refuse the switch (with a toast) whenever gocui reports a busy foreground task. DispatchSwitchTo carries the guard for the simple callers. The callers that do work before the switch check up front instead, so a refused switch doesn't leave that work half-done: worktree creation checks before creating (its own waiting-status spinner would otherwise make the query busy and refuse its own switch); submodule-enter and the recent-repos menu check before mutating the repo-path stack (pushing / clearing it); and escape-to-parent (SwitchToParentRepo) checks before popping it, so a refusal doesn't consume the entry and strand the user with nowhere to escape back to. All then call the unguarded switchTo, which is safe because their own operation is complete by then.
This commit is contained in:
parent
8655d3f5a5
commit
56932abe06
|
|
@ -43,13 +43,20 @@ func NewRecentReposHelper(
|
|||
}
|
||||
|
||||
func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
// Check before pushing onto the repo-path stack, so a refused switch
|
||||
// doesn't leave a stale entry there (which escape would later switch back
|
||||
// to, needlessly reloading the current repo).
|
||||
if self.switchRefusedBecauseBusy() {
|
||||
return nil
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self.c.State().GetRepoPathStack().Push(wd)
|
||||
|
||||
return self.DispatchSwitchToRepo(submodule.FullPath(), context.NO_CONTEXT)
|
||||
return self.switchTo(submodule.FullPath(), self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT)
|
||||
}
|
||||
|
||||
func (self *ReposHelper) getCurrentBranch(path string) string {
|
||||
|
|
@ -129,10 +136,16 @@ func (self *ReposHelper) CreateRecentReposMenu() error {
|
|||
style.FgMagenta.Sprint(path),
|
||||
},
|
||||
OnPress: func() error {
|
||||
// Check before clearing the stack, so a refused switch doesn't
|
||||
// forget the submodule breadcrumb (which would leave escape
|
||||
// unable to return to the parent repo).
|
||||
if self.switchRefusedBecauseBusy() {
|
||||
return nil
|
||||
}
|
||||
// if we were in a submodule, we want to forget about that stack of repos
|
||||
// so that hitting escape in the new repo does nothing
|
||||
self.c.State().GetRepoPathStack().Clear()
|
||||
return self.DispatchSwitchToRepo(path, context.NO_CONTEXT)
|
||||
return self.switchTo(path, self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
@ -140,17 +153,49 @@ func (self *ReposHelper) CreateRecentReposMenu() error {
|
|||
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.RecentRepos, Items: menuItems})
|
||||
}
|
||||
|
||||
func (self *ReposHelper) DispatchSwitchToRepo(path string, contextKey types.ContextKey) error {
|
||||
return self.DispatchSwitchTo(path, self.c.Tr.ErrRepositoryMovedOrDeleted, contextKey)
|
||||
// SwitchToParentRepo switches back to the repo the current submodule was
|
||||
// entered from (the top of the repo-path stack). Like the other callers that do
|
||||
// work before switching, it checks for an in-flight operation *before* popping
|
||||
// the stack, so a refused switch leaves the stack intact — otherwise the entry
|
||||
// would be consumed and escape would no longer return to the parent once the
|
||||
// operation finished. The caller must only call this when the stack is
|
||||
// non-empty.
|
||||
func (self *ReposHelper) SwitchToParentRepo() error {
|
||||
if self.switchRefusedBecauseBusy() {
|
||||
return nil
|
||||
}
|
||||
return self.switchTo(self.c.State().GetRepoPathStack().Pop(), self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT)
|
||||
}
|
||||
|
||||
// DispatchSwitchTo switches lazygit to the repository (or worktree) at the
|
||||
// given path. It runs synchronously on the UI thread: the switch swaps
|
||||
// gui.State (in resetState) and reassigns gui.git and the process cwd, all of
|
||||
// which the UI thread also reads, so doing it here rather than on a worker
|
||||
// avoids racing those reads. The heavy data loading is still dispatched
|
||||
// asynchronously by the refresh that onNewRepo kicks off.
|
||||
func (self *ReposHelper) DispatchSwitchTo(path string, errMsg string, contextKey types.ContextKey) error {
|
||||
if self.switchRefusedBecauseBusy() {
|
||||
return nil
|
||||
}
|
||||
return self.switchTo(path, errMsg, contextKey)
|
||||
}
|
||||
|
||||
// switchRefusedBecauseBusy reports (and shows a toast) whether a repo switch
|
||||
// must be refused because a foreground git operation is in flight. Switching
|
||||
// reassigns gui.git and the process cwd, so switching mid-operation would run
|
||||
// the operation's remaining git commands against the wrong repo. Callers that
|
||||
// do work before the switch (creating a worktree, recording the repo-path
|
||||
// stack) check this up front, so they don't do that work only to have the
|
||||
// switch refused; the switch itself (switchTo) is then unguarded.
|
||||
func (self *ReposHelper) switchRefusedBecauseBusy() bool {
|
||||
if self.c.GocuiGui().Busy() {
|
||||
self.c.ErrorToast(self.c.Tr.CantSwitchWhileOperationInProgress)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// switchTo switches lazygit to the repository (or worktree) at the given path.
|
||||
// It runs synchronously on the UI thread: the switch swaps gui.State (in
|
||||
// resetState) and reassigns gui.git and the process cwd, all of which the UI
|
||||
// thread also reads, so doing it here rather than on a worker avoids racing
|
||||
// those reads. The heavy data loading is still dispatched asynchronously by the
|
||||
// refresh that onNewRepo kicks off.
|
||||
func (self *ReposHelper) switchTo(path string, errMsg string, contextKey types.ContextKey) error {
|
||||
env.UnsetGitLocationEnvVars()
|
||||
originalPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -426,6 +426,13 @@ func (self *WorktreeHelper) promptForWorktreeLocation(dirName string, prompt str
|
|||
}
|
||||
|
||||
func (self *WorktreeHelper) createWorktree(opts git_commands.NewWorktreeOpts, contextKey types.ContextKey) error {
|
||||
// Check now, before we create the worktree, rather than when we come to
|
||||
// switch to it afterwards: by then this operation's own waiting-status
|
||||
// spinner would make Busy() true and refuse our own switch.
|
||||
if self.reposHelper.switchRefusedBecauseBusy() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.c.WithWaitingStatus(self.c.Tr.AddingWorktree, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.AddWorktree)
|
||||
if err := self.c.Git().Worktree.New(opts); err != nil {
|
||||
|
|
@ -434,9 +441,11 @@ func (self *WorktreeHelper) createWorktree(opts git_commands.NewWorktreeOpts, co
|
|||
|
||||
// The switch swaps gui.State and must run on the UI thread, but
|
||||
// we're on a worker here (creating the worktree is git work), so
|
||||
// dispatch it rather than calling it directly.
|
||||
// dispatch it. It's unguarded (switchTo, not DispatchSwitchTo)
|
||||
// because we checked above and creating the worktree is now
|
||||
// complete, so switching to it is safe.
|
||||
self.c.OnUIThread(func() error {
|
||||
return self.reposHelper.DispatchSwitchTo(opts.Path, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
|
||||
return self.reposHelper.switchTo(opts.Path, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package controllers
|
|||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
|
|
@ -81,9 +80,8 @@ func (self *QuitActions) Escape() error {
|
|||
}
|
||||
}
|
||||
|
||||
repoPathStack := self.c.State().GetRepoPathStack()
|
||||
if !repoPathStack.IsEmpty() {
|
||||
return self.c.Helpers().Repos.DispatchSwitchToRepo(repoPathStack.Pop(), context.NO_CONTEXT)
|
||||
if !self.c.State().GetRepoPathStack().IsEmpty() {
|
||||
return self.c.Helpers().Repos.SwitchToParentRepo()
|
||||
}
|
||||
|
||||
if self.c.UserConfig().QuitOnTopLevelReturn {
|
||||
|
|
|
|||
|
|
@ -769,6 +769,7 @@ type TranslationSet struct {
|
|||
ErrStageDirWithInlineMergeConflicts string
|
||||
ErrRepositoryMovedOrDeleted string
|
||||
ErrWorktreeMovedOrRemoved string
|
||||
CantSwitchWhileOperationInProgress string
|
||||
CommandLog string
|
||||
ToggleShowCommandLog string
|
||||
FocusCommandLog string
|
||||
|
|
@ -1921,6 +1922,7 @@ func EnglishTranslationSet() *TranslationSet {
|
|||
ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯",
|
||||
CommandLog: "Command log",
|
||||
ErrWorktreeMovedOrRemoved: "Cannot find worktree. It might have been moved or removed ¯\\_(ツ)_/¯",
|
||||
CantSwitchWhileOperationInProgress: "Can't switch repositories while an operation is in progress",
|
||||
ToggleShowCommandLog: "Toggle show/hide command log",
|
||||
FocusCommandLog: "Focus command log",
|
||||
CommandLogHeader: "You can hide/focus this panel by pressing '%s'\n",
|
||||
|
|
|
|||
Loading…
Reference in a new issue