From 9e85d37fb949bbc83f28cb079f2ac4b45ae895ce Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 31 Mar 2021 23:26:53 +1100 Subject: refactor to no longer call these things file changes --- pkg/commands/files.go | 10 +- pkg/commands/models/file.go | 2 +- pkg/gui/commit_files_panel.go | 28 ++-- pkg/gui/discard_changes_menu_panel.go | 2 +- pkg/gui/files_panel.go | 54 ++++---- pkg/gui/filetree/build_tree.go | 24 ++-- pkg/gui/filetree/commit_file_change_manager.go | 114 ---------------- pkg/gui/filetree/commit_file_change_node.go | 162 ---------------------- pkg/gui/filetree/commit_file_manager.go | 114 ++++++++++++++++ pkg/gui/filetree/commit_file_node.go | 162 ++++++++++++++++++++++ pkg/gui/filetree/file_change_manager.go | 88 ------------ pkg/gui/filetree/file_change_manager_test.go | 91 ------------- pkg/gui/filetree/file_change_node.go | 178 ------------------------- pkg/gui/filetree/file_change_node_test.go | 125 ----------------- pkg/gui/filetree/file_manager.go | 88 ++++++++++++ pkg/gui/filetree/file_manager_test.go | 91 +++++++++++++ pkg/gui/filetree/file_node.go | 178 +++++++++++++++++++++++++ pkg/gui/filetree/file_node_test.go | 125 +++++++++++++++++ pkg/gui/filtering_menu_panel.go | 2 +- pkg/gui/gui.go | 24 ++-- pkg/gui/line_by_line_panel.go | 2 +- pkg/gui/list_context.go | 12 +- pkg/gui/patch_building_panel.go | 2 +- pkg/gui/submodules_panel.go | 2 +- 24 files changed, 839 insertions(+), 841 deletions(-) delete mode 100644 pkg/gui/filetree/commit_file_change_manager.go delete mode 100644 pkg/gui/filetree/commit_file_change_node.go create mode 100644 pkg/gui/filetree/commit_file_manager.go create mode 100644 pkg/gui/filetree/commit_file_node.go delete mode 100644 pkg/gui/filetree/file_change_manager.go delete mode 100644 pkg/gui/filetree/file_change_manager_test.go delete mode 100644 pkg/gui/filetree/file_change_node.go delete mode 100644 pkg/gui/filetree/file_change_node_test.go create mode 100644 pkg/gui/filetree/file_manager.go create mode 100644 pkg/gui/filetree/file_manager_test.go create mode 100644 pkg/gui/filetree/file_node.go create mode 100644 pkg/gui/filetree/file_node_test.go diff --git a/pkg/commands/files.go b/pkg/commands/files.go index 5443b7d04..3f6d1c377 100644 --- a/pkg/commands/files.go +++ b/pkg/commands/files.go @@ -138,12 +138,12 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error { return c.DiscardUnstagedFileChanges(file) } -func (c *GitCommand) DiscardAllDirChanges(node *filetree.FileChangeNode) error { +func (c *GitCommand) DiscardAllDirChanges(node *filetree.FileNode) error { // this could be more efficient but we would need to handle all the edge cases return node.ForEachFile(c.DiscardAllFileChanges) } -func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileChangeNode) error { +func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error { if err := c.RemoveUntrackedDirFiles(node); err != nil { return err } @@ -156,9 +156,9 @@ func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileChangeNode) er return nil } -func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileChangeNode) error { +func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error { untrackedFilePaths := node.GetPathsMatching( - func(n *filetree.FileChangeNode) bool { return n.File != nil && !n.File.GetIsTracked() }, + func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() }, ) for _, path := range untrackedFilePaths { @@ -189,7 +189,7 @@ func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool return s } -func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFileChange, plain bool, cached bool) string { +func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool) string { cachedArg := "" trackedArg := "--" colorArg := c.colorArg() diff --git a/pkg/commands/models/file.go b/pkg/commands/models/file.go index a02beae63..0bbca78ae 100644 --- a/pkg/commands/models/file.go +++ b/pkg/commands/models/file.go @@ -22,7 +22,7 @@ type File struct { } // sometimes we need to deal with either a node (which contains a file) or an actual file -type IFileChange interface { +type IFile interface { GetHasUnstagedChanges() bool GetHasStagedChanges() bool GetIsTracked() bool diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go index aa95b0862..25e999c87 100644 --- a/pkg/gui/commit_files_panel.go +++ b/pkg/gui/commit_files_panel.go @@ -7,17 +7,15 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/filetree" ) -// todo: rename to getSelectedCommitFileChangeNode, or decide to remove the change part in the context of files -func (gui *Gui) getSelectedCommitFileNode() *filetree.CommitFileChangeNode { +func (gui *Gui) getSelectedCommitFileNode() *filetree.CommitFileNode { selectedLine := gui.State.Panels.CommitFiles.SelectedLineIdx - if selectedLine == -1 || selectedLine > gui.State.CommitFileChangeManager.GetItemsLength()-1 { + if selectedLine == -1 || selectedLine > gui.State.CommitFileManager.GetItemsLength()-1 { return nil } - return gui.State.CommitFileChangeManager.GetItemAtIndex(selectedLine) + return gui.State.CommitFileManager.GetItemAtIndex(selectedLine) } -// todo: rename to getSelectedCommitFileChange func (gui *Gui) getSelectedCommitFile() *models.CommitFile { node := gui.getSelectedCommitFileNode() if node == nil { @@ -42,7 +40,7 @@ func (gui *Gui) handleCommitFileSelect() error { return nil } - to := gui.State.CommitFileChangeManager.GetParent() + to := gui.State.CommitFileManager.GetParent() from, reverse := gui.getFromAndReverseArgsForDiff(to) cmd := gui.OSCommand.ExecutableFromString( @@ -66,7 +64,7 @@ func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error { } // TODO: verify this works for directories - if err := gui.GitCommand.CheckoutFile(gui.State.CommitFileChangeManager.GetParent(), node.GetPath()); err != nil { + if err := gui.GitCommand.CheckoutFile(gui.State.CommitFileManager.GetParent(), node.GetPath()); err != nil { return gui.surfaceError(err) } @@ -109,7 +107,7 @@ func (gui *Gui) refreshCommitFilesView() error { if err != nil { return gui.surfaceError(err) } - gui.State.CommitFileChangeManager.SetFiles(files, to) + gui.State.CommitFileManager.SetFiles(files, to) return gui.postRefreshUpdate(gui.Contexts.CommitFiles.Context) } @@ -152,7 +150,7 @@ func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error { // if there is any file that hasn't been fully added we'll fully add everything, // otherwise we'll remove everything adding := node.AnyFile(func(file *models.CommitFile) bool { - return gui.GitCommand.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileChangeManager.GetParent()) != patch.WHOLE + return gui.GitCommand.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileManager.GetParent()) != patch.WHOLE }) err := node.ForEachFile(func(file *models.CommitFile) error { @@ -174,7 +172,7 @@ func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error { return gui.postRefreshUpdate(gui.Contexts.CommitFiles.Context) } - if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileChangeManager.GetParent() { + if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() { return gui.ask(askOpts{ title: gui.Tr.DiscardPatch, prompt: gui.Tr.DiscardPatchConfirm, @@ -225,7 +223,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error { return gui.handleRefreshPatchBuildingPanel(selectedLineIdx) } - if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileChangeManager.GetParent() { + if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() { return gui.ask(askOpts{ title: gui.Tr.DiscardPatch, prompt: gui.Tr.DiscardPatchConfirm, @@ -249,7 +247,7 @@ func (gui *Gui) handleToggleCommitFileDirCollapsed() error { return nil } - gui.State.CommitFileChangeManager.ToggleCollapsed(node.GetPath()) + gui.State.CommitFileManager.ToggleCollapsed(node.GetPath()) if err := gui.postRefreshUpdate(gui.Contexts.CommitFiles.Context); err != nil { gui.Log.Error(err) @@ -280,12 +278,12 @@ func (gui *Gui) switchToCommitFilesContext(refName string, canRebase bool, conte func (gui *Gui) handleToggleCommitFileTreeView() error { path := gui.getSelectedCommitFilePath() - gui.State.CommitFileChangeManager.ToggleShowTree() + gui.State.CommitFileManager.ToggleShowTree() // find that same node in the new format and move the cursor to it if path != "" { - gui.State.CommitFileChangeManager.ExpandToPath(path) - index, found := gui.State.CommitFileChangeManager.GetIndexForPath(path) + gui.State.CommitFileManager.ExpandToPath(path) + index, found := gui.State.CommitFileManager.GetIndexForPath(path) if found { gui.commitFilesListContext().GetPanelState().SetSelectedLineIdx(index) } diff --git a/pkg/gui/discard_changes_menu_panel.go b/pkg/gui/discard_changes_menu_panel.go index 9499080e2..daa2d76d8 100644 --- a/pkg/gui/discard_changes_menu_panel.go +++ b/pkg/gui/discard_changes_menu_panel.go @@ -1,7 +1,7 @@ package gui func (gui *Gui) handleCreateDiscardMenu() error { - node := gui.getSelectedFileChangeNode() + node := gui.getSelectedFileNode() if node == nil { return nil } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index c244c46e8..c6fcb07d9 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -22,17 +22,17 @@ import ( // list panel functions -func (gui *Gui) getSelectedFileChangeNode() *filetree.FileChangeNode { +func (gui *Gui) getSelectedFileNode() *filetree.FileNode { selectedLine := gui.State.Panels.Files.SelectedLineIdx if selectedLine == -1 { return nil } - return gui.State.FileChangeManager.GetItemAtIndex(selectedLine) + return gui.State.FileManager.GetItemAtIndex(selectedLine) } func (gui *Gui) getSelectedFile() *models.File { - node := gui.getSelectedFileChangeNode() + node := gui.getSelectedFileNode() if node == nil { return nil } @@ -40,7 +40,7 @@ func (gui *Gui) getSelectedFile() *models.File { } func (gui *Gui) getSelectedPath() string { - node := gui.getSelectedFileChangeNode() + node := gui.getSelectedFileNode() if node == nil { return "" } @@ -51,7 +51,7 @@ func (gui *Gui) getSelectedPath() string { func (gui *Gui) selectFile(alreadySelected bool) error { gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx) - node := gui.getSelectedFileChangeNode() + node := gui.getSelectedFileNode() if node == nil { return gui.refreshMainViews(refreshMainOpts{ @@ -152,7 +152,7 @@ func (gui *Gui) refreshFilesAndSubmodules() error { // specific functions func (gui *Gui) stagedFiles() []*models.File { - files := gui.State.FileChangeManager.GetAllFiles() + files := gui.State.FileManager.GetAllFiles() result := make([]*models.File, 0) for _, file := range files { if file.HasStagedChanges { @@ -163,7 +163,7 @@ func (gui *Gui) stagedFiles() []*models.File { } func (gui *Gui) trackedFiles() []*models.File { - files := gui.State.FileChangeManager.GetAllFiles() + files := gui.State.FileManager.GetAllFiles() result := make([]*models.File, 0, len(files)) for _, file := range files { if file.Tracked { @@ -187,7 +187,7 @@ func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error { - node := gui.getSelectedFileChangeNode() + node := gui.getSelectedFileNode() if node == nil { return nil } @@ -216,7 +216,7 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error } func (gui *Gui) handleFilePress() error { - node := gui.getSelectedFileChangeNode() + node := gui.getSelectedFileNode() if node == nil { return nil } @@ -264,7 +264,7 @@ func (gui *Gui) handleFilePress() error { } func (gui *Gui) allFilesStaged() bool { - for _, file := range gui.State.FileChangeManager.GetAllFiles() { + for _, file := range gui.State.FileManager.GetAllFiles() { if file.HasUnstagedChanges { return false } @@ -295,7 +295,7 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleIgnoreFile() error { - node := gui.getSelectedFileChangeNode() + node := gui.getSelectedFileNode() if node == nil { return nil } @@ -500,7 +500,7 @@ func (gui *Gui) editFile(filename string) error { } func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error { - node := gui.getSelectedFileChangeNode() + node := gui.getSelectedFileNode() if node == nil { return nil } @@ -513,7 +513,7 @@ func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error { - node := gui.getSelectedFileChangeNode() + node := gui.getSelectedFileNode() if node == nil { return nil } @@ -530,9 +530,9 @@ func (gui *Gui) refreshStateFiles() error { // when we refresh, go looking for a matching name // move the cursor to there. - selectedNode := gui.getSelectedFileChangeNode() + selectedNode := gui.getSelectedFileNode() - prevNodes := gui.State.FileChangeManager.GetAllItems() + prevNodes := gui.State.FileManager.GetAllItems() prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{}) @@ -540,24 +540,24 @@ func (gui *Gui) refreshStateFiles() error { // for when you stage the old file of a rename and the new file is in a collapsed dir for _, file := range files { if selectedNode != nil && selectedNode.Path != "" && file.PreviousName == selectedNode.Path { - gui.State.FileChangeManager.ExpandToPath(file.Name) + gui.State.FileManager.ExpandToPath(file.Name) } } - gui.State.FileChangeManager.SetFiles(files) + gui.State.FileManager.SetFiles(files) if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil { return err } if selectedNode != nil { - newIdx := gui.findNewSelectedIdx(prevNodes[prevSelectedLineIdx:], gui.State.FileChangeManager.GetAllItems()) + newIdx := gui.findNewSelectedIdx(prevNodes[prevSelectedLineIdx:], gui.State.FileManager.GetAllItems()) if newIdx != -1 && newIdx != prevSelectedLineIdx { gui.State.Panels.Files.SelectedLineIdx = newIdx } } - gui.refreshSelectedLine(gui.State.Panels.Files, gui.State.FileChangeManager.GetItemsLength()) + gui.refreshSelectedLine(gui.State.Panels.Files, gui.State.FileManager.GetItemsLength()) return nil } @@ -568,8 +568,8 @@ func (gui *Gui) refreshStateFiles() error { // nodes until we find one that exists in the new set of nodes, then move the cursor // to that. // prevNodes starts from our previously selected node because we don't need to consider anything above that -func (gui *Gui) findNewSelectedIdx(prevNodes []*filetree.FileChangeNode, currNodes []*filetree.FileChangeNode) int { - getPaths := func(node *filetree.FileChangeNode) []string { +func (gui *Gui) findNewSelectedIdx(prevNodes []*filetree.FileNode, currNodes []*filetree.FileNode) int { + getPaths := func(node *filetree.FileNode) []string { if node == nil { return nil } @@ -788,7 +788,7 @@ func (gui *Gui) openFile(filename string) error { } func (gui *Gui) anyFilesWithMergeConflicts() bool { - for _, file := range gui.State.FileChangeManager.GetAllFiles() { + for _, file := range gui.State.FileManager.GetAllFiles() { if file.HasMergeConflicts { return true } @@ -834,12 +834,12 @@ func (gui *Gui) handleCreateResetToUpstreamMenu(g *gocui.Gui, v *gocui.View) err } func (gui *Gui) handleToggleDirCollapsed() error { - node := gui.getSelectedFileChangeNode() + node := gui.getSelectedFileNode() if node == nil { return nil } - gui.State.FileChangeManager.ToggleCollapsed(node.GetPath()) + gui.State.FileManager.ToggleCollapsed(node.GetPath()) if err := gui.postRefreshUpdate(gui.Contexts.Files.Context); err != nil { gui.Log.Error(err) @@ -852,12 +852,12 @@ func (gui *Gui) handleToggleFileTreeView() error { // get path of currently selected file path := gui.getSelectedPath() - gui.State.FileChangeManager.ToggleShowTree() + gui.State.FileManager.ToggleShowTree() // find that same node in the new format and move the cursor to it if path != "" { - gui.State.FileChangeManager.ExpandToPath(path) - index, found := gui.State.FileChangeManager.GetIndexForPath(path) + gui.State.FileManager.ExpandToPath(path) + index, found := gui.State.FileManager.GetIndexForPath(path) if found { gui.filesListContext().GetPanelState().SetSelectedLineIdx(index) } diff --git a/pkg/gui/filetree/build_tree.go b/pkg/gui/filetree/build_tree.go index d6fe9b98d..ab7ac40a4 100644 --- a/pkg/gui/filetree/build_tree.go +++ b/pkg/gui/filetree/build_tree.go @@ -9,10 +9,10 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" ) -func BuildTreeFromFiles(files []*models.File) *FileChangeNode { - root := &FileChangeNode{} +func BuildTreeFromFiles(files []*models.File) *FileNode { + root := &FileNode{} - var curr *FileChangeNode + var curr *FileNode for _, file := range files { split := strings.Split(file.Name, string(os.PathSeparator)) curr = root @@ -33,7 +33,7 @@ func BuildTreeFromFiles(files []*models.File) *FileChangeNode { } } - newChild := &FileChangeNode{ + newChild := &FileNode{ Path: path, File: setFile, } @@ -49,17 +49,17 @@ func BuildTreeFromFiles(files []*models.File) *FileChangeNode { return root } -func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *CommitFileChangeNode { +func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode { rootAux := BuildTreeFromCommitFiles(files) sortedFiles := rootAux.GetLeaves() - return &CommitFileChangeNode{Children: sortedFiles} + return &CommitFileNode{Children: sortedFiles} } -func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileChangeNode { - root := &CommitFileChangeNode{} +func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode { + root := &CommitFileNode{} - var curr *CommitFileChangeNode + var curr *CommitFileNode for _, file := range files { split := strings.Split(file.Name, string(os.PathSeparator)) curr = root @@ -80,7 +80,7 @@ func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileChangeNode } } - newChild := &CommitFileChangeNode{ + newChild := &CommitFileNode{ Path: path, File: setFile, } @@ -96,7 +96,7 @@ func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileChangeNode return root } -func BuildFlatTreeFromFiles(files []*models.File) *FileChangeNode { +func BuildFlatTreeFromFiles(files []*models.File) *FileNode { rootAux := BuildTreeFromFiles(files) sortedFiles := rootAux.GetLeaves() @@ -106,5 +106,5 @@ func BuildFlatTreeFromFiles(files []*models.File) *FileChangeNode { return sortedFiles[i].File != nil && sortedFiles[i].File.HasMergeConflicts && !(sortedFiles[j].File != nil && sortedFiles[j].File.HasMergeConflicts) }) - return &FileChangeNode{Children: sortedFiles} + return &FileNode{Children: sortedFiles} } diff --git a/pkg/gui/filetree/commit_file_change_manager.go b/pkg/gui/filetree/commit_file_change_manager.go deleted file mode 100644 index ee05ca3b9..000000000 --- a/pkg/gui/filetree/commit_file_change_manager.go +++ /dev/null @@ -1,114 +0,0 @@ -package filetree - -import ( - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/commands/patch" - "github.com/jesseduffield/lazygit/pkg/gui/presentation" - "github.com/sirupsen/logrus" -) - -type CommitFileChangeManager struct { - files []*models.CommitFile - tree *CommitFileChangeNode - showTree bool - log *logrus.Entry - collapsedPaths CollapsedPaths - // parent is the identifier of the parent object e.g. a commit SHA if this commit file is for a commit, or a stash entry ref like 'stash@{1}' - parent string -} - -func (m *CommitFileChangeManager) GetParent() string { - return m.parent -} - -func NewCommitFileChangeManager(files []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileChangeManager { - return &CommitFileChangeManager{ - files: files, - log: log, - showTree: showTree, - collapsedPaths: CollapsedPaths{}, - } -} - -func (m *CommitFileChangeManager) ExpandToPath(path string) { - m.collapsedPaths.ExpandToPath(path) -} - -func (m *CommitFileChangeManager) ToggleShowTree() { - m.showTree = !m.showTree - m.SetTree() -} - -func (m *CommitFileChangeManager) GetItemAtIndex(index int) *CommitFileChangeNode { - // need to traverse the three depth first until we get to the index. - return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root -} - -func (m *CommitFileChangeManager) GetIndexForPath(path string) (int, bool) { - index, found := m.tree.GetIndexForPath(path, m.collapsedPaths) - return index - 1, found -} - -func (m *CommitFileChangeManager) GetAllItems() []*CommitFileChangeNode { - if m.tree == nil { - return nil - } - - return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root -} - -func (m *CommitFileChangeManager) GetItemsLength() int { - return m.tree.Size(m.collapsedPaths) - 1 // ignoring root -} - -func (m *CommitFileChangeManager) GetAllFiles() []*models.CommitFile { - return m.files -} - -func (m *CommitFileChangeManager) SetFiles(files []*models.CommitFile, parent string) { - m.files = files - m.parent = parent - - m.SetTree() -} - -func (m *CommitFileChangeManager) SetTree() { - if m.showTree { - m.tree = BuildTreeFromCommitFiles(m.files) - } else { - m.tree = BuildFlatTreeFromCommitFiles(m.files) - } -} - -func (m *CommitFileChangeManager) IsCollapsed(path string) bool { - return m.collapsedPaths.IsCollapsed(path) -} - -func (m *CommitFileChangeManager) ToggleCollapsed(path string) { - m.collapsedPaths.ToggleCollapsed(path) -} - -func (m *CommitFileChangeManager) Render(diffName string, patchManager *patch.PatchManager) []string { - return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string { - castN := n.(*CommitFileChangeNode) - - // This is a little convoluted because we're dealing with either a leaf or a non-leaf. - // But this code actually applies to both. If it's a leaf, the status will just - // be whatever status it is, but if it's a non-leaf it will determine its status - // based on the leaves of that subtree - var status patch.PatchStatus - if castN.EveryFile(func(file *models.CommitFile) bool { - return patchManager.GetFileStatus(file.Name, m.parent) == patch.WHOLE - }) { - status = patch.WHOLE - } else if castN.EveryFile(func(file *models.CommitFile) bool { - return patchManager.GetFileStatus(file.Name, m.parent) == patch.UNSELECTED - }) { - status = patch.UNSELECTED - } else { - status = patch.PART - } - - return presentation.GetCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status) - }) -} diff --git a/pkg/gui/filetree/commit_file_change_node.go b/pkg/gui/filetree/commit_file_change_node.go deleted file mode 100644 index 011c1d954..000000000 --- a/pkg/gui/filetree/commit_file_change_node.go +++ /dev/null @@ -1,162 +0,0 @@ -package filetree - -import ( - "os" - "path/filepath" - "strings" - - "github.com/jesseduffield/lazygit/pkg/commands/models" -) - -type CommitFileChangeNode struct { - Children []*CommitFileChangeNode - File *models.CommitFile - Path string // e.g. '/path/to/mydir' - CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode -} - -// methods satisfying ListItem interface - -func (s *CommitFileChangeNode) ID() string { - return s.GetPath() -} - -func (s *CommitFileChangeNode) Description() string { - return s.GetPath() -} - -// methods satisfying INode interface - -func (s *CommitFileChangeNode) IsLeaf() bool { - return s.File != nil -} - -func (s *CommitFileChangeNode) GetPath() string { - return s.Path -} - -func (s *CommitFileChangeNode) GetChildren() []INode { - result := make([]INode, len(s.Children)) - for i, child := range s.Children { - result[i] = child - } - - return result -} - -func (s *CommitFileChangeNode) SetChildren(children []INode) { - castChildren := make([]*CommitFileChangeNode, len(children)) - for i, child := range children { - castChildren[i] = child.(*CommitFileChangeNode) - } - - s.Children = castChildren -} - -func (s *CommitFileChangeNode) GetCompressionLevel() int { - return s.CompressionLevel -} - -func (s *CommitFileChangeNode) SetCompressionLevel(level int) { - s.CompressionLevel = level -} - -// methods utilising generic functions for INodes - -func (s *CommitFileChangeNode) Sort() { - sortNode(s) -} - -func (s *CommitFileChangeNode) ForEachFile(cb func(*models.CommitFile) error) error { - return forEachLeaf(s, func(n INode) error { - castNode := n.(*CommitFileChangeNode) - return cb(castNode.File) - }) -} - -func (s *CommitFileChangeNode) Any(test func(node *CommitFileChangeNode) bool) bool { - return any(s, func(n INode) bool { - castNode := n.(*CommitFileChangeNode) - return test(castNode) - }) -} - -func (s *CommitFileChangeNode) Every(test func(node *CommitFileChangeNode) bool) bool { - return every(s, func(n INode) bool { - castNode := n.(*CommitFileChangeNode) - return test(castNode) - }) -} - -func (s *CommitFileChangeNode) EveryFile(test func(file *models.CommitFile) bool) bool { - return every(s, func(n INode) bool { - castNode := n.(*CommitFileChangeNode) - - return castNode.File == nil || test(castNode.File) - }) -} - -func (n *CommitFileChangeNode) Flatten(collapsedPaths map[string]bool) []*CommitFileChangeNode { - results := flatten(n, collapsedPaths) - nodes := make([]*CommitFileChangeNode, len(results)) - for i, result := range results { - nodes[i] = result.(*CommitFileChangeNode) - } - - return nodes -} - -func (node *CommitFileChangeNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *CommitFileChangeNode { - return getNodeAtIndex(node, index, collapsedPaths).(*CommitFileChangeNode) -} - -func (node *CommitFileChangeNode) GetIndexForPath(path string, collapsedPaths map[string]bool) (int, bool) { - return getIndexForPath(node, path, collapsedPaths) -} - -func (node *CommitFileChangeNode) Size(collapsedPaths map[string]bool) int { - return size(node, collapsedPaths) -} - -func (s *CommitFileChangeNode) Compress() { - // with these functions I try to only have type conversion code on the actual struct, - // but comparing interface values to nil is fraught with danger so I'm duplicating - // that code here. - if s == nil { - return - } - - compressAux(s) -} - -// This ignores the root -func (node *CommitFileChangeNode) GetPathsMatching(test func(*CommitFileChangeNode) bool) []string { - return getPathsMatching(node, func(n INode) bool { - return test(n.(*CommitFileChangeNode)) - }) -} - -func (s *CommitFileChangeNode) GetLeaves() []*CommitFileChangeNode { - leaves := getLeaves(s) - castLeaves := make([]*CommitFileChangeNode, len(leaves)) - for i := range leaves { - castLeaves[i] = leaves[i].(*CommitFileChangeNode) - } - - return castLeaves -} - -// extra methods - -func (s *CommitFileChangeNode) AnyFile(test func(file *models.CommitFile) bool) bool { - return s.Any(func(node *CommitFileChangeNode) bool { - return node.IsLeaf() && test(node.File) - }) -} - -func (s *CommitFileChangeNode) NameAtDepth(depth int) string { - splitName := strings.Split(s.Path, string(os.PathSeparator)) - name := filepath.Join(splitName[depth:]...) - - return name -} diff --git a/pkg/gui/filetree/commit_file_manager.go b/pkg/gui/filetree/commit_file_manager.go new file mode 100644 index 000000000..3fd89b63e --- /dev/null +++ b/pkg/gui/filetree/commit_file_manager.go @@ -0,0 +1,114 @@ +package filetree + +import ( + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/patch" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" + "github.com/sirupsen/logrus" +) + +type CommitFileManager struct { + files []*models.CommitFile + tree *CommitFileNode + showTree bool + log *logrus.Entry + collapsedPaths CollapsedPaths + // parent is the identifier of the parent object e.g. a commit SHA if this commit file is for a commit, or a stash entry ref like 'stash@{1}' + parent string +} + +func (m *CommitFileManager) GetParent() string { + return m.parent +} + +func NewCommitFileManager(files []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileManager { + return &CommitFileManager{ + files: files, + log: log, + showTree: showTree, + collapsedPaths: CollapsedPaths{}, + } +} + +func (m *CommitFileManager) ExpandToPath(path string) { + m.collapsedPaths.ExpandToPath(path) +} + +func (m *CommitFileManager) ToggleShowTree() { + m.showTree = !m.showTree + m.SetTree() +} + +func (m *CommitFileManager) GetItemAtIndex(index int) *CommitFileNode { + // need to traverse the three depth first until we get to the index. + return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root +} + +func (m *CommitFileManager) GetIndexForPath(path string) (int, bool) { + index, found := m.tree.GetIndexForPath(path, m.collapsedPaths) + return index - 1, found +} + +func (m *CommitFileManager) GetAllItems() []*CommitFileNode { + if m.tree == nil { + return nil + } + + return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root +} + +func (m *CommitFileManager) GetItemsLength() int { + return m.tree.Size(m.collapsedPaths) - 1 // ignoring root +} + +func (m *CommitFileManager) GetAllFiles() []*models.CommitFile { + return m.files +} + +func (m *CommitFileManager) SetFiles(files []*models.CommitFile, parent string) { + m.files = files + m.parent = parent + + m.SetTree() +} + +func (m *CommitFileManager) SetTree() { + if m.showTree { + m.tree = BuildTreeFromCommitFiles(m.files) + } else { + m.tree = BuildFlatTreeFromCommitFiles(m.files) + } +} + +func (m *CommitFileManager) IsCollapsed(path string) bool { + return m.collapsedPaths.IsCollapsed(path) +} + +func (m *CommitFileManager) ToggleCollapsed(path string) { + m.collapsedPaths.ToggleCollapsed(path) +} + +func (m *CommitFileManager) Render(diffName string, patchManager *patch.PatchManager) []string { + return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string { + castN := n.(*CommitFileNode) + + // This is a little convoluted because we're dealing with either a leaf or a non-leaf. + // But this code actually applies to both. If it's a leaf, the status will just + // be whatever status it is, but if it's a non-leaf it will determine its status + // based on the leaves of that subtree + var status patch.PatchStatus + if castN.EveryFile(func(file *models.CommitFile) bool { + return patchManager.GetFileStatus(file.Name, m.parent) == patch.WHOLE + }) { + status = patch.WHOLE + } else if castN.EveryFile(func(file *models.CommitFile) bool { + return patchManager.GetFileStatus(file.Name, m.parent) == patch.UNSELECTED + }) { + status = patch.UNSELECTED + } else { + status = patch.PART + } + + return presentation.GetCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status) + }) +} diff --git a/pkg/gui/filetree/commit_file_node.go b/pkg/gui/filetree/commit_file_node.go new file mode 100644 index 000000000..1f1be2a3f --- /dev/null +++ b/pkg/gui/filetree/commit_file_node.go @@ -0,0 +1,162 @@ +package filetree + +import ( + "os" + "path/filepath" + "strings" + + "github.com/jesseduffield/lazygit/pkg/commands/models" +) + +type CommitFileNode struct { + Children []*CommitFileNode + File *models.CommitFile + Path string // e.g. '/path/to/mydir' + CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode +} + +// methods satisfying ListItem interface + +func (s *CommitFileNode) ID() string { + return s.GetPath() +} + +func (s *CommitFileNode) Description() string { + return s.GetPath() +} + +// methods satisfying INode interface + +func (s *CommitFileNode) IsLeaf() bool { + return s.File != nil +} + +func (s *CommitFileNode) GetPath() string { + return s.Path +} + +func (s *CommitFileNode) GetChildren() []INode { + result := make([]INode, len(s.Children)) + for i, child := range s.Children { + result[i] = child + } + + return result +} + +func (s *CommitFileNode) SetChildren(children []INode) { + castChildren := make([]*CommitFileNode, len(children)) + for i, child := range children { + castChildren[i] = child.(*CommitFileNode) + } + + s.Children = castChildren +} + +func (s *CommitFileNode) GetCompressionLevel() int { + return s.CompressionLevel +} + +func (s *CommitFileNode) SetCompressionLevel(level int) { + s.CompressionLevel = level +} + +// methods utilising generic functions for INodes + +func (s *CommitFileNode) Sort() { + sortNode(s) +} + +func (s *CommitFileNode) ForEachFile(cb func(*models.CommitFile) error) error { + return forEachLeaf(s, func(n INode) error { + castNode := n.(*CommitFileNode) + return cb(castNode.File) + }) +} + +func (s *CommitFileNode) Any(test func(node *CommitFileNode) bool) bool { + return any(s, func(n INode) bool { + castNode := n.(*CommitFileNode) + return test(castNode) + }) +} + +func (s *CommitFileNode) Every(test func(node *CommitFileNode) bool) bool { + return every(s, func(n INode) bool { + castNode := n.(*CommitFileNode) + return test(castNode) + }) +} + +func (s *CommitFileNode) EveryFile(test func(file *models.CommitFile) bool) bool { + return every(s, func(n INode) bool { + castNode := n.(*CommitFileNode) + + return castNode.File == nil || test(castNode.File) + }) +} + +func (n *CommitFileNode) Flatten(collapsedPaths map[string]bool) []*CommitFileNode { + results := flatten(n, collapsedPaths) + nodes := make([]*CommitFileNode, len(results)) + for i, result := range results { + nodes[i] = result.(*CommitFileNode) + } + + return nodes +} + +func (node *CommitFileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *CommitFileNode { + return getNodeAtIndex(node, index, collapsedPaths).(*CommitFileNode) +} + +func (node *CommitFileNode) GetIndexForPath(path string, collapsedPaths map[string]bool) (int, bool) { + return getIndexForPath(node, path, collapsedPaths) +} + +func (node *CommitFileNode) Size(collapsedPaths map[string]bool) int { + return size(node, collapsedPaths) +} + +func (s *CommitFileNode) Compress() { + // with these functions I try to only have type conversion code on the actual struct, + // but comparing interface values to nil is fraught with danger so I'm duplicating + // that code here. + if s == nil { + return + } + + compressAux(s) +} + +// This ignores the root +func (node *CommitFileNode) GetPathsMatching(test func(*CommitFileNode) bool) []string { + return getPathsMatching(node, func(n INode) bool { + return test(n.(*CommitFileNode)) + }) +} + +func (s *CommitFileNode) GetLeaves() []*CommitFileNode { + leaves := getLeaves(s) + castLeaves := make([]*CommitFileNode, len(leaves)) + for i := range leaves { + castLeaves[i] = leaves[i].(*CommitFileNode) + } + + return castLeaves +} + +// extra methods + +func (s *CommitFileNode) AnyFile(test func(file *models.CommitFile) bool) bool { + return s.Any(func(node *CommitFileNode) bool { + return node.IsLeaf() && test(node.File) + }) +} + +func (s *CommitFileNode) NameAtDepth(depth int) string { + splitName := strings.Split(s.Path, string(os.PathSeparator)) + name := filepath.Join(splitName[depth:]...) + + return name +} diff --git a/pkg/gui/filetree/file_change_manager.go b/pkg/gui/filetree/file_change_manager.go deleted file mode 100644 index b663e127b..000000000 --- a/pkg/gui/filetree/file_change_manager.go +++ /dev/null @@ -1,88 +0,0 @@ -package filetree - -import ( - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/presentation" - "github.com/sirupsen/logrus" -) - -type FileChangeManager struct { - files []*models.File - tree *FileChangeNode - showTree bool - log *logrus.Entry - collapsedPaths CollapsedPaths -} - -func NewFileChangeManager(files []*models.File, log *logrus.Entry, showTree bool) *FileChangeManager { - return &FileChangeManager{ - files: files, - log: log, - showTree: showTree, - collapsedPaths: CollapsedPaths{}, - } -} - -func (m *FileChangeManager) ExpandToPath(path string) { - m.collapsedPaths.ExpandToPath(path) -} - -func (m *FileChangeManager) ToggleShowTree() { - m.showTree = !m.showTree - m.SetTree() -} - -func (m *FileChangeManager) GetItemAtIndex(index int) *FileChangeNode { - // need to traverse the three depth first until we get to the index. - return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root -} - -func (m *FileChangeManager) GetIndexForPath(path string) (int, bool) { - index, found := m.tree.GetIndexForPath(path, m.collapsedPaths) - return index - 1, found -} - -func (m *FileChangeManager) GetAllItems() []*FileChangeNode { - if m.tree == nil { - return nil - } - - return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root -} - -func (m *FileChangeManager) GetItemsLength() int { - return m.tree.Size(m.collapsedPaths) - 1 // ignoring root -} - -func (m *FileChangeManager) GetAllFiles() []*models.File { - return m.files -} - -func (m *FileChangeManager) SetFiles(files []*models.File) { - m.files = files - - m.SetTree() -} - -func (m *FileChangeManager) SetTree() { - if m.showTree { - m.tree = BuildTreeFromFiles(m.files) - } else { - m.tree = BuildFlatTreeFromFiles(m.files) - } -} - -func (m *FileChangeManager) IsCollapsed(path string) bool { - return m.collapsedPaths.IsCollapsed(path) -} - -func (m *FileChangeManager) ToggleCollapsed(path string) { - m.collapsedPaths.ToggleCollapsed(path) -} - -func (m *FileChangeManager) Render(diffName string, submoduleConfigs []*models.SubmoduleConfig) []string { - return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string { - castN := n.(*FileChangeNode) - return presentation.GetFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File) - }) -} diff --git a/pkg/gui/filetree/file_change_manager_test.go b/pkg/gui/filetree/file_change_manager_test.go deleted file mode 100644 index fb9022fec..000000000 --- a/pkg/gui/filetree/file_change_manager_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package filetree - -import ( - "testing" - - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/stretchr/testify/assert" -) - -func TestRender(t *testing.T) { - scenarios := []struct { - name string - root *FileChangeNode - collapsedPaths map[string]bool - expected []string - }{ - { - name: "nil node", - root: nil, - expected: []string{}, - }, - { - name: "leaf node", - root: &FileChangeNode{ - Path: "", - Children: []*FileChangeNode{ - {File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"}, - }, - }, - expected: []string{" M test"}, - }, - { - name: "big example", - root: &FileChangeNode{ - Path: "", - Children: []*FileChangeNode{ - { - Path: "dir1", - Children: []*FileChangeNode{ - { - File: &models.File{Name: "dir1/file2", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir1/file2", - }, - { - File: &models.File{Name: "dir1/file3", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir1/file3", - }, - }, - }, - { - Path: "dir2", - Children: []*FileChangeNode{ - { - Path: "dir2/dir2", - Children: []*FileChangeNode{ - { - File: &models.File{Name: "dir2/dir2/file3", ShortStatus: " M", HasStagedChanges: true}, - Path: "dir2/dir2/file3", - }, - { - File: &models.File{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir2/dir2/file4", - }, - }, - }, - { - File: &models.File{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir2/file5", - }, - }, - }, - { - File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "file1", - }, - }, - }, - expected: []string{"dir1 ►", "dir2 ▼", "├─ dir2 ▼", "│ ├─ M file3", "│ └─ M file4", "└─ M file5", "M file1"}, - collapsedPaths: map[string]bool{"dir1": true}, - }, - } - - for _, s := range scenarios { - s := s - t.Run(s.name, func(t *testing.T) { - mngr := &FileChangeManager{tree: s.root, collapsedPaths: s.collapsedPaths} - result := mngr.Render("", nil) - assert.EqualValues(t, s.expected, result) - }) - } -} diff --git a/pkg/gui/filetree/file_change_node.go b/pkg/gui/filetree/file_change_node.go deleted file mode 100644 index a99a47ac1..000000000 --- a/pkg/gui/filetree/file_change_node.go +++ /dev/null @@ -1,178 +0,0 @@ -package filetree - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/jesseduffield/lazygit/pkg/commands/models" -) - -type FileChangeNode struct { - Children []*FileChangeNode - File *models.File - Path string // e.g. '/path/to/mydir' - CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode -} - -// methods satisfying ListItem interface - -func (s *FileChangeNode) ID() string { - return s.GetPath() -} - -func (s *FileChangeNode) Description() string { - return s.GetPath() -} - -// methods satisfying INode interface - -func (s *FileChangeNode) IsLeaf() bool { - return s.File != nil -} - -func (s *FileChangeNode) GetPath() string { - return s.Path -} - -func (s *FileChangeNode) GetChildren() []INode { - result := make([]INode, len(s.Children)) - for i, child := range s.Children { - result[i] = child - } - - return result -} - -func (s *FileChangeNode) SetChildren(children []INode) { - castChildren := make([]*FileChangeNode, len(children)) - for i, child := range children { - castChildren[i] = child.(*FileChangeNode) - } - - s.Children = castChildren -} - -func (s *FileChangeNode) GetCompressionLevel() int { - return s.CompressionLevel -} - -func (s *FileChangeNode) SetCompressionLevel(level int) { - s.CompressionLevel = level -} - -// methods utilising generic functions for INodes - -func (s *FileChangeNode) Sort() { - sortNode(s) -} - -func (s *FileChangeNode) ForEachFile(cb func(*models.File) error) error { - return forEachLeaf(s, func(n INode) error { - castNode := n.(*FileChangeNode) - return cb(castNode.File) - }) -} - -func (s *FileChangeNode) Any(test func(node *FileChangeNode) bool) bool { - return any(s, func(n INode) bool { - castNode := n.(*FileChangeNode) - return test(castNode) - }) -} - -func (n *FileChangeNode) Flatten(collapsedPaths map[string]bool) []*FileChangeNode { - results := flatten(n, collapsedPaths) - nodes := make([]*FileChangeNode, len(results)) - for i, result := range results { - nodes[i] = result.(*FileChangeNode) - } - - return nodes -} - -func (node *FileChangeNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *FileChangeNode { - return getNodeAtIndex(node, index, collapsedPaths).(*FileChangeNode) -} - -func (node *FileChangeNode) GetIndexForPath(path string, collapsedPaths map[string]bool) (int, bool) { - return getIndexForPath(node, path, collapsedPaths) -} - -func (node *FileChangeNode) Size(collapsedPaths map[string]bool) int { - return size(node, collapsedPaths) -} - -func (s *FileChangeNode) Compress() { - // with these functions I try to only have type conversion code on the actual struct, - // but comparing interface values to nil is fraught with danger so I'm duplicating - // that code here. - if s == nil { - return - } - - compressAux(s) -} - -// This ignores the root -func (node *FileChangeNode) GetPathsMatching(test func(*FileChangeNode) bool) []string { - return getPathsMatching(node, func(n INode) bool { - return test(n.(*FileChangeNode)) - }) -} - -func (s *FileChangeNode) GetLeaves() []*FileChangeNode { - leaves := getLeaves(s) - castLeaves := make([]*FileChangeNode, len(leaves)) - for i := range leaves { - castLeaves[i] = leaves[i].(*FileChangeNode) - } - - return castLeaves -} - -// extra methods - -func (s *FileChangeNode) GetHasUnstagedChanges() bool { - return s.AnyFile(func(file *models.File) bool { return file.HasUnstagedChanges }) -} - -func (s *FileChangeNode) GetHasStagedChanges() bool { - return s.AnyFile(func(file *models.File) bool { return file.HasStagedChanges }) -} - -func (s *FileChangeNode) GetHasInlineMergeConflicts() bool { - return s.AnyFile(func(file *models.File) bool { return file.HasInlineMergeConflicts }) -} - -func (s *FileChangeNode) GetIsTracked() bool { - return s.AnyFile(func(file *models.File) bool { return file.Tracked }) -} - -func (s *FileChangeNode) AnyFile(test func(file *models.File) bool) bool { - return s.Any(func(node *FileChangeNode) bool { - return node.IsLeaf() && test(node.File) - }) -} - -func (s *FileChangeNode) NameAtDepth(depth int) string { - splitName := strings.Split(s.Path, string(os.PathSeparator)) - name := filepath.Join(splitName[depth:]...) - - if s.File != nil && s.File.IsRename() { - splitPrevName := strings.Split(s.File.PreviousName, string(os.PathSeparator)) - - prevName := s.File.PreviousName - // if the file has just been renamed inside the same directory, we can shave off - // the prefix for the previous path too. Otherwise we'll keep it unchanged - sameParentDir := len(splitName) == len(splitPrevName) && filepath.Join(splitName[0:depth]...) == filepath.Join(splitPrevName[0:depth]...) - if sameParentDir { - prevName = filepath.Join(splitPrevName[depth:]...) - } - - return fmt.Sprintf("%s%s%s", prevName, " → ", name) - } - - return name -} diff --git a/pkg/gui/filetree/file_change_node_test.go b/pkg/gui/filetree/file_change_node_test.go deleted file mode 100644 index dbb21a6cd..000000000 --- a/pkg/gui/filetree/file_change_node_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package filetree - -import ( - "testing" - - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/stretchr/testify/assert" -) - -func TestCompress(t *testing.T) { - scenarios := []struct { - name string - root *FileChangeNode - expected *FileChangeNode - }{ - { - name: "nil node", - root: nil, - expected: nil, - }, - { - name: "leaf node", - root: &FileChangeNode{ - Path: "", - Children: []*FileChangeNode{ - {File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"}, - }, - }, - expected: &FileChangeNode{ - Path: "", - Children: []*FileChangeNode{ - {File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"}, - }, - }, - }, - { - name: "big example", - root: &FileChangeNode{ - Path: "", - Children: []*FileChangeNode{ - { - Path: "dir1", - Children: []*FileChangeNode{ - { - File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir1/file2", - }, - }, - }, - { - Path: "dir2", - Children: []*FileChangeNode{ - { - File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true}, - Path: "dir2/file3", - }, - { - File: &models.File{Name: "file4", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir2/file4", - }, - }, - }, - { - Path: "dir3", - Children: []*FileChangeNode{ - { - Path: "dir3/dir3-1", - Children: []*FileChangeNode{ - { - File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir3/dir3-1/file5", - }, - }, - }, - }, - }, - { - File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "file1", - }, - }, - }, - expected: &FileChangeNode{ - Path: "", - Children: []*FileChangeNode{ - { - Path: "dir1/file2", - File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true}, - CompressionLevel: 1, - }, - { - Path: "dir2", - Children: []*FileChangeNode{ - { - File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true}, - Path: "dir2/file3", - }, - { - File: &models.File{Name: "file4", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir2/file4", - }, - }, - }, - { - Path: "dir3/dir3-1/file5", - File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true}, - CompressionLevel: 2, - }, - { - File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "file1", - }, - }, - }, - }, - } - - for _, s := range scenarios { - s := s - t.Run(s.name, func(t *testing.T) { - s.root.Compress() - assert.EqualValues(t, s.expected, s.root) - }) - } -} diff --git a/pkg/gui/filetree/file_manager.go b/pkg/gui/filetree/file_manager.go new file mode 100644 index 000000000..6fd688bc2 --- /dev/null +++ b/pkg/gui/filetree/file_manager.go @@ -0,0 +1,88 @@ +package filetree + +import ( + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" + "github.com/sirupsen/logrus" +) + +type FileManager struct { + files []*models.File + tree *FileNode + showTree bool + log *logrus.Entry + collapsedPaths CollapsedPaths +} + +func NewFileChangeManager(files []*models.File, log *logrus.Entry, showTree bool) *FileManager { + return &FileManager{ + files: files, + log: log, + showTree: showTree, + collapsedPaths: CollapsedPaths{}, + } +} + +func (m *FileManager) ExpandToPath(path string) { + m.collapsedPaths.ExpandToPath(path) +} + +func (m *FileManager) ToggleShowTree() { + m.showTree = !m.showTree + m.SetTree() +} + +func (m *FileManager) GetItemAtIndex(index int) *FileNode { + // need to traverse the three depth first until we get to the index. + return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root +} + +func (m *FileManager) GetIndexForPath(path string) (int, bool) { + index, found := m.tree.GetIndexForPath(path, m.collapsedPaths) + return index - 1, found +} + +func (m *FileManager) GetAllItems() []*FileNode { + if m.tree == nil { + return nil + } + + return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root +} + +func (m *FileManager) GetItemsLength() int { + return m.tree.Size(m.collapsedPaths) - 1 // ignoring root +} + +func (m *FileManager) GetAllFiles() []*models.File { + return m.files +} + +func (m *FileManager) SetFiles(files []*models.File) { + m.files = files + + m.SetTree() +} + +func (m *FileManager) SetTree() { + if m.showTree { + m.tree = BuildTreeFromFiles(m.files) + } else { + m.tree = BuildFlatTreeFromFiles(m.files) + } +} + +func (m *FileManager) IsCollapsed(path string) bool { + return m.collapsedPaths.IsCollapsed(path) +} + +func (m *FileManager) ToggleCollapsed(path string) { + m.collapsedPaths.ToggleCollapsed(path) +} + +func (m *FileManager) Render(diffName string, submoduleConfigs []*models.SubmoduleConfig) []string { + return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string { + castN := n.(*FileNode) + return presentation.GetFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File) + }) +} diff --git a/pkg/gui/filetree/file_manager_test.go b/pkg/gui/filetree/file_manager_test.go new file mode 100644 index 000000000..038c03364 --- /dev/null +++ b/pkg/gui/filetree/file_manager_test.go @@ -0,0 +1,91 @@ +package filetree + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/stretchr/testify/assert" +) + +func TestRender(t *testing.T) { + scenarios := []struct { + name string + root *FileNode + collapsedPaths map[string]bool + expected []string + }{ + { + name: "nil node", + root: nil, + expected: []string{}, + }, + { + name: "leaf node", + root: &FileNode{ + Path: "", + Children: []*FileNode{ + {File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"}, + }, + }, + expected: []string{" M test"}, + }, + { + name: "big example", + root: &FileNode{ + Path: "", + Children: []*FileNode{ + { + Path: "dir1", + Children: []*FileNode{ + { + File: &models.File{Name: "dir1/file2", ShortStatus: "M ", HasUnstagedChanges: true}, + Path: "dir1/file2", + }, + { + File: &models.File{Name: "dir1/file3", ShortStatus: "M ", HasUnstagedChanges: true}, + Path: "dir1/file3", + }, + }, + }, + { + Path: "dir2", + Children: []*FileNode{ + { + Path: "dir2/dir2", + Children: []*FileNode{ + { + File: &models.File{Name: "dir2/dir2/file3", ShortStatus: " M", HasStagedChanges: true}, + Path: "dir2/dir2/file3", + }, + { + File: &models.File{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, + Path: "dir2/dir2/file4", + }, + }, + }, + { + File: &models.File{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true}, + Path: "dir2/file5", + }, + }, + }, + { + File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, + Path: "file1", + }, + }, + }, + expected: []string{"dir1 ►", "dir2 ▼", "├─ dir2 ▼", "│ ├─ M file3", "│ └─ M file4", "└─ M file5", "M file1"}, + collapsedPaths: map[string]bool{"dir1": true}, + }, + } + + for _, s := range scenarios { + s := s + t.Run(s.name, func(t *testing.T) { + mngr := &FileManager{tree: s.root, collapsedPaths: s.collapsedPaths} + result := mngr.Render("", nil) + assert.EqualValues(t, s.expected, result) + }) + } +} diff --git a/pkg/gui/filetree/file_node.go b/pkg/gui/filetree/file_node.go new file mode 100644 index 000000000..cf3e69aa8 --- /dev/null +++ b/pkg/gui/filetree/file_node.go @@ -0,0 +1,178 @@ +package filetree + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/jesseduffield/lazygit/pkg/commands/models" +) + +type FileNode struct { + Children []*FileNode + File *models.File + Path string // e.g. '/path/to/mydir' + CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode +} + +// methods satisfying ListItem interface + +func (s *FileNode) ID() string { + return s.GetPath() +} + +func (s *FileNode) Description() string { + return s.GetPath() +} + +// methods satisfying INode interface + +func (s *FileNode) IsLeaf() bool { + return s.File != nil +} + +func (s *FileNode) GetPath() string { + return s.Path +} + +func (s *FileNode) GetChildren() []INode { + result := make([]INode, len(s.Children)) + for i, child := range s.Children { + result[i] = child + } + + return result +} + +func (s *FileNode) SetChildren(children []INode) { + castChildren := make([]*FileNode, len(children)) + for i, child := range children { + castChildren[i] = child.(*FileNode) + } + + s.Children = castChildren +} + +func (s *FileNode) GetCompressionLevel() int { + return s.CompressionLevel +} + +func (s *FileNode) SetCompressionLevel(level int) { + s.CompressionLevel = level +} + +// methods utilising generic functions for INodes + +func (s *FileNode) Sort() { + sortNode(s) +} + +func (s *FileNode) ForEachFile(cb func(*models.File) error) error { + return forEachLeaf(s, func(n INode) error { + castNode := n.(*FileNode) + return cb(castNode.File) + }) +} + +func (s *FileNode) Any(test func(node *FileNode) bool) bool { + return any(s, func(n INode) bool { + castNode := n.(*FileNode) + return test(castNode) + }) +} + +func (n *FileNode) Flatten(collapsedPaths map[string]bool) []*FileNode { + results := flatten(n, collapsedPaths) + nodes := make([]*FileNode, len(results)) + for i, result := range results { + nodes[i] = result.(*FileNode) + } + + return nodes +} + +func (node *FileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *FileNode { + return getNodeAtIndex(node, index, collapsedPaths).(*FileNode) +} + +func (node *FileNode) GetIndexForPath(path string, collapsedPaths map[string]bool) (int, bool) { + return getIndexForPath(node, path, collapsedPaths) +} + +func (node *FileNode) Size(collapsedPaths map[string]bool) int { + return size(node, collapsedPaths) +} + +func (s *FileNode) Compress() { + // with these functions I try to only have type conversion code on the actual struct, + // but comparing interface values to nil is fraught with danger so I'm duplicating + // that code here. + if s == nil { + return + } + + compressAux(s) +} + +// This ignores the root +func (node *FileNode) GetPathsMatching(test func(*FileNode) bool) []string { + return getPathsMatching(node, func(n INode) bool { + return test(n.(*FileNode)) + }) +} + +func (s *FileNode) GetLeaves() []*FileNode { + leaves := getLeaves(s) + castLeaves := make([]*FileNode, len(leaves)) + for i := range leaves { + castLeaves[i] = leaves[i].(*FileNode) + } + + return castLeaves +} + +// extra methods + +func (s *FileNode) GetHasUnstagedChanges() bool { + return s.AnyFile(func(file *models.File) bool { return file.HasUnstagedChanges }) +} + +func (s *FileNode) GetHasStagedChanges() bool { + return s.AnyFile(func(file *models.File) bool { return file.HasStagedChanges }) +} + +func (s *FileNode) GetHasInlineMergeConflicts() bool { + return s.AnyFile(func(file *models.File) bool { return file.HasInlineMergeConflicts }) +} + +func (s *FileNode) GetIsTracked() bool { + return s.AnyFile(func(file *models.File) bool { return file.Tracked }) +} + +func (s *FileNode) AnyFile(test func(file *models.File) bool) bool { + return s.Any(func(node *FileNode) bool { + return node.IsLeaf() && test(node.File) + }) +} + +func (s *FileNode) NameAtDepth(depth int) string { + splitName := strings.Split(s.Path, string(os.PathSeparator)) + name := filepath.Join(splitName[depth:]...) + + if s.File != nil && s.File.IsRename() { + splitPrevName := strings.Split(s.File.PreviousName, string(os.PathSeparator)) + + prevName := s.File.PreviousName + // if the file has just been renamed inside the same directory, we can shave off + // the prefix for the previous path too. Otherwise we'll keep it unchanged + sameParentDir := len(splitName) == len(splitPrevName) && filepath.Join(splitName[0:depth]...) == filepath.Join(splitPrevName[0:depth]...) + if sameParentDir { + prevName = filepath.Join(splitPrevName[depth:]...) + } + + return fmt.Sprintf("%s%s%s", prevName, " → ", name) + } + + return name +} diff --git a/pkg/gui/filetree/file_node_test.go b/pkg/gui/filetree/file_node_test.go new file mode 100644 index 000000000..d47754e69 --- /dev/null +++ b/pkg/gui/filetree/file_node_test.go @@ -0,0 +1,125 @@ +package filetree + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/stretchr/testify/assert" +) + +func TestCompress(t *testing.T) { + scenarios := []struct { + name string + root *FileNode + expected *FileNode + }{ + { + name: "nil node", + root: nil, + expected: nil, + }, + { + name: "leaf node", + root: &FileNode{ + Path: "", + Children: []*FileNode{ + {File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"}, + }, + }, + expected: &FileNode{ + Path: "", + Children: []*FileNode{ + {File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"}, + }, + }, + }, + { + name: "big example", + root: &FileNode{ + Path: "", + Children: []*FileNode{ + { + Path: "dir1", + Children: []*FileNode{ + { + File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true}, + Path: "dir1/file2", + }, + }, + }, + { + Path: "dir2", + Children: []*FileNode{ + { + File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true}, + Path: "dir2/file3", + }, + { + File: &models.File{Name: "file4", ShortStatus: "M ", HasUnstagedChanges: true}, + Path: "dir2/file4", + }, + }, + }, + { + Path: "dir3", + Children: []*FileNode{ + { + Path: "dir3/dir3-1", + Children: []*FileNode{ + { + File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true}, + Path: "dir3/dir3-1/file5", + }, + }, + }, + }, + }, + { + File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, + Path: "file1", + }, + }, + }, + expected: &FileNode{ + Path: "", + Children: []*FileNode{ + { + Path: "dir1/file2", + File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true}, + CompressionLevel: 1, + }, + { + Path: "dir2", + Children: []*FileNode{ + { + File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true}, + Path: "dir2/file3", + }, + { + File: &models.File{Name: "file4", ShortStatus: "M ", HasUnstagedChanges: true}, + Path: "dir2/file4", + }, + }, + }, + { + Path: "dir3/dir3-1/file5", + File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true}, + CompressionLevel: 2, + }, + { + File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, + Path: "file1", + }, + }, + }, + }, + } + + for _, s := range scenarios { + s := s + t.Run(s.name, func(t *testing.T) { + s.root.Compress() + assert.EqualValues(t, s.expected, s.root) + }) + } +} diff --git a/pkg/gui/filtering_menu_panel.go b/pkg/gui/filtering_menu_panel.go index 683bff3e8..ebebbd869 100644 --- a/pkg/gui/filtering_menu_panel.go +++ b/pkg/gui/filtering_menu_panel.go @@ -15,7 +15,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro fileName := "" switch v.Name() { case "files": - node := gui.getSelectedFileChangeNode() + node := gui.getSelectedFileNode() if node != nil { fileName = node.GetPath() } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 13d827751..a5d66153f 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -304,12 +304,12 @@ type guiStateMutexes struct { } type guiState struct { - FileChangeManager *filetree.FileChangeManager - CommitFileChangeManager *filetree.CommitFileChangeManager - Submodules []*models.SubmoduleConfig - Branches []*models.Branch - Commits []*models.Commit - StashEntries []*models.StashEntry + FileManager *filetree.FileManager + CommitFileManager *filetree.CommitFileManager + Submodules []*models.SubmoduleConfig + Branches []*models.Branch + Commits []*models.Commit + StashEntries []*models.StashEntry // Suggestions will sometimes appear when typing into a prompt Suggestions []*types.Suggestion // FilteredReflogCommits are the ones that appear in the reflog panel. @@ -381,12 +381,12 @@ func (gui *Gui) resetState() { showTree := gui.Config.GetUserConfig().Gui.ShowFileTree gui.State = &guiState{ - FileChangeManager: filetree.NewFileChangeManager(make([]*models.File, 0), gui.Log, showTree), - CommitFileChangeManager: filetree.NewCommitFileChangeManager(make([]*models.CommitFile, 0), gui.Log, showTree), - Commits: make([]*models.Commit, 0), - FilteredReflogCommits: make([]*models.Commit, 0), - ReflogCommits: make([]*models.Commit, 0), -