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,