diff --git a/pkg/gui/background.go b/pkg/gui/background.go index 94bf4f678..6215f0d45 100644 --- a/pkg/gui/background.go +++ b/pkg/gui/background.go @@ -114,7 +114,7 @@ func (self *BackgroundRoutineMgr) startBackgroundFetch() { if self.gui.UserConfig().Gui.ShowBottomLine || firstTimeOrRetriggered { return self.gui.helpers.AppStatus.WithWaitingStatusImpl(self.gui.Tr.FetchingStatus, func(gocui.Task) error { return self.backgroundFetch() - }, nil) + }, nil, true) } return self.backgroundFetch() @@ -198,7 +198,10 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru if self.backgroundRefreshesPaused() { return } - self.gui.c.OnWorker(func(gocui.Task) error { + // OnWorkerBackground, not OnWorker: these routines and the refreshes + // they trigger must not count towards lazygit being busy, or they'd + // spuriously block a repo switch every time one happens to be running. + self.gui.c.OnWorkerBackground(func(gocui.Task) error { _ = function(retriggered) done <- struct{}{} return nil diff --git a/pkg/gui/controllers/helpers/app_status_helper.go b/pkg/gui/controllers/helpers/app_status_helper.go index b691db4a4..bffd87866 100644 --- a/pkg/gui/controllers/helpers/app_status_helper.go +++ b/pkg/gui/controllers/helpers/app_status_helper.go @@ -34,7 +34,7 @@ func (self *AppStatusHelper) Toast(message string, kind types.ToastKind) { self.statusMgr().AddToastStatus(message, kind) - self.renderAppStatus() + self.renderAppStatus(false) } // A custom task for WithWaitingStatus calls; it wraps the original one and @@ -61,11 +61,14 @@ func (self appStatusHelperTask) Continue() { // WithWaitingStatus wraps a function and shows a waiting status while the function is still executing func (self *AppStatusHelper) WithWaitingStatus(message string, f func(gocui.Task) error) { self.c.OnWorker(func(task gocui.Task) error { - return self.WithWaitingStatusImpl(message, f, task) + return self.WithWaitingStatusImpl(message, f, task, false) }) } -func (self *AppStatusHelper) WithWaitingStatusImpl(message string, f func(gocui.Task) error, task gocui.Task) error { +// background reports whether this waiting status belongs to a background routine +// (the auto-fetch poller); when it does, the spinner it drives must not count +// towards lazygit being busy, or it'd block repo switches while a fetch runs. +func (self *AppStatusHelper) WithWaitingStatusImpl(message string, f func(gocui.Task) error, task gocui.Task, background bool) error { // A waiting status means lazygit is driving a git operation itself (often // one that internally runs a rebase and continues it). Pause the background // routines for its duration so they don't refresh from an intermediate @@ -73,7 +76,7 @@ func (self *AppStatusHelper) WithWaitingStatusImpl(message string, f func(gocui. self.c.PauseBackgroundRefreshes(true) defer self.c.PauseBackgroundRefreshes(false) - return self.statusMgr().WithWaitingStatus(message, self.renderAppStatus, func(waitingStatusHandle *status.WaitingStatusHandle) error { + return self.statusMgr().WithWaitingStatus(message, func() { self.renderAppStatus(background) }, func(waitingStatusHandle *status.WaitingStatusHandle) error { return f(appStatusHelperTask{task, waitingStatusHandle}) }) } @@ -100,21 +103,33 @@ func (self *AppStatusHelper) GetStatusString() string { return appStatus } -func (self *AppStatusHelper) renderAppStatus() { - self.c.OnWorker(func(_ gocui.Task) error { +func (self *AppStatusHelper) renderAppStatus(background bool) { + // A background waiting status (auto-fetch) must not count towards lazygit + // being busy, so its spinner worker and per-frame UI updates go through the + // background variants. + onWorker := self.c.OnWorker + onUIThread := self.c.OnUIThread + onUIThreadContentOnly := self.c.OnUIThreadContentOnly + if background { + onWorker = self.c.OnWorkerBackground + onUIThread = self.c.OnUIThreadBackground + onUIThreadContentOnly = self.c.OnUIThreadContentOnlyBackground + } + + onWorker(func(_ gocui.Task) error { ticker := time.NewTicker(time.Millisecond * time.Duration(self.c.UserConfig().Gui.Spinner.Rate)) defer ticker.Stop() prevAppStatus := "" for range ticker.C { appStatus, color := self.statusMgr().GetStatusString(self.c.UserConfig()) - update := self.c.OnUIThreadContentOnly + update := onUIThreadContentOnly if utils.StringWidth(appStatus) != utils.StringWidth(prevAppStatus) { // Need a full layout whenever the width of the status string changes. This can't // happen during normal spinning because we validate that all spinner frames have // the same width, so typically this will only be triggered at the beginning and end // of a status, or if the status string changes midway for some reason. - update = self.c.OnUIThread + update = onUIThread } update(func() error { self.c.Views().AppStatus.FgColor = color diff --git a/pkg/gui/controllers/helpers/branches_helper.go b/pkg/gui/controllers/helpers/branches_helper.go index e5c1a07e4..053804760 100644 --- a/pkg/gui/controllers/helpers/branches_helper.go +++ b/pkg/gui/controllers/helpers/branches_helper.go @@ -398,7 +398,7 @@ func (self *BranchesHelper) PostFetchRefresh(fetchErr error, background bool) er if fetchErr != nil { return nil } - err := self.AutoForwardBranches() + err := self.AutoForwardBranches(background) if background && err != nil { // The background poller discards this return value, so surface // the error in the log rather than as a popup for background work. @@ -411,7 +411,7 @@ func (self *BranchesHelper) PostFetchRefresh(fetchErr error, background bool) er return fetchErr } -func (self *BranchesHelper) AutoForwardBranches() error { +func (self *BranchesHelper) AutoForwardBranches(background bool) error { if self.c.UserConfig().Git.AutoForwardBranches == "none" { return nil } @@ -443,7 +443,7 @@ func (self *BranchesHelper) AutoForwardBranches() error { self.c.LogCommand(strings.TrimRight(updateCommands, "\n"), false) err := self.c.Git().Branch.UpdateBranchRefs(updateCommands) - self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES}, Mode: types.SYNC}) + self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES}, Mode: types.SYNC, Background: background}) return err } diff --git a/pkg/gui/controllers/helpers/merge_conflicts_helper.go b/pkg/gui/controllers/helpers/merge_conflicts_helper.go index 6e6a01531..175bc3cc0 100644 --- a/pkg/gui/controllers/helpers/merge_conflicts_helper.go +++ b/pkg/gui/controllers/helpers/merge_conflicts_helper.go @@ -51,11 +51,17 @@ func (self *MergeConflictsHelper) resetMergeState() { self.context().GetState().Reset() } -func (self *MergeConflictsHelper) EscapeMerge() error { +func (self *MergeConflictsHelper) EscapeMerge(background bool) error { self.resetMergeState() // doing this in separate UI thread so that we're not still holding the lock by the time refresh the file - self.c.OnUIThread(func() error { + onUIThread := self.c.OnUIThread + if background { + // Reached from a background files refresh; keep it off the busy count + // (see the *Background dispatch methods) so it doesn't block a repo switch. + onUIThread = self.c.OnUIThreadBackground + } + onUIThread(func() error { // There is a race condition here: refreshing the files scope can trigger the // confirmation context to be pushed if all conflicts are resolved (prompting // to continue the merge/rebase. In that case, we don't want to then push the @@ -120,7 +126,7 @@ func (self *MergeConflictsHelper) Render() { }) } -func (self *MergeConflictsHelper) RefreshMergeState() error { +func (self *MergeConflictsHelper) RefreshMergeState(background bool) error { self.c.Contexts().MergeConflicts.GetMutex().Lock() defer self.c.Contexts().MergeConflicts.GetMutex().Unlock() @@ -134,7 +140,7 @@ func (self *MergeConflictsHelper) RefreshMergeState() error { } if !hasConflicts { - return self.EscapeMerge() + return self.EscapeMerge(background) } return nil diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index fe89bbad6..c0543d095 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -154,7 +154,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) { // everything happens fast and it's better to have everything update // in the one frame if !self.c.InDemo() && options.Mode == types.ASYNC { - self.c.OnWorker(func(t gocui.Task) error { + self.onWorker(options.Background, func(t gocui.Task) error { f() return nil }) @@ -176,14 +176,14 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) { // counts can change. Whenever we change branches we should also change commits // e.g. in the case of switching branches. refresh("commits and commit files", func() { - self.refreshCommitsAndCommitFiles(options.CommitSelection) + self.refreshCommitsAndCommitFiles(options.CommitSelection, options.Background) }) includeWorktreesWithBranches = scopeSet.Includes(types.WORKTREES) if self.c.UserConfig().Git.LocalBranchSortOrder == "recency" { branchesAndRemotesWg.Add(1) refresh("reflog and branches", func() { - self.refreshReflogAndBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex) + self.refreshReflogAndBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex, options.Background) branchesAndRemotesWg.Done() }) } else { @@ -192,24 +192,24 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) { // Not a recency sort, so branches doesn't depend on the reflog // being fresh; it runs concurrently with the reflog refresh // below and reads whatever's in the model, as it always has. - self.refreshBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex, true, self.c.Model().ReflogCommits) + self.refreshBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex, true, self.c.Model().ReflogCommits, options.Background) branchesAndRemotesWg.Done() }) - refresh("reflog", func() { _, _ = self.refreshReflogCommits() }) + refresh("reflog", func() { _, _ = self.refreshReflogCommits(options.Background) }) } } else if scopeSet.Includes(types.REBASE_COMMITS) { // the above block handles rebase commits so we only need to call this one // if we've asked specifically for rebase commits and not those other things - refresh("rebase commits", func() { _ = self.refreshRebaseCommits() }) + refresh("rebase commits", func() { _ = self.refreshRebaseCommits(options.Background) }) } if scopeSet.Includes(types.SUB_COMMITS) { - refresh("sub commits", func() { _ = self.refreshSubCommitsWithLimit() }) + refresh("sub commits", func() { _ = self.refreshSubCommitsWithLimit(options.Background) }) } // reason we're not doing this if the COMMITS type is included is that if the COMMITS type _is_ included we will refresh the commit files context anyway if scopeSet.Includes(types.COMMIT_FILES) && !scopeSet.Includes(types.COMMITS) { - refresh("commit files", func() { _ = self.refreshCommitFilesContext() }) + refresh("commit files", func() { _ = self.refreshCommitFilesContext(options.Background) }) } fileWg := sync.WaitGroup{} @@ -222,17 +222,17 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) { } if scopeSet.Includes(types.STASH) { - refresh("stash", func() { self.refreshStashEntries() }) + refresh("stash", func() { self.refreshStashEntries(options.Background) }) } if scopeSet.Includes(types.TAGS) { - refresh("tags", func() { _ = self.refreshTags() }) + refresh("tags", func() { _ = self.refreshTags(options.Background) }) } if scopeSet.Includes(types.REMOTES) { branchesAndRemotesWg.Add(1) refresh("remotes", func() { - _ = self.refreshRemotes() + _ = self.refreshRemotes(options.Background) branchesAndRemotesWg.Done() }) } @@ -240,12 +240,12 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) { if scopeSet.Includes(types.PULL_REQUESTS) { refresh("pull requests", func() { branchesAndRemotesWg.Wait() - self.refreshGithubPullRequests() + self.refreshGithubPullRequests(options.Background) }) } if scopeSet.Includes(types.WORKTREES) && !includeWorktreesWithBranches { - refresh("worktrees", func() { self.refreshWorktrees() }) + refresh("worktrees", func() { self.refreshWorktrees(options.Background) }) } if scopeSet.Includes(types.STAGING) { @@ -255,7 +255,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) { // scope's model-update bounce — RefreshStagingPanel reads // Model.Files (via Files.GetSelected) and would otherwise // see the pre-refresh model. - self.c.OnUIThread(func() error { + self.onUIThread(options.Background, func() error { self.stagingHelper.RefreshStagingPanel(types.OnFocusOpts{}) return nil }) @@ -267,10 +267,10 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) { } if scopeSet.Includes(types.MERGE_CONFLICTS) { - refresh("merge conflicts", func() { _ = self.mergeConflictsHelper.RefreshMergeState() }) + refresh("merge conflicts", func() { _ = self.mergeConflictsHelper.RefreshMergeState(options.Background) }) } - self.refreshStatus() + self.refreshStatus(options.Background) wg.Wait() @@ -281,7 +281,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) { // returned but their bounces haven't been processed yet, so // invoking Then synchronously would run it on a model that's // still pre-refresh. - self.c.OnUIThread(options.Then) + self.onUIThread(options.Background, options.Then) } } @@ -399,27 +399,27 @@ func getModeName(mode types.RefreshMode) string { // order gives the immediate (non-recency) load a lower branch-load sequence // than the async (recency) load, so the sequence guard in refreshBranches keeps // the recency-sorted result even if the two loads' bounces land out of order. -func (self *RefreshHelper) refreshReflogAndBranches(refreshWorktrees bool, keepBranchSelectionIndex bool) { +func (self *RefreshHelper) refreshReflogAndBranches(refreshWorktrees bool, keepBranchSelectionIndex bool, background bool) { switch self.c.State().GetRepoState().GetStartupStage() { case types.INITIAL: - self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex, false, self.c.Model().ReflogCommits) + self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex, false, self.c.Model().ReflogCommits, background) - self.c.OnWorker(func(_ gocui.Task) error { - reflogCommits, _ := self.refreshReflogCommits() - self.refreshBranches(false, true, true, reflogCommits) + self.onWorker(background, func(_ gocui.Task) error { + reflogCommits, _ := self.refreshReflogCommits(background) + self.refreshBranches(false, true, true, reflogCommits, background) self.c.State().GetRepoState().SetStartupStage(types.COMPLETE) return nil }) case types.COMPLETE: - reflogCommits, _ := self.refreshReflogCommits() - self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex, true, reflogCommits) + reflogCommits, _ := self.refreshReflogCommits(background) + self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex, true, reflogCommits, background) } } -func (self *RefreshHelper) refreshCommitsAndCommitFiles(commitSelection types.CommitSelectionBehavior) { +func (self *RefreshHelper) refreshCommitsAndCommitFiles(commitSelection types.CommitSelectionBehavior, background bool) { generation := self.c.State().GetRepoGeneration() - _ = self.refreshCommitsWithLimit(commitSelection) + _ = self.refreshCommitsWithLimit(commitSelection, background) ctx := self.c.Contexts().CommitFiles.GetParentContext() if ctx != nil && ctx.GetKey() == context.LOCAL_COMMITS_CONTEXT_KEY { // This makes sense when we've e.g. just amended a commit, meaning we get a new commit hash at the same position. @@ -432,13 +432,13 @@ func (self *RefreshHelper) refreshCommitsAndCommitFiles(commitSelection types.Co // The commit selection is restored in refreshCommitsWithLimit's bounce, // so read it on the UI thread after that bounce; then load the commit // files back on a worker (refreshCommitFilesContext does git work). - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { commit := self.c.Contexts().LocalCommits.GetSelected() if commit != nil && commit.RefName() != "" { refRange := self.c.Contexts().LocalCommits.GetSelectedRefRangeForDiffFiles() self.c.Contexts().CommitFiles.ReInit(commit, refRange) - self.c.OnWorker(func(gocui.Task) error { - _ = self.refreshCommitFilesContext() + self.onWorker(background, func(gocui.Task) error { + _ = self.refreshCommitFilesContext(background) return nil }) } @@ -473,7 +473,7 @@ func (self *RefreshHelper) determineCheckedOutRef() models.Ref { return nil } -func (self *RefreshHelper) refreshCommitsWithLimit(commitSelection types.CommitSelectionBehavior) error { +func (self *RefreshHelper) refreshCommitsWithLimit(commitSelection types.CommitSelectionBehavior, background bool) error { generation := self.c.State().GetRepoGeneration() var selectionRange *localCommitSelectionRange @@ -502,7 +502,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit(commitSelection types.CommitS } workingTreeState := self.c.Git().Status.WorkingTreeState() - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Model().BisectInfo = bisectInfo self.c.Model().Commits = commits self.RefreshAuthors(commits) @@ -536,7 +536,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit(commitSelection types.CommitS // Enqueued from within this bounce so it runs after refreshView's // render below (which was enqueued first), matching the previous // ordering where FocusLine ran after the view was re-rendered. - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Contexts().LocalCommits.FocusLine(true) return nil }) @@ -544,7 +544,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit(commitSelection types.CommitS return nil }) - self.refreshView(self.c.Contexts().LocalCommits) + self.refreshView(self.c.Contexts().LocalCommits, background) return nil } @@ -623,7 +623,7 @@ func hasRestorableCommitHash(commits []*models.Commit, idx int) bool { return idx >= 0 && idx < len(commits) && commits[idx].Hash() != "" } -func (self *RefreshHelper) refreshSubCommitsWithLimit() error { +func (self *RefreshHelper) refreshSubCommitsWithLimit(background bool) error { if self.c.Contexts().SubCommits.GetRef() == nil { return nil } @@ -646,13 +646,13 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error { if err != nil { return err } - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Model().SubCommits = commits self.RefreshAuthors(commits) return nil }) - self.refreshView(self.c.Contexts().SubCommits) + self.refreshView(self.c.Contexts().SubCommits, background) return nil } @@ -668,7 +668,7 @@ func (self *RefreshHelper) RefreshAuthors(commits []*models.Commit) { } } -func (self *RefreshHelper) refreshCommitFilesContext() error { +func (self *RefreshHelper) refreshCommitFilesContext(background bool) error { from, to := self.c.Contexts().CommitFiles.GetFromAndToForDiff() from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from) generation := self.c.State().GetRepoGeneration() @@ -677,16 +677,16 @@ func (self *RefreshHelper) refreshCommitFilesContext() error { if err != nil { return err } - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Model().CommitFiles = files self.c.Contexts().CommitFiles.CommitFileTreeViewModel.SetTree() return nil }) - self.refreshView(self.c.Contexts().CommitFiles) + self.refreshView(self.c.Contexts().CommitFiles, background) return nil } -func (self *RefreshHelper) refreshRebaseCommits() error { +func (self *RefreshHelper) refreshRebaseCommits(background bool) error { generation := self.c.State().GetRepoGeneration() updatedCommits, err := self.c.Git().Loaders.CommitLoader.MergeRebasingCommits(self.c.Model().HashPool, self.c.Model().Commits) @@ -695,17 +695,17 @@ func (self *RefreshHelper) refreshRebaseCommits() error { } workingTreeState := self.c.Git().Status.WorkingTreeState() - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Model().Commits = updatedCommits self.c.Model().WorkingTreeStateAtLastCommitRefresh = workingTreeState return nil }) - self.refreshView(self.c.Contexts().LocalCommits) + self.refreshView(self.c.Contexts().LocalCommits, background) return nil } -func (self *RefreshHelper) refreshTags() error { +func (self *RefreshHelper) refreshTags(background bool) error { generation := self.c.State().GetRepoGeneration() tags, err := self.c.Git().Loaders.TagLoader.GetTags() @@ -713,12 +713,12 @@ func (self *RefreshHelper) refreshTags() error { return err } - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Model().Tags = tags return nil }) - self.refreshView(self.c.Contexts().Tags) + self.refreshView(self.c.Contexts().Tags, background) return nil } @@ -728,7 +728,7 @@ func (self *RefreshHelper) refreshStateSubmoduleConfigs() ([]*models.SubmoduleCo // self.refreshStatus is called at the end of this because that's when we can // be sure there is a State.Model.Branches array to pick the current branch from -func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSelectionIndex bool, loadBehindCounts bool, reflogCommits []*models.Commit) { +func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSelectionIndex bool, loadBehindCounts bool, reflogCommits []*models.Commit, background bool) { loadSeq := self.branchLoadSeq.Add(1) generation := self.c.State().GetRepoGeneration() @@ -739,14 +739,14 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele self.c.Model().Branches, loadBehindCounts, func(f func() error) { - self.c.OnWorker(func(_ gocui.Task) error { + self.onWorker(background, func(_ gocui.Task) error { return f() }) }, func() { - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Contexts().Branches.HandleRender() - self.refreshStatus() + self.refreshStatus(background) return nil }) }) @@ -761,7 +761,7 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele worktrees = self.loadWorktrees() } - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { // Drop this write if a branch load that started later has already applied // its result. At the INITIAL startup stage an immediate load (not // recency-sorted) and an async recency-sorted load run concurrently; this @@ -779,7 +779,7 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele if refreshWorktrees { self.c.Model().Worktrees = worktrees - self.refreshView(self.c.Contexts().Worktrees) + self.refreshView(self.c.Contexts().Worktrees, background) } if !keepBranchSelectionIndex && prevSelectedBranch != nil { @@ -798,9 +798,9 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele return nil }) - self.refreshView(self.c.Contexts().Branches) + self.refreshView(self.c.Contexts().Branches, background) - self.refreshStatus() + self.refreshStatus(background) } func (self *RefreshHelper) refreshFilesAndSubmodules(background bool) error { @@ -813,8 +813,8 @@ func (self *RefreshHelper) refreshFilesAndSubmodules(background bool) error { return err } - self.refreshView(self.c.Contexts().Submodules) - self.refreshView(self.c.Contexts().Files) + self.refreshView(self.c.Contexts().Submodules, background) + self.refreshView(self.c.Contexts().Files, background) return nil } @@ -826,8 +826,8 @@ func (self *RefreshHelper) refreshFilesAndSubmodules(background bool) error { // bumps the generation, so a write captured under the old generation must not // clobber the new repo's state. Callers capture the generation with // State().GetRepoGeneration() before doing their git work and pass it in. -func (self *RefreshHelper) onUIThreadUnlessRepoChanged(generation int, f func() error) { - self.c.OnUIThread(func() error { +func (self *RefreshHelper) onUIThreadUnlessRepoChanged(generation int, background bool, f func() error) { + self.onUIThread(background, func() error { if self.c.State().GetRepoGeneration() != generation { return nil } @@ -835,6 +835,27 @@ func (self *RefreshHelper) onUIThreadUnlessRepoChanged(generation int, f func() }) } +// onWorker and onUIThread pick the foreground or background variant of the +// corresponding dispatch method depending on whether we're servicing a +// background refresh. Background refreshes (auto-fetch and friends) must not +// count towards lazygit being busy, or they'd spuriously block a repo switch; +// see the *Background methods on gocui.Gui. +func (self *RefreshHelper) onWorker(background bool, f func(gocui.Task) error) { + if background { + self.c.OnWorkerBackground(f) + } else { + self.c.OnWorker(f) + } +} + +func (self *RefreshHelper) onUIThread(background bool, f func() error) { + if background { + self.c.OnUIThreadBackground(f) + } else { + self.c.OnUIThread(f) + } +} + func (self *RefreshHelper) refreshStateFiles(background bool, submoduleConfigs []*models.SubmoduleConfig) error { fileTreeViewModel := self.c.Contexts().Files.FileTreeViewModel generation := self.c.State().GetRepoGeneration() @@ -898,7 +919,7 @@ func (self *RefreshHelper) refreshStateFiles(background bool, submoduleConfigs [ // (e.g. in the user's editor). Offer to continue it. We only do this // for operations we started ourselves; prompting for one that was // started outside lazygit (e.g. by a coding agent) would be confusing. - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { return self.mergeAndRebaseHelper.PromptToContinueRebase() }) } @@ -913,7 +934,7 @@ func (self *RefreshHelper) refreshStateFiles(background bool, submoduleConfigs [ }) } - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { // only taking over the filter if it hasn't already been set by the user. if conflictFileCount > 0 && prevConflictFileCount == 0 { if fileTreeViewModel.GetStatusFilter() == filetree.DisplayAll { @@ -944,7 +965,7 @@ func (self *RefreshHelper) refreshStateFiles(background bool, submoduleConfigs [ // refreshReflogCommits returns the (non-filtered) ReflogCommits it loaded, so // that a subsequent branches refresh can use them for recency sorting without // having to read them back out of the model. -func (self *RefreshHelper) refreshReflogCommits() ([]*models.Commit, error) { +func (self *RefreshHelper) refreshReflogCommits(background bool) ([]*models.Commit, error) { generation := self.c.State().GetRepoGeneration() // pulling state into its own variable in case it gets swapped out for another state // and we get an out of bounds exception @@ -984,17 +1005,17 @@ func (self *RefreshHelper) refreshReflogCommits() ([]*models.Commit, error) { } } - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { model.ReflogCommits = reflogCommits model.FilteredReflogCommits = filteredReflogCommits return nil }) - self.refreshView(self.c.Contexts().ReflogCommits) + self.refreshView(self.c.Contexts().ReflogCommits, background) return reflogCommits, nil } -func (self *RefreshHelper) refreshRemotes() error { +func (self *RefreshHelper) refreshRemotes(background bool) error { generation := self.c.State().GetRepoGeneration() prevSelectedRemote := self.c.Contexts().Remotes.GetSelected() @@ -1003,14 +1024,14 @@ func (self *RefreshHelper) refreshRemotes() error { return err } - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Model().Remotes = remotes hadPrs := len(self.c.Model().PullRequestsMap) != 0 self.rebuildPullRequestsMap() if !hadPrs && len(self.c.Model().PullRequestsMap) != 0 { // if we didn't have PRs in the map before but now we do, we need to redraw the branches view - self.refreshView(self.c.Contexts().Branches) + self.refreshView(self.c.Contexts().Branches, background) } // we need to ensure our selected remote branches aren't now outdated @@ -1026,8 +1047,8 @@ func (self *RefreshHelper) refreshRemotes() error { return nil }) - self.refreshView(self.c.Contexts().Remotes) - self.refreshView(self.c.Contexts().RemoteBranches) + self.refreshView(self.c.Contexts().Remotes, background) + self.refreshView(self.c.Contexts().RemoteBranches, background) return nil } @@ -1040,44 +1061,44 @@ func (self *RefreshHelper) loadWorktrees() []*models.Worktree { return worktrees } -func (self *RefreshHelper) refreshWorktrees() { +func (self *RefreshHelper) refreshWorktrees(background bool) { generation := self.c.State().GetRepoGeneration() worktrees := self.loadWorktrees() - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Model().Worktrees = worktrees return nil }) // need to refresh branches because the branches view shows worktrees against // branches - self.refreshView(self.c.Contexts().Branches) - self.refreshView(self.c.Contexts().Worktrees) + self.refreshView(self.c.Contexts().Branches, background) + self.refreshView(self.c.Contexts().Worktrees, background) } -func (self *RefreshHelper) refreshStashEntries() { +func (self *RefreshHelper) refreshStashEntries(background bool) { generation := self.c.State().GetRepoGeneration() stashEntries := self.c.Git().Loaders.StashLoader. GetStashEntries(self.c.Modes().Filtering.GetPath()) - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Model().StashEntries = stashEntries return nil }) - self.refreshView(self.c.Contexts().Stash) + self.refreshView(self.c.Contexts().Stash, background) } // never call this on its own, it should only be called from within refreshCommits() -func (self *RefreshHelper) refreshStatus() { +func (self *RefreshHelper) refreshStatus(background bool) { generation := self.c.State().GetRepoGeneration() workingTreeState := self.c.Git().Status.WorkingTreeState() repoName := self.c.Git().RepoPaths.RepoName() - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { // Read the checked-out branch and the linked worktree name here on the UI // thread: both derive from models (Branches, Worktrees) that their // refreshes now write via bounces, so reading them on the worker would @@ -1114,10 +1135,10 @@ func (self *RefreshHelper) refForLog() (string, *git_commands.BisectInfo) { return bisectInfo.GetStartHash(), bisectInfo } -func (self *RefreshHelper) refreshView(context types.Context) { +func (self *RefreshHelper) refreshView(context types.Context, background bool) { // refreshView is called from the worker goroutine that drives async // refreshes, so bounce to the UI thread before mutating view content. - self.c.OnUIThread(func() error { + self.onUIThread(background, func() error { // Re-applying the filter must be done before re-rendering the view, so that // the filtered list model is up to date for rendering. self.searchHelper.ReApplyFilter(context) @@ -1140,11 +1161,11 @@ func (self *RefreshHelper) refreshView(context types.Context) { }) } -func (self *RefreshHelper) refreshGithubPullRequests() { +func (self *RefreshHelper) refreshGithubPullRequests(background bool) { generation := self.c.State().GetRepoGeneration() clearPullRequests := func() { - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Model().PullRequests = nil self.c.Model().PullRequestsMap = nil return nil @@ -1167,7 +1188,7 @@ func (self *RefreshHelper) refreshGithubPullRequests() { return } - self.setGithubPullRequests(baseInfo) + self.setGithubPullRequests(baseInfo, background) } type githubRemoteInfo struct { @@ -1248,7 +1269,7 @@ func (self *RefreshHelper) promptForBaseGithubRepo(githubRemotes []githubRemoteI self.c.Log.Error(err) } - self.setGithubPullRequests(&info) + self.setGithubPullRequests(&info, false) return nil }) }, @@ -1276,7 +1297,7 @@ func (self *RefreshHelper) rebuildPullRequestsMap() { ) } -func (self *RefreshHelper) setGithubPullRequests(baseInfo *githubRemoteInfo) { +func (self *RefreshHelper) setGithubPullRequests(baseInfo *githubRemoteInfo, background bool) { generation := self.c.State().GetRepoGeneration() if len(self.c.Model().Branches) == 0 { @@ -1298,7 +1319,7 @@ func (self *RefreshHelper) setGithubPullRequests(baseInfo *githubRemoteInfo) { self.savePullRequestsToCache(prs) - self.onUIThreadUnlessRepoChanged(generation, func() error { + self.onUIThreadUnlessRepoChanged(generation, background, func() error { self.c.Model().PullRequests = prs // Rebuilding here rather than on the worker means the map is built from // the branches and remotes as they are on the UI thread, after their