summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Goodman <wagoodman@users.noreply.github.com>2019-10-09 08:46:47 -0400
committerGitHub <noreply@github.com>2019-10-09 08:46:47 -0400
commit9cebcfd6903dfe207285c08182ae617184f59c1a (patch)
tree82c4d0abc7827969279116af90a04acb6bd61e95
parent07af6d785b64c0cea3b2bdc344d3ad332c5a2675 (diff)
parent0e49dd0fec1000fbb3f44f97eb570408ff17ff6c (diff)
Merge pull request #234 from wagoodman/add-podman-support
Add initial Podman support
-rw-r--r--.circleci/config.yml6
-rw-r--r--.goreleaser.yml15
-rw-r--r--Dockerfile.full11
-rw-r--r--Dockerfile.slim3
-rw-r--r--Makefile26
-rw-r--r--README.md27
-rw-r--r--cmd/analyze.go17
-rw-r--r--cmd/build.go10
-rw-r--r--cmd/root.go22
-rw-r--r--dive/filetree/cache.go34
-rw-r--r--dive/filetree/efficiency.go15
-rw-r--r--dive/filetree/file_info.go69
-rw-r--r--dive/filetree/file_tree.go5
-rw-r--r--dive/filetree/file_tree_test.go5
-rw-r--r--dive/get_analyzer.go12
-rw-r--r--dive/get_image_handler.go44
-rw-r--r--dive/image/analyzer.go5
-rw-r--r--dive/image/docker/analyzer.go272
-rw-r--r--dive/image/docker/build.go27
-rw-r--r--dive/image/docker/cli.go31
-rw-r--r--dive/image/docker/config.go (renamed from dive/image/docker/image_config.go)19
-rw-r--r--dive/image/docker/image_archive.go187
-rw-r--r--dive/image/docker/image_archive_analysis_test.go43
-rw-r--r--dive/image/docker/layer.go91
-rw-r--r--dive/image/docker/manifest.go (renamed from dive/image/docker/image_manifest.go)6
-rw-r--r--dive/image/docker/resolver.go102
-rw-r--r--dive/image/docker/testing.go25
-rw-r--r--dive/image/image.go39
-rw-r--r--dive/image/layer.go40
-rw-r--r--dive/image/podman/build.go29
-rw-r--r--dive/image/podman/cli.go33
-rw-r--r--dive/image/podman/image_directory.go128
-rw-r--r--dive/image/podman/layer.go78
-rw-r--r--dive/image/podman/resolver_linux.go130
-rw-r--r--dive/image/podman/resolver_notlinux.go22
-rw-r--r--dive/image/resolver.go6
-rw-r--r--go.mod28
-rw-r--r--go.sum615
-rw-r--r--runtime/ci/evaluator_test.go7
-rw-r--r--runtime/export/export.go63
-rw-r--r--runtime/export/export_test.go27
-rw-r--r--runtime/export/file_reference.go7
-rw-r--r--runtime/export/image.go8
-rw-r--r--runtime/export/layer.go9
-rw-r--r--runtime/options.go2
-rw-r--r--runtime/run.go94
-rw-r--r--runtime/ui/details_controller.go40
-rw-r--r--runtime/ui/filetree_controller.go31
-rw-r--r--runtime/ui/filetree_viewmodel.go30
-rw-r--r--runtime/ui/filetree_viewmodel_test.go19
-rw-r--r--runtime/ui/filter_controller.go6
-rw-r--r--runtime/ui/layer_controller.go41
-rw-r--r--runtime/ui/status_controller.go9
-rw-r--r--runtime/ui/ui.go151
-rw-r--r--utils/docker.go37
-rw-r--r--utils/exit.go34
-rw-r--r--utils/format.go12
57 files changed, 1939 insertions, 965 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 4afc0ff..74ad8fd 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -12,7 +12,7 @@ jobs:
- restore_cache:
keys:
- golang-1.11-{{ checksum "go.sum" }}
- - run: make setup
+ - run: make setup-ci
- save_cache:
key: golang-1.11-{{ checksum "go.sum" }}
paths:
@@ -32,7 +32,7 @@ jobs:
- restore_cache:
keys:
- golang-1.12-{{ checksum "go.sum" }}
- - run: make setup
+ - run: make setup-ci
- save_cache:
key: golang-1.12-{{ checksum "go.sum" }}
paths:
@@ -52,7 +52,7 @@ jobs:
- restore_cache:
keys:
- golang-1.13-{{ checksum "go.sum" }}
- - run: make setup
+ - run: make setup-ci
- save_cache:
key: golang-1.13-{{ checksum "go.sum" }}
paths:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index f37ec1a..4f6cd02 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -3,8 +3,6 @@ release:
builds:
- binary: dive
- env:
- - CGO_ENABLED=0
goos:
- windows
- darwin
@@ -17,19 +15,6 @@ dockers:
-
binaries:
- dive
- dockerfile: Dockerfile.slim
- image_templates:
- - "wagoodman/dive:{{ .Tag }}-slim"
- - "wagoodman/dive:v{{ .Major }}-slim"
- - "wagoodman/dive:v{{ .Major }}.{{ .Minor }}-slim"
- - "wagoodman/dive:slim"
- - "quay.io/wagoodman/dive:{{ .Tag }}-slim"
- - "quay.io/wagoodman/dive:v{{ .Major }}-slim"
- - "quay.io/wagoodman/dive:v{{ .Major }}.{{ .Minor }}-slim"
- - "quay.io/wagoodman/dive:slim"
- -
- binaries:
- - dive
dockerfile: Dockerfile
image_templates:
- "wagoodman/dive:{{ .Tag }}"
diff --git a/Dockerfile.full b/Dockerfile.full
index a6e7b8d..600c887 100644
--- a/Dockerfile.full
+++ b/Dockerfile.full
@@ -1,10 +1,7 @@
-FROM alpine:3.10 AS build
+FROM alpine:3.10
ARG DOCKER_CLI_VERSION="19.03.1"
-RUN apk --update add curl \
- && curl -L https://download.docker.com/linux/static/stable/x86_64/docker-$DOCKER_CLI_VERSION.tgz | tar -xzf - docker/docker --strip-component=1 -C /tmp
-
-FROM scratch
-COPY --from=build /tmp/docker /
+RUN apk --update add curl &&\
+ curl -L https://download.docker.com/linux/static/stable/x86_64/docker-$DOCKER_CLI_VERSION.tgz | \
+ tar -xzf - docker/docker --strip-component=1 -C /tmp
COPY dist/dive_linux_amd64/dive /
-ENV PATH /
ENTRYPOINT ["/dive"]
diff --git a/Dockerfile.slim b/Dockerfile.slim
deleted file mode 100644
index d83f888..0000000
--- a/Dockerfile.slim
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM scratch
-COPY dist/dive_linux_amd64/dive /
-ENTRYPOINT ["/dive"]
diff --git a/Makefile b/Makefile
index e9f940f..ac291e5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,20 +1,28 @@
BIN = dive
-BUILD_DIR = ./dist/dive_linux_amd64/
+BUILD_DIR = ./dist/dive_linux_amd64
BUILD_PATH = $(BUILD_DIR)/$(BIN)
+PWD := ${CURDIR}
all: clean build
run: build
$(BUILD_PATH) build -t dive-example:latest -f .data/Dockerfile.example .
-run-ci: build
- CI=true $(BUILD_PATH) dive-example:latest --ci-config .data/.dive-ci
-
run-large: build
$(BUILD_PATH) amir20/clashleaders:latest
+run-podman: build
+ podman build -t dive-example:latest -f .data/Dockerfile.example .
+ $(BUILD_PATH) localhost/dive-example:latest --engine podman
+
+run-podman-large: build
+ $(BUILD_PATH) docker.io/amir20/clashleaders:latest --engine podman
+
+run-ci: build
+ CI=true $(BUILD_PATH) dive-example:latest --ci-config .data/.dive-ci
+
build:
- CGO_ENABLED=0 go build -o $(BUILD_PATH)
+ go build -o $(BUILD_PATH)
release: test-coverage validate
./.scripts/tag.sh
@@ -43,12 +51,16 @@ lint: build
generate-test-data:
docker build -t dive-test:latest -f .data/Dockerfile.test-image . && docker image save -o .data/test-docker-image.tar dive-test:latest && echo "Exported test data!"
-setup:
+setup-ci:
+ sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev
go get ./...
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /go/bin v1.18.0
+dev:
+ docker run -ti --rm -v $(PWD):/app -w /app -v dive-pkg:/go/pkg/ golang:1.13 bash
+
clean:
rm -rf dist
go clean
-.PHONY: build install test lint clean release validate generate-test-data test-coverage ci
+.PHONY: build install test lint clean release validate generate-test-data test-coverage ci dev
diff --git a/README.md b/README.md
index cbda298..6520738 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/wagoodman/dive)](https://goreportcard.com/report/github.com/wagoodman/dive)
[![Pipeline Status](https://circleci.com/gh/wagoodman/dive.svg?style=svg)](https://circleci.com/gh/wagoodman/dive)
-**A tool for exploring a docker image, layer contents, and discovering ways to shrink your Docker image size.**
+**A tool for exploring a docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image.**
![Image](.data/demo.gif)
@@ -16,7 +16,7 @@ or if you want to build your image then jump straight into analyzing it:
dive build -t <some-tag> .
```
-Building on Macbook
+Building on Macbook (supporting only the Docker container engine)
```bash
docker run --rm -it \
@@ -40,22 +40,15 @@ CI=true dive <your-image>
**Show Docker image contents broken down by layer**
-As you select a layer on the left, you are shown the contents of that layer
-combined with all previous layers on the right. Also, you can fully explore the
-file tree with the arrow keys.
+As you select a layer on the left, you are shown the contents of that layer combined with all previous layers on the right. Also, you can fully explore the file tree with the arrow keys.
**Indicate what's changed in each layer**
-Files that have changed, been modified, added, or removed are indicated in the
-file tree. This can be adjusted to show changes for a specific layer, or
-aggregated changes up to this layer.
+Files that have changed, been modified, added, or removed are indicated in the file tree. This can be adjusted to show changes for a specific layer, or aggregated changes up to this layer.
**Estimate "image efficiency"**
-The lower left pane shows basic layer info and an experimental metric that will
-guess how much wasted space your image contains. This might be from duplicating
-files across layers, moving files across layers, or not fully removing files.
-Both a percentage "score" and total wasted file space is provided.
+The lower left pane shows basic layer info and an experimental metric that will guess how much wasted space your image contains. This might be from duplicating files across layers, moving files across layers, or not fully removing files. Both a percentage "score" and total wasted file space is provided.
**Quick build/analysis cycles**
@@ -68,6 +61,13 @@ command.
**CI Integration**
Analyze and image and get a pass/fail result based on the image efficiency and wasted space. Simply set `CI=true` in the environment when invoking any valid dive command.
+**Supported Container Engines**
+- Docker (default)
+- Podman (linux only)
+
+```bash
+dive <your-image-tag> --engine podman
+```
## Installation
@@ -195,6 +195,9 @@ Key Binding | Description
No configuration is necessary, however, you can create a config file and override values:
```yaml
+# supported options are "docker" and "podman"
+container-engine: docker
+
log:
enabled: true
path: ./dive.log
diff --git a/cmd/analyze.go b/cmd/analyze.go
index 8061de9..4e6c27e 100644
--- a/cmd/analyze.go
+++ b/cmd/analyze.go
@@ -2,16 +2,16 @@ package cmd
import (
"fmt"
+ "github.com/wagoodman/dive/dive"
+ "os"
"github.com/spf13/cobra"
"github.com/wagoodman/dive/runtime"
- "github.com/wagoodman/dive/utils"
)
// doAnalyzeCmd takes a docker image tag, digest, or id and displays the
// image analysis to the screen
func doAnalyzeCmd(cmd *cobra.Command, args []string) {
- defer utils.Cleanup()
if len(args) == 0 {
printVersionFlag, err := cmd.PersistentFlags().GetBool("version")
@@ -21,13 +21,13 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
}
fmt.Println("No image argument given")
- utils.Exit(1)
+ os.Exit(1)
}
userImage := args[0]
if userImage == "" {
fmt.Println("No image argument given")
- utils.Exit(1)
+ os.Exit(1)
}
initLogging()
@@ -36,11 +36,18 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
if err != nil {
fmt.Printf("ci configuration error: %v\n", err)
- utils.Exit(1)
+ os.Exit(1)
+ }
+
+ engine, err := cmd.PersistentFlags().GetString("engine")
+ if err != nil {
+ fmt.Printf("unable to determine engine: %v\n", err)
+ os.Exit(1)
}
runtime.Run(runtime.Options{
Ci: isCi,
+ Engine: dive.GetEngine(engine),
ImageId: userImage,
ExportFile: exportFile,
CiConfig: ciConfig,
diff --git a/cmd/build.go b/cmd/build.go
index 1ff2220..0cff973 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -2,8 +2,9 @@ package cmd
import (
"github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "github.com/wagoodman/dive/dive"
"github.com/wagoodman/dive/runtime"
- "github.com/wagoodman/dive/utils"
)
// buildCmd represents the build command
@@ -20,12 +21,15 @@ func init() {
// doBuildCmd implements the steps taken for the build command
func doBuildCmd(cmd *cobra.Command, args []string) {
- defer utils.Cleanup()
-
initLogging()
+ // there is no cli options allowed, only config can be supplied
+ // todo: allow for an engine flag to be passed to dive but not the container engine
+ engine := viper.GetString("container-engine")
+
runtime.Run(runtime.Options{
Ci: isCi,
+ Engine: dive.GetEngine(engine),
BuildArgs: args,
ExportFile: exportFile,
CiConfig: ciConfig,
diff --git a/cmd/root.go b/cmd/root.go
index 9dadad2..877bca7 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -2,6 +2,7 @@ package cmd
import (
"fmt"
+ "github.com/wagoodman/dive/dive"
"github.com/wagoodman/dive/dive/filetree"
"io/ioutil"
"os"
@@ -12,7 +13,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
- "github.com/wagoodman/dive/utils"
)
var cfgFile string
@@ -35,9 +35,8 @@ the amount of wasted space and identifies the offending files from the image.`,
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
- utils.Exit(1)
+ os.Exit(1)
}
- utils.Cleanup()
}
func init() {
@@ -58,9 +57,15 @@ func initCli() {
for _, key := range []string{"lowestEfficiency", "highestWastedBytes", "highestUserWastedPercent"} {
if err := ciConfig.BindPFlag(fmt.Sprintf("rules.%s", key), rootCmd.Flags().Lookup(key)); err != nil {
- log.Fatal("Unable to bind flag:", err)
+ log.Fatalf("Unable to bind '%s' flag: %v", key, err)
}
}
+
+ rootCmd.PersistentFlags().String("engine", "docker", "The container engine to fetch the image from. Allowed values: "+strings.Join(dive.AllowedEngines, ", "))
+
+ if err := viper.BindPFlag("container-engine", rootCmd.PersistentFlags().Lookup("engine")); err != nil {
+ log.Fatal("Unable to bind 'engine' flag:", err)
+ }
}
// initConfig reads in config file and ENV variables if set.
@@ -97,7 +102,12 @@ func initConfig() {
viper.SetDefault("filetree.pane-width", 0.5)
viper.SetDefault("filetree.show-attributes", true)
- viper.AutomaticEnv() // read in environment variables that match
+ viper.SetDefault("container-engine", "docker")
+
+ viper.SetEnvPrefix("DIVE")
+ // replace all - with _ when looking for matching environment variables
+ viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
+ viper.AutomaticEnv()
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
@@ -148,7 +158,7 @@ func getCfgFile(fromFlag string) string {
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
- utils.Exit(0)
+ os.Exit(0)
}
xdgHome := os.Getenv("XDG_CONFIG_HOME")
diff --git a/dive/filetree/cache.go b/dive/filetree/cache.go
index 82c1795..a7f0fc4 100644
--- a/dive/filetree/cache.go
+++ b/dive/filetree/cache.go
@@ -13,29 +13,36 @@ type TreeCache struct {
cache map[TreeCacheKey]*FileTree
}
-func (cache *TreeCache) Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) *FileTree {
+func (cache *TreeCache) Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) (*FileTree, error) {
key := TreeCacheKey{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}
if value, exists := cache.cache[key]; exists {
- return value
+ return value, nil
}
- value := cache.buildTree(key)
+ value, err := cache.buildTree(key)
+ if err != nil {
+ return nil, err
+ }
cache.cache[key] = value
- return value
+ return value, nil
}
-func (cache *TreeCache) buildTree(key TreeCacheKey) *FileTree {
- newTree := StackTreeRange(cache.refTrees, key.bottomTreeStart, key.bottomTreeStop)
+func (cache *TreeCache) buildTree(key TreeCacheKey) (*FileTree, error) {
+ newTree, err := StackTreeRange(cache.refTrees, key.bottomTreeStart, key.bottomTreeStop)
+ if err != nil {
+ return nil, err
+ }
for idx := key.topTreeStart; idx <= key.topTreeStop; idx++ {
err := newTree.CompareAndMark(cache.refTrees[idx])
if err != nil {
logrus.Errorf("unable to build tree: %+v", err)
+ return nil, err
}
}
- return newTree
+ return newTree, nil
}
-func (cache *TreeCache) Build() {
+func (cache *TreeCache) Build() error {
var bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int
// case 1: layer compare (top tree SIZE is fixed (BUT floats forward), Bottom tree SIZE changes)
@@ -51,7 +58,10 @@ func (cache *TreeCache) Build() {
topTreeStart = selectIdx
}
- cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
+ _, err := cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
+ if err != nil {
+ return err
+ }
}
// case 2: aggregated compare (bottom tree is ENTIRELY fixed, top tree SIZE changes)
@@ -66,8 +76,12 @@ func (cache *TreeCache) Build() {
topTreeStart = 1
}
- cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
+ _, err := cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
+ if err != nil {
+ return err
+ }
}
+ return nil
}
func NewFileTreeCache(refTrees []*FileTree) TreeCache {
diff --git a/dive/filetree/efficiency.go b/dive/filetree/efficiency.go
index 7be116b..24ad3b5 100644
--- a/dive/filetree/efficiency.go
+++ b/dive/filetree/efficiency.go
@@ -1,7 +1,6 @@
package filetree
import (
- "fmt"
"sort"
"github.com/sirupsen/logrus"
@@ -63,14 +62,22 @@ func Efficiency(trees []*FileTree) (float64, EfficiencySlice) {
sizeBytes += curNode.Data.FileInfo.Size
return nil
}
- stackedTree := StackTreeRange(trees, 0, currentTree-1)
+ stackedTree, err := StackTreeRange(trees, 0, currentTree-1)
+ if err != nil {
+ logrus.Errorf("unable to stack tree range: %+v", err)
+ return err
+ }
+
previousTreeNode, err := stackedTree.GetNode(node.Path())
if err != nil {
- logrus.Debug(fmt.Sprintf("CurrentTree: %d : %s", currentTree, err))
- } else if previousTreeNode.Data.FileInfo.IsDir {
+ return err
+ }
+
+ if previousTreeNode.Data.FileInfo.IsDir {
err = previousTreeNode.VisitDepthChildFirst(sizer, nil)
if err != nil {
logrus.Errorf("unable to propagate whiteout dir: %+v", err)
+ return err
}
}
diff --git a/dive/filetree/file_info.go b/dive/filetree/file_info.go
index 4d24925..788ac3c 100644
--- a/dive/filetree/file_info.go
+++ b/dive/filetree/file_info.go
@@ -21,24 +21,13 @@ type FileInfo struct {
IsDir bool
}
-// NewFileInfo extracts the metadata from a tar header and file contents and generates a new FileInfo object.
-func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo {
- if header.Typeflag == tar.TypeDir {
- return FileInfo{
- Path: path,
- TypeFlag: header.Typeflag,
- Linkname: header.Linkname,
- hash: 0,
- Size: header.FileInfo().Size(),
- Mode: header.FileInfo().Mode(),
- Uid: header.Uid,
- Gid: header.Gid,
- IsDir: header.FileInfo().IsDir(),
- }
+// NewFileInfoFromTarHeader extracts the metadata from a tar header and file contents and generates a new FileInfo object.
+func NewFileInfoFromTarHeader(reader *tar.Reader, header *tar.Header, path string) FileInfo {
+ var hash uint64
+ if header.Typeflag != tar.TypeDir {
+ hash = getHashFromReader(reader)
}
- hash := getHashFromReader(reader)
-
return FileInfo{
Path: path,
TypeFlag: header.Typeflag,
@@ -52,6 +41,54 @@ func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo {
}
}
+func NewFileInfo(realPath, path string, info os.FileInfo) FileInfo {
+ var err error
+
+ // todo: don't use tar types here, create our own...
+ var fileType byte
+ var linkName string
+ var size int64
+
+ if info.Mode()&os.ModeSymlink != 0 {
+ fileType = tar.TypeSymlink
+
+ linkName, err = os.Readlink(realPath)
+ if err != nil {
+ logrus.Panic("unable to read link:", realPath, err)
+ }
+
+ } else if info.IsDir() {
+ fileType = tar.TypeDir
+ } else {
+ fileType = tar.TypeReg
+
+ size = info.Size()
+ }
+
+ var hash uint64
+ if fileType != tar.TypeDir {
+ file, err := os.Open(realPath)
+ if err != nil {
+ logrus.Panic("unable to read file:", realPath)
+ }
+ defer file.Close()
+ hash = getHashFromReader(file)
+ }
+
+ return FileInfo{
+ Path: path,
+ TypeFlag: fileType,
+ Linkname: linkName,
+ hash: hash,
+ Size: size,
+ Mode: info.Mode(),
+ // todo: support UID/GID
+ Uid: -1,
+ Gid: -1,
+ IsDir: info.IsDir(),
+ }
+}
+
// Copy duplicates a FileInfo
func (data *FileInfo) Copy() *FileInfo {
if data == nil {
diff --git a/dive/filetree/file_tree.go b/dive/filetree/file_tree.go
index c7c3dfc..11311fc 100644
--- a/dive/filetree/file_tree.go
+++ b/dive/filetree/file_tree.go
@@ -367,14 +367,15 @@ func (tree *FileTree) markRemoved(path string) error {
}
// StackTreeRange combines an array of trees into a single tree
-func StackTreeRange(trees []*FileTree, start, stop int) *FileTree {
+func StackTreeRange(trees []*FileTree, start, stop int) (*FileTree, error) {
tree := trees[0].Copy()
for idx := start; idx <= stop; idx++ {
err := tree.Stack(trees[idx])
if err != nil {
logrus.Errorf("could not stack tree range: %v", err)
+ return nil, err
}
}
- return tree
+ return tree, nil
}
diff --git a/dive/filetree/file_tree_test.go b/dive/filetree/file_tree_test.go
index fc57036..d9f9677 100644
--- a/dive/filetree/file_tree_test.go
+++ b/dive/filetree/file_tree_test.go
@@ -745,7 +745,10 @@ func TestStackRange(t *testing.T) {
}
}
trees := []*FileTree{lowerTree, upperTree, tree}
- StackTreeRange(trees, 0, 2)
+ _, err = StackTreeRange(trees, 0, 2)