Skip to content

Commit 8d6d262

Browse files
authored
v0.0.3 (#3)
* Add `--depth` parameter
1 parent 284a6cd commit 8d6d262

File tree

7 files changed

+118
-56
lines changed

7 files changed

+118
-56
lines changed

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
VERSION := 0.0.2
1+
APP_NAME := docker-tree
2+
VERSION := 0.0.3
23

34
build: test
4-
go build -ldflags="-X 'main.version=$(VERSION)'" -o docker-tree ./cmd/
5+
go build -ldflags="-X 'main.version=${VERSION}'" -o ${APP_NAME} ./cmd/
56

67
test:
78
go vet ./...

README.md

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@ mv ./docker-tree ~/.docker/cli-plugins/docker-tree
2121
```shell
2222
# Absent image will be pulled automatically
2323
➜ docker tree alpine:3.20 /etc/ssl
24-
3.20: Pulling from library/alpine
25-
a258b2a6b59a: Pull complete
26-
Digest: sha256:b89d9c93e9ed3597455c90a0b88a8bbb5cb7188438f70953fede212a0c4394e0
27-
Status: Downloaded newer image for alpine:3.20
28-
docker.io/library/alpine:3.20
24+
processing image: alpine:3.20
2925
ssl/
3026
├── cert.pem
3127
├── certs/
@@ -35,17 +31,4 @@ ssl/
3531
├── openssl.cnf
3632
├── openssl.cnf.dist
3733
└── private/
38-
39-
# Show file tree with symlinks
40-
➜ docker tree -l alpine:3.20 | head
41-
precessing image: alpine:3.20
42-
/
43-
├── bin/
44-
│ ├── arch -> /bin/busybox
45-
│ ├── ash -> /bin/busybox
46-
│ ├── base64 -> /bin/busybox
47-
│ ├── bbconfig -> /bin/busybox
48-
│ ├── busybox
49-
│ ├── cat -> /bin/busybox
50-
│ ├── chattr -> /bin/busybox
5134
```

cmd/main.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"fmt"
5+
"math"
56
"os"
67

78
"github.com/skondrashov/docker-tree/internal/docker"
@@ -22,6 +23,7 @@ func main() {
2223
var (
2324
quiet bool
2425
showLinks bool
26+
depth int
2527
)
2628

2729
cmd := &cobra.Command{
@@ -47,11 +49,17 @@ You can also specify a directory to see the file tree relative to this directory
4749
treeRoot = args[1]
4850
}
4951

52+
depth++ // to display not only "/" with --depth == 1
53+
if depth <= 1 {
54+
depth = math.MaxInt
55+
}
56+
5057
treeStrings, err := docker.GetImageTree(docker.GetTreeOpts{
5158
Cli: dockerCli,
5259
ImageID: imageID,
5360
Quiet: quiet,
5461
ShowLinks: showLinks,
62+
Depth: depth,
5563
TreeRoot: treeRoot,
5664
})
5765
if err != nil {
@@ -64,6 +72,7 @@ You can also specify a directory to see the file tree relative to this directory
6472
}
6573

6674
flags := cmd.Flags()
75+
flags.IntVarP(&depth, "depth", "d", 0, "Show maximum depth of hierarchical trees, 0 - unlimited")
6776
flags.BoolVarP(&quiet, "quiet", "q", false, "Suppress verbose output")
6877
flags.BoolVarP(&showLinks, "links", "l", false, "Show symlinks destination")
6978

internal/docker/layers.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ func getFileTreeFromLayer(layerReader *tar.Reader) (*fileTreeNode, error) {
118118
if err != nil {
119119
return nil, errNotATar
120120
}
121-
fileTree.addChild(header)
121+
122+
if !strings.HasSuffix(header.Name, whiteoutDirPrefix) {
123+
fileTree.addChild(header)
124+
}
122125
}
123126
return fileTree, nil
124127
}

internal/docker/main.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type GetTreeOpts struct {
1313
ImageID string
1414
Quiet bool
1515
ShowLinks bool
16+
Depth int
1617
TreeRoot string
1718
}
1819

@@ -36,7 +37,7 @@ func GetImageTree(opts GetTreeOpts) (string, error) {
3637
}
3738

3839
if !opts.Quiet {
39-
fmt.Fprintf(opts.Cli.Out(), "precessing image: %s\n", opts.ImageID)
40+
fmt.Fprintf(opts.Cli.Out(), "processing image: %s\n", opts.ImageID)
4041
}
4142

4243
imageReader, err := opts.Cli.Client().ImageSave(ctx, []string{opts.ImageID})
@@ -64,5 +65,10 @@ func GetImageTree(opts GetTreeOpts) (string, error) {
6465
return "", fmt.Errorf("there is no such path in the image: %s", opts.TreeRoot)
6566
}
6667

67-
return node.getString("", opts.ShowLinks, true, true), nil
68+
printOptions := getStringOpts{
69+
showLinks: opts.ShowLinks,
70+
depth: opts.Depth,
71+
}
72+
73+
return node.getString("", printOptions, true, true), nil
6874
}

