From 7e85cdd02731aa40605f7769a6a38f1fdca8cb14 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 31 Jan 2025 09:30:31 +1100 Subject: [PATCH] Allow user to filter the files view to only show untracked files This handles the situation where the user's own config says to not show untracked files, as is often the case with bare repos managing a user's dotfiles. --- pkg/commands/git_commands/file_loader.go | 6 +- pkg/gui/controllers/files_controller.go | 21 ++++++- pkg/gui/controllers/helpers/refresh_helper.go | 4 +- pkg/gui/filetree/file_tree.go | 8 +++ pkg/i18n/english.go | 2 + .../filter_by_file_status.go | 62 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 7 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 pkg/integration/tests/filter_and_search/filter_by_file_status.go diff --git a/pkg/commands/git_commands/file_loader.go b/pkg/commands/git_commands/file_loader.go index 4cf0da2d0..dcc1615a7 100644 --- a/pkg/commands/git_commands/file_loader.go +++ b/pkg/commands/git_commands/file_loader.go @@ -32,13 +32,17 @@ func NewFileLoader(gitCommon *GitCommon, cmd oscommands.ICmdObjBuilder, config F type GetStatusFileOptions struct { NoRenames bool + // If true, we'll show untracked files even if the user has set the config to hide them. + // 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 } func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File { // check if config wants us ignoring untracked files untrackedFilesSetting := self.config.GetShowUntrackedFiles() - if untrackedFilesSetting == "" { + if opts.ForceShowUntracked || untrackedFilesSetting == "" { untrackedFilesSetting = "all" } untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting) diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 00317f4cf..cdf3f6241 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -777,6 +777,13 @@ func (self *FilesController) handleStatusFilterPressed() error { }, Key: 't', }, + { + Label: self.c.Tr.FilterUntrackedFiles, + OnPress: func() error { + return self.setStatusFiltering(filetree.DisplayUntracked) + }, + Key: 'T', + }, { Label: self.c.Tr.ResetFilter, OnPress: func() error { @@ -789,9 +796,19 @@ func (self *FilesController) handleStatusFilterPressed() error { } func (self *FilesController) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error { + previousFilter := self.context().GetFilter() + self.context().FileTreeViewModel.SetStatusFilter(filter) - self.c.PostRefreshUpdate(self.context()) - return nil + + // Whenever we switch between untracked and other filters, we need to refresh the files view + // because the untracked files filter applies when running `git status`. + if previousFilter != filter && (previousFilter == filetree.DisplayUntracked || filter == filetree.DisplayUntracked) { + return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Mode: types.ASYNC}) + } else { + self.c.PostRefreshUpdate(self.context()) + + return nil + } } func (self *FilesController) edit(nodes []*filetree.FileNode) error { diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 46965bddd..37e451895 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -570,7 +570,9 @@ func (self *RefreshHelper) refreshStateFiles() error { } files := self.c.Git().Loaders.FileLoader. - GetStatusFiles(git_commands.GetStatusFileOptions{}) + GetStatusFiles(git_commands.GetStatusFileOptions{ + ForceShowUntracked: self.c.Contexts().Files.ForceShowUntracked(), + }) conflictFileCount := 0 for _, file := range files { diff --git a/pkg/gui/filetree/file_tree.go b/pkg/gui/filetree/file_tree.go index c7cf1c76c..bd201b7dd 100644 --- a/pkg/gui/filetree/file_tree.go +++ b/pkg/gui/filetree/file_tree.go @@ -16,6 +16,7 @@ const ( DisplayStaged DisplayUnstaged DisplayTracked + DisplayUntracked // this shows files with merge conflicts DisplayConflicted ) @@ -40,6 +41,7 @@ type IFileTree interface { FilterFiles(test func(*models.File) bool) []*models.File SetStatusFilter(filter FileTreeDisplayFilter) + ForceShowUntracked() bool Get(index int) *FileNode GetFile(path string) *models.File GetAllItems() []*FileNode @@ -87,6 +89,8 @@ func (self *FileTree) getFilesForDisplay() []*models.File { return self.FilterFiles(func(file *models.File) bool { return file.HasUnstagedChanges }) case DisplayTracked: return self.FilterFiles(func(file *models.File) bool { return file.Tracked }) + case DisplayUntracked: + return self.FilterFiles(func(file *models.File) bool { return !file.Tracked }) case DisplayConflicted: return self.FilterFiles(func(file *models.File) bool { return file.HasMergeConflicts }) default: @@ -94,6 +98,10 @@ func (self *FileTree) getFilesForDisplay() []*models.File { } } +func (self *FileTree) ForceShowUntracked() bool { + return self.filter == DisplayUntracked +} + func (self *FileTree) FilterFiles(test func(*models.File) bool) []*models.File { return lo.Filter(self.getFiles(), func(file *models.File, _ int) bool { return test(file) }) } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index f92c40993..135ab43e1 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -88,6 +88,7 @@ type TranslationSet struct { FilterStagedFiles string FilterUnstagedFiles string FilterTrackedFiles string + FilterUntrackedFiles string ResetFilter string MergeConflictsTitle string Checkout string @@ -1113,6 +1114,7 @@ func EnglishTranslationSet() *TranslationSet { FilterStagedFiles: "Show only staged files", FilterUnstagedFiles: "Show only unstaged files", FilterTrackedFiles: "Show only tracked files", + FilterUntrackedFiles: "Show only untracked files", ResetFilter: "Reset filter", NoChangedFiles: "No changed files", SoftReset: "Soft reset", diff --git a/pkg/integration/tests/filter_and_search/filter_by_file_status.go b/pkg/integration/tests/filter_and_search/filter_by_file_status.go new file mode 100644 index 000000000..05b38ea96 --- /dev/null +++ b/pkg/integration/tests/filter_and_search/filter_by_file_status.go @@ -0,0 +1,62 @@ +package filter_and_search + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FilterByFileStatus = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Filtering to show untracked files in repo that hides them by default", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + }, + SetupRepo: func(shell *Shell) { + // need to set untracked files to not be displayed in git config + shell.SetConfig("status.showUntrackedFiles", "no") + + shell.CreateFileAndAdd("file-tracked", "foo") + + shell.Commit("first commit") + + shell.CreateFile("file-untracked", "bar") + shell.UpdateFile("file-tracked", "baz") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + Focus(). + Lines( + Contains(`file-tracked`).IsSelected(), + ). + Press(keys.Files.OpenStatusFilter). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Filtering")). + Select(Contains("Show only untracked files")). + Confirm() + }). + Lines( + Contains(`file-untracked`).IsSelected(), + ). + Press(keys.Files.OpenStatusFilter). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Filtering")). + Select(Contains("Show only tracked files")). + Confirm() + }). + Lines( + Contains(`file-tracked`).IsSelected(), + ). + Press(keys.Files.OpenStatusFilter). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Filtering")). + Select(Contains("Reset filter")). + Confirm() + }). + Lines( + Contains(`file-tracked`).IsSelected(), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index c28ab0cdb..4557b8afd 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -186,6 +186,7 @@ var tests = []*components.IntegrationTest{ file.StageChildrenRangeSelect, file.StageDeletedRangeSelect, file.StageRangeSelect, + filter_and_search.FilterByFileStatus, filter_and_search.FilterCommitFiles, filter_and_search.FilterFiles, filter_and_search.FilterFuzzy,