mirror of
https://github.com/wagoodman/dive
synced 2024-06-12 09:02:15 +02:00
Merge branch 'master' into config/toggle-unchanged-files
# Conflicts: # README.md
This commit is contained in:
commit
48df08f117
|
@ -12,3 +12,4 @@ ADD .scripts/ /root/.data/
|
|||
RUN cp /root/saved.txt /tmp/saved.again1.txt
|
||||
RUN cp /root/saved.txt /root/.data/saved.again2.txt
|
||||
RUN chmod +x /root/saved.txt
|
||||
RUN chmod 421 /root
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
/.git
|
||||
/.data
|
||||
/.cover
|
||||
/dist
|
||||
/ui
|
||||
/utils
|
||||
/image
|
||||
/cmd
|
||||
/build
|
||||
coverage.txt
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -20,3 +20,4 @@
|
|||
*.log
|
||||
/dist
|
||||
.cover
|
||||
coverage.txt
|
||||
|
|
26
.gitlab-ci.yml
Normal file
26
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,26 @@
|
|||
image: golang:1.12.5
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .cache
|
||||
|
||||
variables:
|
||||
GOPATH: $CI_PROJECT_DIR/.cache
|
||||
|
||||
stages:
|
||||
- setup
|
||||
- validation
|
||||
|
||||
setup:
|
||||
stage: setup
|
||||
script:
|
||||
- mkdir -p .cache
|
||||
- go get ./...
|
||||
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
|
||||
|
||||
validation:
|
||||
stage: validation
|
||||
before_script:
|
||||
- export PATH="$GOPATH/bin:$PATH"
|
||||
script:
|
||||
- make ci
|
30
.travis.yml
30
.travis.yml
|
@ -1,30 +0,0 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- '1.9.x'
|
||||
- '1.10.x'
|
||||
- '1.11.x'
|
||||
- 'master'
|
||||
|
||||
# Skip the install step. Don't `go get` dependencies. Only build with the
|
||||
# code in vendor/
|
||||
install: true
|
||||
|
||||
matrix:
|
||||
# It's ok if our code fails on unstable development versions of Go.
|
||||
allow_failures:
|
||||
- go: master
|
||||
# Don't wait for tip tests to finish. Mark the test run green if the
|
||||
# tests pass on the stable versions of Go.
|
||||
fast_finish: true
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
before_script:
|
||||
- go get -t ./...
|
||||
|
||||
# Note: scripts always run to completion
|
||||
script:
|
||||
- make validate
|
||||
- make test
|
19
Makefile
19
Makefile
|
@ -14,22 +14,26 @@ run-large: build
|
|||
build:
|
||||
go build -o build/$(BIN)
|
||||
|
||||
release: test validate
|
||||
release: test-coverage validate
|
||||
./.scripts/tag.sh
|
||||
goreleaser --rm-dist
|
||||
|
||||
install:
|
||||
go install ./...
|
||||
|
||||
test: build
|
||||
go test -cover -v ./...
|
||||
ci: clean validate test-coverage
|
||||
|
||||
coverage: build
|
||||
./.scripts/test.sh
|
||||
test: build
|
||||
go test -cover -v -race ./...
|
||||
|
||||
test-coverage: build
|
||||
./.scripts/test-coverage.sh
|
||||
|
||||
validate:
|
||||
@! gofmt -s -d -l . 2>&1 | grep -vE '^\.git/'
|
||||
grep -R 'const allowTestDataCapture = false' ui/
|
||||
go vet ./...
|
||||
@! gofmt -s -l . 2>&1 | grep -vE '^\.git/' | grep -vE '^\.cache/'
|
||||
golangci-lint run
|
||||
|
||||
lint: build
|
||||
golint -set_exit_status $$(go list ./...)
|
||||
|
@ -39,7 +43,6 @@ generate-test-data:
|
|||
|
||||
clean:
|
||||
rm -rf build
|
||||
rm -rf vendor
|
||||
go clean
|
||||
|
||||
.PHONY: build install test lint clean release validate generate-test-data
|
||||
.PHONY: build install test lint clean release validate generate-test-data test-coverage ci
|
||||
|
|
31
README.md
31
README.md
|
@ -1,6 +1,6 @@
|
|||
# dive
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/wagoodman/dive)](https://goreportcard.com/report/github.com/wagoodman/dive)
|
||||
[![Pipeline Status](https://api.travis-ci.org/wagoodman/dive.svg?branch=master)](https://travis-ci.org/wagoodman/dive)
|
||||
[![Pipeline Status](https://gitlab.com/wagoodman/dive/badges/master/pipeline.svg)](https://gitlab.com/wagoodman/dive/pipelines?scope=branches&page=1)
|
||||
|
||||
**A tool for exploring a docker image, layer contents, and discovering ways to shrink your Docker image size.**
|
||||
|
||||
|
@ -74,14 +74,14 @@ Analyze and image and get a pass/fail result based on the image efficiency and w
|
|||
|
||||
**Ubuntu/Debian**
|
||||
```bash
|
||||
wget https://github.com/wagoodman/dive/releases/download/v0.6.0/dive_0.6.0_linux_amd64.deb
|
||||
sudo apt install ./dive_0.6.0_linux_amd64.deb
|
||||
wget https://github.com/wagoodman/dive/releases/download/v0.7.2/dive_0.7.2_linux_amd64.deb
|
||||
sudo apt install ./dive_0.7.2_linux_amd64.deb
|
||||
```
|
||||
|
||||
**RHEL/Centos**
|
||||
```bash
|
||||
curl -OL https://github.com/wagoodman/dive/releases/download/v0.6.0/dive_0.6.0_linux_amd64.rpm
|
||||
rpm -i dive_0.6.0_linux_amd64.rpm
|
||||
curl -OL https://github.com/wagoodman/dive/releases/download/v0.7.2/dive_0.7.2_linux_amd64.rpm
|
||||
rpm -i dive_0.7.2_linux_amd64.rpm
|
||||
```
|
||||
|
||||
**Arch Linux**
|
||||
|
@ -100,11 +100,11 @@ The above example assumes [`yay`](https://aur.archlinux.org/packages/yay/) as th
|
|||
brew tap wagoodman/dive
|
||||
brew install dive
|
||||
```
|
||||
or download the latest Darwin build from the [releases page](https://github.com/wagoodman/dive/releases/download/v0.6.0/dive_0.6.0_darwin_amd64.tar.gz).
|
||||
or download the latest Darwin build from the [releases page](https://github.com/wagoodman/dive/releases/download/v0.7.2/dive_0.7.2_darwin_amd64.tar.gz).
|
||||
|
||||
**Windows**
|
||||
|
||||
Download the [latest release](https://github.com/wagoodman/dive/releases/download/v0.6.0/dive_0.6.0_windows_amd64.zip).
|
||||
Download the [latest release](https://github.com/wagoodman/dive/releases/download/v0.7.2/dive_0.7.2_windows_amd64.zip).
|
||||
|
||||
**Go tools**
|
||||
Requires Go version 1.9 or higher.
|
||||
|
@ -176,15 +176,19 @@ You can override the CI config path with the `--ci-config` option.
|
|||
Key Binding | Description
|
||||
-------------------------------------------|---------------------------------------------------------
|
||||
<kbd>Ctrl + C</kbd> | Exit
|
||||
<kbd>Tab</kbd> or <kbd>Ctrl + Space</kbd> | Switch between the layer and filetree views
|
||||
<kbd>Tab</kbd> | Switch between the layer and filetree views
|
||||
<kbd>Ctrl + F</kbd> | Filter files
|
||||
<kbd>PageUp</kbd> | Scroll up a page
|
||||
<kbd>PageDown</kbd> | Scroll down a page
|
||||
<kbd>Ctrl + A</kbd> | Layer view: see aggregated image modifications
|
||||
<kbd>Ctrl + L</kbd> | Layer view: see current layer modifications
|
||||
<kbd>Space</kbd> | Filetree view: collapse/uncollapse a directory
|
||||
<kbd>Ctrl + Space</kbd> | Filetree view: collapse/uncollapse all directories
|
||||
<kbd>Ctrl + A</kbd> | Filetree view: show/hide added files
|
||||
<kbd>Ctrl + R</kbd> | Filetree view: show/hide removed files
|
||||
<kbd>Ctrl + M</kbd> | Filetree view: show/hide modified files
|
||||
<kbd>Ctrl + U</kbd> | Filetree view: show/hide unchanged files
|
||||
<kbd>Ctrl + U</kbd> | Filetree view: show/hide unmodified files
|
||||
<kbd>Ctrl + B</kbd> | Filetree view: show/hide file attributes
|
||||
<kbd>PageUp</kbd> | Filetree view: scroll up a page
|
||||
<kbd>PageDown</kbd> | Filetree view: scroll down a page
|
||||
|
||||
|
@ -202,7 +206,7 @@ log:
|
|||
keybinding:
|
||||
# Global bindings
|
||||
quit: ctrl+c
|
||||
toggle-view: tab, ctrl+space
|
||||
toggle-view: tab
|
||||
filter-files: ctrl+f, ctrl+slash
|
||||
|
||||
# Layer view specific bindings
|
||||
|
@ -211,10 +215,12 @@ keybinding:
|
|||
|
||||
# File view specific bindings
|
||||
toggle-collapse-dir: space
|
||||
toggle-collapse-all-dir: ctrl+space
|
||||
toggle-added-files: ctrl+a
|
||||
toggle-removed-files: ctrl+r
|
||||
toggle-modified-files: ctrl+m
|
||||
toggle-unchanged-files: ctrl+u
|
||||
toggle-unmodified-files: ctrl+u
|
||||
toggle-filetree-attributes: ctrl+b
|
||||
page-up: pgup
|
||||
page-down: pgdn
|
||||
|
||||
|
@ -232,6 +238,9 @@ filetree:
|
|||
|
||||
# The percentage of screen width the filetree should take on the screen (must be >0 and <1)
|
||||
pane-width: 0.5
|
||||
|
||||
# Show the file attributes next to the filetree
|
||||
show-attributes: true
|
||||
|
||||
layer:
|
||||
# Enable showing all changes from this layer and ever previous layer
|
||||
|
|
|
@ -19,17 +19,19 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
fmt.Println("No image argument given")
|
||||
cmd.Help()
|
||||
_ = cmd.Help()
|
||||
utils.Exit(1)
|
||||
}
|
||||
|
||||
userImage := args[0]
|
||||
if userImage == "" {
|
||||
fmt.Println("No image argument given")
|
||||
cmd.Help()
|
||||
_ = cmd.Help()
|
||||
utils.Exit(1)
|
||||
}
|
||||
|
||||
initLogging()
|
||||
|
||||
runtime.Run(runtime.Options{
|
||||
ImageId: userImage,
|
||||
ExportFile: exportFile,
|
||||
|
|
|
@ -22,6 +22,8 @@ func init() {
|
|||
func doBuildCmd(cmd *cobra.Command, args []string) {
|
||||
defer utils.Cleanup()
|
||||
|
||||
initLogging()
|
||||
|
||||
runtime.Run(runtime.Options{
|
||||
BuildArgs: args,
|
||||
ExportFile: exportFile,
|
||||
|
|
21
cmd/root.go
21
cmd/root.go
|
@ -7,7 +7,6 @@ import (
|
|||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/k0kubun/go-ansi"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -16,8 +15,6 @@ import (
|
|||
"github.com/wagoodman/dive/utils"
|
||||
)
|
||||
|
||||
const pathSep = string(os.PathSeparator)
|
||||
|
||||
var cfgFile string
|
||||
var exportFile string
|
||||
var ciConfigFile string
|
||||
|
@ -42,12 +39,9 @@ func Execute() {
|
|||
}
|
||||
|
||||
func init() {
|
||||
ansi.CursorHide()
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
cobra.OnInitialize(initLogging)
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml, ~/.config/dive.yaml, or $XDG_CONFIG_HOME/dive.yaml)")
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml, ~/.config/dive/*.yaml, or $XDG_CONFIG_HOME/dive.yaml)")
|
||||
rootCmd.PersistentFlags().BoolP("version", "v", false, "display version number")
|
||||
|
||||
rootCmd.Flags().StringVarP(&exportFile, "json", "j", "", "Skip the interactive TUI and write the layer analysis statistics to a given file.")
|
||||
|
@ -64,13 +58,15 @@ func initConfig() {
|
|||
viper.SetDefault("log.enabled", true)
|
||||
// keybindings: status view / global
|
||||
viper.SetDefault("keybinding.quit", "ctrl+c")
|
||||
viper.SetDefault("keybinding.toggle-view", "tab, ctrl+space")
|
||||
viper.SetDefault("keybinding.toggle-view", "tab")
|
||||
viper.SetDefault("keybinding.filter-files", "ctrl+f, ctrl+slash")
|
||||
// keybindings: layer view
|
||||
viper.SetDefault("keybinding.compare-all", "ctrl+a")
|
||||
viper.SetDefault("keybinding.compare-layer", "ctrl+l")
|
||||
// keybindings: filetree view
|
||||
viper.SetDefault("keybinding.toggle-collapse-dir", "space")
|
||||
viper.SetDefault("keybinding.toggle-collapse-all-dir", "ctrl+space")
|
||||
viper.SetDefault("keybinding.toggle-filetree-attributes", "ctrl+b")
|
||||
viper.SetDefault("keybinding.toggle-added-files", "ctrl+a")
|
||||
viper.SetDefault("keybinding.toggle-removed-files", "ctrl+r")
|
||||
viper.SetDefault("keybinding.toggle-modified-files", "ctrl+m")
|
||||
|
@ -84,6 +80,7 @@ func initConfig() {
|
|||
|
||||
viper.SetDefault("filetree.collapse-dir", false)
|
||||
viper.SetDefault("filetree.pane-width", 0.5)
|
||||
viper.SetDefault("filetree.show-attributes", true)
|
||||
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
|
@ -142,7 +139,7 @@ func getCfgFile(fromFlag string) string {
|
|||
xdgHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
xdgDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||
xdgPaths := append([]string{xdgHome}, strings.Split(xdgDirs, ":")...)
|
||||
allDirs := append(xdgPaths, home+pathSep+".config")
|
||||
allDirs := append(xdgPaths, path.Join(home, ".config"))
|
||||
|
||||
for _, val := range allDirs {
|
||||
file := findInPath(val)
|
||||
|
@ -150,13 +147,13 @@ func getCfgFile(fromFlag string) string {
|
|||
return file
|
||||
}
|
||||
}
|
||||
return home + pathSep + "dive.yaml"
|
||||
return path.Join(home, ".dive.yaml")
|
||||
}
|
||||
|
||||
// findInPath returns first "*.yaml" file in path's subdirectory "dive"
|
||||
// if not found returns empty string
|
||||
func findInPath(pathTo string) string {
|
||||
directory := pathTo + pathSep + "dive"
|
||||
directory := path.Join(pathTo, "dive")
|
||||
files, err := ioutil.ReadDir(directory)
|
||||
if err != nil {
|
||||
return ""
|
||||
|
@ -165,7 +162,7 @@ func findInPath(pathTo string) string {
|
|||
for _, file := range files {
|
||||
filename := file.Name()
|
||||
if path.Ext(filename) == ".yaml" {
|
||||
return directory + pathSep + filename
|
||||
return path.Join(directory, filename)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type TreeCacheKey struct {
|
||||
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int
|
||||
}
|
||||
|
@ -13,8 +17,6 @@ func (cache *TreeCache) Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTr
|
|||
key := TreeCacheKey{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}
|
||||
if value, exists := cache.cache[key]; exists {
|
||||
return value
|
||||
} else {
|
||||
|
||||
}
|
||||
value := cache.buildTree(key)
|
||||
cache.cache[key] = value
|
||||
|
@ -23,9 +25,11 @@ func (cache *TreeCache) Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTr
|
|||
|
||||
func (cache *TreeCache) buildTree(key TreeCacheKey) *FileTree {
|
||||
newTree := StackTreeRange(cache.refTrees, key.bottomTreeStart, key.bottomTreeStop)
|
||||
|
||||
for idx := key.topTreeStart; idx <= key.topTreeStop; idx++ {
|
||||
newTree.Compare(cache.refTrees[idx])
|
||||
err := newTree.CompareAndMark(cache.refTrees[idx])
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to build tree: %+v", err)
|
||||
}
|
||||
}
|
||||
return newTree
|
||||
}
|
||||
|
|
|
@ -64,7 +64,10 @@ func getHashFromReader(reader io.Reader) uint64 {
|
|||
break
|
||||
}
|
||||
|
||||
h.Write(buf[:n])
|
||||
_, err = h.Write(buf[:n])
|
||||
if err != nil {
|
||||
logrus.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return h.Sum64()
|
||||
|
|
|
@ -56,7 +56,10 @@ func Efficiency(trees []*FileTree) (float64, EfficiencySlice) {
|
|||
if err != nil {
|
||||
logrus.Debug(fmt.Sprintf("CurrentTree: %d : %s", currentTree, err))
|
||||
} else if previousTreeNode.Data.FileInfo.IsDir {
|
||||
previousTreeNode.VisitDepthChildFirst(sizer, nil)
|
||||
err = previousTreeNode.VisitDepthChildFirst(sizer, nil)
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to propagate whiteout dir: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -80,7 +83,10 @@ func Efficiency(trees []*FileTree) (float64, EfficiencySlice) {
|
|||
}
|
||||
for idx, tree := range trees {
|
||||
currentTree = idx
|
||||
tree.VisitDepthChildFirst(visitor, visitEvaluator)
|
||||
err := tree.VisitDepthChildFirst(visitor, visitEvaluator)
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to propagate ref tree: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the score
|
||||
|
|
|
@ -4,19 +4,31 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func checkError(t *testing.T, err error, message string) {
|
||||
if err != nil {
|
||||
t.Errorf(message+": %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEfficency(t *testing.T) {
|
||||
trees := make([]*FileTree, 3)
|
||||
for idx := range trees {
|
||||
trees[idx] = NewFileTree()
|
||||
}
|
||||
|
||||
trees[0].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 2000})
|
||||
trees[0].AddPath("/etc/nginx/public", FileInfo{Size: 3000})
|
||||
_, _, err := trees[0].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 2000})
|
||||
checkError(t, err, "could not setup test")
|
||||
|
||||
trees[1].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 5000})
|
||||
trees[1].AddPath("/etc/athing", FileInfo{Size: 10000})
|
||||
_, _, err = trees[0].AddPath("/etc/nginx/public", FileInfo{Size: 3000})
|
||||
checkError(t, err, "could not setup test")
|
||||
|
||||
trees[2].AddPath("/etc/.wh.nginx", *BlankFileChangeInfo("/etc/.wh.nginx"))
|
||||
_, _, err = trees[1].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 5000})
|
||||
checkError(t, err, "could not setup test")
|
||||
_, _, err = trees[1].AddPath("/etc/athing", FileInfo{Size: 10000})
|
||||
checkError(t, err, "could not setup test")
|
||||
|
||||
_, _, err = trees[2].AddPath("/etc/.wh.nginx", *BlankFileChangeInfo("/etc/.wh.nginx"))
|
||||
checkError(t, err, "could not setup test")
|
||||
|
||||
var expectedScore = 0.75
|
||||
var expectedMatches = EfficiencySlice{
|
||||
|
@ -50,7 +62,8 @@ func TestEfficency_ScratchImage(t *testing.T) {
|
|||
trees[idx] = NewFileTree()
|
||||
}
|
||||
|
||||
trees[0].AddPath("/nothing", FileInfo{Size: 0})
|
||||
_, _, err := trees[0].AddPath("/nothing", FileInfo{Size: 0})
|
||||
checkError(t, err, "could not setup test")
|
||||
|
||||
var expectedScore = 1.0
|
||||
var expectedMatches = EfficiencySlice{}
|
||||
|
|
|
@ -3,6 +3,7 @@ package filetree
|
|||
import (
|
||||
"archive/tar"
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
|
@ -99,7 +100,10 @@ func (node *FileNode) Remove() error {
|
|||
return fmt.Errorf("cannot remove the tree root")
|
||||
}
|
||||
for _, child := range node.Children {
|
||||
child.Remove()
|
||||
err := child.Remove()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
delete(node.Parent.Children, node.Name)
|
||||
node.Tree.Size--
|
||||
|
@ -149,7 +153,10 @@ func (node *FileNode) MetadataString() string {
|
|||
return nil
|
||||
}
|
||||
|
||||
node.VisitDepthChildFirst(sizer, nil)
|
||||
err := node.VisitDepthChildFirst(sizer, nil)
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to propagate node for metadata: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
size := humanize.Bytes(uint64(sizeBytes))
|
||||
|
@ -258,7 +265,6 @@ func (node *FileNode) deriveDiffType(diffType DiffType) error {
|
|||
myDiffType := diffType
|
||||
for _, v := range node.Children {
|
||||
myDiffType = myDiffType.merge(v.Data.DiffType)
|
||||
|
||||
}
|
||||
|
||||
return node.AssignDiffType(myDiffType)
|
||||
|
@ -303,7 +309,6 @@ func (node *FileNode) compare(other *FileNode) DiffType {
|
|||
if node.Name != other.Name {
|
||||
panic("comparing mismatched nodes")
|
||||
}
|
||||
// TODO: fails on nil
|
||||
|
||||
return node.Data.FileInfo.Compare(other.Data.FileInfo)
|
||||
}
|
||||
|
|
|
@ -56,7 +56,8 @@ func TestRemoveChild(t *testing.T) {
|
|||
forth := two.AddChild("forth", FileInfo{})
|
||||
two.AddChild("fifth", FileInfo{})
|
||||
|
||||
forth.Remove()
|
||||
err := forth.Remove()
|
||||
checkError(t, err, "unable to setup test")
|
||||
|
||||
expected, actual = 4, tree.Size
|
||||
if expected != actual {
|
||||
|
@ -67,7 +68,8 @@ func TestRemoveChild(t *testing.T) {
|
|||
t.Errorf("Expected 'forth' node to be deleted.")
|
||||
}
|
||||
|
||||
two.Remove()
|
||||
err = two.Remove()
|
||||
checkError(t, err, "unable to setup test")
|
||||
|
||||
expected, actual = 2, tree.Size
|
||||
if expected != actual {
|
||||
|
@ -121,7 +123,8 @@ func TestDiffTypeFromAddedChildren(t *testing.T) {
|
|||
node, _, _ = tree.AddPath("/usr/bin2", *BlankFileChangeInfo("/usr/bin2"))
|
||||
node.Data.DiffType = Removed
|
||||
|
||||
tree.Root.Children["usr"].deriveDiffType(Unchanged)
|
||||
err := tree.Root.Children["usr"].deriveDiffType(Unchanged)
|
||||
checkError(t, err, "unable to setup test")
|
||||
|
||||
if tree.Root.Children["usr"].Data.DiffType != Changed {
|
||||
t.Errorf("Expected Changed but got %v", tree.Root.Children["usr"].Data.DiffType)
|
||||
|
@ -129,17 +132,18 @@ func TestDiffTypeFromAddedChildren(t *testing.T) {
|
|||
}
|
||||
func TestDiffTypeFromRemovedChildren(t *testing.T) {
|
||||
tree := NewFileTree()
|
||||
node, _, _ := tree.AddPath("/usr", *BlankFileChangeInfo("/usr"))
|
||||
_, _, _ = tree.AddPath("/usr", *BlankFileChangeInfo("/usr"))
|
||||
|
||||
info1 := BlankFileChangeInfo("/usr/.wh.bin")
|
||||
node, _, _ = tree.AddPath("/usr/.wh.bin", *info1)
|
||||
node, _, _ := tree.AddPath("/usr/.wh.bin", *info1)
|
||||
node.Data.DiffType = Removed
|
||||
|
||||
info2 := BlankFileChangeInfo("/usr/.wh.bin2")
|
||||
node, _, _ = tree.AddPath("/usr/.wh.bin2", *info2)
|
||||
node.Data.DiffType = Removed
|
||||
|
||||
tree.Root.Children["usr"].deriveDiffType(Unchanged)
|
||||
err := tree.Root.Children["usr"].deriveDiffType(Unchanged)
|
||||
checkError(t, err, "unable to setup test")
|
||||
|
||||
if tree.Root.Children["usr"].Data.DiffType != Changed {
|
||||
t.Errorf("Expected Changed but got %v", tree.Root.Children["usr"].Data.DiffType)
|
||||
|
@ -149,9 +153,12 @@ func TestDiffTypeFromRemovedChildren(t *testing.T) {
|
|||
|
||||
func TestDirSize(t *testing.T) {
|
||||
tree1 := NewFileTree()
|
||||
tree1.AddPath("/etc/nginx/public1", FileInfo{Size: 100})
|
||||
tree1.AddPath("/etc/nginx/thing1", FileInfo{Size: 200})
|
||||
tree1.AddPath("/etc/nginx/public3/thing2", FileInfo{Size: 300})
|
||||
_, _, err := tree1.AddPath("/etc/nginx/public1", FileInfo{Size: 100})
|
||||
checkError(t, err, "unable to setup test")
|
||||
_, _, err = tree1.AddPath("/etc/nginx/thing1", FileInfo{Size: 200})
|
||||
checkError(t, err, "unable to setup test")
|
||||
_, _, err = tree1.AddPath("/etc/nginx/public3/thing2", FileInfo{Size: 300})
|
||||
checkError(t, err, "unable to setup test")
|
||||
|
||||
node, _ := tree1.GetNode("/etc/nginx")
|
||||
expected, actual := "---------- 0:0 600 B ", node.MetadataString()
|
||||
|
|
115
filetree/tree.go
115
filetree/tree.go
|
@ -119,14 +119,42 @@ func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttribu
|
|||
return result
|
||||
}
|
||||
|
||||
func (tree *FileTree) VisibleSize() int {
|
||||
var size int
|
||||
|
||||
visitor := func(node *FileNode) error {
|
||||
size++
|
||||
return nil
|
||||
}
|
||||
visitEvaluator := func(node *FileNode) bool {
|
||||
if node.Data.FileInfo.IsDir {
|
||||
// we won't visit a collapsed dir, but we need to count it
|
||||
if node.Data.ViewInfo.Collapsed {
|
||||
size++
|
||||
}
|
||||
return !node.Data.ViewInfo.Collapsed && !node.Data.ViewInfo.Hidden
|
||||
}
|
||||
return !node.Data.ViewInfo.Hidden
|
||||
}
|
||||
err := tree.VisitDepthParentFirst(visitor, visitEvaluator)
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to determine visible tree size: %+v", err)
|
||||
}
|
||||
|
||||
// don't include root
|
||||
size--
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
// String returns the entire tree in an ASCII representation.
|
||||
func (tree *FileTree) String(showAttributes bool) string {
|
||||
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 {
|
||||
return tree.renderStringTreeBetween(int(start), int(stop), showAttributes)
|
||||
func (tree *FileTree) StringBetween(start, stop int, showAttributes bool) string {
|
||||
return tree.renderStringTreeBetween(start, stop, showAttributes)
|
||||
}
|
||||
|
||||
// Copy returns a copy of the given FileTree
|
||||
|
@ -137,11 +165,15 @@ func (tree *FileTree) Copy() *FileTree {
|
|||
newTree.Root = tree.Root.Copy(newTree.Root)
|
||||
|
||||
// update the tree pointers
|
||||
newTree.VisitDepthChildFirst(func(node *FileNode) error {
|
||||
err := newTree.VisitDepthChildFirst(func(node *FileNode) error {
|
||||
node.Tree = newTree
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to propagate tree on copy(): %+v", err)
|
||||
}
|
||||
|
||||
return newTree
|
||||
}
|
||||
|
||||
|
@ -209,6 +241,11 @@ func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, []*FileNod
|
|||
if node.Children[name] != nil {
|
||||
node = node.Children[name]
|
||||
} else {
|
||||
// don't add paths that should be deleted
|
||||
if strings.HasPrefix(name, doubleWhiteoutPrefix) {
|
||||
return nil, addedNodes, nil
|
||||
}
|
||||
|
||||
// don't attach the payload. The payload is destined for the
|
||||
// Path's end node, not any intermediary node.
|
||||
node = node.AddChild(name, FileInfo{})
|
||||
|
@ -216,7 +253,7 @@ func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, []*FileNod
|
|||
|
||||
if node == nil {
|
||||
// the child could not be added
|
||||
return node, addedNodes, fmt.Errorf(fmt.Sprintf("could not add child node '%s'", name))
|
||||
return node, addedNodes, fmt.Errorf(fmt.Sprintf("could not add child node: '%s' (path:'%s')", name, path))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,13 +276,14 @@ func (tree *FileTree) RemovePath(path string) error {
|
|||
}
|
||||
|
||||
type compareMark struct {
|
||||
node *FileNode
|
||||
lowerNode *FileNode
|
||||
upperNode *FileNode
|
||||
tentative DiffType
|
||||
final DiffType
|
||||
}
|
||||
|
||||
// Compare marks the FileNodes in the owning (lower) tree with DiffType annotations when compared to the given (upper) tree.
|
||||
func (tree *FileTree) Compare(upper *FileTree) error {
|
||||
// CompareAndMark marks the FileNodes in the owning (lower) tree with DiffType annotations when compared to the given (upper) tree.
|
||||
func (tree *FileTree) CompareAndMark(upper *FileTree) error {
|
||||
// always compare relative to the original, unaltered tree.
|
||||
originalTree := tree
|
||||
|
||||
|
@ -257,28 +295,30 @@ func (tree *FileTree) Compare(upper *FileTree) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("cannot remove upperNode %s: %v", upperNode.Path(), err.Error())
|
||||
}
|
||||
} else {
|
||||
// note: since we are not comparing against the original tree (copying the tree is expensive) we may mark the parent
|
||||
// of an added node incorrectly as modified. This will be corrected later.
|
||||
originalLowerNode, _ := originalTree.GetNode(upperNode.Path())
|
||||
|
||||
if originalLowerNode == nil {
|
||||
_, newNodes, err := tree.AddPath(upperNode.Path(), upperNode.Data.FileInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot add new upperNode %s: %v", upperNode.Path(), err.Error())
|
||||
}
|
||||
for idx := len(newNodes) - 1; idx >= 0; idx-- {
|
||||
newNode := newNodes[idx]
|
||||
modifications = append(modifications, compareMark{node: newNode, tentative: -1, final: Added})
|
||||
}
|
||||
|
||||
} else {
|
||||
// check the tree for comparison markings
|
||||
lowerNode, _ := tree.GetNode(upperNode.Path())
|
||||
diffType := lowerNode.compare(upperNode)
|
||||
modifications = append(modifications, compareMark{node: lowerNode, tentative: diffType, final: -1})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// note: since we are not comparing against the original tree (copying the tree is expensive) we may mark the parent
|
||||
// of an added node incorrectly as modified. This will be corrected later.
|
||||
originalLowerNode, _ := originalTree.GetNode(upperNode.Path())
|
||||
|
||||
if originalLowerNode == nil {
|
||||
_, newNodes, err := tree.AddPath(upperNode.Path(), upperNode.Data.FileInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot add new upperNode %s: %v", upperNode.Path(), err.Error())
|
||||
}
|
||||
for idx := len(newNodes) - 1; idx >= 0; idx-- {
|
||||
newNode := newNodes[idx]
|
||||
modifications = append(modifications, compareMark{lowerNode: newNode, upperNode: upperNode, tentative: -1, final: Added})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// the file exists in the lower layer
|
||||
lowerNode, _ := tree.GetNode(upperNode.Path())
|
||||
diffType := lowerNode.compare(upperNode)
|
||||
modifications = append(modifications, compareMark{lowerNode: lowerNode, upperNode: upperNode, tentative: diffType, final: -1})
|
||||
|
||||
return nil
|
||||
}
|
||||
// we must visit from the leaves upwards to ensure that diff types can be derived from and assigned to children
|
||||
|
@ -287,15 +327,22 @@ func (tree *FileTree) Compare(upper *FileTree) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// take note of the comparison results on each note in the owning tree
|
||||
// take note of the comparison results on each note in the owning tree.
|
||||
for _, pair := range modifications {
|
||||
if pair.final > 0 {
|
||||
pair.node.AssignDiffType(pair.final)
|
||||
} else {
|
||||
if pair.node.Data.DiffType == Unchanged {
|
||||
pair.node.deriveDiffType(pair.tentative)
|
||||
err = pair.lowerNode.AssignDiffType(pair.final)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if pair.lowerNode.Data.DiffType == Unchanged {
|
||||
err = pair.lowerNode.deriveDiffType(pair.tentative)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// persist the upper's payload on the owning tree
|
||||
pair.lowerNode.Data.FileInfo = *pair.upperNode.Data.FileInfo.Copy()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -316,7 +363,7 @@ func StackTreeRange(trees []*FileTree, start, stop int) *FileTree {
|
|||
for idx := start; idx <= stop; idx++ {
|
||||
err := tree.Stack(trees[idx])
|
||||
if err != nil {
|
||||
logrus.Debug("could not stack tree range:", err)
|
||||
logrus.Errorf("could not stack tree range: %v", err)
|
||||
}
|
||||
}
|
||||
return tree
|
||||
|
|
|
@ -87,12 +87,30 @@ func TestString(t *testing.T) {
|
|||
|
||||
func TestStringBetween(t *testing.T) {
|
||||
tree := NewFileTree()
|
||||
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
tree.AddPath("/etc/nginx/public", FileInfo{})
|
||||
tree.AddPath("/var/run/systemd", FileInfo{})
|
||||
tree.AddPath("/var/run/bashful", FileInfo{})
|
||||
tree.AddPath("/tmp", FileInfo{})
|
||||
tree.AddPath("/tmp/nonsense", FileInfo{})
|
||||
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/tmp", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
expected :=
|
||||
`│ └── public
|
||||
|
@ -109,12 +127,30 @@ func TestStringBetween(t *testing.T) {
|
|||
|
||||
func TestAddPath(t *testing.T) {
|
||||
tree := NewFileTree()
|
||||
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
tree.AddPath("/etc/nginx/public", FileInfo{})
|
||||
tree.AddPath("/var/run/systemd", FileInfo{})
|
||||
tree.AddPath("/var/run/bashful", FileInfo{})
|
||||
tree.AddPath("/tmp", FileInfo{})
|
||||
tree.AddPath("/tmp/nonsense", FileInfo{})
|
||||
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/tmp", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
expected :=
|
||||
`├── etc
|
||||
|
@ -136,17 +172,65 @@ func TestAddPath(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func TestAddWhiteoutPath(t *testing.T) {
|
||||
tree := NewFileTree()
|
||||
node, _, err := tree.AddPath("usr/local/lib/python3.7/site-packages/pip/.wh..wh..opq", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
if node != nil {
|
||||
t.Errorf("expected node to be nil, but got: %v", node)
|
||||
}
|
||||
expected :=
|
||||
`└── usr
|
||||
└── local
|
||||
└── lib
|
||||
└── python3.7
|
||||
└── site-packages
|
||||
└── pip
|
||||
`
|
||||
actual := tree.String(false)
|
||||
|
||||
if expected != actual {
|
||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemovePath(t *testing.T) {
|
||||
tree := NewFileTree()
|
||||
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
tree.AddPath("/etc/nginx/public", FileInfo{})
|
||||
tree.AddPath("/var/run/systemd", FileInfo{})
|
||||
tree.AddPath("/var/run/bashful", FileInfo{})
|
||||
tree.AddPath("/tmp", FileInfo{})
|
||||
tree.AddPath("/tmp/nonsense", FileInfo{})
|
||||
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/tmp", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
tree.RemovePath("/var/run/bashful")
|
||||
tree.RemovePath("/tmp")
|
||||
err = tree.RemovePath("/var/run/bashful")
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
err = tree.RemovePath("/tmp")
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
expected :=
|
||||
`├── etc
|
||||
|
@ -173,24 +257,57 @@ func TestStack(t *testing.T) {
|
|||
|
||||
tree1 := NewFileTree()
|
||||
|
||||
tree1.AddPath("/etc/nginx/public", FileInfo{})
|
||||
tree1.AddPath(payloadKey, FileInfo{})
|
||||
tree1.AddPath("/var/run/bashful", FileInfo{})
|
||||
tree1.AddPath("/tmp", FileInfo{})
|
||||
tree1.AddPath("/tmp/nonsense", FileInfo{})
|
||||
_, _, err := tree1.AddPath("/etc/nginx/public", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree1.AddPath(payloadKey, FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree1.AddPath("/var/run/bashful", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree1.AddPath("/tmp", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree1.AddPath("/tmp/nonsense", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
tree2 := NewFileTree()
|
||||
// add new files
|
||||
tree2.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
_, _, err = tree2.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
// modify current files
|
||||
tree2.AddPath(payloadKey, payloadValue)
|
||||
_, _, err = tree2.AddPath(payloadKey, payloadValue)
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
// whiteout the following files
|
||||
tree2.AddPath("/var/run/.wh.bashful", FileInfo{})
|
||||
tree2.AddPath("/.wh.tmp", FileInfo{})
|
||||
_, _, err = tree2.AddPath("/var/run/.wh.bashful", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree2.AddPath("/.wh.tmp", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
// ignore opaque whiteout files entirely
|
||||
tree2.AddPath("/.wh..wh..opq", FileInfo{})
|
||||
node, _, err := tree2.AddPath("/.wh..wh..opq", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error on whiteout file add, but got %v", err)
|
||||
}
|
||||
if node != nil {
|
||||
t.Errorf("expected no node on whiteout file add, but got %v", node)
|
||||
}
|
||||
|
||||
err := tree1.Stack(tree2)
|
||||
err = tree1.Stack(tree2)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Could not stack refTrees: %v", err)
|
||||
|
@ -206,12 +323,12 @@ func TestStack(t *testing.T) {
|
|||
└── systemd
|
||||
`
|
||||
|
||||
node, err := tree1.GetNode(payloadKey)
|
||||
node, err = tree1.GetNode(payloadKey)
|
||||
if err != nil {
|
||||
t.Errorf("Expected '%s' to still exist, but it doesn't", payloadKey)
|
||||
}
|
||||
|
||||
if node.Data.FileInfo.Path != payloadValue.Path {
|
||||
if node == nil || node.Data.FileInfo.Path != payloadValue.Path {
|
||||
t.Errorf("Expected '%s' value to be %+v but got %+v", payloadKey, payloadValue, node.Data.FileInfo)
|
||||
}
|
||||
|
||||
|
@ -225,15 +342,39 @@ func TestStack(t *testing.T) {
|
|||
|
||||
func TestCopy(t *testing.T) {
|
||||
tree := NewFileTree()
|
||||
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
tree.AddPath("/etc/nginx/public", FileInfo{})
|
||||
tree.AddPath("/var/run/systemd", FileInfo{})
|
||||
tree.AddPath("/var/run/bashful", FileInfo{})
|
||||
tree.AddPath("/tmp", FileInfo{})
|
||||
tree.AddPath("/tmp/nonsense", FileInfo{})
|
||||
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/tmp", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
tree.RemovePath("/var/run/bashful")
|
||||
tree.RemovePath("/tmp")
|
||||
err = tree.RemovePath("/var/run/bashful")
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
err = tree.RemovePath("/tmp")
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
expected :=
|
||||
`├── etc
|
||||
|
@ -265,10 +406,19 @@ func TestCompareWithNoChanges(t *testing.T) {
|
|||
TypeFlag: 1,
|
||||
hash: 123,
|
||||
}
|
||||
lowerTree.AddPath(value, fakeData)
|
||||
upperTree.AddPath(value, fakeData)
|
||||
_, _, err := lowerTree.AddPath(value, fakeData)
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = upperTree.AddPath(value, fakeData)
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
}
|
||||
err := lowerTree.CompareAndMark(upperTree)
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
lowerTree.Compare(upperTree)
|
||||
asserter := func(n *FileNode) error {
|
||||
if n.Path() == "/" {
|
||||
return nil
|
||||
|
@ -278,7 +428,7 @@ func TestCompareWithNoChanges(t *testing.T) {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
err := lowerTree.VisitDepthChildFirst(asserter, nil)
|
||||
err = lowerTree.VisitDepthChildFirst(asserter, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
@ -291,23 +441,29 @@ func TestCompareWithAdds(t *testing.T) {
|
|||
upperPaths := [...]string{"/etc", "/etc/sudoers", "/usr", "/etc/hosts", "/usr/bin", "/usr/bin/bash", "/a/new/path"}
|
||||
|
||||
for _, value := range lowerPaths {
|
||||
lowerTree.AddPath(value, FileInfo{
|
||||
_, _, err := lowerTree.AddPath(value, FileInfo{
|
||||
Path: value,
|
||||
TypeFlag: 1,
|
||||
hash: 123,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, value := range upperPaths {
|
||||
upperTree.AddPath(value, FileInfo{
|
||||
_, _, err := upperTree.AddPath(value, FileInfo{
|
||||
Path: value,
|
||||
TypeFlag: 1,
|
||||
hash: 123,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
failedAssertions := []error{}
|
||||
err := lowerTree.Compare(upperTree)
|
||||
err := lowerTree.CompareAndMark(upperTree)
|
||||
if err != nil {
|
||||
t.Errorf("Expected tree compare to have no errors, got: %v", err)
|
||||
}
|
||||
|
@ -351,39 +507,51 @@ func TestCompareWithChanges(t *testing.T) {
|
|||
changedPaths := []string{"/etc", "/usr", "/etc/hosts", "/etc/sudoers", "/usr/bin"}
|
||||
|
||||
for _, value := range changedPaths {
|
||||
lowerTree.AddPath(value, FileInfo{
|
||||
_, _, err := lowerTree.AddPath(value, FileInfo{
|
||||
Path: value,
|
||||
TypeFlag: 1,
|
||||
hash: 123,
|
||||
})
|
||||
upperTree.AddPath(value, FileInfo{
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = upperTree.AddPath(value, FileInfo{
|
||||
Path: value,
|
||||
TypeFlag: 1,
|
||||
hash: 456,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
chmodPath := "/etc/non-data-change"
|
||||
|
||||
lowerTree.AddPath(chmodPath, FileInfo{
|
||||
_, _, err := lowerTree.AddPath(chmodPath, FileInfo{
|
||||
Path: chmodPath,
|
||||
TypeFlag: 1,
|
||||
hash: 123,
|
||||
Mode: 0,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
upperTree.AddPath(chmodPath, FileInfo{
|
||||
_, _, err = upperTree.AddPath(chmodPath, FileInfo{
|
||||
Path: chmodPath,
|
||||
TypeFlag: 1,
|
||||
hash: 123,
|
||||
Mode: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
changedPaths = append(changedPaths, chmodPath)
|
||||
|
||||
chownPath := "/etc/non-data-change-2"
|
||||
|
||||
lowerTree.AddPath(chmodPath, FileInfo{
|
||||
_, _, err = lowerTree.AddPath(chmodPath, FileInfo{
|
||||
Path: chownPath,
|
||||
TypeFlag: 1,
|
||||
hash: 123,
|
||||
|
@ -391,8 +559,11 @@ func TestCompareWithChanges(t *testing.T) {
|
|||
Gid: 0,
|
||||
Uid: 0,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
upperTree.AddPath(chmodPath, FileInfo{
|
||||
_, _, err = upperTree.AddPath(chmodPath, FileInfo{
|
||||
Path: chownPath,
|
||||
TypeFlag: 1,
|
||||
hash: 123,
|
||||
|
@ -400,10 +571,17 @@ func TestCompareWithChanges(t *testing.T) {
|
|||
Gid: 12,
|
||||
Uid: 12,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
changedPaths = append(changedPaths, chownPath)
|
||||
|
||||
lowerTree.Compare(upperTree)
|
||||
err = lowerTree.CompareAndMark(upperTree)
|
||||
if err != nil {
|
||||
t.Errorf("unable to compare and mark: %+v", err)
|
||||
}
|
||||
|
||||
failedAssertions := []error{}
|
||||
asserter := func(n *FileNode) error {
|
||||
p := n.Path()
|
||||
|
@ -420,7 +598,7 @@ func TestCompareWithChanges(t *testing.T) {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
err := lowerTree.VisitDepthChildFirst(asserter, nil)
|
||||
err = lowerTree.VisitDepthChildFirst(asserter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors when visiting nodes, got: %+v", err)
|
||||
}
|
||||
|
@ -446,7 +624,10 @@ func TestCompareWithRemoves(t *testing.T) {
|
|||
TypeFlag: 1,
|
||||
hash: 123,
|
||||
}
|
||||
lowerTree.AddPath(value, fakeData)
|
||||
_, _, err := lowerTree.AddPath(value, fakeData)
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, value := range upperPaths {
|
||||
|
@ -455,10 +636,16 @@ func TestCompareWithRemoves(t *testing.T) {
|
|||
TypeFlag: 1,
|
||||
hash: 123,
|
||||
}
|
||||
upperTree.AddPath(value, fakeData)
|
||||
_, _, err := upperTree.AddPath(value, fakeData)
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
lowerTree.Compare(upperTree)
|
||||
err := lowerTree.CompareAndMark(upperTree)
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
failedAssertions := []error{}
|
||||
asserter := func(n *FileNode) error {
|
||||
p := n.Path()
|
||||
|
@ -479,7 +666,7 @@ func TestCompareWithRemoves(t *testing.T) {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
err := lowerTree.VisitDepthChildFirst(asserter, nil)
|
||||
err = lowerTree.VisitDepthChildFirst(asserter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors when visiting nodes, got: %+v", err)
|
||||
}
|
||||
|
@ -495,15 +682,39 @@ func TestCompareWithRemoves(t *testing.T) {
|
|||
|
||||
func TestStackRange(t *testing.T) {
|
||||
tree := NewFileTree()
|
||||
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
tree.AddPath("/etc/nginx/public", FileInfo{})
|
||||
tree.AddPath("/var/run/systemd", FileInfo{})
|
||||
tree.AddPath("/var/run/bashful", FileInfo{})
|
||||
tree.AddPath("/tmp", FileInfo{})
|
||||
tree.AddPath("/tmp/nonsense", FileInfo{})
|
||||
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/tmp", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
tree.RemovePath("/var/run/bashful")
|
||||
tree.RemovePath("/tmp")
|
||||
err = tree.RemovePath("/var/run/bashful")
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
err = tree.RemovePath("/tmp")
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
lowerTree := NewFileTree()
|
||||
upperTree := NewFileTree()
|
||||
|
@ -516,7 +727,10 @@ func TestStackRange(t *testing.T) {
|
|||
TypeFlag: 1,
|
||||
hash: 123,
|
||||
}
|
||||
lowerTree.AddPath(value, fakeData)
|
||||
_, _, err = lowerTree.AddPath(value, fakeData)
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, value := range upperPaths {
|
||||
|
@ -525,7 +739,10 @@ func TestStackRange(t *testing.T) {
|
|||
TypeFlag: 1,
|
||||
hash: 456,
|
||||
}
|
||||
upperTree.AddPath(value, fakeData)
|
||||
_, _, err = upperTree.AddPath(value, fakeData)
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
}
|
||||
trees := []*FileTree{lowerTree, upperTree, tree}
|
||||
StackTreeRange(trees, 0, 2)
|
||||
|
@ -548,12 +765,18 @@ func TestRemoveOnIterate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
tree.VisitDepthChildFirst(func(node *FileNode) error {
|
||||
err := tree.VisitDepthChildFirst(func(node *FileNode) error {
|
||||
if node.Data.ViewInfo.Hidden {
|
||||
tree.RemovePath(node.Path())
|
||||
err := tree.RemovePath(node.Path())
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Errorf("could not setup test: %v", err)
|
||||
}
|
||||
|
||||
expected :=
|
||||
`└── usr
|
||||
|
|
16
go.mod
16
go.mod
|
@ -2,41 +2,35 @@ module github.com/wagoodman/dive
|
|||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.11 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/cespare/xxhash v1.1.0
|
||||
github.com/docker/cli v0.0.0-20190303104010-8ddde26af67f
|
||||
github.com/docker/distribution v0.0.0-20181126153310-93e082742a009850ac46962150b2f652a822c5ff // indirect
|
||||
github.com/docker/docker v0.0.0-20181126153310-0b7cb16dde4a20d024c7be59801d63bcfd18611b
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.3.3 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/gogo/protobuf v1.1.1 // indirect
|
||||
github.com/google/go-cmp v0.2.0 // indirect
|
||||
github.com/golangci/golangci-lint v1.17.1 // indirect
|
||||
github.com/google/uuid v1.1.0
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/mux v1.6.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jroimartin/gocui v0.4.0
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a
|
||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.4 // indirect
|
||||
github.com/mitchellh/go-homedir v1.0.0
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/phayes/permbits v0.0.0-20180830030258-59f2482cd460
|
||||
github.com/pkg/errors v0.8.0 // indirect
|
||||
github.com/sergi/go-diff v1.0.0
|
||||
github.com/sirupsen/logrus v1.2.0
|
||||
github.com/spf13/cobra v0.0.3
|
||||
github.com/spf13/viper v1.2.1
|
||||
github.com/wagoodman/keybinding v0.0.0-20181213133715-6a824da6df05
|
||||
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 // indirect
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
||||
golang.org/x/sys v0.0.0-20181116161606-93218def8b18 // indirect
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
|
||||
google.golang.org/grpc v1.16.0 // indirect
|
||||
gotest.tools v2.2.0+incompatible // indirect
|
||||
|
|
164
go.sum
164
go.sum
|
@ -1,13 +1,20 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2 h1:HTOmFEEYrWi4MW5ZKUx6xfeyM10Sx3kQF65xiQJMPYA=
|
||||
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/cli v0.0.0-20190303104010-8ddde26af67f h1:avQULAD2yl7pQnDbjIxI/H5cwpRn8H/NeMNIbhnDtfI=
|
||||
github.com/docker/cli v0.0.0-20190303104010-8ddde26af67f/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v0.0.0-20181126153310-93e082742a009850ac46962150b2f652a822c5ff h1:dtgxyWsA/HV5EbB2FO4YKoCKEjLwr8SXZQCGjA9mRx4=
|
||||
github.com/docker/distribution v0.0.0-20181126153310-93e082742a009850ac46962150b2f652a822c5ff/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v0.0.0-20181126153310-0b7cb16dde4a20d024c7be59801d63bcfd18611b h1:T6w2S9UvmzJQBKIUsOs3+lNWfZea5yUPLh2eMihRrKY=
|
||||
|
@ -18,52 +25,145 @@ github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk
|
|||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 h1:7CU1IXBpPvxpQ/NqJrpuMXMHAw+FB2vfqtRF8tgW9fw=
|
||||
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
|
||||
github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
|
||||
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
|
||||
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
||||
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
|
||||
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
|
||||
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||
github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ=
|
||||
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
|
||||
github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
|
||||
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
|
||||
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
|
||||
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
|
||||
github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
|
||||
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
|
||||
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
|
||||
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
|
||||
github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
|
||||
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||
github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA=
|
||||
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
|
||||
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 h1:i2jIkQFb8RG45DuQs+ElyROY848cSJIoIkBM+7XXypA=
|
||||
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
|
||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
|
||||
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 h1:9rtVlONXLF1rJZzvLt4tfOXtnAFUEhxCJ64Ibzj6ECo=
|
||||
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
||||
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8=
|
||||
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
|
||||
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee h1:J2XAy40+7yz70uaOiMbNnluTg7gyQhtGqLQncQh+4J8=
|
||||
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 h1:ir6/L2ZOJfFrJlOTsuf/hlzdPuUwXV/VzkSlgS6f1vs=
|
||||
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
||||
github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME=
|
||||
github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g=
|
||||
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547 h1:qMomh8bv+kDazm1dSLZ9S3zZ2PJZMHL4ilfBjxFOlmI=
|
||||
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
|
||||
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc h1:XRFao922N8F3EcIXBSNX8Iywk+GI0dxD/8FicMX2D/c=
|
||||
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
|
||||
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217 h1:r7vyX+SN24x6+5AnpnrRn/bdwBb7U+McZqCHOVtXDuk=
|
||||
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
|
||||
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk=
|
||||
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us=
|
||||
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
|
||||
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0 h1:HVfrLniijszjS1aiNg8JbBMO2+E1WIQ+j/gL4SQqGPg=
|
||||
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s=
|
||||
github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jroimartin/gocui v0.4.0 h1:52jnalstgmc25FmtGcWqa0tcbMEWS6RpFLsOIO+I+E8=
|
||||
github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
|
||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=
|
||||
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/nsf/termbox-go v0.0.0-20181027232701-60ab7e3d12ed h1:bAVGG6B+R5qpSylrrA+BAMrzYkdAoiTaKPVxRB+4cyM=
|
||||
github.com/nsf/termbox-go v0.0.0-20181027232701-60ab7e3d12ed/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/phayes/permbits v0.0.0-20180830030258-59f2482cd460 h1:B9xJsGjeteSbA5LYAmW9KF9/jQcmrJkmpgVWsqRxc0k=
|
||||
|
@ -71,49 +171,113 @@ github.com/phayes/permbits v0.0.0-20180830030258-59f2482cd460/go.mod h1:3uODdxMg
|
|||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
|
||||
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
|
||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
|
||||
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/spf13/viper v1.2.1 h1:bIcUwXqLseLF3BDAZduuNfekWG87ibtFxi59Bq+oI9M=
|
||||
github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb h1:lI9ufgFfvuqRctP9Ny8lDDLbSWCMxBPletcSqrnyFYM=
|
||||
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
|
||||
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/wagoodman/keybinding v0.0.0-20181213133715-6a824da6df05 h1:YMcRwVDe8DLDZ/vrhuImCfqjjG/+gZs6SF61DDQkL/8=
|
||||
github.com/wagoodman/keybinding v0.0.0-20181213133715-6a824da6df05/go.mod h1:gXFkc2sM2o06uzn5Lgo6Ql76uweGdxNfeAlFyKiHAdk=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU=
|
||||
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116161606-93218def8b18 h1:Wh+XCfg3kNpjhdq2LXrsiOProjtQZKme5XUx7VcxwAw=
|
||||
golang.org/x/sys v0.0.0-20181116161606-93218def8b18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd h1:7E3PabyysDSEjnaANKBgums/hyvMI/HoHQ50qZEzTrg=
|
||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
||||
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 h1:B1LAOfRqg2QUyCdzfjf46quTSYUTAK5OCwbh6pljHbM=
|
||||
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
|
|
|
@ -4,14 +4,18 @@ import (
|
|||
"archive/tar"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/wagoodman/dive/filetree"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"golang.org/x/net/context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var dockerVersion string
|
||||
|
@ -59,7 +63,33 @@ func (image *dockerImageAnalyzer) Fetch() (io.ReadCloser, error) {
|
|||
|
||||
// pull the image if it does not exist
|
||||
ctx := context.Background()
|
||||
image.client, err = client.NewClientWithOpts(client.WithVersion(dockerVersion), client.FromEnv)
|
||||
|
||||
host := os.Getenv("DOCKER_HOST")
|
||||
var clientOpts []func(*client.Client) error
|
||||
|
||||
switch strings.Split(host, ":")[0] {
|
||||
case "ssh":
|
||||
helper, err := connhelper.GetConnectionHelper(host)
|
||||
if err != nil {
|
||||
fmt.Println("docker host", err)
|
||||
}
|
||||
clientOpts = append(clientOpts, func(c *client.Client) error {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: helper.Dialer,
|
||||
},
|
||||
}
|
||||
return client.WithHTTPClient(httpClient)(c)
|
||||
})
|
||||
clientOpts = append(clientOpts, client.WithHost(helper.Host))
|
||||
clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
|
||||
|
||||
default:
|
||||
clientOpts = append(clientOpts, client.FromEnv)
|
||||
}
|
||||
|
||||
clientOpts = append(clientOpts, client.WithVersion(dockerVersion))
|
||||
image.client, err = client.NewClientWithOpts(clientOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -67,7 +97,10 @@ func (image *dockerImageAnalyzer) Fetch() (io.ReadCloser, error) {
|
|||
if err != nil {
|
||||
// don't use the API, the CLI has more informative output
|
||||
fmt.Println("Image not available locally. Trying to pull '" + image.id + "'...")
|
||||
utils.RunDockerCmd("pull", image.id)
|
||||
err = utils.RunDockerCmd("pull", image.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
readCloser, err := image.client.ImageSave(ctx, []string{image.id})
|
||||
|
@ -86,7 +119,6 @@ func (image *dockerImageAnalyzer) Parse(tarFile io.ReadCloser) error {
|
|||
header, err := tarReader.Next()
|
||||
|
||||
if err == io.EOF {
|
||||
fmt.Println(" ╧")
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -139,25 +171,36 @@ func (image *dockerImageAnalyzer) Analyze() (*AnalysisResult, error) {
|
|||
|
||||
// note that the image config stores images in reverse chronological order, so iterate backwards through layers
|
||||
// as you iterate chronologically through history (ignoring history items that have no layer contents)
|
||||
layerIdx := len(image.trees) - 1
|
||||
// Note: history is not required metadata in a docker image!
|
||||
tarPathIdx := 0
|
||||
for idx := 0; idx < len(config.History); idx++ {
|
||||
// ignore empty layers, we are only observing layers with content
|
||||
if config.History[idx].EmptyLayer {
|
||||
continue
|
||||
}
|
||||
histIdx := 0
|
||||
for layerIdx := len(image.trees) - 1; layerIdx >= 0; layerIdx-- {
|
||||
|
||||
tree := image.trees[(len(image.trees)-1)-layerIdx]
|
||||
config.History[idx].Size = uint64(tree.FileSize)
|
||||
|
||||
// ignore empty layers, we are only observing layers with content
|
||||
historyObj := dockerImageHistoryEntry{
|
||||
CreatedBy: "(missing)",
|
||||
}
|
||||
for nextHistIdx := histIdx; nextHistIdx < len(config.History); nextHistIdx++ {
|
||||
if !config.History[nextHistIdx].EmptyLayer {
|
||||
histIdx = nextHistIdx
|
||||
break
|
||||
}
|
||||
}
|
||||
if histIdx < len(config.History) && !config.History[histIdx].EmptyLayer {
|
||||
historyObj = config.History[histIdx]
|
||||
histIdx++
|
||||
}
|
||||
|
||||
image.layers[layerIdx] = &dockerLayer{
|
||||
history: config.History[idx],
|
||||
index: layerIdx,
|
||||
history: historyObj,
|
||||
index: tarPathIdx,
|
||||
tree: image.trees[layerIdx],
|
||||
tarPath: manifest.LayerTarPaths[tarPathIdx],
|
||||
}
|
||||
image.layers[layerIdx].history.Size = uint64(tree.FileSize)
|
||||
|
||||
layerIdx--
|
||||
tarPathIdx++
|
||||
}
|
||||
|
||||
|
@ -191,36 +234,23 @@ func (image *dockerImageAnalyzer) Analyze() (*AnalysisResult, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// todo: it is bad that this is printing out to the screen. As the interface gets more flushed out, an event update mechanism should be built in (so the caller can format and print updates)
|
||||
func (image *dockerImageAnalyzer) processLayerTar(name string, layerIdx uint, reader *tar.Reader) error {
|
||||
tree := filetree.NewFileTree()
|
||||
tree.Name = name
|
||||
|
||||
title := fmt.Sprintf("[layer: %2d]", layerIdx)
|
||||
message := fmt.Sprintf(" ├─ %s %s ", title, "working...")
|
||||
fmt.Printf("\r%s", message)
|
||||
|
||||
fileInfos, err := image.getFileList(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shortName := name[:15]
|
||||
pb := utils.NewProgressBar(int64(len(fileInfos)), 30)
|
||||
for idx, element := range fileInfos {
|
||||
for _, element := range fileInfos {
|
||||
tree.FileSize += uint64(element.Size)
|
||||
|
||||
// todo: we should check for errors but also allow whiteout files to be not be added (thus not error out)
|
||||
tree.AddPath(element.Path, element)
|
||||
|
||||
if pb.Update(int64(idx)) {
|
||||
message = fmt.Sprintf(" ├─ %s %s : %s", title, shortName, pb.String())
|
||||
fmt.Printf("\r%s", message)
|
||||
_, _, err := tree.AddPath(element.Path, element)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
pb.Done()
|
||||
message = fmt.Sprintf(" ├─ %s %s : %s", title, shortName, pb.String())
|
||||
fmt.Printf("\r%s\n", message)
|
||||
|
||||
image.layerMap[tree.Name] = tree
|
||||
return nil
|
||||
|
|
|
@ -8,7 +8,8 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
LayerFormat = "%-25s %7s %s"
|
||||
// LayerFormat = "%-15s %7s %s"
|
||||
LayerFormat = "%7s %s"
|
||||
)
|
||||
|
||||
// ShortId returns the truncated id of the current layer.
|
||||
|
@ -43,9 +44,9 @@ func (layer *dockerLayer) Command() string {
|
|||
|
||||
// ShortId returns the truncated id of the current layer.
|
||||
func (layer *dockerLayer) ShortId() string {
|
||||
rangeBound := 25
|
||||
rangeBound := 15
|
||||
id := layer.Id()
|
||||
if length := len(id); length < 25 {
|
||||
if length := len(id); length < 15 {
|
||||
rangeBound = length
|
||||
}
|
||||
id = id[0:rangeBound]
|
||||
|
@ -63,12 +64,14 @@ func (layer *dockerLayer) String() string {
|
|||
|
||||
if layer.index == 0 {
|
||||
return fmt.Sprintf(LayerFormat,
|
||||
layer.ShortId(),
|
||||
// layer.ShortId(),
|
||||
// fmt.Sprintf("%d",layer.Index()),
|
||||
humanize.Bytes(layer.Size()),
|
||||
"FROM "+layer.ShortId())
|
||||
}
|
||||
return fmt.Sprintf(LayerFormat,
|
||||
layer.ShortId(),
|
||||
// layer.ShortId(),
|
||||
// fmt.Sprintf("%d",layer.Index()),
|
||||
humanize.Bytes(layer.Size()),
|
||||
layer.Command())
|
||||
}
|
||||
|
|
|
@ -42,10 +42,7 @@ func (ci *Evaluator) LoadConfig(configFile string) error {
|
|||
|
||||
func (ci *Evaluator) isRuleEnabled(rule Rule) bool {
|
||||
value := ci.Config.GetString(rule.Key())
|
||||
if value == "disabled" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return value != "disabled"
|
||||
}
|
||||
|
||||
func (ci *Evaluator) Evaluate(analysis *image.AnalysisResult) bool {
|
||||
|
|
|
@ -17,7 +17,7 @@ func newExport(analysis *image.AnalysisResult) *export {
|
|||
idx := (len(analysis.Layers) - 1) - revIdx
|
||||
|
||||
data.Layer[idx] = exportLayer{
|
||||
Index: idx,
|
||||
Index: layer.Index(),
|
||||
DigestID: layer.Id(),
|
||||
SizeBytes: layer.Size(),
|
||||
Command: layer.Command(),
|
||||
|
|
|
@ -79,7 +79,7 @@ func Run(options Options) {
|
|||
|
||||
analyzer := image.GetAnalyzer(options.ImageId)
|
||||
|
||||
fmt.Println(title("Fetching image..."))
|
||||
fmt.Println(title("Fetching image...") + " (this can take a while with large images)")
|
||||
reader, err := analyzer.Fetch()
|
||||
if err != nil {
|
||||
fmt.Printf("cannot fetch image: %v\n", err)
|
||||
|
|
148
ui/details_controller.go
Normal file
148
ui/details_controller.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/lunixbochs/vtclean"
|
||||
"github.com/wagoodman/dive/filetree"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DetailsController 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 DetailsController struct {
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
header *gocui.View
|
||||
efficiency float64
|
||||
inefficiencies filetree.EfficiencySlice
|
||||
}
|
||||
|
||||
// NewDetailsController creates a new view object attached the the global [gocui] screen object.
|
||||
func NewDetailsController(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice) (controller *DetailsController) {
|
||||
controller = new(DetailsController)
|
||||
|
||||
// populate main fields
|
||||
controller.Name = name
|
||||
controller.gui = gui
|
||||
controller.efficiency = efficiency
|
||||
controller.inefficiencies = inefficiencies
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (controller *DetailsController) Setup(v *gocui.View, header *gocui.View) error {
|
||||
|
||||
// set controller options
|
||||
controller.view = v
|
||||
controller.view.Editable = false
|
||||
controller.view.Wrap = true
|
||||
controller.view.Highlight = false
|
||||
controller.view.Frame = false
|
||||
|
||||
controller.header = header
|
||||
controller.header.Editable = false
|
||||
controller.header.Wrap = false
|
||||
controller.header.Frame = false
|
||||
|
||||
// set keybindings
|
||||
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// IsVisible indicates if the details view pane is currently initialized.
|
||||
func (controller *DetailsController) IsVisible() bool {
|
||||
return controller != nil
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
|
||||
func (controller *DetailsController) CursorDown() error {
|
||||
return CursorDown(controller.gui, controller.view)
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
|
||||
func (controller *DetailsController) CursorUp() error {
|
||||
return CursorUp(controller.gui, controller.view)
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering.
|
||||
func (controller *DetailsController) Update() error {
|
||||
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 (controller *DetailsController) Render() error {
|
||||
currentLayer := Controllers.Layer.currentLayer()
|
||||
|
||||
var wastedSpace int64
|
||||
|
||||
template := "%5s %12s %-s\n"
|
||||
inefficiencyReport := fmt.Sprintf(Formatting.Header(template), "Count", "Total Space", "Path")
|
||||
|
||||
height := 100
|
||||
if controller.view != nil {
|
||||
_, height = controller.view.Size()
|
||||
}
|
||||
|
||||
for idx := 0; idx < len(controller.inefficiencies); idx++ {
|
||||
data := controller.inefficiencies[len(controller.inefficiencies)-1-idx]
|
||||
wastedSpace += data.CumulativeSize
|
||||
|
||||
// todo: make this report scrollable
|
||||
if idx < height {
|
||||
inefficiencyReport += fmt.Sprintf(template, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
|
||||
}
|
||||
}
|
||||
|
||||
imageSizeStr := fmt.Sprintf("%s %s", Formatting.Header("Total Image size:"), humanize.Bytes(Controllers.Layer.ImageSize))
|
||||
effStr := fmt.Sprintf("%s %d %%", Formatting.Header("Image efficiency score:"), int(100.0*controller.efficiency))
|
||||
wastedSpaceStr := fmt.Sprintf("%s %s", Formatting.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
|
||||
|
||||
controller.gui.Update(func(g *gocui.Gui) error {
|
||||
// update header
|
||||
controller.header.Clear()
|
||||
width, _ := controller.view.Size()
|
||||
|
||||
layerHeaderStr := fmt.Sprintf("[Layer Details]%s", strings.Repeat("─", width-15))
|
||||
imageHeaderStr := fmt.Sprintf("[Image Details]%s", strings.Repeat("─", width-15))
|
||||
|
||||
_, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(layerHeaderStr, false)))
|
||||
|
||||
// update contents
|
||||
controller.view.Clear()
|
||||
_, _ = fmt.Fprintln(controller.view, Formatting.Header("Digest: ")+currentLayer.Id())
|
||||
// TODO: add back in with controller model
|
||||
// fmt.Fprintln(view.view, Formatting.Header("Tar ID: ")+currentLayer.TarId())
|
||||
_, _ = fmt.Fprintln(controller.view, Formatting.Header("Command:"))
|
||||
_, _ = fmt.Fprintln(controller.view, currentLayer.Command())
|
||||
|
||||
_, _ = fmt.Fprintln(controller.view, "\n"+Formatting.Header(vtclean.Clean(imageHeaderStr, false)))
|
||||
|
||||
_, _ = fmt.Fprintln(controller.view, imageSizeStr)
|
||||
_, _ = fmt.Fprintln(controller.view, wastedSpaceStr)
|
||||
_, _ = fmt.Fprintln(controller.view, effStr+"\n")
|
||||
|
||||
_, _ = fmt.Fprintln(controller.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 (controller *DetailsController) KeyHelp() string {
|
||||
return "TBD"
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/lunixbochs/vtclean"
|
||||
"github.com/wagoodman/dive/filetree"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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, efficiency float64, inefficiencies filetree.EfficiencySlice) (detailsView *DetailsView) {
|
||||
detailsView = new(DetailsView)
|
||||
|
||||
// populate main fields
|
||||
detailsView.Name = name
|
||||
detailsView.gui = gui
|
||||
detailsView.efficiency = efficiency
|
||||
detailsView.inefficiencies = inefficiencies
|
||||
|
||||
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.
|
||||
func (view *DetailsView) Update() error {
|
||||
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"
|
||||
inefficiencyReport := fmt.Sprintf(Formatting.Header(template), "Count", "Total Space", "Path")
|
||||
|
||||
height := 100
|
||||
if view.view != nil {
|
||||
_, height = view.view.Size()
|
||||
}
|
||||
|
||||
for idx := 0; idx < len(view.inefficiencies); idx++ {
|
||||
data := view.inefficiencies[len(view.inefficiencies)-1-idx]
|
||||
wastedSpace += data.CumulativeSize
|
||||
|
||||
// todo: make this report scrollable
|
||||
if idx < height {
|
||||
inefficiencyReport += fmt.Sprintf(template, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
|
||||
}
|
||||
}
|
||||
|
||||
imageSizeStr := fmt.Sprintf("%s %s", Formatting.Header("Total Image size:"), humanize.Bytes(Views.Layer.ImageSize))
|
||||
effStr := fmt.Sprintf("%s %d %%", Formatting.Header("Image efficiency score:"), int(100.0*view.efficiency))
|
||||
wastedSpaceStr := fmt.Sprintf("%s %s", Formatting.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
|
||||
|
||||
view.gui.Update(func(g *gocui.Gui) error {
|
||||
// update header
|
||||
view.header.Clear()
|
||||
width, _ := view.view.Size()
|
||||
|
||||
layerHeaderStr := fmt.Sprintf("[Layer Details]%s", strings.Repeat("─", width-15))
|
||||
imageHeaderStr := fmt.Sprintf("[Image Details]%s", strings.Repeat("─", width-15))
|
||||
|
||||
fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(layerHeaderStr, false)))
|
||||
|
||||
// update contents
|
||||
view.view.Clear()
|
||||
fmt.Fprintln(view.view, Formatting.Header("Digest: ")+currentLayer.Id())
|
||||
// TODO: add back in with view model
|
||||
// fmt.Fprintln(view.view, Formatting.Header("Tar ID: ")+currentLayer.TarId())
|
||||
fmt.Fprintln(view.view, Formatting.Header("Command:"))
|
||||
fmt.Fprintln(view.view, currentLayer.Command())
|
||||
|
||||
fmt.Fprintln(view.view, "\n"+Formatting.Header(vtclean.Clean(imageHeaderStr, false)))
|
||||
|
||||
fmt.Fprintln(view.view, imageSizeStr)
|
||||
fmt.Fprintln(view.view, wastedSpaceStr)
|
||||
fmt.Fprintln(view.view, effStr+"\n")
|
||||
|
||||
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"
|
||||
}
|
398
ui/filetree_controller.go
Normal file
398
ui/filetree_controller.go
Normal file
|
@ -0,0 +1,398 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lunixbochs/vtclean"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/keybinding"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/wagoodman/dive/filetree"
|
||||
)
|
||||
|
||||
const (
|
||||
CompareLayer CompareType = iota
|
||||
CompareAll
|
||||
)
|
||||
|
||||
type CompareType int
|
||||
|
||||
// FileTreeController 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 FileTreeController struct {
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
header *gocui.View
|
||||
vm *FileTreeViewModel
|
||||
|
||||
keybindingToggleCollapse []keybinding.Key
|
||||
keybindingToggleCollapseAll []keybinding.Key
|
||||
keybindingToggleAttributes []keybinding.Key
|
||||
keybindingToggleAdded []keybinding.Key
|
||||
keybindingToggleRemoved []keybinding.Key
|
||||
keybindingToggleModified []keybinding.Key
|
||||
keybindingToggleUnchanged []keybinding.Key
|
||||
keybindingPageDown []keybinding.Key
|
||||
keybindingPageUp []keybinding.Key
|
||||
}
|
||||
|
||||
// NewFileTreeController creates a new view object attached the the global [gocui] screen object.
|
||||
func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (controller *FileTreeController) {
|
||||
controller = new(FileTreeController)
|
||||
|
||||
// populate main fields
|
||||
controller.Name = name
|
||||
controller.gui = gui
|
||||
controller.vm = NewFileTreeViewModel(tree, refTrees, cache)
|
||||
|
||||
var err error
|
||||
controller.keybindingToggleCollapse, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-dir"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
controller.keybindingToggleCollapseAll, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-all-dir"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
controller.keybindingToggleAttributes, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-filetree-attributes"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
controller.keybindingToggleAdded, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-added-files"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
controller.keybindingToggleRemoved, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-removed-files"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
controller.keybindingToggleModified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-modified-files"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
controller.keybindingToggleUnchanged, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-unchanged-files"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
controller.keybindingPageUp, err = keybinding.ParseAll(viper.GetString("keybinding.page-up"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
controller.keybindingPageDown, err = keybinding.ParseAll(viper.GetString("keybinding.page-down"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (controller *FileTreeController) Setup(v *gocui.View, header *gocui.View) error {
|
||||
|
||||
// set controller options
|
||||
controller.view = v
|
||||
controller.view.Editable = false
|
||||
controller.view.Wrap = false
|
||||
controller.view.Frame = false
|
||||
|
||||
controller.header = header
|
||||
controller.header.Editable = false
|
||||
controller.header.Wrap = false
|
||||
controller.header.Frame = false
|
||||
|
||||
// set keybindings
|
||||
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorLeft() }); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorRight() }); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, key := range controller.keybindingPageUp {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageUp() }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range controller.keybindingPageDown {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageDown() }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range controller.keybindingToggleCollapse {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleCollapse() }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range controller.keybindingToggleCollapseAll {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleCollapseAll() }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range controller.keybindingToggleAttributes {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleAttributes() }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range controller.keybindingToggleAdded {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Added) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range controller.keybindingToggleRemoved {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Removed) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range controller.keybindingToggleModified {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Changed) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range controller.keybindingToggleUnchanged {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Unchanged) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, height := controller.view.Size()
|
||||
controller.vm.Setup(0, height)
|
||||
_ = controller.Update()
|
||||
_ = controller.Render()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsVisible indicates if the file tree view pane is currently initialized
|
||||
func (controller *FileTreeController) IsVisible() bool {
|
||||
return controller != nil
|
||||
}
|
||||
|
||||
// resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
|
||||
func (controller *FileTreeController) resetCursor() {
|
||||
_ = controller.view.SetCursor(0, 0)
|
||||
controller.vm.resetCursor()
|
||||
}
|
||||
|
||||
// setTreeByLayer populates the view model by stacking the indicated image layer file trees.
|
||||
func (controller *FileTreeController) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
|
||||
err := controller.vm.setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// controller.resetCursor()
|
||||
|
||||
_ = controller.Update()
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// 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 (controller *FileTreeController) CursorDown() error {
|
||||
if controller.vm.CursorDown() {
|
||||
return controller.Render()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 (controller *FileTreeController) CursorUp() error {
|
||||
if controller.vm.CursorUp() {
|
||||
return controller.Render()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
|
||||
func (controller *FileTreeController) CursorLeft() error {
|
||||
err := controller.vm.CursorLeft(filterRegex())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = controller.Update()
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// CursorRight descends into directory expanding it if needed
|
||||
func (controller *FileTreeController) CursorRight() error {
|
||||
err := controller.vm.CursorRight(filterRegex())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = controller.Update()
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// PageDown moves to next page putting the cursor on top
|
||||
func (controller *FileTreeController) PageDown() error {
|
||||
err := controller.vm.PageDown()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// PageUp moves to previous page putting the cursor on top
|
||||
func (controller *FileTreeController) PageUp() error {
|
||||
err := controller.vm.PageUp()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
|
||||
// func (controller *FileTreeController) getAbsPositionNode() (node *filetree.FileNode) {
|
||||
// return controller.vm.getAbsPositionNode(filterRegex())
|
||||
// }
|
||||
|
||||
// toggleCollapse will collapse/expand the selected FileNode.
|
||||
func (controller *FileTreeController) toggleCollapse() error {
|
||||
err := controller.vm.toggleCollapse(filterRegex())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = controller.Update()
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// toggleCollapseAll will collapse/expand the all directories.
|
||||
func (controller *FileTreeController) toggleCollapseAll() error {
|
||||
err := controller.vm.toggleCollapseAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if controller.vm.CollapseAll {
|
||||
controller.resetCursor()
|
||||
}
|
||||
_ = controller.Update()
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// toggleAttributes will show/hide file attributes
|
||||
func (controller *FileTreeController) toggleAttributes() error {
|
||||
err := controller.vm.toggleAttributes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// we need to render the changes to the status pane as well
|
||||
Update()
|
||||
Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
// toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
|
||||
func (controller *FileTreeController) toggleShowDiffType(diffType filetree.DiffType) error {
|
||||
controller.vm.toggleShowDiffType(diffType)
|
||||
// we need to render the changes to the status pane as well
|
||||
Update()
|
||||
Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterRegex will return a regular expression object to match the user's filter input.
|
||||
func filterRegex() *regexp.Regexp {
|
||||
if Controllers.Filter == nil || Controllers.Filter.view == nil {
|
||||
return nil
|
||||
}
|
||||
filterString := strings.TrimSpace(Controllers.Filter.view.Buffer())
|
||||
if len(filterString) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
regex, err := regexp.Compile(filterString)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return regex
|
||||
}
|
||||
|
||||
// onLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions
|
||||
func (controller *FileTreeController) onLayoutChange(resized bool) error {
|
||||
_ = controller.Update()
|
||||
if resized {
|
||||
return controller.Render()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering.
|
||||
func (controller *FileTreeController) Update() error {
|
||||
var width, height int
|
||||
|
||||
if controller.view != nil {
|
||||
width, height = controller.view.Size()
|
||||
} else {
|
||||
// before the TUI is setup there may not be a controller to reference. Use the entire screen as reference.
|
||||
width, height = controller.gui.Size()
|
||||
}
|
||||
// height should account for the header
|
||||
return controller.vm.Update(filterRegex(), width, height-1)
|
||||
}
|
||||
|
||||
// Render flushes the state objects (file tree) to the pane.
|
||||
func (controller *FileTreeController) Render() error {
|
||||
title := "Current Layer Contents"
|
||||
if Controllers.Layer.CompareMode == CompareAll {
|
||||
title = "Aggregated Layer Contents"
|
||||
}
|
||||
|
||||
// indicate when selected
|
||||
if controller.gui.CurrentView() == controller.view {
|
||||
title = "● " + title
|
||||
}
|
||||
|
||||
controller.gui.Update(func(g *gocui.Gui) error {
|
||||
// update the header
|
||||
controller.header.Clear()
|
||||
width, _ := g.Size()
|
||||
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
|
||||
if controller.vm.ShowAttributes {
|
||||
headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree")
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false)))
|
||||
|
||||
// update the contents
|
||||
controller.view.Clear()
|
||||
_ = controller.vm.Render()
|
||||
_, _ = fmt.Fprint(controller.view, controller.vm.mainBuf.String())
|
||||
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||
func (controller *FileTreeController) KeyHelp() string {
|
||||
return renderStatusOption(controller.keybindingToggleCollapse[0].String(), "Collapse dir", false) +
|
||||
renderStatusOption(controller.keybindingToggleCollapseAll[0].String(), "Collapse all dir", false) +
|
||||
renderStatusOption(controller.keybindingToggleAdded[0].String(), "Added", !controller.vm.HiddenDiffTypes[filetree.Added]) +
|
||||
renderStatusOption(controller.keybindingToggleRemoved[0].String(), "Removed", !controller.vm.HiddenDiffTypes[filetree.Removed]) +
|
||||
renderStatusOption(controller.keybindingToggleModified[0].String(), "Modified", !controller.vm.HiddenDiffTypes[filetree.Changed]) +
|
||||
renderStatusOption(controller.keybindingToggleUnchanged[0].String(), "Unmodified", !controller.vm.HiddenDiffTypes[filetree.Unchanged]) +
|
||||
renderStatusOption(controller.keybindingToggleAttributes[0].String(), "Attributes", controller.vm.ShowAttributes)
|
||||
}
|
424
ui/filetree_viewmodel.go
Normal file
424
ui/filetree_viewmodel.go
Normal file
|
@ -0,0 +1,424 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/lunixbochs/vtclean"
|
||||
"github.com/wagoodman/dive/filetree"
|
||||
)
|
||||
|
||||
// FileTreeViewModel 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 FileTreeViewModel struct {
|
||||
ModelTree *filetree.FileTree
|
||||
ViewTree *filetree.FileTree
|
||||
RefTrees []*filetree.FileTree
|
||||
cache filetree.TreeCache
|
||||
|
||||
CollapseAll bool
|
||||
ShowAttributes bool
|
||||
HiddenDiffTypes []bool
|
||||
TreeIndex int
|
||||
bufferIndex int
|
||||
bufferIndexLowerBound int
|
||||
|
||||
refHeight int
|
||||
refWidth int
|
||||
|
||||
mainBuf bytes.Buffer
|
||||
}
|
||||
|
||||
// NewFileTreeController creates a new view object attached the the global [gocui] screen object.
|
||||
func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeViewModel *FileTreeViewModel) {
|
||||
treeViewModel = new(FileTreeViewModel)
|
||||
|
||||
// populate main fields
|
||||
treeViewModel.ShowAttributes = viper.GetBool("filetree.show-attributes")
|
||||
treeViewModel.CollapseAll = viper.GetBool("filetree.collapse-dir")
|
||||
treeViewModel.ModelTree = tree
|
||||
treeViewModel.RefTrees = refTrees
|
||||
treeViewModel.cache = cache
|
||||
treeViewModel.HiddenDiffTypes = make([]bool, 4)
|
||||
|
||||
hiddenTypes := viper.GetStringSlice("diff.hide")
|
||||
for _, hType := range hiddenTypes {
|
||||
switch t := strings.ToLower(hType); t {
|
||||
case "added":
|
||||
treeViewModel.HiddenDiffTypes[filetree.Added] = true
|
||||
case "removed":
|
||||
treeViewModel.HiddenDiffTypes[filetree.Removed] = true
|
||||
case "changed":
|
||||
treeViewModel.HiddenDiffTypes[filetree.Changed] = true
|
||||
case "unchanged":
|
||||
treeViewModel.HiddenDiffTypes[filetree.Unchanged] = true
|
||||
default:
|
||||
utils.PrintAndExit(fmt.Sprintf("unknown diff.hide value: %s", t))
|
||||
}
|
||||
}
|
||||
|
||||
return treeViewModel
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (vm *FileTreeViewModel) Setup(lowerBound, height int) {
|
||||
vm.bufferIndexLowerBound = lowerBound
|
||||
vm.refHeight = height
|
||||
}
|
||||
|
||||
// height returns the current height and considers the header
|
||||
func (vm *FileTreeViewModel) height() int {
|
||||
if vm.ShowAttributes {
|
||||
return vm.refHeight - 1
|
||||
}
|
||||
return vm.refHeight
|
||||
}
|
||||
|
||||
// bufferIndexUpperBound returns the current upper bounds for the view
|
||||
func (vm *FileTreeViewModel) bufferIndexUpperBound() int {
|
||||
return vm.bufferIndexLowerBound + vm.height()
|
||||
}
|
||||
|
||||
// IsVisible indicates if the file tree view pane is currently initialized
|
||||
func (vm *FileTreeViewModel) IsVisible() bool {
|
||||
return vm != nil
|
||||
}
|
||||
|
||||
// resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
|
||||
func (vm *FileTreeViewModel) resetCursor() {
|
||||
vm.TreeIndex = 0
|
||||
vm.bufferIndex = 0
|
||||
vm.bufferIndexLowerBound = 0
|
||||
}
|
||||
|
||||
// setTreeByLayer populates the view model by stacking the indicated image layer file trees.
|
||||
func (vm *FileTreeViewModel) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
|
||||
if topTreeStop > len(vm.RefTrees)-1 {
|
||||
return fmt.Errorf("invalid layer index given: %d of %d", topTreeStop, len(vm.RefTrees)-1)
|
||||
}
|
||||
newTree := vm.cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
|
||||
|
||||
// preserve vm state on copy
|
||||
visitor := func(node *filetree.FileNode) error {
|
||||
newNode, err := newTree.GetNode(node.Path())
|
||||
if err == nil {
|
||||
newNode.Data.ViewInfo = node.Data.ViewInfo
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err := vm.ModelTree.VisitDepthChildFirst(visitor, nil)
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to propagate layer tree: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
vm.ModelTree = newTree
|
||||
return nil
|
||||
}
|
||||
|
||||
// doCursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer.
|
||||
func (vm *FileTreeViewModel) CursorUp() bool {
|
||||
if vm.TreeIndex <= 0 {
|
||||
return false
|
||||
}
|
||||
vm.TreeIndex--
|
||||
if vm.TreeIndex < vm.bufferIndexLowerBound {
|
||||
vm.bufferIndexLowerBound--
|
||||
}
|
||||
if vm.bufferIndex > 0 {
|
||||
vm.bufferIndex--
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer.
|
||||
func (vm *FileTreeViewModel) CursorDown() bool {
|
||||
if vm.TreeIndex >= vm.ModelTree.VisibleSize() {
|
||||
return false
|
||||
}
|
||||
vm.TreeIndex++
|
||||
if vm.TreeIndex > vm.bufferIndexUpperBound() {
|
||||
vm.bufferIndexLowerBound++
|
||||
}
|
||||
vm.bufferIndex++
|
||||
if vm.bufferIndex > vm.height() {
|
||||
vm.bufferIndex = vm.height()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
|
||||
func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error {
|
||||
var visitor func(*filetree.FileNode) error
|
||||
var evaluator func(*filetree.FileNode) bool
|
||||
var dfsCounter, newIndex int
|
||||
oldIndex := vm.TreeIndex
|
||||
currentNode := vm.getAbsPositionNode(filterRegex)
|
||||
|
||||
if currentNode == nil {
|
||||
return nil
|
||||
}
|
||||
parentPath := currentNode.Parent.Path()
|
||||
|
||||
visitor = func(curNode *filetree.FileNode) error {
|
||||
if strings.Compare(parentPath, curNode.Path()) == 0 {
|
||||
newIndex = dfsCounter
|
||||
}
|
||||
dfsCounter++
|
||||
return nil
|
||||
}
|
||||
|
||||
evaluator = func(curNode *filetree.FileNode) bool {
|
||||
regexMatch := true
|
||||
if filterRegex != nil {
|
||||
match := filterRegex.Find([]byte(curNode.Path()))
|
||||
regexMatch = match != nil
|
||||
}
|
||||
return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch
|
||||
}
|
||||
|
||||
err := vm.ModelTree.VisitDepthParentFirst(visitor, evaluator)
|
||||
if err != nil {
|
||||
logrus.Errorf("could not propagate tree on cursorLeft: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
vm.TreeIndex = newIndex
|
||||
moveIndex := oldIndex - newIndex
|
||||
if newIndex < vm.bufferIndexLowerBound {
|
||||
vm.bufferIndexLowerBound = vm.TreeIndex
|
||||
}
|
||||
|
||||
if vm.bufferIndex > moveIndex {
|
||||
vm.bufferIndex = vm.bufferIndex - moveIndex
|
||||
} else {
|
||||
vm.bufferIndex = 0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorRight descends into directory expanding it if needed
|
||||
func (vm *FileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error {
|
||||
node := vm.getAbsPositionNode(filterRegex)
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !node.Data.FileInfo.IsDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(node.Children) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.Data.ViewInfo.Collapsed {
|
||||
node.Data.ViewInfo.Collapsed = false
|
||||
}
|
||||
|
||||
vm.TreeIndex++
|
||||
if vm.TreeIndex > vm.bufferIndexUpperBound() {
|
||||
vm.bufferIndexLowerBound++
|
||||
}
|
||||
|
||||
vm.bufferIndex++
|
||||
if vm.bufferIndex > vm.height() {
|
||||
vm.bufferIndex = vm.height()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PageDown moves to next page putting the cursor on top
|
||||
func (vm *FileTreeViewModel) PageDown() error {
|
||||
nextBufferIndexLowerBound := vm.bufferIndexLowerBound + vm.height()
|
||||
nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height()
|
||||
|
||||
// todo: this work should be saved or passed to render...
|
||||
treeString := vm.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, vm.ShowAttributes)
|
||||
lines := strings.Split(treeString, "\n")
|
||||
|
||||
newLines := len(lines) - 1
|
||||
if vm.height() >= newLines {
|
||||
nextBufferIndexLowerBound = vm.bufferIndexLowerBound + newLines
|
||||
}
|
||||
|
||||
vm.bufferIndexLowerBound = nextBufferIndexLowerBound
|
||||
|
||||
if vm.TreeIndex < nextBufferIndexLowerBound {
|
||||
vm.bufferIndex = 0
|
||||
vm.TreeIndex = nextBufferIndexLowerBound
|
||||
} else {
|
||||
vm.bufferIndex = vm.bufferIndex - newLines
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PageUp moves to previous page putting the cursor on top
|
||||
func (vm *FileTreeViewModel) PageUp() error {
|
||||
nextBufferIndexLowerBound := vm.bufferIndexLowerBound - vm.height()
|
||||
nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height()
|
||||
|
||||
// todo: this work should be saved or passed to render...
|
||||
treeString := vm.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, vm.ShowAttributes)
|
||||
lines := strings.Split(treeString, "\n")
|
||||
|
||||
newLines := len(lines) - 2
|
||||
if vm.height() >= newLines {
|
||||
nextBufferIndexLowerBound = vm.bufferIndexLowerBound - newLines
|
||||
}
|
||||
|
||||
vm.bufferIndexLowerBound = nextBufferIndexLowerBound
|
||||
|
||||
if vm.TreeIndex > (nextBufferIndexUpperBound - 1) {
|
||||
vm.bufferIndex = 0
|
||||
vm.TreeIndex = nextBufferIndexLowerBound
|
||||
} else {
|
||||
vm.bufferIndex = vm.bufferIndex + newLines
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
|
||||
func (vm *FileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.FileNode) {
|
||||
var visitor func(*filetree.FileNode) error
|
||||
var evaluator func(*filetree.FileNode) bool
|
||||
var dfsCounter int
|
||||
|
||||
visitor = func(curNode *filetree.FileNode) error {
|
||||
if dfsCounter == vm.TreeIndex {
|
||||
node = curNode
|
||||
}
|
||||
dfsCounter++
|
||||
return nil
|
||||
}
|
||||
|
||||
evaluator = func(curNode *filetree.FileNode) bool {
|
||||
regexMatch := true
|
||||
if filterRegex != nil {
|
||||
match := filterRegex.Find([]byte(curNode.Path()))
|
||||
regexMatch = match != nil
|
||||
}
|
||||
return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch
|
||||
}
|
||||
|
||||
err := vm.ModelTree.VisitDepthParentFirst(visitor, evaluator)
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to get node position: %+v", err)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// toggleCollapse will collapse/expand the selected FileNode.
|
||||
func (vm *FileTreeViewModel) toggleCollapse(filterRegex *regexp.Regexp) error {
|
||||
node := vm.getAbsPositionNode(filterRegex)
|
||||
if node != nil && node.Data.FileInfo.IsDir {
|
||||
node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// toggleCollapseAll will collapse/expand the all directories.
|
||||
func (vm *FileTreeViewModel) toggleCollapseAll() error {
|
||||
vm.CollapseAll = !vm.CollapseAll
|
||||
|
||||
visitor := func(curNode *filetree.FileNode) error {
|
||||
curNode.Data.ViewInfo.Collapsed = vm.CollapseAll
|
||||
return nil
|
||||
}
|
||||
|
||||
evaluator := func(curNode *filetree.FileNode) bool {
|
||||
return curNode.Data.FileInfo.IsDir
|
||||
}
|
||||
|
||||
err := vm.ModelTree.VisitDepthChildFirst(visitor, evaluator)
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to propagate tree on toggleCollapseAll: %+v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// toggleCollapse will collapse/expand the selected FileNode.
|
||||
func (vm *FileTreeViewModel) toggleAttributes() error {
|
||||
vm.ShowAttributes = !vm.ShowAttributes
|
||||
return nil
|
||||
}
|
||||
|
||||
// toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
|
||||
func (vm *FileTreeViewModel) toggleShowDiffType(diffType filetree.DiffType) {
|
||||
vm.HiddenDiffTypes[diffType] = !vm.HiddenDiffTypes[diffType]
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering.
|
||||
func (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height int) error {
|
||||
vm.refWidth = width
|
||||
vm.refHeight = height
|
||||
|
||||
// keep the vm selection in parity with the current DiffType selection
|
||||
err := vm.ModelTree.VisitDepthChildFirst(func(node *filetree.FileNode) error {
|
||||
node.Data.ViewInfo.Hidden = vm.HiddenDiffTypes[node.Data.DiffType]
|
||||
visibleChild := false
|
||||
for _, child := range node.Children {
|
||||
if !child.Data.ViewInfo.Hidden {
|
||||
visibleChild = true
|
||||
node.Data.ViewInfo.Hidden = false
|
||||
}
|
||||
}
|
||||
// hide nodes that do not match the current file filter regex (also don't unhide nodes that are already hidden)
|
||||
if filterRegex != nil && !visibleChild && !node.Data.ViewInfo.Hidden {
|
||||
match := filterRegex.FindString(node.Path())
|
||||
node.Data.ViewInfo.Hidden = len(match) == 0
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to propagate vm model tree: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// make a new tree with only visible nodes
|
||||
vm.ViewTree = vm.ModelTree.Copy()
|
||||
err = vm.ViewTree.VisitDepthParentFirst(func(node *filetree.FileNode) error {
|
||||
if node.Data.ViewInfo.Hidden {
|
||||
err1 := vm.ViewTree.RemovePath(node.Path())
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to propagate vm view tree: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render flushes the state objects (file tree) to the pane.
|
||||
func (vm *FileTreeViewModel) Render() error {
|
||||
treeString := vm.ViewTree.StringBetween(vm.bufferIndexLowerBound, vm.bufferIndexUpperBound(), vm.ShowAttributes)
|
||||
lines := strings.Split(treeString, "\n")
|
||||
|
||||
// update the contents
|
||||
vm.mainBuf.Reset()
|
||||
for idx, line := range lines {
|
||||
if idx == vm.bufferIndex {
|
||||
fmt.Fprintln(&vm.mainBuf, Formatting.Selected(vtclean.Clean(line, false)))
|
||||
} else {
|
||||
fmt.Fprintln(&vm.mainBuf, line)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
384
ui/filetree_viewmodel_test.go
Normal file
384
ui/filetree_viewmodel_test.go
Normal file
|
@ -0,0 +1,384 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/fatih/color"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"github.com/wagoodman/dive/filetree"
|
||||
"github.com/wagoodman/dive/image"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const allowTestDataCapture = false
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
func testCaseDataFilePath(name string) string {
|
||||
return filepath.Join("testdata", name+".txt")
|
||||
}
|
||||
|
||||
func helperLoadBytes(t *testing.T) []byte {
|
||||
path := testCaseDataFilePath(t.Name())
|
||||
theBytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to load test data ('%s'): %+v", t.Name(), err)
|
||||
}
|
||||
return theBytes
|
||||
}
|
||||
|
||||
func helperCaptureBytes(t *testing.T, data []byte) {
|
||||
if !allowTestDataCapture {
|
||||
t.Fatalf("cannot capture data in test mode: %s", t.Name())
|
||||
}
|
||||
|
||||
path := testCaseDataFilePath(t.Name())
|
||||
err := ioutil.WriteFile(path, data, 0644)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unable to save test data ('%s'): %+v", t.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func helperCheckDiff(t *testing.T, expected, actual []byte) {
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf(dmp.DiffPrettyText(diffs))
|
||||
t.Errorf("%s: bytes mismatch", t.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func assertTestData(t *testing.T, actualBytes []byte) {
|
||||
path := testCaseDataFilePath(t.Name())
|
||||
if !fileExists(path) {
|
||||
if allowTestDataCapture {
|
||||
helperCaptureBytes(t, actualBytes)
|
||||
} else {
|
||||
t.Fatalf("missing test data: %s", path)
|
||||
}
|
||||
}
|
||||
expectedBytes := helperLoadBytes(t)
|
||||
helperCheckDiff(t, expectedBytes, actualBytes)
|
||||
}
|
||||
|
||||
func initializeTestViewModel(t *testing.T) *FileTreeViewModel {
|
||||
result, err := image.TestLoadDockerImageTar("../.data/test-docker-image.tar")
|
||||
if err != nil {
|
||||
t.Fatalf("%s: unable to fetch analysis: %v", t.Name(), err)
|
||||
}
|
||||
cache := filetree.NewFileTreeCache(result.RefTrees)
|
||||
cache.Build()
|
||||
|
||||
Formatting.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc()
|
||||
|
||||
return NewFileTreeViewModel(filetree.StackTreeRange(result.RefTrees, 0, 0), result.RefTrees, cache)
|
||||
}
|
||||
|
||||
func runTestCase(t *testing.T, vm *FileTreeViewModel, width, height int, filterRegex *regexp.Regexp) {
|
||||
err := vm.Update(filterRegex, width, height)
|
||||
if err != nil {
|
||||
t.Errorf("failed to update viewmodel: %v", err)
|
||||
}
|
||||
|
||||
err = vm.Render()
|
||||
if err != nil {
|
||||
t.Errorf("failed to render viewmodel: %v", err)
|
||||
}
|
||||
|
||||
assertTestData(t, vm.mainBuf.Bytes())
|
||||
}
|
||||
|
||||
func checkError(t *testing.T, err error, message string) {
|
||||
if err != nil {
|
||||
t.Errorf(message+": %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileTreeGoCase(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 1000
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileTreeNoAttributes(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 1000
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = false
|
||||
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileTreeRestrictedHeight(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 20
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = false
|
||||
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileTreeDirCollapse(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 100
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
|
||||
// collapse /bin
|
||||
err := vm.toggleCollapse(nil)
|
||||
checkError(t, err, "unable to collapse /bin")
|
||||
|
||||
moved := vm.CursorDown()
|
||||
if !moved {
|
||||
t.Error("unable to cursor down")
|
||||
}
|
||||
|
||||
moved = vm.CursorDown()
|
||||
if !moved {
|
||||
t.Error("unable to cursor down")
|
||||
}
|
||||
|
||||
// collapse /etc
|
||||
err = vm.toggleCollapse(nil)
|
||||
checkError(t, err, "unable to collapse /etc")
|
||||
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileTreeDirCollapseAll(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 100
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
|
||||
err := vm.toggleCollapseAll()
|
||||
checkError(t, err, "unable to collapse all dir")
|
||||
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileTreeSelectLayer(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 100
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
|
||||
// collapse /bin
|
||||
err := vm.toggleCollapse(nil)
|
||||
checkError(t, err, "unable to collapse /bin")
|
||||
|
||||
// select the next layer, compareMode = layer
|
||||
err = vm.setTreeByLayer(0, 0, 1, 1)
|
||||
if err != nil {
|
||||
t.Errorf("unable to setTreeByLayer: %v", err)
|
||||
}
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileShowAggregateChanges(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 100
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
|
||||
// collapse /bin
|
||||
err := vm.toggleCollapse(nil)
|
||||
checkError(t, err, "unable to collapse /bin")
|
||||
|
||||
// select the next layer, compareMode = layer
|
||||
err = vm.setTreeByLayer(0, 0, 1, 13)
|
||||
checkError(t, err, "unable to setTreeByLayer")
|
||||
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileTreePageDown(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 10
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
err := vm.Update(nil, width, height)
|
||||
checkError(t, err, "unable to update")
|
||||
|
||||
err = vm.PageDown()
|
||||
checkError(t, err, "unable to page down")
|
||||
|
||||
err = vm.PageDown()
|
||||
checkError(t, err, "unable to page down")
|
||||
|
||||
err = vm.PageDown()
|
||||
checkError(t, err, "unable to page down")
|
||||
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileTreePageUp(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 10
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
|
||||
// these operations have a render step for intermediate results, which require at least one update to be done first
|
||||
err := vm.Update(nil, width, height)
|
||||
checkError(t, err, "unable to update")
|
||||
|
||||
err = vm.PageDown()
|
||||
checkError(t, err, "unable to page down")
|
||||
|
||||
err = vm.PageDown()
|
||||
checkError(t, err, "unable to page down")
|
||||
|
||||
err = vm.PageUp()
|
||||
checkError(t, err, "unable to page up")
|
||||
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileTreeDirCursorRight(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 100
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
|
||||
// collapse /bin
|
||||
err := vm.toggleCollapse(nil)
|
||||
checkError(t, err, "unable to collapse /bin")
|
||||
|
||||
moved := vm.CursorDown()
|
||||
if !moved {
|
||||
t.Error("unable to cursor down")
|
||||
}
|
||||
|
||||
moved = vm.CursorDown()
|
||||
if !moved {
|
||||
t.Error("unable to cursor down")
|
||||
}
|
||||
|
||||
// collapse /etc
|
||||
err = vm.toggleCollapse(nil)
|
||||
checkError(t, err, "unable to collapse /etc")
|
||||
|
||||
// expand /etc
|
||||
err = vm.CursorRight(nil)
|
||||
checkError(t, err, "unable to cursor right")
|
||||
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileTreeFilterTree(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 1000
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
|
||||
regex, err := regexp.Compile("network")
|
||||
if err != nil {
|
||||
t.Errorf("could not create filter regex: %+v", err)
|
||||
}
|
||||
|
||||
runTestCase(t, vm, width, height, regex)
|
||||
}
|
||||
|
||||
func TestFileTreeHideAddedRemovedModified(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 100
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
|
||||
// collapse /bin
|
||||
err := vm.toggleCollapse(nil)
|
||||
checkError(t, err, "unable to collapse /bin")
|
||||
|
||||
// select the 7th layer, compareMode = layer
|
||||
err = vm.setTreeByLayer(0, 0, 1, 7)
|
||||
if err != nil {
|
||||
t.Errorf("unable to setTreeByLayer: %v", err)
|
||||
}
|
||||
|
||||
// hide added files
|
||||
vm.toggleShowDiffType(filetree.Added)
|
||||
|
||||
// hide modified files
|
||||
vm.toggleShowDiffType(filetree.Changed)
|
||||
|
||||
// hide removed files
|
||||
vm.toggleShowDiffType(filetree.Removed)
|
||||
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileTreeHideUnmodified(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 100
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
|
||||
// collapse /bin
|
||||
err := vm.toggleCollapse(nil)
|
||||
checkError(t, err, "unable to collapse /bin")
|
||||
|
||||
// select the 7th layer, compareMode = layer
|
||||
err = vm.setTreeByLayer(0, 0, 1, 7)
|
||||
if err != nil {
|
||||
t.Errorf("unable to setTreeByLayer: %v", err)
|
||||
}
|
||||
|
||||
// hide unmodified files
|
||||
vm.toggleShowDiffType(filetree.Unchanged)
|
||||
|
||||
runTestCase(t, vm, width, height, nil)
|
||||
}
|
||||
|
||||
func TestFileTreeHideTypeWithFilter(t *testing.T) {
|
||||
vm := initializeTestViewModel(t)
|
||||
|
||||
width, height := 100, 100
|
||||
vm.Setup(0, height)
|
||||
vm.ShowAttributes = true
|
||||
|
||||
// collapse /bin
|
||||
err := vm.toggleCollapse(nil)
|
||||
checkError(t, err, "unable to collapse /bin")
|
||||
|
||||
// select the 7th layer, compareMode = layer
|
||||
err = vm.setTreeByLayer(0, 0, 1, 7)
|
||||
if err != nil {
|
||||
t.Errorf("unable to setTreeByLayer: %v", err)
|
||||
}
|
||||
|
||||
// hide added files
|
||||
vm.toggleShowDiffType(filetree.Added)
|
||||
|
||||
regex, err := regexp.Compile("saved")
|
||||
if err != nil {
|
||||
t.Errorf("could not create filter regex: %+v", err)
|
||||
}
|
||||
|
||||
runTestCase(t, vm, width, height, regex)
|
||||
}
|
|
@ -1,583 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"github.com/wagoodman/keybinding"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/lunixbochs/vtclean"
|
||||
"github.com/wagoodman/dive/filetree"
|
||||
)
|
||||
|
||||
const (
|
||||
CompareLayer CompareType = iota
|
||||
CompareAll
|
||||
)
|
||||
|
||||
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 {
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
header *gocui.View
|
||||
ModelTree *filetree.FileTree
|
||||
ViewTree *filetree.FileTree
|
||||
RefTrees []*filetree.FileTree
|
||||
cache filetree.TreeCache
|
||||
HiddenDiffTypes []bool
|
||||
TreeIndex uint
|
||||
bufferIndex uint
|
||||
bufferIndexUpperBound uint
|
||||
bufferIndexLowerBound uint
|
||||
|
||||
keybindingToggleCollapse []keybinding.Key
|
||||
keybindingToggleAdded []keybinding.Key
|
||||
keybindingToggleRemoved []keybinding.Key
|
||||
keybindingToggleModified []keybinding.Key
|
||||
keybindingToggleUnchanged []keybinding.Key
|
||||
keybindingPageDown []keybinding.Key
|
||||
keybindingPageUp []keybinding.Key
|
||||
}
|
||||
|
||||
// NewFileTreeView creates a new view object attached the the global [gocui] screen object.
|
||||
func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeView *FileTreeView) {
|
||||
treeView = new(FileTreeView)
|
||||
|
||||
// populate main fields
|
||||
treeView.Name = name
|
||||
treeView.gui = gui
|
||||
treeView.ModelTree = tree
|
||||
treeView.RefTrees = refTrees
|
||||
treeView.cache = cache
|
||||
treeView.HiddenDiffTypes = make([]bool, 4)
|
||||
|
||||
hiddenTypes := viper.GetStringSlice("diff.hide")
|
||||
for _, hType := range hiddenTypes {
|
||||
switch t := strings.ToLower(hType); t {
|
||||
case "added":
|
||||
treeView.HiddenDiffTypes[filetree.Added] = true
|
||||
case "removed":
|
||||
treeView.HiddenDiffTypes[filetree.Removed] = true
|
||||
case "changed":
|
||||
treeView.HiddenDiffTypes[filetree.Changed] = true
|
||||
case "unchanged":
|
||||
treeView.HiddenDiffTypes[filetree.Unchanged] = true
|
||||
default:
|
||||
utils.PrintAndExit(fmt.Sprintf("unknown diff.hide value: %s", t))
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
treeView.keybindingToggleCollapse, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-dir"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
treeView.keybindingToggleAdded, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-added-files"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
treeView.keybindingToggleRemoved, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-removed-files"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
treeView.keybindingToggleModified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-modified-files"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
treeView.keybindingToggleUnchanged, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-unchanged-files"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
treeView.keybindingPageUp, err = keybinding.ParseAll(viper.GetString("keybinding.page-up"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
treeView.keybindingPageDown, err = keybinding.ParseAll(viper.GetString("keybinding.page-down"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
// set view options
|
||||
view.view = v
|
||||
view.view.Editable = false
|
||||
view.view.Wrap = 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
|
||||
}
|
||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorLeft() }); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorRight() }); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, key := range view.keybindingPageUp {
|
||||
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.PageUp() }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range view.keybindingPageDown {
|
||||
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.PageDown() }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range view.keybindingToggleCollapse {
|
||||
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleCollapse() }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range view.keybindingToggleAdded {
|
||||
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Added) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range view.keybindingToggleRemoved {
|
||||
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Removed) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range view.keybindingToggleModified {
|
||||
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Changed) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range view.keybindingToggleUnchanged {
|
||||
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Unchanged) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
view.bufferIndexLowerBound = 0
|
||||
view.bufferIndexUpperBound = view.height() // don't include the header or footer in the view size
|
||||
|
||||
view.Update()
|
||||
view.Render()
|
||||
|
||||
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 {
|
||||
_, height := view.view.Size()
|
||||
return uint(height - 2)
|
||||
}
|
||||
|
||||
// IsVisible indicates if the file tree view pane is currently initialized
|
||||
func (view *FileTreeView) IsVisible() bool {
|
||||
if view == nil {
|
||||
return false
|
||||
}
|
||||
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 {
|
||||
if topTreeStop > len(view.RefTrees)-1 {
|
||||
return fmt.Errorf("invalid layer index given: %d of %d", topTreeStop, len(view.RefTrees)-1)
|
||||
}
|
||||
newTree := view.cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
|
||||
|
||||
// preserve view state on copy
|
||||
visitor := func(node *filetree.FileNode) error {
|
||||
newNode, err := newTree.GetNode(node.Path())
|
||||
if err == nil {
|
||||
newNode.Data.ViewInfo = node.Data.ViewInfo
|
||||
}
|
||||
return nil
|
||||
}
|
||||
view.ModelTree.VisitDepthChildFirst(visitor, nil)
|
||||
|
||||
view.resetCursor()
|
||||
|
||||
view.ModelTree = newTree
|
||||
view.Update()
|
||||
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() {
|
||||
view.TreeIndex--
|
||||
if view.TreeIndex < view.bufferIndexLowerBound {
|
||||
view.bufferIndexUpperBound--
|
||||
view.bufferIndexLowerBound--
|
||||
}
|
||||
|
||||
if view.bufferIndex > 0 {
|
||||
view.bufferIndex--
|
||||
}
|
||||
}
|
||||
|
||||
// doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer.
|
||||
func (view *FileTreeView) doCursorDown() {
|
||||
view.TreeIndex++
|
||||
if view.TreeIndex > view.bufferIndexUpperBound {
|
||||
view.bufferIndexUpperBound++
|
||||
view.bufferIndexLowerBound++
|
||||
}
|
||||
view.bufferIndex++
|
||||
if view.bufferIndex > view.height() {
|
||||
view.bufferIndex = view.height()
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
view.doCursorDown()
|
||||
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 {
|
||||
if view.TreeIndex > 0 {
|
||||
view.doCursorUp()
|
||||
return view.Render()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
|
||||
func (view *FileTreeView) CursorLeft() error {
|
||||
var visitor func(*filetree.FileNode) error
|
||||
var evaluator func(*filetree.FileNode) bool
|
||||
var dfsCounter, newIndex uint
|
||||
oldIndex := view.TreeIndex
|
||||
currentNode := view.getAbsPositionNode()
|
||||
if currentNode == nil {
|
||||
return nil
|
||||
}
|
||||
parentPath := currentNode.Parent.Path()
|
||||
|
||||
visitor = func(curNode *filetree.FileNode) error {
|
||||
if strings.Compare(parentPath, curNode.Path()) == 0 {
|
||||
newIndex = dfsCounter
|
||||
}
|
||||
dfsCounter++
|
||||
return nil
|
||||
}
|
||||
var filterBytes []byte
|
||||
var filterRegex *regexp.Regexp
|
||||
read, err := Views.Filter.view.Read(filterBytes)
|
||||
if read > 0 && err == nil {
|
||||
regex, err := regexp.Compile(string(filterBytes))
|
||||
if err == nil {
|
||||
filterRegex = regex
|
||||
}
|
||||
}
|
||||
|
||||
evaluator = func(curNode *filetree.FileNode) bool {
|
||||
regexMatch := true
|
||||
if filterRegex != nil {
|
||||
match := filterRegex.Find([]byte(curNode.Path()))
|
||||
regexMatch = match != nil
|
||||
}
|
||||
return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch
|
||||
}
|
||||
|
||||
err = view.ModelTree.VisitDepthParentFirst(visitor, evaluator)
|
||||
if err != nil {
|
||||
logrus.Panic(err)
|
||||
}
|
||||
|
||||
view.TreeIndex = newIndex
|
||||
moveIndex := oldIndex - newIndex
|
||||
if newIndex < view.bufferIndexLowerBound {
|
||||
view.bufferIndexUpperBound = view.TreeIndex + view.height()
|
||||
view.bufferIndexLowerBound = view.TreeIndex
|
||||
}
|
||||
|
||||
if view.bufferIndex > moveIndex {
|
||||
view.bufferIndex = view.bufferIndex - moveIndex
|
||||
} else {
|
||||
view.bufferIndex = 0
|
||||
}
|
||||
|
||||
view.Update()
|
||||
return view.Render()
|
||||
}
|
||||
|
||||
// CursorRight descends into directory expanding it if needed
|
||||
func (view *FileTreeView) CursorRight() error {
|
||||
node := view.getAbsPositionNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
if !node.Data.FileInfo.IsDir {
|
||||
return nil
|
||||
}
|
||||
if len(node.Children) == 0 {
|
||||
return nil
|
||||
}
|
||||
if node.Data.ViewInfo.Collapsed {
|
||||
node.Data.ViewInfo.Collapsed = false
|
||||
}
|
||||
view.TreeIndex++
|
||||
if view.TreeIndex > view.bufferIndexUpperBound {
|
||||
view.bufferIndexUpperBound++
|
||||
view.bufferIndexLowerBound++
|
||||
}
|
||||
view.bufferIndex++
|
||||
if view.bufferIndex > view.height() {
|
||||
view.bufferIndex = view.height()
|
||||
}
|
||||
view.Update()
|
||||
return view.Render()
|
||||
}
|
||||
|
||||
// PageDown moves to next page putting the cursor on top
|
||||
func (view *FileTreeView) PageDown() error {
|
||||
nextBufferIndexLowerBound := view.bufferIndexLowerBound + view.height()
|
||||
nextBufferIndexUpperBound := view.bufferIndexUpperBound + view.height()
|
||||
|
||||
treeString := view.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, true)
|
||||
lines := strings.Split(treeString, "\n")
|
||||
|
||||
newLines := uint(len(lines)) - 1
|
||||
if view.height() >= newLines {
|
||||
nextBufferIndexLowerBound = view.bufferIndexLowerBound + newLines
|
||||
nextBufferIndexUpperBound = view.bufferIndexUpperBound + newLines
|
||||
}
|
||||
view.bufferIndexLowerBound = nextBufferIndexLowerBound
|
||||
view.bufferIndexUpperBound = nextBufferIndexUpperBound
|
||||
|
||||
if view.TreeIndex < nextBufferIndexLowerBound {
|
||||
view.bufferIndex = 0
|
||||
view.TreeIndex = nextBufferIndexLowerBound
|
||||
} else {
|
||||
view.bufferIndex = view.bufferIndex - newLines
|
||||
}
|
||||
return view.Render()
|
||||
}
|
||||
|
||||
// PageUp moves to previous page putting the cursor on top
|
||||
func (view *FileTreeView) PageUp() error {
|
||||
nextBufferIndexLowerBound := view.bufferIndexLowerBound - view.height()
|
||||
nextBufferIndexUpperBound := view.bufferIndexUpperBound - view.height()
|
||||
|
||||
treeString := view.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, true)
|
||||
lines := strings.Split(treeString, "\n")
|
||||
|
||||
newLines := uint(len(lines)) - 2
|
||||
if view.height() >= newLines {
|
||||
nextBufferIndexLowerBound = view.bufferIndexLowerBound - newLines
|
||||
nextBufferIndexUpperBound = view.bufferIndexUpperBound - newLines
|
||||
}
|
||||
view.bufferIndexLowerBound = nextBufferIndexLowerBound
|
||||
view.bufferIndexUpperBound = nextBufferIndexUpperBound
|
||||
|
||||
if view.TreeIndex > (nextBufferIndexUpperBound - 1) {
|
||||
view.bufferIndex = 0
|
||||
view.TreeIndex = nextBufferIndexLowerBound
|
||||
} else {
|
||||
view.bufferIndex = view.bufferIndex + newLines
|
||||
}
|
||||
return view.Render()
|
||||
}
|
||||
|
||||
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
|
||||
func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) {
|
||||
var visitor func(*filetree.FileNode) error
|
||||
var evaluator func(*filetree.FileNode) bool
|
||||
var dfsCounter uint
|
||||
|
||||
visitor = func(curNode *filetree.FileNode) error {
|
||||
if dfsCounter == view.TreeIndex {
|
||||
node = curNode
|
||||
}
|
||||
dfsCounter++
|
||||
return nil
|
||||
}
|
||||
var filterBytes []byte
|
||||
var filterRegex *regexp.Regexp
|
||||
read, err := Views.Filter.view.Read(filterBytes)
|
||||
if read > 0 && err == nil {
|
||||
regex, err := regexp.Compile(string(filterBytes))
|
||||
if err == nil {
|
||||
filterRegex = regex
|
||||
}
|
||||
}
|
||||
|
||||
evaluator = func(curNode *filetree.FileNode) bool {
|
||||
regexMatch := true
|
||||
if filterRegex != nil {
|
||||
match := filterRegex.Find([]byte(curNode.Path()))
|
||||
regexMatch = match != nil
|
||||
}
|
||||
return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch
|
||||
}
|
||||
|
||||
err = view.ModelTree.VisitDepthParentFirst(visitor, evaluator)
|
||||
if err != nil {
|
||||
logrus.Panic(err)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// toggleCollapse will collapse/expand the selected FileNode.
|
||||
func (view *FileTreeView) toggleCollapse() error {
|
||||
node := view.getAbsPositionNode()
|
||||
if node != nil {
|
||||
node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed
|
||||
}
|
||||
view.Update()
|
||||
return view.Render()
|
||||
}
|
||||
|
||||
// toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
|
||||
func (view *FileTreeView) toggleShowDiffType(diffType filetree.DiffType) error {
|
||||
view.HiddenDiffTypes[diffType] = !view.HiddenDiffTypes[diffType]
|
||||
|
||||
view.resetCursor()
|
||||
|
||||
Update()
|
||||
Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterRegex will return a regular expression object to match the user's filter input.
|
||||
func filterRegex() *regexp.Regexp {
|
||||
if Views.Filter == nil || Views.Filter.view == nil {
|
||||
return nil
|
||||
}
|
||||
filterString := strings.TrimSpace(Views.Filter.view.Buffer())
|
||||
if len(filterString) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
regex, err := regexp.Compile(filterString)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return regex
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering.
|
||||
func (view *FileTreeView) Update() error {
|
||||
regex := filterRegex()
|
||||
|
||||
// keep the view selection in parity with the current DiffType selection
|
||||
view.ModelTree.VisitDepthChildFirst(func(node *filetree.FileNode) error {
|
||||
node.Data.ViewInfo.Hidden = view.HiddenDiffTypes[node.Data.DiffType]
|
||||
visibleChild := false
|
||||
for _, child := range node.Children {
|
||||
if !child.Data.ViewInfo.Hidden {
|
||||
visibleChild = true
|
||||
}
|
||||
}
|
||||
if regex != nil && !visibleChild {
|
||||
match := regex.FindString(node.Path())
|
||||
node.Data.ViewInfo.Hidden = len(match) == 0
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
// make a new tree with only visible nodes
|
||||
view.ViewTree = view.ModelTree.Copy()
|
||||
view.ViewTree.VisitDepthParentFirst(func(node *filetree.FileNode) error {
|
||||
if node.Data.ViewInfo.Hidden {
|
||||
view.ViewTree.RemovePath(node.Path())
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render flushes the state objects (file tree) to the pane.
|
||||
func (view *FileTreeView) Render() error {
|
||||
treeString := view.ViewTree.StringBetween(view.bufferIndexLowerBound, view.bufferIndexUpperBound, true)
|
||||
lines := strings.Split(treeString, "\n")
|
||||
|
||||
// undo a cursor down that has gone past bottom of the visible tree
|
||||
if view.bufferIndex >= uint(len(lines))-1 {
|
||||
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 {
|
||||
// update the header
|
||||
view.header.Clear()
|
||||
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)))
|
||||
|
||||
// update the contents
|
||||
view.view.Clear()
|
||||
for idx, line := range lines {
|
||||
if uint(idx) == view.bufferIndex {
|
||||
fmt.Fprintln(view.view, Formatting.Selected(vtclean.Clean(line, false)))
|
||||
} else {
|
||||
fmt.Fprintln(view.view, line)
|
||||
}
|
||||
}
|
||||
// todo: should we check error on the view println?
|
||||
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(view.keybindingToggleCollapse[0].String(), "Collapse dir", false) +
|
||||
renderStatusOption(view.keybindingToggleAdded[0].String(), "Added files", !view.HiddenDiffTypes[filetree.Added]) +
|
||||
renderStatusOption(view.keybindingToggleRemoved[0].String(), "Removed files", !view.HiddenDiffTypes[filetree.Removed]) +
|
||||
renderStatusOption(view.keybindingToggleModified[0].String(), "Modified files", !view.HiddenDiffTypes[filetree.Changed]) +
|
||||
renderStatusOption(view.keybindingToggleUnchanged[0].String(), "Unmodified files", !view.HiddenDiffTypes[filetree.Unchanged])
|
||||
}
|
114
ui/filter_controller.go
Normal file
114
ui/filter_controller.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
)
|
||||
|
||||
// FilterController 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 FilterController struct {
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
header *gocui.View
|
||||
headerStr string
|
||||
maxLength int
|
||||
hidden bool
|
||||
}
|
||||
|
||||
// NewFilterController creates a new view object attached the the global [gocui] screen object.
|
||||
func NewFilterController(name string, gui *gocui.Gui) (controller *FilterController) {
|
||||
controller = new(FilterController)
|
||||
|
||||
// populate main fields
|
||||
controller.Name = name
|
||||
controller.gui = gui
|
||||
controller.headerStr = "Path Filter: "
|
||||
controller.hidden = true
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (controller *FilterController) Setup(v *gocui.View, header *gocui.View) error {
|
||||
|
||||
// set controller options
|
||||
controller.view = v
|
||||
controller.maxLength = 200
|
||||
controller.view.Frame = false
|
||||
controller.view.BgColor = gocui.AttrReverse
|
||||
controller.view.Editable = true
|
||||
controller.view.Editor = controller
|
||||
|
||||
controller.header = header
|
||||
controller.header.BgColor = gocui.AttrReverse
|
||||
controller.header.Editable = false
|
||||
controller.header.Wrap = false
|
||||
controller.header.Frame = false
|
||||
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// IsVisible indicates if the filter view pane is currently initialized
|
||||
func (controller *FilterController) IsVisible() bool {
|
||||
if controller == nil {
|
||||
return false
|
||||
}
|
||||
return !controller.hidden
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down in the filter pane (currently indicates nothing).
|
||||
func (controller *FilterController) CursorDown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up in the filter pane (currently indicates nothing).
|
||||
func (controller *FilterController) CursorUp() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Edit intercepts the key press events in the filer view to update the file view in real time.
|
||||
func (controller *FilterController) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
||||
if !controller.IsVisible() {
|
||||
return
|
||||
}
|
||||
|
||||
cx, _ := v.Cursor()
|
||||
ox, _ := v.Origin()
|
||||
limit := ox+cx+1 > controller.maxLength
|
||||
switch {
|
||||
case ch != 0 && mod == 0 && !limit:
|
||||
v.EditWrite(ch)
|
||||
case key == gocui.KeySpace && !limit:
|
||||
v.EditWrite(' ')
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
}
|
||||
if Controllers.Tree != nil {
|
||||
_ = Controllers.Tree.Update()
|
||||
_ = Controllers.Tree.Render()
|
||||
}
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||
func (controller *FilterController) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render flushes the state objects to the screen. Currently this is the users path filter input.
|
||||
func (controller *FilterController) Render() error {
|
||||
controller.gui.Update(func(g *gocui.Gui) error {
|
||||
// render the header
|
||||
_, err := fmt.Fprintln(controller.header, Formatting.Header(controller.headerStr))
|
||||
|
||||
return err
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||
func (controller *FilterController) KeyHelp() string {
|
||||
return Formatting.StatusControlNormal("▏Type to filter the file tree ")
|
||||
}
|
116
ui/filterview.go
116
ui/filterview.go
|
@ -1,116 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
header *gocui.View
|
||||
headerStr string
|
||||
maxLength int
|
||||
hidden bool
|
||||
}
|
||||
|
||||
// NewFilterView creates a new view object attached the the global [gocui] screen object.
|
||||
func NewFilterView(name string, gui *gocui.Gui) (filterView *FilterView) {
|
||||
filterView = new(FilterView)
|
||||
|
||||
// populate main fields
|
||||
filterView.Name = name
|
||||
filterView.gui = gui
|
||||
filterView.headerStr = "Path Filter: "
|
||||
filterView.hidden = true
|
||||
|
||||
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 {
|
||||
|
||||
// set view options
|
||||
view.view = v
|
||||
view.maxLength = 200
|
||||
view.view.Frame = false
|
||||
view.view.BgColor = gocui.AttrReverse
|
||||
view.view.Editable = true
|
||||
view.view.Editor = view
|
||||
|
||||
view.header = header
|
||||
view.header.BgColor = gocui.AttrReverse
|
||||
view.header.Editable = false
|
||||
view.header.Wrap = false
|
||||
view.header.Frame = false
|
||||
|
||||
view.Render()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsVisible indicates if the filter view pane is currently initialized
|
||||
func (view *FilterView) IsVisible() bool {
|
||||
if view == nil {
|
||||
return false
|
||||
}
|
||||
return !view.hidden
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down in the filter pane (currently indicates nothing).
|
||||
func (view *FilterView) CursorDown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up in the filter pane (currently indicates nothing).
|
||||
func (view *FilterView) CursorUp() error {
|
||||
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) {
|
||||
if !view.IsVisible() {
|
||||
return
|
||||
}
|
||||
|
||||
cx, _ := v.Cursor()
|
||||
ox, _ := v.Origin()
|
||||
limit := ox+cx+1 > view.maxLength
|
||||
switch {
|
||||
case ch != 0 && mod == 0 && !limit:
|
||||
v.EditWrite(ch)
|
||||
case key == gocui.KeySpace && !limit:
|
||||
v.EditWrite(' ')
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
}
|
||||
if Views.Tree != nil {
|
||||
Views.Tree.Update()
|
||||
Views.Tree.Render()
|
||||
}
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||
func (view *FilterView) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render flushes the state objects to the screen. Currently this is the users path filter input.
|
||||
func (view *FilterView) Render() error {
|
||||
view.gui.Update(func(g *gocui.Gui) error {
|
||||
// render the header
|
||||
fmt.Fprintln(view.header, Formatting.Header(view.headerStr))
|
||||
|
||||
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 ")
|
||||
}
|
313
ui/layer_controller.go
Normal file
313
ui/layer_controller.go
Normal file
|
@ -0,0 +1,313 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/lunixbochs/vtclean"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/dive/image"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"github.com/wagoodman/keybinding"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LayerController 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 LayerController struct {
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
header *gocui.View
|
||||
LayerIndex int
|
||||
Layers []image.Layer
|
||||
CompareMode CompareType
|
||||
CompareStartIndex int
|
||||
ImageSize uint64
|
||||
|
||||
keybindingCompareAll []keybinding.Key
|
||||
keybindingCompareLayer []keybinding.Key
|
||||
keybindingPageDown []keybinding.Key
|
||||
keybindingPageUp []keybinding.Key
|
||||
}
|
||||
|
||||
// NewLayerController creates a new view object attached the the global [gocui] screen object.
|
||||
func NewLayerController(name string, gui *gocui.Gui, layers []image.Layer) (controller *LayerController) {
|
||||
controller = new(LayerController)
|
||||
|
||||
// populate main fields
|
||||
controller.Name = name
|
||||
controller.gui = gui
|
||||
controller.Layers = layers
|
||||
|
||||
switch mode := viper.GetBool("layer.show-aggregated-changes"); mode {
|
||||
case true:
|
||||
controller.CompareMode = CompareAll
|
||||
case false:
|
||||
controller.CompareMode = CompareLayer
|
||||
default:
|
||||
utils.PrintAndExit(fmt.Sprintf("unknown layer.show-aggregated-changes value: %v", mode))
|
||||
}
|
||||
|
||||
var err error
|
||||
controller.keybindingCompareAll, err = keybinding.ParseAll(viper.GetString("keybinding.compare-all"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
controller.keybindingCompareLayer, err = keybinding.ParseAll(viper.GetString("keybinding.compare-layer"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
controller.keybindingPageUp, err = keybinding.ParseAll(viper.GetString("keybinding.page-up"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
controller.keybindingPageDown, err = keybinding.ParseAll(viper.GetString("keybinding.page-down"))
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (controller *LayerController) Setup(v *gocui.View, header *gocui.View) error {
|
||||
|
||||
// set controller options
|
||||
controller.view = v
|
||||
controller.view.Editable = false
|
||||
controller.view.Wrap = false
|
||||
controller.view.Frame = false
|
||||
|
||||
controller.header = header
|
||||
controller.header.Editable = false
|
||||
controller.header.Wrap = false
|
||||
controller.header.Frame = false
|
||||
|
||||
// set keybindings
|
||||
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, key := range controller.keybindingPageUp {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageUp() }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, key := range controller.keybindingPageDown {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageDown() }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range controller.keybindingCompareLayer {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.setCompareMode(CompareLayer) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range controller.keybindingCompareAll {
|
||||
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.setCompareMode(CompareAll) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// height obtains the height of the current pane (taking into account the lost space due to the header).
|
||||
func (controller *LayerController) height() uint {
|
||||
_, height := controller.view.Size()
|
||||
return uint(height - 1)
|
||||
}
|
||||
|
||||
// IsVisible indicates if the layer view pane is currently initialized.
|
||||
func (controller *LayerController) IsVisible() bool {
|
||||
return controller != nil
|
||||
}
|
||||
|
||||
// PageDown moves to next page putting the cursor on top
|
||||
func (controller *LayerController) PageDown() error {
|
||||
step := int(controller.height()) + 1
|
||||
targetLayerIndex := controller.LayerIndex + step
|
||||
|
||||
if targetLayerIndex > len(controller.Layers) {
|
||||
step -= targetLayerIndex - (len(controller.Layers) - 1)
|
||||
}
|
||||
|
||||
if step > 0 {
|
||||
err := CursorStep(controller.gui, controller.view, step)
|
||||
if err == nil {
|
||||
return controller.SetCursor(controller.LayerIndex + step)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PageUp moves to previous page putting the cursor on top
|
||||
func (controller *LayerController) PageUp() error {
|
||||
step := int(controller.height()) + 1
|
||||
targetLayerIndex := controller.LayerIndex - step
|
||||
|
||||
if targetLayerIndex < 0 {
|
||||
step += targetLayerIndex
|
||||
}
|
||||
|
||||
if step > 0 {
|
||||
err := CursorStep(controller.gui, controller.view, -step)
|
||||
if err == nil {
|
||||
return controller.SetCursor(controller.LayerIndex - step)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down in the layer pane (selecting a higher layer).
|
||||
func (controller *LayerController) CursorDown() error {
|
||||
if controller.LayerIndex < len(controller.Layers) {
|
||||
err := CursorDown(controller.gui, controller.view)
|
||||
if err == nil {
|
||||
return controller.SetCursor(controller.LayerIndex + 1)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up in the layer pane (selecting a lower layer).
|
||||
func (controller *LayerController) CursorUp() error {
|
||||
if controller.LayerIndex > 0 {
|
||||
err := CursorUp(controller.gui, controller.view)
|
||||
if err == nil {
|
||||
return controller.SetCursor(controller.LayerIndex - 1)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCursor resets the cursor and orients the file tree view based on the given layer index.
|
||||
func (controller *LayerController) SetCursor(layer int) error {
|
||||
controller.LayerIndex = layer
|
||||
err := Controllers.Tree.setTreeByLayer(controller.getCompareIndexes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = Controllers.Details.Render()
|
||||
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// currentLayer returns the Layer object currently selected.
|
||||
func (controller *LayerController) currentLayer() image.Layer {
|
||||
return controller.Layers[(len(controller.Layers)-1)-controller.LayerIndex]
|
||||
}
|
||||
|
||||
// setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison.
|
||||
func (controller *LayerController) setCompareMode(compareMode CompareType) error {
|
||||
controller.CompareMode = compareMode
|
||||
Update()
|
||||
Render()
|
||||
return Controllers.Tree.setTreeByLayer(controller.getCompareIndexes())
|
||||
}
|
||||
|
||||
// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode)
|
||||
func (controller *LayerController) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) {
|
||||
bottomTreeStart = controller.CompareStartIndex
|
||||
topTreeStop = controller.LayerIndex
|
||||
|
||||
if controller.LayerIndex == controller.CompareStartIndex {
|
||||
bottomTreeStop = controller.LayerIndex
|
||||
topTreeStart = controller.LayerIndex
|
||||
} else if controller.CompareMode == CompareLayer {
|
||||
bottomTreeStop = controller.LayerIndex - 1
|
||||
topTreeStart = controller.LayerIndex
|
||||
} else {
|
||||
bottomTreeStop = controller.CompareStartIndex
|
||||
topTreeStart = controller.CompareStartIndex + 1
|
||||
}
|
||||
|
||||
return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop
|
||||
}
|
||||
|
||||
// renderCompareBar returns the formatted string for the given layer.
|
||||
func (controller *LayerController) renderCompareBar(layerIdx int) string {
|
||||
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := controller.getCompareIndexes()
|
||||
result := " "
|
||||
|
||||
if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop {
|
||||
result = Formatting.CompareBottom(" ")
|
||||
}
|
||||
if layerIdx >= topTreeStart && layerIdx <= topTreeStop {
|
||||
result = Formatting.CompareTop(" ")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||
func (controller *LayerController) Update() error {
|
||||
controller.ImageSize = 0
|
||||
for idx := 0; idx < len(controller.Layers); idx++ {
|
||||
controller.ImageSize += controller.Layers[idx].Size()
|
||||
}
|
||||
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 (controller *LayerController) Render() error {
|
||||
|
||||
// indicate when selected
|
||||
title := "Layers"
|
||||
if controller.gui.CurrentView() == controller.view {
|
||||
title = "● " + title
|
||||
}
|
||||
|
||||
controller.gui.Update(func(g *gocui.Gui) error {
|
||||
// update header
|
||||
controller.header.Clear()
|
||||
width, _ := g.Size()
|
||||
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
|
||||
// headerStr += fmt.Sprintf("Cmp "+image.LayerFormat, "Layer Digest", "Size", "Command")
|
||||
headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command")
|
||||
_, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false)))
|
||||
|
||||
// update contents
|
||||
controller.view.Clear()
|
||||
for revIdx := len(controller.Layers) - 1; revIdx >= 0; revIdx-- {
|
||||
layer := controller.Layers[revIdx]
|
||||
idx := (len(controller.Layers) - 1) - revIdx
|
||||
|
||||
layerStr := layer.String()
|
||||
compareBar := controller.renderCompareBar(idx)
|
||||
|
||||
if idx == controller.LayerIndex {
|
||||
_, _ = fmt.Fprintln(controller.view, compareBar+" "+Formatting.Selected(layerStr))
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(controller.view, compareBar+" "+layerStr)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||
func (controller *LayerController) KeyHelp() string {
|
||||
return renderStatusOption(controller.keybindingCompareLayer[0].String(), "Show layer changes", controller.CompareMode == CompareLayer) +
|
||||
renderStatusOption(controller.keybindingCompareAll[0].String(), "Show aggregated changes", controller.CompareMode == CompareAll)
|
||||
}
|
242
ui/layerview.go
242
ui/layerview.go
|
@ -1,242 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"github.com/wagoodman/keybinding"
|
||||
"log"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/lunixbochs/vtclean"
|
||||
"github.com/wagoodman/dive/image"
|
||||
"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 {
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
header *gocui.View
|
||||
LayerIndex int
|
||||
Layers []image.Layer
|
||||
CompareMode CompareType
|
||||
CompareStartIndex int
|
||||
ImageSize uint64
|
||||
|
||||
keybindingCompareAll []keybinding.Key
|
||||
keybindingCompareLayer []keybinding.Key
|
||||
}
|
||||
|
||||
// NewDetailsView creates a new view object attached the the global [gocui] screen object.
|
||||
func NewLayerView(name string, gui *gocui.Gui, layers []image.Layer) (layerView *LayerView) {
|
||||
layerView = new(LayerView)
|
||||
|
||||
// populate main fields
|
||||
layerView.Name = name
|
||||
layerView.gui = gui
|
||||
layerView.Layers = layers
|
||||
|
||||
switch mode := viper.GetBool("layer.show-aggregated-changes"); mode {
|
||||
case true:
|
||||
layerView.CompareMode = CompareAll
|
||||
case false:
|
||||
layerView.CompareMode = CompareLayer
|
||||
default:
|
||||
utils.PrintAndExit(fmt.Sprintf("unknown layer.show-aggregated-changes value: %v", mode))
|
||||
}
|
||||
|
||||
var err error
|
||||
layerView.keybindingCompareAll, err = keybinding.ParseAll(viper.GetString("keybinding.compare-all"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
layerView.keybindingCompareLayer, err = keybinding.ParseAll(viper.GetString("keybinding.compare-layer"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
// set view options
|
||||
view.view = v
|
||||
view.view.Editable = false
|
||||
view.view.Wrap = 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
|
||||
}
|
||||
|
||||
for _, key := range view.keybindingCompareLayer {
|
||||
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.setCompareMode(CompareLayer) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range view.keybindingCompareAll {
|
||||
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.setCompareMode(CompareAll) }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return view.Render()
|
||||
}
|
||||
|
||||
// IsVisible indicates if the layer view pane is currently initialized.
|
||||
func (view *LayerView) IsVisible() bool {
|
||||
if view == nil {
|
||||
return false
|
||||
}
|
||||
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 {
|
||||
view.CompareMode = compareMode
|
||||
Update()
|
||||
Render()
|
||||
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) {
|
||||
bottomTreeStart = view.CompareStartIndex
|
||||
topTreeStop = view.LayerIndex
|
||||
|
||||
if view.LayerIndex == view.CompareStartIndex {
|
||||
bottomTreeStop = view.LayerIndex
|
||||
topTreeStart = view.LayerIndex
|
||||
} else if view.CompareMode == CompareLayer {
|
||||
bottomTreeStop = view.LayerIndex - 1
|
||||
topTreeStart = view.LayerIndex
|
||||
} else {
|
||||
bottomTreeStop = view.CompareStartIndex
|
||||
topTreeStart = view.CompareStartIndex + 1
|
||||
}
|
||||
|
||||
return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop
|
||||
}
|
||||
|
||||
// renderCompareBar returns the formatted string for the given layer.
|
||||
func (view *LayerView) renderCompareBar(layerIdx int) string {
|
||||
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := view.getCompareIndexes()
|
||||
result := " "
|
||||
|
||||
if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop {
|
||||
result = Formatting.CompareBottom(" ")
|
||||
}
|
||||
if layerIdx >= topTreeStart && layerIdx <= topTreeStop {
|
||||
result = Formatting.CompareTop(" ")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||
func (view *LayerView) Update() error {
|
||||
view.ImageSize = 0
|
||||
for idx := 0; idx < len(view.Layers); idx++ {
|
||||
view.ImageSize += view.Layers[idx].Size()
|
||||
}
|
||||
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 {
|
||||
|
||||
// indicate when selected
|
||||
title := "Layers"
|
||||
if view.gui.CurrentView() == view.view {
|
||||
title = "● " + title
|
||||
}
|
||||
|
||||
view.gui.Update(func(g *gocui.Gui) error {
|
||||
// update header
|
||||
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)))
|
||||
|
||||
// update contents
|
||||
view.view.Clear()
|
||||
for revIdx := len(view.Layers) - 1; revIdx >= 0; revIdx-- {
|
||||
layer := view.Layers[revIdx]
|
||||
idx := (len(view.Layers) - 1) - revIdx
|
||||
|
||||
layerStr := layer.String()
|
||||
compareBar := view.renderCompareBar(idx)
|
||||
|
||||
if idx == view.LayerIndex {
|
||||
fmt.Fprintln(view.view, compareBar+" "+Formatting.Selected(layerStr))
|
||||
} else {
|
||||
fmt.Fprintln(view.view, compareBar+" "+layerStr)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||
func (view *LayerView) KeyHelp() string {
|
||||
return renderStatusOption(view.keybindingCompareLayer[0].String(), "Show layer changes", view.CompareMode == CompareLayer) +
|
||||
renderStatusOption(view.keybindingCompareAll[0].String(), "Show aggregated changes", view.CompareMode == CompareAll)
|
||||
}
|
76
ui/status_controller.go
Normal file
76
ui/status_controller.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StatusController holds the UI objects and data models for populating the bottom-most pane. Specifically the panel
|
||||
// shows the user a set of possible actions to take in the window and currently selected pane.
|
||||
type StatusController struct {
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
}
|
||||
|
||||
// NewStatusController creates a new view object attached the the global [gocui] screen object.
|
||||
func NewStatusController(name string, gui *gocui.Gui) (controller *StatusController) {
|
||||
controller = new(StatusController)
|
||||
|
||||
// populate main fields
|
||||
controller.Name = name
|
||||
controller.gui = gui
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (controller *StatusController) Setup(v *gocui.View, header *gocui.View) error {
|
||||
|
||||
// set controller options
|
||||
controller.view = v
|
||||
controller.view.Frame = false
|
||||
|
||||
return controller.Render()
|
||||
}
|
||||
|
||||
// IsVisible indicates if the status view pane is currently initialized.
|
||||
func (controller *StatusController) IsVisible() bool {
|
||||
return controller != nil
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
|
||||
func (controller *StatusController) CursorDown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
|
||||
func (controller *StatusController) CursorUp() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||
func (controller *StatusController) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render flushes the state objects to the screen.
|
||||
func (controller *StatusController) Render() error {
|
||||
controller.gui.Update(func(g *gocui.Gui) error {
|
||||
controller.view.Clear()
|
||||
_, _ = fmt.Fprintln(controller.view, controller.KeyHelp()+Controllers.lookup[controller.gui.CurrentView().Name()].KeyHelp()+Formatting.StatusNormal("▏"+strings.Repeat(" ", 1000)))
|
||||
|
||||
return nil
|
||||
})
|
||||
// todo: blerg
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyHelp indicates all the possible global actions a user can take when any pane is selected.
|
||||
func (controller *StatusController) KeyHelp() string {
|
||||
return renderStatusOption(GlobalKeybindings.quit[0].String(), "Quit", false) +
|
||||
renderStatusOption(GlobalKeybindings.toggleView[0].String(), "Switch view", false) +
|
||||
renderStatusOption(GlobalKeybindings.filterView[0].String(), "Filter", Controllers.Filter.IsVisible())
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
"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 {
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
}
|
||||
|
||||
// NewStatusView creates a new view object attached the the global [gocui] screen object.
|
||||
func NewStatusView(name string, gui *gocui.Gui) (statusView *StatusView) {
|
||||
statusView = new(StatusView)
|
||||
|
||||
// populate main fields
|
||||
statusView.Name = name
|
||||
statusView.gui = gui
|
||||
|
||||
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 {
|
||||
|
||||
// set view options
|
||||
view.view = v
|
||||
view.view.Frame = false
|
||||
|
||||
view.Render()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsVisible indicates if the status view pane is currently initialized.
|
||||
func (view *StatusView) IsVisible() bool {
|
||||
if view == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
|
||||
func (view *StatusView) CursorDown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
|
||||
func (view *StatusView) CursorUp() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||
func (view *StatusView) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render flushes the state objects to the screen.
|
||||
func (view *StatusView) Render() error {
|
||||
view.gui.Update(func(g *gocui.Gui) error {
|
||||
view.view.Clear()
|
||||
fmt.Fprintln(view.view, view.KeyHelp()+Views.lookup[view.gui.CurrentView().Name()].KeyHelp()+Formatting.StatusNormal("▏"+strings.Repeat(" ", 1000)))
|
||||
|
||||
return nil
|
||||
})
|
||||
// todo: blerg
|
||||
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(GlobalKeybindings.quit[0].String(), "Quit", false) +
|
||||
renderStatusOption(GlobalKeybindings.toggleView[0].String(), "Switch view", false) +
|
||||
renderStatusOption(GlobalKeybindings.filterView[0].String(), "Filter files", Views.Filter.IsVisible())
|
||||
}
|
36
ui/testdata/TestFileShowAggregateChanges.txt
vendored
Normal file
36
ui/testdata/TestFileShowAggregateChanges.txt
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
|
||||
drwxr-xr-x 0:0 0 B ├── dev
|
||||
drwxr-xr-x 0:0 1.0 kB ├── etc
|
||||
-rw-rw-r-- 0:0 307 B │ ├── group
|
||||
-rw-r--r-- 0:0 127 B │ ├── localtime
|
||||
drwxr-xr-x 0:0 0 B │ ├── network
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-down.d
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-post-down.d
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-pre-up.d
|
||||
drwxr-xr-x 0:0 0 B │ │ └── if-up.d
|
||||
-rw-r--r-- 0:0 340 B │ ├── passwd
|
||||
-rw------- 0:0 243 B │ └── shadow
|
||||
drwxr-xr-x 65534:65534 0 B ├── home
|
||||
drwx------ 0:0 21 kB ├── root
|
||||
drwxr-xr-x 0:0 8.6 kB │ ├── .data
|
||||
-rw-r--r-- 0:0 6.4 kB │ │ ├── saved.again2.txt
|
||||
-rwxrwxr-x 0:0 917 B │ │ ├── tag.sh
|
||||
-rwxr-xr-x 0:0 1.3 kB │ │ └── test.sh
|
||||
-rw-r--r-- 0:0 6.4 kB │ ├── .saved.txt
|
||||
drwxr-xr-x 0:0 19 kB │ ├── example
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── really
|
||||
drwxr-xr-x 0:0 0 B │ │ │ └── nested
|
||||
-r--r--r-- 0:0 6.4 kB │ │ ├── somefile1.txt
|
||||
-rw-r--r-- 0:0 6.4 kB │ │ ├── somefile2.txt
|
||||
-rw-r--r-- 0:0 6.4 kB │ │ └── somefile3.txt
|
||||
-rwxr-xr-x 0:0 6.4 kB │ └── saved.txt
|
||||
-rw-rw-r-- 0:0 6.4 kB ├── somefile.txt
|
||||
drwxrwxrwx 0:0 6.4 kB ├── tmp
|
||||
-rw-r--r-- 0:0 6.4 kB │ └── saved.again1.txt
|
||||
drwxr-xr-x 0:0 0 B ├── usr
|
||||
drwxr-xr-x 1:1 0 B │ └── sbin
|
||||
drwxr-xr-x 0:0 0 B └── var
|
||||
drwxr-xr-x 0:0 0 B ├── spool
|
||||
drwxr-xr-x 8:8 0 B │ └── mail
|
||||
drwxr-xr-x 0:0 0 B └── www
|
||||
|
13
ui/testdata/TestFileTreeDirCollapse.txt
vendored
Normal file
13
ui/testdata/TestFileTreeDirCollapse.txt
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
|
||||
drwxr-xr-x 0:0 0 B ├── dev
|
||||
drwxr-xr-x 0:0 1.0 kB ├─⊕ etc
|
||||
drwxr-xr-x 65534:65534 0 B ├── home
|
||||
drwx------ 0:0 0 B ├── root
|
||||
drwxrwxrwx 0:0 0 B ├── tmp
|
||||
drwxr-xr-x 0:0 0 B ├── usr
|
||||
drwxr-xr-x 1:1 0 B │ └── sbin
|
||||
drwxr-xr-x 0:0 0 B └── var
|
||||
drwxr-xr-x 0:0 0 B ├── spool
|
||||
drwxr-xr-x 8:8 0 B │ └── mail
|
||||
drwxr-xr-x 0:0 0 B └── www
|
||||
|
9
ui/testdata/TestFileTreeDirCollapseAll.txt
vendored
Normal file
9
ui/testdata/TestFileTreeDirCollapseAll.txt
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
|
||||
drwxr-xr-x 0:0 0 B ├── dev
|
||||
drwxr-xr-x 0:0 1.0 kB ├─⊕ etc
|
||||
drwxr-xr-x 65534:65534 0 B ├── home
|
||||
drwx------ 0:0 0 B ├── root
|
||||
drwxrwxrwx 0:0 0 B ├── tmp
|
||||
drwxr-xr-x 0:0 0 B ├─⊕ usr
|
||||
drwxr-xr-x 0:0 0 B └─⊕ var
|
||||
|
22
ui/testdata/TestFileTreeDirCursorRight.txt
vendored
Normal file
22
ui/testdata/TestFileTreeDirCursorRight.txt
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
|
||||
drwxr-xr-x 0:0 0 B ├── dev
|
||||
drwxr-xr-x 0:0 1.0 kB ├── etc
|
||||
-rw-rw-r-- 0:0 307 B │ ├── group
|
||||
-rw-r--r-- 0:0 127 B │ ├── localtime
|
||||
drwxr-xr-x 0:0 0 B │ ├── network
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-down.d
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-post-down.d
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-pre-up.d
|
||||
drwxr-xr-x 0:0 0 B │ │ └── if-up.d
|
||||
-rw-r--r-- 0:0 340 B │ ├── passwd
|
||||
-rw------- 0:0 243 B │ └── shadow
|
||||
drwxr-xr-x 65534:65534 0 B ├── home
|
||||
drwx------ 0:0 0 B ├── root
|
||||
drwxrwxrwx 0:0 0 B ├── tmp
|
||||
drwxr-xr-x 0:0 0 B ├── usr
|
||||
drwxr-xr-x 1:1 0 B │ └── sbin
|
||||
drwxr-xr-x 0:0 0 B └── var
|
||||
drwxr-xr-x 0:0 0 B ├── spool
|
||||
drwxr-xr-x 8:8 0 B │ └── mail
|
||||
drwxr-xr-x 0:0 0 B └── www
|
||||
|
7
ui/testdata/TestFileTreeFilterTree.txt
vendored
Normal file
7
ui/testdata/TestFileTreeFilterTree.txt
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
drwxr-xr-x 0:0 0 B └── etc
|
||||
drwxr-xr-x 0:0 0 B └── network
|
||||
drwxr-xr-x 0:0 0 B ├── if-down.d
|
||||
drwxr-xr-x 0:0 0 B ├── if-post-down.d
|
||||
drwxr-xr-x 0:0 0 B ├── if-pre-up.d
|
||||
drwxr-xr-x 0:0 0 B └── if-up.d
|
||||
|
416
ui/testdata/TestFileTreeGoCase.txt
vendored
Normal file
416
ui/testdata/TestFileTreeGoCase.txt
vendored
Normal file
|
@ -0,0 +1,416 @@
|
|||
drwxr-xr-x 0:0 1.2 MB ├── bin
|
||||
-rwxr-xr-x 0:0 1.1 MB │ ├── [
|
||||
-rwxr-xr-x 0:0 0 B │ ├── [[ → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── acpid → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── add-shell → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── addgroup → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── adduser → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── adjtimex → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ar → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── arch → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── arp → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── arping → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ash → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── awk → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── base64 → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── basename → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── beep → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── blkdiscard → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── blkid → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── blockdev → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── bootchartd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── brctl → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── bunzip2 → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── busybox → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── bzcat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── bzip2 → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── cal → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── cat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chattr → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chgrp → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chmod → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chown → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chpasswd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chpst → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chroot → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chrt → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chvt → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── cksum → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── clear → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── cmp → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── comm → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── conspy → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── cp → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── cpio → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── crond → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── crontab → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── cryptpw → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── cttyhack → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── cut → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── date → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dc → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── deallocvt → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── delgroup → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── deluser → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── depmod → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── devmem → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── df → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dhcprelay → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── diff → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dirname → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dmesg → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dnsd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dnsdomainname → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dos2unix → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dpkg → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dpkg-deb → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── du → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dumpkmap → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── dumpleases → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── echo → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ed → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── egrep → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── eject → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── env → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── envdir → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── envuidgid → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ether-wake → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── expand → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── expr → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── factor → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fakeidentd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fallocate → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── false → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fatattr → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fbset → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fbsplash → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fdflush → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fdformat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fdisk → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fgconsole → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fgrep → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── find → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── findfs → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── flock → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fold → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── free → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── freeramdisk → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fsck → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fsck.minix → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fsfreeze → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fstrim → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fsync → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ftpd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ftpget → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ftpput → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── fuser → bin/[
|
||||
-rwxr-xr-x 0:0 78 kB │ ├── getconf
|
||||
-rwxr-xr-x 0:0 0 B │ ├── getopt → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── getty → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── grep → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── groups → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── gunzip → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── gzip → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── halt → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── hd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── hdparm → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── head → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── hexdump → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── hexedit → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── hostid → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── hostname → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── httpd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── hush → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── hwclock → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── i2cdetect → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── i2cdump → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── i2cget → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── i2cset → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── id → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ifconfig → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ifdown → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ifenslave → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ifplugd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ifup → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── inetd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── init → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── insmod → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── install → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ionice → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── iostat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ip → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ipaddr → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ipcalc → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ipcrm → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ipcs → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── iplink → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ipneigh → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── iproute → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── iprule → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── iptunnel → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── kbd_mode → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── kill → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── killall → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── killall5 → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── klogd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── last → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── less → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── link → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── linux32 → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── linux64 → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── linuxrc → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ln → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── loadfont → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── loadkmap → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── logger → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── login → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── logname → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── logread → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── losetup → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lpd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lpq → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lpr → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ls → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lsattr → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lsmod → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lsof → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lspci → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lsscsi → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lsusb → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lzcat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lzma → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── lzop → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── makedevs → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── makemime → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── man → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── md5sum → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mdev → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mesg → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── microcom → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mkdir → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mkdosfs → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mke2fs → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mkfifo → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mkfs.ext2 → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mkfs.minix → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mkfs.vfat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mknod → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mkpasswd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mkswap → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mktemp → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── modinfo → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── modprobe → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── more → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mount → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mountpoint → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mpstat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mt → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── mv → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nameif → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nanddump → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nandwrite → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nbd-client → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nc → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── netstat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nice → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nl → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nmeter → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nohup → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nproc → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nsenter → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nslookup → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ntpd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── nuke → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── od → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── openvt → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── partprobe → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── passwd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── paste → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── patch → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── pgrep → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── pidof → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ping → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ping6 → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── pipe_progress → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── pivot_root → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── pkill → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── pmap → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── popmaildir → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── poweroff → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── powertop → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── printenv → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── printf → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ps → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── pscan → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── pstree → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── pwd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── pwdx → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── raidautorun → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── rdate → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── rdev → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── readahead → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── readlink → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── readprofile → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── realpath → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── reboot → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── reformime → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── remove-shell → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── renice → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── reset → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── resize → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── resume → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── rev → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── rm → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── rmdir → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── rmmod → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── route → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── rpm → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── rpm2cpio → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── rtcwake → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── run-init → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── run-parts → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── runlevel → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── runsv → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── runsvdir → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── rx → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── script → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── scriptreplay → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sed → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sendmail → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── seq → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── setarch → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── setconsole → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── setfattr → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── setfont → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── setkeycodes → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── setlogcons → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── setpriv → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── setserial → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── setsid → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── setuidgid → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sh → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sha1sum → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sha256sum → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sha3sum → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sha512sum → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── showkey → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── shred → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── shuf → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── slattach → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sleep → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── smemcap → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── softlimit → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sort → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── split → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ssl_client → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── start-stop-daemon → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── stat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── strings → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── stty → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── su → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sulogin → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sum → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sv → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── svc → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── svlogd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── svok → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── swapoff → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── swapon → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── switch_root → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sync → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── sysctl → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── syslogd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── tac → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── tail → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── tar → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── taskset → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── tc → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── tcpsvd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── tee → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── telnet → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── telnetd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── test → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── tftp → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── tftpd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── time → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── timeout → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── top → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── touch → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── tr → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── traceroute → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── traceroute6 → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── true → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── truncate → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── tty → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ttysize → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── tunctl → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ubiattach → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ubidetach → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ubimkvol → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ubirename → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ubirmvol → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ubirsvol → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ubiupdatevol → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── udhcpc → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── udhcpd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── udpsvd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── uevent → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── umount → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── uname → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── unexpand → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── uniq → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── unix2dos → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── unlink → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── unlzma → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── unshare → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── unxz → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── unzip → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── uptime → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── users → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── usleep → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── uudecode → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── uuencode → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── vconfig → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── vi → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── vlock → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── volname → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── w → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── wall → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── watch → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── watchdog → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── wc → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── wget → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── which → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── who → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── whoami → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── whois → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── xargs → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── xxd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── xz → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── xzcat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── yes → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── zcat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ └── zcip → bin/[
|
||||
drwxr-xr-x 0:0 0 B ├── dev
|
||||
drwxr-xr-x 0:0 1.0 kB ├── etc
|
||||
-rw-rw-r-- 0:0 307 B │ ├── group
|
||||
-rw-r--r-- 0:0 127 B │ ├── localtime
|
||||
drwxr-xr-x 0:0 0 B │ ├── network
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-down.d
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-post-down.d
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-pre-up.d
|
||||
drwxr-xr-x 0:0 0 B │ │ └── if-up.d
|
||||
-rw-r--r-- 0:0 340 B │ ├── passwd
|
||||
-rw------- 0:0 243 B │ └── shadow
|
||||
drwxr-xr-x 65534:65534 0 B ├── home
|
||||
drwx------ 0:0 0 B ├── root
|
||||
drwxrwxrwx 0:0 0 B ├── tmp
|
||||
drwxr-xr-x 0:0 0 B ├── usr
|
||||
drwxr-xr-x 1:1 0 B │ └── sbin
|
||||
drwxr-xr-x 0:0 0 B └── var
|
||||
drwxr-xr-x 0:0 0 B ├── spool
|
||||
drwxr-xr-x 8:8 0 B │ └── mail
|
||||
drwxr-xr-x 0:0 0 B └── www
|
||||
|
21
ui/testdata/TestFileTreeHideAddedRemovedModified.txt
vendored
Normal file
21
ui/testdata/TestFileTreeHideAddedRemovedModified.txt
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
|
||||
drwxr-xr-x 0:0 0 B ├── dev
|
||||
drwxr-xr-x 0:0 1.0 kB ├── etc
|
||||
-rw-rw-r-- 0:0 307 B │ ├── group
|
||||
-rw-r--r-- 0:0 127 B │ ├── localtime
|
||||
drwxr-xr-x 0:0 0 B │ ├── network
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-down.d
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-post-down.d
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-pre-up.d
|
||||
drwxr-xr-x 0:0 0 B │ │ └── if-up.d
|
||||
-rw-r--r-- 0:0 340 B │ ├── passwd
|
||||
-rw------- 0:0 243 B │ └── shadow
|
||||
drwxr-xr-x 65534:65534 0 B ├── home
|
||||
drwxrwxrwx 0:0 0 B ├── tmp
|
||||
drwxr-xr-x 0:0 0 B ├── usr
|
||||
drwxr-xr-x 1:1 0 B │ └── sbin
|
||||
drwxr-xr-x 0:0 0 B └── var
|
||||
drwxr-xr-x 0:0 0 B ├── spool
|
||||
drwxr-xr-x 8:8 0 B │ └── mail
|
||||
drwxr-xr-x 0:0 0 B └── www
|
||||
|
1
ui/testdata/TestFileTreeHideTypeWithFilter.txt
vendored
Normal file
1
ui/testdata/TestFileTreeHideTypeWithFilter.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
|
10
ui/testdata/TestFileTreeHideUnmodified.txt
vendored
Normal file
10
ui/testdata/TestFileTreeHideUnmodified.txt
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
drwx------ 0:0 19 kB ├── root
|
||||
drwxr-xr-x 0:0 13 kB │ ├── example
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── really
|
||||
drwxr-xr-x 0:0 0 B │ │ │ └── nested
|
||||
-r--r--r-- 0:0 6.4 kB │ │ ├── somefile1.txt
|
||||
-rw-r--r-- 0:0 6.4 kB │ │ ├── somefile2.txt
|
||||
-rw-r--r-- 0:0 6.4 kB │ │ └── somefile3.txt
|
||||
-rw-r--r-- 0:0 6.4 kB │ └── saved.txt
|
||||
-rw-rw-r-- 0:0 6.4 kB └── somefile.txt
|
||||
|
416
ui/testdata/TestFileTreeNoAttributes.txt
vendored
Normal file
416
ui/testdata/TestFileTreeNoAttributes.txt
vendored
Normal file
|
@ -0,0 +1,416 @@
|
|||
├── bin
|
||||
│ ├── [
|
||||
│ ├── [[ → bin/[
|
||||
│ ├── acpid → bin/[
|
||||
│ ├── add-shell → bin/[
|
||||
│ ├── addgroup → bin/[
|
||||
│ ├── adduser → bin/[
|
||||
│ ├── adjtimex → bin/[
|
||||
│ ├── ar → bin/[
|
||||
│ ├── arch → bin/[
|
||||
│ ├── arp → bin/[
|
||||
│ ├── arping → bin/[
|
||||
│ ├── ash → bin/[
|
||||
│ ├── awk → bin/[
|
||||
│ ├── base64 → bin/[
|
||||
│ ├── basename → bin/[
|
||||
│ ├── beep → bin/[
|
||||
│ ├── blkdiscard → bin/[
|
||||
│ ├── blkid → bin/[
|
||||
│ ├── blockdev → bin/[
|
||||
│ ├── bootchartd → bin/[
|
||||
│ ├── brctl → bin/[
|
||||
│ ├── bunzip2 → bin/[
|
||||
│ ├── busybox → bin/[
|
||||
│ ├── bzcat → bin/[
|
||||
│ ├── bzip2 → bin/[
|
||||
│ ├── cal → bin/[
|
||||
│ ├── cat → bin/[
|
||||
│ ├── chat → bin/[
|
||||
│ ├── chattr → bin/[
|
||||
│ ├── chgrp → bin/[
|
||||
│ ├── chmod → bin/[
|
||||
│ ├── chown → bin/[
|
||||
│ ├── chpasswd → bin/[
|
||||
│ ├── chpst → bin/[
|
||||
│ ├── chroot → bin/[
|
||||
│ ├── chrt → bin/[
|
||||
│ ├── chvt → bin/[
|
||||
│ ├── cksum → bin/[
|
||||
│ ├── clear → bin/[
|
||||
│ ├── cmp → bin/[
|
||||
│ ├── comm → bin/[
|
||||
│ ├── conspy → bin/[
|
||||
│ ├── cp → bin/[
|
||||
│ ├── cpio → bin/[
|
||||
│ ├── crond → bin/[
|
||||
│ ├── crontab → bin/[
|
||||
│ ├── cryptpw → bin/[
|
||||
│ ├── cttyhack → bin/[
|
||||
│ ├── cut → bin/[
|
||||
│ ├── date → bin/[
|
||||
│ ├── dc → bin/[
|
||||
│ ├── dd → bin/[
|
||||
│ ├── deallocvt → bin/[
|
||||
│ ├── delgroup → bin/[
|
||||
│ ├── deluser → bin/[
|
||||
│ ├── depmod → bin/[
|
||||
│ ├── devmem → bin/[
|
||||
│ ├── df → bin/[
|
||||
│ ├── dhcprelay → bin/[
|
||||
│ ├── diff → bin/[
|
||||
│ ├── dirname → bin/[
|
||||
│ ├── dmesg → bin/[
|
||||
│ ├── dnsd → bin/[
|
||||
│ ├── dnsdomainname → bin/[
|
||||
│ ├── dos2unix → bin/[
|
||||
│ ├── dpkg → bin/[
|
||||
│ ├── dpkg-deb → bin/[
|
||||
│ ├── du → bin/[
|
||||
│ ├── dumpkmap → bin/[
|
||||
│ ├── dumpleases → bin/[
|
||||
│ ├── echo → bin/[
|
||||
│ ├── ed → bin/[
|
||||
│ ├── egrep → bin/[
|
||||
│ ├── eject → bin/[
|
||||
│ ├── env → bin/[
|
||||
│ ├── envdir → bin/[
|
||||
│ ├── envuidgid → bin/[
|
||||
│ ├── ether-wake → bin/[
|
||||
│ ├── expand → bin/[
|
||||
│ ├── expr → bin/[
|
||||
│ ├── factor → bin/[
|
||||
│ ├── fakeidentd → bin/[
|
||||
│ ├── fallocate → bin/[
|
||||
│ ├── false → bin/[
|
||||
│ ├── fatattr → bin/[
|
||||
│ ├── fbset → bin/[
|
||||
│ ├── fbsplash → bin/[
|
||||
│ ├── fdflush → bin/[
|
||||
│ ├── fdformat → bin/[
|
||||
│ ├── fdisk → bin/[
|
||||
│ ├── fgconsole → bin/[
|
||||
│ ├── fgrep → bin/[
|
||||
│ ├── find → bin/[
|
||||
│ ├── findfs → bin/[
|
||||
│ ├── flock → bin/[
|
||||
│ ├── fold → bin/[
|
||||
│ ├── free → bin/[
|
||||
│ ├── freeramdisk → bin/[
|
||||
│ ├── fsck → bin/[
|
||||
│ ├── fsck.minix → bin/[
|
||||
│ ├── fsfreeze → bin/[
|
||||
│ ├── fstrim → bin/[
|
||||
│ ├── fsync → bin/[
|
||||
│ ├── ftpd → bin/[
|
||||
│ ├── ftpget → bin/[
|
||||
│ ├── ftpput → bin/[
|
||||
│ ├── fuser → bin/[
|
||||
│ ├── getconf
|
||||
│ ├── getopt → bin/[
|
||||
│ ├── getty → bin/[
|
||||
│ ├── grep → bin/[
|
||||
│ ├── groups → bin/[
|
||||
│ ├── gunzip → bin/[
|
||||
│ ├── gzip → bin/[
|
||||
│ ├── halt → bin/[
|
||||
│ ├── hd → bin/[
|
||||
│ ├── hdparm → bin/[
|
||||
│ ├── head → bin/[
|
||||
│ ├── hexdump → bin/[
|
||||
│ ├── hexedit → bin/[
|
||||
│ ├── hostid → bin/[
|
||||
│ ├── hostname → bin/[
|
||||
│ ├── httpd → bin/[
|
||||
│ ├── hush → bin/[
|
||||
│ ├── hwclock → bin/[
|
||||
│ ├── i2cdetect → bin/[
|
||||
│ ├── i2cdump → bin/[
|
||||
│ ├── i2cget → bin/[
|
||||
│ ├── i2cset → bin/[
|
||||
│ ├── id → bin/[
|
||||
│ ├── ifconfig → bin/[
|
||||
│ ├── ifdown → bin/[
|
||||
│ ├── ifenslave → bin/[
|
||||
│ ├── ifplugd → bin/[
|
||||
│ ├── ifup → bin/[
|
||||
│ ├── inetd → bin/[
|
||||
│ ├── init → bin/[
|
||||
│ ├── insmod → bin/[
|
||||
│ ├── install → bin/[
|
||||
│ ├── ionice → bin/[
|
||||
│ ├── iostat → bin/[
|
||||
│ ├── ip → bin/[
|
||||
│ ├── ipaddr → bin/[
|
||||
│ ├── ipcalc → bin/[
|
||||
│ ├── ipcrm → bin/[
|
||||
│ ├── ipcs → bin/[
|
||||
│ ├── iplink → bin/[
|
||||
│ ├── ipneigh → bin/[
|
||||
│ ├── iproute → bin/[
|
||||
│ ├── iprule → bin/[
|
||||
│ ├── iptunnel → bin/[
|
||||
│ ├── kbd_mode → bin/[
|
||||
│ ├── kill → bin/[
|
||||
│ ├── killall → bin/[
|
||||
│ ├── killall5 → bin/[
|
||||
│ ├── klogd → bin/[
|
||||
│ ├── last → bin/[
|
||||
│ ├── less → bin/[
|
||||
│ ├── link → bin/[
|
||||
│ ├── linux32 → bin/[
|
||||
│ ├── linux64 → bin/[
|
||||
│ ├── linuxrc → bin/[
|
||||
│ ├── ln → bin/[
|
||||
│ ├── loadfont → bin/[
|
||||
│ ├── loadkmap → bin/[
|
||||
│ ├── logger → bin/[
|
||||
│ ├── login → bin/[
|
||||
│ ├── logname → bin/[
|
||||
│ ├── logread → bin/[
|
||||
│ ├── losetup → bin/[
|
||||
│ ├── lpd → bin/[
|
||||
│ ├── lpq → bin/[
|
||||
│ ├── lpr → bin/[
|
||||
│ ├── ls → bin/[
|
||||
│ ├── lsattr → bin/[
|
||||
│ ├── lsmod → bin/[
|
||||
│ ├── lsof → bin/[
|
||||
│ ├── lspci → bin/[
|
||||
│ ├── lsscsi → bin/[
|
||||
│ ├── lsusb → bin/[
|
||||
│ ├── lzcat → bin/[
|
||||
│ ├── lzma → bin/[
|
||||
│ ├── lzop → bin/[
|
||||
│ ├── makedevs → bin/[
|
||||
│ ├── makemime → bin/[
|
||||
│ ├── man → bin/[
|
||||
│ ├── md5sum → bin/[
|
||||
│ ├── mdev → bin/[
|
||||
│ ├── mesg → bin/[
|
||||
│ ├── microcom → bin/[
|
||||
│ ├── mkdir → bin/[
|
||||
│ ├── mkdosfs → bin/[
|
||||
│ ├── mke2fs → bin/[
|
||||
│ ├── mkfifo → bin/[
|
||||
│ ├── mkfs.ext2 → bin/[
|
||||
│ ├── mkfs.minix → bin/[
|
||||
│ ├── mkfs.vfat → bin/[
|
||||
│ ├── mknod → bin/[
|
||||
│ ├── mkpasswd → bin/[
|
||||
│ ├── mkswap → bin/[
|
||||
│ ├── mktemp → bin/[
|
||||
│ ├── modinfo → bin/[
|
||||
│ ├── modprobe → bin/[
|
||||
│ ├── more → bin/[
|
||||
│ ├── mount → bin/[
|
||||
│ ├── mountpoint → bin/[
|
||||
│ ├── mpstat → bin/[
|
||||
│ ├── mt → bin/[
|
||||
│ ├── mv → bin/[
|
||||
│ ├── nameif → bin/[
|
||||
│ ├── nanddump → bin/[
|
||||
│ ├── nandwrite → bin/[
|
||||
│ ├── nbd-client → bin/[
|
||||
│ ├── nc → bin/[
|
||||
│ ├── netstat → bin/[
|
||||
│ ├── nice → bin/[
|
||||
│ ├── nl → bin/[
|
||||
│ ├── nmeter → bin/[
|
||||
│ ├── nohup → bin/[
|
||||
│ ├── nproc → bin/[
|
||||
│ ├── nsenter → bin/[
|
||||
│ ├── nslookup → bin/[
|
||||
│ ├── ntpd → bin/[
|
||||
│ ├── nuke → bin/[
|
||||
│ ├── od → bin/[
|
||||
│ ├── openvt → bin/[
|
||||
│ ├── partprobe → bin/[
|
||||
│ ├── passwd → bin/[
|
||||
│ ├── paste → bin/[
|
||||
│ ├── patch → bin/[
|
||||
│ ├── pgrep → bin/[
|
||||
│ ├── pidof → bin/[
|
||||
│ ├── ping → bin/[
|
||||
│ ├── ping6 → bin/[
|
||||
│ ├── pipe_progress → bin/[
|
||||
│ ├── pivot_root → bin/[
|
||||
│ ├── pkill → bin/[
|
||||
│ ├── pmap → bin/[
|
||||
│ ├── popmaildir → bin/[
|
||||
│ ├── poweroff → bin/[
|
||||
│ ├── powertop → bin/[
|
||||
│ ├── printenv → bin/[
|
||||
│ ├── printf → bin/[
|
||||
│ ├── ps → bin/[
|
||||
│ ├── pscan → bin/[
|
||||
│ ├── pstree → bin/[
|
||||
│ ├── pwd → bin/[
|
||||
│ ├── pwdx → bin/[
|
||||
│ ├── raidautorun → bin/[
|
||||
│ ├── rdate → bin/[
|
||||
│ ├── rdev → bin/[
|
||||
│ ├── readahead → bin/[
|
||||
│ ├── readlink → bin/[
|
||||
│ ├── readprofile → bin/[
|
||||
│ ├── realpath → bin/[
|
||||
│ ├── reboot → bin/[
|
||||
│ ├── reformime → bin/[
|
||||
│ ├── remove-shell → bin/[
|
||||
│ ├── renice → bin/[
|
||||
│ ├── reset → bin/[
|
||||
│ ├── resize → bin/[
|
||||
│ ├── resume → bin/[
|
||||
│ ├── rev → bin/[
|
||||
│ ├── rm → bin/[
|
||||
│ ├── rmdir → bin/[
|
||||
│ ├── rmmod → bin/[
|
||||
│ ├── route → bin/[
|
||||
│ ├── rpm → bin/[
|
||||
│ ├── rpm2cpio → bin/[
|
||||
│ ├── rtcwake → bin/[
|
||||
│ ├── run-init → bin/[
|
||||
│ ├── run-parts → bin/[
|
||||
│ ├── runlevel → bin/[
|
||||
│ ├── runsv → bin/[
|
||||
│ ├── runsvdir → bin/[
|
||||
│ ├── rx → bin/[
|
||||
│ ├── script → bin/[
|
||||
│ ├── scriptreplay → bin/[
|
||||
│ ├── sed → bin/[
|
||||
│ ├── sendmail → bin/[
|
||||
│ ├── seq → bin/[
|
||||
│ ├── setarch → bin/[
|
||||
│ ├── setconsole → bin/[
|
||||
│ ├── setfattr → bin/[
|
||||
│ ├── setfont → bin/[
|
||||
│ ├── setkeycodes → bin/[
|
||||
│ ├── setlogcons → bin/[
|
||||
│ ├── setpriv → bin/[
|
||||
│ ├── setserial → bin/[
|
||||
│ ├── setsid → bin/[
|
||||
│ ├── setuidgid → bin/[
|
||||
│ ├── sh → bin/[
|
||||
│ ├── sha1sum → bin/[
|
||||
│ ├── sha256sum → bin/[
|
||||
│ ├── sha3sum → bin/[
|
||||
│ ├── sha512sum → bin/[
|
||||
│ ├── showkey → bin/[
|
||||
│ ├── shred → bin/[
|
||||
│ ├── shuf → bin/[
|
||||
│ ├── slattach → bin/[
|
||||
│ ├── sleep → bin/[
|
||||
│ ├── smemcap → bin/[
|
||||
│ ├── softlimit → bin/[
|
||||
│ ├── sort → bin/[
|
||||
│ ├── split → bin/[
|
||||
│ ├── ssl_client → bin/[
|
||||
│ ├── start-stop-daemon → bin/[
|
||||
│ ├── stat → bin/[
|
||||
│ ├── strings → bin/[
|
||||
│ ├── stty → bin/[
|
||||
│ ├── su → bin/[
|
||||
│ ├── sulogin → bin/[
|
||||
│ ├── sum → bin/[
|
||||
│ ├── sv → bin/[
|
||||
│ ├── svc → bin/[
|
||||
│ ├── svlogd → bin/[
|
||||
│ ├── svok → bin/[
|
||||
│ ├── swapoff → bin/[
|
||||
│ ├── swapon → bin/[
|
||||
│ ├── switch_root → bin/[
|
||||
│ ├── sync → bin/[
|
||||
│ ├── sysctl → bin/[
|
||||
│ ├── syslogd → bin/[
|
||||
│ ├── tac → bin/[
|
||||
│ ├── tail → bin/[
|
||||
│ ├── tar → bin/[
|
||||
│ ├── taskset → bin/[
|
||||
│ ├── tc → bin/[
|
||||
│ ├── tcpsvd → bin/[
|
||||
│ ├── tee → bin/[
|
||||
│ ├── telnet → bin/[
|
||||
│ ├── telnetd → bin/[
|
||||
│ ├── test → bin/[
|
||||
│ ├── tftp → bin/[
|
||||
│ ├── tftpd → bin/[
|
||||
│ ├── time → bin/[
|
||||
│ ├── timeout → bin/[
|
||||
│ ├── top → bin/[
|
||||
│ ├── touch → bin/[
|
||||
│ ├── tr → bin/[
|
||||
│ ├── traceroute → bin/[
|
||||
│ ├── traceroute6 → bin/[
|
||||
│ ├── true → bin/[
|
||||
│ ├── truncate → bin/[
|
||||
│ ├── tty → bin/[
|
||||
│ ├── ttysize → bin/[
|
||||
│ ├── tunctl → bin/[
|
||||
│ ├── ubiattach → bin/[
|
||||
│ ├── ubidetach → bin/[
|
||||
│ ├── ubimkvol → bin/[
|
||||
│ ├── ubirename → bin/[
|
||||
│ ├── ubirmvol → bin/[
|
||||
│ ├── ubirsvol → bin/[
|
||||
│ ├── ubiupdatevol → bin/[
|
||||
│ ├── udhcpc → bin/[
|
||||
│ ├── udhcpd → bin/[
|
||||
│ ├── udpsvd → bin/[
|
||||
│ ├── uevent → bin/[
|
||||
│ ├── umount → bin/[
|
||||
│ ├── uname → bin/[
|
||||
│ ├── unexpand → bin/[
|
||||
│ ├── uniq → bin/[
|
||||
│ ├── unix2dos → bin/[
|
||||
│ ├── unlink → bin/[
|
||||
│ ├── unlzma → bin/[
|
||||
│ ├── unshare → bin/[
|
||||
│ ├── unxz → bin/[
|
||||
│ ├── unzip → bin/[
|
||||
│ ├── uptime → bin/[
|
||||
│ ├── users → bin/[
|
||||
│ ├── usleep → bin/[
|
||||
│ ├── uudecode → bin/[
|
||||
│ ├── uuencode → bin/[
|
||||
│ ├── vconfig → bin/[
|
||||
│ ├── vi → bin/[
|
||||
│ ├── vlock → bin/[
|
||||
│ ├── volname → bin/[
|
||||
│ ├── w → bin/[
|
||||
│ ├── wall → bin/[
|
||||
│ ├── watch → bin/[
|
||||
│ ├── watchdog → bin/[
|
||||
│ ├── wc → bin/[
|
||||
│ ├── wget → bin/[
|
||||
│ ├── which → bin/[
|
||||
│ ├── who → bin/[
|
||||
│ ├── whoami → bin/[
|
||||
│ ├── whois → bin/[
|
||||
│ ├── xargs → bin/[
|
||||
│ ├── xxd → bin/[
|
||||
│ ├── xz → bin/[
|
||||
│ ├── xzcat → bin/[
|
||||
│ ├── yes → bin/[
|
||||
│ ├── zcat → bin/[
|
||||
│ └── zcip → bin/[
|
||||
├── dev
|
||||
├── etc
|
||||
│ ├── group
|
||||
│ ├── localtime
|
||||
│ ├── network
|
||||
│ │ ├── if-down.d
|
||||
│ │ ├── if-post-down.d
|
||||
│ │ ├── if-pre-up.d
|
||||
│ │ └── if-up.d
|
||||
│ ├── passwd
|
||||
│ └── shadow
|
||||
├── home
|
||||
├── root
|
||||
├── tmp
|
||||
├── usr
|
||||
│ └── sbin
|
||||
└── var
|
||||
├── spool
|
||||
│ └── mail
|
||||
└── www
|
||||
|
11
ui/testdata/TestFileTreePageDown.txt
vendored
Normal file
11
ui/testdata/TestFileTreePageDown.txt
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
-rwxr-xr-x 0:0 0 B │ ├── cat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chat → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chattr → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chgrp → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chmod → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chown → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chpasswd → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chpst → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chroot → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── chrt → bin/[
|
||||
|
11
ui/testdata/TestFileTreePageUp.txt
vendored
Normal file
11
ui/testdata/TestFileTreePageUp.txt
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
-rwxr-xr-x 0:0 0 B │ ├── arch → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── arp → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── arping → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── ash → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── awk → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── base64 → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── basename → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── beep → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── blkdiscard → bin/[
|
||||
-rwxr-xr-x 0:0 0 B │ ├── blkid → bin/[
|
||||
|
22
ui/testdata/TestFileTreeRestrictedHeight.txt
vendored
Normal file
22
ui/testdata/TestFileTreeRestrictedHeight.txt
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
├── bin
|
||||
│ ├── [
|
||||
│ ├── [[ → bin/[
|
||||
│ ├── acpid → bin/[
|
||||
│ ├── add-shell → bin/[
|
||||
│ ├── addgroup → bin/[
|
||||
│ ├── adduser → bin/[
|
||||
│ ├── adjtimex → bin/[
|
||||
│ ├── ar → bin/[
|
||||
│ ├── arch → bin/[
|
||||
│ ├── arp → bin/[
|
||||
│ ├── arping → bin/[
|
||||
│ ├── ash → bin/[
|
||||
│ ├── awk → bin/[
|
||||
│ ├── base64 → bin/[
|
||||
│ ├── basename → bin/[
|
||||
│ ├── beep → bin/[
|
||||
│ ├── blkdiscard → bin/[
|
||||
│ ├── blkid → bin/[
|
||||
│ ├── blockdev → bin/[
|
||||
│ ├── bootchartd → bin/[
|
||||
|
23
ui/testdata/TestFileTreeSelectLayer.txt
vendored
Normal file
23
ui/testdata/TestFileTreeSelectLayer.txt
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
drwxr-xr-x 0:0 1.2 MB ├─⊕ bin
|
||||
drwxr-xr-x 0:0 0 B ├── dev
|
||||
drwxr-xr-x 0:0 1.0 kB ├── etc
|
||||
-rw-rw-r-- 0:0 307 B │ ├── group
|
||||
-rw-r--r-- 0:0 127 B │ ├── localtime
|
||||
drwxr-xr-x 0:0 0 B │ ├── network
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-down.d
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-post-down.d
|
||||
drwxr-xr-x 0:0 0 B │ │ ├── if-pre-up.d
|
||||
drwxr-xr-x 0:0 0 B │ │ └── if-up.d
|
||||
-rw-r--r-- 0:0 340 B │ ├── passwd
|
||||
-rw------- 0:0 243 B │ └── shadow
|
||||
drwxr-xr-x 65534:65534 0 B ├── home
|
||||
drwx------ 0:0 0 B ├── root
|
||||
-rw-rw-r-- 0:0 6.4 kB ├── somefile.txt
|
||||
drwxrwxrwx 0:0 0 B ├── tmp
|
||||
drwxr-xr-x 0:0 0 B ├── usr
|
||||
drwxr-xr-x 1:1 0 B │ └── sbin
|
||||
drwxr-xr-x 0:0 0 B └── var
|
||||
drwxr-xr-x 0:0 0 B ├── spool
|
||||
drwxr-xr-x 8:8 0 B │ └── mail
|
||||
drwxr-xr-x 0:0 0 B └── www
|
||||
|
204
ui/ui.go
204
ui/ui.go
|
@ -2,7 +2,6 @@ package ui
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -11,7 +10,6 @@ import (
|
|||
"github.com/wagoodman/dive/image"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"github.com/wagoodman/keybinding"
|
||||
"log"
|
||||
)
|
||||
|
||||
const debug = false
|
||||
|
@ -20,17 +18,17 @@ const debug = false
|
|||
// var onExit func()
|
||||
|
||||
// debugPrint writes the given string to the debug pane (if the debug pane is enabled)
|
||||
func debugPrint(s string) {
|
||||
if debug && Views.Tree != nil && Views.Tree.gui != nil {
|
||||
v, _ := Views.Tree.gui.View("debug")
|
||||
if v != nil {
|
||||
if len(v.BufferLines()) > 20 {
|
||||
v.Clear()
|
||||
}
|
||||
_, _ = fmt.Fprintln(v, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
// func debugPrint(s string) {
|
||||
// if Controllers.Tree != nil && Controllers.Tree.gui != nil {
|
||||
// v, _ := Controllers.Tree.gui.View("debug")
|
||||
// if v != nil {
|
||||
// if len(v.BufferLines()) > 20 {
|
||||
// v.Clear()
|
||||
// }
|
||||
// _, _ = fmt.Fprintln(v, s)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Formatting defines standard functions for formatting UI sections.
|
||||
var Formatting struct {
|
||||
|
@ -44,13 +42,13 @@ var Formatting struct {
|
|||
CompareBottom func(...interface{}) string
|
||||
}
|
||||
|
||||
// Views contains all rendered UI panes.
|
||||
var Views struct {
|
||||
Tree *FileTreeView
|
||||
Layer *LayerView
|
||||
Status *StatusView
|
||||
Filter *FilterView
|
||||
Details *DetailsView
|
||||
// Controllers contains all rendered UI panes.
|
||||
var Controllers struct {
|
||||
Tree *FileTreeController
|
||||
Layer *LayerController
|
||||
Status *StatusController
|
||||
Filter *FilterController
|
||||
Details *DetailsController
|
||||
lookup map[string]View
|
||||
}
|
||||
|
||||
|
@ -60,6 +58,8 @@ var GlobalKeybindings struct {
|
|||
filterView []keybinding.Key
|
||||
}
|
||||
|
||||
var lastX, lastY int
|
||||
|
||||
// View defines the a renderable terminal screen pane.
|
||||
type View interface {
|
||||
Setup(*gocui.View, *gocui.View) error
|
||||
|
@ -72,14 +72,12 @@ type View interface {
|
|||
}
|
||||
|
||||
// toggleView switches between the file view and the layer view and re-renders the screen.
|
||||
func toggleView(g *gocui.Gui, v *gocui.View) error {
|
||||
if v == nil || v.Name() == Views.Layer.Name {
|
||||
_, err := g.SetCurrentView(Views.Tree.Name)
|
||||
Update()
|
||||
Render()
|
||||
return err
|
||||
func toggleView(g *gocui.Gui, v *gocui.View) (err error) {
|
||||
if v == nil || v.Name() == Controllers.Layer.Name {
|
||||
_, err = g.SetCurrentView(Controllers.Tree.Name)
|
||||
} else {
|
||||
_, err = g.SetCurrentView(Controllers.Layer.Name)
|
||||
}
|
||||
_, err := g.SetCurrentView(Views.Layer.Name)
|
||||
Update()
|
||||
Render()
|
||||
return err
|
||||
|
@ -88,21 +86,24 @@ func toggleView(g *gocui.Gui, v *gocui.View) error {
|
|||
// toggleFilterView shows/hides the file tree filter pane.
|
||||
func toggleFilterView(g *gocui.Gui, v *gocui.View) error {
|
||||
// delete all user input from the tree view
|
||||
Views.Filter.view.Clear()
|
||||
Views.Filter.view.SetCursor(0, 0)
|
||||
Controllers.Filter.view.Clear()
|
||||
err := Controllers.Filter.view.SetCursor(0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// toggle hiding
|
||||
Views.Filter.hidden = !Views.Filter.hidden
|
||||
Controllers.Filter.hidden = !Controllers.Filter.hidden
|
||||
|
||||
if !Views.Filter.hidden {
|
||||
_, err := g.SetCurrentView(Views.Filter.Name)
|
||||
if !Controllers.Filter.hidden {
|
||||
_, err := g.SetCurrentView(Controllers.Filter.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Update()
|
||||
Render()
|
||||
} else {
|
||||
toggleView(g, v)
|
||||
return toggleView(g, v)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -110,31 +111,29 @@ func toggleFilterView(g *gocui.Gui, v *gocui.View) error {
|
|||
|
||||
// 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 {
|
||||
cx, cy := v.Cursor()
|
||||
|
||||
// if there isn't a next line
|
||||
line, err := v.Line(cy + 1)
|
||||
if err != nil {
|
||||
// todo: handle error
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return errors.New("unable to move cursor down, empty line")
|
||||
}
|
||||
if err := v.SetCursor(cx, cy+1); err != nil {
|
||||
ox, oy := v.Origin()
|
||||
if err := v.SetOrigin(ox, oy+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return CursorStep(g, v, 1)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
ox, oy := v.Origin()
|
||||
return CursorStep(g, v, -1)
|
||||
}
|
||||
|
||||
// Moves the cursor the given step distance, setting the origin to the new cursor line
|
||||
func CursorStep(g *gocui.Gui, v *gocui.View, step int) error {
|
||||
cx, cy := v.Cursor()
|
||||
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
|
||||
if err := v.SetOrigin(ox, oy-1); err != nil {
|
||||
|
||||
// if there isn't a next line
|
||||
line, err := v.Line(cy + step)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return errors.New("unable to move the cursor, empty line")
|
||||
}
|
||||
if err := v.SetCursor(cx, cy+step); err != nil {
|
||||
ox, oy := v.Origin()
|
||||
if err := v.SetOrigin(ox, oy+step); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -179,7 +178,7 @@ func isNewView(errs ...error) bool {
|
|||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if err != nil && err != gocui.ErrUnknownView {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -192,6 +191,14 @@ 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()
|
||||
var resized bool
|
||||
if maxX != lastX {
|
||||
resized = true
|
||||
}
|
||||
if maxY != lastY {
|
||||
resized = true
|
||||
}
|
||||
lastX, lastY = maxX, maxY
|
||||
fileTreeSplitRatio := viper.GetFloat64("filetree.pane-width")
|
||||
if fileTreeSplitRatio >= 1 || fileTreeSplitRatio <= 0 {
|
||||
logrus.Errorf("invalid config value: 'filetree.pane-width' should be 0 < value < 1, given '%v'", fileTreeSplitRatio)
|
||||
|
@ -212,7 +219,7 @@ func layout(g *gocui.Gui) error {
|
|||
statusBarIndex := 1
|
||||
filterBarIndex := 2
|
||||
|
||||
layersHeight := len(Views.Layer.Layers) + headerRows + 1 // layers + header + base image layer row
|
||||
layersHeight := len(Controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row
|
||||
maxLayerHeight := int(0.75 * float64(maxY))
|
||||
if layersHeight > maxLayerHeight {
|
||||
layersHeight = maxLayerHeight
|
||||
|
@ -221,7 +228,7 @@ func layout(g *gocui.Gui) error {
|
|||
var view, header *gocui.View
|
||||
var viewErr, headerErr, err error
|
||||
|
||||
if Views.Filter.hidden {
|
||||
if Controllers.Filter.hidden {
|
||||
bottomRows--
|
||||
filterBarHeight = 0
|
||||
}
|
||||
|
@ -236,43 +243,48 @@ func layout(g *gocui.Gui) error {
|
|||
}
|
||||
|
||||
// Layers
|
||||
view, viewErr = g.SetView(Views.Layer.Name, -1, -1+headerRows, splitCols, layersHeight)
|
||||
header, headerErr = g.SetView(Views.Layer.Name+"header", -1, -1, splitCols, headerRows)
|
||||
view, viewErr = g.SetView(Controllers.Layer.Name, -1, -1+headerRows, splitCols, layersHeight)
|
||||
header, headerErr = g.SetView(Controllers.Layer.Name+"header", -1, -1, splitCols, headerRows)
|
||||
if isNewView(viewErr, headerErr) {
|
||||
Views.Layer.Setup(view, header)
|
||||
_ = Controllers.Layer.Setup(view, header)
|
||||
|
||||
if _, err = g.SetCurrentView(Views.Layer.Name); err != nil {
|
||||
if _, err = g.SetCurrentView(Controllers.Layer.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
// since we are selecting the view, we should rerender to indicate it is selected
|
||||
Views.Layer.Render()
|
||||
_ = Controllers.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)
|
||||
view, viewErr = g.SetView(Controllers.Details.Name, -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows)
|
||||
header, headerErr = g.SetView(Controllers.Details.Name+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows)
|
||||
if isNewView(viewErr, headerErr) {
|
||||
Views.Details.Setup(view, header)
|
||||
_ = Controllers.Details.Setup(view, header)
|
||||
}
|
||||
|
||||
// Filetree
|
||||
view, viewErr = g.SetView(Views.Tree.Name, splitCols, -1+headerRows, debugCols, maxY-bottomRows)
|
||||
header, headerErr = g.SetView(Views.Tree.Name+"header", splitCols, -1, debugCols, headerRows)
|
||||
if isNewView(viewErr, headerErr) {
|
||||
Views.Tree.Setup(view, header)
|
||||
offset := 0
|
||||
if !Controllers.Tree.vm.ShowAttributes {
|
||||
offset = 1
|
||||
}
|
||||
view, viewErr = g.SetView(Controllers.Tree.Name, splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows)
|
||||
header, headerErr = g.SetView(Controllers.Tree.Name+"header", splitCols, -1, debugCols, headerRows-offset)
|
||||
if isNewView(viewErr, headerErr) {
|
||||
_ = Controllers.Tree.Setup(view, header)
|
||||
}
|
||||
_ = Controllers.Tree.onLayoutChange(resized)
|
||||
|
||||
// Status Bar
|
||||
view, viewErr = g.SetView(Views.Status.Name, -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1))
|
||||
view, viewErr = g.SetView(Controllers.Status.Name, -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1))
|
||||
if isNewView(viewErr, headerErr) {
|
||||
Views.Status.Setup(view, nil)
|
||||
_ = Controllers.Status.Setup(view, nil)
|
||||
}
|
||||
|
||||
// Filter Bar
|
||||
view, viewErr = g.SetView(Views.Filter.Name, len(Views.Filter.headerStr)-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1))
|
||||
header, headerErr = g.SetView(Views.Filter.Name+"header", -1, maxY-filterBarHeight-filterBarIndex, len(Views.Filter.headerStr), maxY-(filterBarIndex-1))
|
||||
view, viewErr = g.SetView(Controllers.Filter.Name, len(Controllers.Filter.headerStr)-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1))
|
||||
header, headerErr = g.SetView(Controllers.Filter.Name+"header", -1, maxY-filterBarHeight-filterBarIndex, len(Controllers.Filter.headerStr), maxY-(filterBarIndex-1))
|
||||
if isNewView(viewErr, headerErr) {
|
||||
Views.Filter.Setup(view, header)
|
||||
_ = Controllers.Filter.Setup(view, header)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -280,16 +292,16 @@ func layout(g *gocui.Gui) error {
|
|||
|
||||
// Update refreshes the state objects for future rendering.
|
||||
func Update() {
|
||||
for _, view := range Views.lookup {
|
||||
view.Update()
|
||||
for _, view := range Controllers.lookup {
|
||||
_ = view.Update()
|
||||
}
|
||||
}
|
||||
|
||||
// Render flushes the state objects to the screen.
|
||||
func Render() {
|
||||
for _, view := range Views.lookup {
|
||||
for _, view := range Controllers.lookup {
|
||||
if view.IsVisible() {
|
||||
view.Render()
|
||||
_ = view.Render()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -297,9 +309,9 @@ func Render() {
|
|||
// renderStatusOption formats key help bindings-to-title pairs.
|
||||
func renderStatusOption(control, title string, selected bool) string {
|
||||
if selected {
|
||||
return Formatting.StatusSelected("▏") + Formatting.StatusControlSelected(control) + Formatting.StatusSelected(" "+title+" ")
|
||||
return Formatting.StatusSelected("▏") + Formatting.StatusControlSelected(control) + Formatting.StatusSelected(" "+title+" ")
|
||||
} else {
|
||||
return Formatting.StatusNormal("▏") + Formatting.StatusControlNormal(control) + Formatting.StatusNormal(" "+title+" ")
|
||||
return Formatting.StatusNormal("▏") + Formatting.StatusControlNormal(control) + Formatting.StatusNormal(" "+title+" ")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -318,40 +330,40 @@ func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) {
|
|||
var err error
|
||||
GlobalKeybindings.quit, err = keybinding.ParseAll(viper.GetString("keybinding.quit"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
logrus.Error(err)
|
||||
}
|
||||
GlobalKeybindings.toggleView, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-view"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
logrus.Error(err)
|
||||
}
|
||||
GlobalKeybindings.filterView, err = keybinding.ParseAll(viper.GetString("keybinding.filter-files"))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
g, err := gocui.NewGui(gocui.OutputNormal)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
logrus.Error(err)
|
||||
}
|
||||
utils.SetUi(g)
|
||||
defer g.Close()
|
||||
|
||||
Views.lookup = make(map[string]View)
|
||||
Controllers.lookup = make(map[string]View)
|
||||
|
||||
Views.Layer = NewLayerView("side", g, analysis.Layers)
|
||||
Views.lookup[Views.Layer.Name] = Views.Layer
|
||||
Controllers.Layer = NewLayerController("side", g, analysis.Layers)
|
||||
Controllers.lookup[Controllers.Layer.Name] = Controllers.Layer
|
||||
|
||||
Views.Tree = NewFileTreeView("main", g, filetree.StackTreeRange(analysis.RefTrees, 0, 0), analysis.RefTrees, cache)
|
||||
Views.lookup[Views.Tree.Name] = Views.Tree
|
||||
Controllers.Tree = NewFileTreeController("main", g, filetree.StackTreeRange(analysis.RefTrees, 0, 0), analysis.RefTrees, cache)
|
||||
Controllers.lookup[Controllers.Tree.Name] = Controllers.Tree
|
||||
|
||||
Views.Status = NewStatusView("status", g)
|
||||
Views.lookup[Views.Status.Name] = Views.Status
|
||||
Controllers.Status = NewStatusController("status", g)
|
||||
Controllers.lookup[Controllers.Status.Name] = Controllers.Status
|
||||
|
||||
Views.Filter = NewFilterView("command", g)
|
||||
Views.lookup[Views.Filter.Name] = Views.Filter
|
||||
Controllers.Filter = NewFilterController("command", g)
|
||||
Controllers.lookup[Controllers.Filter.Name] = Controllers.Filter
|
||||
|
||||
Views.Details = NewDetailsView("details", g, analysis.Efficiency, analysis.Inefficiencies)
|
||||
Views.lookup[Views.Details.Name] = Views.Details
|
||||
Controllers.Details = NewDetailsController("details", g, analysis.Efficiency, analysis.Inefficiencies)
|
||||
Controllers.lookup[Controllers.Details.Name] = Controllers.Details
|
||||
|
||||
g.Cursor = false
|
||||
//g.Mouse = true
|
||||
|
@ -368,11 +380,11 @@ func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) {
|
|||
Render()
|
||||
|
||||
if err := keyBindings(g); err != nil {
|
||||
log.Panicln(err)
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
|
||||
log.Panicln(err)
|
||||
logrus.Error(err)
|
||||
}
|
||||
utils.Exit(0)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package utils
|
|||
import (
|
||||
"fmt"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/k0kubun/go-ansi"
|
||||
"github.com/sirupsen/logrus"
|
||||
"os"
|
||||
)
|
||||
|
@ -31,5 +30,4 @@ func Cleanup() {
|
|||
if ui != nil {
|
||||
ui.Close()
|
||||
}
|
||||
ansi.CursorShow()
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type progressBar struct {
|
||||
width int
|
||||
percent int
|
||||
rawTotal int64
|
||||
rawCurrent int64
|
||||
}
|
||||
|
||||
func NewProgressBar(total int64, width int) *progressBar {
|
||||
return &progressBar{
|
||||
rawTotal: total,
|
||||
width: width,
|
||||
}
|
||||
}
|
||||
|
||||
func (pb *progressBar) Done() {
|
||||
pb.rawCurrent = pb.rawTotal
|
||||
pb.percent = 100
|
||||
}
|
||||
|
||||
func (pb *progressBar) Update(currentValue int64) (hasChanged bool) {
|
||||
pb.rawCurrent = currentValue
|
||||
percent := int(100.0 * (float64(pb.rawCurrent) / float64(pb.rawTotal)))
|
||||
if percent != pb.percent {
|
||||
hasChanged = true
|
||||
}
|
||||
pb.percent = percent
|
||||
return hasChanged
|
||||
}
|
||||
|
||||
func (pb *progressBar) String() string {
|
||||
done := int((pb.percent * pb.width) / 100.0)
|
||||
if done > pb.width {
|
||||
done = pb.width
|
||||
}
|
||||
todo := pb.width - done
|
||||
if todo < 0 {
|
||||
todo = 0
|
||||
}
|
||||
head := 1
|
||||
|
||||
return "[" + strings.Repeat("=", done) + strings.Repeat(">", head) + strings.Repeat(" ", todo) + "]" + fmt.Sprintf(" %d %% (%d/%d)", pb.percent, pb.rawCurrent, pb.rawTotal)
|
||||
}
|
Loading…
Reference in a new issue