Skip to content

Commit

Permalink
feat: add CTRL+e for extracting current focused file
Browse files Browse the repository at this point in the history
Refs wagoodman#224

Co-authored-by: kaedwen <[email protected]>
  • Loading branch information
joschi and kaedwen committed Nov 7, 2024
1 parent 91577bd commit 10fa1dd
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 10 deletions.
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 @@ -35,3 +35,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 @@ -50,6 +50,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 @@ -9,6 +9,7 @@ import (
"io"
"os"
"path"
"path/filepath"
"strings"

"github.com/wagoodman/dive/dive/filetree"
Expand Down Expand Up @@ -253,3 +254,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 @@ -41,6 +41,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 @@ -26,3 +26,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 @@ -4,4 +4,5 @@ type Resolver interface {
Name() string
Fetch(id string) (*Image, error)
Build(options []string) (*Image, error)
Extract(id string, layer string, path string) error
}
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 @@ -20,6 +20,10 @@ func (r *defaultResolver) Name() string {
return "default-resolver"
}

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 @@ -38,6 +42,10 @@ func (r *failedBuildResolver) Name() string {
return "failed-build-resolver"
}

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 @@ -56,6 +64,10 @@ func (r *failedFetchResolver) Name() string {
return "failed-fetch-resolver"
}

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

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

0 comments on commit 10fa1dd

Please sign in to comment.