diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 7ae7b4119..27f465f69 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -91,10 +91,19 @@ func (self *RefreshHelper) RefreshFromWorker(options types.RefreshOptions) { } type refreshEnv struct { - // whether this is a background refresh (which selects the dispatch variant that - // doesn't count towards lazygit being busy) + // Whether everything this refresh dispatches uses the background task + // variants, which don't count towards lazygit being busy — so the refresh + // doesn't block switching repos. Set for refreshes initiated by a + // background routine, and for foreground ones that opted in via + // RefreshOptions.DontBlockRepoSwitch. background bool + // Whether the refresh was initiated by an unattended background routine + // (RefreshOptions.Background) rather than by user activity. The files + // refresh uses this to decide whether git may take optional locks and + // persist its refreshed stat cache. + backgroundRoutine bool + // the repo generation captured when the refresh started generation int @@ -175,6 +184,13 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr panic("Refresh called from a worker, or RefreshFromWorker called from the UI thread") } + if options.Then != nil && options.DontBlockRepoSwitch { + // Then is not generation-guarded, so if a switch crossed the refresh it + // would run against the newly switched-to repo. A refresh carrying a + // Then must keep blocking switches. + panic("a refresh with a Then callback must not set DontBlockRepoSwitch") + } + // Capture the refresh's baseline once, here at the start: the repo // generation that every scope's bounce is guarded against, and the git // command instance the scopes run their commands through. The two are @@ -186,9 +202,10 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr // against the repo it started in, and the generation guard drops its // writes. env := refreshEnv{ - background: options.Background, + background: options.Background || options.DontBlockRepoSwitch, + backgroundRoutine: options.Background, } - self.captureOnUIThread(calledFromWorker, options.Background, func() { + self.captureOnUIThread(calledFromWorker, env.background, func() { env.generation = self.c.State().GetRepoGeneration() env.git = self.c.Git() }) @@ -1276,7 +1293,7 @@ func (self *RefreshHelper) refreshStateFiles(captured capturedFilesState, env re files := env.git.Loaders.FileLoader. GetStatusFiles(git_commands.GetStatusFileOptions{ ForceShowUntracked: captured.forceShowUntracked, - Background: env.background, + Background: env.backgroundRoutine, }) conflictFileCount := 0 diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 533c01fbd..8776040e7 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -390,7 +390,7 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context } gui.c.Log.Info("Receiving focus - refreshing") - gui.helpers.Refresh.Refresh(types.RefreshOptions{}) + gui.helpers.Refresh.Refresh(types.RefreshOptions{DontBlockRepoSwitch: true}) return reloadErr } @@ -1031,7 +1031,7 @@ func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess *oscommands.CmdOb return err } - gui.c.Refresh(types.RefreshOptions{}) + gui.c.Refresh(types.RefreshOptions{DontBlockRepoSwitch: true}) return nil } @@ -1108,7 +1108,7 @@ func (gui *Gui) loadNewRepo() error { return err } - gui.c.Refresh(types.RefreshOptions{}) + gui.c.Refresh(types.RefreshOptions{DontBlockRepoSwitch: true}) if err := gui.os.UpdateWindowTitle(); err != nil { return err diff --git a/pkg/gui/types/refresh.go b/pkg/gui/types/refresh.go index 937c3a30e..c733e589e 100644 --- a/pkg/gui/types/refresh.go +++ b/pkg/gui/types/refresh.go @@ -94,4 +94,19 @@ type RefreshOptions struct { // fast. Background refreshes leave the suppression in place: not persisting // the stat-cache is the right trade-off for unattended work. Background bool + + // When true, this foreground refresh does not block switching repos while + // it is in flight. A refresh is switch-safe by construction — its git + // commands run against the repo it was started for, and the generation + // guard drops its model/view updates if the repo changed — but a refresh + // triggered by a user operation still blocks switching (its tasks count + // towards Busy()), because the operation's follow-up work isn't covered + // by those guards. A refresh that merely reloads state (on focus, after a + // repo switch, after returning from a subprocess) has no such follow-up, + // so it opts in here and a repo switch during it is allowed rather than + // refused with a toast. + // + // Must not be combined with Then: Then is not generation-guarded, so it + // would run against the newly switched-to repo. + DontBlockRepoSwitch bool }