mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Capture the files, reflog, branches and stash refresh inputs
The remaining refresh scopes each still read model, context, and mode state directly on their worker, racing the UI thread — the same class of race the commits refresh had: - files reads Model.Files (to detect resolved conflicts and drive the auto-stage) and the Files context's ForceShowUntracked; - reflog reads the existing reflog slices (for the incremental fetch), Model.HashPool and the filtering path/author; - branches reads Model.MainBranches and the previous branches (for the BehindBaseBranch carry-over); - stash reads the filtering path. Gather each scope's inputs into an immutable snapshot on the UI thread (via captureOnUIThread) before dispatching the git work, and have the refresh compute from the snapshot — for branches, threaded through both the immediate and the recency-sorted startup loads, which share one snapshot (the BehindBaseBranch carry-over is identical either way). Status, tags and worktrees read nothing UI-owned, so they're left alone. For the snapshots to actually run on the UI thread, the worker callers that reach these scopes must announce themselves: convert the submodule operations, the submodule stash-and-reset, and the background files poller to RefreshFromWorker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3e5c99e1e4
commit
fd6b20847a
|
|
@ -133,7 +133,7 @@ func (self *BackgroundRoutineMgr) startBackgroundFilesRefresh() {
|
|||
|
||||
userConfig := self.gui.UserConfig()
|
||||
self.goEvery(userConfig.Refresher.RefreshIntervalDuration(), self.gui.stopChan, func(_ bool) error {
|
||||
self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Background: true})
|
||||
self.gui.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Background: true})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1808,7 +1808,7 @@ func (self *FilesController) ResetSubmodule(submodule *models.SubmoduleConfig) e
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.SUBMODULES}})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.SUBMODULES}})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,13 +227,17 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
|
|||
// whenever we change commits, we should update branches because the upstream/downstream
|
||||
// counts can change. Whenever we change branches we should also change commits
|
||||
// e.g. in the case of switching branches.
|
||||
// Capture the commits refresh's model/context/mode inputs on the UI
|
||||
// thread, before the git work is dispatched to a worker, so the
|
||||
// worker computes from an immutable snapshot instead of reading
|
||||
// state the UI thread concurrently mutates.
|
||||
// Capture the commits, reflog and branches refresh inputs (model,
|
||||
// contexts, modes) on the UI thread, before the git work is dispatched
|
||||
// to a worker, so the workers compute from an immutable snapshot
|
||||
// instead of reading state the UI thread concurrently mutates.
|
||||
var capturedCommits capturedCommitState
|
||||
var capturedReflog capturedReflogState
|
||||
var capturedBranches capturedBranchState
|
||||
self.captureOnUIThread(fRunsOnUIThread, options.Background, func() {
|
||||
capturedCommits = self.captureCommitsState(options.CommitSelection)
|
||||
capturedReflog = self.captureReflogState()
|
||||
capturedBranches = self.captureBranchState()
|
||||
})
|
||||
refresh("commits and commit files", func() {
|
||||
self.refreshCommitsAndCommitFiles(capturedCommits, options.CommitSelection, options.Background)
|
||||
|
|
@ -243,7 +247,7 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
|
|||
if self.c.UserConfig().Git.LocalBranchSortOrder == "recency" {
|
||||
branchesAndRemotesWg.Add(1)
|
||||
refresh("reflog and branches", func() {
|
||||
loadedBranches = self.refreshReflogAndBranches(includeWorktreesWithBranches, options.BranchSelection, options.SelectTopReflogCommit, options.Background)
|
||||
loadedBranches = self.refreshReflogAndBranches(capturedReflog, capturedBranches, includeWorktreesWithBranches, options.BranchSelection, options.SelectTopReflogCommit, options.Background)
|
||||
branchesAndRemotesWg.Done()
|
||||
})
|
||||
} else {
|
||||
|
|
@ -251,11 +255,13 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
|
|||
refresh("branches", func() {
|
||||
// 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.
|
||||
loadedBranches = self.refreshBranches(includeWorktreesWithBranches, options.BranchSelection, true, self.c.Model().ReflogCommits, options.Background)
|
||||
// below and uses the reflog we captured up front, as it always has.
|
||||
loadedBranches = self.refreshBranches(capturedBranches, includeWorktreesWithBranches, options.BranchSelection, true, capturedReflog.reflogCommits, options.Background)
|
||||
branchesAndRemotesWg.Done()
|
||||
})
|
||||
refresh("reflog", func() { _, _ = self.refreshReflogCommits(options.Background, options.SelectTopReflogCommit) })
|
||||
refresh("reflog", func() {
|
||||
_, _ = self.refreshReflogCommits(capturedReflog, options.Background, options.SelectTopReflogCommit)
|
||||
})
|
||||
}
|
||||
} else if scopeSet.Includes(types.REBASE_COMMITS) {
|
||||
// the above block handles rebase commits so we only need to call this one
|
||||
|
|
@ -287,15 +293,23 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
|
|||
|
||||
fileWg := sync.WaitGroup{}
|
||||
if scopeSet.Includes(types.FILES) {
|
||||
var capturedFiles capturedFilesState
|
||||
self.captureOnUIThread(fRunsOnUIThread, options.Background, func() {
|
||||
capturedFiles = self.captureFilesState()
|
||||
})
|
||||
fileWg.Add(1)
|
||||
refresh("files", func() {
|
||||
_ = self.refreshFilesAndSubmodules(options.Background)
|
||||
_ = self.refreshFilesAndSubmodules(capturedFiles, options.Background)
|
||||
fileWg.Done()
|
||||
})
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.STASH) {
|
||||
refresh("stash", func() { self.refreshStashEntries(options.Background) })
|
||||
var stashFilterPath string
|
||||
self.captureOnUIThread(fRunsOnUIThread, options.Background, func() {
|
||||
stashFilterPath = self.c.Modes().Filtering.GetPath()
|
||||
})
|
||||
refresh("stash", func() { self.refreshStashEntries(stashFilterPath, options.Background) })
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.TAGS) {
|
||||
|
|
@ -483,17 +497,60 @@ 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, branchSelection types.BranchSelectionBehavior, selectTopReflogCommit bool, background bool) []*models.Branch {
|
||||
// capturedReflogState holds the reflog refresh's model/mode inputs, gathered on
|
||||
// the UI thread before the git work runs. The existing reflog slices feed the
|
||||
// incremental fetch (we only load entries newer than the ones we already have).
|
||||
type capturedReflogState struct {
|
||||
reflogCommits []*models.Commit
|
||||
filteredReflogCommits []*models.Commit
|
||||
hashPool *utils.StringPool
|
||||
filteringActive bool
|
||||
filterPath string
|
||||
filterAuthor string
|
||||
}
|
||||
|
||||
// captureReflogState reads the reflog refresh's inputs into an immutable
|
||||
// snapshot. It must run on the UI thread.
|
||||
func (self *RefreshHelper) captureReflogState() capturedReflogState {
|
||||
return capturedReflogState{
|
||||
reflogCommits: self.c.Model().ReflogCommits,
|
||||
filteredReflogCommits: self.c.Model().FilteredReflogCommits,
|
||||
hashPool: self.c.Model().HashPool,
|
||||
filteringActive: self.c.Modes().Filtering.Active(),
|
||||
filterPath: self.c.Modes().Filtering.GetPath(),
|
||||
filterAuthor: self.c.Modes().Filtering.GetAuthor(),
|
||||
}
|
||||
}
|
||||
|
||||
// capturedBranchState holds the branches refresh's model inputs, gathered on the
|
||||
// UI thread before the git work runs. oldBranches is used only to carry over the
|
||||
// previous BehindBaseBranch values (to reduce flicker) — an atomic each, so a
|
||||
// pre-refresh snapshot serves both the immediate and recency loads identically.
|
||||
type capturedBranchState struct {
|
||||
mainBranches *git_commands.MainBranches
|
||||
oldBranches []*models.Branch
|
||||
}
|
||||
|
||||
// captureBranchState reads the branches refresh's model inputs into an immutable
|
||||
// snapshot. It must run on the UI thread.
|
||||
func (self *RefreshHelper) captureBranchState() capturedBranchState {
|
||||
return capturedBranchState{
|
||||
mainBranches: self.c.Model().MainBranches,
|
||||
oldBranches: self.c.Model().Branches,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshReflogAndBranches(capturedReflog capturedReflogState, capturedBranches capturedBranchState, refreshWorktrees bool, branchSelection types.BranchSelectionBehavior, selectTopReflogCommit bool, background bool) []*models.Branch {
|
||||
switch self.c.State().GetRepoState().GetStartupStage() {
|
||||
case types.INITIAL:
|
||||
// Return the immediate (non-recency) load's branches; the recency-sorted
|
||||
// reload below runs on its own worker after we return. Both hold the same
|
||||
// set of branches, which is all the caller (the PR fetch) needs.
|
||||
branches := self.refreshBranches(refreshWorktrees, branchSelection, false, self.c.Model().ReflogCommits, background)
|
||||
branches := self.refreshBranches(capturedBranches, refreshWorktrees, branchSelection, false, capturedReflog.reflogCommits, background)
|
||||
|
||||
self.onWorker(background, func(_ gocui.Task) error {
|
||||
reflogCommits, _ := self.refreshReflogCommits(background, false)
|
||||
self.refreshBranches(false, types.SelectCheckedOutBranch, true, reflogCommits, background)
|
||||
reflogCommits, _ := self.refreshReflogCommits(capturedReflog, background, false)
|
||||
self.refreshBranches(capturedBranches, false, types.SelectCheckedOutBranch, true, reflogCommits, background)
|
||||
self.c.State().GetRepoState().SetStartupStage(types.COMPLETE)
|
||||
return nil
|
||||
})
|
||||
|
|
@ -501,8 +558,8 @@ func (self *RefreshHelper) refreshReflogAndBranches(refreshWorktrees bool, branc
|
|||
return branches
|
||||
|
||||
case types.COMPLETE:
|
||||
reflogCommits, _ := self.refreshReflogCommits(background, selectTopReflogCommit)
|
||||
return self.refreshBranches(refreshWorktrees, branchSelection, true, reflogCommits, background)
|
||||
reflogCommits, _ := self.refreshReflogCommits(capturedReflog, background, selectTopReflogCommit)
|
||||
return self.refreshBranches(capturedBranches, refreshWorktrees, branchSelection, true, reflogCommits, background)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -901,15 +958,15 @@ 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, branchSelection types.BranchSelectionBehavior, loadBehindCounts bool, reflogCommits []*models.Commit, background bool) []*models.Branch {
|
||||
func (self *RefreshHelper) refreshBranches(captured capturedBranchState, refreshWorktrees bool, branchSelection types.BranchSelectionBehavior, loadBehindCounts bool, reflogCommits []*models.Commit, background bool) []*models.Branch {
|
||||
loadSeq := self.branchLoadSeq.Add(1)
|
||||
|
||||
generation := self.c.State().GetRepoGeneration()
|
||||
|
||||
branches, err := self.c.Git().Loaders.BranchLoader.Load(
|
||||
reflogCommits,
|
||||
self.c.Model().MainBranches,
|
||||
self.c.Model().Branches,
|
||||
captured.mainBranches,
|
||||
captured.oldBranches,
|
||||
loadBehindCounts,
|
||||
func(f func() error) {
|
||||
self.onWorker(background, func(_ gocui.Task) error {
|
||||
|
|
@ -994,13 +1051,13 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, branchSelectio
|
|||
return branches
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshFilesAndSubmodules(background bool) error {
|
||||
func (self *RefreshHelper) refreshFilesAndSubmodules(captured capturedFilesState, background bool) error {
|
||||
configs, err := self.refreshStateSubmoduleConfigs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.refreshStateFiles(background, configs); err != nil {
|
||||
if err := self.refreshStateFiles(captured, background, configs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -1077,7 +1134,25 @@ func (self *RefreshHelper) captureOnUIThread(fRunsOnUIThread bool, background bo
|
|||
}
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshStateFiles(background bool, submoduleConfigs []*models.SubmoduleConfig) error {
|
||||
// capturedFilesState holds the files refresh's context/model inputs, gathered
|
||||
// on the UI thread before the git work runs: the previous files list (to detect
|
||||
// resolved conflicts and drive the auto-stage), and whether untracked files are
|
||||
// force-shown.
|
||||
type capturedFilesState struct {
|
||||
prevFiles []*models.File
|
||||
forceShowUntracked bool
|
||||
}
|
||||
|
||||
// captureFilesState reads the files refresh's inputs into an immutable snapshot.
|
||||
// It must run on the UI thread.
|
||||
func (self *RefreshHelper) captureFilesState() capturedFilesState {
|
||||
return capturedFilesState{
|
||||
prevFiles: self.c.Model().Files,
|
||||
forceShowUntracked: self.c.Contexts().Files.ForceShowUntracked(),
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshStateFiles(captured capturedFilesState, background bool, submoduleConfigs []*models.SubmoduleConfig) error {
|
||||
fileTreeViewModel := self.c.Contexts().Files.FileTreeViewModel
|
||||
generation := self.c.State().GetRepoGeneration()
|
||||
|
||||
|
|
@ -1091,7 +1166,7 @@ func (self *RefreshHelper) refreshStateFiles(background bool, submoduleConfigs [
|
|||
// Although this also means that at startup we won't be staging anything until
|
||||
// we call git status again.
|
||||
pathsToStage := []string{}
|
||||
for _, file := range self.c.Model().Files {
|
||||
for _, file := range captured.prevFiles {
|
||||
if file.HasMergeConflicts {
|
||||
prevConflictFileCount++
|
||||
}
|
||||
|
|
@ -1115,7 +1190,7 @@ func (self *RefreshHelper) refreshStateFiles(background bool, submoduleConfigs [
|
|||
|
||||
files := self.c.Git().Loaders.FileLoader.
|
||||
GetStatusFiles(git_commands.GetStatusFileOptions{
|
||||
ForceShowUntracked: self.c.Contexts().Files.ForceShowUntracked(),
|
||||
ForceShowUntracked: captured.forceShowUntracked,
|
||||
Background: background,
|
||||
})
|
||||
|
||||
|
|
@ -1186,15 +1261,15 @@ 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(background bool, selectTopEntry bool) ([]*models.Commit, error) {
|
||||
func (self *RefreshHelper) refreshReflogCommits(captured capturedReflogState, background bool, selectTopEntry 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
|
||||
model := self.c.Model()
|
||||
|
||||
// load does the git work on the worker and returns the new value for a
|
||||
// reflog slice, reading the existing slice for the incremental fetch. The
|
||||
// caller writes the result in the bounce.
|
||||
// reflog slice, reading the existing slice (captured on the UI thread) for
|
||||
// the incremental fetch. The caller writes the result in the bounce.
|
||||
load := func(existing []*models.Commit, filterPath string, filterAuthor string) ([]*models.Commit, error) {
|
||||
var lastReflogCommit *models.Commit
|
||||
if filterPath == "" && filterAuthor == "" && len(existing) > 0 {
|
||||
|
|
@ -1202,7 +1277,7 @@ func (self *RefreshHelper) refreshReflogCommits(background bool, selectTopEntry
|
|||
}
|
||||
|
||||
commits, onlyObtainedNewReflogCommits, err := self.c.Git().Loaders.ReflogCommitLoader.
|
||||
GetReflogCommits(model.HashPool, lastReflogCommit, filterPath, filterAuthor)
|
||||
GetReflogCommits(captured.hashPool, lastReflogCommit, filterPath, filterAuthor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1213,14 +1288,14 @@ func (self *RefreshHelper) refreshReflogCommits(background bool, selectTopEntry
|
|||
return commits, nil
|
||||
}
|
||||
|
||||
reflogCommits, err := load(model.ReflogCommits, "", "")
|
||||
reflogCommits, err := load(captured.reflogCommits, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filteredReflogCommits := reflogCommits
|
||||
if self.c.Modes().Filtering.Active() {
|
||||
filteredReflogCommits, err = load(model.FilteredReflogCommits, self.c.Modes().Filtering.GetPath(), self.c.Modes().Filtering.GetAuthor())
|
||||
if captured.filteringActive {
|
||||
filteredReflogCommits, err = load(captured.filteredReflogCommits, captured.filterPath, captured.filterAuthor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1304,11 +1379,11 @@ func (self *RefreshHelper) refreshWorktrees(background bool) {
|
|||
self.refreshView(self.c.Contexts().Worktrees, background)
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshStashEntries(background bool) {
|
||||
func (self *RefreshHelper) refreshStashEntries(filterPath string, background bool) {
|
||||
generation := self.c.State().GetRepoGeneration()
|
||||
|
||||
stashEntries := self.c.Git().Loaders.StashLoader.
|
||||
GetStashEntries(self.c.Modes().Filtering.GetPath())
|
||||
GetStashEntries(filterPath)
|
||||
|
||||
self.onUIThreadUnlessRepoChanged(generation, background, func() error {
|
||||
self.c.Model().StashEntries = stashEntries
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ func (self *SubmodulesController) add() error {
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
return nil
|
||||
})
|
||||
},
|
||||
|
|
@ -193,7 +193,7 @@ func (self *SubmodulesController) editURL(submodule *models.SubmoduleConfig) err
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
return nil
|
||||
})
|
||||
},
|
||||
|
|
@ -210,7 +210,7 @@ func (self *SubmodulesController) init(submodule *models.SubmoduleConfig) error
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -229,7 +229,7 @@ func (self *SubmodulesController) openBulkActionsMenu() error {
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
return nil
|
||||
})
|
||||
},
|
||||
|
|
@ -244,7 +244,7 @@ func (self *SubmodulesController) openBulkActionsMenu() error {
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
return nil
|
||||
})
|
||||
},
|
||||
|
|
@ -259,7 +259,7 @@ func (self *SubmodulesController) openBulkActionsMenu() error {
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
return nil
|
||||
})
|
||||
},
|
||||
|
|
@ -274,7 +274,7 @@ func (self *SubmodulesController) openBulkActionsMenu() error {
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
return nil
|
||||
})
|
||||
},
|
||||
|
|
@ -292,7 +292,7 @@ func (self *SubmodulesController) update(submodule *models.SubmoduleConfig) erro
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue