Repository: jesseduffield/lazygit (language: go)

Issue:
When hiding untracked files, adding a folder adds also untracked files (git add instead of git add -u)
**Describe the bug**
When hiding untracked files, adding a folder adds also untracked files (git add instead of git add -u).
This means you are also in the commit only shown a subset of the files that will be actually commited.

**To Reproduce**
In a repo, create a folder, add two files, track one file.
Then change the tracked file.
Open lazy git. Filter to tracked files (ctrl+b, t).
Now with space select the folder to commit.
Without your knowledge it also adds the untracked file.


**Expected behavior**
It should only add from the tracked files, if the view is filtered.

**Version info:**
commit=, build date=, build source=nix, version=0.45.2, os=linux, arch=amd64, git version=2.47.1

**Additional context**
There might be a reason to do it this way. If that is the case, at least a warning should be displayed.



Discussion / hints:
Yeah, I think this is a problem. I will try to put a PR soon.
Sorry for being so late. I have been very busy for the last couple of weeks.
Anyway, I am a bit confused about how this thing will work. Suppose some untracked files were already staged before applying track filter. If I commit in that filter view what should I do?
- Keep the unstaged files in commit. This might be confusing like before.
- Only commit the files staged in current filter view.

Also, during unstaging in filter view, what should be the expected behavior?

