Avoid enumerating untracked files when staging hunks of tracked files

We know that staging or unstaging a hunk in a tracked file doesn't have
influence on untracked files, so we can take them over from the previous state
of the model. This can be a performance improvement in repos with many untracked
files.
This commit is contained in:
Stefan Haller 2026-04-06 11:36:22 +02:00
parent cf491d1b5b
commit 38f35ae346
3 changed files with 34 additions and 7 deletions

View file

@ -169,7 +169,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) {
if scopeSet.Includes(types.FILES) || scopeSet.Includes(types.SUBMODULES) {
fileWg.Add(1)
refresh("files", func() {
_ = self.refreshFilesAndSubmodules()
_ = self.refreshFilesAndSubmodules(options.TakeOverUntrackedFilesFromPreviousModel)
fileWg.Done()
})
}
@ -539,7 +539,7 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
self.refreshStatus()
}
func (self *RefreshHelper) refreshFilesAndSubmodules() error {
func (self *RefreshHelper) refreshFilesAndSubmodules(takeOverUntrackedFilesFromPreviousModel bool) error {
self.c.Mutexes().RefreshingFilesMutex.Lock()
self.c.State().SetIsRefreshingFiles(true)
defer func() {
@ -551,7 +551,7 @@ func (self *RefreshHelper) refreshFilesAndSubmodules() error {
return err
}
if err := self.refreshStateFiles(); err != nil {
if err := self.refreshStateFiles(takeOverUntrackedFilesFromPreviousModel); err != nil {
return err
}
@ -564,7 +564,7 @@ func (self *RefreshHelper) refreshFilesAndSubmodules() error {
return nil
}
func (self *RefreshHelper) refreshStateFiles() error {
func (self *RefreshHelper) refreshStateFiles(takeOverUntrackedFilesFromPreviousModel bool) error {
fileTreeViewModel := self.c.Contexts().Files.FileTreeViewModel
prevConflictFileCount := 0
@ -599,8 +599,16 @@ func (self *RefreshHelper) refreshStateFiles() error {
}
}
var previousUntrackedFiles []*models.File
if takeOverUntrackedFilesFromPreviousModel {
previousUntrackedFiles = lo.Filter(self.c.Model().Files,
func(file *models.File, _ int) bool { return file.ShortStatus == "??" })
}
showUntracked := git_commands.ShowUntrackedModeAuto
if self.c.Contexts().Files.ForceShowUntracked() {
if takeOverUntrackedFilesFromPreviousModel {
showUntracked = git_commands.ShowUntrackedModeOff
} else if self.c.Contexts().Files.ForceShowUntracked() {
showUntracked = git_commands.ShowUntrackedModeOn
}
files := self.c.Git().Loaders.FileLoader.
@ -608,6 +616,10 @@ func (self *RefreshHelper) refreshStateFiles() error {
ShowUntracked: showUntracked,
})
if takeOverUntrackedFilesFromPreviousModel {
files = append(files, previousUntrackedFiles...)
}
conflictFileCount := 0
for _, file := range files {
if file.HasMergeConflicts {

View file

@ -226,11 +226,16 @@ func (self *StagingController) DiscardSelection() error {
}
func (self *StagingController) applySelectionAndRefresh(reverse bool) error {
isTracked := self.c.Contexts().Files.GetSelectedFile().Tracked
if err := self.applySelection(reverse); err != nil {
return err
}
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.STAGING}})
self.c.Refresh(types.RefreshOptions{
Scope: []types.RefreshableView{types.FILES, types.STAGING},
TakeOverUntrackedFilesFromPreviousModel: isTracked,
})
return nil
}
@ -281,11 +286,16 @@ func (self *StagingController) applySelection(reverse bool) error {
}
func (self *StagingController) EditHunkAndRefresh() error {
isTracked := self.c.Contexts().Files.GetSelectedFile().Tracked
if err := self.editHunk(); err != nil {
return err
}
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.STAGING}})
self.c.Refresh(types.RefreshOptions{
Scope: []types.RefreshableView{types.FILES, types.STAGING},
TakeOverUntrackedFilesFromPreviousModel: isTracked,
})
return nil
}

View file

@ -44,4 +44,9 @@ type RefreshOptions struct {
// keeps the selection index the same. Useful after checking out a detached
// head, and selecting index 0.
KeepBranchSelectionIndex bool
// If true, call git status with --untracked-files=no to skip enumerating untracked files, and
// keep the ones we have in the model. Useful as a performance optimization when we know the
// untracked files can't have changed, e.g. after staging/unstaging hunks of tracked files.
TakeOverUntrackedFilesFromPreviousModel bool
}