Commit only tracked files in tracked filter view (#4386)

- **PR Description**
When in tracked filter view, selecting a folder for staging should only
stage files that are tracked by git. Previously, it staged even
untracked files that were not in view.
All staged (even untracked) files are shown in tracked filter view.
Fixes #4245 

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
This commit is contained in:
Stefan Haller 2025-03-17 19:56:53 +01:00 committed by GitHub
commit 71c5fa9688
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 87 additions and 12 deletions

View file

@ -34,11 +34,15 @@ func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj {
// StageFile stages a file // StageFile stages a file
func (self *WorkingTreeCommands) StageFile(path string) error { 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 { func (self *WorkingTreeCommands) StageFiles(paths []string, extraArgs []string) error {
cmdArgs := NewGitCmd("add").Arg("--").Arg(paths...).ToArgv() cmdArgs := NewGitCmd("add").
Arg(extraArgs...).
Arg("--").
Arg(paths...).
ToArgv()
return self.cmd.New(cmdArgs).Run() return self.cmd.New(cmdArgs).Run()
} }

View file

@ -30,7 +30,7 @@ func TestWorkingTreeStageFiles(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: runner}) 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() runner.CheckForMissingCalls()
} }

View file

@ -421,13 +421,19 @@ func (self *FilesController) pressWithLock(selectedNodes []*filetree.FileNode) e
unstagedSelectedNodes := filterNodesHaveUnstagedChanges(selectedNodes) unstagedSelectedNodes := filterNodesHaveUnstagedChanges(selectedNodes)
if len(unstagedSelectedNodes) > 0 { if len(unstagedSelectedNodes) > 0 {
var extraArgs []string
if self.context().GetFilter() == filetree.DisplayTracked {
extraArgs = []string{"-u"}
}
self.c.LogAction(self.c.Tr.Actions.StageFile) self.c.LogAction(self.c.Tr.Actions.StageFile)
if err := self.optimisticChange(unstagedSelectedNodes, self.optimisticStage); err != nil { if err := self.optimisticChange(unstagedSelectedNodes, self.optimisticStage); err != nil {
return err 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 return err
} }
} else { } else {

View file

@ -565,7 +565,7 @@ func (self *RefreshHelper) refreshStateFiles() error {
if len(pathsToStage) > 0 { if len(pathsToStage) > 0 {
self.c.LogAction(self.c.Tr.Actions.StageResolvedFiles) 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 return err
} }
} }

View file

@ -88,9 +88,11 @@ func (self *FileTree) getFilesForDisplay() []*models.File {
case DisplayUnstaged: case DisplayUnstaged:
return self.FilterFiles(func(file *models.File) bool { return file.HasUnstagedChanges }) return self.FilterFiles(func(file *models.File) bool { return file.HasUnstagedChanges })
case DisplayTracked: 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: 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: case DisplayConflicted:
return self.FilterFiles(func(file *models.File) bool { return file.HasMergeConflicts }) return self.FilterFiles(func(file *models.File) bool { return file.HasMergeConflicts })
default: default:

View file

@ -21,12 +21,16 @@ var FilterByFileStatus = NewIntegrationTest(NewIntegrationTestArgs{
shell.CreateFile("file-untracked", "bar") shell.CreateFile("file-untracked", "bar")
shell.UpdateFile("file-tracked", "baz") 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) { Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files(). t.Views().Files().
Focus(). Focus().
Lines( Lines(
Contains(`file-tracked`).IsSelected(), Equals("A file-staged-but-untracked").IsSelected(),
Equals(" M file-tracked"),
). ).
Press(keys.Files.OpenStatusFilter). Press(keys.Files.OpenStatusFilter).
Tap(func() { Tap(func() {
@ -36,7 +40,7 @@ var FilterByFileStatus = NewIntegrationTest(NewIntegrationTestArgs{
Confirm() Confirm()
}). }).
Lines( Lines(
Contains(`file-untracked`).IsSelected(), Equals("?? file-untracked").IsSelected(),
). ).
Press(keys.Files.OpenStatusFilter). Press(keys.Files.OpenStatusFilter).
Tap(func() { Tap(func() {
@ -46,7 +50,8 @@ var FilterByFileStatus = NewIntegrationTest(NewIntegrationTestArgs{
Confirm() Confirm()
}). }).
Lines( Lines(
Contains(`file-tracked`).IsSelected(), Equals("A file-staged-but-untracked").IsSelected(),
Equals(" M file-tracked"),
). ).
Press(keys.Files.OpenStatusFilter). Press(keys.Files.OpenStatusFilter).
Tap(func() { Tap(func() {
@ -56,7 +61,8 @@ var FilterByFileStatus = NewIntegrationTest(NewIntegrationTestArgs{
Confirm() Confirm()
}). }).
Lines( Lines(
Contains(`file-tracked`).IsSelected(), Equals("A file-staged-but-untracked").IsSelected(),
Equals(" M file-tracked"),
) )
}, },
}) })

View file

@ -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"),
)
},
})

View file

@ -207,6 +207,7 @@ var tests = []*components.IntegrationTest{
filter_and_search.NestedFilter, filter_and_search.NestedFilter,
filter_and_search.NestedFilterTransient, filter_and_search.NestedFilterTransient,
filter_and_search.NewSearch, filter_and_search.NewSearch,
filter_and_search.StagingFolderStagesOnlyTrackedFilesInTrackedOnlyFilter,
filter_by_author.SelectAuthor, filter_by_author.SelectAuthor,
filter_by_author.TypeAuthor, filter_by_author.TypeAuthor,
filter_by_path.CliArg, filter_by_path.CliArg,