Merge branch 'master' into parallel-tar-proc
This commit is contained in:
commit
54b20c096b
@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/wagoodman/dive/ui"
|
"github.com/wagoodman/dive/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// analyze takes a docker image tag, digest, or id and displayes the
|
||||||
|
// image analysis to the screen
|
||||||
func analyze(cmd *cobra.Command, args []string) {
|
func analyze(cmd *cobra.Command, args []string) {
|
||||||
userImage := args[0]
|
userImage := args[0]
|
||||||
if userImage == "" {
|
if userImage == "" {
|
||||||
|
72
cmd/build.go
72
cmd/build.go
@ -1,9 +1,14 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"os/exec"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"io/ioutil"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/wagoodman/dive/image"
|
||||||
|
"github.com/wagoodman/dive/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// buildCmd represents the build command
|
// buildCmd represents the build command
|
||||||
@ -11,21 +16,58 @@ var buildCmd = &cobra.Command{
|
|||||||
Use: "build",
|
Use: "build",
|
||||||
Short: "Build and analyze a docker image",
|
Short: "Build and analyze a docker image",
|
||||||
Long: `Build and analyze a docker image`,
|
Long: `Build and analyze a docker image`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
DisableFlagParsing: true,
|
||||||
fmt.Println("build called")
|
Run: doBuild,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(buildCmd)
|
rootCmd.AddCommand(buildCmd)
|
||||||
|
}
|
||||||
// Here you will define your flags and configuration settings.
|
|
||||||
|
// doBuild implements the steps taken for the build command
|
||||||
// Cobra supports Persistent Flags which will work for this command
|
func doBuild(cmd *cobra.Command, args []string) {
|
||||||
// and all subcommands, e.g.:
|
iidfile, err := ioutil.TempFile("/tmp", "dive.*.iid")
|
||||||
// buildCmd.PersistentFlags().String("foo", "", "A help for foo")
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
// Cobra supports local flags which will only run when this command
|
}
|
||||||
// is called directly, e.g.:
|
defer os.Remove(iidfile.Name())
|
||||||
// buildCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
|
||||||
|
allArgs := append([]string{"--iidfile", iidfile.Name()}, args...)
|
||||||
|
err = runDockerCmd("build", allArgs...)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageId, err := ioutil.ReadFile(iidfile.Name())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, refTrees := image.InitializeData(string(imageId))
|
||||||
|
ui.Run(manifest, refTrees)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runDockerCmd runs a given Docker command in the current tty
|
||||||
|
func runDockerCmd(cmdStr string, args... string) error {
|
||||||
|
|
||||||
|
allArgs := cleanArgs(append([]string{cmdStr}, args...))
|
||||||
|
|
||||||
|
cmd := exec.Command("docker", allArgs...)
|
||||||
|
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanArgs trims the whitespace from the given set of strings.
|
||||||
|
func cleanArgs(s []string) []string {
|
||||||
|
var r []string
|
||||||
|
for _, str := range s {
|
||||||
|
if str != "" {
|
||||||
|
r = append(r, strings.Trim(str, " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
}
|
}
|
@ -21,7 +21,6 @@ var rootCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@ -69,6 +68,7 @@ func initConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initLogging sets up the loggin object with a formatter and location
|
||||||
func initLogging() {
|
func initLogging() {
|
||||||
// TODO: clean this up and make more configurable
|
// TODO: clean this up and make more configurable
|
||||||
var filename string = "dive.log"
|
var filename string = "dive.log"
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// enum to show whether a file has changed
|
|
||||||
const (
|
const (
|
||||||
Unchanged DiffType = iota
|
Unchanged DiffType = iota
|
||||||
Changed
|
Changed
|
||||||
@ -16,26 +15,31 @@ const (
|
|||||||
Removed
|
Removed
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NodeData is the payload for a FileNode
|
||||||
type NodeData struct {
|
type NodeData struct {
|
||||||
ViewInfo ViewInfo
|
ViewInfo ViewInfo
|
||||||
FileInfo FileInfo
|
FileInfo FileInfo
|
||||||
DiffType DiffType
|
DiffType DiffType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ViewInfo contains UI specific detail for a specific FileNode
|
||||||
type ViewInfo struct {
|
type ViewInfo struct {
|
||||||
Collapsed bool
|
Collapsed bool
|
||||||
Hidden bool
|
Hidden bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileInfo contains tar metadata for a specific FileNode
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Path string
|
Path string
|
||||||
Typeflag byte
|
TypeFlag byte
|
||||||
MD5sum [16]byte
|
MD5sum [16]byte
|
||||||
TarHeader tar.Header
|
TarHeader tar.Header
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DiffType defines the comparison result between two FileNodes
|
||||||
type DiffType int
|
type DiffType int
|
||||||
|
|
||||||
|
// NewNodeData creates an empty NodeData struct for a FileNode
|
||||||
func NewNodeData() (*NodeData) {
|
func NewNodeData() (*NodeData) {
|
||||||
return &NodeData{
|
return &NodeData{
|
||||||
ViewInfo: *NewViewInfo(),
|
ViewInfo: *NewViewInfo(),
|
||||||
@ -44,6 +48,7 @@ func NewNodeData() (*NodeData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy duplicates a NodeData
|
||||||
func (data *NodeData) Copy() (*NodeData) {
|
func (data *NodeData) Copy() (*NodeData) {
|
||||||
return &NodeData{
|
return &NodeData{
|
||||||
ViewInfo: *data.ViewInfo.Copy(),
|
ViewInfo: *data.ViewInfo.Copy(),
|
||||||
@ -53,6 +58,7 @@ func (data *NodeData) Copy() (*NodeData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// NewViewInfo creates a default ViewInfo
|
||||||
func NewViewInfo() (view *ViewInfo) {
|
func NewViewInfo() (view *ViewInfo) {
|
||||||
return &ViewInfo{
|
return &ViewInfo{
|
||||||
Collapsed: false,
|
Collapsed: false,
|
||||||
@ -60,17 +66,19 @@ func NewViewInfo() (view *ViewInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy duplicates a ViewInfo
|
||||||
func (view *ViewInfo) Copy() (newView *ViewInfo) {
|
func (view *ViewInfo) Copy() (newView *ViewInfo) {
|
||||||
newView = NewViewInfo()
|
newView = NewViewInfo()
|
||||||
*newView = *view
|
*newView = *view
|
||||||
return newView
|
return newView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo {
|
||||||
if header.Typeflag == tar.TypeDir {
|
if header.Typeflag == tar.TypeDir {
|
||||||
return FileInfo{
|
return FileInfo{
|
||||||
Path: path,
|
Path: path,
|
||||||
Typeflag: header.Typeflag,
|
TypeFlag: header.Typeflag,
|
||||||
MD5sum: [16]byte{},
|
MD5sum: [16]byte{},
|
||||||
TarHeader: *header,
|
TarHeader: *header,
|
||||||
}
|
}
|
||||||
@ -83,14 +91,39 @@ func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo {
|
|||||||
|
|
||||||
return FileInfo{
|
return FileInfo{
|
||||||
Path: path,
|
Path: path,
|
||||||
Typeflag: header.Typeflag,
|
TypeFlag: header.Typeflag,
|
||||||
MD5sum: md5.Sum(fileBytes),
|
MD5sum: md5.Sum(fileBytes),
|
||||||
TarHeader: *header,
|
TarHeader: *header,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DiffType) String() string {
|
// Copy duplicates a FileInfo
|
||||||
switch d {
|
func (data *FileInfo) Copy() *FileInfo {
|
||||||
|
if data == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &FileInfo{
|
||||||
|
Path: data.Path,
|
||||||
|
TypeFlag: data.TypeFlag,
|
||||||
|
MD5sum: data.MD5sum,
|
||||||
|
TarHeader: data.TarHeader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 bytes.Compare(data.MD5sum[:], other.MD5sum[:]) == 0 {
|
||||||
|
return Unchanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Changed
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// String of a DiffType
|
||||||
|
func (diff DiffType) String() string {
|
||||||
|
switch diff {
|
||||||
case Unchanged:
|
case Unchanged:
|
||||||
return "Unchanged"
|
return "Unchanged"
|
||||||
case Changed:
|
case Changed:
|
||||||
@ -100,34 +133,17 @@ func (d DiffType) String() string {
|
|||||||
case Removed:
|
case Removed:
|
||||||
return "Removed"
|
return "Removed"
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("%d", int(d))
|
return fmt.Sprintf("%d", int(diff))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a DiffType) merge(b DiffType) DiffType {
|
// merge two DiffTypes into a single result. Essentially, return the given value unless they two values differ,
|
||||||
if a == b {
|
// in which case we can only determine that there is "a change".
|
||||||
return a
|
func (diff DiffType) merge(other DiffType) DiffType {
|
||||||
|
if diff == other {
|
||||||
|
return diff
|
||||||
}
|
}
|
||||||
return Changed
|
return Changed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *FileInfo) Copy() *FileInfo {
|
|
||||||
if data == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &FileInfo{
|
|
||||||
Path: data.Path,
|
|
||||||
Typeflag: data.Typeflag,
|
|
||||||
MD5sum: data.MD5sum,
|
|
||||||
TarHeader: data.TarHeader,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (data *FileInfo) getDiffType(other FileInfo) DiffType {
|
|
||||||
if data.Typeflag == other.Typeflag {
|
|
||||||
if bytes.Compare(data.MD5sum[:], other.MD5sum[:]) == 0 {
|
|
||||||
return Unchanged
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Changed
|
|
||||||
}
|
|
||||||
|
@ -34,7 +34,7 @@ func TestMergeDiffTypes(t *testing.T) {
|
|||||||
func BlankFileChangeInfo(path string) (f *FileInfo) {
|
func BlankFileChangeInfo(path string) (f *FileInfo) {
|
||||||
result := FileInfo{
|
result := FileInfo{
|
||||||
Path: path,
|
Path: path,
|
||||||
Typeflag: 1,
|
TypeFlag: 1,
|
||||||
MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0},
|
MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0},
|
||||||
}
|
}
|
||||||
return &result
|
return &result
|
||||||
|
85
filetree/efficiency.go
Normal file
85
filetree/efficiency.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package filetree
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EfficiencyData represents the storage and reference statistics for a given file tree path.
|
||||||
|
type EfficiencyData struct {
|
||||||
|
Path string
|
||||||
|
Nodes []*FileNode
|
||||||
|
CumulativeSize int64
|
||||||
|
minDiscoveredSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// EfficiencySlice represents an ordered set of EfficiencyData data structures.
|
||||||
|
type EfficiencySlice []*EfficiencyData
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
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]
|
||||||
|
data.CumulativeSize += node.Data.FileInfo.TarHeader.Size
|
||||||
|
if data.minDiscoveredSize < 0 || node.Data.FileInfo.TarHeader.Size < data.minDiscoveredSize {
|
||||||
|
data.minDiscoveredSize = node.Data.FileInfo.TarHeader.Size
|
||||||
|
}
|
||||||
|
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 _, tree := range trees {
|
||||||
|
tree.VisitDepthChildFirst(visitor, visitEvaluator)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// calculate the score
|
||||||
|
var minimumPathSizes int64
|
||||||
|
var discoveredPathSizes int64
|
||||||
|
|
||||||
|
for _, value := range efficiencyMap {
|
||||||
|
minimumPathSizes += value.minDiscoveredSize
|
||||||
|
discoveredPathSizes += value.CumulativeSize
|
||||||
|
}
|
||||||
|
score := float64(minimumPathSizes) / float64(discoveredPathSizes)
|
||||||
|
|
||||||
|
sort.Sort(inefficientMatches)
|
||||||
|
|
||||||
|
return score, inefficientMatches
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -22,6 +22,7 @@ var diffTypeColor = map[DiffType]*color.Color {
|
|||||||
Unchanged: color.New(color.Reset),
|
Unchanged: 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 {
|
type FileNode struct {
|
||||||
Tree *FileTree
|
Tree *FileTree
|
||||||
Parent *FileNode
|
Parent *FileNode
|
||||||
@ -31,6 +32,7 @@ type FileNode struct {
|
|||||||
path string
|
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) {
|
func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) {
|
||||||
node = new(FileNode)
|
node = new(FileNode)
|
||||||
node.Name = name
|
node.Name = name
|
||||||
@ -45,6 +47,7 @@ func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) {
|
|||||||
return node
|
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 {
|
func (node *FileNode) renderTreeLine(spaces []bool, last bool, collapsed bool) string {
|
||||||
var otherBranches string
|
var otherBranches string
|
||||||
for _, space := range spaces {
|
for _, space := range spaces {
|
||||||
@ -68,6 +71,7 @@ func (node *FileNode) renderTreeLine(spaces []bool, last bool, collapsed bool) s
|
|||||||
return otherBranches + thisBranch + collapsedIndicator + node.String() + newLine
|
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 {
|
func (node *FileNode) Copy(parent *FileNode) *FileNode {
|
||||||
newNode := NewNode(parent, node.Name, node.Data.FileInfo)
|
newNode := NewNode(parent, node.Name, node.Data.FileInfo)
|
||||||
newNode.Data.ViewInfo = node.Data.ViewInfo
|
newNode.Data.ViewInfo = node.Data.ViewInfo
|
||||||
@ -79,6 +83,7 @@ func (node *FileNode) Copy(parent *FileNode) *FileNode {
|
|||||||
return newNode
|
return newNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddChild creates a new node relative to the current FileNode.
|
||||||
func (node *FileNode) AddChild(name string, data FileInfo) (child *FileNode) {
|
func (node *FileNode) AddChild(name string, data FileInfo) (child *FileNode) {
|
||||||
child = NewNode(node, name, data)
|
child = NewNode(node, name, data)
|
||||||
if node.Children[name] != nil {
|
if node.Children[name] != nil {
|
||||||
@ -91,6 +96,7 @@ func (node *FileNode) AddChild(name string, data FileInfo) (child *FileNode) {
|
|||||||
return child
|
return child
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove deletes the current FileNode from it's parent FileNode's relations.
|
||||||
func (node *FileNode) Remove() error {
|
func (node *FileNode) Remove() error {
|
||||||
if node == node.Tree.Root {
|
if node == node.Tree.Root {
|
||||||
return fmt.Errorf("cannot remove the tree root")
|
return fmt.Errorf("cannot remove the tree root")
|
||||||
@ -103,6 +109,7 @@ func (node *FileNode) Remove() error {
|
|||||||
return nil
|
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 {
|
func (node *FileNode) String() string {
|
||||||
var display string
|
var display string
|
||||||
if node == nil {
|
if node == nil {
|
||||||
@ -116,6 +123,7 @@ func (node *FileNode) String() string {
|
|||||||
return diffTypeColor[node.Data.DiffType].Sprint(display)
|
return diffTypeColor[node.Data.DiffType].Sprint(display)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MetadatString returns the FileNode metadata in a columnar string.
|
||||||
func (node *FileNode) MetadataString() string {
|
func (node *FileNode) MetadataString() string {
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return ""
|
return ""
|
||||||
@ -130,7 +138,6 @@ func (node *FileNode) MetadataString() string {
|
|||||||
group := node.Data.FileInfo.TarHeader.Gid
|
group := node.Data.FileInfo.TarHeader.Gid
|
||||||
userGroup := fmt.Sprintf("%d:%d", user, group)
|
userGroup := fmt.Sprintf("%d:%d", user, group)
|
||||||
|
|
||||||
//size := humanize.Bytes(uint64(node.Data.FileInfo.TarHeader.FileInfo().Size()))
|
|
||||||
var sizeBytes int64
|
var sizeBytes int64
|
||||||
|
|
||||||
if node.Data.FileInfo.TarHeader.FileInfo().IsDir() {
|
if node.Data.FileInfo.TarHeader.FileInfo().IsDir() {
|
||||||
@ -152,7 +159,8 @@ func (node *FileNode) MetadataString() string {
|
|||||||
return diffTypeColor[node.Data.DiffType].Sprint(fmt.Sprintf(AttributeFormat, dir, fileMode, userGroup, size))
|
return diffTypeColor[node.Data.DiffType].Sprint(fmt.Sprintf(AttributeFormat, dir, fileMode, userGroup, size))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *FileNode) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvaluator) error {
|
// 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
|
var keys []string
|
||||||
for key := range node.Children {
|
for key := range node.Children {
|
||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
@ -160,7 +168,7 @@ func (node *FileNode) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvalu
|
|||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
for _, name := range keys {
|
for _, name := range keys {
|
||||||
child := node.Children[name]
|
child := node.Children[name]
|
||||||
err := child.VisitDepthChildFirst(visiter, evaluator)
|
err := child.VisitDepthChildFirst(visitor, evaluator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -169,13 +177,14 @@ func (node *FileNode) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvalu
|
|||||||
if node == node.Tree.Root {
|
if node == node.Tree.Root {
|
||||||
return nil
|
return nil
|
||||||
} else if evaluator != nil && evaluator(node) || evaluator == nil {
|
} else if evaluator != nil && evaluator(node) || evaluator == nil {
|
||||||
return visiter(node)
|
return visitor(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *FileNode) VisitDepthParentFirst(visiter Visiter, evaluator VisitEvaluator) error {
|
// 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
|
var err error
|
||||||
|
|
||||||
doVisit := evaluator != nil && evaluator(node) || evaluator == nil
|
doVisit := evaluator != nil && evaluator(node) || evaluator == nil
|
||||||
@ -186,7 +195,7 @@ func (node *FileNode) VisitDepthParentFirst(visiter Visiter, evaluator VisitEval
|
|||||||
|
|
||||||
// never visit the root node
|
// never visit the root node
|
||||||
if node != node.Tree.Root {
|
if node != node.Tree.Root {
|
||||||
err = visiter(node)
|
err = visitor(node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -199,7 +208,7 @@ func (node *FileNode) VisitDepthParentFirst(visiter Visiter, evaluator VisitEval
|
|||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
for _, name := range keys {
|
for _, name := range keys {
|
||||||
child := node.Children[name]
|
child := node.Children[name]
|
||||||
err = child.VisitDepthParentFirst(visiter, evaluator)
|
err = child.VisitDepthParentFirst(visitor, evaluator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -207,10 +216,17 @@ func (node *FileNode) VisitDepthParentFirst(visiter Visiter, evaluator VisitEval
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsWhiteout returns an indication if this file may be a overlay-whiteout file.
|
||||||
func (node *FileNode) IsWhiteout() bool {
|
func (node *FileNode) IsWhiteout() bool {
|
||||||
return strings.HasPrefix(node.Name, whiteoutPrefix)
|
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 {
|
func (node *FileNode) Path() string {
|
||||||
if node.path == "" {
|
if node.path == "" {
|
||||||
path := []string{}
|
path := []string{}
|
||||||
@ -234,14 +250,9 @@ func (node *FileNode) Path() string {
|
|||||||
return node.path
|
return node.path
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *FileNode) IsLeaf() bool {
|
// deriveDiffType determines a DiffType to the current FileNode. Note: the DiffType of a node is always the DiffType of
|
||||||
return len(node.Children) == 0
|
// 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 {
|
func (node *FileNode) deriveDiffType(diffType DiffType) error {
|
||||||
// THE DIFF_TYPE OF A NODE IS ALWAYS THE DIFF_TYPE OF ITS ATTRIBUTES AND ITS CONTENTS
|
|
||||||
// THE CONTENTS ARE THE BYTES OF A FILE OR THE CHILDREN OF A DIRECTORY
|
|
||||||
|
|
||||||
if node.IsLeaf() {
|
if node.IsLeaf() {
|
||||||
return node.AssignDiffType(diffType)
|
return node.AssignDiffType(diffType)
|
||||||
}
|
}
|
||||||
@ -255,6 +266,7 @@ func (node *FileNode) deriveDiffType(diffType DiffType) error {
|
|||||||
return node.AssignDiffType(myDiffType)
|
return node.AssignDiffType(myDiffType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssignDiffType will assign the given DiffType to this node, possible affecting child nodes.
|
||||||
func (node *FileNode) AssignDiffType(diffType DiffType) error {
|
func (node *FileNode) AssignDiffType(diffType DiffType) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@ -277,27 +289,27 @@ func (node *FileNode) AssignDiffType(diffType DiffType) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *FileNode) compare(b *FileNode) DiffType {
|
// compare the current node against the given node, returning a definitive DiffType.
|
||||||
if a == nil && b == nil {
|
func (node *FileNode) compare(other *FileNode) DiffType {
|
||||||
|
if node == nil && other == nil {
|
||||||
return Unchanged
|
return Unchanged
|
||||||
}
|
}
|
||||||
// a is nil but not b
|
|
||||||
if a == nil && b != nil {
|
if node == nil && other != nil {
|
||||||
return Added
|
return Added
|
||||||
}
|
}
|
||||||
|
|
||||||
// b is nil but not a
|
if node != nil && other == nil {
|
||||||
if a != nil && b == nil {
|
|
||||||
return Removed
|
return Removed
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.IsWhiteout() {
|
if other.IsWhiteout() {
|
||||||
return Removed
|
return Removed
|
||||||
}
|
}
|
||||||
if a.Name != b.Name {
|
if node.Name != other.Name {
|
||||||
panic("comparing mismatched nodes")
|
panic("comparing mismatched nodes")
|
||||||
}
|
}
|
||||||
// TODO: fails on nil
|
// TODO: fails on nil
|
||||||
|
|
||||||
return a.Data.FileInfo.getDiffType(b.Data.FileInfo)
|
return node.Data.FileInfo.Compare(other.Data.FileInfo)
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ const (
|
|||||||
collapsedItem = "⊕ "
|
collapsedItem = "⊕ "
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FileTree represents a set of files, directories, and their relations.
|
||||||
type FileTree struct {
|
type FileTree struct {
|
||||||
Root *FileNode
|
Root *FileNode
|
||||||
Size int
|
Size int
|
||||||
@ -26,6 +27,7 @@ type FileTree struct {
|
|||||||
Id uuid.UUID
|
Id uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewFileTree creates an empty FileTree
|
||||||
func NewFileTree() (tree *FileTree) {
|
func NewFileTree() (tree *FileTree) {
|
||||||
tree = new(FileTree)
|
tree = new(FileTree)
|
||||||
tree.Size = 0
|
tree.Size = 0
|
||||||
@ -36,6 +38,8 @@ func NewFileTree() (tree *FileTree) {
|
|||||||
return tree
|
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{
|
type renderParams struct{
|
||||||
node *FileNode
|
node *FileNode
|
||||||
spaces []bool
|
spaces []bool
|
||||||
@ -44,13 +48,15 @@ type renderParams struct{
|
|||||||
isLast 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 {
|
func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttributes bool) string {
|
||||||
// generate a list of nodes to render
|
// generate a list of nodes to render
|
||||||
var params []renderParams = make([]renderParams,0)
|
var params = make([]renderParams,0)
|
||||||
var result string
|
var result string
|
||||||
|
|
||||||
// visit from the front of the list
|
// visit from the front of the list
|
||||||
var paramsToVisit = []renderParams{ renderParams{node: tree.Root, spaces: []bool{}, showCollapsed: false, isLast: false} }
|
var paramsToVisit = []renderParams{ {node: tree.Root, spaces: []bool{}, showCollapsed: false, isLast: false} }
|
||||||
for currentRow := 0; len(paramsToVisit) > 0 && currentRow <= stopRow; currentRow++ {
|
for currentRow := 0; len(paramsToVisit) > 0 && currentRow <= stopRow; currentRow++ {
|
||||||
// pop the first node
|
// pop the first node
|
||||||
var currentParams renderParams
|
var currentParams renderParams
|
||||||
@ -61,6 +67,7 @@ func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttribu
|
|||||||
for key := range currentParams.node.Children {
|
for key := range currentParams.node.Children {
|
||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
|
// we should always visit nodes in order
|
||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
|
|
||||||
var childParams = make([]renderParams,0)
|
var childParams = make([]renderParams,0)
|
||||||
@ -119,14 +126,17 @@ func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttribu
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns the entire tree in an ASCII representation.
|
||||||
func (tree *FileTree) String(showAttributes bool) string {
|
func (tree *FileTree) String(showAttributes bool) string {
|
||||||
return tree.renderStringTreeBetween(0, tree.Size, showAttributes)
|
return tree.renderStringTreeBetween(0, tree.Size, showAttributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StringBetween returns a partial tree in an ASCII representation.
|
||||||
func (tree *FileTree) StringBetween(start, stop uint, showAttributes bool) string {
|
func (tree *FileTree) StringBetween(start, stop uint, showAttributes bool) string {
|
||||||
return tree.renderStringTreeBetween(int(start), int(stop), showAttributes)
|
return tree.renderStringTreeBetween(int(start), int(stop), showAttributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy returns a copy of the given FileTree
|
||||||
func (tree *FileTree) Copy() *FileTree {
|
func (tree *FileTree) Copy() *FileTree {
|
||||||
newTree := NewFileTree()
|
newTree := NewFileTree()
|
||||||
newTree.Size = tree.Size
|
newTree.Size = tree.Size
|
||||||
@ -142,19 +152,23 @@ func (tree *FileTree) Copy() *FileTree {
|
|||||||
return newTree
|
return newTree
|
||||||
}
|
}
|
||||||
|
|
||||||
type Visiter func(*FileNode) error
|
// 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
|
type VisitEvaluator func(*FileNode) bool
|
||||||
|
|
||||||
// DFS bubble up
|
// VisitDepthChildFirst iterates the given tree depth-first, evaluating the deepest depths first (visit on bubble up)
|
||||||
func (tree *FileTree) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvaluator) error {
|
func (tree *FileTree) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator) error {
|
||||||
return tree.Root.VisitDepthChildFirst(visiter, evaluator)
|
return tree.Root.VisitDepthChildFirst(visitor, evaluator)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DFS sink down
|
// VisitDepthParentFirst iterates the given tree depth-first, evaluating the shallowest depths first (visit while sinking down)
|
||||||
func (tree *FileTree) VisitDepthParentFirst(visiter Visiter, evaluator VisitEvaluator) error {
|
func (tree *FileTree) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator) error {
|
||||||
return tree.Root.VisitDepthParentFirst(visiter, evaluator)
|
return tree.Root.VisitDepthParentFirst(visitor, evaluator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) error {
|
func (tree *FileTree) Stack(upper *FileTree) error {
|
||||||
graft := func(node *FileNode) error {
|
graft := func(node *FileNode) error {
|
||||||
if node.IsWhiteout() {
|
if node.IsWhiteout() {
|
||||||
@ -173,6 +187,7 @@ func (tree *FileTree) Stack(upper *FileTree) error {
|
|||||||
return upper.VisitDepthChildFirst(graft, nil)
|
return upper.VisitDepthChildFirst(graft, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func (tree *FileTree) GetNode(path string) (*FileNode, error) {
|
||||||
nodeNames := strings.Split(strings.Trim(path, "/"), "/")
|
nodeNames := strings.Split(strings.Trim(path, "/"), "/")
|
||||||
node := tree.Root
|
node := tree.Root
|
||||||
@ -188,6 +203,7 @@ func (tree *FileTree) GetNode(path string) (*FileNode, error) {
|
|||||||
return node, nil
|
return node, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddPath adds a new node to the tree with the given payload
|
||||||
func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, error) {
|
func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, error) {
|
||||||
nodeNames := strings.Split(strings.Trim(path, "/"), "/")
|
nodeNames := strings.Split(strings.Trim(path, "/"), "/")
|
||||||
node := tree.Root
|
node := tree.Root
|
||||||
@ -213,6 +229,7 @@ func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, error) {
|
|||||||
return node, nil
|
return node, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemovePath removes a node from the tree given its path.
|
||||||
func (tree *FileTree) RemovePath(path string) error {
|
func (tree *FileTree) RemovePath(path string) error {
|
||||||
node, err := tree.GetNode(path)
|
node, err := tree.GetNode(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -221,10 +238,11 @@ func (tree *FileTree) RemovePath(path string) error {
|
|||||||
return node.Remove()
|
return node.Remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare marks the FileNodes in the owning tree with DiffType annotations when compared to the given tree.
|
||||||
func (tree *FileTree) Compare(upper *FileTree) error {
|
func (tree *FileTree) Compare(upper *FileTree) error {
|
||||||
graft := func(upperNode *FileNode) error {
|
graft := func(upperNode *FileNode) error {
|
||||||
if upperNode.IsWhiteout() {
|
if upperNode.IsWhiteout() {
|
||||||
err := tree.MarkRemoved(upperNode.Path())
|
err := tree.markRemoved(upperNode.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot remove upperNode %s: %v", upperNode.Path(), err.Error())
|
return fmt.Errorf("cannot remove upperNode %s: %v", upperNode.Path(), err.Error())
|
||||||
}
|
}
|
||||||
@ -246,7 +264,8 @@ func (tree *FileTree) Compare(upper *FileTree) error {
|
|||||||
return upper.VisitDepthChildFirst(graft, nil)
|
return upper.VisitDepthChildFirst(graft, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree *FileTree) MarkRemoved(path string) error {
|
// markRemoved annotates the FileNode at the given path as Removed.
|
||||||
|
func (tree *FileTree) markRemoved(path string) error {
|
||||||
node, err := tree.GetNode(path)
|
node, err := tree.GetNode(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -254,6 +273,7 @@ func (tree *FileTree) MarkRemoved(path string) error {
|
|||||||
return node.AssignDiffType(Removed)
|
return node.AssignDiffType(Removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StackRange combines an array of trees into a single tree
|
||||||
func StackRange(trees []*FileTree, start, stop int) *FileTree {
|
func StackRange(trees []*FileTree, start, stop int) *FileTree {
|
||||||
tree := trees[0].Copy()
|
tree := trees[0].Copy()
|
||||||
for idx := start; idx <= stop; idx++ {
|
for idx := start; idx <= stop; idx++ {
|
||||||
@ -262,30 +282,3 @@ func StackRange(trees []*FileTree, start, stop int) *FileTree {
|
|||||||
|
|
||||||
return 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)
|
|
||||||
}
|
|
||||||
|
@ -262,7 +262,7 @@ func TestCompareWithNoChanges(t *testing.T) {
|
|||||||
for _, value := range paths {
|
for _, value := range paths {
|
||||||
fakeData := FileInfo{
|
fakeData := FileInfo{
|
||||||
Path: value,
|
Path: value,
|
||||||
Typeflag: 1,
|
TypeFlag: 1,
|
||||||
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
}
|
}
|
||||||
lowerTree.AddPath(value, fakeData)
|
lowerTree.AddPath(value, fakeData)
|
||||||
@ -293,7 +293,7 @@ func TestCompareWithAdds(t *testing.T) {
|
|||||||
for _, value := range lowerPaths {
|
for _, value := range lowerPaths {
|
||||||
lowerTree.AddPath(value, FileInfo{
|
lowerTree.AddPath(value, FileInfo{
|
||||||
Path: value,
|
Path: value,
|
||||||
Typeflag: 1,
|
TypeFlag: 1,
|
||||||
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -301,7 +301,7 @@ func TestCompareWithAdds(t *testing.T) {
|
|||||||
for _, value := range upperPaths {
|
for _, value := range upperPaths {
|
||||||
upperTree.AddPath(value, FileInfo{
|
upperTree.AddPath(value, FileInfo{
|
||||||
Path: value,
|
Path: value,
|
||||||
Typeflag: 1,
|
TypeFlag: 1,
|
||||||
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -353,12 +353,12 @@ func TestCompareWithChanges(t *testing.T) {
|
|||||||
for _, value := range paths {
|
for _, value := range paths {
|
||||||
lowerTree.AddPath(value, FileInfo{
|
lowerTree.AddPath(value, FileInfo{
|
||||||
Path: value,
|
Path: value,
|
||||||
Typeflag: 1,
|
TypeFlag: 1,
|
||||||
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
})
|
})
|
||||||
upperTree.AddPath(value, FileInfo{
|
upperTree.AddPath(value, FileInfo{
|
||||||
Path: value,
|
Path: value,
|
||||||
Typeflag: 1,
|
TypeFlag: 1,
|
||||||
MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0},
|
MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -403,7 +403,7 @@ func TestCompareWithRemoves(t *testing.T) {
|
|||||||
for _, value := range lowerPaths {
|
for _, value := range lowerPaths {
|
||||||
fakeData := FileInfo{
|
fakeData := FileInfo{
|
||||||
Path: value,
|
Path: value,
|
||||||
Typeflag: 1,
|
TypeFlag: 1,
|
||||||
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
}
|
}
|
||||||
lowerTree.AddPath(value, fakeData)
|
lowerTree.AddPath(value, fakeData)
|
||||||
@ -412,7 +412,7 @@ func TestCompareWithRemoves(t *testing.T) {
|
|||||||
for _, value := range upperPaths {
|
for _, value := range upperPaths {
|
||||||
fakeData := FileInfo{
|
fakeData := FileInfo{
|
||||||
Path: value,
|
Path: value,
|
||||||
Typeflag: 1,
|
TypeFlag: 1,
|
||||||
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
}
|
}
|
||||||
upperTree.AddPath(value, fakeData)
|
upperTree.AddPath(value, fakeData)
|
||||||
@ -473,7 +473,7 @@ func TestStackRange(t *testing.T) {
|
|||||||
for _, value := range lowerPaths {
|
for _, value := range lowerPaths {
|
||||||
fakeData := FileInfo{
|
fakeData := FileInfo{
|
||||||
Path: value,
|
Path: value,
|
||||||
Typeflag: 1,
|
TypeFlag: 1,
|
||||||
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
}
|
}
|
||||||
lowerTree.AddPath(value, fakeData)
|
lowerTree.AddPath(value, fakeData)
|
||||||
@ -482,7 +482,7 @@ func TestStackRange(t *testing.T) {
|
|||||||
for _, value := range upperPaths {
|
for _, value := range upperPaths {
|
||||||
fakeData := FileInfo{
|
fakeData := FileInfo{
|
||||||
Path: value,
|
Path: value,
|
||||||
Typeflag: 1,
|
TypeFlag: 1,
|
||||||
MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0},
|
MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0},
|
||||||
}
|
}
|
||||||
upperTree.AddPath(value, fakeData)
|
upperTree.AddPath(value, fakeData)
|
||||||
@ -499,7 +499,7 @@ func TestRemoveOnIterate(t *testing.T) {
|
|||||||
for _, value := range paths {
|
for _, value := range paths {
|
||||||
fakeData := FileInfo{
|
fakeData := FileInfo{
|
||||||
Path: value,
|
Path: value,
|
||||||
Typeflag: 1,
|
TypeFlag: 1,
|
||||||
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
}
|
}
|
||||||
node, err := tree.AddPath(value, fakeData)
|
node, err := tree.AddPath(value, fakeData)
|
||||||
@ -554,7 +554,7 @@ func TestEfficiencyScore(t *testing.T) {
|
|||||||
trees[ix] = tree
|
trees[ix] = tree
|
||||||
}
|
}
|
||||||
expected := 2.0 / 6.0
|
expected := 2.0 / 6.0
|
||||||
actual := EfficiencyScore(trees)
|
actual := CalculateEfficiency(trees)
|
||||||
if math.Abs(expected-actual) > 0.0001 {
|
if math.Abs(expected-actual) > 0.0001 {
|
||||||
t.Fatalf("Expected %f but got %f", expected, actual)
|
t.Fatalf("Expected %f but got %f", expected, actual)
|
||||||
}
|
}
|
||||||
@ -567,7 +567,7 @@ func TestEfficiencyScore(t *testing.T) {
|
|||||||
trees[ix] = tree
|
trees[ix] = tree
|
||||||
}
|
}
|
||||||
expected = 1.0
|
expected = 1.0
|
||||||
actual = EfficiencyScore(trees)
|
actual = CalculateEfficiency(trees)
|
||||||
if math.Abs(expected-actual) > 0.0001 {
|
if math.Abs(expected-actual) > 0.0001 {
|
||||||
t.Fatalf("Expected %f but got %f", expected, actual)
|
t.Fatalf("Expected %f but got %f", expected, actual)
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,6 @@ import (
|
|||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
LayerFormat = "%-25s %5s %7s %s"
|
|
||||||
)
|
|
||||||
|
|
||||||
func check(e error) {
|
func check(e error) {
|
||||||
if e != nil {
|
if e != nil {
|
||||||
panic(e)
|
panic(e)
|
||||||
@ -147,13 +143,13 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
|
|||||||
// save this image to disk temporarily to get the content info
|
// save this image to disk temporarily to get the content info
|
||||||
fmt.Println("Fetching image...")
|
fmt.Println("Fetching image...")
|
||||||
imageTarPath, tmpDir := saveImage(imageID)
|
imageTarPath, tmpDir := saveImage(imageID)
|
||||||
// imageTarPath := "/tmp/dive932744808/image.tar"
|
// imageTarPath := "/tmp/dive229500681/image.tar"
|
||||||
// tmpDir := "/tmp/dive031537738"
|
// tmpDir := "/tmp/dive229500681"
|
||||||
// fmt.Println(tmpDir)
|
// fmt.Println(tmpDir)
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
// read through the image contents and build a tree
|
// read through the image contents and build a tree
|
||||||
fmt.Println("Reading image...")
|
fmt.Printf("Reading image '%s'...\n", imageID)
|
||||||
tarFile, err := os.Open(imageTarPath)
|
tarFile, err := os.Open(imageTarPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
@ -4,10 +4,14 @@ import (
|
|||||||
"github.com/wagoodman/dive/filetree"
|
"github.com/wagoodman/dive/filetree"
|
||||||
"strings"
|
"strings"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LayerFormat = "%-25s %7s %s"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Layer represents a Docker image layer and metadata
|
||||||
type Layer struct {
|
type Layer struct {
|
||||||
TarPath string
|
TarPath string
|
||||||
History ImageHistoryEntry
|
History ImageHistoryEntry
|
||||||
@ -16,6 +20,7 @@ type Layer struct {
|
|||||||
RefTrees []*filetree.FileTree
|
RefTrees []*filetree.FileTree
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Id returns the truncated id of the current layer.
|
||||||
func (layer *Layer) Id() string {
|
func (layer *Layer) Id() string {
|
||||||
rangeBound := 25
|
rangeBound := 25
|
||||||
if length := len(layer.History.ID); length < 25 {
|
if length := len(layer.History.ID); length < 25 {
|
||||||
@ -31,12 +36,11 @@ func (layer *Layer) Id() string {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String represents a layer in a columnar format.
|
||||||
func (layer *Layer) String() string {
|
func (layer *Layer) String() string {
|
||||||
|
|
||||||
return fmt.Sprintf(LayerFormat,
|
return fmt.Sprintf(LayerFormat,
|
||||||
layer.Id(),
|
layer.Id(),
|
||||||
strconv.Itoa(int(100.0*filetree.EfficiencyScore(layer.RefTrees[:layer.Index+1]))) + "%",
|
|
||||||
//"100%",
|
|
||||||
humanize.Bytes(uint64(layer.History.Size)),
|
humanize.Bytes(uint64(layer.History.Size)),
|
||||||
strings.TrimPrefix(layer.History.CreatedBy, "/bin/sh -c "))
|
strings.TrimPrefix(layer.History.CreatedBy, "/bin/sh -c "))
|
||||||
}
|
}
|
||||||
|
138
ui/detailsview.go
Normal file
138
ui/detailsview.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jroimartin/gocui"
|
||||||
|
"github.com/lunixbochs/vtclean"
|
||||||
|
"strings"
|
||||||
|
"github.com/wagoodman/dive/filetree"
|
||||||
|
"strconv"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DetailsView holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
|
||||||
|
// shows the layer details and image statistics.
|
||||||
|
type DetailsView struct {
|
||||||
|
Name string
|
||||||
|
gui *gocui.Gui
|
||||||
|
view *gocui.View
|
||||||
|
header *gocui.View
|
||||||
|
efficiency float64
|
||||||
|
inefficiencies filetree.EfficiencySlice
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDetailsView creates a new view object attached the the global [gocui] screen object.
|
||||||
|
func NewDetailsView(name string, gui *gocui.Gui) (detailsView *DetailsView) {
|
||||||
|
detailsView = new(DetailsView)
|
||||||
|
|
||||||
|
// populate main fields
|
||||||
|
detailsView.Name = name
|
||||||
|
detailsView.gui = gui
|
||||||
|
|
||||||
|
return detailsView
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||||
|
func (view *DetailsView) Setup(v *gocui.View, header *gocui.View) error {
|
||||||
|
|
||||||
|
// set view options
|
||||||
|
view.view = v
|
||||||
|
view.view.Editable = false
|
||||||
|
view.view.Wrap = true
|
||||||
|
view.view.Highlight = false
|
||||||
|
view.view.Frame = false
|
||||||
|
|
||||||
|
view.header = header
|
||||||
|
view.header.Editable = false
|
||||||
|
view.header.Wrap = false
|
||||||
|
view.header.Frame = false
|
||||||
|
|
||||||
|
// set keybindings
|
||||||
|
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorDown() }); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorUp() }); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsVisible indicates if the details view pane is currently initialized.
|
||||||
|
func (view *DetailsView) IsVisible() bool {
|
||||||
|
if view == nil {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
|
||||||
|
func (view *DetailsView) CursorDown() error {
|
||||||
|
return CursorDown(view.gui, view.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
|
||||||
|
func (view *DetailsView) CursorUp() error {
|
||||||
|
return CursorUp(view.gui, view.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update refreshes the state objects for future rendering. Note: we only need to update this view upon the initial tree load
|
||||||
|
func (view *DetailsView) Update() error {
|
||||||
|
layerTrees := Views.Tree.RefTrees
|
||||||
|
view.efficiency, view.inefficiencies = filetree.Efficiency(layerTrees[:len(layerTrees)-1])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render flushes the state objects to the screen. The details pane reports:
|
||||||
|
// 1. the current selected layer's command string
|
||||||
|
// 2. the image efficiency score
|
||||||
|
// 3. the estimated wasted image space
|
||||||
|
// 4. a list of inefficient file allocations
|
||||||
|
func (view *DetailsView) Render() error {
|
||||||
|
currentLayer := Views.Layer.currentLayer()
|
||||||
|
|
||||||
|
var wastedSpace int64
|
||||||
|
|
||||||
|
template := "%5s %12s %-s\n"
|
||||||
|
var trueInefficiencies int
|
||||||
|
inefficiencyReport := fmt.Sprintf(Formatting.Header(template), "Count", "Total Space", "Path")
|
||||||
|
for idx := len(view.inefficiencies)-1; idx > 0; idx-- {
|
||||||
|
data := view.inefficiencies[idx]
|
||||||
|
if data.CumulativeSize == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trueInefficiencies++
|
||||||
|
wastedSpace += data.CumulativeSize
|
||||||
|
inefficiencyReport += fmt.Sprintf(template, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
|
||||||
|
}
|
||||||
|
if trueInefficiencies == 0 {
|
||||||
|
inefficiencyReport = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
effStr := fmt.Sprintf("\n%s %d %%", Formatting.Header("Image efficiency score:"), int(100.0*view.efficiency))
|
||||||
|
spaceStr := fmt.Sprintf("%s %s\n", Formatting.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
|
||||||
|
|
||||||
|
view.gui.Update(func(g *gocui.Gui) error {
|
||||||
|
// update header
|
||||||
|
view.header.Clear()
|
||||||
|
width, _ := g.Size()
|
||||||
|
headerStr := fmt.Sprintf("[Image & Layer Details]%s", strings.Repeat("─",width*2))
|
||||||
|
fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
|
||||||
|
|
||||||
|
// update contents
|
||||||
|
view.view.Clear()
|
||||||
|
fmt.Fprintln(view.view, Formatting.Header("Layer Command"))
|
||||||
|
fmt.Fprintln(view.view, currentLayer.History.CreatedBy)
|
||||||
|
|
||||||
|
fmt.Fprintln(view.view, effStr)
|
||||||
|
fmt.Fprintln(view.view, spaceStr)
|
||||||
|
|
||||||
|
fmt.Fprintln(view.view, inefficiencyReport)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
|
||||||
|
func (view *DetailsView) KeyHelp() string {
|
||||||
|
return "TBD"
|
||||||
|
}
|
@ -17,7 +17,8 @@ const (
|
|||||||
|
|
||||||
type CompareType int
|
type CompareType int
|
||||||
|
|
||||||
|
// FileTreeView holds the UI objects and data models for populating the right pane. Specifically the pane that
|
||||||
|
// shows selected layer or aggregate file ASCII tree.
|
||||||
type FileTreeView struct {
|
type FileTreeView struct {
|
||||||
Name string
|
Name string
|
||||||
gui *gocui.Gui
|
gui *gocui.Gui
|
||||||
@ -33,19 +34,21 @@ type FileTreeView struct {
|
|||||||
bufferIndexLowerBound uint
|
bufferIndexLowerBound uint
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree) (treeview *FileTreeView) {
|
// NewFileTreeView creates a new view object attached the the global [gocui] screen object.
|
||||||
treeview = new(FileTreeView)
|
func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree) (treeView *FileTreeView) {
|
||||||
|
treeView = new(FileTreeView)
|
||||||
|
|
||||||
// populate main fields
|
// populate main fields
|
||||||
treeview.Name = name
|
treeView.Name = name
|
||||||
treeview.gui = gui
|
treeView.gui = gui
|
||||||
treeview.ModelTree = tree
|
treeView.ModelTree = tree
|
||||||
treeview.RefTrees = refTrees
|
treeView.RefTrees = refTrees
|
||||||
treeview.HiddenDiffTypes = make([]bool, 4)
|
treeView.HiddenDiffTypes = make([]bool, 4)
|
||||||
|
|
||||||
return treeview
|
return treeView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||||
func (view *FileTreeView) Setup(v *gocui.View, header *gocui.View) error {
|
func (view *FileTreeView) Setup(v *gocui.View, header *gocui.View) error {
|
||||||
|
|
||||||
// set view options
|
// set view options
|
||||||
@ -91,20 +94,31 @@ func (view *FileTreeView) Setup(v *gocui.View, header *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// height obtains the height of the current pane (taking into account the lost space due to headers and footers).
|
||||||
func (view *FileTreeView) height() uint {
|
func (view *FileTreeView) height() uint {
|
||||||
_, height := view.view.Size()
|
_, height := view.view.Size()
|
||||||
return uint(height - 2)
|
return uint(height - 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsVisible indicates if the file tree view pane is currently initialized
|
||||||
func (view *FileTreeView) IsVisible() bool {
|
func (view *FileTreeView) IsVisible() bool {
|
||||||
if view == nil {return false}
|
if view == nil {return false}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
|
||||||
|
func (view *FileTreeView) resetCursor() {
|
||||||
|
view.view.SetCursor(0, 0)
|
||||||
|
view.TreeIndex = 0
|
||||||
|
view.bufferIndex = 0
|
||||||
|
view.bufferIndexLowerBound = 0
|
||||||
|
view.bufferIndexUpperBound = view.height()
|
||||||
|
}
|
||||||
|
|
||||||
|
// setTreeByLayer populates the view model by stacking the indicated image layer file trees.
|
||||||
func (view *FileTreeView) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
|
func (view *FileTreeView) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
|
||||||
if topTreeStop > len(view.RefTrees)-1 {
|
if topTreeStop > len(view.RefTrees)-1 {
|
||||||
return fmt.Errorf("Invalid layer index given: %d of %d", topTreeStop, len(view.RefTrees)-1)
|
return fmt.Errorf("invalid layer index given: %d of %d", topTreeStop, len(view.RefTrees)-1)
|
||||||
}
|
}
|
||||||
newTree := filetree.StackRange(view.RefTrees, bottomTreeStart, bottomTreeStop)
|
newTree := filetree.StackRange(view.RefTrees, bottomTreeStart, bottomTreeStop)
|
||||||
|
|
||||||
@ -122,13 +136,14 @@ func (view *FileTreeView) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTre
|
|||||||
}
|
}
|
||||||
view.ModelTree.VisitDepthChildFirst(visitor, nil)
|
view.ModelTree.VisitDepthChildFirst(visitor, nil)
|
||||||
|
|
||||||
view.view.SetCursor(0, 0)
|
view.resetCursor()
|
||||||
view.TreeIndex = 0
|
|
||||||
view.ModelTree = newTree
|
view.ModelTree = newTree
|
||||||
view.Update()
|
view.Update()
|
||||||
return view.Render()
|
return view.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// doCursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer.
|
||||||
func (view *FileTreeView) doCursorUp() {
|
func (view *FileTreeView) doCursorUp() {
|
||||||
view.TreeIndex--
|
view.TreeIndex--
|
||||||
if view.TreeIndex < view.bufferIndexLowerBound {
|
if view.TreeIndex < view.bufferIndexLowerBound {
|
||||||
@ -141,6 +156,7 @@ func (view *FileTreeView) doCursorUp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer.
|
||||||
func (view *FileTreeView) doCursorDown() {
|
func (view *FileTreeView) doCursorDown() {
|
||||||
view.TreeIndex++
|
view.TreeIndex++
|
||||||
if view.TreeIndex > view.bufferIndexUpperBound {
|
if view.TreeIndex > view.bufferIndexUpperBound {
|
||||||
@ -153,22 +169,21 @@ func (view *FileTreeView) doCursorDown() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorDown moves the cursor down and renders the view.
|
||||||
|
// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
|
||||||
|
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
|
||||||
|
// this range into the view buffer. This is much faster when tree sizes are large.
|
||||||
func (view *FileTreeView) CursorDown() error {
|
func (view *FileTreeView) CursorDown() error {
|
||||||
// we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
|
|
||||||
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
|
|
||||||
// this range into the view buffer. This is much faster when tree sizes are large.
|
|
||||||
|
|
||||||
view.doCursorDown()
|
view.doCursorDown()
|
||||||
return view.Render()
|
return view.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CursorUp moves the cursor up and renders the view.
|
||||||
|
// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
|
||||||
|
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
|
||||||
|
// this range into the view buffer. This is much faster when tree sizes are large.
|
||||||
func (view *FileTreeView) CursorUp() error {
|
func (view *FileTreeView) CursorUp() error {
|
||||||
// we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
|
|
||||||
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
|
|
||||||
// this range into the view buffer. This is much faster when tree sizes are large.
|
|
||||||
|
|
||||||
if view.TreeIndex > 0 {
|
if view.TreeIndex > 0 {
|
||||||
view.doCursorUp()
|
view.doCursorUp()
|
||||||
return view.Render()
|
return view.Render()
|
||||||
@ -176,12 +191,13 @@ func (view *FileTreeView) CursorUp() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
|
||||||
func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) {
|
func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) {
|
||||||
var visiter func(*filetree.FileNode) error
|
var visitor func(*filetree.FileNode) error
|
||||||
var evaluator func(*filetree.FileNode) bool
|
var evaluator func(*filetree.FileNode) bool
|
||||||
var dfsCounter uint
|
var dfsCounter uint
|
||||||
|
|
||||||
visiter = func(curNode *filetree.FileNode) error {
|
visitor = func(curNode *filetree.FileNode) error {
|
||||||
if dfsCounter == view.TreeIndex {
|
if dfsCounter == view.TreeIndex {
|
||||||
node = curNode
|
node = curNode
|
||||||
}
|
}
|
||||||
@ -207,7 +223,7 @@ func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) {
|
|||||||
return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch
|
return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
err = view.ModelTree.VisitDepthParentFirst(visiter, evaluator)
|
err = view.ModelTree.VisitDepthParentFirst(visitor, evaluator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -215,6 +231,7 @@ func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toggleCollapse will collapse/expand the selected FileNode.
|
||||||
func (view *FileTreeView) toggleCollapse() error {
|
func (view *FileTreeView) toggleCollapse() error {
|
||||||
node := view.getAbsPositionNode()
|
node := view.getAbsPositionNode()
|
||||||
if node != nil {
|
if node != nil {
|
||||||
@ -224,17 +241,18 @@ func (view *FileTreeView) toggleCollapse() error {
|
|||||||
return view.Render()
|
return view.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
|
||||||
func (view *FileTreeView) toggleShowDiffType(diffType filetree.DiffType) error {
|
func (view *FileTreeView) toggleShowDiffType(diffType filetree.DiffType) error {
|
||||||
view.HiddenDiffTypes[diffType] = !view.HiddenDiffTypes[diffType]
|
view.HiddenDiffTypes[diffType] = !view.HiddenDiffTypes[diffType]
|
||||||
|
|
||||||
view.view.SetCursor(0, 0)
|
view.resetCursor()
|
||||||
view.TreeIndex = 0
|
|
||||||
|
|
||||||
Update()
|
Update()
|
||||||
Render()
|
Render()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filterRegex will return a regular expression object to match the user's filter input.
|
||||||
func filterRegex() *regexp.Regexp {
|
func filterRegex() *regexp.Regexp {
|
||||||
if Views.Filter == nil || Views.Filter.view == nil {
|
if Views.Filter == nil || Views.Filter.view == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -252,6 +270,7 @@ func filterRegex() *regexp.Regexp {
|
|||||||
return regex
|
return regex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update refreshes the state objects for future rendering.
|
||||||
func (view *FileTreeView) Update() error {
|
func (view *FileTreeView) Update() error {
|
||||||
regex := filterRegex()
|
regex := filterRegex()
|
||||||
|
|
||||||
@ -282,14 +301,7 @@ func (view *FileTreeView) Update() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (view *FileTreeView) KeyHelp() string {
|
// Render flushes the state objects (file tree) to the pane.
|
||||||
return renderStatusOption("Space","Collapse dir", false) +
|
|
||||||
renderStatusOption("^A","Added files", !view.HiddenDiffTypes[filetree.Added]) +
|
|
||||||
renderStatusOption("^R","Removed files", !view.HiddenDiffTypes[filetree.Removed]) +
|
|
||||||
renderStatusOption("^M","Modified files", !view.HiddenDiffTypes[filetree.Changed]) +
|
|
||||||
renderStatusOption("^U","Unmodified files", !view.HiddenDiffTypes[filetree.Unchanged])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (view *FileTreeView) Render() error {
|
func (view *FileTreeView) Render() error {
|
||||||
treeString := view.ViewTree.StringBetween(view.bufferIndexLowerBound, view.bufferIndexUpperBound,true)
|
treeString := view.ViewTree.StringBetween(view.bufferIndexLowerBound, view.bufferIndexUpperBound,true)
|
||||||
lines := strings.Split(treeString, "\n")
|
lines := strings.Split(treeString, "\n")
|
||||||
@ -299,10 +311,22 @@ func (view *FileTreeView) Render() error {
|
|||||||
view.doCursorUp()
|
view.doCursorUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
title := "Current Layer Contents"
|
||||||
|
if Views.Layer.CompareMode == CompareAll {
|
||||||
|
title = "Aggregated Layer Contents"
|
||||||
|
}
|
||||||
|
|
||||||
|
// indicate when selected
|
||||||
|
if view.gui.CurrentView() == view.view {
|
||||||
|
title = "● "+title
|
||||||
|
}
|
||||||
|
|
||||||
view.gui.Update(func(g *gocui.Gui) error {
|
view.gui.Update(func(g *gocui.Gui) error {
|
||||||
// update the header
|
// update the header
|
||||||
view.header.Clear()
|
view.header.Clear()
|
||||||
headerStr := fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree")
|
width, _ := g.Size()
|
||||||
|
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
|
||||||
|
headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree")
|
||||||
fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
|
fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
|
||||||
|
|
||||||
// update the contents
|
// update the contents
|
||||||
@ -319,3 +343,12 @@ func (view *FileTreeView) Render() error {
|
|||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||||
|
func (view *FileTreeView) KeyHelp() string {
|
||||||
|
return renderStatusOption("Space","Collapse dir", false) +
|
||||||
|
renderStatusOption("^A","Added files", !view.HiddenDiffTypes[filetree.Added]) +
|
||||||
|
renderStatusOption("^R","Removed files", !view.HiddenDiffTypes[filetree.Removed]) +
|
||||||
|
renderStatusOption("^M","Modified files", !view.HiddenDiffTypes[filetree.Changed]) +
|
||||||
|
renderStatusOption("^U","Unmodified files", !view.HiddenDiffTypes[filetree.Unchanged])
|
||||||
|
}
|
@ -6,8 +6,8 @@ import (
|
|||||||
"github.com/jroimartin/gocui"
|
"github.com/jroimartin/gocui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// with special thanks to https://gist.github.com/jroimartin/3b2e943a3811d795e0718b4a95b89bec
|
// DetailsView holds the UI objects and data models for populating the bottom row. Specifically the pane that
|
||||||
|
// allows the user to filter the file tree by path.
|
||||||
type FilterView struct {
|
type FilterView struct {
|
||||||
Name string
|
Name string
|
||||||
gui *gocui.Gui
|
gui *gocui.Gui
|
||||||
@ -18,25 +18,20 @@ type FilterView struct {
|
|||||||
hidden bool
|
hidden bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Input struct {
|
// NewFilterView creates a new view object attached the the global [gocui] screen object.
|
||||||
name string
|
func NewFilterView(name string, gui *gocui.Gui) (filterView *FilterView) {
|
||||||
x, y int
|
filterView = new(FilterView)
|
||||||
w int
|
|
||||||
maxLength int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFilterView(name string, gui *gocui.Gui) (filterview *FilterView) {
|
|
||||||
filterview = new(FilterView)
|
|
||||||
|
|
||||||
// populate main fields
|
// populate main fields
|
||||||
filterview.Name = name
|
filterView.Name = name
|
||||||
filterview.gui = gui
|
filterView.gui = gui
|
||||||
filterview.headerStr = "Path Filter: "
|
filterView.headerStr = "Path Filter: "
|
||||||
filterview.hidden = true
|
filterView.hidden = true
|
||||||
|
|
||||||
return filterview
|
return filterView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||||
func (view *FilterView) Setup(v *gocui.View, header *gocui.View) error {
|
func (view *FilterView) Setup(v *gocui.View, header *gocui.View) error {
|
||||||
|
|
||||||
// set view options
|
// set view options
|
||||||
@ -53,32 +48,28 @@ func (view *FilterView) Setup(v *gocui.View, header *gocui.View) error {
|
|||||||
view.header.Wrap = false
|
view.header.Wrap = false
|
||||||
view.header.Frame = false
|
view.header.Frame = false
|
||||||
|
|
||||||
// set keybindings
|
|
||||||
// if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorDown() }); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorUp() }); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
view.Render()
|
view.Render()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsVisible indicates if the filter view pane is currently initialized
|
||||||
func (view *FilterView) IsVisible() bool {
|
func (view *FilterView) IsVisible() bool {
|
||||||
if view == nil {return false}
|
if view == nil {return false}
|
||||||
return !view.hidden
|
return !view.hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorDown moves the cursor down in the filter pane (currently indicates nothing).
|
||||||
func (view *FilterView) CursorDown() error {
|
func (view *FilterView) CursorDown() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorUp moves the cursor up in the filter pane (currently indicates nothing).
|
||||||
func (view *FilterView) CursorUp() error {
|
func (view *FilterView) CursorUp() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit intercepts the key press events in the filer view to update the file view in real time.
|
||||||
func (view *FilterView) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
func (view *FilterView) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
||||||
if !view.IsVisible() {
|
if !view.IsVisible() {
|
||||||
return
|
return
|
||||||
@ -101,14 +92,12 @@ func (view *FilterView) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Mo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (view *FilterView) KeyHelp() string {
|
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||||
return Formatting.StatusControlNormal("▏Type to filter the file tree ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (view *FilterView) Update() error {
|
func (view *FilterView) Update() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render flushes the state objects to the screen. Currently this is the users path filter input.
|
||||||
func (view *FilterView) Render() error {
|
func (view *FilterView) Render() error {
|
||||||
view.gui.Update(func(g *gocui.Gui) error {
|
view.gui.Update(func(g *gocui.Gui) error {
|
||||||
// render the header
|
// render the header
|
||||||
@ -116,6 +105,10 @@ func (view *FilterView) Render() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
// todo: blerg
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||||
|
func (view *FilterView) KeyHelp() string {
|
||||||
|
return Formatting.StatusControlNormal("▏Type to filter the file tree ")
|
||||||
|
}
|
139
ui/layerview.go
139
ui/layerview.go
@ -7,8 +7,11 @@ import (
|
|||||||
"github.com/wagoodman/dive/image"
|
"github.com/wagoodman/dive/image"
|
||||||
"github.com/lunixbochs/vtclean"
|
"github.com/lunixbochs/vtclean"
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LayerView holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
|
||||||
|
// shows the image layers and layer selector.
|
||||||
type LayerView struct {
|
type LayerView struct {
|
||||||
Name string
|
Name string
|
||||||
gui *gocui.Gui
|
gui *gocui.Gui
|
||||||
@ -20,27 +23,26 @@ type LayerView struct {
|
|||||||
CompareStartIndex int
|
CompareStartIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (layerview *LayerView) {
|
// NewDetailsView creates a new view object attached the the global [gocui] screen object.
|
||||||
layerview = new(LayerView)
|
func NewLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (layerView *LayerView) {
|
||||||
|
layerView = new(LayerView)
|
||||||
|
|
||||||
// populate main fields
|
// populate main fields
|
||||||
layerview.Name = name
|
layerView.Name = name
|
||||||
layerview.gui = gui
|
layerView.gui = gui
|
||||||
layerview.Layers = layers
|
layerView.Layers = layers
|
||||||
layerview.CompareMode = CompareLayer
|
layerView.CompareMode = CompareLayer
|
||||||
|
|
||||||
return layerview
|
return layerView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||||
func (view *LayerView) Setup(v *gocui.View, header *gocui.View) error {
|
func (view *LayerView) Setup(v *gocui.View, header *gocui.View) error {
|
||||||
|
|
||||||
// set view options
|
// set view options
|
||||||
view.view = v
|
view.view = v
|
||||||
view.view.Editable = false
|
view.view.Editable = false
|
||||||
view.view.Wrap = false
|
view.view.Wrap = false
|
||||||
//view.view.Highlight = true
|
|
||||||
//view.view.SelBgColor = gocui.ColorGreen
|
|
||||||
//view.view.SelFgColor = gocui.ColorBlack
|
|
||||||
view.view.Frame = false
|
view.view.Frame = false
|
||||||
|
|
||||||
view.header = header
|
view.header = header
|
||||||
@ -65,11 +67,50 @@ func (view *LayerView) Setup(v *gocui.View, header *gocui.View) error {
|
|||||||
return view.Render()
|
return view.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsVisible indicates if the layer view pane is currently initialized.
|
||||||
func (view *LayerView) IsVisible() bool {
|
func (view *LayerView) IsVisible() bool {
|
||||||
if view == nil {return false}
|
if view == nil {return false}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorDown moves the cursor down in the layer pane (selecting a higher layer).
|
||||||
|
func (view *LayerView) CursorDown() error {
|
||||||
|
if view.LayerIndex < len(view.Layers) {
|
||||||
|
err := CursorDown(view.gui, view.view)
|
||||||
|
if err == nil {
|
||||||
|
view.SetCursor(view.LayerIndex+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorUp moves the cursor up in the layer pane (selecting a lower layer).
|
||||||
|
func (view *LayerView) CursorUp() error {
|
||||||
|
if view.LayerIndex > 0 {
|
||||||
|
err := CursorUp(view.gui, view.view)
|
||||||
|
if err == nil {
|
||||||
|
view.SetCursor(view.LayerIndex-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCursor resets the cursor and orients the file tree view based on the given layer index.
|
||||||
|
func (view *LayerView) SetCursor(layer int) error {
|
||||||
|
view.LayerIndex = layer
|
||||||
|
Views.Tree.setTreeByLayer(view.getCompareIndexes())
|
||||||
|
Views.Details.Render()
|
||||||
|
view.Render()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// currentLayer returns the Layer object currently selected.
|
||||||
|
func (view *LayerView) currentLayer() *image.Layer {
|
||||||
|
return view.Layers[(len(view.Layers)-1)-view.LayerIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison.
|
||||||
func (view *LayerView) setCompareMode(compareMode CompareType) error {
|
func (view *LayerView) setCompareMode(compareMode CompareType) error {
|
||||||
view.CompareMode = compareMode
|
view.CompareMode = compareMode
|
||||||
Update()
|
Update()
|
||||||
@ -77,6 +118,7 @@ func (view *LayerView) setCompareMode(compareMode CompareType) error {
|
|||||||
return Views.Tree.setTreeByLayer(view.getCompareIndexes())
|
return Views.Tree.setTreeByLayer(view.getCompareIndexes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode)
|
||||||
func (view *LayerView) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) {
|
func (view *LayerView) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) {
|
||||||
bottomTreeStart = view.CompareStartIndex
|
bottomTreeStart = view.CompareStartIndex
|
||||||
topTreeStop = view.LayerIndex
|
topTreeStop = view.LayerIndex
|
||||||
@ -95,16 +137,11 @@ func (view *LayerView) getCompareIndexes() (bottomTreeStart, bottomTreeStop, top
|
|||||||
return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop
|
return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderCompareBar returns the formatted string for the given layer.
|
||||||
func (view *LayerView) renderCompareBar(layerIdx int) string {
|
func (view *LayerView) renderCompareBar(layerIdx int) string {
|
||||||
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := view.getCompareIndexes()
|
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := view.getCompareIndexes()
|
||||||
result := " "
|
result := " "
|
||||||
|
|
||||||
//if debug {
|
|
||||||
// v, _ := view.gui.View("debug")
|
|
||||||
// v.Clear()
|
|
||||||
// _, _ = fmt.Fprintf(v, "bStart: %d bStop: %d tStart: %d tStop: %d", bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
|
|
||||||
//}
|
|
||||||
|
|
||||||
if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop {
|
if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop {
|
||||||
result = Formatting.CompareBottom(" ")
|
result = Formatting.CompareBottom(" ")
|
||||||
}
|
}
|
||||||
@ -112,29 +149,31 @@ func (view *LayerView) renderCompareBar(layerIdx int) string {
|
|||||||
result = Formatting.CompareTop(" ")
|
result = Formatting.CompareTop(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
//if bottomTreeStop == topTreeStart {
|
|
||||||
// result += " "
|
|
||||||
//} else {
|
|
||||||
// if layerIdx == bottomTreeStop {
|
|
||||||
// result += "─┐"
|
|
||||||
// } else if layerIdx == topTreeStart {
|
|
||||||
// result += "─┘"
|
|
||||||
// } else {
|
|
||||||
// result += " "
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||||
func (view *LayerView) Update() error {
|
func (view *LayerView) Update() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render flushes the state objects to the screen. The layers pane reports:
|
||||||
|
// 1. the layers of the image + metadata
|
||||||
|
// 2. the current selected image
|
||||||
func (view *LayerView) Render() error {
|
func (view *LayerView) Render() error {
|
||||||
|
|
||||||
|
// indicate when selected
|
||||||
|
title := "Layers"
|
||||||
|
if view.gui.CurrentView() == view.view {
|
||||||
|
title = "● "+title
|
||||||
|
}
|
||||||
|
|
||||||
view.gui.Update(func(g *gocui.Gui) error {
|
view.gui.Update(func(g *gocui.Gui) error {
|
||||||
// update header
|
// update header
|
||||||
headerStr := fmt.Sprintf("Cmp "+image.LayerFormat, "Image ID", "%Eff.", "Size", "Filter")
|
view.header.Clear()
|
||||||
|
width, _ := g.Size()
|
||||||
|
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
|
||||||
|
headerStr += fmt.Sprintf("Cmp "+image.LayerFormat, "Image ID", "Size", "Command")
|
||||||
fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
|
fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
|
||||||
|
|
||||||
// update contents
|
// update contents
|
||||||
@ -152,7 +191,7 @@ func (view *LayerView) Render() error {
|
|||||||
layerId = fmt.Sprintf("%-25s", layer.History.ID)
|
layerId = fmt.Sprintf("%-25s", layer.History.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
layerStr = fmt.Sprintf(image.LayerFormat, layerId, "", humanize.Bytes(uint64(layer.History.Size)), "FROM "+layer.Id())
|
layerStr = fmt.Sprintf(image.LayerFormat, layerId, humanize.Bytes(uint64(layer.History.Size)), "FROM "+layer.Id())
|
||||||
}
|
}
|
||||||
|
|
||||||
compareBar := view.renderCompareBar(idx)
|
compareBar := view.renderCompareBar(idx)
|
||||||
@ -166,46 +205,12 @@ func (view *LayerView) Render() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
// todo: blerg
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (view *LayerView) CursorDown() error {
|
|
||||||
if view.LayerIndex < len(view.Layers) {
|
|
||||||
err := CursorDown(view.gui, view.view)
|
|
||||||
if err == nil {
|
|
||||||
view.LayerIndex++
|
|
||||||
Views.Tree.setTreeByLayer(view.getCompareIndexes())
|
|
||||||
view.Render()
|
|
||||||
// debugPrint(fmt.Sprintf("%d",len(filetree.Cache)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (view *LayerView) CursorUp() error {
|
|
||||||
if view.LayerIndex > 0 {
|
|
||||||
err := CursorUp(view.gui, view.view)
|
|
||||||
if err == nil {
|
|
||||||
view.LayerIndex--
|
|
||||||
Views.Tree.setTreeByLayer(view.getCompareIndexes())
|
|
||||||
view.Render()
|
|
||||||
// debugPrint(fmt.Sprintf("%d",len(filetree.Cache)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (view *LayerView) SetCursor(layer int) error {
|
|
||||||
// view.view.SetCursor(0, layer)
|
|
||||||
view.LayerIndex = layer
|
|
||||||
Views.Tree.setTreeByLayer(view.getCompareIndexes())
|
|
||||||
view.Render()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||||
func (view *LayerView) KeyHelp() string {
|
func (view *LayerView) KeyHelp() string {
|
||||||
return renderStatusOption("^L","Layer changes", view.CompareMode == CompareLayer) +
|
return renderStatusOption("^L","Show layer changes", view.CompareMode == CompareLayer) +
|
||||||
renderStatusOption("^A","All changes", view.CompareMode == CompareAll)
|
renderStatusOption("^A","Show aggregated changes", view.CompareMode == CompareAll)
|
||||||
}
|
}
|
||||||
|
@ -7,57 +7,59 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DetailsView holds the UI objects and data models for populating the bottom-most pane. Specifcially the panel
|
||||||
|
// shows the user a set of possible actions to take in the window and currently selected pane.
|
||||||
type StatusView struct {
|
type StatusView struct {
|
||||||
Name string
|
Name string
|
||||||
gui *gocui.Gui
|
gui *gocui.Gui
|
||||||
view *gocui.View
|
view *gocui.View
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStatusView(name string, gui *gocui.Gui) (statusview *StatusView) {
|
// NewStatusView creates a new view object attached the the global [gocui] screen object.
|
||||||
statusview = new(StatusView)
|
func NewStatusView(name string, gui *gocui.Gui) (statusView *StatusView) {
|
||||||
|
statusView = new(StatusView)
|
||||||
|
|
||||||
// populate main fields
|
// populate main fields
|
||||||
statusview.Name = name
|
statusView.Name = name
|
||||||
statusview.gui = gui
|
statusView.gui = gui
|
||||||
|
|
||||||
return statusview
|
return statusView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||||
func (view *StatusView) Setup(v *gocui.View, header *gocui.View) error {
|
func (view *StatusView) Setup(v *gocui.View, header *gocui.View) error {
|
||||||
|
|
||||||
// set view options
|
// set view options
|
||||||
view.view = v
|
view.view = v
|
||||||
view.view.Frame = false
|
view.view.Frame = false
|
||||||
//view.view.BgColor = gocui.ColorDefault + gocui.AttrReverse
|
|
||||||
|
|
||||||
view.Render()
|
view.Render()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsVisible indicates if the status view pane is currently initialized.
|
||||||
func (view *StatusView) IsVisible() bool {
|
func (view *StatusView) IsVisible() bool {
|
||||||
if view == nil {return false}
|
if view == nil {return false}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
|
||||||
func (view *StatusView) CursorDown() error {
|
func (view *StatusView) CursorDown() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
|
||||||
func (view *StatusView) CursorUp() error {
|
func (view *StatusView) CursorUp() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (view *StatusView) KeyHelp() string {
|
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||||
return renderStatusOption("^C","Quit", false) +
|
|
||||||
renderStatusOption("^Space","Switch view", false) +
|
|
||||||
renderStatusOption("^/","Filter files", Views.Filter.IsVisible())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (view *StatusView) Update() error {
|
func (view *StatusView) Update() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render flushes the state objects to the screen.
|
||||||
func (view *StatusView) Render() error {
|
func (view *StatusView) Render() error {
|
||||||
view.gui.Update(func(g *gocui.Gui) error {
|
view.gui.Update(func(g *gocui.Gui) error {
|
||||||
view.view.Clear()
|
view.view.Clear()
|
||||||
@ -68,3 +70,10 @@ func (view *StatusView) Render() error {
|
|||||||
// todo: blerg
|
// todo: blerg
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyHelp indicates all the possible global actions a user can take when any pane is selected.
|
||||||
|
func (view *StatusView) KeyHelp() string {
|
||||||
|
return renderStatusOption("^C","Quit", false) +
|
||||||
|
renderStatusOption("^Space","Switch view", false) +
|
||||||
|
renderStatusOption("^/","Filter files", Views.Filter.IsVisible())
|
||||||
|
}
|
56
ui/ui.go
56
ui/ui.go
@ -17,6 +17,10 @@ import (
|
|||||||
const debug = false
|
const debug = false
|
||||||
const profile = false
|
const profile = false
|
||||||
|
|
||||||
|
var cpuProfilePath *os.File
|
||||||
|
var memoryProfilePath *os.File
|
||||||
|
|
||||||
|
// debugPrint writes the given string to the debug pane (if the debug pane is enabled)
|
||||||
func debugPrint(s string) {
|
func debugPrint(s string) {
|
||||||
if debug && Views.Tree != nil && Views.Tree.gui != nil {
|
if debug && Views.Tree != nil && Views.Tree.gui != nil {
|
||||||
v, _ := Views.Tree.gui.View("debug")
|
v, _ := Views.Tree.gui.View("debug")
|
||||||
@ -29,6 +33,7 @@ func debugPrint(s string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Formatting defines standard functions for formatting UI sections.
|
||||||
var Formatting struct {
|
var Formatting struct {
|
||||||
Header func(...interface{})(string)
|
Header func(...interface{})(string)
|
||||||
Selected func(...interface{})(string)
|
Selected func(...interface{})(string)
|
||||||
@ -40,14 +45,17 @@ var Formatting struct {
|
|||||||
CompareBottom func(...interface{})(string)
|
CompareBottom func(...interface{})(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Views contains all rendered UI panes.
|
||||||
var Views struct {
|
var Views struct {
|
||||||
Tree *FileTreeView
|
Tree *FileTreeView
|
||||||
Layer *LayerView
|
Layer *LayerView
|
||||||
Status *StatusView
|
Status *StatusView
|
||||||
Filter *FilterView
|
Filter *FilterView
|
||||||
|
Details *DetailsView
|
||||||
lookup map[string]View
|
lookup map[string]View
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// View defines the a renderable terminal screen pane.
|
||||||
type View interface {
|
type View interface {
|
||||||
Setup(*gocui.View, *gocui.View) error
|
Setup(*gocui.View, *gocui.View) error
|
||||||
CursorDown() error
|
CursorDown() error
|
||||||
@ -58,6 +66,7 @@ type View interface {
|
|||||||
IsVisible() bool
|
IsVisible() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toggleView switches between the file view and the layer view and re-renders the screen.
|
||||||
func toggleView(g *gocui.Gui, v *gocui.View) error {
|
func toggleView(g *gocui.Gui, v *gocui.View) error {
|
||||||
if v == nil || v.Name() == Views.Layer.Name {
|
if v == nil || v.Name() == Views.Layer.Name {
|
||||||
_, err := g.SetCurrentView(Views.Tree.Name)
|
_, err := g.SetCurrentView(Views.Tree.Name)
|
||||||
@ -71,6 +80,7 @@ func toggleView(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toggleFilterView shows/hides the file tree filter pane.
|
||||||
func toggleFilterView(g *gocui.Gui, v *gocui.View) error {
|
func toggleFilterView(g *gocui.Gui, v *gocui.View) error {
|
||||||
// delete all user input from the tree view
|
// delete all user input from the tree view
|
||||||
Views.Filter.view.Clear()
|
Views.Filter.view.Clear()
|
||||||
@ -93,6 +103,7 @@ func toggleFilterView(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorDown moves the cursor down in the currently selected gocui pane, scrolling the screen as needed.
|
||||||
func CursorDown(g *gocui.Gui, v *gocui.View) error {
|
func CursorDown(g *gocui.Gui, v *gocui.View) error {
|
||||||
cx, cy := v.Cursor()
|
cx, cy := v.Cursor()
|
||||||
|
|
||||||
@ -113,6 +124,7 @@ func CursorDown(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorUp moves the cursor up in the currently selected gocui pane, scrolling the screen as needed.
|
||||||
func CursorUp(g *gocui.Gui, v *gocui.View) error {
|
func CursorUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
ox, oy := v.Origin()
|
ox, oy := v.Origin()
|
||||||
cx, cy := v.Cursor()
|
cx, cy := v.Cursor()
|
||||||
@ -124,10 +136,7 @@ func CursorUp(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// quit is the gocui callback invoked when the user hits Ctrl+C
|
||||||
var cpuProfilePath *os.File
|
|
||||||
var memoryProfilePath *os.File
|
|
||||||
|
|
||||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||||
if profile {
|
if profile {
|
||||||
pprof.StopCPUProfile()
|
pprof.StopCPUProfile()
|
||||||
@ -139,7 +148,8 @@ func quit(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return gocui.ErrQuit
|
return gocui.ErrQuit
|
||||||
}
|
}
|
||||||
|
|
||||||
func keybindings(g *gocui.Gui) error {
|
// keyBindings registers global key press actions, valid when in any pane.
|
||||||
|
func keyBindings(g *gocui.Gui) error {
|
||||||
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -156,6 +166,7 @@ func keybindings(g *gocui.Gui) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isNewView determines if a view has already been created based on the set of errors given (a bit hokie)
|
||||||
func isNewView(errs ...error) bool {
|
func isNewView(errs ...error) bool {
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -168,8 +179,12 @@ func isNewView(errs ...error) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this logic should be refactored into an abstraction that takes care of the math for us
|
|
||||||
|
// layout defines the definition of the window pane size and placement relations to one another. This
|
||||||
|
// is invoked at application start and whenever the screen dimensions change.
|
||||||
func layout(g *gocui.Gui) error {
|
func layout(g *gocui.Gui) error {
|
||||||
|
// TODO: this logic should be refactored into an abstraction that takes care of the math for us
|
||||||
|
|
||||||
maxX, maxY := g.Size()
|
maxX, maxY := g.Size()
|
||||||
splitCols := maxX / 2
|
splitCols := maxX / 2
|
||||||
debugWidth := 0
|
debugWidth := 0
|
||||||
@ -178,7 +193,7 @@ func layout(g *gocui.Gui) error {
|
|||||||
}
|
}
|
||||||
debugCols := maxX - debugWidth
|
debugCols := maxX - debugWidth
|
||||||
bottomRows := 1
|
bottomRows := 1
|
||||||
headerRows := 1
|
headerRows := 2
|
||||||
|
|
||||||
filterBarHeight := 1
|
filterBarHeight := 1
|
||||||
statusBarHeight := 1
|
statusBarHeight := 1
|
||||||
@ -186,6 +201,8 @@ func layout(g *gocui.Gui) error {
|
|||||||
statusBarIndex := 1
|
statusBarIndex := 1
|
||||||
filterBarIndex := 2
|
filterBarIndex := 2
|
||||||
|
|
||||||
|
layersHeight := len(Views.Layer.Layers) + headerRows + 1 // layers + header + base image layer row
|
||||||
|
|
||||||
var view, header *gocui.View
|
var view, header *gocui.View
|
||||||
var viewErr, headerErr, err error
|
var viewErr, headerErr, err error
|
||||||
|
|
||||||
@ -204,7 +221,7 @@ func layout(g *gocui.Gui) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Layers
|
// Layers
|
||||||
view, viewErr = g.SetView(Views.Layer.Name, -1, -1+headerRows, splitCols, maxY-bottomRows)
|
view, viewErr = g.SetView(Views.Layer.Name, -1, -1+headerRows, splitCols, layersHeight)
|
||||||
header, headerErr = g.SetView(Views.Layer.Name+"header", -1, -1, splitCols, headerRows)
|
header, headerErr = g.SetView(Views.Layer.Name+"header", -1, -1, splitCols, headerRows)
|
||||||
if isNewView(viewErr, headerErr) {
|
if isNewView(viewErr, headerErr) {
|
||||||
Views.Layer.Setup(view, header)
|
Views.Layer.Setup(view, header)
|
||||||
@ -212,6 +229,15 @@ func layout(g *gocui.Gui) error {
|
|||||||
if _, err = g.SetCurrentView(Views.Layer.Name); err != nil {
|
if _, err = g.SetCurrentView(Views.Layer.Name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// since we are selecting the view, we should rerender to indicate it is selected
|
||||||
|
Views.Layer.Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details
|
||||||
|
view, viewErr = g.SetView(Views.Details.Name, -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows)
|
||||||
|
header, headerErr = g.SetView(Views.Details.Name+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows)
|
||||||
|
if isNewView(viewErr, headerErr) {
|
||||||
|
Views.Details.Setup(view, header)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filetree
|
// Filetree
|
||||||
@ -238,12 +264,14 @@ func layout(g *gocui.Gui) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update refreshes the state objects for future rendering.
|
||||||
func Update() {
|
func Update() {
|
||||||
for _, view := range Views.lookup {
|
for _, view := range Views.lookup {
|
||||||
view.Update()
|
view.Update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render flushes the state objects to the screen.
|
||||||
func Render() {
|
func Render() {
|
||||||
for _, view := range Views.lookup {
|
for _, view := range Views.lookup {
|
||||||
if view.IsVisible() {
|
if view.IsVisible() {
|
||||||
@ -252,6 +280,7 @@ func Render() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderStatusOption formats key help bindings-to-title pairs.
|
||||||
func renderStatusOption(control, title string, selected bool) string {
|
func renderStatusOption(control, title string, selected bool) string {
|
||||||
if selected {
|
if selected {
|
||||||
return Formatting.StatusSelected("▏") + Formatting.StatusControlSelected(control) + Formatting.StatusSelected(" " + title + " ")
|
return Formatting.StatusSelected("▏") + Formatting.StatusControlSelected(control) + Formatting.StatusSelected(" " + title + " ")
|
||||||
@ -260,6 +289,7 @@ func renderStatusOption(control, title string, selected bool) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run is the UI entrypoint.
|
||||||
func Run(layers []*image.Layer, refTrees []*filetree.FileTree) {
|
func Run(layers []*image.Layer, refTrees []*filetree.FileTree) {
|
||||||
|
|
||||||
Formatting.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc()
|
Formatting.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc()
|
||||||
@ -291,14 +321,22 @@ func Run(layers []*image.Layer, refTrees []*filetree.FileTree) {
|
|||||||
Views.Filter = NewFilterView("command", g)
|
Views.Filter = NewFilterView("command", g)
|
||||||
Views.lookup[Views.Filter.Name] = Views.Filter
|
Views.lookup[Views.Filter.Name] = Views.Filter
|
||||||
|
|
||||||
|
Views.Details = NewDetailsView("details", g)
|
||||||
|
Views.lookup[Views.Details.Name] = Views.Details
|
||||||
|
|
||||||
|
|
||||||
g.Cursor = false
|
g.Cursor = false
|
||||||
//g.Mouse = true
|
//g.Mouse = true
|
||||||
g.SetManagerFunc(layout)
|
g.SetManagerFunc(layout)
|
||||||
|
|
||||||
|
// perform the first update and render now that all resources have been loaded
|
||||||
|
Update()
|
||||||
|
Render()
|
||||||
|
|
||||||
// let the default position of the cursor be the last layer
|
// let the default position of the cursor be the last layer
|
||||||
// Views.Layer.SetCursor(len(Views.Layer.Layers)-1)
|
// Views.Layer.SetCursor(len(Views.Layer.Layers)-1)
|
||||||
|
|
||||||
if err := keybindings(g); err != nil {
|
if err := keyBindings(g); err != nil {
|
||||||
log.Panicln(err)
|
log.Panicln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user