summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Goodman <wagoodman@gmail.com>2019-09-21 16:28:45 -0400
committerAlex Goodman <wagoodman@gmail.com>2019-09-21 16:28:45 -0400
commit576709ad30571d7fb11d15229719c693cf0e92c4 (patch)
treedebb1e2e4a4cb305149e51f4856e600d68a4c719
parent4b73bc89b8bb9782443c569ecb5edbb3a8f6260c (diff)
rework package structure
-rw-r--r--Makefile2
-rw-r--r--cmd/root.go2
-rw-r--r--filetree/cache.go79
-rw-r--r--filetree/data.go161
-rw-r--r--filetree/data_test.go41
-rw-r--r--filetree/efficiency.go111
-rw-r--r--filetree/efficiency_test.go80
-rw-r--r--filetree/node.go315
-rw-r--r--filetree/node_test.go168
-rw-r--r--filetree/tree.go371
-rw-r--r--filetree/tree_test.go791
-rw-r--r--filetree/types.go66
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--image/docker_image.go293
-rw-r--r--image/docker_layer.go78
-rw-r--r--image/root.go10
-rw-r--r--image/testing.go19
-rw-r--r--image/types.go80
-rw-r--r--runtime/ci/evaluator.go (renamed from runtime/ci_evaluator.go)11
-rw-r--r--runtime/ci/evaluator_test.go (renamed from runtime/ci_evaluator_test.go)6
-rw-r--r--runtime/ci/reference_file.go (renamed from runtime/reference_file.go)2
-rw-r--r--runtime/ci/rule.go (renamed from runtime/ci_rule.go)6
-rw-r--r--runtime/export/export.go (renamed from runtime/export.go)27
-rw-r--r--runtime/export/export_test.go (renamed from runtime/export_test.go)11
-rw-r--r--runtime/run.go32
-rw-r--r--runtime/ui/details_controller.go (renamed from ui/details_controller.go)2
-rw-r--r--runtime/ui/filetree_controller.go (renamed from ui/filetree_controller.go)2
-rw-r--r--runtime/ui/filetree_viewmodel.go (renamed from ui/filetree_viewmodel.go)2
-rw-r--r--runtime/ui/filetree_viewmodel_test.go (renamed from ui/filetree_viewmodel_test.go)6
-rw-r--r--runtime/ui/filter_controller.go (renamed from ui/filter_controller.go)0
-rw-r--r--runtime/ui/layer_controller.go (renamed from ui/layer_controller.go)3
-rw-r--r--runtime/ui/status_controller.go (renamed from ui/status_controller.go)0
-rw-r--r--runtime/ui/testdata/TestFileShowAggregateChanges.txt (renamed from ui/testdata/TestFileShowAggregateChanges.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreeDirCollapse.txt (renamed from ui/testdata/TestFileTreeDirCollapse.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreeDirCollapseAll.txt (renamed from ui/testdata/TestFileTreeDirCollapseAll.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreeDirCursorRight.txt (renamed from ui/testdata/TestFileTreeDirCursorRight.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreeFilterTree.txt (renamed from ui/testdata/TestFileTreeFilterTree.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreeGoCase.txt (renamed from ui/testdata/TestFileTreeGoCase.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreeHideAddedRemovedModified.txt (renamed from ui/testdata/TestFileTreeHideAddedRemovedModified.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreeHideTypeWithFilter.txt (renamed from ui/testdata/TestFileTreeHideTypeWithFilter.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreeHideUnmodified.txt (renamed from ui/testdata/TestFileTreeHideUnmodified.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreeNoAttributes.txt (renamed from ui/testdata/TestFileTreeNoAttributes.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreePageDown.txt (renamed from ui/testdata/TestFileTreePageDown.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreePageUp.txt (renamed from ui/testdata/TestFileTreePageUp.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreeRestrictedHeight.txt (renamed from ui/testdata/TestFileTreeRestrictedHeight.txt)0
-rw-r--r--runtime/ui/testdata/TestFileTreeSelectLayer.txt (renamed from ui/testdata/TestFileTreeSelectLayer.txt)0
-rw-r--r--runtime/ui/ui.go (renamed from ui/ui.go)4
-rw-r--r--utils/format.go9
49 files changed, 72 insertions, 2721 deletions
diff --git a/Makefile b/Makefile
index 06cf507..15a0574 100644
--- a/Makefile
+++ b/Makefile
@@ -30,7 +30,7 @@ test-coverage: build
./.scripts/test-coverage.sh
validate:
- grep -R 'const allowTestDataCapture = false' ui/
+ grep -R 'const allowTestDataCapture = false' runtime/ui/
go vet ./...
@! gofmt -s -l . 2>&1 | grep -vE '^\.git/' | grep -vE '^\.cache/'
golangci-lint run
diff --git a/cmd/root.go b/cmd/root.go
index 2ed2534..9dadad2 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -2,6 +2,7 @@ package cmd
import (
"fmt"
+ "github.com/wagoodman/dive/dive/filetree"
"io/ioutil"
"os"
"path"
@@ -11,7 +12,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
- "github.com/wagoodman/dive/filetree"
"github.com/wagoodman/dive/utils"
)
diff --git a/filetree/cache.go b/filetree/cache.go
deleted file mode 100644
index 82c1795..0000000
--- a/filetree/cache.go
+++ /dev/null
@@ -1,79 +0,0 @@
-package filetree
-
-import (
- "github.com/sirupsen/logrus"
-)
-
-type TreeCacheKey struct {
- bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int
-}
-
-type TreeCache struct {
- refTrees []*FileTree
- cache map[TreeCacheKey]*FileTree
-}
-
-func (cache *TreeCache) Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) *FileTree {
- key := TreeCacheKey{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}
- if value, exists := cache.cache[key]; exists {
- return value
- }
-
- value := cache.buildTree(key)
- cache.cache[key] = value
- return value
-}
-
-func (cache *TreeCache) buildTree(key TreeCacheKey) *FileTree {
- newTree := StackTreeRange(cache.refTrees, key.bottomTreeStart, key.bottomTreeStop)
- for idx := key.topTreeStart; idx <= key.topTreeStop; idx++ {
- err := newTree.CompareAndMark(cache.refTrees[idx])
- if err != nil {
- logrus.Errorf("unable to build tree: %+v", err)
- }
- }
- return newTree
-}
-
-func (cache *TreeCache) Build() {
- var bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int
-
- // case 1: layer compare (top tree SIZE is fixed (BUT floats forward), Bottom tree SIZE changes)
- for selectIdx := 0; selectIdx < len(cache.refTrees); selectIdx++ {
- bottomTreeStart = 0
- topTreeStop = selectIdx
-
- if selectIdx == 0 {
- bottomTreeStop = selectIdx
- topTreeStart = selectIdx
- } else {
- bottomTreeStop = selectIdx - 1
- topTreeStart = selectIdx
- }
-
- cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
- }
-
- // case 2: aggregated compare (bottom tree is ENTIRELY fixed, top tree SIZE changes)
- for selectIdx := 0; selectIdx < len(cache.refTrees); selectIdx++ {
- bottomTreeStart = 0
- topTreeStop = selectIdx
- if selectIdx == 0 {
- bottomTreeStop = selectIdx
- topTreeStart = selectIdx
- } else {
- bottomTreeStop = 0
- topTreeStart = 1
- }
-
- cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
- }
-}
-
-func NewFileTreeCache(refTrees []*FileTree) TreeCache {
-
- return TreeCache{
- refTrees: refTrees,
- cache: make(map[TreeCacheKey]*FileTree),
- }
-}
diff --git a/filetree/data.go b/filetree/data.go
deleted file mode 100644
index 743ecb0..0000000
--- a/filetree/data.go
+++ /dev/null
@@ -1,161 +0,0 @@
-package filetree
-
-import (
- "archive/tar"
- "fmt"
- "io"
-
- "github.com/cespare/xxhash"
- "github.com/sirupsen/logrus"
-)
-
-const (
- Unmodified DiffType = iota
- Modified
- Added
- Removed
-)
-
-var GlobalFileTreeCollapse bool
-
-// NewNodeData creates an empty NodeData struct for a FileNode
-func NewNodeData() *NodeData {
- return &NodeData{
- ViewInfo: *NewViewInfo(),
- FileInfo: FileInfo{},
- DiffType: Unmodified,
- }
-}
-
-// Copy duplicates a NodeData
-func (data *NodeData) Copy() *NodeData {
- return &NodeData{
- ViewInfo: *data.ViewInfo.Copy(),
- FileInfo: *data.FileInfo.Copy(),
- DiffType: data.DiffType,
- }
-}
-
-// NewViewInfo creates a default ViewInfo
-func NewViewInfo() (view *ViewInfo) {
- return &ViewInfo{
- Collapsed: GlobalFileTreeCollapse,
- Hidden: false,
- }
-}
-
-// Copy duplicates a ViewInfo
-func (view *ViewInfo) Copy() (newView *ViewInfo) {
- newView = NewViewInfo()
- *newView = *view
- return newView
-}
-
-func getHashFromReader(reader io.Reader) uint64 {
- h := xxhash.New()
-
- buf := make([]byte, 1024)
- for {
- n, err := reader.Read(buf)
- if err != nil && err != io.EOF {
- logrus.Panic(err)
- }
- if n == 0 {
- break
- }
-
- _, err = h.Write(buf[:n])
- if err != nil {
- logrus.Panic(err)
- }
- }
-
- return h.Sum64()
-}
-
-// NewFileInfo extracts the metadata from a tar header and file contents and generates a new FileInfo object.
-func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo {
- if header.Typeflag == tar.TypeDir {
- return FileInfo{
- Path: path,
- TypeFlag: header.Typeflag,
- Linkname: header.Linkname,
- hash: 0,
- Size: header.FileInfo().Size(),
- Mode: header.FileInfo().Mode(),
- Uid: header.Uid,
- Gid: header.Gid,
- IsDir: header.FileInfo().IsDir(),
- }
- }
-
- hash := getHashFromReader(reader)
-
- return FileInfo{
- Path: path,
- TypeFlag: header.Typeflag,
- Linkname: header.Linkname,
- hash: hash,
- Size: header.FileInfo().Size(),
- Mode: header.FileInfo().Mode(),
- Uid: header.Uid,
- Gid: header.Gid,
- IsDir: header.FileInfo().IsDir(),
- }
-}
-
-// Copy duplicates a FileInfo
-func (data *FileInfo) Copy() *FileInfo {
- if data == nil {
- return nil
- }
- return &FileInfo{
- Path: data.Path,
- TypeFlag: data.TypeFlag,
- Linkname: data.Linkname,
- hash: data.hash,
- Size: data.Size,
- Mode: data.Mode,
- Uid: data.Uid,
- Gid: data.Gid,
- IsDir: data.IsDir,
- }
-}
-
-// Compare determines the DiffType between two FileInfos based on the type and contents of each given FileInfo
-func (data *FileInfo) Compare(other FileInfo) DiffType {
- if data.TypeFlag == other.TypeFlag {
- if data.hash == other.hash &&
- data.Mode == other.Mode &&
- data.Uid == other.Uid &&
- data.Gid == other.Gid {
- return Unmodified
- }
- }
- return Modified
-}
-
-// String of a DiffType
-func (diff DiffType) String() string {
- switch diff {
- case Unmodified:
- return "Unmodified"
- case Modified:
- return "Modified"
- case Added:
- return "Added"
- case Removed:
- return "Removed"
- default:
- return fmt.Sprintf("%d", int(diff))
- }
-}
-
-// merge two DiffTypes into a single result. Essentially, return the given value unless they two values differ,
-// in which case we can only determine that there is "a change".
-func (diff DiffType) merge(other DiffType) DiffType {
- if diff == other {
- return diff
- }
- return Modified
-}
diff --git a/filetree/data_test.go b/filetree/data_test.go
deleted file mode 100644
index 351d6df..0000000
--- a/filetree/data_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package filetree
-
-import (
- "testing"
-)
-
-func TestAssignDiffType(t *testing.T) {
- tree := NewFileTree()
- node, _, err := tree.AddPath("/usr", *BlankFileChangeInfo("/usr"))
- if err != nil {
- t.Errorf("Expected no error from fetching path. got: %v", err)
- }
- node.Data.DiffType = Modified
- if tree.Root.Children["usr"].Data.DiffType != Modified {
- t.Fail()
- }
-}
-
-func TestMergeDiffTypes(t *testing.T) {
- a := Unmodified
- b := Unmodified
- merged := a.merge(b)
- if merged != Unmodified {
- t.Errorf("Expected Unchaged (0) but got %v", merged)
- }
- a = Modified
- b = Unmodified
- merged = a.merge(b)
- if merged != Modified {
- t.Errorf("Expected Unchaged (0) but got %v", merged)
- }
-}
-
-func BlankFileChangeInfo(path string) (f *FileInfo) {
- result := FileInfo{
- Path: path,
- TypeFlag: 1,
- hash: 123,
- }
- return &result
-}
diff --git a/filetree/efficiency.go b/filetree/efficiency.go
deleted file mode 100644
index e0b21cf..0000000
--- a/filetree/efficiency.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package filetree
-
-import (
- "fmt"
- "sort"
-
- "github.com/sirupsen/logrus"
-)
-
-// Len is required for sorting.
-func (efs EfficiencySlice) Len() int {
- return len(efs)
-}
-
-// Swap operation is required for sorting.
-func (efs EfficiencySlice) Swap(i, j int) {
- efs[i], efs[j] = efs[j], efs[i]
-}
-
-// Less comparison is required for sorting.
-func (efs EfficiencySlice) Less(i, j int) bool {
- return efs[i].CumulativeSize < efs[j].CumulativeSize
-}
-
-// Efficiency returns the score and file set of the given set of FileTrees (layers). This is loosely based on:
-// 1. Files that are duplicated across layers discounts your score, weighted by file size
-// 2. Files that are removed discounts your score, weighted by the original file size
-func Efficiency(trees []*FileTree) (float64, EfficiencySlice) {
- efficiencyMap := make(map[string]*EfficiencyData)
- inefficientMatches := make(EfficiencySlice, 0)
- currentTree := 0
-
- visitor := func(node *FileNode) error {
- path := node.Path()
- if _, ok := efficiencyMap[path]; !ok {
- efficiencyMap[path] = &EfficiencyData{
- Path: path,
- Nodes: make([]*FileNode, 0),
- minDiscoveredSize: -1,
- }
- }
- data := efficiencyMap[path]
-
- // this node may have had children that were deleted, however, we won't explicitly list out every child, only
- // the top-most parent with the cumulative size. These operations will need to be done on the full (stacked)
- // tree.
- // Note: whiteout files may also represent directories, so we need to find out if this was previously a file or dir.
- var sizeBytes int64
-
- if node.IsWhiteout() {
- sizer := func(curNode *FileNode) error {
- sizeBytes += curNode.Data.FileInfo.Size
- return nil
- }
- stackedTree := StackTreeRange(trees, 0, currentTree-1)
- previousTreeNode, err := stackedTree.GetNode(node.Path())
- if err != nil {
- logrus.Debug(fmt.Sprintf("CurrentTree: %d : %s", currentTree, err))
- } else if previousTreeNode.Data.FileInfo.IsDir {
- err = previousTreeNode.VisitDepthChildFirst(sizer, nil)
- if err != nil {
- logrus.Errorf("unable to propagate whiteout dir: %+v", err)
- }
- }
-
- } else {
- sizeBytes = node.Data.FileInfo.Size
- }
-
- data.CumulativeSize += sizeBytes
- if data.minDiscoveredSize < 0 || sizeBytes < data.minDiscoveredSize {
- data.minDiscoveredSize = sizeBytes
- }
- data.Nodes = append(data.Nodes, node)
-
- if len(data.Nodes) == 2 {
- inefficientMatches = append(inefficientMatches, data)
- }
-
- return nil
- }
- visitEvaluator := func(node *FileNode) bool {
- return node.IsLeaf()
- }
- for idx, tree := range trees {
- currentTree = idx
- err := tree.VisitDepthChildFirst(visitor, visitEvaluator)
- if err != nil {
- logrus.Errorf("unable to propagate ref tree: %+v", err)
- }
- }
-
- // calculate the score
- var minimumPathSizes int64
- var discoveredPathSizes int64
-
- for _, value := range efficiencyMap {
- minimumPathSizes += value.minDiscoveredSize
- discoveredPathSizes += value.CumulativeSize
- }
- var score float64
- if discoveredPathSizes == 0 {
- score = 1.0
- } else {
- score = float64(minimumPathSizes) / float64(discoveredPathSizes)
- }
-
- sort.Sort(inefficientMatches)
-
- return score, inefficientMatches
-}
diff --git a/filetree/efficiency_test.go b/filetree/efficiency_test.go
deleted file mode 100644
index 831ceb5..0000000
--- a/filetree/efficiency_test.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package filetree
-
-import (
- "testing"
-)
-
-func checkError(t *testing.T, err error, message string) {
- if err != nil {
- t.Errorf(message+": %+v", err)
- }
-}
-
-func TestEfficency(t *testing.T) {
- trees := make([]*FileTree, 3)
- for idx := range trees {
- trees[idx] = NewFileTree()
- }
-
- _, _, err := trees[0].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 2000})
- checkError(t, err, "could not setup test")
-
- _, _, err = trees[0].AddPath("/etc/nginx/public", FileInfo{Size: 3000})
- checkError(t, err, "could not setup test")
-
- _, _, err = trees[1].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 5000})
- checkError(t, err, "could not setup test")
- _, _, err = trees[1].AddPath("/etc/athing", FileInfo{Size: 10000})
- checkError(t, err, "could not setup test")
-
- _, _, err = trees[2].AddPath("/etc/.wh.nginx", *BlankFileChangeInfo("/etc/.wh.nginx"))
- checkError(t, err, "could not setup test")
-
- var expectedScore = 0.75
- var expectedMatches = EfficiencySlice{
- &EfficiencyData{Path: "/etc/nginx/nginx.conf", CumulativeSize: 7000},
- }
- actualScore, actualMatches := Efficiency(trees)
-
- if expectedScore != actualScore {
- t.Errorf("Expected score of %v but go %v", expectedScore, actualScore)
- }
-
- if len(actualMatches) != len(expectedMatches) {
- for _, match := range actualMatches {
- t.Logf(" match: %+v", match)
- }
- t.Fatalf("Expected to find %d inefficient paths, but found %d", len(expectedMatches), len(actualMatches))
- }
-
- if expectedMatches[0].Path != actualMatches[0].Path {
- t.Errorf("Expected path of %s but go %s", expectedMatches[0].Path, actualMatches[0].Path)
- }
-
- if expectedMatches[0].CumulativeSize != actualMatches[0].CumulativeSize {
- t.Errorf("Expected cumulative size of %v but go %v", expectedMatches[0].CumulativeSize, actualMatches[0].CumulativeSize)
- }
-}
-
-func TestEfficency_ScratchImage(t *testing.T) {
- trees := make([]*FileTree, 3)
- for idx := range trees {
- trees[idx] = NewFileTree()
- }
-
- _, _, err := trees[0].AddPath("/nothing", FileInfo{Size: 0})
- checkError(t, err, "could not setup test")
-
- var expectedScore = 1.0
- var expectedMatches = EfficiencySlice{}
- actualScore, actualMatches := Efficiency(trees)
-
- if expectedScore != actualScore {
- t.Errorf("Expected score of %v but go %v", expectedScore, actualScore)
- }
-
- if len(actualMatches) > 0 {
- t.Fatalf("Expected to find %d inefficient paths, but found %d", len(expectedMatches), len(actualMatches))
- }
-
-}
diff --git a/filetree/node.go b/filetree/node.go
deleted file mode 100644
index d136f0d..0000000
--- a/filetree/node.go
+++ /dev/null
@@ -1,315 +0,0 @@
-package filetree
-
-import (
- "archive/tar"
- "fmt"
- "sort"
- "strings"
-
- "github.com/sirupsen/logrus"
-
- "github.com/dustin/go-humanize"
- "github.com/fatih/color"
- "github.com/phayes/permbits"
-)
-
-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),
-}
-
-// 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) MetadataSt