a few low hanging perf improvements (#16)
This commit is contained in:
parent
ae4335620a
commit
c599ca5ad2
2
Makefile
2
Makefile
@ -13,7 +13,7 @@ install:
|
|||||||
go install ./...
|
go install ./...
|
||||||
|
|
||||||
test: build
|
test: build
|
||||||
go test -v ./...
|
go test -cover -v ./...
|
||||||
|
|
||||||
lint: build
|
lint: build
|
||||||
golint -set_exit_status $$(go list ./...)
|
golint -set_exit_status $$(go list ./...)
|
||||||
|
@ -1,23 +1,3 @@
|
|||||||
// Copyright © 2018 Alex Goodman
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
20
cmd/build.go
20
cmd/build.go
@ -1,23 +1,3 @@
|
|||||||
// Copyright © 2018 Alex Goodman
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
20
cmd/root.go
20
cmd/root.go
@ -1,23 +1,3 @@
|
|||||||
// Copyright © 2018 Alex Goodman
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
110
filetree/node.go
110
filetree/node.go
@ -15,12 +15,20 @@ const (
|
|||||||
AttributeFormat = "%s%s %10s %10s "
|
AttributeFormat = "%s%s %10s %10s "
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var diffTypeColor = map[DiffType]*color.Color {
|
||||||
|
Added: color.New(color.FgGreen),
|
||||||
|
Removed: color.New(color.FgRed),
|
||||||
|
Changed: color.New(color.FgYellow),
|
||||||
|
Unchanged: color.New(color.Reset),
|
||||||
|
}
|
||||||
|
|
||||||
type FileNode struct {
|
type FileNode struct {
|
||||||
Tree *FileTree
|
Tree *FileTree
|
||||||
Parent *FileNode
|
Parent *FileNode
|
||||||
Name string
|
Name string
|
||||||
Data NodeData
|
Data NodeData
|
||||||
Children map[string]*FileNode
|
Children map[string]*FileNode
|
||||||
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) {
|
func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) {
|
||||||
@ -37,6 +45,59 @@ func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: make more performant
|
||||||
|
// todo: rewrite with visitor functions
|
||||||
|
func (node *FileNode) renderTreeLine(spaces []bool, last bool, collapsed bool) string {
|
||||||
|
var otherBranches string
|
||||||
|
for _, space := range spaces {
|
||||||
|
if space {
|
||||||
|
otherBranches += noBranchSpace
|
||||||
|
} else {
|
||||||
|
otherBranches += branchSpace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thisBranch := middleItem
|
||||||
|
if last {
|
||||||
|
thisBranch = lastItem
|
||||||
|
}
|
||||||
|
|
||||||
|
collapsedIndicator := uncollapsedItem
|
||||||
|
if collapsed {
|
||||||
|
collapsedIndicator = collapsedItem
|
||||||
|
}
|
||||||
|
|
||||||
|
return otherBranches + thisBranch + collapsedIndicator + node.String() + newLine
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: make more performant
|
||||||
|
// todo: rewrite with visitor functions
|
||||||
|
func (node *FileNode) renderStringTree(spaces []bool, showAttributes bool, depth int) string {
|
||||||
|
var result string
|
||||||
|
var keys []string
|
||||||
|
for key := range node.Children {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for idx, name := range keys {
|
||||||
|
child := node.Children[name]
|
||||||
|
if child.Data.ViewInfo.Hidden {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
last := idx == (len(node.Children) - 1)
|
||||||
|
showCollapsed := child.Data.ViewInfo.Collapsed && len(child.Children) > 0
|
||||||
|
if showAttributes {
|
||||||
|
result += child.MetadataString() + " "
|
||||||
|
}
|
||||||
|
result += child.renderTreeLine(spaces, last, showCollapsed)
|
||||||
|
if len(child.Children) > 0 && !child.Data.ViewInfo.Collapsed {
|
||||||
|
spacesChild := append(spaces, last)
|
||||||
|
result += child.renderStringTree(spacesChild, showAttributes, depth+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@ -73,47 +134,22 @@ func (node *FileNode) Remove() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (node *FileNode) String() string {
|
func (node *FileNode) String() string {
|
||||||
var style *color.Color
|
|
||||||
var display string
|
var display string
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
switch node.Data.DiffType {
|
|
||||||
case Added:
|
|
||||||
style = color.New(color.FgGreen)
|
|
||||||
case Removed:
|
|
||||||
style = color.New(color.FgRed)
|
|
||||||
case Changed:
|
|
||||||
style = color.New(color.FgYellow)
|
|
||||||
case Unchanged:
|
|
||||||
style = color.New(color.Reset)
|
|
||||||
default:
|
|
||||||
style = color.New(color.BgMagenta)
|
|
||||||
}
|
|
||||||
display = node.Name
|
display = node.Name
|
||||||
if node.Data.FileInfo.TarHeader.Typeflag == tar.TypeSymlink || node.Data.FileInfo.TarHeader.Typeflag == tar.TypeLink {
|
if node.Data.FileInfo.TarHeader.Typeflag == tar.TypeSymlink || node.Data.FileInfo.TarHeader.Typeflag == tar.TypeLink {
|
||||||
display += " → " + node.Data.FileInfo.TarHeader.Linkname
|
display += " → " + node.Data.FileInfo.TarHeader.Linkname
|
||||||
}
|
}
|
||||||
return style.Sprint(display)
|
return diffTypeColor[node.Data.DiffType].Sprint(display)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *FileNode) MetadataString() string {
|
func (node *FileNode) MetadataString() string {
|
||||||
var style *color.Color
|
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
switch node.Data.DiffType {
|
|
||||||
case Added:
|
|
||||||
style = color.New(color.FgGreen)
|
|
||||||
case Removed:
|
|
||||||
style = color.New(color.FgRed)
|
|
||||||
case Changed:
|
|
||||||
style = color.New(color.FgYellow)
|
|
||||||
case Unchanged:
|
|
||||||
style = color.New(color.Reset)
|
|
||||||
default:
|
|
||||||
style = color.New(color.BgMagenta)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileMode := permbits.FileMode(node.Data.FileInfo.TarHeader.FileInfo().Mode()).String()
|
fileMode := permbits.FileMode(node.Data.FileInfo.TarHeader.FileInfo().Mode()).String()
|
||||||
dir := "-"
|
dir := "-"
|
||||||
@ -143,7 +179,7 @@ func (node *FileNode) MetadataString() string {
|
|||||||
|
|
||||||
size := humanize.Bytes(uint64(sizeBytes))
|
size := humanize.Bytes(uint64(sizeBytes))
|
||||||
|
|
||||||
return style.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 {
|
func (node *FileNode) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvaluator) error {
|
||||||
@ -205,7 +241,21 @@ func (node *FileNode) IsWhiteout() bool {
|
|||||||
return strings.HasPrefix(node.Name, whiteoutPrefix)
|
return strings.HasPrefix(node.Name, whiteoutPrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: make path() more efficient, similar to so (buggy):
|
||||||
|
// func (node *FileNode) Path() string {
|
||||||
|
// if node.path == "" {
|
||||||
|
// path := "/"
|
||||||
|
//
|
||||||
|
// if node.Parent != nil {
|
||||||
|
// path = node.Parent.Path()
|
||||||
|
// }
|
||||||
|
// node.path = path + "/" + strings.TrimPrefix(node.Name, whiteoutPrefix)
|
||||||
|
// }
|
||||||
|
// return node.path
|
||||||
|
// }
|
||||||
|
|
||||||
func (node *FileNode) Path() string {
|
func (node *FileNode) Path() string {
|
||||||
|
if node.path == "" {
|
||||||
path := []string{}
|
path := []string{}
|
||||||
curNode := node
|
curNode := node
|
||||||
for {
|
for {
|
||||||
@ -222,7 +272,9 @@ func (node *FileNode) Path() string {
|
|||||||
path = append([]string{name}, path...)
|
path = append([]string{name}, path...)
|
||||||
curNode = curNode.Parent
|
curNode = curNode.Parent
|
||||||
}
|
}
|
||||||
return "/" + strings.Join(path, "/")
|
node.path = "/" + strings.Join(path, "/")
|
||||||
|
}
|
||||||
|
return node.path
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *FileNode) IsLeaf() bool {
|
func (node *FileNode) IsLeaf() bool {
|
||||||
|
@ -2,8 +2,8 @@ package filetree
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"github.com/satori/go.uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -21,6 +21,7 @@ type FileTree struct {
|
|||||||
Root *FileNode
|
Root *FileNode
|
||||||
Size int
|
Size int
|
||||||
Name string
|
Name string
|
||||||
|
Id uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileTree() (tree *FileTree) {
|
func NewFileTree() (tree *FileTree) {
|
||||||
@ -29,63 +30,12 @@ func NewFileTree() (tree *FileTree) {
|
|||||||
tree.Root = new(FileNode)
|
tree.Root = new(FileNode)
|
||||||
tree.Root.Tree = tree
|
tree.Root.Tree = tree
|
||||||
tree.Root.Children = make(map[string]*FileNode)
|
tree.Root.Children = make(map[string]*FileNode)
|
||||||
|
tree.Id = uuid.Must(uuid.NewV4())
|
||||||
return tree
|
return tree
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree *FileTree) String(showAttributes bool) string {
|
func (tree *FileTree) String(showAttributes bool) string {
|
||||||
var renderTreeLine func(string, []bool, bool, bool) string
|
return tree.Root.renderStringTree([]bool{}, showAttributes, 0)
|
||||||
var walkTree func(*FileNode, []bool, int) string
|
|
||||||
|
|
||||||
renderTreeLine = func(nodeText string, spaces []bool, last bool, collapsed bool) string {
|
|
||||||
var otherBranches string
|
|
||||||
for _, space := range spaces {
|
|
||||||
if space {
|
|
||||||
otherBranches += noBranchSpace
|
|
||||||
} else {
|
|
||||||
otherBranches += branchSpace
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
thisBranch := middleItem
|
|
||||||
if last {
|
|
||||||
thisBranch = lastItem
|
|
||||||
}
|
|
||||||
|
|
||||||
collapsedIndicator := uncollapsedItem
|
|
||||||
if collapsed {
|
|
||||||
collapsedIndicator = collapsedItem
|
|
||||||
}
|
|
||||||
|
|
||||||
return otherBranches + thisBranch + collapsedIndicator + nodeText + newLine
|
|
||||||
}
|
|
||||||
|
|
||||||
walkTree = func(node *FileNode, spaces []bool, depth int) string {
|
|
||||||
var result string
|
|
||||||
var keys []string
|
|
||||||
for key := range node.Children {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for idx, name := range keys {
|
|
||||||
child := node.Children[name]
|
|
||||||
if child.Data.ViewInfo.Hidden {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
last := idx == (len(node.Children) - 1)
|
|
||||||
showCollapsed := child.Data.ViewInfo.Collapsed && len(child.Children) > 0
|
|
||||||
if showAttributes {
|
|
||||||
result += child.MetadataString() + " "
|
|
||||||
}
|
|
||||||
result += renderTreeLine(child.String(), spaces, last, showCollapsed)
|
|
||||||
if len(child.Children) > 0 && !child.Data.ViewInfo.Collapsed {
|
|
||||||
spacesChild := append(spaces, last)
|
|
||||||
result += walkTree(child, spacesChild, depth+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return walkTree(tree.Root, []bool{}, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree *FileTree) Copy() *FileTree {
|
func (tree *FileTree) Copy() *FileTree {
|
||||||
@ -214,11 +164,37 @@ func (tree *FileTree) MarkRemoved(path string) error {
|
|||||||
return node.AssignDiffType(Removed)
|
return node.AssignDiffType(Removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// memoize StackRange for performance
|
||||||
|
type stackRangeCacheKey struct {
|
||||||
|
// Ids mapset.Set
|
||||||
|
start, stop int
|
||||||
|
}
|
||||||
|
|
||||||
|
var stackRangeCache = make(map[stackRangeCacheKey]*FileTree)
|
||||||
|
|
||||||
func StackRange(trees []*FileTree, start, stop int) *FileTree {
|
func StackRange(trees []*FileTree, start, stop int) *FileTree {
|
||||||
|
|
||||||
|
// var ids []interface{}
|
||||||
|
//
|
||||||
|
// for _, tree := range trees {
|
||||||
|
// ids = append(ids, tree.Id)
|
||||||
|
// }
|
||||||
|
//mapset.NewSetFromSlice(ids)
|
||||||
|
// key := stackRangeCacheKey{start, stop}
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// cachedResult, ok := stackRangeCache[key]
|
||||||
|
// if ok {
|
||||||
|
// return cachedResult
|
||||||
|
// }
|
||||||
|
|
||||||
tree := trees[0].Copy()
|
tree := trees[0].Copy()
|
||||||
for idx := start; idx <= stop; idx++ {
|
for idx := start; idx <= stop; idx++ {
|
||||||
tree.Stack(trees[idx])
|
tree.Stack(trees[idx])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stackRangeCache[key] = tree
|
||||||
|
|
||||||
return tree
|
return tree
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,12 +12,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/image"
|
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
humanize "github.com/dustin/go-humanize"
|
|
||||||
"github.com/wagoodman/dive/filetree"
|
"github.com/wagoodman/dive/filetree"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -43,42 +40,12 @@ func NewManifest(reader *tar.Reader, header *tar.Header) ImageManifest {
|
|||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
var m []ImageManifest
|
var manifest []ImageManifest
|
||||||
err = json.Unmarshal(manifestBytes, &m)
|
err = json.Unmarshal(manifestBytes, &manifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return m[0]
|
return manifest[0]
|
||||||
}
|
|
||||||
|
|
||||||
type Layer struct {
|
|
||||||
TarPath string
|
|
||||||
History image.HistoryResponseItem
|
|
||||||
Index int
|
|
||||||
Tree *filetree.FileTree
|
|
||||||
RefTrees []*filetree.FileTree
|
|
||||||
}
|
|
||||||
|
|
||||||
func (layer *Layer) Id() string {
|
|
||||||
rangeBound := 25
|
|
||||||
if length := len(layer.History.ID); length < 25 {
|
|
||||||
rangeBound = length
|
|
||||||
}
|
|
||||||
id := layer.History.ID[0:rangeBound]
|
|
||||||
if len(layer.History.Tags) > 0 {
|
|
||||||
id = "[" + strings.Join(layer.History.Tags, ",") + "]"
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (layer *Layer) String() string {
|
|
||||||
|
|
||||||
return fmt.Sprintf(LayerFormat,
|
|
||||||
layer.Id(),
|
|
||||||
strconv.Itoa(int(100.0*filetree.EfficiencyScore(layer.RefTrees[:layer.Index+1]))) + "%",
|
|
||||||
//"100%",
|
|
||||||
humanize.Bytes(uint64(layer.History.Size)),
|
|
||||||
strings.TrimPrefix(layer.History.CreatedBy, "/bin/sh -c "))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
|
func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
|
||||||
@ -87,10 +54,15 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
|
|||||||
var trees []*filetree.FileTree = make([]*filetree.FileTree, 0)
|
var trees []*filetree.FileTree = make([]*filetree.FileTree, 0)
|
||||||
|
|
||||||
// save this image to disk temporarily to get the content info
|
// save this image to disk temporarily to get the content info
|
||||||
imageTarPath, tmpDir := saveImage(imageID)
|
fmt.Println("Fetching image...")
|
||||||
defer os.RemoveAll(tmpDir)
|
// imageTarPath, tmpDir := saveImage(imageID)
|
||||||
|
imageTarPath := "/tmp/dive031537738/image.tar"
|
||||||
|
// tmpDir := "/tmp/dive031537738"
|
||||||
|
// fmt.Println(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...")
|
||||||
tarFile, err := os.Open(imageTarPath)
|
tarFile, err := os.Open(imageTarPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@ -135,13 +107,14 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// build the content tree
|
// build the content tree
|
||||||
|
fmt.Println("Building tree...")
|
||||||
for _, treeName := range manifest.LayerTarPaths {
|
for _, treeName := range manifest.LayerTarPaths {
|
||||||
trees = append(trees, layerMap[treeName])
|
trees = append(trees, layerMap[treeName])
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the history of this image
|
// get the history of this image
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
dockerClient, err := client.NewEnvClient()
|
dockerClient, err := client.NewClientWithOpts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -149,13 +122,14 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
|
|||||||
history, err := dockerClient.ImageHistory(ctx, imageID)
|
history, err := dockerClient.ImageHistory(ctx, imageID)
|
||||||
|
|
||||||
// build the layers array
|
// build the layers array
|
||||||
layers := make([]*Layer, len(history)-1)
|
layers := make([]*Layer, len(trees))
|
||||||
for idx := 0; idx < len(layers); idx++ {
|
for idx := 0; idx < len(trees); idx++ {
|
||||||
layers[idx] = new(Layer)
|
layers[idx] = &Layer{
|
||||||
layers[idx].History = history[idx]
|
History: history[idx],
|
||||||
layers[idx].Index = idx
|
Index: idx,
|
||||||
layers[idx].Tree = trees[idx]
|
Tree: trees[idx],
|
||||||
layers[idx].RefTrees = trees
|
RefTrees: trees,
|
||||||
|
}
|
||||||
if len(manifest.LayerTarPaths) > idx {
|
if len(manifest.LayerTarPaths) > idx {
|
||||||
layers[idx].TarPath = manifest.LayerTarPaths[idx]
|
layers[idx].TarPath = manifest.LayerTarPaths[idx]
|
||||||
}
|
}
|
||||||
@ -166,7 +140,7 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
|
|||||||
|
|
||||||
func saveImage(imageID string) (string, string) {
|
func saveImage(imageID string) (string, string) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
dockerClient, err := client.NewEnvClient()
|
dockerClient, err := client.NewClientWithOpts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -175,7 +149,7 @@ func saveImage(imageID string) (string, string) {
|
|||||||
check(err)
|
check(err)
|
||||||
defer readCloser.Close()
|
defer readCloser.Close()
|
||||||
|
|
||||||
tmpDir, err := ioutil.TempDir("", "docker-image-explorer")
|
tmpDir, err := ioutil.TempDir("", "dive")
|
||||||
check(err)
|
check(err)
|
||||||
|
|
||||||
imageTarPath := filepath.Join(tmpDir, "image.tar")
|
imageTarPath := filepath.Join(tmpDir, "image.tar")
|
||||||
|
44
image/layer.go
Normal file
44
image/layer.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/wagoodman/dive/filetree"
|
||||||
|
"strings"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Layer struct {
|
||||||
|
TarPath string
|
||||||
|
History image.HistoryResponseItem
|
||||||
|
Index int
|
||||||
|
Tree *filetree.FileTree
|
||||||
|
RefTrees []*filetree.FileTree
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layer *Layer) Id() string {
|
||||||
|
rangeBound := 25
|
||||||
|
if length := len(layer.History.ID); length < 25 {
|
||||||
|
rangeBound = length
|
||||||
|
}
|
||||||
|
id := layer.History.ID[0:rangeBound]
|
||||||
|
|
||||||
|
// show the tagged image as the last layer
|
||||||
|
// if len(layer.History.Tags) > 0 {
|
||||||
|
// id = "[" + strings.Join(layer.History.Tags, ",") + "]"
|
||||||
|
// }
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layer *Layer) String() string {
|
||||||
|
|
||||||
|
return fmt.Sprintf(LayerFormat,
|
||||||
|
layer.Id(),
|
||||||
|
strconv.Itoa(int(100.0*filetree.EfficiencyScore(layer.RefTrees[:layer.Index+1]))) + "%",
|
||||||
|
//"100%",
|
||||||
|
humanize.Bytes(uint64(layer.History.Size)),
|
||||||
|
strings.TrimPrefix(layer.History.CreatedBy, "/bin/sh -c "))
|
||||||
|
}
|
||||||
|
|
5
main.go
5
main.go
@ -20,8 +20,11 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/wagoodman/dive/cmd"
|
import (
|
||||||
|
"github.com/wagoodman/dive/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,8 +144,15 @@ func (view *LayerView) Render() error {
|
|||||||
|
|
||||||
layerStr := layer.String()
|
layerStr := layer.String()
|
||||||
if idx == 0 {
|
if idx == 0 {
|
||||||
|
var layerId string
|
||||||
|
if len(layer.History.ID) >= 25 {
|
||||||
|
layerId = layer.History.ID[0:25]
|
||||||
|
} else {
|
||||||
|
layerId = fmt.Sprintf("%-25s", layer.History.ID)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: add size
|
// TODO: add size
|
||||||
layerStr = fmt.Sprintf(image.LayerFormat, layer.History.ID[0:25], "", "", "FROM "+layer.Id())
|
layerStr = fmt.Sprintf(image.LayerFormat, layerId, "", "", "FROM "+layer.Id())
|
||||||
}
|
}
|
||||||
|
|
||||||
compareBar := view.renderCompareBar(idx)
|
compareBar := view.renderCompareBar(idx)
|
||||||
@ -170,6 +177,7 @@ func (view *LayerView) CursorDown() error {
|
|||||||
view.LayerIndex++
|
view.LayerIndex++
|
||||||
Views.Tree.setTreeByLayer(view.getCompareIndexes())
|
Views.Tree.setTreeByLayer(view.getCompareIndexes())
|
||||||
view.Render()
|
view.Render()
|
||||||
|
// debugPrint(fmt.Sprintf("%d",len(filetree.Cache)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -182,11 +190,21 @@ func (view *LayerView) CursorUp() error {
|
|||||||
view.LayerIndex--
|
view.LayerIndex--
|
||||||
Views.Tree.setTreeByLayer(view.getCompareIndexes())
|
Views.Tree.setTreeByLayer(view.getCompareIndexes())
|
||||||
view.Render()
|
view.Render()
|
||||||
|
// debugPrint(fmt.Sprintf("%d",len(filetree.Cache)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
||||||
func (view *LayerView) KeyHelp() string {
|
func (view *LayerView) KeyHelp() string {
|
||||||
return renderStatusOption("^L","Layer changes", view.CompareMode == CompareLayer) +
|
return renderStatusOption("^L","Layer changes", view.CompareMode == CompareLayer) +
|
||||||
renderStatusOption("^A","All changes", view.CompareMode == CompareAll)
|
renderStatusOption("^A","All changes", view.CompareMode == CompareAll)
|
||||||
|
27
ui/ui.go
27
ui/ui.go
@ -9,9 +9,13 @@ import (
|
|||||||
"github.com/wagoodman/dive/filetree"
|
"github.com/wagoodman/dive/filetree"
|
||||||
"github.com/wagoodman/dive/image"
|
"github.com/wagoodman/dive/image"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"runtime/pprof"
|
||||||
)
|
)
|
||||||
|
|
||||||
const debug = false
|
const debug = true
|
||||||
|
const profile = false
|
||||||
|
|
||||||
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 {
|
||||||
@ -120,7 +124,18 @@ func CursorUp(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
||||||
|
pprof.StopCPUProfile()
|
||||||
|
runtime.GC() // get up-to-date statistics
|
||||||
|
pprof.WriteHeapProfile(memoryProfilePath)
|
||||||
|
memoryProfilePath.Close()
|
||||||
|
cpuProfilePath.Close()
|
||||||
|
}
|
||||||
return gocui.ErrQuit
|
return gocui.ErrQuit
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,6 +261,7 @@ func renderStatusOption(control, title string, selected bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
Formatting.Header = color.New(color.Bold).SprintFunc()
|
Formatting.Header = color.New(color.Bold).SprintFunc()
|
||||||
Formatting.StatusSelected = color.New(color.BgMagenta, color.FgWhite).SprintFunc()
|
Formatting.StatusSelected = color.New(color.BgMagenta, color.FgWhite).SprintFunc()
|
||||||
@ -279,10 +295,19 @@ func Run(layers []*image.Layer, refTrees []*filetree.FileTree) {
|
|||||||
//g.Mouse = true
|
//g.Mouse = true
|
||||||
g.SetManagerFunc(layout)
|
g.SetManagerFunc(layout)
|
||||||
|
|
||||||
|
// let the default position of the cursor be the last layer
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if profile {
|
||||||
|
os.Create("cpu.pprof")
|
||||||
|
os.Create("mem.pprof")
|
||||||
|
pprof.StartCPUProfile(cpuProfilePath)
|
||||||
|
}
|
||||||
|
|
||||||
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
|
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
|
||||||
log.Panicln(err)
|
log.Panicln(err)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user