diff --git a/docs/Config.md b/docs/Config.md index a1ce72caa..afc17b723 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -190,6 +190,9 @@ gui: # This can be toggled from within Lazygit with the '`' key, but that will not change the default. showFileTree: true + # If true, add a "/" root item in the file tree representing the root of the repository. It is only added when necessary, i.e. when there is more than one item at top level. + showRootItemInFileTree: true + # If true, show the number of lines changed per file in the Files view showNumstatInFilesView: false diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index db1a8b6dc..af80dd9a4 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -123,6 +123,8 @@ type GuiConfig struct { // If true, display the files in the file views as a tree. If false, display the files as a flat list. // This can be toggled from within Lazygit with the '`' key, but that will not change the default. ShowFileTree bool `yaml:"showFileTree"` + // If true, add a "/" root item in the file tree representing the root of the repository. It is only added when necessary, i.e. when there is more than one item at top level. + ShowRootItemInFileTree bool `yaml:"showRootItemInFileTree"` // If true, show the number of lines changed per file in the Files view ShowNumstatInFilesView bool `yaml:"showNumstatInFilesView"` // If true, show a random tip in the command log when Lazygit starts @@ -764,6 +766,7 @@ func GetDefaultConfig() *UserConfig { ShowBottomLine: true, ShowPanelJumps: true, ShowFileTree: true, + ShowRootItemInFileTree: true, ShowNumstatInFilesView: false, ShowRandomTip: true, ShowIcons: false, diff --git a/pkg/gui/context/working_tree_context.go b/pkg/gui/context/working_tree_context.go index 23eff3c28..f7df1b84a 100644 --- a/pkg/gui/context/working_tree_context.go +++ b/pkg/gui/context/working_tree_context.go @@ -31,7 +31,7 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext { getDisplayStrings := func(_ int, _ int) [][]string { showFileIcons := icons.IsIconEnabled() && c.UserConfig().Gui.ShowFileIcons showNumstat := c.UserConfig().Gui.ShowNumstatInFilesView - lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumstat, &c.UserConfig().Gui.CustomIcons) + lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumstat, &c.UserConfig().Gui.CustomIcons, c.UserConfig().Gui.ShowRootItemInFileTree) return lo.Map(lines, func(line string, _ int) []string { return []string{line} }) diff --git a/pkg/gui/filetree/build_tree.go b/pkg/gui/filetree/build_tree.go index 1168b1a3d..8e6e9264b 100644 --- a/pkg/gui/filetree/build_tree.go +++ b/pkg/gui/filetree/build_tree.go @@ -7,14 +7,14 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" ) -func BuildTreeFromFiles(files []*models.File) *Node[models.File] { +func BuildTreeFromFiles(files []*models.File, showRootItem bool) *Node[models.File] { root := &Node[models.File]{} childrenMapsByNode := make(map[*Node[models.File]]map[string]*Node[models.File]) var curr *Node[models.File] for _, file := range files { - splitPath := split("./" + file.Path) + splitPath := SplitFileTreePath(file.Path, showRootItem) curr = root outer: for i := range splitPath { @@ -63,19 +63,19 @@ func BuildTreeFromFiles(files []*models.File) *Node[models.File] { return root } -func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFile] { - rootAux := BuildTreeFromCommitFiles(files) +func BuildFlatTreeFromCommitFiles(files []*models.CommitFile, showRootItem bool) *Node[models.CommitFile] { + rootAux := BuildTreeFromCommitFiles(files, showRootItem) sortedFiles := rootAux.GetLeaves() return &Node[models.CommitFile]{Children: sortedFiles} } -func BuildTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFile] { +func BuildTreeFromCommitFiles(files []*models.CommitFile, showRootItem bool) *Node[models.CommitFile] { root := &Node[models.CommitFile]{} var curr *Node[models.CommitFile] for _, file := range files { - splitPath := split("./" + file.Path) + splitPath := SplitFileTreePath(file.Path, showRootItem) curr = root outer: for i := range splitPath { @@ -115,8 +115,8 @@ func BuildTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFil return root } -func BuildFlatTreeFromFiles(files []*models.File) *Node[models.File] { - rootAux := BuildTreeFromFiles(files) +func BuildFlatTreeFromFiles(files []*models.File, showRootItem bool) *Node[models.File] { + rootAux := BuildTreeFromFiles(files, showRootItem) sortedFiles := rootAux.GetLeaves() // from top down we have merge conflict files, then tracked file, then untracked @@ -160,3 +160,11 @@ func split(str string) []string { func join(strs []string) string { return strings.Join(strs, "/") } + +func SplitFileTreePath(path string, showRootItem bool) []string { + if showRootItem { + return split("./" + path) + } + + return split(path) +} diff --git a/pkg/gui/filetree/build_tree_test.go b/pkg/gui/filetree/build_tree_test.go index ca69a33cc..c3077783b 100644 --- a/pkg/gui/filetree/build_tree_test.go +++ b/pkg/gui/filetree/build_tree_test.go @@ -9,9 +9,10 @@ import ( func TestBuildTreeFromFiles(t *testing.T) { scenarios := []struct { - name string - files []*models.File - expected *Node[models.File] + name string + files []*models.File + showRootItem bool + expected *Node[models.File] }{ { name: "no files", @@ -31,6 +32,7 @@ func TestBuildTreeFromFiles(t *testing.T) { Path: "dir1/b", }, }, + showRootItem: true, expected: &Node[models.File]{ path: "", Children: []*Node[models.File]{ @@ -51,6 +53,37 @@ func TestBuildTreeFromFiles(t *testing.T) { }, }, }, + { + name: "files in same directory, not root item", + files: []*models.File{ + { + Path: "dir1/a", + }, + { + Path: "dir1/b", + }, + }, + showRootItem: false, + expected: &Node[models.File]{ + path: "", + Children: []*Node[models.File]{ + { + path: "dir1", + CompressionLevel: 0, + Children: []*Node[models.File]{ + { + File: &models.File{Path: "dir1/a"}, + path: "dir1/a", + }, + { + File: &models.File{Path: "dir1/b"}, + path: "dir1/b", + }, + }, + }, + }, + }, + }, { name: "paths that can be compressed", files: []*models.File{ @@ -61,6 +94,7 @@ func TestBuildTreeFromFiles(t *testing.T) { Path: "dir2/dir4/b", }, }, + showRootItem: true, expected: &Node[models.File]{ path: "", Children: []*Node[models.File]{ @@ -92,6 +126,43 @@ func TestBuildTreeFromFiles(t *testing.T) { }, }, }, + { + name: "paths that can be compressed, no root item", + files: []*models.File{ + { + Path: "dir1/dir3/a", + }, + { + Path: "dir2/dir4/b", + }, + }, + showRootItem: false, + expected: &Node[models.File]{ + path: "", + Children: []*Node[models.File]{ + { + path: "dir1/dir3", + Children: []*Node[models.File]{ + { + File: &models.File{Path: "dir1/dir3/a"}, + path: "dir1/dir3/a", + }, + }, + CompressionLevel: 1, + }, + { + path: "dir2/dir4", + Children: []*Node[models.File]{ + { + File: &models.File{Path: "dir2/dir4/b"}, + path: "dir2/dir4/b", + }, + }, + CompressionLevel: 1, + }, + }, + }, + }, { name: "paths that can be sorted", files: []*models.File{ @@ -102,6 +173,7 @@ func TestBuildTreeFromFiles(t *testing.T) { Path: "a", }, }, + showRootItem: true, expected: &Node[models.File]{ path: "", Children: []*Node[models.File]{ @@ -135,6 +207,7 @@ func TestBuildTreeFromFiles(t *testing.T) { Path: "a", }, }, + showRootItem: true, expected: &Node[models.File]{ path: "", Children: []*Node[models.File]{ @@ -164,7 +237,7 @@ func TestBuildTreeFromFiles(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - result := BuildTreeFromFiles(s.files) + result := BuildTreeFromFiles(s.files, s.showRootItem) assert.EqualValues(t, s.expected, result) }) } @@ -172,9 +245,10 @@ func TestBuildTreeFromFiles(t *testing.T) { func TestBuildFlatTreeFromFiles(t *testing.T) { scenarios := []struct { - name string - files []*models.File - expected *Node[models.File] + name string + files []*models.File + showRootItem bool + expected *Node[models.File] }{ { name: "no files", @@ -194,6 +268,7 @@ func TestBuildFlatTreeFromFiles(t *testing.T) { Path: "dir1/b", }, }, + showRootItem: true, expected: &Node[models.File]{ path: "", Children: []*Node[models.File]{ @@ -210,6 +285,33 @@ func TestBuildFlatTreeFromFiles(t *testing.T) { }, }, }, + { + name: "files in same directory, not root item", + files: []*models.File{ + { + Path: "dir1/a", + }, + { + Path: "dir1/b", + }, + }, + showRootItem: false, + expected: &Node[models.File]{ + path: "", + Children: []*Node[models.File]{ + { + File: &models.File{Path: "dir1/a"}, + path: "dir1/a", + CompressionLevel: 0, + }, + { + File: &models.File{Path: "dir1/b"}, + path: "dir1/b", + CompressionLevel: 0, + }, + }, + }, + }, { name: "paths that can be compressed", files: []*models.File{ @@ -220,6 +322,7 @@ func TestBuildFlatTreeFromFiles(t *testing.T) { Path: "dir2/b", }, }, + showRootItem: true, expected: &Node[models.File]{ path: "", Children: []*Node[models.File]{ @@ -236,6 +339,33 @@ func TestBuildFlatTreeFromFiles(t *testing.T) { }, }, }, + { + name: "paths that can be compressed, no root item", + files: []*models.File{ + { + Path: "dir1/a", + }, + { + Path: "dir2/b", + }, + }, + showRootItem: false, + expected: &Node[models.File]{ + path: "", + Children: []*Node[models.File]{ + { + File: &models.File{Path: "dir1/a"}, + path: "dir1/a", + CompressionLevel: 0, + }, + { + File: &models.File{Path: "dir2/b"}, + path: "dir2/b", + CompressionLevel: 0, + }, + }, + }, + }, { name: "paths that can be sorted", files: []*models.File{ @@ -246,6 +376,7 @@ func TestBuildFlatTreeFromFiles(t *testing.T) { Path: "a", }, }, + showRootItem: true, expected: &Node[models.File]{ path: "", Children: []*Node[models.File]{ @@ -288,6 +419,7 @@ func TestBuildFlatTreeFromFiles(t *testing.T) { Tracked: true, }, }, + showRootItem: true, expected: &Node[models.File]{ path: "", Children: []*Node[models.File]{ @@ -322,7 +454,7 @@ func TestBuildFlatTreeFromFiles(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - result := BuildFlatTreeFromFiles(s.files) + result := BuildFlatTreeFromFiles(s.files, s.showRootItem) assert.EqualValues(t, s.expected, result) }) } @@ -330,9 +462,10 @@ func TestBuildFlatTreeFromFiles(t *testing.T) { func TestBuildTreeFromCommitFiles(t *testing.T) { scenarios := []struct { - name string - files []*models.CommitFile - expected *Node[models.CommitFile] + name string + files []*models.CommitFile + showRootItem bool + expected *Node[models.CommitFile] }{ { name: "no files", @@ -352,6 +485,7 @@ func TestBuildTreeFromCommitFiles(t *testing.T) { Path: "dir1/b", }, }, + showRootItem: true, expected: &Node[models.CommitFile]{ path: "", Children: []*Node[models.CommitFile]{ @@ -372,6 +506,37 @@ func TestBuildTreeFromCommitFiles(t *testing.T) { }, }, }, + { + name: "files in same directory, not root item", + files: []*models.CommitFile{ + { + Path: "dir1/a", + }, + { + Path: "dir1/b", + }, + }, + showRootItem: false, + expected: &Node[models.CommitFile]{ + path: "", + Children: []*Node[models.CommitFile]{ + { + path: "dir1", + CompressionLevel: 0, + Children: []*Node[models.CommitFile]{ + { + File: &models.CommitFile{Path: "dir1/a"}, + path: "dir1/a", + }, + { + File: &models.CommitFile{Path: "dir1/b"}, + path: "dir1/b", + }, + }, + }, + }, + }, + }, { name: "paths that can be compressed", files: []*models.CommitFile{ @@ -382,6 +547,7 @@ func TestBuildTreeFromCommitFiles(t *testing.T) { Path: "dir2/dir4/b", }, }, + showRootItem: true, expected: &Node[models.CommitFile]{ path: "", Children: []*Node[models.CommitFile]{ @@ -413,6 +579,43 @@ func TestBuildTreeFromCommitFiles(t *testing.T) { }, }, }, + { + name: "paths that can be compressed, no root item", + files: []*models.CommitFile{ + { + Path: "dir1/dir3/a", + }, + { + Path: "dir2/dir4/b", + }, + }, + showRootItem: false, + expected: &Node[models.CommitFile]{ + path: "", + Children: []*Node[models.CommitFile]{ + { + path: "dir1/dir3", + Children: []*Node[models.CommitFile]{ + { + File: &models.CommitFile{Path: "dir1/dir3/a"}, + path: "dir1/dir3/a", + }, + }, + CompressionLevel: 1, + }, + { + path: "dir2/dir4", + Children: []*Node[models.CommitFile]{ + { + File: &models.CommitFile{Path: "dir2/dir4/b"}, + path: "dir2/dir4/b", + }, + }, + CompressionLevel: 1, + }, + }, + }, + }, { name: "paths that can be sorted", files: []*models.CommitFile{ @@ -423,6 +626,7 @@ func TestBuildTreeFromCommitFiles(t *testing.T) { Path: "a", }, }, + showRootItem: true, expected: &Node[models.CommitFile]{ path: "", Children: []*Node[models.CommitFile]{ @@ -446,7 +650,7 @@ func TestBuildTreeFromCommitFiles(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - result := BuildTreeFromCommitFiles(s.files) + result := BuildTreeFromCommitFiles(s.files, s.showRootItem) assert.EqualValues(t, s.expected, result) }) } @@ -454,9 +658,10 @@ func TestBuildTreeFromCommitFiles(t *testing.T) { func TestBuildFlatTreeFromCommitFiles(t *testing.T) { scenarios := []struct { - name string - files []*models.CommitFile - expected *Node[models.CommitFile] + name string + files []*models.CommitFile + showRootItem bool + expected *Node[models.CommitFile] }{ { name: "no files", @@ -476,6 +681,7 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) { Path: "dir1/b", }, }, + showRootItem: true, expected: &Node[models.CommitFile]{ path: "", Children: []*Node[models.CommitFile]{ @@ -492,6 +698,33 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) { }, }, }, + { + name: "files in same directory, not root item", + files: []*models.CommitFile{ + { + Path: "dir1/a", + }, + { + Path: "dir1/b", + }, + }, + showRootItem: false, + expected: &Node[models.CommitFile]{ + path: "", + Children: []*Node[models.CommitFile]{ + { + File: &models.CommitFile{Path: "dir1/a"}, + path: "dir1/a", + CompressionLevel: 0, + }, + { + File: &models.CommitFile{Path: "dir1/b"}, + path: "dir1/b", + CompressionLevel: 0, + }, + }, + }, + }, { name: "paths that can be compressed", files: []*models.CommitFile{ @@ -502,6 +735,7 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) { Path: "dir2/b", }, }, + showRootItem: true, expected: &Node[models.CommitFile]{ path: "", Children: []*Node[models.CommitFile]{ @@ -528,6 +762,7 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) { Path: "a", }, }, + showRootItem: true, expected: &Node[models.CommitFile]{ path: "", Children: []*Node[models.CommitFile]{ @@ -546,7 +781,7 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - result := BuildFlatTreeFromCommitFiles(s.files) + result := BuildFlatTreeFromCommitFiles(s.files, s.showRootItem) assert.EqualValues(t, s.expected, result) }) } diff --git a/pkg/gui/filetree/commit_file_tree.go b/pkg/gui/filetree/commit_file_tree.go index bf221a42e..7af83176f 100644 --- a/pkg/gui/filetree/commit_file_tree.go +++ b/pkg/gui/filetree/commit_file_tree.go @@ -94,10 +94,11 @@ func (self *CommitFileTree) GetAllFiles() []*models.CommitFile { } func (self *CommitFileTree) SetTree() { + showRootItem := self.common.UserConfig().Gui.ShowRootItemInFileTree if self.showTree { - self.tree = BuildTreeFromCommitFiles(self.getFiles()) + self.tree = BuildTreeFromCommitFiles(self.getFiles(), showRootItem) } else { - self.tree = BuildFlatTreeFromCommitFiles(self.getFiles()) + self.tree = BuildFlatTreeFromCommitFiles(self.getFiles(), showRootItem) } } diff --git a/pkg/gui/filetree/file_tree.go b/pkg/gui/filetree/file_tree.go index 8bc48c208..d0056a0a8 100644 --- a/pkg/gui/filetree/file_tree.go +++ b/pkg/gui/filetree/file_tree.go @@ -168,10 +168,11 @@ func (self *FileTree) GetAllFiles() []*models.File { func (self *FileTree) SetTree() { filesForDisplay := self.getFilesForDisplay() + showRootItem := self.common.UserConfig().Gui.ShowRootItemInFileTree if self.showTree { - self.tree = BuildTreeFromFiles(filesForDisplay) + self.tree = BuildTreeFromFiles(filesForDisplay, showRootItem) } else { - self.tree = BuildFlatTreeFromFiles(filesForDisplay) + self.tree = BuildFlatTreeFromFiles(filesForDisplay, showRootItem) } } diff --git a/pkg/gui/presentation/files.go b/pkg/gui/presentation/files.go index d1acd02da..64296aa8c 100644 --- a/pkg/gui/presentation/files.go +++ b/pkg/gui/presentation/files.go @@ -25,12 +25,13 @@ func RenderFileTree( showFileIcons bool, showNumstat bool, customIconsConfig *config.CustomIconsConfig, + showRootItem bool, ) []string { collapsedPaths := tree.CollapsedPaths() return renderAux(tree.GetRoot().Raw(), collapsedPaths, -1, -1, func(node *filetree.Node[models.File], treeDepth int, visualDepth int, isCollapsed bool) string { fileNode := filetree.NewFileNode(node) - return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showNumstat, showFileIcons, submoduleConfigs, node, customIconsConfig) + return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showNumstat, showFileIcons, submoduleConfigs, node, customIconsConfig, showRootItem) }) } @@ -120,8 +121,9 @@ func getFileLine( submoduleConfigs []*models.SubmoduleConfig, node *filetree.Node[models.File], customIconsConfig *config.CustomIconsConfig, + showRootItem bool, ) string { - name := fileNameAtDepth(node, treeDepth) + name := fileNameAtDepth(node, treeDepth, showRootItem) output := "" var nameColor style.TextStyle @@ -297,7 +299,7 @@ func getColorForChangeStatus(changeStatus string) style.TextStyle { } } -func fileNameAtDepth(node *filetree.Node[models.File], depth int) string { +func fileNameAtDepth(node *filetree.Node[models.File], depth int, showRootItem bool) string { splitName := split(node.GetInternalPath()) if depth == 0 && splitName[0] == "." { if len(splitName) == 1 { @@ -308,7 +310,7 @@ func fileNameAtDepth(node *filetree.Node[models.File], depth int) string { name := join(splitName[depth:]) if node.File != nil && node.File.IsRename() { - splitPrevName := split("./" + node.File.PreviousPath) + splitPrevName := filetree.SplitFileTreePath(node.File.PreviousPath, showRootItem) prevName := node.File.PreviousPath // if the file has just been renamed inside the same directory, we can shave off diff --git a/pkg/gui/presentation/files_test.go b/pkg/gui/presentation/files_test.go index f8e07c535..a5b01c156 100644 --- a/pkg/gui/presentation/files_test.go +++ b/pkg/gui/presentation/files_test.go @@ -26,6 +26,7 @@ func TestRenderFileTree(t *testing.T) { files []*models.File collapsedPaths []string showLineChanges bool + showRootItem bool expected []string }{ { @@ -38,7 +39,8 @@ func TestRenderFileTree(t *testing.T) { files: []*models.File{ {Path: "test", ShortStatus: " M", HasStagedChanges: true}, }, - expected: []string{" M test"}, + showRootItem: true, + expected: []string{" M test"}, }, { name: "numstat", @@ -49,6 +51,7 @@ func TestRenderFileTree(t *testing.T) { {Path: "test4", ShortStatus: " M", HasStagedChanges: true, LinesAdded: 0, LinesDeleted: 0}, }, showLineChanges: true, + showRootItem: true, expected: []string{ "▼ /", " M test +1 -1", @@ -67,6 +70,7 @@ func TestRenderFileTree(t *testing.T) { {Path: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true}, {Path: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, }, + showRootItem: true, expected: toStringSlice( ` ▼ / @@ -81,6 +85,30 @@ func TestRenderFileTree(t *testing.T) { ), collapsedPaths: []string{"./dir1"}, }, + { + name: "big example without root item", + files: []*models.File{ + {Path: "dir1/file2", ShortStatus: "M ", HasUnstagedChanges: true}, + {Path: "dir1/file3", ShortStatus: "M ", HasUnstagedChanges: true}, + {Path: "dir2/dir2/file3", ShortStatus: " M", HasStagedChanges: true}, + {Path: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, + {Path: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true}, + {Path: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, + }, + showRootItem: false, + expected: toStringSlice( + ` +▶ dir1 +▼ dir2 + ▼ dir2 + M file3 + M file4 + M file5 +M file1 +`, + ), + collapsedPaths: []string{"dir1"}, + }, } oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone) @@ -89,12 +117,13 @@ func TestRenderFileTree(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { common := common.NewDummyCommon() + common.UserConfig().Gui.ShowRootItemInFileTree = s.showRootItem viewModel := filetree.NewFileTree(func() []*models.File { return s.files }, common, true) viewModel.SetTree() for _, path := range s.collapsedPaths { viewModel.ToggleCollapsed(path) } - result := RenderFileTree(viewModel, nil, false, s.showLineChanges, &config.CustomIconsConfig{}) + result := RenderFileTree(viewModel, nil, false, s.showLineChanges, &config.CustomIconsConfig{}, s.showRootItem) assert.EqualValues(t, s.expected, result) }) } @@ -106,6 +135,7 @@ func TestRenderCommitFileTree(t *testing.T) { root *filetree.FileNode files []*models.CommitFile collapsedPaths []string + showRootItem bool expected []string }{ { @@ -118,7 +148,8 @@ func TestRenderCommitFileTree(t *testing.T) { files: []*models.CommitFile{ {Path: "test", ChangeStatus: "A"}, }, - expected: []string{"A test"}, + showRootItem: true, + expected: []string{"A test"}, }, { name: "big example", @@ -130,6 +161,7 @@ func TestRenderCommitFileTree(t *testing.T) { {Path: "dir2/file5", ChangeStatus: "M"}, {Path: "file1", ChangeStatus: "M"}, }, + showRootItem: true, expected: toStringSlice( ` ▼ / @@ -144,6 +176,30 @@ func TestRenderCommitFileTree(t *testing.T) { ), collapsedPaths: []string{"./dir1"}, }, + { + name: "big example without root item", + files: []*models.CommitFile{ + {Path: "dir1/file2", ChangeStatus: "M"}, + {Path: "dir1/file3", ChangeStatus: "A"}, + {Path: "dir2/dir2/file3", ChangeStatus: "D"}, + {Path: "dir2/dir2/file4", ChangeStatus: "M"}, + {Path: "dir2/file5", ChangeStatus: "M"}, + {Path: "file1", ChangeStatus: "M"}, + }, + showRootItem: false, + expected: toStringSlice( + ` +▶ dir1 +▼ dir2 + ▼ dir2 + D file3 + M file4 + M file5 +M file1 +`, + ), + collapsedPaths: []string{"dir1"}, + }, } oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone) @@ -154,6 +210,7 @@ func TestRenderCommitFileTree(t *testing.T) { hashPool := &utils.StringPool{} common := common.NewDummyCommon() + common.UserConfig().Gui.ShowRootItemInFileTree = s.showRootItem viewModel := filetree.NewCommitFileTreeViewModel(func() []*models.CommitFile { return s.files }, common, true) viewModel.SetRef(models.NewCommit(hashPool, models.NewCommitOpts{Hash: "1234"})) viewModel.SetTree() diff --git a/pkg/integration/tests/file/renamed_files_no_root_item.go b/pkg/integration/tests/file/renamed_files_no_root_item.go new file mode 100644 index 000000000..3ed8b2251 --- /dev/null +++ b/pkg/integration/tests/file/renamed_files_no_root_item.go @@ -0,0 +1,36 @@ +package file + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var RenamedFilesNoRootItem = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Regression test for the display of renamed files in the file tree, when the root item is disabled", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Gui.ShowRootItemInFileTree = false + }, + SetupRepo: func(shell *Shell) { + shell.CreateDir("dir") + shell.CreateDir("dir/nested") + shell.CreateFileAndAdd("file1", "file1 content\n") + shell.CreateFileAndAdd("dir/file2", "file2 content\n") + shell.CreateFileAndAdd("dir/nested/file3", "file3 content\n") + shell.Commit("initial commit") + shell.RunCommand([]string{"git", "mv", "file1", "dir/file1"}) + shell.RunCommand([]string{"git", "mv", "dir/file2", "dir/file2-renamed"}) + shell.RunCommand([]string{"git", "mv", "dir/nested/file3", "file3"}) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Equals("▼ dir"), + Equals(" R file1 → file1"), + Equals(" R file2 → file2-renamed"), + Equals("R dir/nested/file3 → file3"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 23a05fa5c..9470858a1 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -207,6 +207,7 @@ var tests = []*components.IntegrationTest{ file.RememberCommitMessageAfterFail, file.RenameSimilarityThresholdChange, file.RenamedFiles, + file.RenamedFilesNoRootItem, file.StageChildrenRangeSelect, file.StageDeletedRangeSelect, file.StageRangeSelect, diff --git a/schema/config.json b/schema/config.json index e88723f50..9560c3ab4 100644 --- a/schema/config.json +++ b/schema/config.json @@ -569,6 +569,11 @@ "description": "If true, display the files in the file views as a tree. If false, display the files as a flat list.\nThis can be toggled from within Lazygit with the '`' key, but that will not change the default.", "default": true }, + "showRootItemInFileTree": { + "type": "boolean", + "description": "If true, add a \"/\" root item in the file tree representing the root of the repository. It is only added when necessary, i.e. when there is more than one item at top level.", + "default": true + }, "showNumstatInFilesView": { "type": "boolean", "description": "If true, show the number of lines changed per file in the Files view",