dive/dive/filetree/file_node.go
Alex Goodman d5e8a92968
Rework CI validation workflow and makefile (#460)
* rework CI validation workflow and makefile

* enable push

* fix job names

* fix license check

* fix snapshot builds

* fix acceptance tests

* fix linting

* disable pull request event

* rework windows runner caching

* disable release pipeline and add issue templates
2023-07-06 22:01:46 -04:00

325 lines
8.2 KiB
Go

package filetree
import (
"archive/tar"
"fmt"
"sort"
"strings"
"github.com/dustin/go-humanize"
"github.com/fatih/color"
"github.com/phayes/permbits"
"github.com/sirupsen/logrus"
)
const (
AttributeFormat = "%s%s %11s %10s "
)
var diffTypeColor = map[DiffType]*color.Color{
Added: color.New(color.FgGreen),
Removed: color.New(color.FgRed),
Modified: color.New(color.FgYellow),
Unmodified: color.New(color.Reset),
}
// FileNode represents a single file, its relation to files beneath it, the tree it exists in, and the metadata of the given file.
type FileNode struct {
Tree *FileTree
Parent *FileNode
Name string
Data NodeData
Children map[string]*FileNode
path string
}
// NewNode creates a new FileNode relative to the given parent node with a payload.
func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) {
node = new(FileNode)
node.Name = name
node.Data = *NewNodeData()
node.Data.FileInfo = *data.Copy()
node.Children = make(map[string]*FileNode)
node.Parent = parent
if parent != nil {
node.Tree = parent.Tree
}
return node
}
// renderTreeLine returns a string representing this FileNode in the context of a greater ASCII tree.
func (node *FileNode) renderTreeLine(spaces []bool, last bool, collapsed bool) string {
var otherBranches string
for _, space := range spaces {
if space {
otherBranches += noBranchSpace
} else {
otherBranches += branchSpace
}
}
thisBranch := middleItem
if last {
thisBranch = lastItem
}
collapsedIndicator := uncollapsedItem
if collapsed {
collapsedIndicator = collapsedItem
}
return otherBranches + thisBranch + collapsedIndicator + node.String() + newLine
}
// Copy duplicates the existing node relative to a new parent node.
func (node *FileNode) Copy(parent *FileNode) *FileNode {
newNode := NewNode(parent, node.Name, node.Data.FileInfo)
newNode.Data.ViewInfo = node.Data.ViewInfo
newNode.Data.DiffType = node.Data.DiffType
for name, child := range node.Children {
newNode.Children[name] = child.Copy(newNode)
child.Parent = newNode
}
return newNode
}
// AddChild creates a new node relative to the current FileNode.
func (node *FileNode) AddChild(name string, data FileInfo) (child *FileNode) {
// never allow processing of purely whiteout flag files (for now)
if strings.HasPrefix(name, doubleWhiteoutPrefix) {
return nil
}
child = NewNode(node, name, data)
if node.Children[name] != nil {
// tree node already exists, replace the payload, keep the children
node.Children[name].Data.FileInfo = *data.Copy()
} else {
node.Children[name] = child
node.Tree.Size++
}
return child
}
// Remove deletes the current FileNode from it's parent FileNode's relations.
func (node *FileNode) Remove() error {
if node == node.Tree.Root {
return fmt.Errorf("cannot remove the tree root")
}
for _, child := range node.Children {
err := child.Remove()
if err != nil {
return err
}
}
delete(node.Parent.Children, node.Name)
node.Tree.Size--
return nil
}
// String shows the filename formatted into the proper color (by DiffType), additionally indicating if it is a symlink.
func (node *FileNode) String() string {
var display string
if node == nil {
return ""
}
display = node.Name
if node.Data.FileInfo.TypeFlag == tar.TypeSymlink || node.Data.FileInfo.TypeFlag == tar.TypeLink {
display += " → " + node.Data.FileInfo.Linkname
}
return diffTypeColor[node.Data.DiffType].Sprint(display)
}
// MetadatString returns the FileNode metadata in a columnar string.
func (node *FileNode) MetadataString() string {
if node == nil {
return ""
}
fileMode := permbits.FileMode(node.Data.FileInfo.Mode).String()
dir := "-"
if node.Data.FileInfo.IsDir {
dir = "d"
}
user := node.Data.FileInfo.Uid
group := node.Data.FileInfo.Gid
userGroup := fmt.Sprintf("%d:%d", user, group)
var sizeBytes int64
if node.IsLeaf() {
sizeBytes = node.Data.FileInfo.Size
} else {
sizer := func(curNode *FileNode) error {
// don't include file sizes of children that have been removed (unless the node in question is a removed dir,
// then show the accumulated size of removed files)
if curNode.Data.DiffType != Removed || node.Data.DiffType == Removed {
sizeBytes += curNode.Data.FileInfo.Size
}
return nil
}
err := node.VisitDepthChildFirst(sizer, nil)
if err != nil {
logrus.Errorf("unable to propagate node for metadata: %+v", err)
}
}
size := humanize.Bytes(uint64(sizeBytes))
return diffTypeColor[node.Data.DiffType].Sprint(fmt.Sprintf(AttributeFormat, dir, fileMode, userGroup, size))
}
// VisitDepthChildFirst iterates a tree depth-first (starting at this FileNode), evaluating the deepest depths first (visit on bubble up)
func (node *FileNode) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator) error {
var keys []string
for key := range node.Children {
keys = append(keys, key)
}
sort.Strings(keys)
for _, name := range keys {
child := node.Children[name]
err := child.VisitDepthChildFirst(visitor, evaluator)
if err != nil {
return err
}
}
// never visit the root node
if node == node.Tree.Root {
return nil
} else if evaluator != nil && evaluator(node) || evaluator == nil {
return visitor(node)
}
return nil
}
// VisitDepthParentFirst iterates a tree depth-first (starting at this FileNode), evaluating the shallowest depths first (visit while sinking down)
func (node *FileNode) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator) error {
var err error
doVisit := evaluator != nil && evaluator(node) || evaluator == nil
if !doVisit {
return nil
}
// never visit the root node
if node != node.Tree.Root {
err = visitor(node)
if err != nil {
return err
}
}
var keys []string
for key := range node.Children {
keys = append(keys, key)
}
sort.Strings(keys)
for _, name := range keys {
child := node.Children[name]
err = child.VisitDepthParentFirst(visitor, evaluator)
if err != nil {
return err
}
}
return err
}
// IsWhiteout returns an indication if this file may be a overlay-whiteout file.
func (node *FileNode) IsWhiteout() bool {
return strings.HasPrefix(node.Name, whiteoutPrefix)
}
// IsLeaf returns true is the current node has no child nodes.
func (node *FileNode) IsLeaf() bool {
return len(node.Children) == 0
}
// Path returns a slash-delimited string from the root of the greater tree to the current node (e.g. /a/path/to/here)
func (node *FileNode) Path() string {
if node.path == "" {
var path []string
curNode := node
for {
if curNode.Parent == nil {
break
}
name := curNode.Name
if curNode == node {
// white out prefixes are fictitious on leaf nodes
name = strings.TrimPrefix(name, whiteoutPrefix)
}
path = append([]string{name}, path...)
curNode = curNode.Parent
}
node.path = "/" + strings.Join(path, "/")
}
return strings.Replace(node.path, "//", "/", -1)
}
// deriveDiffType determines a DiffType to the current FileNode. Note: the DiffType of a node is always the DiffType of
// its attributes and its contents. The contents are the bytes of the file of the children of a directory.
func (node *FileNode) deriveDiffType(diffType DiffType) error {
if node.IsLeaf() {
return node.AssignDiffType(diffType)
}
myDiffType := diffType
for _, v := range node.Children {
myDiffType = myDiffType.merge(v.Data.DiffType)
}
return node.AssignDiffType(myDiffType)
}
// AssignDiffType will assign the given DiffType to this node, possibly affecting child nodes.
func (node *FileNode) AssignDiffType(diffType DiffType) error {
var err error
node.Data.DiffType = diffType
if diffType == Removed {
// if we've removed this node, then all children have been removed as well
for _, child := range node.Children {
err = child.AssignDiffType(diffType)
if err != nil {
return err
}
}
}
return nil
}
// compare the current node against the given node, returning a definitive DiffType.
func (node *FileNode) compare(other *FileNode) DiffType {
if node == nil && other == nil {
return Unmodified
}
if node == nil && other != nil {
return Added
}
if node != nil && other == nil {
return Removed
}
if other.IsWhiteout() {
return Removed
}
if node.Name != other.Name {
panic("comparing mismatched nodes")
}
return node.Data.FileInfo.Compare(other.Data.FileInfo)
}