In my opinion, when in filtered view, we should only consider files in current view for all things (staging, unstaging, commit). But this will complicate a lot of things and might be confusing in many use cases.
Interesting questions. I don't use the filter feature much, but when playing with it just now, I found it confusing that untracked but staged files don't show up when I filter for only tracked files. In my intuition, a staged file already counts as a tracked file (even though I know that it technically isn't). So I would propose to simply include them (and, for symmetry, probably exclude them when filtering for only untracked files? Not so sure about that).

This would solve the problems you brought up, or am I still missing scenarios where it doesn't?

Here's a patch that does this:
```diff
diff --git i/pkg/gui/filetree/file_tree.go w/pkg/gui/filetree/file_tree.go
index 9ef980faa..5298f79e8 100644
--- i/pkg/gui/filetree/file_tree.go
+++ w/pkg/gui/filetree/file_tree.go
@@ -88,9 +88,9 @@ func (self *FileTree) getFilesForDisplay() []*models.File {
 	case DisplayUnstaged:
 		return self.FilterFiles(func(file *models.File) bool { return file.HasUnstagedChanges })
 	case DisplayTracked:
-		return self.FilterFiles(func(file *models.File) bool { return file.Tracked })
+		return self.FilterFiles(func(file *models.File) bool { return file.Tracked || file.HasStagedChanges })
 	case DisplayUntracked:
-		return self.FilterFiles(func(file *models.File) bool { return !file.Tracked })
+		return self.FilterFiles(func(file *models.File) bool { return !(file.Tracked || file.HasStagedChanges) })
 	case DisplayConflicted:
 		return self.FilterFiles(func(file *models.File) bool { return file.HasMergeConflicts })
 	default:
```

That is a good point.
I still find the behavior of it adding the whole folder unintuitive but i can also see that for some people it might be the other way around.
This way, no matter the implementation you DO have a proper view of what will be committed though - which i like!

> I still find the behavior of it adding the whole folder unintuitive 

Well yes of course. The patch above wasn't meant as the only measure we take, we still also want to do `git add -u`, which I understand @parthokunda is working on. The patch was only meant to address the issue that Partho was running into while doing that.
> Interesting questions. I don't use the filter feature much, but when playing with it just now, I found it confusing that untracked but staged files don't show up when I filter for only tracked files. In my intuition, a staged file already counts as a tracked file (even though I know that it technically isn't). So I would propose to simply include them (and, for symmetry, probably exclude them when filtering for only untracked files? Not so sure about that).

This is a nice solution. However, if we unstage in tracked filter view, the untracked files will disappear. This might be a bit confusing, but nevertheless, seems like a good tradeoff to make.
Created a draft PR for now. Gonna clean this up hopefully tomorrow.
> However, if we unstage in tracked filter view, the untracked files will disappear. This might be a bit confusing, 

I don't find this confusing at all. This is not really very different from filtering by staged files, and then unstaging a file (or vice versa); of course the file then disappears.
I have made a PR, kindly check at your convenience.

Fix patch:
diff --git a/pkg/commands/git_commands/working_tree.go b/pkg/commands/git_commands/working_tree.go
index 3b818d83f77..cff946a09a2 100644
--- a/pkg/commands/git_commands/working_tree.go
+++ b/pkg/commands/git_commands/working_tree.go
@@ -34,11 +34,15 @@ func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj {
 
 // StageFile stages a file
 func (self *WorkingTreeCommands) StageFile(path string) error {
-	return self.StageFiles([]string{path})
+	return self.StageFiles([]string{path}, nil)
 }
 
-func (self *WorkingTreeCommands) StageFiles(paths []string) error {
-	cmdArgs := NewGitCmd("add").Arg("--").Arg(paths...).ToArgv()
+func (self *WorkingTreeCommands) StageFiles(paths []string, extraArgs []string) error {
+	cmdArgs := NewGitCmd("add").
+		Arg(extraArgs...).
+		Arg("--").
+		Arg(paths...).
+		ToArgv()
 
 	return self.cmd.New(cmdArgs).Run()
 }
diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go
index ede3a32c4ad..723c16681dc 100644
--- a/pkg/gui/controllers/files_controller.go
+++ b/pkg/gui/controllers/files_controller.go
@@ -421,13 +421,19 @@ func (self *FilesController) pressWithLock(selectedNodes []*filetree.FileNode) e
 	unstagedSelectedNodes := filterNodesHaveUnstagedChanges(selectedNodes)
 
 	if len(unstagedSelectedNodes) > 0 {
+		var extraArgs []string
+
+		if self.context().GetFilter() == filetree.DisplayTracked {
+			extraArgs = []string{"-u"}
+		}
+
 		self.c.LogAction(self.c.Tr.Actions.StageFile)
 
 		if err := self.optimisticChange(unstagedSelectedNodes, self.optimisticStage); err != nil {
 			return err
 		}
 
-		if err := self.c.Git().WorkingTree.StageFiles(toPaths(unstagedSelectedNodes)); err != nil {
+		if err := self.c.Git().WorkingTree.StageFiles(toPaths(unstagedSelectedNodes), extraArgs); err != nil {
 			return err
 		}
 	} else {
diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go
index cac9310d161..e267f836030 100644
--- a/pkg/gui/controllers/helpers/refresh_helper.go
+++ b/pkg/gui/controllers/helpers/refresh_helper.go
@@ -565,7 +565,7 @@ func (self *RefreshHelper) refreshStateFiles() error {
 
 		if len(pathsToStage) > 0 {
 			self.c.LogAction(self.c.Tr.Actions.StageResolvedFiles)
-			if err := self.c.Git().WorkingTree.StageFiles(pathsToStage); err != nil {
+			if err := self.c.Git().WorkingTree.StageFiles(pathsToStage, nil); err != nil {
 				return err
 			}
 		}
diff --git a/pkg/gui/filetree/file_tree.go b/pkg/gui/filetree/file_tree.go
index bd201b7dd09..fe18db0c062 100644
--- a/pkg/gui/filetree/file_tree.go
+++ b/pkg/gui/filetree/file_tree.go
@@ -88,9 +88,11 @@ func (self *FileTree) getFilesForDisplay() []*models.File {
 	case DisplayUnstaged:
 		return self.FilterFiles(func(file *models.File) bool { return file.HasUnstagedChanges })
 	case DisplayTracked:
-		return self.FilterFiles(func(file *models.File) bool { return file.Tracked })
+		// untracked but staged files are technically not tracked by git
+		// but including such files in the filtered mode helps see what files are getting committed
+		return self.FilterFiles(func(file *models.File) bool { return file.Tracked || file.HasStagedChanges })
 	case DisplayUntracked:
-		return self.FilterFiles(func(file *models.File) bool { return !file.Tracked })
+		return self.FilterFiles(func(file *models.File) bool { return !(file.Tracked || file.HasStagedChanges) })
 	case DisplayConflicted:
 		return self.FilterFiles(func(file *models.File) bool { return file.HasMergeConflicts })
 	default:


Test patch:
diff --git a/pkg/commands/git_commands/working_tree_test.go b/pkg/commands/git_commands/working_tree_test.go
index e56818c1b3e..0016e9fe879 100644
--- a/pkg/commands/git_commands/working_tree_test.go
+++ b/pkg/commands/git_commands/working_tree_test.go
@@ -30,7 +30,7 @@ func TestWorkingTreeStageFiles(t *testing.T) {
 
 	instance := buildWorkingTreeCommands(commonDeps{runner: runner})
 
-	assert.NoError(t, instance.StageFiles([]string{"test.txt", "test2.txt"}))
+	assert.NoError(t, instance.StageFiles([]string{"test.txt", "test2.txt"}, nil))
 	runner.CheckForMissingCalls()
 }
 
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
index f2335d28c07..0c5f95e1d05 100644
--- a/pkg/integration/tests/filter_and_search/filter_by_file_status.go
+++ b/pkg/integration/tests/filter_and_search/filter_by_file_status.go
@@ -21,12 +21,16 @@ var FilterByFileStatus = NewIntegrationTest(NewIntegrationTestArgs{
 
 		shell.CreateFile("file-untracked", "bar")
 		shell.UpdateFile("file-tracked", "baz")
+
+		shell.CreateFile("file-staged-but-untracked", "qux")
+		shell.GitAdd("file-staged-but-untracked")
 	},
 	Run: func(t *TestDriver, keys config.KeybindingConfig) {
 		t.Views().Files().
 			Focus().
 			Lines(
-				Contains(`file-tracked`).IsSelected(),
+				Equals("A  file-staged-but-untracked").IsSelected(),
+				Equals(" M file-tracked"),
 			).
 			Press(keys.Files.OpenStatusFilter).
 			Tap(func() {
@@ -36,7 +40,7 @@ var FilterByFileStatus = NewIntegrationTest(NewIntegrationTestArgs{
 					Confirm()
 			}).
 			Lines(
-				Contains(`file-untracked`).IsSelected(),
+				Equals("?? file-untracked").IsSelected(),
 			).
 			Press(keys.Files.OpenStatusFilter).
 			Tap(func() {
@@ -46,7 +50,8 @@ var FilterByFileStatus = NewIntegrationTest(NewIntegrationTestArgs{
 					Confirm()
 			}).
 			Lines(
-				Contains(`file-tracked`).IsSelected(),
+				Equals("A  file-staged-but-untracked").IsSelected(),
+				Equals(" M file-tracked"),
 			).
 			Press(keys.Files.OpenStatusFilter).
 			Tap(func() {
@@ -56,7 +61,8 @@ var FilterByFileStatus = NewIntegrationTest(NewIntegrationTestArgs{
 					Confirm()
 			}).
 			Lines(
-				Contains(`file-tracked`).IsSelected(),
+				Equals("A  file-staged-but-untracked").IsSelected(),
+				Equals(" M file-tracked"),
 			)
 	},
 })
diff --git a/pkg/integration/tests/filter_and_search/staging_folder_stages_only_tracked_files_in_tracked_only_filter.go b/pkg/integration/tests/filter_and_search/staging_folder_stages_only_tracked_files_in_tracked_only_filter.go
new file mode 100644
index 00000000000..aa9220b95e8
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/staging_folder_stages_only_tracked_files_in_tracked_only_filter.go
@@ -0,0 +1,56 @@
+package filter_and_search
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var StagingFolderStagesOnlyTrackedFilesInTrackedOnlyFilter = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Staging entire folder in tracked only view, should stage only tracked files",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig: func(config *config.AppConfig) {
+	},
+	SetupRepo: func(shell *Shell) {
+		shell.CreateDir("test")
+		shell.CreateFileAndAdd("test/file-tracked", "foo")
+
+		shell.Commit("first commit")
+
+		shell.CreateFile("test/file-untracked", "bar")
+		shell.UpdateFile("test/file-tracked", "baz")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Files().
+			Focus().
+			Lines(
+				Equals("▼ test").IsSelected(),
+				Equals("   M file-tracked"),
+				Equals("  ?? file-untracked"),
+			).
+			Press(keys.Files.OpenStatusFilter).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Filtering")).
+					Select(Contains("Show only tracked files")).
+					Confirm()
+			}).
+			Lines(
+				Equals("▼ test").IsSelected(),
+				Equals("   M file-tracked"),
+			).
+			PressPrimaryAction().
+			Press(keys.Files.OpenStatusFilter).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Filtering")).
+					Select(Contains("No filter")).
+					Confirm()
+			}).
+			Lines(
+				Equals("▼ test").IsSelected(),
+				Equals("  M  file-tracked"), // 'M' is now in the left column, so file is staged
+				Equals("  ?? file-untracked"),
+			)
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index 6ff5c2f07c1..c826faf7c88 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -207,6 +207,7 @@ var tests = []*components.IntegrationTest{
 	filter_and_search.NestedFilter,
 	filter_and_search.NestedFilterTransient,
 	filter_and_search.NewSearch,
+	filter_and_search.StagingFolderStagesOnlyTrackedFilesInTrackedOnlyFilter,
 	filter_by_author.SelectAuthor,
 	filter_by_author.TypeAuthor,
 	filter_by_path.CliArg,