229 lines
5.3 KiB
Go
229 lines
5.3 KiB
Go
package filetree
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"github.com/satori/go.uuid"
|
|
)
|
|
|
|
const (
|
|
newLine = "\n"
|
|
noBranchSpace = " "
|
|
branchSpace = "│ "
|
|
middleItem = "├─"
|
|
lastItem = "└─"
|
|
whiteoutPrefix = ".wh."
|
|
uncollapsedItem = "─ "
|
|
collapsedItem = "⊕ "
|
|
)
|
|
|
|
type FileTree struct {
|
|
Root *FileNode
|
|
Size int
|
|
FileSize uint64
|
|
Name string
|
|
Id uuid.UUID
|
|
}
|
|
|
|
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.Must(uuid.NewV4())
|
|
return tree
|
|
}
|
|
|
|
func (tree *FileTree) String(showAttributes bool) string {
|
|
return tree.Root.renderStringTree([]bool{}, showAttributes, 0)
|
|
}
|
|
|
|
func (tree *FileTree) Copy() *FileTree {
|
|
newTree := NewFileTree()
|
|
newTree.Size = tree.Size
|
|
newTree.FileSize = tree.FileSize
|
|
newTree.Root = tree.Root.Copy(newTree.Root)
|
|
|
|
// update the tree pointers
|
|
newTree.VisitDepthChildFirst(func(node *FileNode) error {
|
|
node.Tree = newTree
|
|
return nil
|
|
}, nil)
|
|
|
|
return newTree
|
|
}
|
|
|
|
type Visiter func(*FileNode) error
|
|
type VisitEvaluator func(*FileNode) bool
|
|
|
|
// DFS bubble up
|
|
func (tree *FileTree) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvaluator) error {
|
|
return tree.Root.VisitDepthChildFirst(visiter, evaluator)
|
|
}
|
|
|
|
// DFS sink down
|
|
func (tree *FileTree) VisitDepthParentFirst(visiter Visiter, evaluator VisitEvaluator) error {
|
|
return tree.Root.VisitDepthParentFirst(visiter, evaluator)
|
|
}
|
|
|
|
func (tree *FileTree) Stack(upper *FileTree) error {
|
|
graft := func(node *FileNode) error {
|
|
if node.IsWhiteout() {
|
|
err := tree.RemovePath(node.Path())
|
|
if err != nil {
|
|
return fmt.Errorf("cannot remove node %s: %v", node.Path(), err.Error())
|
|
}
|
|
} else {
|
|
newNode, err := tree.AddPath(node.Path(), node.Data.FileInfo)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot add node %s: %v", newNode.Path(), err.Error())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return upper.VisitDepthChildFirst(graft, nil)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, error) {
|
|
nodeNames := strings.Split(strings.Trim(path, "/"), "/")
|
|
node := tree.Root
|
|
for idx, name := range nodeNames {
|
|
if name == "" {
|
|
continue
|
|
}
|
|
// find or create node
|
|
if node.Children[name] != nil {
|
|
node = node.Children[name]
|
|
} else {
|
|
// don't attach the payload. The payload is destined for the
|
|
// Path's end node, not any intermediary node.
|
|
node = node.AddChild(name, FileInfo{})
|
|
}
|
|
|
|
// attach payload to the last specified node
|
|
if idx == len(nodeNames)-1 {
|
|
node.Data.FileInfo = data
|
|
}
|
|
|
|
}
|
|
return node, nil
|
|
}
|
|
|
|
func (tree *FileTree) RemovePath(path string) error {
|
|
node, err := tree.GetNode(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return node.Remove()
|
|
}
|
|
|
|
func (tree *FileTree) Compare(upper *FileTree) error {
|
|
graft := func(upperNode *FileNode) error {
|
|
if upperNode.IsWhiteout() {
|
|
err := tree.MarkRemoved(upperNode.Path())
|
|
if err != nil {
|
|
return fmt.Errorf("cannot remove upperNode %s: %v", upperNode.Path(), err.Error())
|
|
}
|
|
} else {
|
|
lowerNode, _ := tree.GetNode(upperNode.Path())
|
|
if lowerNode == nil {
|
|
newNode, err := tree.AddPath(upperNode.Path(), upperNode.Data.FileInfo)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot add new upperNode %s: %v", upperNode.Path(), err.Error())
|
|
}
|
|
newNode.AssignDiffType(Added)
|
|
} else {
|
|
diffType := lowerNode.compare(upperNode)
|
|
return lowerNode.deriveDiffType(diffType)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return upper.VisitDepthChildFirst(graft, nil)
|
|
}
|
|
|
|
func (tree *FileTree) MarkRemoved(path string) error {
|
|
node, err := tree.GetNode(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return node.AssignDiffType(Removed)
|
|
}
|
|
|
|
// memoize StackRange for performance
|
|
type stackRangeCacheKey struct {
|
|
// Ids mapset.Set
|
|
start, stop int
|
|
}
|
|
|
|
var stackRangeCache = make(map[stackRangeCacheKey]*FileTree)
|
|
|
|
func StackRange(trees []*FileTree, start, stop int) *FileTree {
|
|
|
|
// var ids []interface{}
|
|
//
|
|
// for _, tree := range trees {
|
|
// ids = append(ids, tree.Id)
|
|
// }
|
|
//mapset.NewSetFromSlice(ids)
|
|
// key := stackRangeCacheKey{start, stop}
|
|
//
|
|
//
|
|
// cachedResult, ok := stackRangeCache[key]
|
|
// if ok {
|
|
// return cachedResult
|
|
// }
|
|
|
|
tree := trees[0].Copy()
|
|
for idx := start; idx <= stop; idx++ {
|
|
tree.Stack(trees[idx])
|
|
}
|
|
|
|
// stackRangeCache[key] = tree
|
|
|
|
return tree
|
|
}
|
|
|
|
// EfficiencyMap creates a map[string]int showing how often each int
|
|
// appears in the
|
|
func EfficiencyMap(trees []*FileTree) map[string]int {
|
|
result := make(map[string]int)
|
|
visitor := func(node *FileNode) error {
|
|
result[node.Path()]++
|
|
return nil
|
|
}
|
|
visitEvaluator := func(node *FileNode) bool {
|
|
return node.IsLeaf()
|
|
}
|
|
for _, tree := range trees {
|
|
tree.VisitDepthChildFirst(visitor, visitEvaluator)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func EfficiencyScore(trees []*FileTree) float64 {
|
|
efficiencyMap := EfficiencyMap(trees)
|
|
uniquePaths := len(efficiencyMap)
|
|
pathAppearances := 0
|
|
for _, value := range efficiencyMap {
|
|
pathAppearances += value
|
|
}
|
|
return float64(uniquePaths) / float64(pathAppearances)
|
|
}
|