mirror of
https://github.com/wagoodman/dive
synced 2024-06-11 16:42:11 +02:00
Compare commits
64 commits
Author | SHA1 | Date | |
---|---|---|---|
925cdd8648 | |||
fd526464b2 | |||
9f08f7e6cc | |||
7556be352a | |||
2d86aa7b4c | |||
5d6a406df1 | |||
3ef1dd2c74 | |||
559e5e2dbe | |||
8003980604 | |||
6f20438ae4 | |||
d5e8a92968 | |||
42925c1b05 | |||
ec5528bce5 | |||
8a10c0d46b | |||
6ba95122a5 | |||
69e6ba9993 | |||
a70b7acffc | |||
e9169b9ccd | |||
77c11047cf | |||
a7dfbb7927 | |||
c4b2723d97 | |||
255e3c2ff7 | |||
32c1c1b7bf | |||
99124abb7a | |||
ceb9688d92 | |||
67aa2f1bf8 | |||
ae996cd718 | |||
25a226d51d | |||
bf39a82f2a | |||
2db0aaa920 | |||
a95e899663 | |||
f5458e6b57 | |||
b6961534af | |||
f17140f8ce | |||
621e677e04 | |||
2a99dd25fe | |||
f50a009f8e | |||
e2d9b623f9 | |||
8bf4341f70 | |||
abbac157bb | |||
dfe9a8c5c9 | |||
d131aebc05 | |||
7933564b4c | |||
cd63ad53fb | |||
02182266ec | |||
1eb78e1ab7 | |||
ebe293c24b | |||
3d7eb32d7e | |||
ba3c9125e1 | |||
9fe4975733 | |||
6c0552e182 | |||
0c9b09ea77 | |||
ac0fa872cd | |||
741f95aa8a | |||
a19a6f9acc | |||
4fad38207e | |||
f359b8a9d7 | |||
2aad87c37e | |||
2030e74234 | |||
4146421e60 | |||
7dfef036c7 | |||
4d5b2824ee | |||
b5b377444e | |||
b7d32324e6 |
12
.bouncer.yaml
Normal file
12
.bouncer.yaml
Normal 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
|
||||
|
|
@ -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"
|
BIN
.data/test-oci-docker-image.tar
Normal file
BIN
.data/test-oci-docker-image.tar
Normal file
Binary file not shown.
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1,2 +1 @@
|
|||
github: ['wagoodman']
|
||||
custom: ['https://www.paypal.me/wagoodman']
|
||||
|
|
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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)
|
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
76
.github/actions/bootstrap/action.yaml
vendored
Normal 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
11
.github/scripts/ci-check.sh
vendored
Executable 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
36
.github/scripts/coverage.py
vendored
Executable 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
31
.github/scripts/go-mod-tidy-check.sh
vendored
Executable 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
50
.github/scripts/trigger-release.sh
vendored
Executable 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)
|
180
.github/workflows/pipeline.yml
vendored
180
.github/workflows/pipeline.yml
vendored
|
@ -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
116
.github/workflows/release.yaml
vendored
Normal 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
135
.github/workflows/validations.yaml
vendored
Normal 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
24
.gitignore
vendored
|
@ -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
74
.golangci.yaml
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
321
Makefile
|
@ -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 ./...
|
||||
|
|
58
README.md
58
README.md
|
@ -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
24
RELEASE.md
Normal 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).
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
17
cmd/root.go
17
cmd/root.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
61
dive/filetree/order_strategy.go
Normal file
61
dive/filetree/order_strategy.go
Normal 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
|
||||
}
|
|
@ -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, ""
|
||||
}
|
||||
|
|
|
@ -2,8 +2,9 @@ package docker
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"os"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
)
|
||||
|
||||
type archiveResolver struct{}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,6 +2,7 @@ package docker
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,6 +2,7 @@ package docker
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -10,7 +10,6 @@ type Image struct {
|
|||
}
|
||||
|
||||
func (img *Image) Analyze() (*AnalysisResult, error) {
|
||||
|
||||
efficiency, inefficiencies := filetree.Efficiency(img.Trees)
|
||||
var sizeBytes, userSizeBytes uint64
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
71
go.mod
|
@ -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
78
go.sum
|
@ -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=
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -2,6 +2,7 @@ package export
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
diveImage "github.com/wagoodman/dive/dive/image"
|
||||
)
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package runtime
|
|||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/dive"
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
|
|
13
runtime/ui/job_control_other.go
Normal file
13
runtime/ui/job_control_other.go
Normal 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
|
||||
}
|
19
runtime/ui/job_control_unix.go
Normal file
19
runtime/ui/job_control_unix.go
Normal 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()
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package layout
|
||||
|
||||
import (
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"testing"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
)
|
||||
|
||||
type testElement struct {
|
||||
|
|
|
@ -2,6 +2,7 @@ package view
|
|||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
)
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
175
runtime/ui/view/image_details.go
Normal file
175
runtime/ui/view/image_details.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
})
|
||||
|
|
142
runtime/ui/view/layer_details.go
Normal file
142
runtime/ui/view/layer_details.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"github.com/logrusorgru/aurora"
|
||||
"strings"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
)
|
||||
|
||||
func TitleFormat(s string) string {
|
||||
|
|
Loading…
Reference in a new issue