Compare commits

...

64 commits

Author SHA1 Message Date
Nathan-Moignard 925cdd8648
Update Dockerfile Alpine Image from alpine:3.12 => alpine:3.18 (#481)
Trivy Report showing vunlarability:

WARN	This OS version is no longer supported by the distribution: alpine 3.12.12

│ zlib    │ CVE-2022-37434 │ CRITICAL │ fixed  │ 1.2.12-r0         │ 1.2.12-r2     |
2024-02-02 15:27:41 +00:00
suguds fd526464b2
update golang.org/x/net v0.11.0 to 0.17.0 (#503) 2024-02-02 10:21:42 -05:00
dependabot[bot] 9f08f7e6cc
Bump github.com/docker/docker (#483)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 24.0.2+incompatible to 24.0.7+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v24.0.2...v24.0.7)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 09:29:04 -05:00
Rene Nyffenegger 7556be352a
DIVE_VERSION does not need to be exported (#492)
The value of the variable DIVE_VERSION is not needed in a
spawned subprocess. Thus, the variable does not necessarily
need to be exported. Arguably, the environment is less
polluted if it is not exported.
2024-02-02 14:26:54 +00:00
Guillaume Belanger 2d86aa7b4c
chore: Adds instructions for snap installation (#484) 2024-02-02 09:26:06 -05:00
Thomas Broyer 5d6a406df1
Fix compatibility with Docker 25+ (#500)
Add support for OCI-compatible Docker images.

Fixes #498
2024-02-02 09:21:05 -05:00
Sandro 3ef1dd2c74
Bump to more recent go version, remove no longer required overrides (#461) 2023-07-10 20:44:51 +00:00
Alex Goodman 559e5e2dbe add release process 2023-07-07 11:54:08 -04:00
Alex Goodman 8003980604 enable release pipeline 2023-07-07 11:42:48 -04:00
Ian Ray 6f20438ae4
feat: add support for alternative ordering strategies (#424) 2023-07-07 10:01:53 -04:00
Alex Goodman d5e8a92968
Rework CI validation workflow and makefile (#460)
* rework CI validation workflow and makefile

* enable push

* fix job names

* fix license check

* fix snapshot builds

* fix acceptance tests

* fix linting

* disable pull request event

* rework windows runner caching

* disable release pipeline and add issue templates
2023-07-06 22:01:46 -04:00
Alex Goodman 42925c1b05
Merge pull request #459 from orhun/docs/update_arch_linux_link
Update the link for the Arch Linux package
2023-07-06 20:16:58 -04:00
Orhun Parmaksız ec5528bce5
update the link for the Arch Linux package 2023-07-06 19:10:44 +03:00
Alex Goodman 8a10c0d46b
Merge pull request #403 from orihomie/patch-1
ubuntu install latest version script
2023-07-06 11:16:52 -04:00
Alex Goodman 6ba95122a5 orient all install refs around the latest version 2023-07-06 11:15:49 -04:00
Orkhan 69e6ba9993 ubuntu install latest version script 2023-07-06 11:15:06 -04:00
Alex Goodman a70b7acffc
Merge pull request #447 from yurenchen000/feat-ctrl-z
Feat: add ctrl-z support
2023-07-06 11:04:51 -04:00
Alex Goodman e9169b9ccd check error of kill and return calls 2023-07-06 11:03:30 -04:00
chen 77c11047cf feat: add ctrl+z support 2023-07-06 11:01:00 -04:00
chen a7dfbb7927 fix layer view curosr pgup/pgdn 2023-07-06 11:01:00 -04:00
chen c4b2723d97 fix layer view curosr up/down 2023-07-06 11:01:00 -04:00
chen 255e3c2ff7 update lib gocui to v1.1.0 2023-07-06 11:00:59 -04:00
Alex Goodman 32c1c1b7bf
Merge pull request #443 from lutzky/master
Fix rendering for multi-line commands
2023-07-06 10:49:15 -04:00
Alex Goodman 99124abb7a
Merge pull request #429 from zachary-walters/update-deprecated-dependency-ioutil
Updated the deprecated ioutil dependency
2023-07-06 10:49:03 -04:00
Ohad Lutzky ceb9688d92 Fix rendering for multi-line commands
Heredocs can be used to create multi-line commands, which broke rendering.

Details about heredocs: https://www.docker.com/blog/introduction-to-heredocs-in-dockerfiles/
2023-07-06 10:48:06 -04:00
Alex Goodman 67aa2f1bf8
Merge pull request #428 from orhun/docs/update_readme
Update README.md about installation on Arch Linux
2023-07-06 10:42:12 -04:00
zachary-walters ae996cd718 Updated the deprecated ioutil dependency
The ioutil package has been deprecated as of Go v1.16: https://go.dev/doc/go1.16#ioutil

This commit replaces ioutil functions with their respective io/os functions.
2023-07-06 10:41:54 -04:00
Orhun Parmaksız 25a226d51d Update README.md about installation on Arch Linux
`dive` is moved to community: https://archlinux.org/packages/community/x86_64/dive/
2023-07-06 10:40:42 -04:00
Alex Goodman bf39a82f2a
Merge pull request #400 from iwataka/correct-installation-instruction-for-ubuntu-debian
Correct installation instruction for Ubuntu/Debian
2023-07-06 10:39:27 -04:00
Alex Goodman 2db0aaa920
Merge pull request #344 from ozbillwang/master
improve README with new docker run command
2023-07-06 10:37:16 -04:00
Alex Goodman a95e899663
Merge pull request #349 from hedrox/master
Fix stream podman command hanging
2023-07-06 10:36:36 -04:00
Alex Goodman f5458e6b57
Merge pull request #348 from alexandregv/patch-1
Add Nix/NixOS package installation instructions
2023-07-06 10:36:03 -04:00
Alex Goodman b6961534af
Merge pull request #354 from abitrolly/master
Fix #352. Quit on `q`
2023-07-06 10:34:13 -04:00
Anatoli Babenia f17140f8ce Document q shortcut 2023-07-06 10:32:50 -04:00
Anatoli Babenia 621e677e04 Fix #352. Quit on q
This uses latest `github.com/awesome-gocui/keybinding`
It will also be possible to separate quit shortcuts by space when
https://github.com/awesome-gocui/keybinding/pull/3 is merged.
2023-07-06 10:32:48 -04:00
Alex Goodman 2a99dd25fe
Merge pull request #390 from michaelherold/support-podman-macos
Support podman in macOS
2023-07-06 10:28:44 -04:00
Alex Goodman f50a009f8e
Merge pull request #395 from lightsnowball/fix-index-out-of-bound-layer
Fix error on Layer section and reduce information on one line per step.
2023-07-06 10:28:11 -04:00
Alex Goodman e2d9b623f9
Merge pull request #401 from dosisod/autodetect-yml-files
Allow for autodetecting both `.yaml` and `.yml` config files
2023-07-06 10:25:48 -04:00
Alex Goodman 8bf4341f70
Merge pull request #399 from mark2185/feature/gui-layout-rework
GUI rework
2023-07-06 10:25:30 -04:00
Alex Goodman abbac157bb
Merge pull request #457 from abitrolly/patch-1
Fix CI
2023-07-06 10:06:34 -04:00
Anatoli Babenia dfe9a8c5c9
goreleaser deprecated --rm-dist
https://goreleaser.com/deprecations/#-rm-dist
2023-07-06 08:19:29 +03:00
Anatoli Babenia d131aebc05
.goreleaser.yml deprecated brews.tap
https://goreleaser.com/deprecations/#brewstap
2023-07-06 08:15:41 +03:00
Anatoli Babenia 7933564b4c
_v1 suffix for mac and windows 2023-07-06 08:03:27 +03:00
Anatoli Babenia cd63ad53fb
Make tabs not spaces
https://stackoverflow.com/questions/2131213/can-you-make-valid-makefiles-without-tab-characters
2023-07-06 07:58:00 +03:00
Anatoli Babenia 02182266ec
goreleaser now adds _v1 suffix to binaries
https://goreleaser.com/customization/builds/#why-is-there-a-_v1-suffix-on-amd64-builds
2023-07-06 07:44:19 +03:00
Anatoli Babenia 1eb78e1ab7
.goreleaser.yml deprecated docker.binaries
https://goreleaser.com/deprecations/?h=#dockerbinaries
2023-07-06 07:27:44 +03:00
Anatoli Babenia ebe293c24b
.goreleaser.yml deprecated brews.github
https://goreleaser.com/deprecations/?h=#brewsgithub
2023-07-06 07:25:47 +03:00
Anatoli Babenia 3d7eb32d7e
Make goreleaser script executable 2023-07-06 07:18:29 +03:00
Anatoli Babenia ba3c9125e1
golangci-lint got new install URL too
https://goreleaser.com/deprecations/#godownloader
2023-07-06 07:03:13 +03:00
Anatoli Babenia 9fe4975733
Debug GitHub Actions /home/runner/.local/bin
It says this PATH dir does not exist
2023-07-06 06:58:24 +03:00
Anatoli Babenia 6c0552e182
Change PATH to $HOME/.local/bin for GitHub Actions
PATH in GitHub Actions:
/opt/hostedtoolcache/go/1.19.10/x64/bin:/home/runner/.local/bin:/opt/pipx_bin:/home/runner/.cargo/bin:/home/runner/.config/composer/vendor/bin:/usr/local/.ghcup/bin:/home/runner/.dotnet/tools:/snap/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

PATH in CircleCI:
/home/circleci/go/bin:/usr/local/go/bin:/home/circleci/bin:/home/circleci/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
2023-07-06 06:53:07 +03:00
Anatoli Babenia 0c9b09ea77
Add PATH inspection to an earlier command 2023-07-06 06:47:23 +03:00
Anatoli Babenia ac0fa872cd
Test what directories are available on PATH
Actual for CircleCI and GitHub Actions
2023-07-06 06:45:14 +03:00
Anatoli Babenia 741f95aa8a
New goreleaser install method
https://goreleaser.com/deprecations/#godownloader
2023-07-06 06:34:11 +03:00
Anatoli Babenia a19a6f9acc
Catch pipefail errors in Makefile
https://stackoverflow.com/questions/33925523/how-to-set-pipefail-in-a-makefile
2023-07-06 06:26:02 +03:00
dosisod 4fad38207e Allow for autodetecting both .yaml and .yml config files 2022-05-13 23:33:31 -07:00
iwataka f359b8a9d7 Correct installation instruction for Ubuntu/Debian 2022-05-09 20:48:49 +09:00
Luka Markušić 2aad87c37e Refactor the GUI layout
The Details struct was split into two, LayerDetails and ImageDetails,
Each of the three views (Layer, LayerDetails, ImageDetails) takes up
a third of the available height of the screen, and they are all now
selectable and scrollable.
2022-04-30 00:17:32 +02:00
lightsnowball 2030e74234 Fix error on Layer section and reduce information on one line per step.
Instead of printing out multiple lines for some steps in Layer section,
now its only printing one line while other informations can be found in
Layer details. This change also provides fix for index out of bounds
error when user scrolls through steps in Layer section and there exists
at least one step with multi-line commands.
2022-04-29 21:57:16 +02:00
lightsnowball 4146421e60 Fix error on Layer section and reduce information on one line per step.
Instead of printing out multiple lines for some steps in Layer section,
now its only printing one line while other informations can be found in
Layer details. This change also provides fix for index out of bounds
error when user scrolls through steps in Layer section and there exists
at least one step with multi-line commands.
2022-04-22 20:12:36 +02:00
Michael Herold 7dfef036c7
Support podman in macOS
With [a fix][1] in [podman v3.4.3][2], these commands now work as
expected in macOS. Potentially, it might make sense to version check
podman to ensure that the minimum version is met, but I'm not sure
that's needed because it's unlikely that people have an older version
installed _and_ wish to use this tool.

I'm unsure whether the commands work on Windows so I left the
unsupported version there compiling with the negation of the supported
flags.

[1]: https://github.com/containers/podman/issues/12402
[2]: 4ba71f955a/RELEASE_NOTES.md (bugfixes-2)
2022-03-31 10:10:55 -05:00
Andrei Marin 4d5b2824ee
Fix stream podman command hanging 2021-04-11 18:53:58 +00:00
Alexandre GV b5b377444e
Add Nix/NixOS install in README.md 2021-04-01 13:10:43 +02:00
Bill Wang b7d32324e6 improve README with new docker run command 2021-03-20 21:34:10 +11:00
81 changed files with 1872 additions and 1015 deletions

12
.bouncer.yaml Normal file
View file

@ -0,0 +1,12 @@
permit:
- BSD.*
- MIT.*
- Apache.*
- MPL.*
- ISC
- WTFPL
ignore-packages:
# crypto/internal/boring is released under the openSSL license as a part of the Golang Standard Library
- crypto/internal/boring

View file

@ -1,59 +0,0 @@
version: 2.1
jobs:
run-static-analyses:
parameters:
version:
type: string
working_directory: /home/circleci/app
docker:
- image: cimg/go:<< parameters.version >>
environment:
GO111MODULE: "on"
steps:
- checkout
- restore_cache:
keys:
- golang-<< parameters.version >>-{{ checksum "go.sum" }}
- run: make ci-install-go-tools
- save_cache:
key: golang-<< parameters.version >>-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
- run:
name: run static analysis
command: make ci-static-analysis
run-tests:
parameters:
version:
type: string
working_directory: /home/circleci/app
docker:
- image: cimg/go:<< parameters.version >>
environment:
GO111MODULE: "on"
steps:
- checkout
- restore_cache:
keys:
- golang-<< parameters.version >>-{{ checksum "go.sum" }}
- run: make ci-install-go-tools
- save_cache:
key: golang-<< parameters.version >>-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
- run:
name: run unit tests
command: make ci-unit-test
workflows:
commit:
jobs:
- run-static-analyses:
version: "1.19"
- run-tests:
version: "1.19"
- run-tests:
version: "1.19"

Binary file not shown.

1
.github/FUNDING.yml vendored
View file

@ -1,2 +1 @@
github: ['wagoodman']
custom: ['https://www.paypal.me/wagoodman']

20
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,20 @@
---
name: Bug report
about: Something isn't working as expected
title: ''
labels: bug
assignees: ''
---
**What happened**:
**What you expected to happen**:
**How to reproduce it (as minimally and precisely as possible)**:
**Anything else we need to know?**:
**Environment**:
- OS version
- Docker version (if applicable)

View file

@ -0,0 +1,15 @@
---
name: Feature request
about: Got an idea for a new feature? Let us know!
title: ''
labels: enhancement
assignees: ''
---
**What would you like to be added**:
**Why is this needed**:
**Additional context**:
<!-- Add any other context or screenshots about the feature request here. -->

76
.github/actions/bootstrap/action.yaml vendored Normal file
View file

@ -0,0 +1,76 @@
name: "Bootstrap"
description: "Bootstrap all tools and dependencies"
inputs:
go-version:
description: "Go version to install"
required: true
default: "1.20.x"
use-go-cache:
description: "Restore go cache"
required: true
default: "true"
cache-key-prefix:
description: "Prefix all cache keys with this value"
required: true
default: "efa04b89c1b1"
build-cache-key-prefix:
description: "Prefix build cache key with this value"
required: true
default: "f8b6d31dea"
bootstrap-apt-packages:
description: "Space delimited list of tools to install via apt"
default: ""
runs:
using: "composite"
steps:
- uses: actions/setup-go@v3
with:
go-version: ${{ inputs.go-version }}
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
# note: we need to keep restoring the go mod cache before bootstrapping tools since `go install` is used in
# some installations of project tools.
- name: Restore go module cache
id: go-mod-cache
if: inputs.use-go-cache == 'true'
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ inputs.cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-
- name: (cache-miss) Bootstrap project tools
shell: bash
if: steps.tool-cache.outputs.cache-hit != 'true'
run: make bootstrap-tools
- name: Restore go build cache
id: go-cache
if: inputs.use-go-cache == 'true'
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
key: ${{ inputs.cache-key-prefix }}-${{ inputs.build-cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ inputs.cache-key-prefix }}-${{ inputs.build-cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-
- name: (cache-miss) Bootstrap go dependencies
shell: bash
if: steps.go-mod-cache.outputs.cache-hit != 'true' && inputs.use-go-cache == 'true'
run: make bootstrap-go
- name: Install apt packages
if: inputs.bootstrap-apt-packages != ''
shell: bash
run: |
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y ${{ inputs.bootstrap-apt-packages }}

11
.github/scripts/ci-check.sh vendored Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
red=$(tput setaf 1)
bold=$(tput bold)
normal=$(tput sgr0)
# assert we are running in CI (or die!)
if [[ -z "$CI" ]]; then
echo "${bold}${red}This step should ONLY be run in CI. Exiting...${normal}"
exit 1
fi

36
.github/scripts/coverage.py vendored Executable file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env python3
import subprocess
import sys
import shlex
class bcolors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
if len(sys.argv) < 3:
print("Usage: coverage.py [threshold] [go-coverage-report]")
sys.exit(1)
threshold = float(sys.argv[1])
report = sys.argv[2]
args = shlex.split(f"go tool cover -func {report}")
p = subprocess.run(args, capture_output=True, text=True)
percent_coverage = float(p.stdout.splitlines()[-1].split()[-1].replace("%", ""))
print(f"{bcolors.BOLD}Coverage: {percent_coverage}%{bcolors.ENDC}")
if percent_coverage < threshold:
print(f"{bcolors.BOLD}{bcolors.FAIL}Coverage below threshold of {threshold}%{bcolors.ENDC}")
sys.exit(1)

31
.github/scripts/go-mod-tidy-check.sh vendored Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -eu
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
trap "cp -v ${ORIGINAL_STATE_DIR}/* ./ && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
echo "Capturing original state of files..."
cp -v go.mod go.sum "${ORIGINAL_STATE_DIR}"
echo "Capturing state of go.mod and go.sum after running go mod tidy..."
go mod tidy
cp -v go.mod go.sum "${TIDY_STATE_DIR}"
echo ""
set +e
# Detect difference between the git HEAD state and the go mod tidy state
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
if [[ -n "${DIFF_MOD}" || -n "${DIFF_SUM}" ]]; then
echo "go.mod diff:"
echo "${DIFF_MOD}"
echo "go.sum diff:"
echo "${DIFF_SUM}"
echo ""
printf "FAILED! go.mod and/or go.sum are NOT tidy; please run 'go mod tidy'.\n\n"
exit 1
fi

50
.github/scripts/trigger-release.sh vendored Executable file
View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -eu
bold=$(tput bold)
normal=$(tput sgr0)
if ! [ -x "$(command -v gh)" ]; then
echo "The GitHub CLI could not be found. To continue follow the instructions at https://github.com/cli/cli#installation"
exit 1
fi
gh auth status
# we need all of the git state to determine the next version. Since tagging is done by
# the release pipeline it is possible to not have all of the tags from previous releases.
git fetch --tags
# populates the CHANGELOG.md and VERSION files
echo "${bold}Generating changelog...${normal}"
make changelog 2> /dev/null
NEXT_VERSION=$(cat VERSION)
if [[ "$NEXT_VERSION" == "" || "${NEXT_VERSION}" == "(Unreleased)" ]]; then
echo "Could not determine the next version to release. Exiting..."
exit 1
fi
while true; do
read -p "${bold}Do you want to trigger a release for version '${NEXT_VERSION}'?${normal} [y/n] " yn
case $yn in
[Yy]* ) echo; break;;
[Nn]* ) echo; echo "Cancelling release..."; exit;;
* ) echo "Please answer yes or no.";;
esac
done
echo "${bold}Kicking off release for ${NEXT_VERSION}${normal}..."
echo
gh workflow run release.yaml -f version=${NEXT_VERSION}
echo
echo "${bold}Waiting for release to start...${normal}"
sleep 10
set +e
echo "${bold}Head to the release workflow to monitor the release:${normal} $(gh run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')"
id=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId')
gh run watch $id --exit-status || (echo ; echo "${bold}Logs of failed step:${normal}" && GH_PAGER="" gh run view $id --log-failed)

View file

@ -1,180 +0,0 @@
name: 'app-pipeline'
on:
push:
pull_request:
types: [ opened, reopened ]
env:
DOCKER_CLI_VERSION: "19.03.1"
jobs:
unit-test:
strategy:
matrix:
go-version: [1.19.x]
# todo: support windows
platform: [ubuntu-latest, macos-latest]
# platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v1
- name: Cache go dependencies
id: unit-cache-go-dependencies
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-${{ matrix.go-version }}-
- name: Install go dependencies
if: steps.unit-cache-go-dependencies.outputs.cache-hit != 'true'
run: go get ./...
- name: Test
run: make ci-unit-test
build-artifacts:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v1
with:
go-version: '1.19.x'
- uses: actions/checkout@v1
- name: Install tooling
run: |
make ci-install-go-tools
make ci-install-ci-tools
- name: Cache go dependencies
id: package-cache-go-dependencies
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-prod-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-prod-
- name: Install dependencies
if: steps.package-cache-go-dependencies.outputs.cache-hit != 'true'
run: go get ./...
- name: Linting, formatting, and other static code analyses
run: make ci-static-analysis
- name: Build snapshot artifacts
run: make ci-build-snapshot-packages
- run: docker images wagoodman/dive
# todo: compare against known json output in shared volume
- name: Test production image
run: make ci-test-production-image
- uses: actions/upload-artifact@master
with:
name: artifacts
path: dist
test-linux-artifacts:
needs: [ build-artifacts ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/download-artifact@master
with:
name: artifacts
path: dist
- name: Test linux run
run: make ci-test-linux-run
- name: Test DEB package installation
run: make ci-test-deb-package-install
- name: Test RPM package installation
run: make ci-test-rpm-package-install
test-mac-artifacts:
needs: [ build-artifacts ]
runs-on: macos-latest
steps:
- uses: actions/checkout@master
- uses: actions/download-artifact@master
with:
name: artifacts
path: dist
- name: Test darwin run
run: make ci-test-mac-run
test-windows-artifacts:
needs: [ build-artifacts ]
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- uses: actions/download-artifact@master
with:
name: artifacts
path: dist
- name: Test windows run
run: make ci-test-windows-run
release:
needs: [ unit-test, build-artifacts, test-linux-artifacts, test-mac-artifacts, test-windows-artifacts ]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/setup-go@v1
with:
go-version: '1.19.x'
- uses: actions/checkout@v1
- name: Install tooling
run: make ci-install-ci-tools
- name: Cache go dependencies
id: release-cache-go-dependencies
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-prod-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-prod-
- name: Install dependencies
if: steps.release-cache-go-dependencies.outputs.cache-hit != 'true'
run: go get ./...
- name: Docker login
run: make ci-docker-login
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Publish GitHub release
run: make ci-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Docker logout
run: make ci-docker-logout
- name: Smoke test published image
run: make ci-test-production-image

116
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,116 @@
name: "Release"
on:
workflow_dispatch:
inputs:
version:
description: tag the latest commit on main with the given version (prefixed with v)
required: true
jobs:
quality-gate:
environment: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check if tag already exists
# note: this will fail if the tag already exists
run: |
[[ "${{ github.event.inputs.version }}" == v* ]] || (echo "version '${{ github.event.inputs.version }}' does not have a 'v' prefix" && exit 1)
git tag ${{ github.event.inputs.version }}
- name: Check static analysis results
uses: fountainhead/action-wait-for-check@v1.1.0
id: static-analysis
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
checkName: "Static analysis"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check unit test results
uses: fountainhead/action-wait-for-check@v1.1.0
id: unit
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
checkName: "Unit tests (ubuntu-latest)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (linux)
uses: fountainhead/action-wait-for-check@v1.1.0
id: acceptance-linux
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
checkName: "Acceptance tests (Linux)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (mac)
uses: fountainhead/action-wait-for-check@v1.1.0
id: acceptance-mac
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
checkName: "Acceptance tests (Mac)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (windows)
uses: fountainhead/action-wait-for-check@v1.1.0
id: acceptance-windows
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
checkName: "Acceptance tests (Windows)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Quality gate
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' || steps.acceptance-windows.outputs.conclusion != 'success'
run: |
echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}"
echo "Unit Test Status: ${{ steps.unit.outputs.conclusion }}"
echo "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}"
echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}"
echo "Acceptance Test (Windows) Status: ${{ steps.acceptance-windows.outputs.conclusion }}"
false
release:
needs: [quality-gate]
runs-on: ubuntu-latest
permissions:
# for tagging
contents: write
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Tag release
run: |
git tag ${{ github.event.inputs.version }}
git push origin --tags
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build & publish release artifacts
run: make ci-release
env:
# for creating the release (requires write access to content)
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# for updating brew formula in wagoodman/homebrew-dive
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
- name: Smoke test published image
run: make ci-test-docker-image

135
.github/workflows/validations.yaml vendored Normal file
View file

@ -0,0 +1,135 @@
name: "Validations"
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
jobs:
Static-Analysis:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Static analysis"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Run static analysis
run: make static-analysis
Unit-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Unit tests"
strategy:
matrix:
platform:
- ubuntu-latest
# - macos-latest # todo: mac runners are expensive minute-wise
# - windows-latest # todo: support windows
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Run unit tests
run: make unit
Build-Snapshot-Artifacts:
name: "Build snapshot artifacts"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Build snapshot artifacts
run: make snapshot
- run: docker images wagoodman/dive
# todo: compare against known json output in shared volume
- name: Test production image
run: make ci-test-docker-image
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
# see https://github.com/actions/upload-artifact/issues/199 for more info
- name: Upload snapshot artifacts
uses: actions/cache/save@v3
with:
path: snapshot
key: snapshot-build-${{ github.run_id }}
# ... however the cache trick doesn't work on windows :(
- uses: actions/upload-artifact@v3
with:
name: windows-artifacts
path: snapshot/dive_windows_amd64_v1/dive.exe
Acceptance-Linux:
name: "Acceptance tests (Linux)"
needs: [ Build-Snapshot-Artifacts ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Download snapshot build
uses: actions/cache/restore@v3
with:
path: snapshot
key: snapshot-build-${{ github.run_id }}
- name: Test linux run
run: make ci-test-linux-run
- name: Test DEB package installation
run: make ci-test-deb-package-install
- name: Test RPM package installation
run: make ci-test-rpm-package-install
Acceptance-Mac:
name: "Acceptance tests (Mac)"
needs: [ Build-Snapshot-Artifacts ]
runs-on: macos-latest
steps:
- uses: actions/checkout@master
- name: Download snapshot build
uses: actions/cache/restore@v3
with:
path: snapshot
key: snapshot-build-${{ github.run_id }}
- name: Test darwin run
run: make ci-test-mac-run
Acceptance-Windows:
name: "Acceptance tests (Windows)"
needs: [ Build-Snapshot-Artifacts ]
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- uses: actions/download-artifact@v3
with:
name: windows-artifacts
- name: Test windows run
run: make ci-test-windows-run

24
.gitignore vendored
View file

@ -1,6 +1,25 @@
# misc
/.image
*.log
CHANGELOG.md
VERSION
# IDEs
/.idea
/.vscode
# tooling
/bin
/.tool-versions
/.tmp
# builds
/dist
/snapshot
# testing
.cover
coverage.txt
# Binaries for programs and plugins
*.exe
@ -18,8 +37,3 @@
/build
/_vendor*
/vendor
/.image
*.log
/dist
.cover
coverage.txt

74
.golangci.yaml Normal file
View file

@ -0,0 +1,74 @@
# TODO: enable this when we have coverage on docstring comments
#issues:
# # The list of ids of default excludes to include or disable.
# include:
# - EXC0002 # disable excluding of issues about comments from golint
linters-settings:
funlen:
# Checks the number of lines in a function.
# If lower than 0, disable the check.
# Default: 60
# TODO: drop this down over time...
lines: 110
# Checks the number of statements in a function.
# If lower than 0, disable the check.
# Default: 40
statements: 60
# TODO: use the default linters for now, but include these over time
#linters:
# # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
# disable-all: true
# enable:
# - asciicheck
# - bodyclose
# - depguard
# - dogsled
# - dupl
# - errcheck
# - exportloopref
# - funlen
# - gocognit
# - goconst
# - gocritic
# - gocyclo
# - gofmt
# - goimports
# - goprintffuncname
# - gosec
# - gosimple
# - govet
# - ineffassign
# - misspell
# - nakedret
# - nolintlint
# - revive
# - staticcheck
# - stylecheck
# - typecheck
# - unconvert
# - unparam
# - unused
# - whitespace
# do not enable...
# - gochecknoglobals
# - gochecknoinits # this is too aggressive
# - godot
# - godox
# - goerr113
# - golint # deprecated
# - gomnd # this is too aggressive
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
# - lll # without a way to specify per-line exception cases, this is not usable
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
# - nestif
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
# - scopelint # deprecated
# - testpackage
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
# - varcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
# - deadcode # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
# - structcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
# - rowserrcheck # we're not using sql.Rows at all in the codebase

View file

@ -1,5 +1,10 @@
release:
prerelease: false
# If set to auto, will mark the release as not ready for production in case there is an indicator for this in the
# tag e.g. v1.0.0-rc1 .If set to true, will mark the release as not ready for production.
prerelease: auto
# If set to true, will not auto-publish the release. This is done to allow us to review the changelog before publishing.
draft: false
builds:
- binary: dive
@ -11,14 +16,16 @@ builds:
- linux
goarch:
- amd64
- arm64
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.buildTime={{.Date}}`.
brews:
- github:
- repository:
owner: wagoodman
name: homebrew-dive
token: "{{.Env.TAP_GITHUB_TOKEN}}"
homepage: "https://github.com/wagoodman/dive/"
description: "A tool for exploring each layer in a docker image"
description: "A tool for exploring layers in a docker image"
archives:
- format: tar.gz
@ -30,14 +37,14 @@ nfpms:
- license: MIT
maintainer: Alex Goodman
homepage: https://github.com/wagoodman/dive/
description: "A tool for exploring each layer in a docker image"
description: "A tool for exploring layers in a docker image"
formats:
- rpm
- deb
dockers:
-
binaries:
ids:
- dive
dockerfile: Dockerfile
# todo: on 1.0 remove 'v' prefix

View file

@ -1,37 +0,0 @@
#!/bin/bash
set -u
BOLD=$(tput bold)
NORMAL=$(tput sgr0)
echo "${BOLD}Tagging${NORMAL}"
#get highest tag number
VERSION=`git describe --abbrev=0 --tags`
#replace . with space so can split into an array
VERSION_BITS=(${VERSION//./ })
#get number parts and increase last one by 1
VNUM1=${VERSION_BITS[0]}
VNUM2=${VERSION_BITS[1]}
VNUM3=${VERSION_BITS[2]}
VNUM3=$((VNUM3+1))
#create new tag
NEW_TAG="$VNUM1.$VNUM2.$VNUM3"
echo "Updating $VERSION to $NEW_TAG"
#get current hash and see if it already has a tag
GIT_COMMIT=`git rev-parse HEAD`
NEEDS_TAG=`git describe --contains $GIT_COMMIT`
#only tag if no tag already (would be better if the git describe command above could have a silent option)
if [ -z "$NEEDS_TAG" ]; then
echo "Tagged with $NEW_TAG (Ignoring fatal:cannot describe - this means commit is untagged) "
git tag $NEW_TAG
git push --tags
else
echo "Already a tag on this commit"
fi

View file

@ -1,55 +0,0 @@
#!/bin/sh
# Generate test coverage statistics for Go packages.
#
# Works around the fact that `go test -coverprofile` currently does not work
# with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909
#
# Usage: script/coverage [--html|--coveralls]
#
# --html Additionally create HTML report and open it in browser
# --coveralls Push coverage statistics to coveralls.io
#
# Source: https://github.com/mlafeldt/chef-runner/blob/v0.7.0/script/coverage
set -e
workdir=.cover
profile="$workdir/cover.out"
mode=count
generate_cover_data() {
rm -rf "$workdir"
mkdir "$workdir"
for pkg in "$@"; do
f="$workdir/$(echo $pkg | tr / -).cover"
go test -v -covermode="$mode" -coverprofile="$f" "$pkg"
done
echo "mode: $mode" >"$profile"
grep -h -v "^mode:" "$workdir"/*.cover >>"$profile"
}
show_cover_report() {
go tool cover -${1}="$profile"
}
push_to_coveralls() {
echo "Pushing coverage statistics to coveralls.io"
goveralls -coverprofile="$profile"
}
generate_cover_data $(go list ./...)
case "$1" in
"")
show_cover_report func
;;
--html)
show_cover_report html
;;
--coveralls)
push_to_coveralls
;;
*)
echo >&2 "error: invalid option: $1"; exit 1 ;;
esac

View file

@ -1,4 +1,4 @@
FROM alpine:3.12
FROM alpine:3.18
ARG DOCKER_CLI_VERSION=${DOCKER_CLI_VERSION}
RUN wget -O- https://download.docker.com/linux/static/stable/$(uname -m)/docker-${DOCKER_CLI_VERSION}.tgz | \

321
Makefile
View file

@ -1,62 +1,201 @@
BIN = dive
BUILD_DIR = ./dist/dive_linux_amd64
BUILD_PATH = $(BUILD_DIR)/$(BIN)
TEMP_DIR = ./.tmp
PWD := ${CURDIR}
PRODUCTION_REGISTRY = docker.io
SHELL = /bin/bash -o pipefail
TEST_IMAGE = busybox:latest
all: gofmt clean build
# Tool versions #################################
GOLANG_CI_VERSION = v1.52.2
GOBOUNCER_VERSION = v0.4.0
GORELEASER_VERSION = v1.19.1
GOSIMPORTS_VERSION = v0.3.8
CHRONICLE_VERSION = v0.6.0
GLOW_VERSION = v1.5.0
DOCKER_CLI_VERSION = 23.0.6
## For CI
# Command templates #################################
LINT_CMD = $(TEMP_DIR)/golangci-lint run --tests=false --timeout=2m --config .golangci.yaml
GOIMPORTS_CMD = $(TEMP_DIR)/gosimports -local github.com/wagoodman
RELEASE_CMD = DOCKER_CLI_VERSION=$(DOCKER_CLI_VERSION) $(TEMP_DIR)/goreleaser release --clean
SNAPSHOT_CMD = $(RELEASE_CMD) --skip-publish --snapshot --skip-sign
CHRONICLE_CMD = $(TEMP_DIR)/chronicle
GLOW_CMD = $(TEMP_DIR)/glow
ci-unit-test:
go test -cover -v -race ./...
# Formatting variables #################################
BOLD := $(shell tput -T linux bold)
PURPLE := $(shell tput -T linux setaf 5)
GREEN := $(shell tput -T linux setaf 2)
CYAN := $(shell tput -T linux setaf 6)
RED := $(shell tput -T linux setaf 1)
RESET := $(shell tput -T linux sgr0)
TITLE := $(BOLD)$(PURPLE)
SUCCESS := $(BOLD)$(GREEN)
ci-static-analysis:
grep -R 'const allowTestDataCapture = false' runtime/ui/viewmodel
go vet ./...
gofmt -s -l . 2>&1 | grep -vE '^\.git/' | grep -vE '^\.cache/'
golangci-lint run
# Test variables #################################
# the quality gate lower threshold for unit test total % coverage (by function statements)
COVERAGE_THRESHOLD := 55
ci-install-go-tools:
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sudo sh -s -- -b /usr/local/bin/ latest
## Build variables #################################
DIST_DIR = dist
SNAPSHOT_DIR = snapshot
OS=$(shell uname | tr '[:upper:]' '[:lower:]')
SNAPSHOT_BIN=$(realpath $(shell pwd)/$(SNAPSHOT_DIR)/$(OS)-build_$(OS)_amd64_v1/$(BIN))
CHANGELOG := CHANGELOG.md
VERSION=$(shell git describe --dirty --always --tags)
ci-install-ci-tools:
curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sudo sh -s -- -b /usr/local/bin/ "v0.122.0"
ifeq "$(strip $(VERSION))" ""
override VERSION = $(shell git describe --always --tags --dirty)
endif
ci-docker-login:
echo '${DOCKER_PASSWORD}' | docker login -u '${DOCKER_USERNAME}' --password-stdin '${PRODUCTION_REGISTRY}'
## Variable assertions
ci-docker-logout:
docker logout '${PRODUCTION_REGISTRY}'
ifndef TEMP_DIR
$(error TEMP_DIR is not set)
endif
ci-publish-release:
goreleaser --rm-dist
ifndef DIST_DIR
$(error DIST_DIR is not set)
endif
ci-build-snapshot-packages:
goreleaser \
--snapshot \
--skip-publish \
--rm-dist
ifndef SNAPSHOT_DIR
$(error SNAPSHOT_DIR is not set)
endif
ci-release:
goreleaser release --rm-dist
define title
@printf '$(TITLE)$(1)$(RESET)\n'
endef
.PHONY: all
all: clean static-analysis test ## Run all static analysis and tests
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
.PHONY: test
test: unit ## Run all tests (currently unit and cli tests)
$(TEMP_DIR):
mkdir -p $(TEMP_DIR)
## Bootstrapping targets #################################
.PHONY: bootstrap-tools
bootstrap-tools: $(TEMP_DIR)
$(call title,Bootstrapping tools)
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMP_DIR)/ $(CHRONICLE_VERSION)
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMP_DIR)/ $(GOLANG_CI_VERSION)
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMP_DIR)/ $(GOBOUNCER_VERSION)
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/goreleaser/goreleaser@$(GORELEASER_VERSION)
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@$(GOSIMPORTS_VERSION)
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/charmbracelet/glow@$(GLOW_VERSION)
.PHONY: bootstrap-go
bootstrap-go:
$(call title,Bootstrapping go dependencies)
go mod download
.PHONY: bootstrap
bootstrap: bootstrap-go bootstrap-tools ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
## Development targets ###################################
#run: build
# $(BUILD_PATH) build -t dive-example:latest -f .data/Dockerfile.example .
#
#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
#
#dev:
# docker run -ti --rm -v $(PWD):/app -w /app -v dive-pkg:/go/pkg/ golang:1.13 bash
#
#build: gofmt
# go build -o $(BUILD_PATH)
.PHONY: generate-test-data
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!'
## Static analysis targets #################################
.PHONY: static-analysis
static-analysis: lint check-go-mod-tidy check-licenses
.PHONY: lint
lint: ## Run gofmt + golangci lint checks
$(call title,Running linters)
# ensure there are no go fmt differences
@printf "files with gofmt issues: [$(shell gofmt -l -s .)]\n"
@test -z "$(shell gofmt -l -s .)"
# run all golangci-lint rules
$(LINT_CMD)
@[ -z "$(shell $(GOIMPORTS_CMD) -d .)" ] || (echo "goimports needs to be fixed" && false)
# go tooling does not play well with certain filename characters, ensure the common cases don't result in future "go get" failures
$(eval MALFORMED_FILENAMES := $(shell find . | grep -e ':'))
@bash -c "[[ '$(MALFORMED_FILENAMES)' == '' ]] || (printf '\nfound unsupported filename characters:\n$(MALFORMED_FILENAMES)\n\n' && false)"
.PHONY: format
format: ## Auto-format all source code
$(call title,Running formatters)
gofmt -w -s .
$(GOIMPORTS_CMD) -w .
go mod tidy
.PHONY: lint-fix
lint-fix: format ## Auto-format all source code + run golangci lint fixers
$(call title,Running lint fixers)
$(LINT_CMD) --fix
.PHONY: check-licenses
check-licenses:
$(TEMP_DIR)/bouncer check ./...
check-go-mod-tidy:
@ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
## Testing targets #################################
.PHONY: unit
unit: $(TEMP_DIR) ## Run unit tests (with coverage)
$(call title,Running unit tests)
go test -race -coverprofile $(TEMP_DIR)/unit-coverage-details.txt ./...
@.github/scripts/coverage.py $(COVERAGE_THRESHOLD) $(TEMP_DIR)/unit-coverage-details.txt
## Acceptance testing targets (CI only) #################################
# todo: add --pull=never when supported by host box
ci-test-production-image:
.PHONY: ci-test-docker-image
ci-test-docker-image:
docker run \
--rm \
-t \
-v //var/run/docker.sock://var/run/docker.sock \
-v /var/run/docker.sock:/var/run/docker.sock \
'${PRODUCTION_REGISTRY}/wagoodman/dive:latest' \
'${TEST_IMAGE}' \
--ci
.PHONY: ci-test-deb-package-install
ci-test-deb-package-install:
docker run \
-v //var/run/docker.sock://var/run/docker.sock \
-v /${PWD}://src \
-w //src \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /${PWD}:/src \
-w /src \
ubuntu:latest \
/bin/bash -x -c "\
apt update && \
@ -65,76 +204,120 @@ ci-test-deb-package-install:
tar -vxzf - docker/docker --strip-component=1 && \
mv docker /usr/local/bin/ &&\
docker version && \
apt install ./dist/dive_*_linux_amd64.deb -y && \
apt install ./snapshot/dive_*_linux_amd64.deb -y && \
dive --version && \
dive '${TEST_IMAGE}' --ci \
"
.PHONY: ci-test-deb-package-install
ci-test-rpm-package-install:
docker run \
-v //var/run/docker.sock://var/run/docker.sock \
-v /${PWD}://src \
-w //src \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /${PWD}:/src \
-w /src \
fedora:latest \
/bin/bash -x -c "\
curl -L 'https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_CLI_VERSION}.tgz' | \
tar -vxzf - docker/docker --strip-component=1 && \
mv docker /usr/local/bin/ &&\
docker version && \
dnf install ./dist/dive_*_linux_amd64.rpm -y && \
dnf install ./snapshot/dive_*_linux_amd64.rpm -y && \
dive --version && \
dive '${TEST_IMAGE}' --ci \
"
.PHONY: ci-test-linux-run
ci-test-linux-run:
chmod 755 ./dist/dive_linux_amd64/dive && \
./dist/dive_linux_amd64/dive '${TEST_IMAGE}' --ci && \
./dist/dive_linux_amd64/dive --source docker-archive .data/test-kaniko-image.tar --ci --ci-config .data/.dive-ci
ls -la $(SNAPSHOT_DIR)
ls -la $(SNAPSHOT_DIR)/dive_linux_amd64_v1
chmod 755 $(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive && \
$(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive '${TEST_IMAGE}' --ci && \
$(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive --source docker-archive .data/test-kaniko-image.tar --ci --ci-config .data/.dive-ci
# we're not attempting to test docker, just our ability to run on these systems. This avoids setting up docker in CI.
.PHONY: ci-test-mac-run
ci-test-mac-run:
chmod 755 ./dist/dive_darwin_amd64/dive && \
./dist/dive_darwin_amd64/dive --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
chmod 755 $(SNAPSHOT_DIR)/dive_darwin_amd64_v1/dive && \
$(SNAPSHOT_DIR)/dive_darwin_amd64_v1/dive --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
# we're not attempting to test docker, just our ability to run on these systems. This avoids setting up docker in CI.
.PHONY: ci-test-windows-run
ci-test-windows-run:
./dist/dive_windows_amd64/dive --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
dive.exe --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
## Build-related targets #################################
## For development
.PHONY: build
build: $(SNAPSHOT_DIR) ## Build release snapshot binaries and packages
run: build
$(BUILD_PATH) build -t dive-example:latest -f .data/Dockerfile.example .
$(SNAPSHOT_DIR): ## Build snapshot release binaries and packages
$(call title,Building snapshot artifacts)
run-large: build
$(BUILD_PATH) amir20/clashleaders:latest
@# create a config with the dist dir overridden
@echo "dist: $(SNAPSHOT_DIR)" > $(TEMP_DIR)/goreleaser.yaml
@cat .goreleaser.yaml >> $(TEMP_DIR)/goreleaser.yaml
run-podman: build
podman build -t dive-example:latest -f .data/Dockerfile.example .
$(BUILD_PATH) localhost/dive-example:latest --engine podman
@# build release snapshots
@bash -c "\
VERSION=$(VERSION:v%=%) \
$(SNAPSHOT_CMD) --config $(TEMP_DIR)/goreleaser.yaml \
"
run-podman-large: build
$(BUILD_PATH) docker.io/amir20/clashleaders:latest --engine podman
.PHONY: cli
cli: $(SNAPSHOT_DIR) ## Run CLI tests
chmod 755 "$(SNAPSHOT_BIN)"
$(SNAPSHOT_BIN) version
go test -count=1 -timeout=15m -v ./test/cli
run-ci: build
CI=true $(BUILD_PATH) dive-example:latest --ci-config .data/.dive-ci
.PHONY: changelog
changelog: clean-changelog ## Generate and show the changelog for the current unreleased version
$(CHRONICLE_CMD) -vvv -n --version-file VERSION > $(CHANGELOG)
@$(GLOW_CMD) $(CHANGELOG)
build: gofmt
go build -o $(BUILD_PATH)
$(CHANGELOG):
$(CHRONICLE_CMD) -vvv > $(CHANGELOG)
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!'
.PHONY: release
release: ## Cut a new release
@.github/scripts/trigger-release.sh
test: gofmt
./.scripts/test-coverage.sh
.PHONY: release
ci-release: ci-check clean-dist $(CHANGELOG)
$(call title,Publishing release artifacts)
dev:
docker run -ti --rm -v $(PWD):/app -w /app -v dive-pkg:/go/pkg/ golang:1.13 bash
# create a config with the dist dir overridden
echo "dist: $(DIST_DIR)" > $(TEMP_DIR)/goreleaser.yaml
cat .goreleaser.yaml >> $(TEMP_DIR)/goreleaser.yaml
clean:
rm -rf dist
go clean
bash -c "$(RELEASE_CMD) --release-notes <(cat CHANGELOG.md) --config $(TEMP_DIR)/goreleaser.yaml"
.PHONY: ci-check
ci-check:
@.github/scripts/ci-check.sh
## Cleanup targets #################################
.PHONY: clean
clean: clean-dist clean-snapshot ## Remove previous builds, result reports, and test cache
.PHONY: clean-snapshot
clean-snapshot:
rm -rf $(SNAPSHOT_DIR) $(TEMP_DIR)/goreleaser.yaml
.PHONY: clean-dist
clean-dist: clean-changelog
rm -rf $(DIST_DIR) $(TEMP_DIR)/goreleaser.yaml
.PHONY: clean-changelog
clean-changelog:
rm -f $(CHANGELOG) VERSION
## Halp! #################################
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
gofmt:
go fmt -x ./...

View file

@ -1,6 +1,8 @@
# dive
[![GitHub release](https://img.shields.io/github/release/wagoodman/dive.svg)](https://github.com/wagoodman/dive/releases/latest)
[![Validations](https://github.com/wagoodman/dive/actions/workflows/validations.yaml/badge.svg)](https://github.com/wagoodman/dive/actions/workflows/validations.yaml)
[![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)
[![License: MIT](https://img.shields.io/badge/License-MIT%202.0-blue.svg)](https://github.com/wagoodman/dive/blob/main/LICENSE)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?style=flat)](https://www.paypal.me/wagoodman)
**A tool for exploring a docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image.**
@ -13,6 +15,15 @@ To analyze a Docker image simply run dive with an image tag/id/digest:
dive <your-image-tag>
```
or you can dive with docker command directly
```
alias dive="docker run -ti --rm -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive"
dive <your-image-tag>
# for example
dive nginx:latest
```
or if you want to build your image then jump straight into analyzing it:
```bash
dive build -t <some-tag> .
@ -83,27 +94,37 @@ With valid `source` options as such:
## Installation
**Ubuntu/Debian**
Using debs:
```bash
wget https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_linux_amd64.deb
sudo apt install ./dive_0.9.2_linux_amd64.deb
DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
curl -OL https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.deb
sudo apt install ./dive_${DIVE_VERSION}_linux_amd64.deb
```
Using snap:
```bash
sudo snap install docker
sudo snap install dive
sudo snap connect dive:docker-executables docker:docker-executables
sudo snap connect dive:docker-daemon docker:docker-daemon
```
**RHEL/Centos**
```bash
curl -OL https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_linux_amd64.rpm
rpm -i dive_0.9.2_linux_amd64.rpm
DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
curl -OL https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.rpm
rpm -i dive_${DIVE_VERSION}_linux_amd64.rpm
```
**Arch Linux**
Available as [dive](https://aur.archlinux.org/packages/dive/) in the Arch User Repository (AUR).
Available in the [extra repository](https://archlinux.org/packages/extra/x86_64/dive/) and can be installed via [pacman](https://wiki.archlinux.org/title/Pacman):
```bash
yay -S dive
pacman -S dive
```
The above example assumes [`yay`](https://aur.archlinux.org/packages/yay/) as the tool for installing AUR packages.
**Mac**
If you use [Homebrew](https://brew.sh):
@ -118,11 +139,11 @@ If you use [MacPorts](https://www.macports.org):
sudo port install dive
```
Or download the latest Darwin build from the [releases page](https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_darwin_amd64.tar.gz).
Or download the latest Darwin build from the [releases page](https://github.com/wagoodman/dive/releases/latest).
**Windows**
Download the [latest release](https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_windows_amd64.zip).
Download the [latest release](https://github.com/wagoodman/dive/releases/latest).
**Go tools**
Requires Go version 1.10 or higher.
@ -132,6 +153,17 @@ go get github.com/wagoodman/dive
```
*Note*: installing in this way you will not see a proper version when running `dive -v`.
**Nix/NixOS**
On NixOS:
```bash
nix-env -iA nixos.dive
```
On non-NixOS (Linux, Mac)
```bash
nix-env -iA nixpkgs.dive
```
**Docker**
```bash
docker pull wagoodman/dive
@ -193,7 +225,7 @@ You can override the CI config path with the `--ci-config` option.
Key Binding | Description
-------------------------------------------|---------------------------------------------------------
<kbd>Ctrl + C</kbd> | Exit
<kbd>Ctrl + C</kbd> or <kbd>Q</kbd> | Exit
<kbd>Tab</kbd> | Switch between the layer and filetree views
<kbd>Ctrl + F</kbd> | Filter files
<kbd>PageUp</kbd> | Scroll up a page
@ -275,3 +307,5 @@ dive will search for configs in the following locations:
- `$XDG_CONFIG_DIRS/dive/*.yaml`
- `~/.config/dive/*.yaml`
- `~/.dive.yaml`
`.yml` can be used instead of `.yaml` if desired.

24
RELEASE.md Normal file
View file

@ -0,0 +1,24 @@
# Release process
## Creating a release
**Trigger a new release with `make release`**.
At this point you'll see a preview changelog in the terminal. If you're happy with the
changelog, press `y` to continue, otherwise you can abort and adjust the labels on the
PRs and issues to be included in the release and re-run the release trigger command.
## Retracting a release
If a release is found to be problematic, it can be retracted with the following steps:
- Deleting the GitHub Release
- Untag the docker images in the `docker.io` registry
- Revert the brew formula in [`wagoodman/homebrew-dive`](https://github.com/wagoodman/homebrew-dive) to point to the previous release
- Add a new `retract` entry in the go.mod for the versioned release
**Note**: do not delete release tags from the git repository since there may already be references to the release
in the go proxy, which will cause confusion when trying to reuse the tag later (the H1 hash will not match and there
will be a warning when users try to pull the new release).

View file

@ -2,19 +2,19 @@ package cmd
import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive"
"os"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive"
"github.com/wagoodman/dive/runtime"
)
// doAnalyzeCmd takes a docker image tag, digest, or id and displays the
// image analysis to the screen
func doAnalyzeCmd(cmd *cobra.Command, args []string) {
if len(args) == 0 {
printVersionFlag, err := cmd.PersistentFlags().GetBool("version")
if err == nil && printVersionFlag {

View file

@ -3,6 +3,7 @@ package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive"
"github.com/wagoodman/dive/runtime"
)

View file

@ -3,7 +3,6 @@ package cmd
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"strconv"
@ -11,7 +10,6 @@ import (
)
func configureCi() (bool, *viper.Viper, error) {
isCiFromEnv, _ := strconv.ParseBool(os.Getenv("CI"))
isCi = isCi || isCiFromEnv
@ -21,7 +19,7 @@ func configureCi() (bool, *viper.Viper, error) {
if _, err := os.Stat(ciConfigFile); !os.IsNotExist(err) {
fmt.Printf(" Using CI config: %s\n", ciConfigFile)
fileBytes, err := ioutil.ReadFile(ciConfigFile)
fileBytes, err := os.ReadFile(ciConfigFile)
if err != nil {
return isCi, nil, err
}

View file

@ -2,18 +2,18 @@ package cmd
import (
"fmt"
"io/ioutil"
"io"
"os"
"path"
"strings"
"github.com/wagoodman/dive/dive"
"github.com/wagoodman/dive/dive/filetree"
"github.com/mitchellh/go-homedir"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive"
"github.com/wagoodman/dive/dive/filetree"
)
var cfgFile string
@ -77,7 +77,7 @@ func initConfig() {
viper.SetDefault("log.path", "./dive.log")
viper.SetDefault("log.enabled", false)
// keybindings: status view / global
viper.SetDefault("keybinding.quit", "ctrl+c")
viper.SetDefault("keybinding.quit", "ctrl+c,q")
viper.SetDefault("keybinding.toggle-view", "tab")
viper.SetDefault("keybinding.filter-files", "ctrl+f, ctrl+slash")
// keybindings: layer view
@ -86,6 +86,7 @@ func initConfig() {
// keybindings: filetree view
viper.SetDefault("keybinding.toggle-collapse-dir", "space")
viper.SetDefault("keybinding.toggle-collapse-all-dir", "ctrl+space")
viper.SetDefault("keybinding.toggle-sort-order", "ctrl+o")
viper.SetDefault("keybinding.toggle-filetree-attributes", "ctrl+b")
viper.SetDefault("keybinding.toggle-added-files", "ctrl+a")
viper.SetDefault("keybinding.toggle-removed-files", "ctrl+r")
@ -146,7 +147,7 @@ func initLogging() {
logFileObj, err = os.OpenFile(viper.GetString("log.path"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
log.SetOutput(logFileObj)
} else {
log.SetOutput(ioutil.Discard)
log.SetOutput(io.Discard)
}
if err != nil {
@ -198,14 +199,14 @@ func getDefaultCfgFile() string {
// if not found returns empty string
func findInPath(pathTo string) string {
directory := path.Join(pathTo, "dive")
files, err := ioutil.ReadDir(directory)
files, err := os.ReadDir(directory)
if err != nil {
return ""
}
for _, file := range files {
filename := file.Name()
if path.Ext(filename) == ".yaml" {
if path.Ext(filename) == ".yaml" || path.Ext(filename) == ".yml" {
return path.Join(directory, filename)
}
}

View file

@ -2,6 +2,7 @@ package filetree
import (
"fmt"
"github.com/sirupsen/logrus"
)
@ -52,8 +53,8 @@ func (cmp *Comparer) GetPathErrors(key TreeIndexKey) ([]PathError, error) {
}
func (cmp *Comparer) GetTree(key TreeIndexKey) (*FileTree, error) {
//func (cmp *Comparer) GetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) (*FileTree, []PathError, error) {
//key := TreeIndexKey{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}
// func (cmp *Comparer) GetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) (*FileTree, []PathError, error) {
// key := TreeIndexKey{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}
if value, exists := cmp.trees[key]; exists {
return value, nil
@ -114,7 +115,6 @@ func (cmp *Comparer) NaturalIndexes() <-chan TreeIndexKey {
}
}()
return indexes
}
// case 2: aggregated compare (bottom tree is ENTIRELY fixed, top tree SIZE changes)
@ -146,7 +146,6 @@ func (cmp *Comparer) AggregatedIndexes() <-chan TreeIndexKey {
}
}()
return indexes
}
func (cmp *Comparer) BuildCache() (errors []error) {

View file

@ -79,13 +79,12 @@ func Efficiency(trees []*FileTree) (float64, EfficiencySlice) {
}
if previousTreeNode.Data.FileInfo.IsDir {
err = previousTreeNode.VisitDepthChildFirst(sizer, nil)
err = previousTreeNode.VisitDepthChildFirst(sizer, nil, nil)
if err != nil {
logrus.Errorf("unable to propagate whiteout dir: %+v", err)
return err
}
}
} else {
sizeBytes = node.Data.FileInfo.Size
}

View file

@ -2,10 +2,11 @@ package filetree
import (
"archive/tar"
"github.com/cespare/xxhash"
"github.com/sirupsen/logrus"
"io"
"os"
"github.com/cespare/xxhash"
"github.com/sirupsen/logrus"
)
// FileInfo contains tar metadata for a specific FileNode
@ -56,7 +57,6 @@ func NewFileInfo(realPath, path string, info os.FileInfo) FileInfo {
if err != nil {
logrus.Panic("unable to read link:", realPath, err)
}
} else if info.IsDir() {
fileType = tar.TypeDir
} else {

View file

@ -3,14 +3,12 @@ package filetree
import (
"archive/tar"
"fmt"
"sort"
"strings"
"github.com/sirupsen/logrus"
"github.com/dustin/go-humanize"
"github.com/fatih/color"
"github.com/phayes/permbits"
"github.com/sirupsen/logrus"
)
const (
@ -28,6 +26,7 @@ var diffTypeColor = map[DiffType]*color.Color{
type FileNode struct {
Tree *FileTree
Parent *FileNode
Size int64 // memoized total size of file or directory
Name string
Data NodeData
Children map[string]*FileNode
@ -40,6 +39,7 @@ func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) {
node.Name = name
node.Data = *NewNodeData()
node.Data.FileInfo = *data.Copy()
node.Size = -1 // signal lazy load later
node.Children = make(map[string]*FileNode)
node.Parent = parent
@ -150,41 +150,49 @@ func (node *FileNode) MetadataString() string {
group := node.Data.FileInfo.Gid
userGroup := fmt.Sprintf("%d:%d", user, group)
var sizeBytes int64
if node.IsLeaf() {
sizeBytes = node.Data.FileInfo.Size
} else {
sizer := func(curNode *FileNode) error {
// don't include file sizes of children that have been removed (unless the node in question is a removed dir,
// then show the accumulated size of removed files)
if curNode.Data.DiffType != Removed || node.Data.DiffType == Removed {
sizeBytes += curNode.Data.FileInfo.Size
}
return nil
}
err := node.VisitDepthChildFirst(sizer, nil)
if err != nil {
logrus.Errorf("unable to propagate node for metadata: %+v", err)
}
}
// don't include file sizes of children that have been removed (unless the node in question is a removed dir,
// then show the accumulated size of removed files)
sizeBytes := node.GetSize()
size := humanize.Bytes(uint64(sizeBytes))
return diffTypeColor[node.Data.DiffType].Sprint(fmt.Sprintf(AttributeFormat, dir, fileMode, userGroup, size))
}
// VisitDepthChildFirst iterates a tree depth-first (starting at this FileNode), evaluating the deepest depths first (visit on bubble up)
func (node *FileNode) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator) error {
var keys []string
for key := range node.Children {
keys = append(keys, key)
func (node *FileNode) GetSize() int64 {
if 0 <= node.Size {
return node.Size
}
sort.Strings(keys)
var sizeBytes int64
if node.IsLeaf() {
sizeBytes = node.Data.FileInfo.Size
} else {
sizer := func(curNode *FileNode) error {
if curNode.Data.DiffType != Removed || node.Data.DiffType == Removed {
sizeBytes += curNode.Data.FileInfo.Size
}
return nil
}
err := node.VisitDepthChildFirst(sizer, nil, nil)
if err != nil {
logrus.Errorf("unable to propagate node for metadata: %+v", err)
}
}
node.Size = sizeBytes
return node.Size
}
// VisitDepthChildFirst iterates a tree depth-first (starting at this FileNode), evaluating the deepest depths first (visit on bubble up)
func (node *FileNode) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator, sorter OrderStrategy) error {
if sorter == nil {
sorter = GetSortOrderStrategy(ByName)
}
keys := sorter.orderKeys(node.Children)
for _, name := range keys {
child := node.Children[name]
err := child.VisitDepthChildFirst(visitor, evaluator)
err := child.VisitDepthChildFirst(visitor, evaluator, sorter)
if err != nil {
return err
}
@ -200,7 +208,7 @@ func (node *FileNode) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvalu
}
// VisitDepthParentFirst iterates a tree depth-first (starting at this FileNode), evaluating the shallowest depths first (visit while sinking down)
func (node *FileNode) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator) error {
func (node *FileNode) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator, sorter OrderStrategy) error {
var err error
doVisit := evaluator != nil && evaluator(node) || evaluator == nil
@ -217,14 +225,13 @@ func (node *FileNode) VisitDepthParentFirst(visitor Visitor, evaluator VisitEval
}
}
var keys []string
for key := range node.Children {
keys = append(keys, key)
if sorter == nil {
sorter = GetSortOrderStrategy(ByName)
}
sort.Strings(keys)
keys := sorter.orderKeys(node.Children)
for _, name := range keys {
child := node.Children[name]
err = child.VisitDepthParentFirst(visitor, evaluator)
err = child.VisitDepthParentFirst(visitor, evaluator, sorter)
if err != nil {
return err
}

View file

@ -3,7 +3,6 @@ package filetree
import (
"fmt"
"path"
"sort"
"strings"
"github.com/google/uuid"
@ -24,11 +23,12 @@ const (
// FileTree represents a set of files, directories, and their relations.
type FileTree struct {
Root *FileNode
Size int
FileSize uint64
Name string
Id uuid.UUID
Root *FileNode
Size int
FileSize uint64
Name string
Id uuid.UUID
SortOrder SortOrder
}
// NewFileTree creates an empty FileTree
@ -39,6 +39,7 @@ func NewFileTree() (tree *FileTree) {
tree.Root.Tree = tree
tree.Root.Children = make(map[string]*FileNode)
tree.Id = uuid.New()
tree.SortOrder = ByName
return tree
}
@ -67,12 +68,8 @@ func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttribu
currentParams, paramsToVisit = paramsToVisit[0], paramsToVisit[1:]
// take note of the next nodes to visit later
var keys []string
for key := range currentParams.node.Children {
keys = append(keys, key)
}
// we should always visit nodes in order
sort.Strings(keys)
sorter := GetSortOrderStrategy(tree.SortOrder)
keys := sorter.orderKeys(currentParams.node.Children)
var childParams = make([]renderParams, 0)
for idx, name := range keys {
@ -174,6 +171,7 @@ func (tree *FileTree) Copy() *FileTree {
newTree.Size = tree.Size
newTree.FileSize = tree.FileSize
newTree.Root = tree.Root.Copy(newTree.Root)
newTree.SortOrder = tree.SortOrder
// update the tree pointers
err := newTree.VisitDepthChildFirst(func(node *FileNode) error {
@ -196,12 +194,14 @@ type VisitEvaluator func(*FileNode) bool
// VisitDepthChildFirst iterates the given tree depth-first, evaluating the deepest depths first (visit on bubble up)
func (tree *FileTree) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator) error {
return tree.Root.VisitDepthChildFirst(visitor, evaluator)
sorter := GetSortOrderStrategy(tree.SortOrder)
return tree.Root.VisitDepthChildFirst(visitor, evaluator, sorter)
}
// VisitDepthParentFirst iterates the given tree depth-first, evaluating the shallowest depths first (visit while sinking down)
func (tree *FileTree) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator) error {
return tree.Root.VisitDepthParentFirst(visitor, evaluator)
sorter := GetSortOrderStrategy(tree.SortOrder)
return tree.Root.VisitDepthParentFirst(visitor, evaluator, sorter)
}
// Stack takes two trees and combines them together. This is done by "stacking" the given tree on top of the owning tree.
@ -277,7 +277,6 @@ func (tree *FileTree) AddPath(filepath string, data FileInfo) (*FileNode, []*Fil
if idx == len(nodeNames)-1 {
node.Data.FileInfo = data
}
}
return node, addedNodes, nil
}

View file

@ -0,0 +1,61 @@
package filetree
import (
"sort"
)
type SortOrder int
const (
ByName = iota
BySizeDesc
NumSortOrderConventions
)
type OrderStrategy interface {
orderKeys(files map[string]*FileNode) []string
}
func GetSortOrderStrategy(sortOrder SortOrder) OrderStrategy {
switch sortOrder {
case ByName:
return orderByNameStrategy{}
case BySizeDesc:
return orderBySizeDescStrategy{}
}
return orderByNameStrategy{}
}
type orderByNameStrategy struct{}
func (orderByNameStrategy) orderKeys(files map[string]*FileNode) []string {
var keys []string
for key := range files {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
type orderBySizeDescStrategy struct{}
func (orderBySizeDescStrategy) orderKeys(files map[string]*FileNode) []string {
var keys []string
for key := range files {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
ki, kj := keys[i], keys[j]
ni, nj := files[ki], files[kj]
if ni.GetSize() == nj.GetSize() {
return ki < kj
}
return ni.GetSize() > nj.GetSize()
})
return keys
}

View file

@ -2,11 +2,12 @@ package dive
import (
"fmt"
"net/url"
"strings"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/dive/image/docker"
"github.com/wagoodman/dive/dive/image/podman"
"net/url"
"strings"
)
const (
@ -56,7 +57,6 @@ func DeriveImageSource(image string) (ImageSource, string) {
return SourceDockerArchive, imageSource
case "docker-tar":
return SourceDockerArchive, imageSource
}
return SourceUnknown, ""
}

View file

@ -2,8 +2,9 @@ package docker
import (
"fmt"
"github.com/wagoodman/dive/dive/image"
"os"
"github.com/wagoodman/dive/dive/image"
)
type archiveResolver struct{}

View file

@ -1,12 +1,11 @@
package docker
import (
"io/ioutil"
"os"
)
func buildImageFromCli(buildArgs []string) (string, error) {
iidfile, err := ioutil.TempFile("/tmp", "dive.*.iid")
iidfile, err := os.CreateTemp("/tmp", "dive.*.iid")
if err != nil {
return "", err
}
@ -18,7 +17,7 @@ func buildImageFromCli(buildArgs []string) (string, error) {
return "", err
}
imageId, err := ioutil.ReadFile(iidfile.Name())
imageId, err := os.ReadFile(iidfile.Name())
if err != nil {
return "", err
}

View file

@ -2,9 +2,10 @@ package docker
import (
"fmt"
"github.com/wagoodman/dive/utils"
"os"
"os/exec"
"github.com/wagoodman/dive/utils"
)
// runDockerCmd runs a given Docker command in the current tty

View file

@ -2,6 +2,7 @@ package docker
import (
"encoding/json"
"github.com/sirupsen/logrus"
)

View file

@ -2,7 +2,6 @@ package docker
import (
"fmt"
"github.com/wagoodman/dive/dive/image"
"io"
"net/http"
"os"
@ -11,6 +10,8 @@ import (
"github.com/docker/cli/cli/connhelper"
"github.com/docker/docker/client"
"golang.org/x/net/context"
"github.com/wagoodman/dive/dive/image"
)
type engineResolver struct{}
@ -20,7 +21,6 @@ func NewResolverFromEngine() *engineResolver {
}
func (r *engineResolver) Fetch(id string) (*image.Image, error) {
reader, err := r.fetchArchive(id)
if err != nil {
return nil, err

View file

@ -2,10 +2,11 @@ package docker
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strings"
@ -47,7 +48,7 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
// some layer tars can be relative layer symlinks to other layer tars
if header.Typeflag == tar.TypeSymlink || header.Typeflag == tar.TypeReg {
// For the Docker image format, use file name conventions
if strings.HasSuffix(name, ".tar") {
currentLayer++
layerReader := tar.NewReader(tarReader)
@ -58,7 +59,6 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
// add the layer to the image
img.layerMap[tree.Name] = tree
} else if strings.HasSuffix(name, ".tar.gz") || strings.HasSuffix(name, "tgz") {
currentLayer++
@ -79,13 +79,61 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
// add the layer to the image
img.layerMap[tree.Name] = tree
} else if strings.HasSuffix(name, ".json") || strings.HasPrefix(name, "sha256:") {
fileBuffer, err := ioutil.ReadAll(tarReader)
fileBuffer, err := io.ReadAll(tarReader)
if err != nil {
return img, err
}
jsonFiles[name] = fileBuffer
} else if strings.HasPrefix(name, "blobs/") {
// For the OCI-compatible image format (used since Docker 25), use mime sniffing
// but limit this to only the blobs/ (containing the config, and the layers)
// The idea here is that we try various formats in turn, and those tries should
// never consume more bytes than this buffer contains so we can start again.
// 512 bytes ought to be enough (as that's the size of a TAR entry header),
// but play it safe with 1024 bytes. This should also include very small layers
// (unless they've also been gzipped, but Docker does not appear to do it)
buffer := make([]byte, 1024)
n, err := io.ReadFull(tarReader, buffer)
if err != nil && err != io.ErrUnexpectedEOF {
return img, err
}
// Only try reading a TAR if file is "big enough"
if n == cap(buffer) {
var unwrappedReader io.Reader
unwrappedReader, err = gzip.NewReader(io.MultiReader(bytes.NewReader(buffer[:n]), tarReader))
if err != nil {
// Not a gzipped entry
unwrappedReader = io.MultiReader(bytes.NewReader(buffer[:n]), tarReader)
}
// Try reading a TAR
layerReader := tar.NewReader(unwrappedReader)
tree, err := processLayerTar(name, layerReader)
if err == nil {
currentLayer++
// add the layer to the image
img.layerMap[tree.Name] = tree
continue
}
}
// Not a TAR (or smaller than our buffer), might be a JSON file
decoder := json.NewDecoder(bytes.NewReader(buffer[:n]))
token, err := decoder.Token()
if _, ok := token.(json.Delim); err == nil && ok {
// Looks like a JSON object (or array)
// XXX: should we add a header.Size check too?
fileBuffer, err := io.ReadAll(io.MultiReader(bytes.NewReader(buffer[:n]), tarReader))
if err != nil {
return img, err
}
jsonFiles[name] = fileBuffer
}
// Ignore every other unknown file type
}
}
}
@ -207,5 +255,4 @@ func (img *ImageArchive) ToImage() (*image.Image, error) {
Trees: trees,
Layers: layers,
}, nil
}

View file

@ -1,10 +1,10 @@
package docker
import (
"github.com/wagoodman/dive/dive/image"
"strings"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
)
// Layer represents a Docker image layer and metadata

View file

@ -2,6 +2,7 @@ package docker
import (
"encoding/json"
"github.com/sirupsen/logrus"
)

View file

@ -1,9 +1,10 @@
package docker
import (
"github.com/wagoodman/dive/dive/image"
"os"
"testing"
"github.com/wagoodman/dive/dive/image"
)
func TestLoadArchive(tarPath string) (*ImageArchive, error) {

View file

@ -10,7 +10,6 @@ type Image struct {
}
func (img *Image) Analyze() (*AnalysisResult, error) {
efficiency, inefficiencies := filetree.Efficiency(img.Trees)
var sizeBytes, userSizeBytes uint64

View file

@ -2,7 +2,10 @@ package image
import (
"fmt"
"strings"
"github.com/dustin/go-humanize"
"github.com/wagoodman/dive/dive/filetree"
)
@ -31,6 +34,12 @@ func (l *Layer) ShortId() string {
return id
}
func (l *Layer) commandPreview() string {
// Layers using heredocs can be multiple lines; rendering relies on
// Layer.String to be a single line.
return strings.Replace(l.Command, "\n", "↵", -1)
}
func (l *Layer) String() string {
if l.Index == 0 {
return fmt.Sprintf(LayerFormat,
@ -39,5 +48,5 @@ func (l *Layer) String() string {
}
return fmt.Sprintf(LayerFormat,
humanize.Bytes(l.Size),
l.Command)
l.commandPreview())
}

View file

@ -1,14 +1,14 @@
// +build linux
//go:build linux || darwin
// +build linux darwin
package podman
import (
"io/ioutil"
"os"
)
func buildImageFromCli(buildArgs []string) (string, error) {
iidfile, err := ioutil.TempFile("/tmp", "dive.*.iid")
iidfile, err := os.CreateTemp("/tmp", "dive.*.iid")
if err != nil {
return "", err
}
@ -20,7 +20,7 @@ func buildImageFromCli(buildArgs []string) (string, error) {
return "", err
}
imageId, err := ioutil.ReadFile(iidfile.Name())
imageId, err := os.ReadFile(iidfile.Name())
if err != nil {
return "", err
}

View file

@ -1,13 +1,15 @@
// +build linux
//go:build linux || darwin
// +build linux darwin
package podman
import (
"fmt"
"github.com/wagoodman/dive/utils"
"io"
"os"
"os/exec"
"github.com/wagoodman/dive/utils"
)
// runPodmanCmd runs a given Podman command in the current tty
@ -40,6 +42,7 @@ func streamPodmanCmd(args ...string) (error, io.Reader) {
if err != nil {
return err, nil
}
defer writer.Close()
cmd.Stdout = writer
cmd.Stderr = os.Stderr

View file

@ -1,10 +1,14 @@
//go:build linux || darwin
// +build linux darwin
package podman
import (
"fmt"
"io"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/dive/image/docker"
"io/ioutil"
)
type resolver struct{}
@ -38,7 +42,7 @@ func (r *resolver) resolveFromDockerArchive(id string) (*image.Image, error) {
return nil, err
}
img, err := docker.NewImageArchive(ioutil.NopCloser(reader))
img, err := docker.NewImageArchive(io.NopCloser(reader))
if err != nil {
return nil, err
}

View file

@ -1,9 +1,11 @@
// +build !linux
//go:build !linux && !darwin
// +build !linux,!darwin
package podman
import (
"fmt"
"github.com/wagoodman/dive/dive/image"
)

71
go.mod
View file

@ -1,62 +1,61 @@
module github.com/wagoodman/dive
go 1.13
go 1.19
require (
github.com/awesome-gocui/gocui v1.1.0
github.com/awesome-gocui/keybinding v1.0.1-0.20190805183143-864552bd36b7
github.com/cespare/xxhash v1.1.0
github.com/docker/cli v0.0.0-20190906153656-016a3232168d
github.com/docker/docker v24.0.7+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.7.0
github.com/google/uuid v1.1.1
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b
github.com/lunixbochs/vtclean v1.0.0
github.com/mitchellh/go-homedir v1.1.0
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee
github.com/sergi/go-diff v1.0.0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/afero v1.2.2
github.com/spf13/cobra v0.0.5
github.com/spf13/viper v1.4.0
golang.org/x/net v0.17.0
)
require (
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/awesome-gocui/gocui v0.6.0
github.com/awesome-gocui/keybinding v1.0.0
github.com/cespare/xxhash v1.1.0
github.com/docker/cli v0.0.0-20190906153656-016a3232168d
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v24.0.2+incompatible
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.7.0
github.com/fsnotify/fsnotify v1.4.7 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.1.1
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b
github.com/lunixbochs/vtclean v1.0.0
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
github.com/magiconair/properties v1.8.1 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.9 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/mattn/go-runewidth v0.0.10 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pelletier/go-toml v1.4.0 // indirect
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee
github.com/pkg/errors v0.9.1 // indirect
github.com/sergi/go-diff v1.0.0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/afero v1.2.2
github.com/spf13/cobra v0.0.5
github.com/rivo/uniseg v0.1.0 // indirect
github.com/spf13/cast v1.3.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.4.0
github.com/stretchr/testify v1.4.0 // indirect
golang.org/x/net v0.11.0
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
gotest.tools v2.2.0+incompatible // indirect
gotest.tools/v3 v3.5.0 // indirect
)
// relates to https://github.com/golangci/golangci-lint/issues/581
replace github.com/go-critic/go-critic => github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540
replace github.com/golangci/errcheck => github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6
replace github.com/golangci/go-tools => github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196
replace github.com/golangci/gofmt => github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98
replace github.com/golangci/gosec => github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547
replace github.com/golangci/ineffassign => github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc
replace github.com/golangci/lint-1 => github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217
replace mvdan.cc/unparam => mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34

78
go.sum
View file

@ -1,6 +1,5 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
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.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
@ -11,11 +10,10 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/awesome-gocui/gocui v0.5.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
github.com/awesome-gocui/gocui v0.6.0 h1:hhDJiQC12tEsJNJ+iZBBVaSSLFYo9llFuYpQlL5JZVI=
github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
github.com/awesome-gocui/keybinding v1.0.0 h1:CrnjCfEhWpjcqIQUan9IllaXeRGELdwfjeUmY7ljbng=
github.com/awesome-gocui/keybinding v1.0.0/go.mod h1:z0TyCwIhaT97yU+becTse8Dqh2CvYT0FLw0R0uTk0ag=
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc h1:wGNpKcHU8Aadr9yOzsT3GEsFLS7HQu8HxQIomnekqf0=
github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII=
github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg=
github.com/awesome-gocui/keybinding v1.0.1-0.20190805183143-864552bd36b7 h1:DDdWoFOtXWySkgCiGGn80TM/E2FS2T1qJBJJxup9+Vo=
github.com/awesome-gocui/keybinding v1.0.1-0.20190805183143-864552bd36b7/go.mod h1:z0TyCwIhaT97yU+becTse8Dqh2CvYT0FLw0R0uTk0ag=
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@ -29,8 +27,6 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -40,8 +36,8 @@ github.com/docker/cli v0.0.0-20190906153656-016a3232168d h1:gwX/88xJZfxZV1yjhhuQ
github.com/docker/cli v0.0.0-20190906153656-016a3232168d/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.2+incompatible h1:eATx+oLz9WdNVkQrr0qjQ8HvRJ4bOOxfzEo8R+dA3cg=
github.com/docker/docker v24.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
@ -52,8 +48,11 @@ 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/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@ -71,7 +70,6 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
@ -98,6 +96,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b h1:PMbSa9CgaiQR9NLlUTwKi+7aeLl3GG5JX5ERJxfQ3IE=
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@ -108,8 +108,9 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -145,6 +146,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
@ -184,7 +187,6 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
@ -194,16 +196,10 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -213,13 +209,8 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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=
@ -227,8 +218,6 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -242,29 +231,15 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -274,9 +249,6 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -2,16 +2,16 @@ package ci
import (
"fmt"
"github.com/dustin/go-humanize"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/utils"
"sort"
"strconv"
"strings"
"github.com/dustin/go-humanize"
"github.com/logrusorgru/aurora"
"github.com/spf13/viper"
"github.com/logrusorgru/aurora"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/utils"
)
type CiEvaluator struct {
@ -67,7 +67,6 @@ func (ci *CiEvaluator) Evaluate(analysis *image.AnalysisResult) bool {
message: "test",
}
}
}
if !canEvaluate {
@ -111,7 +110,6 @@ func (ci *CiEvaluator) Evaluate(analysis *image.AnalysisResult) bool {
status: status,
message: message,
}
}
ci.Tally.Total = len(ci.Results)
@ -174,7 +172,6 @@ func (ci *CiEvaluator) Report() string {
if ci.Misconfigured {
fmt.Fprintln(&sb, aurora.Red("CI Misconfigured"))
} else {
summary := fmt.Sprintf("Result:%s [Total:%d] [Passed:%d] [Failed:%d] [Warn:%d] [Skipped:%d]", status, ci.Tally.Total, ci.Tally.Pass, ci.Tally.Fail, ci.Tally.Warn, ci.Tally.Skip)
if ci.Pass {

View file

@ -1,11 +1,12 @@
package ci
import (
"github.com/wagoodman/dive/dive/image/docker"
"strings"
"testing"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive/image/docker"
)
func Test_Evaluator(t *testing.T) {

View file

@ -2,13 +2,13 @@ package ci
import (
"fmt"
"github.com/wagoodman/dive/dive/image"
"strconv"
"github.com/spf13/viper"
"github.com/dustin/go-humanize"
"github.com/logrusorgru/aurora"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive/image"
)
const (

View file

@ -2,6 +2,7 @@ package export
import (
"encoding/json"
diveImage "github.com/wagoodman/dive/dive/image"
)

View file

@ -1,9 +1,11 @@
package export
import (
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/wagoodman/dive/dive/image/docker"
"testing"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/wagoodman/dive/dive/image/docker"
)
func Test_Export(t *testing.T) {

View file

@ -2,6 +2,7 @@ package runtime
import (
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive"
)

View file

@ -2,9 +2,13 @@ package runtime
import (
"fmt"
"os"
"time"
"github.com/dustin/go-humanize"
"github.com/sirupsen/logrus"
"github.com/spf13/afero"
"github.com/wagoodman/dive/dive"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
@ -12,8 +16,6 @@ import (
"github.com/wagoodman/dive/runtime/export"
"github.com/wagoodman/dive/runtime/ui"
"github.com/wagoodman/dive/utils"
"os"
"time"
)
func run(enableUi bool, options Options, imageResolver image.Resolver, events eventChannel, filesystem afero.Fs) {
@ -84,7 +86,6 @@ func run(enableUi bool, options Options, imageResolver image.Resolver, events ev
}
return
} else {
events.message(utils.TitleFormat("Building cache..."))
treeStack := filetree.NewComparer(analysis.RefTrees)

View file

@ -2,14 +2,16 @@ package runtime
import (
"fmt"
"os"
"testing"
"github.com/lunixbochs/vtclean"
"github.com/spf13/afero"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/dive/image/docker"
"os"
"testing"
)
type defaultResolver struct{}

View file

@ -3,14 +3,14 @@ package ui
import (
"sync"
"github.com/awesome-gocui/gocui"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/key"
"github.com/wagoodman/dive/runtime/ui/layout"
"github.com/wagoodman/dive/runtime/ui/layout/compound"
"github.com/awesome-gocui/gocui"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/filetree"
)
const debug = false
@ -42,7 +42,7 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
lm := layout.NewManager()
lm.Add(controller.views.Status, layout.LocationFooter)
lm.Add(controller.views.Filter, layout.LocationFooter)
lm.Add(compound.NewLayerDetailsCompoundLayout(controller.views.Layer, controller.views.Details), layout.LocationColumn)
lm.Add(compound.NewLayerDetailsCompoundLayout(controller.views.Layer, controller.views.LayerDetails, controller.views.ImageDetails), layout.LocationColumn)
lm.Add(controller.views.Tree, layout.LocationColumn)
// todo: access this more programmatically
@ -50,7 +50,7 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
lm.Add(controller.views.Debug, layout.LocationColumn)
}
gui.Cursor = false
//g.Mouse = true
// g.Mouse = true
gui.SetManagerFunc(lm.Layout)
// var profileObj = profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook)
@ -76,6 +76,14 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
OnAction: controller.ToggleView,
Display: "Switch view",
},
{
Key: gocui.KeyArrowRight,
OnAction: controller.NextPane,
},
{
Key: gocui.KeyArrowLeft,
OnAction: controller.PrevPane,
},
{
ConfigKeys: []string{"keybinding.filter-files"},
OnAction: controller.ToggleFilterView,
@ -96,7 +104,6 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
if err != nil {
return
}
})
return appSingleton, err
@ -120,7 +127,6 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
// quit is the gocui callback invoked when the user hits Ctrl+C
func (a *app) quit() error {
// profileObj.Stop()
// onExit()
@ -142,6 +148,11 @@ func Run(imageName string, analysis *image.AnalysisResult, treeStack filetree.Co
return err
}
key, mod := gocui.MustParse("Ctrl+Z")
if err := g.SetKeybinding("", key, mod, handle_ctrl_z); err != nil {
return err
}
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
logrus.Error("main loop error: ", err)
return err

View file

@ -1,13 +1,15 @@
package ui
import (
"regexp"
"github.com/awesome-gocui/gocui"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/view"
"github.com/wagoodman/dive/runtime/ui/viewmodel"
"regexp"
)
type Controller struct {
@ -82,7 +84,7 @@ func (c *Controller) onFilterEdit(filter string) error {
func (c *Controller) onLayerChange(selection viewmodel.LayerSelection) error {
// update the details
c.views.Details.SetCurrentLayer(selection.Layer)
c.views.LayerDetails.CurrentLayer = selection.Layer
// update the filetree
err := c.views.Tree.SetTree(selection.BottomTreeStart, selection.BottomTreeStop, selection.TopTreeStart, selection.TopTreeStop)
@ -141,6 +143,56 @@ func (c *Controller) Render() error {
return nil
}
//nolint:dupl
func (c *Controller) NextPane() (err error) {
v := c.gui.CurrentView()
if v == nil {
panic("Current view is nil")
}
if v.Name() == c.views.Layer.Name() {
_, err = c.gui.SetCurrentView(c.views.LayerDetails.Name())
c.views.Status.SetCurrentView(c.views.LayerDetails)
} else if v.Name() == c.views.LayerDetails.Name() {
_, err = c.gui.SetCurrentView(c.views.ImageDetails.Name())
c.views.Status.SetCurrentView(c.views.ImageDetails)
} else if v.Name() == c.views.ImageDetails.Name() {
_, err = c.gui.SetCurrentView(c.views.Layer.Name())
c.views.Status.SetCurrentView(c.views.Layer)
}
if err != nil {
logrus.Error("unable to toggle view: ", err)
return err
}
return c.UpdateAndRender()
}
//nolint:dupl
func (c *Controller) PrevPane() (err error) {
v := c.gui.CurrentView()
if v == nil {
panic("Current view is nil")
}
if v.Name() == c.views.Layer.Name() {
_, err = c.gui.SetCurrentView(c.views.ImageDetails.Name())
c.views.Status.SetCurrentView(c.views.ImageDetails)
} else if v.Name() == c.views.LayerDetails.Name() {
_, err = c.gui.SetCurrentView(c.views.Layer.Name())
c.views.Status.SetCurrentView(c.views.Layer)
} else if v.Name() == c.views.ImageDetails.Name() {
_, err = c.gui.SetCurrentView(c.views.LayerDetails.Name())
c.views.Status.SetCurrentView(c.views.LayerDetails)
}
if err != nil {
logrus.Error("unable to toggle view: ", err)
return err
}
return c.UpdateAndRender()
}
// ToggleView switches between the file view and the layer view and re-renders the screen.
func (c *Controller) ToggleView() (err error) {
v := c.gui.CurrentView()

View file

@ -2,23 +2,24 @@ package format
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/lunixbochs/vtclean"
"strings"
)
const (
//selectedLeftBracketStr = " "
//selectedRightBracketStr = " "
//selectedFillStr = " "
// selectedLeftBracketStr = " "
// selectedRightBracketStr = " "
// selectedFillStr = " "
//
//leftBracketStr = "▏"
//rightBracketStr = "▕"
//fillStr = "─"
//selectedLeftBracketStr = " "
//selectedRightBracketStr = " "
//selectedFillStr = "━"
// selectedLeftBracketStr = " "
// selectedRightBracketStr = " "
// selectedFillStr = "━"
//
//leftBracketStr = "▏"
//rightBracketStr = "▕"
@ -33,7 +34,7 @@ const (
fillStr = "─"
selectStr = " ● "
//selectStr = " "
// selectStr = " "
)
var (
@ -74,8 +75,8 @@ func RenderHeader(title string, width int, selected bool) string {
repeatCount = 0
}
return fmt.Sprintf("%s%s%s%s\n", selectedLeftBracketStr, body, selectedRightBracketStr, strings.Repeat(selectedFillStr, repeatCount))
//return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), Selected(strings.Repeat(selectedFillStr, width-bodyLen-2)))
//return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), strings.Repeat(selectedFillStr, width-bodyLen-2))
// return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), Selected(strings.Repeat(selectedFillStr, width-bodyLen-2)))
// return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), strings.Repeat(selectedFillStr, width-bodyLen-2))
}
body := Header(fmt.Sprintf(" %s ", title))
bodyLen := len(vtclean.Clean(body, false))

View file

@ -0,0 +1,13 @@
//go:build windows
// +build windows
package ui
import (
"github.com/awesome-gocui/gocui"
)
// handle ctrl+z not supported on windows
func handle_ctrl_z(_ *gocui.Gui, _ *gocui.View) error {
return nil
}

View file

@ -0,0 +1,19 @@
//go:build !windows
// +build !windows
package ui
import (
"syscall"
"github.com/awesome-gocui/gocui"
)
// handle ctrl+z
func handle_ctrl_z(g *gocui.Gui, v *gocui.View) error {
gocui.Suspend()
if err := syscall.Kill(syscall.Getpid(), syscall.SIGSTOP); err != nil {
return err
}
return gocui.Resume()
}

View file

@ -7,6 +7,7 @@ import (
"github.com/awesome-gocui/keybinding"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/runtime/ui/format"
)

View file

@ -3,20 +3,23 @@ package compound
import (
"github.com/awesome-gocui/gocui"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/runtime/ui/view"
"github.com/wagoodman/dive/utils"
)
type LayerDetailsCompoundLayout struct {
layer *view.Layer
details *view.Details
layerDetails *view.LayerDetails
imageDetails *view.ImageDetails
constrainRealEstate bool
}
func NewLayerDetailsCompoundLayout(layer *view.Layer, details *view.Details) *LayerDetailsCompoundLayout {
func NewLayerDetailsCompoundLayout(layer *view.Layer, layerDetails *view.LayerDetails, imageDetails *view.ImageDetails) *LayerDetailsCompoundLayout {
return &LayerDetailsCompoundLayout{
layer: layer,
details: details,
layer: layer,
layerDetails: layerDetails,
imageDetails: imageDetails,
}
}
@ -32,87 +35,65 @@ func (cl *LayerDetailsCompoundLayout) OnLayoutChange() error {
return err
}
err = cl.details.OnLayoutChange()
err = cl.layerDetails.OnLayoutChange()
if err != nil {
logrus.Error("unable to setup details controller onLayoutChange", err)
logrus.Error("unable to setup layer details controller onLayoutChange", err)
return err
}
err = cl.imageDetails.OnLayoutChange()
if err != nil {
logrus.Error("unable to setup image details controller onLayoutChange", err)
return err
}
return nil
}
func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {
logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, cl.Name())
////////////////////////////////////////////////////////////////////////////////////
// Layers View
func (cl *LayerDetailsCompoundLayout) layoutRow(g *gocui.Gui, minX, minY, maxX, maxY int, viewName string, setup func(*gocui.View, *gocui.View) error) error {
logrus.Tracef("layoutRow(g, minX: %d, minY: %d, maxX: %d, maxY: %d, viewName: %s, <setup func>)", minX, minY, maxX, maxY, viewName)
// header + border
layerHeaderHeight := 2
layersHeight := cl.layer.LayerCount() + layerHeaderHeight + 1 // layers + header + base image layer row
maxLayerHeight := int(0.75 * float64(maxY))
if layersHeight > maxLayerHeight {
layersHeight = maxLayerHeight
}
headerHeight := 2
// TODO: investigate overlap
// note: maxY needs to account for the (invisible) border, thus a +1
header, headerErr := g.SetView(cl.layer.Name()+"header", minX, minY, maxX, minY+layerHeaderHeight+1, 0)
headerView, headerErr := g.SetView(viewName+"Header", minX, minY, maxX, minY+headerHeight+1, 0)
// we are going to overlap the view over the (invisible) border (so minY will be one less than expected)
main, viewErr := g.SetView(cl.layer.Name(), minX, minY+layerHeaderHeight, maxX, minY+layerHeaderHeight+layersHeight, 0)
bodyView, bodyErr := g.SetView(viewName, minX, minY+headerHeight, maxX, maxY, 0)
if utils.IsNewView(viewErr, headerErr) {
err := cl.layer.Setup(main, header)
if utils.IsNewView(bodyErr, headerErr) {
err := setup(bodyView, headerView)
if err != nil {
logrus.Error("unable to setup layer layout", err)
logrus.Debug("unable to setup row layout for ", viewName, err)
return err
}
}
return nil
}
if _, err = g.SetCurrentView(cl.layer.Name()); err != nil {
func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {
logrus.Tracef("LayerDetailsCompountLayout.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, cl.Name())
layouts := []view.IView{
cl.layer,
cl.layerDetails,
cl.imageDetails,
}
rowHeight := maxY / 3
for i := 0; i < 3; i++ {
if err := cl.layoutRow(g, minX, i*rowHeight, maxX, (i+1)*rowHeight, layouts[i].Name(), layouts[i].Setup); err != nil {
logrus.Debug("Laying out layers view errored!")
return err
}
}
if g.CurrentView() == nil {
if _, err := g.SetCurrentView(cl.layer.Name()); err != nil {
logrus.Error("unable to set view to layer", err)
return err
}
}
////////////////////////////////////////////////////////////////////////////////////
// Details
detailsMinY := minY + layersHeight
// header + border
detailsHeaderHeight := 2
v, _ := g.View(cl.details.Name())
if v != nil {
// the view exists already!
// don't show the details pane when there isn't enough room on the screen
if cl.constrainRealEstate {
// take note: deleting a view will invoke layout again, so ensure this call is protected from an infinite loop
err := g.DeleteView(cl.details.Name())
if err != nil {
return err
}
// take note: deleting a view will invoke layout again, so ensure this call is protected from an infinite loop
err = g.DeleteView(cl.details.Name() + "header")
if err != nil {
return err
}
return nil
}
}
header, headerErr = g.SetView(cl.details.Name()+"header", minX, detailsMinY, maxX, detailsMinY+detailsHeaderHeight, 0)
main, viewErr = g.SetView(cl.details.Name(), minX, detailsMinY+detailsHeaderHeight, maxX, maxY, 0)
if utils.IsNewView(viewErr, headerErr) {
err := cl.details.Setup(main, header)
if err != nil {
return err
}
}
return nil
}

View file

@ -51,7 +51,6 @@ func (lm *Manager) planAndLayoutHeaders(g *gocui.Gui, area Area) (Area, error) {
// restrict the available screen real estate
area.minY += height
}
}
return area, nil
@ -141,7 +140,6 @@ func (lm *Manager) planAndLayoutColumns(g *gocui.Gui, area Area) (Area, error) {
// move left to right, scratching off real estate as it is taken
area.minX += width
}
}
return area, nil

View file

@ -1,8 +1,9 @@
package layout
import (
"github.com/awesome-gocui/gocui"
"testing"
"github.com/awesome-gocui/gocui"
)
type testElement struct {

View file

@ -2,6 +2,7 @@ package view
import (
"errors"
"github.com/awesome-gocui/gocui"
)

View file

@ -5,6 +5,7 @@ import (
"github.com/awesome-gocui/gocui"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/utils"
)

View file

@ -1,204 +0,0 @@
package view
import (
"fmt"
"strconv"
"strings"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"github.com/awesome-gocui/gocui"
"github.com/dustin/go-humanize"
)
// Details 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 Details struct {
name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
imageName string
efficiency float64
inefficiencies filetree.EfficiencySlice
imageSize uint64
currentLayer *image.Layer
}
// newDetailsView creates a new view object attached the the global [gocui] screen object.
func newDetailsView(gui *gocui.Gui, imageName string, efficiency float64, inefficiencies filetree.EfficiencySlice, imageSize uint64) (controller *Details) {
controller = new(Details)
// populate main fields
controller.name = "details"
controller.gui = gui
controller.imageName = imageName
controller.efficiency = efficiency
controller.inefficiencies = inefficiencies
controller.imageSize = imageSize
return controller
}
func (v *Details) Name() string {
return v.name
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (v *Details) Setup(view *gocui.View, header *gocui.View) error {
logrus.Tracef("view.Setup() %s", v.Name())
// set controller options
v.view = view
v.view.Editable = false
v.view.Wrap = false
v.view.Highlight = false
v.view.Frame = false
v.header = header
v.header.Editable = false
v.header.Wrap = false
v.header.Frame = false
var infos = []key.BindingInfo{
{
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
OnAction: v.CursorDown,
},
{
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
OnAction: v.CursorUp,
},
}
_, err := key.GenerateBindings(v.gui, v.name, infos)
if err != nil {
return err
}
return v.Render()
}
// IsVisible indicates if the details view pane is currently initialized.
func (v *Details) IsVisible() bool {
return v != nil
}
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
func (v *Details) CursorDown() error {
return CursorDown(v.gui, v.view)
}
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
func (v *Details) CursorUp() error {
return CursorUp(v.gui, v.view)
}
// OnLayoutChange is called whenever the screen dimensions are changed
func (v *Details) OnLayoutChange() error {
err := v.Update()
if err != nil {
return err
}
return v.Render()
}
// Update refreshes the state objects for future rendering.
func (v *Details) Update() error {
return nil
}
func (v *Details) SetCurrentLayer(layer *image.Layer) {
v.currentLayer = layer
}
// 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 (v *Details) Render() error {
logrus.Tracef("view.Render() %s", v.Name())
if v.currentLayer == nil {
return fmt.Errorf("no layer selected")
}
var wastedSpace int64
template := "%5s %12s %-s\n"
inefficiencyReport := fmt.Sprintf(format.Header(template), "Count", "Total Space", "Path")
height := 100
if v.view != nil {
_, height = v.view.Size()
}
for idx := 0; idx < len(v.inefficiencies); idx++ {
data := v.inefficiencies[len(v.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)
}
}
imageNameStr := fmt.Sprintf("%s %s", format.Header("Image name:"), v.imageName)
imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(v.imageSize))
effStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*v.efficiency))
wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
v.gui.Update(func(g *gocui.Gui) error {
// update header
v.header.Clear()
width, _ := v.view.Size()
layerHeaderStr := format.RenderHeader("Layer Details", width, false)
imageHeaderStr := format.RenderHeader("Image Details", width, false)
_, err := fmt.Fprintln(v.header, layerHeaderStr)
if err != nil {
return err
}
// update contents
v.view.Clear()
var lines = make([]string, 0)
if v.currentLayer.Names != nil && len(v.currentLayer.Names) > 0 {
lines = append(lines, format.Header("Tags: ")+strings.Join(v.currentLayer.Names, ", "))
} else {
lines = append(lines, format.Header("Tags: ")+"(none)")
}
lines = append(lines, format.Header("Id: ")+v.currentLayer.Id)
lines = append(lines, format.Header("Digest: ")+v.currentLayer.Digest)
lines = append(lines, format.Header("Command:"))
lines = append(lines, v.currentLayer.Command)
lines = append(lines, "\n"+imageHeaderStr)
lines = append(lines, imageNameStr)
lines = append(lines, imageSizeStr)
lines = append(lines, wastedSpaceStr)
lines = append(lines, effStr+"\n")
lines = append(lines, inefficiencyReport)
_, err = fmt.Fprintln(v.view, strings.Join(lines, "\n"))
if err != nil {
logrus.Debug("unable to write to buffer: ", err)
}
return err
})
return nil
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
func (v *Details) KeyHelp() string {
return "TBD"
}

View file

@ -7,6 +7,7 @@ import (
"github.com/awesome-gocui/gocui"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
@ -23,7 +24,7 @@ type FileTree struct {
gui *gocui.Gui
view *gocui.View
header *gocui.View
vm *viewmodel.FileTree
vm *viewmodel.FileTreeViewModel
title string
filterRegex *regexp.Regexp
@ -72,7 +73,7 @@ func (v *FileTree) Name() string {
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (v *FileTree) Setup(view *gocui.View, header *gocui.View) error {
func (v *FileTree) Setup(view, header *gocui.View) error {
logrus.Tracef("view.Setup() %s", v.Name())
// set controller options
@ -97,6 +98,11 @@ func (v *FileTree) Setup(view *gocui.View, header *gocui.View) error {
OnAction: v.toggleCollapseAll,
Display: "Collapse all dir",
},
{
ConfigKeys: []string{"keybinding.toggle-sort-order"},
OnAction: v.toggleSortOrder,
Display: "Toggle sort order",
},
{
ConfigKeys: []string{"keybinding.toggle-added-files"},
OnAction: func() error { return v.toggleShowDiffType(filetree.Added) },
@ -287,6 +293,16 @@ func (v *FileTree) toggleCollapseAll() error {
return v.Render()
}
func (v *FileTree) toggleSortOrder() error {
err := v.vm.ToggleSortOrder()
if err != nil {
return err
}
v.resetCursor()
_ = v.Update()
return v.Render()
}
func (v *FileTree) toggleWrapTree() error {
v.view.Wrap = !v.view.Wrap
return nil
@ -406,7 +422,7 @@ func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {
logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name())
attributeRowSize := 0
// make the layout responsive to the available realestate. Make more room for the main content by hiding auxillary
// make the layout responsive to the available realestate. Make more room for the main content by hiding auxiliary
// content when there is not enough room
if maxX-minX < 60 {
v.vm.ConstrainLayout()
@ -436,7 +452,7 @@ func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {
}
func (v *FileTree) RequestedSize(available int) *int {
//var requestedWidth = int(float64(available) * (1.0 - v.requestedWidthRatio))
//return &requestedWidth
// var requestedWidth = int(float64(available) * (1.0 - v.requestedWidthRatio))
// return &requestedWidth
return nil
}

View file

@ -6,6 +6,7 @@ import (
"github.com/awesome-gocui/gocui"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/utils"
)
@ -15,7 +16,6 @@ type FilterEditListener func(string) error
// Filter 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 Filter struct {
name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
@ -34,7 +34,6 @@ func newFilterView(gui *gocui.Gui) (controller *Filter) {
controller.filterEditListeners = make([]FilterEditListener, 0)
// populate main fields
controller.name = "filter"
controller.gui = gui
controller.labelStr = "Path Filter: "
controller.hidden = true
@ -49,11 +48,11 @@ func (v *Filter) AddFilterEditListener(listener ...FilterEditListener) {
}
func (v *Filter) Name() string {
return v.name
return "filter"
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (v *Filter) Setup(view *gocui.View, header *gocui.View) error {
func (v *Filter) Setup(view, header *gocui.View) error {
logrus.Tracef("view.Setup() %s", v.Name())
// set controller options
@ -82,7 +81,7 @@ func (v *Filter) ToggleVisible() error {
v.hidden = !v.hidden
if !v.hidden {
_, err := v.gui.SetCurrentView(v.name)
_, err := v.gui.SetCurrentView(v.Name())
if err != nil {
logrus.Error("unable to toggle filter view: ", err)
return err

View file

@ -0,0 +1,175 @@
package view
import (
"fmt"
"strconv"
"strings"
"github.com/awesome-gocui/gocui"
"github.com/dustin/go-humanize"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
)
type ImageDetails struct {
gui *gocui.Gui
body *gocui.View
header *gocui.View
imageName string
imageSize uint64
efficiency float64
inefficiencies filetree.EfficiencySlice
}
func (v *ImageDetails) Name() string {
return "imageDetails"
}
func (v *ImageDetails) Setup(body, header *gocui.View) error {
logrus.Tracef("ImageDetails setup()")
v.body = body
v.body.Editable = false
v.body.Wrap = true
v.body.Highlight = true
v.body.Frame = false
v.header = header
v.header.Editable = false
v.header.Wrap = true
v.header.Highlight = false
v.header.Frame = false
var infos = []key.BindingInfo{
{
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
OnAction: v.CursorDown,
},
{
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
OnAction: v.CursorUp,
},
{
ConfigKeys: []string{"keybinding.page-up"},
OnAction: v.PageUp,
},
{
ConfigKeys: []string{"keybinding.page-down"},
OnAction: v.PageDown,
},
}
_, err := key.GenerateBindings(v.gui, v.Name(), infos)
if err != nil {
return err
}
return nil
}
// Render flushes the state objects to the screen. The details pane reports:
// 1. the image efficiency score
// 2. the estimated wasted image space
// 3. a list of inefficient file allocations
func (v *ImageDetails) Render() error {
analysisTemplate := "%5s %12s %-s\n"
inefficiencyReport := fmt.Sprintf(format.Header(analysisTemplate), "Count", "Total Space", "Path")
var wastedSpace int64
for idx := 0; idx < len(v.inefficiencies); idx++ {
data := v.inefficiencies[len(v.inefficiencies)-1-idx]
wastedSpace += data.CumulativeSize
inefficiencyReport += fmt.Sprintf(analysisTemplate, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
}
imageNameStr := fmt.Sprintf("%s %s", format.Header("Image name:"), v.imageName)
imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(v.imageSize))
efficiencyStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*v.efficiency))
wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
v.gui.Update(func(g *gocui.Gui) error {
width, _ := v.body.Size()
imageHeaderStr := format.RenderHeader("Image Details", width, v.gui.CurrentView() == v.body)
v.header.Clear()
_, err := fmt.Fprintln(v.header, imageHeaderStr)
if err != nil {
logrus.Debug("unable to write to buffer: ", err)
}
var lines = []string{
imageNameStr,
imageSizeStr,
wastedSpaceStr,
efficiencyStr,
" ", // to avoid an empty line so CursorDown can work as expected
inefficiencyReport,
}
v.body.Clear()
_, err = fmt.Fprintln(v.body, strings.Join(lines, "\n"))
if err != nil {
logrus.Debug("unable to write to buffer: ", err)
}
return err
})
return nil
}
func (v *ImageDetails) OnLayoutChange() error {
if err := v.Update(); err != nil {
return err
}
return v.Render()
}
// IsVisible indicates if the details view pane is currently initialized.
func (v *ImageDetails) IsVisible() bool {
return v.body != nil
}
func (v *ImageDetails) PageUp() error {
_, height := v.body.Size()
if err := CursorStep(v.gui, v.body, -height); err != nil {
logrus.Debugf("Couldn't move the cursor up by %d steps", height)
}
return nil
}
func (v *ImageDetails) PageDown() error {
_, height := v.body.Size()
if err := CursorStep(v.gui, v.body, height); err != nil {
logrus.Debugf("Couldn't move the cursor down by %d steps", height)
}
return nil
}
func (v *ImageDetails) CursorUp() error {
if err := CursorUp(v.gui, v.body); err != nil {
logrus.Debug("Couldn't move the cursor up")
}
return nil
}
func (v *ImageDetails) CursorDown() error {
if err := CursorDown(v.gui, v.body); err != nil {
logrus.Debug("Couldn't move the cursor down")
}
return nil
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
func (v *ImageDetails) KeyHelp() string {
return ""
}
// Update refreshes the state objects for future rendering.
func (v *ImageDetails) Update() error {
return nil
}

View file

@ -2,21 +2,23 @@ package view
import (
"fmt"
"github.com/awesome-gocui/gocui"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"github.com/wagoodman/dive/runtime/ui/viewmodel"
)
// Layer holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
// shows the image layers and layer selector.
// Layer 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 Layer struct {
name string
gui *gocui.Gui
view *gocui.View
body *gocui.View
header *gocui.View
vm *viewmodel.LayerSetState
constrainedRealEstate bool
@ -72,6 +74,12 @@ func (v *Layer) notifyLayerChangeListeners() error {
return err
}
}
// this is hacky, and I do not like it
if layerDetails, err := v.gui.View("layerDetails"); err == nil {
if err := layerDetails.SetCursor(0, 0); err != nil {
logrus.Debug("Couldn't set cursor to 0,0 for layerDetails")
}
}
return nil
}
@ -80,14 +88,14 @@ func (v *Layer) Name() string {
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (v *Layer) Setup(view *gocui.View, header *gocui.View) error {
func (v *Layer) Setup(body *gocui.View, header *gocui.View) error {
logrus.Tracef("view.Setup() %s", v.Name())
// set controller options
v.view = view
v.view.Editable = false
v.view.Wrap = false
v.view.Frame = false
v.body = body
v.body.Editable = false
v.body.Wrap = false
v.body.Frame = false
v.header = header
v.header.Editable = false
@ -117,16 +125,6 @@ func (v *Layer) Setup(view *gocui.View, header *gocui.View) error {
Modifier: gocui.ModNone,
OnAction: v.CursorUp,
},
{
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
OnAction: v.CursorUp,
},
{
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
OnAction: v.CursorDown,
},
{
ConfigKeys: []string{"keybinding.page-up"},
OnAction: v.PageUp,
@ -148,7 +146,7 @@ func (v *Layer) Setup(view *gocui.View, header *gocui.View) error {
// height obtains the height of the current pane (taking into account the lost space due to the header).
func (v *Layer) height() uint {
_, height := v.view.Size()
_, height := v.body.Size()
return uint(height - 1)
}
@ -171,7 +169,8 @@ func (v *Layer) PageDown() error {
}
if step > 0 {
err := CursorStep(v.gui, v.view, step)
// err := CursorStep(v.gui, v.body, step)
err := error(nil)
if err == nil {
return v.SetCursor(v.vm.LayerIndex + step)
}
@ -189,7 +188,8 @@ func (v *Layer) PageUp() error {
}
if step > 0 {
err := CursorStep(v.gui, v.view, -step)
// err := CursorStep(v.gui, v.body, -step)
err := error(nil)
if err == nil {
return v.SetCursor(v.vm.LayerIndex - step)
}
@ -199,8 +199,9 @@ func (v *Layer) PageUp() error {
// CursorDown moves the cursor down in the layer pane (selecting a higher layer).
func (v *Layer) CursorDown() error {
if v.vm.LayerIndex < len(v.vm.Layers) {
err := CursorDown(v.gui, v.view)
if v.vm.LayerIndex < len(v.vm.Layers)-1 {
// err := CursorDown(v.gui, v.body)
err := error(nil)
if err == nil {
return v.SetCursor(v.vm.LayerIndex + 1)
}
@ -211,7 +212,8 @@ func (v *Layer) CursorDown() error {
// CursorUp moves the cursor up in the layer pane (selecting a lower layer).
func (v *Layer) CursorUp() error {
if v.vm.LayerIndex > 0 {
err := CursorUp(v.gui, v.view)
// err := CursorUp(v.gui, v.body)
err := error(nil)
if err == nil {
return v.SetCursor(v.vm.LayerIndex - 1)
}
@ -292,7 +294,7 @@ func (v *Layer) Render() error {
// indicate when selected
title := "Layers"
isSelected := v.gui.CurrentView() == v.view
isSelected := v.gui.CurrentView() == v.body
v.gui.Update(func(g *gocui.Gui) error {
var err error
@ -316,9 +318,8 @@ func (v *Layer) Render() error {
}
// update contents
v.view.Clear()
v.body.Clear()
for idx, layer := range v.vm.Layers {
var layerStr string
if v.constrainedRealEstate {
layerStr = fmt.Sprintf("%-4d", layer.Index)
@ -329,16 +330,15 @@ func (v *Layer) Render() error {
compareBar := v.renderCompareBar(idx)
if idx == v.vm.LayerIndex {
_, err = fmt.Fprintln(v.view, compareBar+" "+format.Selected(layerStr))
_, err = fmt.Fprintln(v.body, compareBar+" "+format.Selected(layerStr))
} else {
_, err = fmt.Fprintln(v.view, compareBar+" "+layerStr)
_, err = fmt.Fprintln(v.body, compareBar+" "+layerStr)
}
if err != nil {
logrus.Debug("unable to write to buffer: ", err)
return err
}
}
return nil
})

View file

@ -0,0 +1,142 @@
package view
import (
"fmt"
"strings"
"github.com/awesome-gocui/gocui"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
)
type LayerDetails struct {
gui *gocui.Gui
header *gocui.View
body *gocui.View
CurrentLayer *image.Layer
}
func (v *LayerDetails) Name() string {
return "layerDetails"
}
func (v *LayerDetails) Setup(body, header *gocui.View) error {
logrus.Tracef("LayerDetails setup()")
v.body = body
v.body.Editable = false
v.body.Wrap = true
v.body.Highlight = true
v.body.Frame = false
v.header = header
v.header.Editable = false
v.header.Wrap = true
v.header.Highlight = false
v.header.Frame = false
var infos = []key.BindingInfo{
{
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
OnAction: v.CursorDown,
},
{
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
OnAction: v.CursorUp,
},
}
_, err := key.GenerateBindings(v.gui, v.Name(), infos)
if err != nil {
return err
}
return nil
}
// Render flushes the state objects to the screen.
// The details pane reports the currently selected layer's:
// 1. tags
// 2. ID
// 3. digest
// 4. command
func (v *LayerDetails) Render() error {
v.gui.Update(func(g *gocui.Gui) error {
v.header.Clear()
width, _ := v.body.Size()
layerHeaderStr := format.RenderHeader("Layer Details", width, v.gui.CurrentView() == v.body)
_, err := fmt.Fprintln(v.header, layerHeaderStr)
if err != nil {
return err
}
// this is for layer details
var lines = make([]string, 0)
tags := "(none)"
if v.CurrentLayer.Names != nil && len(v.CurrentLayer.Names) > 0 {
tags = strings.Join(v.CurrentLayer.Names, ", ")
}
lines = append(lines, []string{
format.Header("Tags: ") + tags,
format.Header("Id: ") + v.CurrentLayer.Id,
format.Header("Digest: ") + v.CurrentLayer.Digest,
format.Header("Command:"),
v.CurrentLayer.Command,
}...)
v.body.Clear()
if _, err = fmt.Fprintln(v.body, strings.Join(lines, "\n")); err != nil {
logrus.Debug("unable to write to buffer: ", err)
}
return nil
})
return nil
}
func (v *LayerDetails) OnLayoutChange() error {
if err := v.Update(); err != nil {
return err
}
return v.Render()
}
// IsVisible indicates if the details view pane is currently initialized.
func (v *LayerDetails) IsVisible() bool {
return v.body != nil
}
// CursorUp moves the cursor up in the details pane
func (v *LayerDetails) CursorUp() error {
if err := CursorUp(v.gui, v.body); err != nil {
logrus.Debug("Couldn't move the cursor up")
}
return nil
}
// CursorDown moves the cursor up in the details pane
func (v *LayerDetails) CursorDown() error {
if err := CursorDown(v.gui, v.body); err != nil {
logrus.Debug("Couldn't move the cursor down")
}
return nil
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
func (v *LayerDetails) KeyHelp() string {
return ""
}
// Update refreshes the state objects for future rendering.
func (v *LayerDetails) Update() error {
return nil
}
func (v *LayerDetails) SetCursor(x, y int) error {
return v.body.SetCursor(x, y)
}

View file

@ -4,12 +4,12 @@ import (
"fmt"
"strings"
"github.com/awesome-gocui/gocui"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"github.com/wagoodman/dive/utils"
"github.com/awesome-gocui/gocui"
)
// Status holds the UI objects and data models for populating the bottom-most pane. Specifically the panel

View file

@ -2,17 +2,34 @@ package view
import (
"github.com/awesome-gocui/gocui"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
)
type IView interface {
Setup(*gocui.View, *gocui.View) error
Name() string
IsVisible() bool
}
type Views struct {
Tree *FileTree
Layer *Layer
Status *Status
Filter *Filter
Details *Details
Debug *Debug
Tree *FileTree
Layer *Layer
Status *Status
Filter *Filter
LayerDetails *LayerDetails
ImageDetails *ImageDetails
Debug *Debug
}
var _ []IView = []IView{
&FileTree{},
&Layer{},
&Filter{},
&LayerDetails{},
&ImageDetails{},
&Debug{},
}
func NewViews(g *gocui.Gui, imageName string, analysis *image.AnalysisResult, cache filetree.Comparer) (*Views, error) {
@ -34,17 +51,25 @@ func NewViews(g *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
Filter := newFilterView(g)
Details := newDetailsView(g, imageName, analysis.Efficiency, analysis.Inefficiencies, analysis.SizeBytes)
LayerDetails := &LayerDetails{gui: g}
ImageDetails := &ImageDetails{
gui: g,
imageName: imageName,
imageSize: analysis.SizeBytes,
efficiency: analysis.Efficiency,
inefficiencies: analysis.Inefficiencies,
}
Debug := newDebugView(g)
return &Views{
Tree: Tree,
Layer: Layer,
Status: Status,
Filter: Filter,
Details: Details,
Debug: Debug,
Tree: Tree,
Layer: Layer,
Status: Status,
Filter: Filter,
ImageDetails: ImageDetails,
LayerDetails: LayerDetails,
Debug: Debug,
}, nil
}
@ -54,6 +79,7 @@ func (views *Views) All() []Renderer {
views.Layer,
views.Status,
views.Filter,
views.Details,
views.LayerDetails,
views.ImageDetails,
}
}

View file

@ -3,19 +3,20 @@ package viewmodel
import (
"bytes"
"fmt"
"github.com/wagoodman/dive/runtime/ui/format"
"regexp"
"strings"
"github.com/lunixbochs/vtclean"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/runtime/ui/format"
)
// 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 FileTree struct {
type FileTreeViewModel struct {
ModelTree *filetree.FileTree
ViewTree *filetree.FileTree
RefTrees []*filetree.FileTree
@ -38,8 +39,8 @@ type FileTree struct {
}
// NewFileTreeViewModel creates a new view object attached the the global [gocui] screen object.
func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.Comparer) (treeViewModel *FileTree, err error) {
treeViewModel = new(FileTree)
func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.Comparer) (treeViewModel *FileTreeViewModel, err error) {
treeViewModel = new(FileTreeViewModel)
// populate main fields
treeViewModel.ShowAttributes = viper.GetBool("filetree.show-attributes")
@ -70,13 +71,13 @@ func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (vm *FileTree) Setup(lowerBound, height int) {
func (vm *FileTreeViewModel) Setup(lowerBound, height int) {
vm.bufferIndexLowerBound = lowerBound
vm.refHeight = height
}
// height returns the current height and considers the header
func (vm *FileTree) height() int {
func (vm *FileTreeViewModel) height() int {
if vm.ShowAttributes {
return vm.refHeight - 1
}
@ -84,24 +85,24 @@ func (vm *FileTree) height() int {
}
// bufferIndexUpperBound returns the current upper bounds for the view
func (vm *FileTree) bufferIndexUpperBound() int {
func (vm *FileTreeViewModel) bufferIndexUpperBound() int {
return vm.bufferIndexLowerBound + vm.height()
}
// IsVisible indicates if the file tree view pane is currently initialized
func (vm *FileTree) IsVisible() bool {
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 *FileTree) ResetCursor() {
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 *FileTree) SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
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)
}
@ -130,7 +131,7 @@ func (vm *FileTree) SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart
}
// doCursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer.
func (vm *FileTree) CursorUp() bool {
func (vm *FileTreeViewModel) CursorUp() bool {
if vm.TreeIndex <= 0 {
return false
}
@ -145,7 +146,7 @@ func (vm *FileTree) CursorUp() bool {
}
// doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer.
func (vm *FileTree) CursorDown() bool {
func (vm *FileTreeViewModel) CursorDown() bool {
if vm.TreeIndex >= vm.ModelTree.VisibleSize() {
return false
}
@ -161,7 +162,7 @@ func (vm *FileTree) CursorDown() bool {
}
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
func (vm *FileTree) CursorLeft(filterRegex *regexp.Regexp) error {
func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error {
var visitor func(*filetree.FileNode) error
var evaluator func(*filetree.FileNode) bool
var dfsCounter, newIndex int
@ -212,7 +213,7 @@ func (vm *FileTree) CursorLeft(filterRegex *regexp.Regexp) error {
}
// CursorRight descends into directory expanding it if needed
func (vm *FileTree) CursorRight(filterRegex *regexp.Regexp) error {
func (vm *FileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error {
node := vm.getAbsPositionNode(filterRegex)
if node == nil {
return nil
@ -244,7 +245,7 @@ func (vm *FileTree) CursorRight(filterRegex *regexp.Regexp) error {
}
// PageDown moves to next page putting the cursor on top
func (vm *FileTree) PageDown() error {
func (vm *FileTreeViewModel) PageDown() error {
nextBufferIndexLowerBound := vm.bufferIndexLowerBound + vm.height()
nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height()
@ -270,7 +271,7 @@ func (vm *FileTree) PageDown() error {
}
// PageUp moves to previous page putting the cursor on top
func (vm *FileTree) PageUp() error {
func (vm *FileTreeViewModel) PageUp() error {
nextBufferIndexLowerBound := vm.bufferIndexLowerBound - vm.height()
nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height()
@ -295,7 +296,7 @@ func (vm *FileTree) PageUp() error {
}
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
func (vm *FileTree) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.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
@ -326,7 +327,7 @@ func (vm *FileTree) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetr
}
// ToggleCollapse will collapse/expand the selected FileNode.
func (vm *FileTree) ToggleCollapse(filterRegex *regexp.Regexp) error {
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
@ -335,7 +336,7 @@ func (vm *FileTree) ToggleCollapse(filterRegex *regexp.Regexp) error {
}
// ToggleCollapseAll will collapse/expand the all directories.
func (vm *FileTree) ToggleCollapseAll() error {
func (vm *FileTreeViewModel) ToggleCollapseAll() error {
vm.CollapseAll = !vm.CollapseAll
visitor := func(curNode *filetree.FileNode) error {
@ -355,7 +356,14 @@ func (vm *FileTree) ToggleCollapseAll() error {
return nil
}
func (vm *FileTree) ConstrainLayout() {
// ToggleSortOrder will toggle the sort order in which files are displayed
func (vm *FileTreeViewModel) ToggleSortOrder() error {
vm.ModelTree.SortOrder = (vm.ModelTree.SortOrder + 1) % filetree.NumSortOrderConventions
return nil
}
func (vm *FileTreeViewModel) ConstrainLayout() {
if !vm.constrainedRealEstate {
logrus.Debugf("constraining filetree layout")
vm.constrainedRealEstate = true
@ -364,7 +372,7 @@ func (vm *FileTree) ConstrainLayout() {
}
}
func (vm *FileTree) ExpandLayout() {
func (vm *FileTreeViewModel) ExpandLayout() {
if vm.constrainedRealEstate {
logrus.Debugf("expanding filetree layout")
vm.ShowAttributes = vm.unconstrainedShowAttributes
@ -373,7 +381,7 @@ func (vm *FileTree) ExpandLayout() {
}
// ToggleCollapse will collapse/expand the selected FileNode.
func (vm *FileTree) ToggleAttributes() error {
func (vm *FileTreeViewModel) ToggleAttributes() error {
// ignore any attempt to show the attributes when the layout is constrained
if vm.constrainedRealEstate {
return nil
@ -383,12 +391,12 @@ func (vm *FileTree) ToggleAttributes() error {
}
// ToggleShowDiffType will show/hide the selected DiffType in the filetree pane.
func (vm *FileTree) ToggleShowDiffType(diffType filetree.DiffType) {
func (vm *FileTreeViewModel) ToggleShowDiffType(diffType filetree.DiffType) {
vm.HiddenDiffTypes[diffType] = !vm.HiddenDiffTypes[diffType]
}
// Update refreshes the state objects for future rendering.
func (vm *FileTree) Update(filterRegex *regexp.Regexp, width, height int) error {
func (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height int) error {
vm.refWidth = width
vm.refHeight = height
@ -436,7 +444,7 @@ func (vm *FileTree) Update(filterRegex *regexp.Regexp, width, height int) error
}
// Render flushes the state objects (file tree) to the pane.
func (vm *FileTree) Render() error {
func (vm *FileTreeViewModel) Render() error {
treeString := vm.ViewTree.StringBetween(vm.bufferIndexLowerBound, vm.bufferIndexUpperBound(), vm.ShowAttributes)
lines := strings.Split(treeString, "\n")

View file

@ -2,9 +2,6 @@ package viewmodel
import (
"bytes"
"github.com/wagoodman/dive/dive/image/docker"
"github.com/wagoodman/dive/runtime/ui/format"
"io/ioutil"
"os"
"path/filepath"
"regexp"
@ -12,7 +9,10 @@ import (
"github.com/fatih/color"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image/docker"
"github.com/wagoodman/dive/runtime/ui/format"
)
const allowTestDataCapture = false
@ -31,7 +31,7 @@ func testCaseDataFilePath(name string) string {
func helperLoadBytes(t *testing.T) []byte {
path := testCaseDataFilePath(t.Name())
theBytes, err := ioutil.ReadFile(path)
theBytes, err := os.ReadFile(path)
if err != nil {
t.Fatalf("unable to load test data ('%s'): %+v", t.Name(), err)
}
@ -44,7 +44,7 @@ func helperCaptureBytes(t *testing.T, data []byte) {
}
path := testCaseDataFilePath(t.Name())
err := ioutil.WriteFile(path, data, 0644)
err := os.WriteFile(path, data, 0644)
if err != nil {
t.Fatalf("unable to save test data ('%s'): %+v", t.Name(), err)
@ -73,7 +73,7 @@ func assertTestData(t *testing.T, actualBytes []byte) {
helperCheckDiff(t, expectedBytes, actualBytes)
}
func initializeTestViewModel(t *testing.T) *FileTree {
func initializeTestViewModel(t *testing.T) *FileTreeViewModel {
result := docker.TestAnalysisFromArchive(t, "../../../.data/test-docker-image.tar")
cache := filetree.NewComparer(result.RefTrees)
@ -98,7 +98,7 @@ func initializeTestViewModel(t *testing.T) *FileTree {
return vm
}
func runTestCase(t *testing.T, vm *FileTree, width, height int, filterRegex *regexp.Regexp) {
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)

View file

@ -1,8 +1,9 @@
package utils
import (
"github.com/logrusorgru/aurora"
"strings"
"github.com/logrusorgru/aurora"
)
func TitleFormat(s string) string {