dive/dive/filetree/file_tree.go

392 lines
12 KiB
Go

package filetree
import (
"fmt"
"path"
"strings"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
const (
newLine = "\n"
noBranchSpace = " "
branchSpace = "│ "
middleItem = "├─"
lastItem = "└─"
whiteoutPrefix = ".wh."
doubleWhiteoutPrefix = ".wh..wh.."
uncollapsedItem = "─ "
collapsedItem = "⊕ "
)
// FileTree represents a set of files, directories, and their relations.
type FileTree struct {
Root *FileNode
Size int
FileSize uint64
Name string
Id uuid.UUID
SortOrder SortOrder
}
// NewFileTree creates an empty FileTree
func NewFileTree() (tree *FileTree) {
tree = new(FileTree)
tree.Size = 0
tree.Root = new(FileNode)
tree.Root.Tree = tree
tree.Root.Children = make(map[string]*FileNode)
tree.Id = uuid.New()
tree.SortOrder = ByName
return tree
}
// renderParams is a representation of a FileNode in the context of the greater tree. All
// data stored is necessary for rendering a single line in a tree format.
type renderParams struct {
node *FileNode
spaces []bool
childSpaces []bool
showCollapsed bool
isLast bool
}
// renderStringTreeBetween returns a string representing the given tree between the given rows. Since each node
// is rendered on its own line, the returned string shows the visible nodes not affected by a collapsed parent.
func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttributes bool) string {
// generate a list of nodes to render
var params = make([]renderParams, 0)
var result string
// visit from the front of the list
var paramsToVisit = []renderParams{{node: tree.Root, spaces: []bool{}, showCollapsed: false, isLast: false}}
for currentRow := 0; len(paramsToVisit) > 0 && currentRow <= stopRow; currentRow++ {
// pop the first node
var currentParams renderParams
currentParams, paramsToVisit = paramsToVisit[0], paramsToVisit[1:]
// take note of the next nodes to visit later
sorter := GetSortOrderStrategy(tree.SortOrder)
keys := sorter.orderKeys(currentParams.node.Children)
var childParams = make([]renderParams, 0)
for idx, name := range keys {
child := currentParams.node.Children[name]
// don't visit this node...
if child.Data.ViewInfo.Hidden || currentParams.node.Data.ViewInfo.Collapsed {
continue
}
// visit this node...
isLast := idx == (len(currentParams.node.Children) - 1)
showCollapsed := child.Data.ViewInfo.Collapsed && len(child.Children) > 0
// completely copy the reference slice
childSpaces := make([]bool, len(currentParams.childSpaces))
copy(childSpaces, currentParams.childSpaces)
if len(child.Children) > 0 && !child.Data.ViewInfo.Collapsed {
childSpaces = append(childSpaces, isLast)
}
childParams = append(childParams, renderParams{
node: child,
spaces: currentParams.childSpaces,
childSpaces: childSpaces,
showCollapsed: showCollapsed,
isLast: isLast,
})
}
// keep the child nodes to visit later
paramsToVisit = append(childParams, paramsToVisit...)
// never process the root node
if currentParams.node == tree.Root {
currentRow--
continue
}
// process the current node
if currentRow >= startRow && currentRow <= stopRow {
params = append(params, currentParams)
}
}
// render the result
for idx := range params {
currentParams := params[idx]
if showAttributes {
result += currentParams.node.MetadataString() + " "
}
result += currentParams.node.renderTreeLine(currentParams.spaces, currentParams.isLast, currentParams.showCollapsed)
}
return result
}
func (tree *FileTree) VisibleSize() int {
var size int
visitor := func(node *FileNode) error {
size++
return nil
}
visitEvaluator := func(node *FileNode) bool {
if node.Data.FileInfo.IsDir {
// we won't visit a collapsed dir, but we need to count it
if node.Data.ViewInfo.Collapsed {
size++
}
return !node.Data.ViewInfo.Collapsed && !node.Data.ViewInfo.Hidden
}
return !node.Data.ViewInfo.Hidden
}
err := tree.VisitDepthParentFirst(visitor, visitEvaluator)
if err != nil {
logrus.Errorf("unable to determine visible tree size: %+v", err)
}
// don't include root
size--
return size
}
// String returns the entire tree in an ASCII representation.
func (tree *FileTree) String(showAttributes bool) string {
return tree.renderStringTreeBetween(0, tree.Size, showAttributes)
}
// StringBetween returns a partial tree in an ASCII representation.
func (tree *FileTree) StringBetween(start, stop int, showAttributes bool) string {
return tree.renderStringTreeBetween(start, stop, showAttributes)
}
// Copy returns a copy of the given FileTree
func (tree *FileTree) Copy() *FileTree {
newTree := NewFileTree()
newTree.Size = tree.Size
newTree.FileSize = tree.FileSize
newTree.Root = tree.Root.Copy(newTree.Root)
newTree.SortOrder = tree.SortOrder
// update the tree pointers
err := newTree.VisitDepthChildFirst(func(node *FileNode) error {
node.Tree = newTree
return nil
}, nil)
if err != nil {
logrus.Errorf("unable to propagate tree on copy(): %+v", err)
}
return newTree
}
// Visitor is a function that processes, observes, or otherwise transforms the given node
type Visitor func(*FileNode) error
// VisitEvaluator is a function that indicates whether the given node should be visited by a Visitor.
type VisitEvaluator func(*FileNode) bool
// VisitDepthChildFirst iterates the given tree depth-first, evaluating the deepest depths first (visit on bubble up)
func (tree *FileTree) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator) error {
sorter := GetSortOrderStrategy(tree.SortOrder)
return tree.Root.VisitDepthChildFirst(visitor, evaluator, sorter)
}
// VisitDepthParentFirst iterates the given tree depth-first, evaluating the shallowest depths first (visit while sinking down)
func (tree *FileTree) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator) error {
sorter := GetSortOrderStrategy(tree.SortOrder)
return tree.Root.VisitDepthParentFirst(visitor, evaluator, sorter)
}
// Stack takes two trees and combines them together. This is done by "stacking" the given tree on top of the owning tree.
func (tree *FileTree) Stack(upper *FileTree) (failed []PathError, stackErr error) {
graft := func(node *FileNode) error {
if node.IsWhiteout() {
err := tree.RemovePath(node.Path())
if err != nil {
failed = append(failed, NewPathError(node.Path(), ActionAdd, err))
}
} else {
_, _, err := tree.AddPath(node.Path(), node.Data.FileInfo)
if err != nil {
failed = append(failed, NewPathError(node.Path(), ActionRemove, err))
}
}
return nil
}
stackErr = upper.VisitDepthChildFirst(graft, nil)
return failed, stackErr
}
// GetNode fetches a single node when given a slash-delimited string from root ('/') to the desired node (e.g. '/a/node/path')
func (tree *FileTree) GetNode(path string) (*FileNode, error) {
nodeNames := strings.Split(strings.Trim(path, "/"), "/")
node := tree.Root
for _, name := range nodeNames {
if name == "" {
continue
}
if node.Children[name] == nil {
return nil, fmt.Errorf("path does not exist: %s", path)
}
node = node.Children[name]
}
return node, nil
}
// AddPath adds a new node to the tree with the given payload
func (tree *FileTree) AddPath(filepath string, data FileInfo) (*FileNode, []*FileNode, error) {
filepath = path.Clean(filepath)
if filepath == "." {
return nil, nil, fmt.Errorf("cannot add relative path '%s'", filepath)
}
nodeNames := strings.Split(strings.Trim(filepath, "/"), "/")
node := tree.Root
addedNodes := make([]*FileNode, 0)
for idx, name := range nodeNames {
if name == "" {
continue
}
// find or create node
if node.Children[name] != nil {
node = node.Children[name]
} else {
// don't add paths that should be deleted
if strings.HasPrefix(name, doubleWhiteoutPrefix) {
return nil, addedNodes, nil
}
// don't attach the payload. The payload is destined for the
// Path's end node, not any intermediary node.
node = node.AddChild(name, FileInfo{})
addedNodes = append(addedNodes, node)
if node == nil {
// the child could not be added
return node, addedNodes, fmt.Errorf(fmt.Sprintf("could not add child node: '%s' (path:'%s')", name, filepath))
}
}
// attach payload to the last specified node
if idx == len(nodeNames)-1 {
node.Data.FileInfo = data
}
}
return node, addedNodes, nil
}
// RemovePath removes a node from the tree given its path.
func (tree *FileTree) RemovePath(path string) error {
node, err := tree.GetNode(path)
if err != nil {
return err
}
return node.Remove()
}
type compareMark struct {
lowerNode *FileNode
upperNode *FileNode
tentative DiffType
final DiffType
}
// CompareAndMark marks the FileNodes in the owning (lower) tree with DiffType annotations when compared to the given (upper) tree.
func (tree *FileTree) CompareAndMark(upper *FileTree) ([]PathError, error) {
// always compare relative to the original, unaltered tree.
originalTree := tree
modifications := make([]compareMark, 0)
failed := make([]PathError, 0)
graft := func(upperNode *FileNode) error {
if upperNode.IsWhiteout() {
err := tree.markRemoved(upperNode.Path())
if err != nil {
failed = append(failed, NewPathError(upperNode.Path(), ActionRemove, err))
}
return nil
}
// note: since we are not comparing against the original tree (copying the tree is expensive) we may mark the parent
// of an added node incorrectly as modified. This will be corrected later.
originalLowerNode, _ := originalTree.GetNode(upperNode.Path())
if originalLowerNode == nil {
_, newNodes, err := tree.AddPath(upperNode.Path(), upperNode.Data.FileInfo)
if err != nil {
failed = append(failed, NewPathError(upperNode.Path(), ActionAdd, err))
return nil
}
for idx := len(newNodes) - 1; idx >= 0; idx-- {
newNode := newNodes[idx]
modifications = append(modifications, compareMark{lowerNode: newNode, upperNode: upperNode, tentative: -1, final: Added})
}
return nil
}
// the file exists in the lower layer
lowerNode, _ := tree.GetNode(upperNode.Path())
diffType := lowerNode.compare(upperNode)
modifications = append(modifications, compareMark{lowerNode: lowerNode, upperNode: upperNode, tentative: diffType, final: -1})
return nil
}
// we must visit from the leaves upwards to ensure that diff types can be derived from and assigned to children
err := upper.VisitDepthChildFirst(graft, nil)
if err != nil {
return failed, err
}
// take note of the comparison results on each note in the owning tree.
for _, pair := range modifications {
if pair.final > 0 {
err = pair.lowerNode.AssignDiffType(pair.final)
if err != nil {
return failed, err
}
} else if pair.lowerNode.Data.DiffType == Unmodified {
err = pair.lowerNode.deriveDiffType(pair.tentative)
if err != nil {
return failed, err
}
}
// persist the upper's payload on the owning tree
pair.lowerNode.Data.FileInfo = *pair.upperNode.Data.FileInfo.Copy()
}
return failed, nil
}
// markRemoved annotates the FileNode at the given path as Removed.
func (tree *FileTree) markRemoved(path string) error {
node, err := tree.GetNode(path)
if err != nil {
return err
}
return node.AssignDiffType(Removed)
}
// StackTreeRange combines an array of trees into a single tree
func StackTreeRange(trees []*FileTree, start, stop int) (*FileTree, []PathError, error) {
errors := make([]PathError, 0)
tree := trees[0].Copy()
for idx := start; idx <= stop; idx++ {
failedPaths, err := tree.Stack(trees[idx])
if len(failedPaths) > 0 {
errors = append(errors, failedPaths...)
}
if err != nil {
logrus.Errorf("could not stack tree range: %v", err)
return nil, nil, err
}
}
return tree, errors, nil
}