mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-02-03 13:21:56 +02:00
properly resolve cyclic dependency
This commit is contained in:
parent
4ab5e54139
commit
5b7dd9e43c
@ -10,7 +10,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@ -160,12 +159,18 @@ func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error
|
||||
return self.DiscardUnstagedFileChanges(file)
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) DiscardAllDirChanges(node *filetree.FileNode) error {
|
||||
type IFileNode interface {
|
||||
ForEachFile(cb func(*models.File) error) error
|
||||
GetFilePathsMatching(test func(*models.File) bool) []string
|
||||
GetPath() string
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) DiscardAllDirChanges(node IFileNode) error {
|
||||
// this could be more efficient but we would need to handle all the edge cases
|
||||
return node.ForEachFile(self.DiscardAllFileChanges)
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
|
||||
func (self *WorkingTreeCommands) DiscardUnstagedDirChanges(node IFileNode) error {
|
||||
if err := self.RemoveUntrackedDirFiles(node); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -178,9 +183,9 @@ func (self *WorkingTreeCommands) DiscardUnstagedDirChanges(node *filetree.FileNo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
|
||||
untrackedFilePaths := node.GetPathsMatching(
|
||||
func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() },
|
||||
func (self *WorkingTreeCommands) RemoveUntrackedDirFiles(node IFileNode) error {
|
||||
untrackedFilePaths := node.GetFilePathsMatching(
|
||||
func(file *models.File) bool { return !file.GetIsTracked() },
|
||||
)
|
||||
|
||||
for _, path := range untrackedFilePaths {
|
||||
|
@ -8,11 +8,11 @@ import (
|
||||
|
||||
func (gui *Gui) getSelectedCommitFileNode() *filetree.CommitFileNode {
|
||||
selectedLine := gui.State.Panels.CommitFiles.SelectedLineIdx
|
||||
if selectedLine == -1 || selectedLine > gui.State.CommitFileManager.GetItemsLength()-1 {
|
||||
if selectedLine == -1 || selectedLine > gui.State.CommitFileTreeViewModel.GetItemsLength()-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.CommitFileManager.GetItemAtIndex(selectedLine)
|
||||
return gui.State.CommitFileTreeViewModel.GetItemAtIndex(selectedLine)
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedCommitFile() *models.CommitFile {
|
||||
@ -42,7 +42,7 @@ func (gui *Gui) commitFilesRenderToMain() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
to := gui.State.CommitFileManager.GetParent()
|
||||
to := gui.State.CommitFileTreeViewModel.GetParent()
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
|
||||
cmdObj := gui.Git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
|
||||
@ -64,7 +64,7 @@ func (gui *Gui) handleCheckoutCommitFile() error {
|
||||
}
|
||||
|
||||
gui.logAction(gui.Tr.Actions.CheckoutFile)
|
||||
if err := gui.Git.WorkingTree.CheckoutFile(gui.State.CommitFileManager.GetParent(), node.GetPath()); err != nil {
|
||||
if err := gui.Git.WorkingTree.CheckoutFile(gui.State.CommitFileTreeViewModel.GetParent(), node.GetPath()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@ -111,7 +111,8 @@ func (gui *Gui) refreshCommitFilesView() error {
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.CommitFileManager.SetFiles(files, to)
|
||||
gui.State.CommitFileTreeViewModel.SetParent(to)
|
||||
gui.State.CommitFileTreeViewModel.SetFiles(files)
|
||||
|
||||
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
|
||||
}
|
||||
@ -154,7 +155,7 @@ func (gui *Gui) handleToggleFileForPatch() 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.Git.Patch.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileManager.GetParent()) != patch.WHOLE
|
||||
return gui.Git.Patch.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileTreeViewModel.GetParent()) != patch.WHOLE
|
||||
})
|
||||
|
||||
err := node.ForEachFile(func(file *models.CommitFile) error {
|
||||
@ -176,7 +177,7 @@ func (gui *Gui) handleToggleFileForPatch() error {
|
||||
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
|
||||
}
|
||||
|
||||
if gui.Git.Patch.PatchManager.Active() && gui.Git.Patch.PatchManager.To != gui.State.CommitFileManager.GetParent() {
|
||||
if gui.Git.Patch.PatchManager.Active() && gui.Git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.DiscardPatch,
|
||||
prompt: gui.Tr.DiscardPatchConfirm,
|
||||
@ -224,7 +225,7 @@ func (gui *Gui) enterCommitFile(opts OnFocusOpts) error {
|
||||
return gui.pushContext(gui.State.Contexts.PatchBuilding, opts)
|
||||
}
|
||||
|
||||
if gui.Git.Patch.PatchManager.Active() && gui.Git.Patch.PatchManager.To != gui.State.CommitFileManager.GetParent() {
|
||||
if gui.Git.Patch.PatchManager.Active() && gui.Git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.DiscardPatch,
|
||||
prompt: gui.Tr.DiscardPatchConfirm,
|
||||
@ -244,7 +245,7 @@ func (gui *Gui) handleToggleCommitFileDirCollapsed() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.CommitFileManager.ToggleCollapsed(node.GetPath())
|
||||
gui.State.CommitFileTreeViewModel.ToggleCollapsed(node.GetPath())
|
||||
|
||||
if err := gui.postRefreshUpdate(gui.State.Contexts.CommitFiles); err != nil {
|
||||
gui.Log.Error(err)
|
||||
@ -275,12 +276,12 @@ func (gui *Gui) switchToCommitFilesContext(refName string, canRebase bool, conte
|
||||
func (gui *Gui) handleToggleCommitFileTreeView() error {
|
||||
path := gui.getSelectedCommitFilePath()
|
||||
|
||||
gui.State.CommitFileManager.ToggleShowTree()
|
||||
gui.State.CommitFileTreeViewModel.ToggleShowTree()
|
||||
|
||||
// find that same node in the new format and move the cursor to it
|
||||
if path != "" {
|
||||
gui.State.CommitFileManager.ExpandToPath(path)
|
||||
index, found := gui.State.CommitFileManager.GetIndexForPath(path)
|
||||
gui.State.CommitFileTreeViewModel.ExpandToPath(path)
|
||||
index, found := gui.State.CommitFileTreeViewModel.GetIndexForPath(path)
|
||||
if found {
|
||||
gui.State.Contexts.CommitFiles.GetPanelState().SetSelectedLineIdx(index)
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ func (gui *Gui) getSelectedFileNode() *filetree.FileNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.FileManager.GetItemAtIndex(selectedLine)
|
||||
return gui.State.FileTreeViewModel.GetItemAtIndex(selectedLine)
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedFile() *models.File {
|
||||
@ -129,7 +129,7 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) stagedFiles() []*models.File {
|
||||
files := gui.State.FileManager.GetAllFiles()
|
||||
files := gui.State.FileTreeViewModel.GetAllFiles()
|
||||
result := make([]*models.File, 0)
|
||||
for _, file := range files {
|
||||
if file.HasStagedChanges {
|
||||
@ -140,7 +140,7 @@ func (gui *Gui) stagedFiles() []*models.File {
|
||||
}
|
||||
|
||||
func (gui *Gui) trackedFiles() []*models.File {
|
||||
files := gui.State.FileManager.GetAllFiles()
|
||||
files := gui.State.FileTreeViewModel.GetAllFiles()
|
||||
result := make([]*models.File, 0, len(files))
|
||||
for _, file := range files {
|
||||
if file.Tracked {
|
||||
@ -244,7 +244,7 @@ func (gui *Gui) handleFilePress() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) allFilesStaged() bool {
|
||||
for _, file := range gui.State.FileManager.GetAllFiles() {
|
||||
for _, file := range gui.State.FileTreeViewModel.GetAllFiles() {
|
||||
if file.HasUnstagedChanges {
|
||||
return false
|
||||
}
|
||||
@ -378,7 +378,7 @@ func (gui *Gui) handleCommitPress() error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if gui.State.FileManager.GetItemsLength() == 0 {
|
||||
if gui.State.FileTreeViewModel.GetItemsLength() == 0 {
|
||||
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
@ -433,7 +433,7 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAmendCommitPress() error {
|
||||
if gui.State.FileManager.GetItemsLength() == 0 {
|
||||
if gui.State.FileTreeViewModel.GetItemsLength() == 0 {
|
||||
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
@ -459,7 +459,7 @@ func (gui *Gui) handleAmendCommitPress() error {
|
||||
// handleCommitEditorPress - handle when the user wants to commit changes via
|
||||
// their editor rather than via the popup panel
|
||||
func (gui *Gui) handleCommitEditorPress() error {
|
||||
if gui.State.FileManager.GetItemsLength() == 0 {
|
||||
if gui.State.FileTreeViewModel.GetItemsLength() == 0 {
|
||||
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
@ -498,9 +498,9 @@ func (gui *Gui) handleStatusFilterPressed() error {
|
||||
return gui.createMenu(gui.Tr.FilteringMenuTitle, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) setStatusFiltering(filter filetree.FileManagerDisplayFilter) error {
|
||||
func (gui *Gui) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error {
|
||||
state := gui.State
|
||||
state.FileManager.SetDisplayFilter(filter)
|
||||
state.FileTreeViewModel.SetDisplayFilter(filter)
|
||||
return gui.handleRefreshFiles()
|
||||
}
|
||||
|
||||
@ -555,31 +555,31 @@ func (gui *Gui) refreshStateFiles() error {
|
||||
|
||||
selectedNode := gui.getSelectedFileNode()
|
||||
|
||||
prevNodes := gui.State.FileManager.GetAllItems()
|
||||
prevNodes := gui.State.FileTreeViewModel.GetAllItems()
|
||||
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
|
||||
|
||||
files := gui.Git.Loaders.Files.
|
||||
GetStatusFiles(loaders.GetStatusFileOptions{})
|
||||
|
||||
// for when you stage the old file of a rename and the new file is in a collapsed dir
|
||||
state.FileManager.RWMutex.Lock()
|
||||
state.FileTreeViewModel.RWMutex.Lock()
|
||||
for _, file := range files {
|
||||
if selectedNode != nil && selectedNode.Path != "" && file.PreviousName == selectedNode.Path {
|
||||
state.FileManager.ExpandToPath(file.Name)
|
||||
state.FileTreeViewModel.ExpandToPath(file.Name)
|
||||
}
|
||||
}
|
||||
|
||||
state.FileManager.SetFiles(files)
|
||||
state.FileManager.RWMutex.Unlock()
|
||||
state.FileTreeViewModel.SetFiles(files)
|
||||
state.FileTreeViewModel.RWMutex.Unlock()
|
||||
|
||||
if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if selectedNode != nil {
|
||||
newIdx := gui.findNewSelectedIdx(prevNodes[prevSelectedLineIdx:], state.FileManager.GetAllItems())
|
||||
newIdx := gui.findNewSelectedIdx(prevNodes[prevSelectedLineIdx:], state.FileTreeViewModel.GetAllItems())
|
||||
if newIdx != -1 && newIdx != prevSelectedLineIdx {
|
||||
newNode := state.FileManager.GetItemAtIndex(newIdx)
|
||||
newNode := state.FileTreeViewModel.GetItemAtIndex(newIdx)
|
||||
// when not in tree mode, we show merge conflict files at the top, so you
|
||||
// can work through them one by one without having to sift through a large
|
||||
// set of files. If you have just fixed the merge conflicts of a file, we
|
||||
@ -588,7 +588,7 @@ func (gui *Gui) refreshStateFiles() error {
|
||||
// conflicts: the user in this case would rather work on the next file
|
||||
// with merge conflicts, which will have moved up to fill the gap left by
|
||||
// the last file, meaning the cursor doesn't need to move at all.
|
||||
leaveCursor := !state.FileManager.InTreeMode() && newNode != nil &&
|
||||
leaveCursor := !state.FileTreeViewModel.InTreeMode() && newNode != nil &&
|
||||
selectedNode.File != nil && selectedNode.File.HasMergeConflicts &&
|
||||
newNode.File != nil && !newNode.File.HasMergeConflicts
|
||||
|
||||
@ -598,7 +598,7 @@ func (gui *Gui) refreshStateFiles() error {
|
||||
}
|
||||
}
|
||||
|
||||
gui.refreshSelectedLine(state.Panels.Files, state.FileManager.GetItemsLength())
|
||||
gui.refreshSelectedLine(state.Panels.Files, state.FileTreeViewModel.GetItemsLength())
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -871,7 +871,7 @@ func (gui *Gui) openFile(filename string) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
for _, file := range gui.State.FileManager.GetAllFiles() {
|
||||
for _, file := range gui.State.FileTreeViewModel.GetAllFiles() {
|
||||
if file.HasMergeConflicts {
|
||||
return true
|
||||
}
|
||||
@ -939,7 +939,7 @@ func (gui *Gui) handleToggleDirCollapsed() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.FileManager.ToggleCollapsed(node.GetPath())
|
||||
gui.State.FileTreeViewModel.ToggleCollapsed(node.GetPath())
|
||||
|
||||
if err := gui.postRefreshUpdate(gui.State.Contexts.Files); err != nil {
|
||||
gui.Log.Error(err)
|
||||
@ -952,12 +952,12 @@ func (gui *Gui) handleToggleFileTreeView() error {
|
||||
// get path of currently selected file
|
||||
path := gui.getSelectedPath()
|
||||
|
||||
gui.State.FileManager.ToggleShowTree()
|
||||
gui.State.FileTreeViewModel.ToggleShowTree()
|
||||
|
||||
// find that same node in the new format and move the cursor to it
|
||||
if path != "" {
|
||||
gui.State.FileManager.ExpandToPath(path)
|
||||
index, found := gui.State.FileManager.GetIndexForPath(path)
|
||||
gui.State.FileTreeViewModel.ExpandToPath(path)
|
||||
index, found := gui.State.FileTreeViewModel.GetIndexForPath(path)
|
||||
if found {
|
||||
gui.filesListContext().GetPanelState().SetSelectedLineIdx(index)
|
||||
}
|
||||
|
22
pkg/gui/filetree/README.md
Normal file
22
pkg/gui/filetree/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
## FileTree Package
|
||||
|
||||
This package handles the representation of file trees. There are two ways to render files: one is to render them flat, so something like this:
|
||||
|
||||
```
|
||||
dir1/file1
|
||||
dir1/file2
|
||||
file3
|
||||
```
|
||||
|
||||
And the other is to render them as a tree
|
||||
|
||||
```
|
||||
dir1/
|
||||
file1
|
||||
file2
|
||||
file3
|
||||
```
|
||||
|
||||
Internally we represent each of the above as a tree, but with the flat approach there's just a single root node and every path is a direct child of that root. Viewing in 'tree' mode (as opposed to 'flat' mode) allows for collapsing and expanding directories, and lets you perform actions on directories e.g. staging a whole directory. But it takes up more vertical space and sometimes you just want to have a flat view where you can go flick through your files one by one to see the diff.
|
||||
|
||||
This package is not concerned about rendering the tree: only representing its internal state.
|
@ -1,118 +0,0 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"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 {
|
||||
// can't rely on renderAux to check for nil because an interface won't be nil if its concrete value is nil
|
||||
if m.tree == nil {
|
||||
return []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 getCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status)
|
||||
})
|
||||
}
|
@ -11,6 +11,8 @@ type CommitFileNode struct {
|
||||
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
|
||||
}
|
||||
|
||||
var _ INode = &CommitFileNode{}
|
||||
|
||||
// methods satisfying ListItem interface
|
||||
|
||||
func (s *CommitFileNode) ID() string {
|
||||
@ -23,6 +25,10 @@ func (s *CommitFileNode) Description() string {
|
||||
|
||||
// methods satisfying INode interface
|
||||
|
||||
func (s *CommitFileNode) IsNil() bool {
|
||||
return s == nil
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) IsLeaf() bool {
|
||||
return s.File != nil
|
||||
}
|
||||
@ -139,13 +145,6 @@ func (s *CommitFileNode) Compress() {
|
||||
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))
|
||||
|
101
pkg/gui/filetree/commit_file_tree_view_model.go
Normal file
101
pkg/gui/filetree/commit_file_tree_view_model.go
Normal file
@ -0,0 +1,101 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CommitFileTreeViewModel 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 (self *CommitFileTreeViewModel) GetParent() string {
|
||||
return self.parent
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) SetParent(parent string) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func NewCommitFileTreeViewModel(files []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileTreeViewModel {
|
||||
viewModel := &CommitFileTreeViewModel{
|
||||
log: log,
|
||||
showTree: showTree,
|
||||
collapsedPaths: CollapsedPaths{},
|
||||
}
|
||||
|
||||
viewModel.SetFiles(files)
|
||||
|
||||
return viewModel
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) ExpandToPath(path string) {
|
||||
self.collapsedPaths.ExpandToPath(path)
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) ToggleShowTree() {
|
||||
self.showTree = !self.showTree
|
||||
self.SetTree()
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) GetItemAtIndex(index int) *CommitFileNode {
|
||||
// need to traverse the three depth first until we get to the index.
|
||||
return self.tree.GetNodeAtIndex(index+1, self.collapsedPaths) // ignoring root
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) GetIndexForPath(path string) (int, bool) {
|
||||
index, found := self.tree.GetIndexForPath(path, self.collapsedPaths)
|
||||
return index - 1, found
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) GetAllItems() []*CommitFileNode {
|
||||
if self.tree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.tree.Flatten(self.collapsedPaths)[1:] // ignoring root
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) GetItemsLength() int {
|
||||
return self.tree.Size(self.collapsedPaths) - 1 // ignoring root
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) GetAllFiles() []*models.CommitFile {
|
||||
return self.files
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) SetFiles(files []*models.CommitFile) {
|
||||
self.files = files
|
||||
|
||||
self.SetTree()
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) SetTree() {
|
||||
if self.showTree {
|
||||
self.tree = BuildTreeFromCommitFiles(self.files)
|
||||
} else {
|
||||
self.tree = BuildFlatTreeFromCommitFiles(self.files)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) IsCollapsed(path string) bool {
|
||||
return self.collapsedPaths.IsCollapsed(path)
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) ToggleCollapsed(path string) {
|
||||
self.collapsedPaths.ToggleCollapsed(path)
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) Tree() INode {
|
||||
return self.tree
|
||||
}
|
||||
|
||||
func (self *CommitFileTreeViewModel) CollapsedPaths() CollapsedPaths {
|
||||
return self.collapsedPaths
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package filetree
|
||||
|
||||
const EXPANDED_ARROW = "▼"
|
||||
const COLLAPSED_ARROW = "►"
|
||||
|
||||
const INNER_ITEM = "├─ "
|
||||
const LAST_ITEM = "└─ "
|
||||
const NESTED = "│ "
|
||||
const NOTHING = " "
|
@ -1,140 +0,0 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type FileManagerDisplayFilter int
|
||||
|
||||
const (
|
||||
DisplayAll FileManagerDisplayFilter = iota
|
||||
DisplayStaged
|
||||
DisplayUnstaged
|
||||
)
|
||||
|
||||
type FileManager struct {
|
||||
files []*models.File
|
||||
tree *FileNode
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
filter FileManagerDisplayFilter
|
||||
collapsedPaths CollapsedPaths
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewFileManager(files []*models.File, log *logrus.Entry, showTree bool) *FileManager {
|
||||
return &FileManager{
|
||||
files: files,
|
||||
log: log,
|
||||
showTree: showTree,
|
||||
filter: DisplayAll,
|
||||
collapsedPaths: CollapsedPaths{},
|
||||
RWMutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FileManager) InTreeMode() bool {
|
||||
return m.showTree
|
||||
}
|
||||
|
||||
func (m *FileManager) ExpandToPath(path string) {
|
||||
m.collapsedPaths.ExpandToPath(path)
|
||||
}
|
||||
|
||||
func (m *FileManager) GetFilesForDisplay() []*models.File {
|
||||
files := m.files
|
||||
if m.filter == DisplayAll {
|
||||
return files
|
||||
}
|
||||
|
||||
result := make([]*models.File, 0)
|
||||
if m.filter == DisplayStaged {
|
||||
for _, file := range files {
|
||||
if file.HasStagedChanges {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, file := range files {
|
||||
if !file.HasStagedChanges {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *FileManager) SetDisplayFilter(filter FileManagerDisplayFilter) {
|
||||
m.filter = filter
|
||||
m.SetTree()
|
||||
}
|
||||
|
||||
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() {
|
||||
filesForDisplay := m.GetFilesForDisplay()
|
||||
if m.showTree {
|
||||
m.tree = BuildTreeFromFiles(filesForDisplay)
|
||||
} else {
|
||||
m.tree = BuildFlatTreeFromFiles(filesForDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// can't rely on renderAux to check for nil because an interface won't be nil if its concrete value is nil
|
||||
if m.tree == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string {
|
||||
castN := n.(*FileNode)
|
||||
return getFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File)
|
||||
})
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xo/terminfo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
color.ForceSetColorLevel(terminfo.ColorLevelNone)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterAction(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
filter FileManagerDisplayFilter
|
||||
files []*models.File
|
||||
expected []*models.File
|
||||
}{
|
||||
{
|
||||
name: "filter files with unstaged changes",
|
||||
filter: DisplayUnstaged,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter files with staged changes",
|
||||
filter: DisplayStaged,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: false},
|
||||
{Name: "file1", ShortStatus: "M ", HasStagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasStagedChanges: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter all files",
|
||||
filter: DisplayAll,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
mngr := &FileManager{files: s.files, filter: s.filter}
|
||||
result := mngr.GetFilesForDisplay()
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@ type FileNode struct {
|
||||
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
|
||||
}
|
||||
|
||||
var _ INode = &FileNode{}
|
||||
|
||||
// methods satisfying ListItem interface
|
||||
|
||||
func (s *FileNode) ID() string {
|
||||
@ -23,6 +25,12 @@ func (s *FileNode) Description() string {
|
||||
|
||||
// methods satisfying INode interface
|
||||
|
||||
// interfaces values whose concrete value is nil are not themselves nil
|
||||
// hence the existence of this method
|
||||
func (s *FileNode) IsNil() bool {
|
||||
return s == nil
|
||||
}
|
||||
|
||||
func (s *FileNode) IsLeaf() bool {
|
||||
return s.File != nil
|
||||
}
|
||||
@ -124,10 +132,13 @@ func (s *FileNode) Compress() {
|
||||
compressAux(s)
|
||||
}
|
||||
|
||||
// This ignores the root
|
||||
func (node *FileNode) GetPathsMatching(test func(*FileNode) bool) []string {
|
||||
func (node *FileNode) GetFilePathsMatching(test func(*models.File) bool) []string {
|
||||
return getPathsMatching(node, func(n INode) bool {
|
||||
return test(n.(*FileNode))
|
||||
castNode := n.(*FileNode)
|
||||
if castNode.File == nil {
|
||||
return false
|
||||
}
|
||||
return test(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
|
139
pkg/gui/filetree/file_tree_view_model.go
Normal file
139
pkg/gui/filetree/file_tree_view_model.go
Normal file
@ -0,0 +1,139 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type FileTreeDisplayFilter int
|
||||
|
||||
const (
|
||||
DisplayAll FileTreeDisplayFilter = iota
|
||||
DisplayStaged
|
||||
DisplayUnstaged
|
||||
)
|
||||
|
||||
type FileTreeViewModel struct {
|
||||
files []*models.File
|
||||
tree *FileNode
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
filter FileTreeDisplayFilter
|
||||
collapsedPaths CollapsedPaths
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewFileTreeViewModel(files []*models.File, log *logrus.Entry, showTree bool) *FileTreeViewModel {
|
||||
viewModel := &FileTreeViewModel{
|
||||
log: log,
|
||||
showTree: showTree,
|
||||
filter: DisplayAll,
|
||||
collapsedPaths: CollapsedPaths{},
|
||||
RWMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
viewModel.SetFiles(files)
|
||||
|
||||
return viewModel
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) InTreeMode() bool {
|
||||
return self.showTree
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) ExpandToPath(path string) {
|
||||
self.collapsedPaths.ExpandToPath(path)
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) GetFilesForDisplay() []*models.File {
|
||||
files := self.files
|
||||
if self.filter == DisplayAll {
|
||||
return files
|
||||
}
|
||||
|
||||
result := make([]*models.File, 0)
|
||||
if self.filter == DisplayStaged {
|
||||
for _, file := range files {
|
||||
if file.HasStagedChanges {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, file := range files {
|
||||
if !file.HasStagedChanges {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) SetDisplayFilter(filter FileTreeDisplayFilter) {
|
||||
self.filter = filter
|
||||
self.SetTree()
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) ToggleShowTree() {
|
||||
self.showTree = !self.showTree
|
||||
self.SetTree()
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) GetItemAtIndex(index int) *FileNode {
|
||||
// need to traverse the three depth first until we get to the index.
|
||||
return self.tree.GetNodeAtIndex(index+1, self.collapsedPaths) // ignoring root
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) GetIndexForPath(path string) (int, bool) {
|
||||
index, found := self.tree.GetIndexForPath(path, self.collapsedPaths)
|
||||
return index - 1, found
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) GetAllItems() []*FileNode {
|
||||
if self.tree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.tree.Flatten(self.collapsedPaths)[1:] // ignoring root
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) GetItemsLength() int {
|
||||
return self.tree.Size(self.collapsedPaths) - 1 // ignoring root
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) GetAllFiles() []*models.File {
|
||||
return self.files
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) SetFiles(files []*models.File) {
|
||||
self.files = files
|
||||
|
||||
self.SetTree()
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) SetTree() {
|
||||
filesForDisplay := self.GetFilesForDisplay()
|
||||
if self.showTree {
|
||||
self.tree = BuildTreeFromFiles(filesForDisplay)
|
||||
} else {
|
||||
self.tree = BuildFlatTreeFromFiles(filesForDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) IsCollapsed(path string) bool {
|
||||
return self.collapsedPaths.IsCollapsed(path)
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) ToggleCollapsed(path string) {
|
||||
self.collapsedPaths.ToggleCollapsed(path)
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) Tree() INode {
|
||||
return self.tree
|
||||
}
|
||||
|
||||
func (self *FileTreeViewModel) CollapsedPaths() CollapsedPaths {
|
||||
return self.collapsedPaths
|
||||
}
|
67
pkg/gui/filetree/file_tree_view_model_test.go
Normal file
67
pkg/gui/filetree/file_tree_view_model_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterAction(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
filter FileTreeDisplayFilter
|
||||
files []*models.File
|
||||
expected []*models.File
|
||||
}{
|
||||
{
|
||||
name: "filter files with unstaged changes",
|
||||
filter: DisplayUnstaged,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter files with staged changes",
|
||||
filter: DisplayStaged,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: false},
|
||||
{Name: "file1", ShortStatus: "M ", HasStagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasStagedChanges: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter all files",
|
||||
filter: DisplayAll,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
mngr := &FileTreeViewModel{files: s.files, filter: s.filter}
|
||||
result := mngr.GetFilesForDisplay()
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type INode interface {
|
||||
IsNil() bool
|
||||
IsLeaf() bool
|
||||
GetPath() string
|
||||
GetChildren() []INode
|
||||
@ -212,51 +211,3 @@ func getLeaves(node INode) []INode {
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func renderAux(s INode, collapsedPaths CollapsedPaths, prefix string, depth int, renderLine func(INode, int) string) []string {
|
||||
isRoot := depth == -1
|
||||
|
||||
renderLineWithPrefix := func() string {
|
||||
return prefix + renderLine(s, depth)
|
||||
}
|
||||
|
||||
if s.IsLeaf() {
|
||||
if isRoot {
|
||||
return []string{}
|
||||
}
|
||||
return []string{renderLineWithPrefix()}
|
||||
}
|
||||
|
||||
if collapsedPaths.IsCollapsed(s.GetPath()) {
|
||||
return []string{fmt.Sprintf("%s %s", renderLineWithPrefix(), COLLAPSED_ARROW)}
|
||||
}
|
||||
|
||||
arr := []string{}
|
||||
if !isRoot {
|
||||
arr = append(arr, fmt.Sprintf("%s %s", renderLineWithPrefix(), EXPANDED_ARROW))
|
||||
}
|
||||
|
||||
newPrefix := prefix
|
||||
if strings.HasSuffix(prefix, LAST_ITEM) {
|
||||
newPrefix = strings.TrimSuffix(prefix, LAST_ITEM) + NOTHING
|
||||
} else if strings.HasSuffix(prefix, INNER_ITEM) {
|
||||
newPrefix = strings.TrimSuffix(prefix, INNER_ITEM) + NESTED
|
||||
}
|
||||
|
||||
for i, child := range s.GetChildren() {
|
||||
isLast := i == len(s.GetChildren())-1
|
||||
|
||||
var childPrefix string
|
||||
if isRoot {
|
||||
childPrefix = newPrefix
|
||||
} else if isLast {
|
||||
childPrefix = newPrefix + LAST_ITEM
|
||||
} else {
|
||||
childPrefix = newPrefix + INNER_ITEM
|
||||
}
|
||||
|
||||
arr = append(arr, renderAux(child, collapsedPaths, childPrefix, depth+1+s.GetCompressionLevel(), renderLine)...)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
|
@ -1,96 +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/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// TODO: move this back into presentation package and fix the import cycle
|
||||
|
||||
func getFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, diffName string, submoduleConfigs []*models.SubmoduleConfig, file *models.File) string {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
partiallyModifiedColor := style.FgYellow
|
||||
|
||||
restColor := style.FgGreen
|
||||
if name == diffName {
|
||||
restColor = theme.DiffTerminalColor
|
||||
} else if file == nil && hasStagedChanges && hasUnstagedChanges {
|
||||
restColor = partiallyModifiedColor
|
||||
} else if hasUnstagedChanges {
|
||||
restColor = style.FgRed
|
||||
}
|
||||
|
||||
output := ""
|
||||
if file != nil {
|
||||
// this is just making things look nice when the background attribute is 'reverse'
|
||||
firstChar := file.ShortStatus[0:1]
|
||||
firstCharCl := style.FgGreen
|
||||
if firstChar == "?" {
|
||||
firstCharCl = style.FgRed
|
||||
} else if firstChar == " " {
|
||||
firstCharCl = restColor
|
||||
}
|
||||
|
||||
secondChar := file.ShortStatus[1:2]
|
||||
secondCharCl := style.FgRed
|
||||
if secondChar == " " {
|
||||
secondCharCl = restColor
|
||||
}
|
||||
|
||||
output = firstCharCl.Sprint(firstChar)
|
||||
output += secondCharCl.Sprint(secondChar)
|
||||
output += restColor.Sprint(" ")
|
||||
}
|
||||
|
||||
output += restColor.Sprint(utils.EscapeSpecialChars(name))
|
||||
|
||||
if file != nil && file.IsSubmodule(submoduleConfigs) {
|
||||
output += theme.DefaultTextColor.Sprint(" (submodule)")
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func getCommitFileLine(name string, diffName string, commitFile *models.CommitFile, status patch.PatchStatus) string {
|
||||
var colour style.TextStyle
|
||||
if diffName == name {
|
||||
colour = theme.DiffTerminalColor
|
||||
} else {
|
||||
switch status {
|
||||
case patch.WHOLE:
|
||||
colour = style.FgGreen
|
||||
case patch.PART:
|
||||
colour = style.FgYellow
|
||||
case patch.UNSELECTED:
|
||||
colour = theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
|
||||
name = utils.EscapeSpecialChars(name)
|
||||
if commitFile == nil {
|
||||
return colour.Sprint(name)
|
||||
}
|
||||
|
||||
return getColorForChangeStatus(commitFile.ChangeStatus).Sprint(commitFile.ChangeStatus) + " " + colour.Sprint(name)
|
||||
}
|
||||
|
||||
func getColorForChangeStatus(changeStatus string) style.TextStyle {
|
||||
switch changeStatus {
|
||||
case "A":
|
||||
return style.FgGreen
|
||||
case "M", "R":
|
||||
return style.FgYellow
|
||||
case "D":
|
||||
return style.FgRed
|
||||
case "C":
|
||||
return style.FgCyan
|
||||
case "T":
|
||||
return style.FgMagenta
|
||||
default:
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
@ -290,12 +290,12 @@ type guiMutexes struct {
|
||||
type guiState struct {
|
||||
// the file panels (files and commit files) can render as a tree, so we have
|
||||
// managers for them which handle rendering a flat list of files in tree form
|
||||
FileManager *filetree.FileManager
|
||||
CommitFileManager *filetree.CommitFileManager
|
||||
Submodules []*models.SubmoduleConfig
|
||||
Branches []*models.Branch
|
||||
Commits []*models.Commit
|
||||
StashEntries []*models.StashEntry
|
||||
FileTreeViewModel *filetree.FileTreeViewModel
|
||||
CommitFileTreeViewModel *filetree.CommitFileTreeViewModel
|
||||
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.
|
||||
@ -390,13 +390,13 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
}
|
||||
|
||||
gui.State = &guiState{
|
||||
FileManager: filetree.NewFileManager(make([]*models.File, 0), gui.Log, showTree),
|
||||
CommitFileManager: filetree.NewCommitFileManager(make([]*models.CommitFile, 0), gui.Log, showTree),
|
||||
Commits: make([]*models.Commit, 0),
|
||||
FilteredReflogCommits: make([]*models.Commit, 0),
|
||||
ReflogCommits: make([]*models.Commit, 0),
|
||||
StashEntries: make([]*models.StashEntry, 0),
|
||||
BisectInfo: gui.Git.Bisect.GetInfo(),
|
||||
FileTreeViewModel: filetree.NewFileTreeViewModel(make([]*models.File, 0), gui.Log, showTree),
|
||||
CommitFileTreeViewModel: filetree.NewCommitFileTreeViewModel(make([]*models.CommitFile, 0), gui.Log, showTree),
|
||||
Commits: make([]*models.Commit, 0),
|
||||
FilteredReflogCommits: make([]*models.Commit, 0),
|
||||
ReflogCommits: make([]*models.Commit, 0),
|
||||
StashEntries: make([]*models.StashEntry, 0),
|
||||
BisectInfo: gui.Git.Bisect.GetInfo(),
|
||||
Panels: &panelStates{
|
||||
// TODO: work out why some of these are -1 and some are 0. Last time I checked there was a good reason but I'm less certain now
|
||||
Files: &filePanelState{listPanelState{SelectedLineIdx: -1}},
|
||||
|
@ -133,7 +133,7 @@ func (gui *Gui) handleMouseDrag() error {
|
||||
func (gui *Gui) getSelectedCommitFileName() string {
|
||||
idx := gui.State.Panels.CommitFiles.SelectedLineIdx
|
||||
|
||||
return gui.State.CommitFileManager.GetItemAtIndex(idx).GetPath()
|
||||
return gui.State.CommitFileTreeViewModel.GetItemAtIndex(idx).GetPath()
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMainViewForLineByLine(state *LblPanelState) error {
|
||||
|
@ -34,14 +34,14 @@ func (gui *Gui) filesListContext() IListContext {
|
||||
Key: FILES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return gui.State.FileManager.GetItemsLength() },
|
||||
GetItemsLength: func() int { return gui.State.FileTreeViewModel.GetItemsLength() },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Files },
|
||||
OnFocus: OnFocusWrapper(gui.onFocusFile),
|
||||
OnRenderToMain: OnFocusWrapper(gui.filesRenderToMain),
|
||||
OnClickSelectedItem: gui.handleFilePress,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
lines := gui.State.FileManager.Render(gui.State.Modes.Diffing.Ref, gui.State.Submodules)
|
||||
lines := presentation.RenderFileTree(gui.State.FileTreeViewModel, gui.State.Modes.Diffing.Ref, gui.State.Submodules)
|
||||
mappedLines := make([][]string, len(lines))
|
||||
for i, line := range lines {
|
||||
mappedLines[i] = []string{line}
|
||||
@ -309,17 +309,17 @@ func (gui *Gui) commitFilesListContext() IListContext {
|
||||
Key: COMMIT_FILES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
|
||||
GetItemsLength: func() int { return gui.State.CommitFileTreeViewModel.GetItemsLength() },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
|
||||
OnFocus: OnFocusWrapper(gui.onCommitFileFocus),
|
||||
OnRenderToMain: OnFocusWrapper(gui.commitFilesRenderToMain),
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
if gui.State.CommitFileManager.GetItemsLength() == 0 {
|
||||
if gui.State.CommitFileTreeViewModel.GetItemsLength() == 0 {
|
||||
return [][]string{{style.FgRed.Sprint("(none)")}}
|
||||
}
|
||||
|
||||
lines := gui.State.CommitFileManager.Render(gui.State.Modes.Diffing.Ref, gui.Git.Patch.PatchManager)
|
||||
lines := presentation.RenderCommitFileTree(gui.State.CommitFileTreeViewModel, gui.State.Modes.Diffing.Ref, gui.Git.Patch.PatchManager)
|
||||
mappedLines := make([][]string, len(lines))
|
||||
for i, line := range lines {
|
||||
mappedLines[i] = []string{line}
|
||||
|
@ -31,7 +31,7 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
to := gui.State.CommitFileManager.GetParent()
|
||||
to := gui.State.CommitFileTreeViewModel.GetParent()
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
diff, err := gui.Git.WorkingTree.ShowFileDiff(from, to, reverse, node.GetPath(), true)
|
||||
if err != nil {
|
||||
|
204
pkg/gui/presentation/files.go
Normal file
204
pkg/gui/presentation/files.go
Normal file
@ -0,0 +1,204 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const EXPANDED_ARROW = "▼"
|
||||
const COLLAPSED_ARROW = "►"
|
||||
|
||||
const INNER_ITEM = "├─ "
|
||||
const LAST_ITEM = "└─ "
|
||||
const NESTED = "│ "
|
||||
const NOTHING = " "
|
||||
|
||||
func RenderFileTree(
|
||||
fileMgr *filetree.FileTreeViewModel,
|
||||
diffName string,
|
||||
submoduleConfigs []*models.SubmoduleConfig,
|
||||
) []string {
|
||||
return renderAux(fileMgr.Tree(), fileMgr.CollapsedPaths(), "", -1, func(n filetree.INode, depth int) string {
|
||||
castN := n.(*filetree.FileNode)
|
||||
return getFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File)
|
||||
})
|
||||
}
|
||||
|
||||
func RenderCommitFileTree(
|
||||
commitFileMgr *filetree.CommitFileTreeViewModel,
|
||||
diffName string,
|
||||
patchManager *patch.PatchManager,
|
||||
) []string {
|
||||
return renderAux(commitFileMgr.Tree(), commitFileMgr.CollapsedPaths(), "", -1, func(n filetree.INode, depth int) string {
|
||||
castN := n.(*filetree.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, commitFileMgr.GetParent()) == patch.WHOLE
|
||||
}) {
|
||||
status = patch.WHOLE
|
||||
} else if castN.EveryFile(func(file *models.CommitFile) bool {
|
||||
return patchManager.GetFileStatus(file.Name, commitFileMgr.GetParent()) == patch.UNSELECTED
|
||||
}) {
|
||||
status = patch.UNSELECTED
|
||||
} else {
|
||||
status = patch.PART
|
||||
}
|
||||
|
||||
return getCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status)
|
||||
})
|
||||
}
|
||||
|
||||
func renderAux(
|
||||
s filetree.INode,
|
||||
collapsedPaths filetree.CollapsedPaths,
|
||||
prefix string,
|
||||
depth int,
|
||||
renderLine func(filetree.INode, int) string,
|
||||
) []string {
|
||||
if s == nil || s.IsNil() {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
isRoot := depth == -1
|
||||
|
||||
renderLineWithPrefix := func() string {
|
||||
return prefix + renderLine(s, depth)
|
||||
}
|
||||
|
||||
if s.IsLeaf() {
|
||||
if isRoot {
|
||||
return []string{}
|
||||
}
|
||||
return []string{renderLineWithPrefix()}
|
||||
}
|
||||
|
||||
if collapsedPaths.IsCollapsed(s.GetPath()) {
|
||||
return []string{fmt.Sprintf("%s %s", renderLineWithPrefix(), COLLAPSED_ARROW)}
|
||||
}
|
||||
|
||||
arr := []string{}
|
||||
if !isRoot {
|
||||
arr = append(arr, fmt.Sprintf("%s %s", renderLineWithPrefix(), EXPANDED_ARROW))
|
||||
}
|
||||
|
||||
newPrefix := prefix
|
||||
if strings.HasSuffix(prefix, LAST_ITEM) {
|
||||
newPrefix = strings.TrimSuffix(prefix, LAST_ITEM) + NOTHING
|
||||
} else if strings.HasSuffix(prefix, INNER_ITEM) {
|
||||
newPrefix = strings.TrimSuffix(prefix, INNER_ITEM) + NESTED
|
||||
}
|
||||
|
||||
for i, child := range s.GetChildren() {
|
||||
isLast := i == len(s.GetChildren())-1
|
||||
|
||||
var childPrefix string
|
||||
if isRoot {
|
||||
childPrefix = newPrefix
|
||||
} else if isLast {
|
||||
childPrefix = newPrefix + LAST_ITEM
|
||||
} else {
|
||||
childPrefix = newPrefix + INNER_ITEM
|
||||
}
|
||||
|
||||
arr = append(arr, renderAux(child, collapsedPaths, childPrefix, depth+1+s.GetCompressionLevel(), renderLine)...)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
func getFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, diffName string, submoduleConfigs []*models.SubmoduleConfig, file *models.File) string {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
partiallyModifiedColor := style.FgYellow
|
||||
|
||||
restColor := style.FgGreen
|
||||
if name == diffName {
|
||||
restColor = theme.DiffTerminalColor
|
||||
} else if file == nil && hasStagedChanges && hasUnstagedChanges {
|
||||
restColor = partiallyModifiedColor
|
||||
} else if hasUnstagedChanges {
|
||||
restColor = style.FgRed
|
||||
}
|
||||
|
||||
output := ""
|
||||
if file != nil {
|
||||
// this is just making things look nice when the background attribute is 'reverse'
|
||||
firstChar := file.ShortStatus[0:1]
|
||||
firstCharCl := style.FgGreen
|
||||
if firstChar == "?" {
|
||||
firstCharCl = style.FgRed
|
||||
} else if firstChar == " " {
|
||||
firstCharCl = restColor
|
||||
}
|
||||
|
||||
secondChar := file.ShortStatus[1:2]
|
||||
secondCharCl := style.FgRed
|
||||
if secondChar == " " {
|
||||
secondCharCl = restColor
|
||||
}
|
||||
|
||||
output = firstCharCl.Sprint(firstChar)
|
||||
output += secondCharCl.Sprint(secondChar)
|
||||
output += restColor.Sprint(" ")
|
||||
}
|
||||
|
||||
output += restColor.Sprint(utils.EscapeSpecialChars(name))
|
||||
|
||||
if file != nil && file.IsSubmodule(submoduleConfigs) {
|
||||
output += theme.DefaultTextColor.Sprint(" (submodule)")
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func getCommitFileLine(name string, diffName string, commitFile *models.CommitFile, status patch.PatchStatus) string {
|
||||
var colour style.TextStyle
|
||||
if diffName == name {
|
||||
colour = theme.DiffTerminalColor
|
||||
} else {
|
||||
switch status {
|
||||
case patch.WHOLE:
|
||||
colour = style.FgGreen
|
||||
case patch.PART:
|
||||
colour = style.FgYellow
|
||||
case patch.UNSELECTED:
|
||||
colour = theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
|
||||
name = utils.EscapeSpecialChars(name)
|
||||
if commitFile == nil {
|
||||
return colour.Sprint(name)
|
||||
}
|
||||
|
||||
return getColorForChangeStatus(commitFile.ChangeStatus).Sprint(commitFile.ChangeStatus) + " " + colour.Sprint(name)
|
||||
}
|
||||
|
||||
func getColorForChangeStatus(changeStatus string) style.TextStyle {
|
||||
switch changeStatus {
|
||||
case "A":
|
||||
return style.FgGreen
|
||||
case "M", "R":
|
||||
return style.FgYellow
|
||||
case "D":
|
||||
return style.FgRed
|
||||
case "C":
|
||||
return style.FgCyan
|
||||
case "T":
|
||||
return style.FgMagenta
|
||||
default:
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
146
pkg/gui/presentation/files_test.go
Normal file
146
pkg/gui/presentation/files_test.go
Normal file
@ -0,0 +1,146 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xo/terminfo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
color.ForceSetColorLevel(terminfo.ColorLevelNone)
|
||||
}
|
||||
|
||||
func toStringSlice(str string) []string {
|
||||
return strings.Split(strings.TrimSpace(str), "\n")
|
||||
}
|
||||
|
||||
func TestRenderFileTree(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
root *filetree.FileNode
|
||||
files []*models.File
|
||||
collapsedPaths []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "nil node",
|
||||
files: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "leaf node",
|
||||
files: []*models.File{
|
||||
{Name: "test", ShortStatus: " M", HasStagedChanges: true},
|
||||
},
|
||||
expected: []string{" M test"},
|
||||
},
|
||||
{
|
||||
name: "big example",
|
||||
files: []*models.File{
|
||||
{Name: "dir1/file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir1/file3", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/dir2/file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
expected: toStringSlice(
|
||||
`
|
||||
dir1 ►
|
||||
dir2 ▼
|
||||
├─ dir2 ▼
|
||||
│ ├─ M file3
|
||||
│ └─ M file4
|
||||
└─ M file5
|
||||
M file1
|
||||
`,
|
||||
),
|
||||
collapsedPaths: []string{"dir1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
viewModel := filetree.NewFileTreeViewModel(s.files, utils.NewDummyLog(), true)
|
||||
for _, path := range s.collapsedPaths {
|
||||
viewModel.ToggleCollapsed(path)
|
||||
}
|
||||
result := RenderFileTree(viewModel, "", nil)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCommitFileTree(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
root *filetree.FileNode
|
||||
files []*models.CommitFile
|
||||
collapsedPaths []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "nil node",
|
||||
files: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "leaf node",
|
||||
files: []*models.CommitFile{
|
||||
{Name: "test", ChangeStatus: "A"},
|
||||
},
|
||||
expected: []string{"A test"},
|
||||
},
|
||||
{
|
||||
name: "big example",
|
||||
files: []*models.CommitFile{
|
||||
{Name: "dir1/file2", ChangeStatus: "M"},
|
||||
{Name: "dir1/file3", ChangeStatus: "A"},
|
||||
{Name: "dir2/dir2/file3", ChangeStatus: "D"},
|
||||
{Name: "dir2/dir2/file4", ChangeStatus: "M"},
|
||||
{Name: "dir2/file5", ChangeStatus: "M"},
|
||||
{Name: "file1", ChangeStatus: "M"},
|
||||
},
|
||||
expected: toStringSlice(
|
||||
`
|
||||
dir1 ►
|
||||
dir2 ▼
|
||||
├─ dir2 ▼
|
||||
│ ├─ D file3
|
||||
│ └─ M file4
|
||||
└─ M file5
|
||||
M file1
|
||||
`,
|
||||
),
|
||||
collapsedPaths: []string{"dir1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
viewModel := filetree.NewCommitFileTreeViewModel(s.files, utils.NewDummyLog(), true)
|
||||
for _, path := range s.collapsedPaths {
|
||||
viewModel.ToggleCollapsed(path)
|
||||
}
|
||||
patchManager := patch.NewPatchManager(
|
||||
utils.NewDummyLog(),
|
||||
func(patch string, flags ...string) error { return nil },
|
||||
func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
)
|
||||
patchManager.Start("from", "to", false, false)
|
||||
result := RenderCommitFileTree(viewModel, "", patchManager)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
@ -96,7 +96,7 @@ func (gui *Gui) handleResetSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File {
|
||||
for _, file := range gui.State.FileManager.GetAllFiles() {
|
||||
for _, file := range gui.State.FileTreeViewModel.GetAllFiles() {
|
||||
if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) {
|
||||
return file
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user