internal/docker/tree.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const (
1515
last = "└── "
1616
link = " -> "
1717

18-
delFilePrefix = ".wh."
18+
whiteoutFilePrefix = ".wh."
19+
whiteoutDirPrefix = ".wh..wh..opq"
1920
)
2021

2122
type fileTreeNode struct {
@@ -25,7 +26,17 @@ type fileTreeNode struct {
2526
Children []*fileTreeNode
2627
}
2728

28-
func (n *fileTreeNode) getString(prefix string, showLinks, isFirst, isLast bool) string {
29+
type getStringOpts struct {
30+
showLinks bool
31+
depth int
32+
}
33+
34+
func (n *fileTreeNode) getString(prefix string, opts getStringOpts, isFirst, isLast bool) string {
35+
opts.depth--
36+
if opts.depth == -1 {
37+
return empty
38+
}
39+
2940
passPrefix := prefix
3041
currentPrefix := empty
3142

@@ -45,12 +56,12 @@ func (n *fileTreeNode) getString(prefix string, showLinks, isFirst, isLast bool)
4556
}
4657

4758
result := fmt.Sprintf("%s%s%s\n", prefix, currentPrefix, name)
48-
if showLinks && n.Symlink != "" {
59+
if opts.showLinks && n.Symlink != "" {
4960
result = fmt.Sprintf("%s%s%s%s%s\n", prefix, currentPrefix, name, link, n.Symlink)
5061
}
5162

5263
for i, child := range n.Children {
53-
result += child.getString(passPrefix, showLinks, false, i == len(n.Children)-1)
64+
result += child.getString(passPrefix, opts, false, i == len(n.Children)-1)
5465
}
5566

5667
return result
@@ -127,8 +138,8 @@ func mergeFileTrees(original, updated *fileTreeNode) (*fileTreeNode, error) {
127138
continue
128139
}
129140

130-
if strings.HasPrefix(updatedChild.Name, delFilePrefix) {
131-
updatedChild.Name = strings.TrimPrefix(updatedChild.Name, delFilePrefix)
141+
if strings.HasPrefix(updatedChild.Name, whiteoutFilePrefix) {
142+
updatedChild.Name = strings.TrimPrefix(updatedChild.Name, whiteoutFilePrefix)
132143
if err := original.deleteNode(updatedChild); err != nil {
133144
return nil, fmt.Errorf("error deleting file %s: %w", updatedChild.Name, err)
134145
}

internal/docker/tree_test.go

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,45 @@ func Test_fileTreeNode_String(t *testing.T) {
2020
tests := []struct {
2121
name string
2222
fields fields
23+
opts getStringOpts
2324
want string
2425
}{
2526
{
2627
name: "get string of only root node",
2728
fields: fields{"/", true, nil},
29+
opts: getStringOpts{showLinks: false, depth: 99999},
2830
want: "/\n",
2931
},
3032
{
3133
name: "get string of /etc/file",
3234
fields: fields{"/", true, []*fileTreeNode{&etcNode}},
35+
opts: getStringOpts{showLinks: false, depth: 99999},
3336
want: "/\n└── etc/\n └── file\n",
3437
},
3538
{
3639
name: "get string of /etc/file + /other_file",
3740
fields: fields{"/", true, []*fileTreeNode{&etcNode, &otherFileNode}},
41+
opts: getStringOpts{showLinks: false, depth: 99999},
3842
want: "/\n├── etc/\n│ └── file\n└── other_file\n",
3943
},
4044
{
4145
name: "get string with symlink",
4246
fields: fields{"/", true, []*fileTreeNode{&etcNode, &binNodeWithSymlink}},
47+
opts: getStringOpts{showLinks: true, depth: 99999},
4348
want: "/\n├── etc/\n│ └── file\n└── bin/\n ├── file\n └── link -> /tmp/file\n",
4449
},
50+
{
51+
name: "get string with depth = 1",
52+
fields: fields{"/", true, []*fileTreeNode{&etcNode, &binNodeWithSymlink}},
53+
opts: getStringOpts{showLinks: false, depth: 2}, // we use depth == 2 because we want it to handle root + one more level of nesting
54+
want: "/\n├── etc/\n└── bin/\n",
55+
},
56+
{
57+
name: "get string with depth = 2",
58+
fields: fields{"/", true, []*fileTreeNode{&etcNode, &binNodeWithSymlink}},
59+
opts: getStringOpts{showLinks: false, depth: 3},
60+
want: "/\n├── etc/\n│ └── file\n└── bin/\n ├── file\n └── link\n",
61+
},
4562
}
4663
for _, tt := range tests {
4764
t.Run(tt.name, func(t *testing.T) {
@@ -50,28 +67,15 @@ func Test_fileTreeNode_String(t *testing.T) {
5067
IsDir: tt.fields.IsDir,
5168
Children: tt.fields.Children,
5269
}
53-
if got := n.getString("", true, true, true); got != tt.want {
70+
71+
if got := n.getString("", tt.opts, true, true); got != tt.want {
5472
t.Errorf("getString() = %v, want %v", got, tt.want)
5573
}
5674
})
5775
}
5876
}
5977

6078
func Test_mergeFileTrees(t *testing.T) {
61-
singleFileTree := &fileTreeNode{"file", "", false, nil}
62-
63-
etcWithFile := &fileTreeNode{"etc", "", true, []*fileTreeNode{singleFileTree}}
64-
rootWithEtcTreeNode := &fileTreeNode{"/", "", true, []*fileTreeNode{etcWithFile}}
65-
66-
varWithFile := &fileTreeNode{"var", "", true, []*fileTreeNode{singleFileTree}}
67-
rootWithVarTreeNode := &fileTreeNode{"/", "", true, []*fileTreeNode{varWithFile}}
68-
69-
deleteSingleFileTree := &fileTreeNode{".wh.file", "", false, nil}
70-
etcWithDeleteFile := &fileTreeNode{"etc", "", true, []*fileTreeNode{deleteSingleFileTree}}
71-
rootWithEtcWithDeleteFileTreeNode := &fileTreeNode{"/", "", true, []*fileTreeNode{etcWithDeleteFile}}
72-
73-
rootWithEtcWithDeleteFileAndAddVarFileTreeNode := &fileTreeNode{"/", "", true, []*fileTreeNode{etcWithDeleteFile, varWithFile}}
74-
7579
type args struct {
7680
original *fileTreeNode
7781
updated *fileTreeNode
@@ -86,40 +90,83 @@ func Test_mergeFileTrees(t *testing.T) {
8690
name: "original is nil",
8791
args: args{
8892
original: nil,
89-
updated: singleFileTree,
93+
updated: &fileTreeNode{"file", "", false, nil},
9094
},
91-
want: singleFileTree,
95+
want: &fileTreeNode{"file", "", false, nil},
9296
wantErr: false,
9397
},
9498
{
9599
name: "add /var/file to /etc/file",
96100
args: args{
97-
original: rootWithEtcTreeNode,
98-
updated: rootWithVarTreeNode,
101+
original: &fileTreeNode{"/", "", true, []*fileTreeNode{
102+
{"etc", "", true, []*fileTreeNode{
103+
{"file", "", false, nil},
104+
}},
105+
}},
106+
updated: &fileTreeNode{"/", "", true, []*fileTreeNode{
107+
{"var", "", true, []*fileTreeNode{
108+
{"file", "", false, nil},
109+
}},
110+
}},
99111
},
100-
want: &fileTreeNode{"/", "", true, []*fileTreeNode{etcWithFile, varWithFile}},
112+
want: &fileTreeNode{"/", "", true, []*fileTreeNode{
113+
{"etc", "", true, []*fileTreeNode{
114+
{"file", "", false, nil}},
115+
},
116+
{"var", "", true, []*fileTreeNode{
117+
{"file", "", false, nil}},
118+
},
119+
}},
120+
101121
wantErr: false,
102122
},
103123
{
104124
name: "delete /etc/file",
105125
args: args{
106-
original: rootWithEtcTreeNode,
107-
updated: rootWithEtcWithDeleteFileTreeNode,
126+
original: &fileTreeNode{"/", "", true, []*fileTreeNode{
127+
{"etc", "", true, []*fileTreeNode{
128+
{"file", "", false, nil},
129+
}},
130+
}},
131+
updated: &fileTreeNode{"/", "", true, []*fileTreeNode{
132+
{"etc", "", true, []*fileTreeNode{
133+
{".wh.file", "", false, nil},
134+
}},
135+
}},
108136
},
109-
want: &fileTreeNode{"/", "", true, []*fileTreeNode{{"etc", "", true, []*fileTreeNode{}}}},
137+
want: &fileTreeNode{"/", "", true, []*fileTreeNode{
138+
{"etc", "", true, []*fileTreeNode{}},
139+
}},
110140
wantErr: false,
111141
},
112142
{
113143
name: "delete /etc/file and add /var/file",
114144
args: args{
115-
original: rootWithEtcTreeNode,
116-
updated: rootWithEtcWithDeleteFileAndAddVarFileTreeNode,
145+
original: &fileTreeNode{"/", "", true, []*fileTreeNode{
146+
{"etc", "", true, []*fileTreeNode{
147+
{"file", "", false, nil},
148+
}},
149+
}},
150+
updated: &fileTreeNode{"/", "", true, []*fileTreeNode{
151+
{"etc", "", true, []*fileTreeNode{
152+
{".wh.file", "", false, nil},
153+
}},
154+
{"var", "", false, []*fileTreeNode{
155+
{"file", "", false, nil},
156+
}},
157+
}},
117158
},
118-
want: &fileTreeNode{"/", "", true, []*fileTreeNode{{"etc", "", true, []*fileTreeNode{}}, varWithFile}},
159+
want: &fileTreeNode{"/", "", true, []*fileTreeNode{
160+
{"etc", "", true, []*fileTreeNode{}},
161+
{"var", "", false, []*fileTreeNode{
162+
{"file", "", false, nil},
163+
}},
164+
}},
119165
wantErr: false,
120166
},
121167
}
122168

169+
defaultOpts := getStringOpts{showLinks: true, depth: 99999}
123170
for _, tt := range tests {
124171
t.Run(tt.name, func(t *testing.T) {
125172
got, err := mergeFileTrees(tt.args.original, tt.args.updated)
@@ -128,7 +175,9 @@ func Test_mergeFileTrees(t *testing.T) {
128175
return
129176
}
130177
if !reflect.DeepEqual(got, tt.want) {
131-
t.Errorf("mergeFileTrees() got:\n%v, want:\n%v", got, tt.want)
178+
t.Errorf("mergeFileTrees() got:\n%v, want:\n%v",
179+
got.getString("", defaultOpts, true, false),
180+
tt.want.getString("", defaultOpts, true, false))
132181
}
133182
})
134183
}

0 commit comments

Comments
 (0)