Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add CTRL+e for extracting current focused file #472

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func initConfig() {
viper.SetDefault("keybinding.toggle-modified-files", "ctrl+m")
viper.SetDefault("keybinding.toggle-unmodified-files", "ctrl+u")
viper.SetDefault("keybinding.toggle-wrap-tree", "ctrl+p")
viper.SetDefault("keybinding.extract-file", "ctrl+e")
viper.SetDefault("keybinding.page-up", "pgup")
viper.SetDefault("keybinding.page-down", "pgdn")

Expand Down
4 changes: 4 additions & 0 deletions dive/image/docker/archive_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ func (r *archiveResolver) Fetch(path string) (*image.Image, error) {
func (r *archiveResolver) Build(args []string) (*image.Image, error) {
return nil, fmt.Errorf("build option not supported for docker archive resolver")
}

func (r *archiveResolver) Extract(id string, l string, p string) error {
return fmt.Errorf("not implemented")
}
13 changes: 13 additions & 0 deletions dive/image/docker/engine_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ func (r *engineResolver) Build(args []string) (*image.Image, error) {
return r.Fetch(id)
}

func (r *engineResolver) Extract(id string, l string, p string) error {
reader, err := r.fetchArchive(id)
if err != nil {
return err
}

if err := ExtractFromImage(io.NopCloser(reader), l, p); err == nil {
return nil
}

return fmt.Errorf("unable to extract from image '%s': %+v", id, err)
}

func (r *engineResolver) fetchArchive(id string) (io.ReadCloser, error) {
var err error
var dockerClient *client.Client
Expand Down
78 changes: 78 additions & 0 deletions dive/image/docker/image_archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"os"
"path"
"path/filepath"
"strings"

"github.com/wagoodman/dive/dive/filetree"
Expand Down Expand Up @@ -204,3 +205,80 @@ func (img *ImageArchive) ToImage() (*image.Image, error) {
Layers: layers,
}, nil
}

func ExtractFromImage(tarFile io.ReadCloser, l string, p string) error {
tarReader := tar.NewReader(tarFile)

for {
header, err := tarReader.Next()

if err == io.EOF {
break
}

if err != nil {
fmt.Println(err)
os.Exit(1)
}

name := header.Name

switch header.Typeflag {
case tar.TypeReg:
if name == l {
err = extractInner(tar.NewReader(tarReader), p)
if err != nil {
return err
}
return nil
}
default:
continue
}
}

return nil
}

func extractInner(reader *tar.Reader, p string) error {
target := strings.TrimPrefix(p, "/")

for {
header, err := reader.Next()

if err == io.EOF {
break
}

if err != nil {
fmt.Println(err)
os.Exit(1)
}

name := header.Name

switch header.Typeflag {
case tar.TypeReg:
if strings.HasPrefix(name, target) {
err := os.MkdirAll(filepath.Dir(name), 0755)
if err != nil {
return err
}

out, err := os.Create(name)
if err != nil {
return err
}

_, err = io.Copy(out, reader)
if err != nil {
return err
}
}
default:
continue
}
}

return nil
}
15 changes: 15 additions & 0 deletions dive/image/podman/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ func (r *resolver) Fetch(id string) (*image.Image, error) {
return nil, fmt.Errorf("unable to resolve image '%s': %+v", id, err)
}

func (r *resolver) Extract(id string, l string, p string) error {
// todo: add podman fetch attempt via varlink first...

err, reader := streamPodmanCmd("image", "save", id)
if err != nil {
return err
}

if err := docker.ExtractFromImage(io.NopCloser(reader), l, p); err == nil {
return nil
}

return fmt.Errorf("unable to extract from image '%s': %+v", id, err)
}

func (r *resolver) resolveFromDockerArchive(id string) (*image.Image, error) {
err, reader := streamPodmanCmd("image", "save", id)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions dive/image/podman/resolver_unsupported.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ func (r *resolver) Build(args []string) (*image.Image, error) {
func (r *resolver) Fetch(id string) (*image.Image, error) {
return nil, fmt.Errorf("unsupported platform")
}

func (r *resolver) Extract(id string, l string, p string) error {
return fmt.Errorf("unsupported platform")
}
1 change: 1 addition & 0 deletions dive/image/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package image
type Resolver interface {
Fetch(id string) (*Image, error)
Build(options []string) (*Image, error)
Extract(id string, layer string, path string) error
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ require (
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
gotest.tools v2.2.0+incompatible // indirect
gotest.tools/v3 v3.5.0 // indirect
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,9 @@ 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.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.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/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down
2 changes: 1 addition & 1 deletion runtime/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func run(enableUi bool, options Options, imageResolver image.Resolver, events ev
// enough sleep will prevent this behavior (todo: remove this hack)
time.Sleep(100 * time.Millisecond)

err = ui.Run(options.Image, analysis, treeStack)
err = ui.Run(options.Image, imageResolver, analysis, treeStack)
if err != nil {
events.exitWithError(err)
return
Expand Down
12 changes: 12 additions & 0 deletions runtime/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import (

type defaultResolver struct{}

func (r *defaultResolver) Extract(id string, l string, p string) error {
return nil
}

func (r *defaultResolver) Fetch(id string) (*image.Image, error) {
archive, err := docker.TestLoadArchive("../.data/test-docker-image.tar")
if err != nil {
Expand All @@ -30,6 +34,10 @@ func (r *defaultResolver) Build(args []string) (*image.Image, error) {

type failedBuildResolver struct{}

func (r *failedBuildResolver) Extract(id string, l string, p string) error {
return fmt.Errorf("some extract failure")
}

func (r *failedBuildResolver) Fetch(id string) (*image.Image, error) {
archive, err := docker.TestLoadArchive("../.data/test-docker-image.tar")
if err != nil {
Expand All @@ -44,6 +52,10 @@ func (r *failedBuildResolver) Build(args []string) (*image.Image, error) {

type failedFetchResolver struct{}

func (r *failedFetchResolver) Extract(id string, l string, p string) error {
return fmt.Errorf("some extract failure")
}

func (r *failedFetchResolver) Fetch(id string) (*image.Image, error) {
return nil, fmt.Errorf("some fetch failure")
}
Expand Down
8 changes: 4 additions & 4 deletions runtime/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ var (
appSingleton *app
)

func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, cache filetree.Comparer) (*app, error) {
func newApp(gui *gocui.Gui, imageName string, resolver image.Resolver, analysis *image.AnalysisResult, cache filetree.Comparer) (*app, error) {
var err error
once.Do(func() {
var controller *Controller
var globalHelpKeys []*key.Binding

controller, err = NewCollection(gui, imageName, analysis, cache)
controller, err = NewCollection(gui, imageName, resolver, analysis, cache)
if err != nil {
return
}
Expand Down Expand Up @@ -134,7 +134,7 @@ func (a *app) quit() error {
}

// Run is the UI entrypoint.
func Run(imageName string, analysis *image.AnalysisResult, treeStack filetree.Comparer) error {
func Run(imageName string, resolver image.Resolver, analysis *image.AnalysisResult, treeStack filetree.Comparer) error {
var err error

g, err := gocui.NewGui(gocui.OutputNormal, true)
Expand All @@ -143,7 +143,7 @@ func Run(imageName string, analysis *image.AnalysisResult, treeStack filetree.Co
}
defer g.Close()

_, err = newApp(g, imageName, analysis, treeStack)
_, err = newApp(g, imageName, resolver, analysis, treeStack)
if err != nil {
return err
}
Expand Down
21 changes: 16 additions & 5 deletions runtime/ui/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,23 @@ import (
)

type Controller struct {
gui *gocui.Gui
views *view.Views
gui *gocui.Gui
views *view.Views
resolver image.Resolver
imageName string
}

func NewCollection(g *gocui.Gui, imageName string, analysis *image.AnalysisResult, cache filetree.Comparer) (*Controller, error) {
func NewCollection(g *gocui.Gui, imageName string, resolver image.Resolver, analysis *image.AnalysisResult, cache filetree.Comparer) (*Controller, error) {
views, err := view.NewViews(g, imageName, analysis, cache)
if err != nil {
return nil, err
}

controller := &Controller{
gui: g,
views: views,
gui: g,
views: views,
resolver: resolver,
imageName: imageName,
}

// layer view cursor down event should trigger an update in the file tree
Expand All @@ -34,6 +38,9 @@ func NewCollection(g *gocui.Gui, imageName string, analysis *image.AnalysisResul
// update the status pane when a filetree option is changed by the user
controller.views.Tree.AddViewOptionChangeListener(controller.onFileTreeViewOptionChange)

// update the status pane when a filetree option is changed by the user
controller.views.Tree.AddViewExtractListener(controller.onFileTreeViewExtract)

// update the tree view while the user types into the filter view
controller.views.Filter.AddFilterEditListener(controller.onFilterEdit)

Expand All @@ -53,6 +60,10 @@ func NewCollection(g *gocui.Gui, imageName string, analysis *image.AnalysisResul
return controller, nil
}

func (c *Controller) onFileTreeViewExtract(p string) error {
return c.resolver.Extract(c.imageName, c.views.LayerDetails.CurrentLayer.Id, p)
}

func (c *Controller) onFileTreeViewOptionChange() error {
err := c.views.Status.Update()
if err != nil {
Expand Down
24 changes: 24 additions & 0 deletions runtime/ui/view/filetree.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (

type ViewOptionChangeListener func() error

type ViewExtractListener func(string) error

// FileTree 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 {
Expand All @@ -29,6 +31,7 @@ type FileTree struct {

filterRegex *regexp.Regexp
listeners []ViewOptionChangeListener
extractListeners []ViewExtractListener
helpKeys []*key.Binding
requestedWidthRatio float64
}
Expand Down Expand Up @@ -60,6 +63,10 @@ func (v *FileTree) AddViewOptionChangeListener(listener ...ViewOptionChangeListe
v.listeners = append(v.listeners, listener...)
}

func (v *FileTree) AddViewExtractListener(listener ...ViewExtractListener) {
v.extractListeners = append(v.extractListeners, listener...)
}

func (v *FileTree) SetTitle(title string) {
v.title = title
}
Expand Down Expand Up @@ -103,6 +110,11 @@ func (v *FileTree) Setup(view, header *gocui.View) error {
OnAction: v.toggleSortOrder,
Display: "Toggle sort order",
},
{
ConfigKeys: []string{"keybinding.extract-file"},
OnAction: v.extractFile,
Display: "Extract File",
},
{
ConfigKeys: []string{"keybinding.toggle-added-files"},
OnAction: func() error { return v.toggleShowDiffType(filetree.Added) },
Expand Down Expand Up @@ -303,6 +315,18 @@ func (v *FileTree) toggleSortOrder() error {
return v.Render()
}

func (v *FileTree) extractFile() error {
node := v.vm.CurrentNode(v.filterRegex)
for _, listener := range v.extractListeners {
err := listener(node.Path())
if err != nil {
return err
}
}

return nil
}

func (v *FileTree) toggleWrapTree() error {
v.view.Wrap = !v.view.Wrap
return nil
Expand Down
5 changes: 5 additions & 0 deletions runtime/ui/viewmodel/filetree.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ func (vm *FileTreeViewModel) CursorDown() bool {
return true
}

// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
func (vm *FileTreeViewModel) CurrentNode(filterRegex *regexp.Regexp) *filetree.FileNode {
return vm.getAbsPositionNode(filterRegex)
}

// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error {
var visitor func(*filetree.FileNode) error
Expand Down