mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-11-06 13:00:59 -04:00
Show a directory that git status reports as a whole as a single entry
git status doesn't descend into a nested repository, such as the directory that a removed submodule leaves behind. It reports the directory as a whole, with a trailing slash. The file tree splits paths at slashes, so it showed a directory with a file without a name in it. Discarding the directory removed it, but then the cleanup of emptied directories took the parent of "modules/sub/" to be "modules/sub" itself, and failed to read it. Take the slash off when loading the files. This also covers any untracked directory if status.showUntrackedFiles is "normal", and linked worktrees inside the repo. Their slash was taken off when they were marked as worktrees; this is no longer needed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
165b4bb499
commit
2ddeebca72
@@ -73,8 +73,16 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
|
||||
continue
|
||||
}
|
||||
|
||||
// git status reports a directory that it doesn't descend into with a
|
||||
// trailing slash. This is the case for a nested repository, and for
|
||||
// any untracked directory if status.showUntrackedFiles is "normal". We
|
||||
// take the slash off so that the file tree shows the directory as a
|
||||
// single entry; it would otherwise split the path into a directory and
|
||||
// a file without a name.
|
||||
path := strings.TrimSuffix(status.Path, "/")
|
||||
|
||||
file := &models.File{
|
||||
Path: status.Path,
|
||||
Path: path,
|
||||
PreviousPath: status.PreviousPath,
|
||||
DisplayString: status.StatusString,
|
||||
}
|
||||
|
||||
@@ -188,6 +188,26 @@ func TestFileGetStatusFiles(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Directory that git doesn't descend into",
|
||||
similarityThreshold: 50,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "?? dir/nested/", nil),
|
||||
expectedFiles: []*models.File{
|
||||
{
|
||||
Path: "dir/nested",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? dir/nested/",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Renamed files",
|
||||
similarityThreshold: 50,
|
||||
|
||||
@@ -1467,9 +1467,7 @@ func (self *RefreshHelper) refreshStateFiles(captured capturedFilesState, env re
|
||||
}
|
||||
|
||||
// markWorktreeFiles marks the files that are linked worktrees of this repo, so
|
||||
// that the files view can render them as such. `git status` reports a worktree
|
||||
// as an untracked directory, i.e. with a trailing slash, which we take off:
|
||||
// keeping it would build a directory node with a nameless file inside it.
|
||||
// that the files view can render them as such.
|
||||
//
|
||||
// It must run on the UI thread, as it works on the model. Both models it needs
|
||||
// are written by refreshes of their own, so it is called after either of them
|
||||
@@ -1487,12 +1485,6 @@ func markWorktreeFiles(files []*models.File, worktrees []*models.Worktree, workt
|
||||
file.IsWorktree = isWorktree
|
||||
changed = true
|
||||
}
|
||||
if isWorktree {
|
||||
if trimmed := strings.TrimSuffix(file.Path, "/"); trimmed != file.Path {
|
||||
file.Path = trimmed
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changed
|
||||
|
||||
@@ -309,12 +309,12 @@ func TestMarkWorktreeFiles(t *testing.T) {
|
||||
{Path: filepath.Join("/", "path", "to", "worktree3")},
|
||||
}
|
||||
|
||||
t.Run("marks the files that are worktrees, and takes their slash off", func(t *testing.T) {
|
||||
t.Run("marks the files that are worktrees", func(t *testing.T) {
|
||||
files := []*models.File{
|
||||
{Path: "file"},
|
||||
{Path: "worktree1/"},
|
||||
{Path: "dir/worktree2/"},
|
||||
{Path: "dir/"},
|
||||
{Path: "worktree1"},
|
||||
{Path: "dir/worktree2"},
|
||||
{Path: "dir"},
|
||||
}
|
||||
|
||||
assert.True(t, markWorktreeFiles(files, worktrees, worktreePath))
|
||||
@@ -322,12 +322,12 @@ func TestMarkWorktreeFiles(t *testing.T) {
|
||||
{Path: "file"},
|
||||
{Path: "worktree1", IsWorktree: true},
|
||||
{Path: "dir/worktree2", IsWorktree: true},
|
||||
{Path: "dir/"},
|
||||
{Path: "dir"},
|
||||
}, files)
|
||||
})
|
||||
|
||||
t.Run("reports no change when there is nothing to mark", func(t *testing.T) {
|
||||
files := []*models.File{{Path: "file"}, {Path: "dir/"}}
|
||||
files := []*models.File{{Path: "file"}, {Path: "dir"}}
|
||||
|
||||
assert.False(t, markWorktreeFiles(files, worktrees, worktreePath))
|
||||
})
|
||||
|
||||
@@ -26,12 +26,8 @@ var DiscardRemovedSubmodule = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
/* EXPECTED:
|
||||
Equals("▼ modules").IsSelected(),
|
||||
Equals(" ?? sub"),
|
||||
ACTUAL: */
|
||||
Equals("▼ modules/sub").IsSelected(),
|
||||
Equals(" ?? "),
|
||||
).
|
||||
Press(keys.Universal.Remove).
|
||||
Tap(func() {
|
||||
@@ -40,15 +36,7 @@ var DiscardRemovedSubmodule = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Select(Contains("Discard all changes")).
|
||||
Confirm()
|
||||
}).
|
||||
/* EXPECTED:
|
||||
IsFocused().
|
||||
ACTUAL: */
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("open modules/sub: no such file or directory")).
|
||||
Confirm()
|
||||
}).
|
||||
IsEmpty()
|
||||
|
||||
t.FileSystem().PathNotPresent("modules/sub")
|
||||
|
||||
Reference in New Issue
Block a user