Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions pkg/commands/git_commands/file_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,25 @@ type GetStatusFileOptions struct {
// This is useful for users with bare repos for dotfiles who default to hiding untracked files,
// but want to occasionally see them to `git add` a new file.
ForceShowUntracked bool
// If true, pass --untracked-files=no to skip enumerating untracked files.
// This is a performance optimization for refreshes that only affect tracked
// files, avoiding a costly directory walk in large repos.
NoUntracked bool
}

func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files
untrackedFilesSetting := self.config.GetShowUntrackedFiles()

if opts.ForceShowUntracked || untrackedFilesSetting == "" {
untrackedFilesSetting = "all"
var untrackedFilesArg string
if opts.NoUntracked {
untrackedFilesArg = "--untracked-files=no"
} else {
// check if config wants us ignoring untracked files
untrackedFilesSetting := self.config.GetShowUntrackedFiles()

if opts.ForceShowUntracked || untrackedFilesSetting == "" {
untrackedFilesSetting = "all"
}
untrackedFilesArg = fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
}
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)

statuses, err := self.gitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
if err != nil {
Expand Down
27 changes: 22 additions & 5 deletions pkg/gui/controllers/helpers/refresh_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.KeepUntrackedFiles)
fileWg.Done()
})
}
Expand Down Expand Up @@ -539,7 +539,7 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
self.refreshStatus()
}

func (self *RefreshHelper) refreshFilesAndSubmodules() error {
func (self *RefreshHelper) refreshFilesAndSubmodules(keepUntrackedFiles bool) error {
self.c.Mutexes().RefreshingFilesMutex.Lock()
self.c.State().SetIsRefreshingFiles(true)
defer func() {
Expand All @@ -551,7 +551,7 @@ func (self *RefreshHelper) refreshFilesAndSubmodules() error {
return err
}

if err := self.refreshStateFiles(); err != nil {
if err := self.refreshStateFiles(keepUntrackedFiles); err != nil {
return err
}

Expand All @@ -564,7 +564,7 @@ func (self *RefreshHelper) refreshFilesAndSubmodules() error {
return nil
}

func (self *RefreshHelper) refreshStateFiles() error {
func (self *RefreshHelper) refreshStateFiles(keepUntrackedFiles bool) error {
fileTreeViewModel := self.c.Contexts().Files.FileTreeViewModel

prevConflictFileCount := 0
Expand Down Expand Up @@ -599,11 +599,28 @@ func (self *RefreshHelper) refreshStateFiles() error {
}
}

// When keepUntrackedFiles is true, we skip enumerating untracked files
// in git status (avoiding a costly directory walk in large repos) and
// preserve the untracked files from the previous model state instead.
var previousUntrackedFiles []*models.File
if keepUntrackedFiles {
for _, file := range self.c.Model().Files {
if !file.Tracked {
previousUntrackedFiles = append(previousUntrackedFiles, file)
}
}
}

files := self.c.Git().Loaders.FileLoader.
GetStatusFiles(git_commands.GetStatusFileOptions{
ForceShowUntracked: self.c.Contexts().Files.ForceShowUntracked(),
ForceShowUntracked: !keepUntrackedFiles && self.c.Contexts().Files.ForceShowUntracked(),
NoUntracked: keepUntrackedFiles,
})

if keepUntrackedFiles {
files = append(files, previousUntrackedFiles...)
}

conflictFileCount := 0
for _, file := range files {
if file.HasMergeConflicts {
Expand Down
18 changes: 16 additions & 2 deletions pkg/gui/controllers/staging_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,19 @@ func (self *StagingController) DiscardSelection() error {
}

func (self *StagingController) applySelectionAndRefresh(reverse bool) error {
// Check if the file is tracked before applying, so we can optimize the
// refresh by skipping untracked file enumeration in large repos.
file := self.c.Contexts().Files.FileTreeViewModel.GetSelectedFile()
isTracked := file != nil && file.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},
KeepUntrackedFiles: isTracked,
})
return nil
}

Expand Down Expand Up @@ -281,11 +289,17 @@ func (self *StagingController) applySelection(reverse bool) error {
}

func (self *StagingController) EditHunkAndRefresh() error {
file := self.c.Contexts().Files.FileTreeViewModel.GetSelectedFile()
isTracked := file != nil && file.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},
KeepUntrackedFiles: isTracked,
})
return nil
}

Expand Down
7 changes: 7 additions & 0 deletions pkg/gui/types/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,11 @@ type RefreshOptions struct {
// keeps the selection index the same. Useful after checking out a detached
// head, and selecting index 0.
KeepBranchSelectionIndex bool

// When true, skip enumerating untracked files during the files refresh
// and preserve untracked files from the previous model state instead.
// This is a performance optimization for operations that only affect
// tracked files (e.g. staging hunks), avoiding a costly
// git status --untracked-files=all in large repos.
KeepUntrackedFiles bool
}