diff --git a/cmd/oras/root/discover.go b/cmd/oras/root/discover.go index 162313c8c..d853e77d2 100644 --- a/cmd/oras/root/discover.go +++ b/cmd/oras/root/discover.go @@ -22,7 +22,6 @@ import ( "os" "strings" - "github.com/need-being/go-tree" "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" @@ -31,6 +30,7 @@ import ( "oras.land/oras-go/v2" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/graph" + "oras.land/oras/internal/tree" ) type discoverOptions struct { @@ -153,7 +153,7 @@ func fetchAllReferrers(ctx context.Context, repo oras.ReadOnlyGraphTarget, desc if err != nil { return err } - referrerNode.AddPathString(strings.TrimSpace(string(bytes))) + referrerNode.AddPath(strings.TrimSpace(string(bytes))) } } err := fetchAllReferrers( diff --git a/go.mod b/go.mod index 6aa1bf622..15c448295 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module oras.land/oras go 1.20 require ( - github.com/need-being/go-tree v0.1.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc2 github.com/oras-project/oras-credentials-go v0.2.0 diff --git a/go.sum b/go.sum index 8dddeea13..b6f24d871 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ 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= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/need-being/go-tree v0.1.0 h1:blQrtD006cFm97UDeMUfixwPc9o06A6c+uLaUskdNNw= -github.com/need-being/go-tree v0.1.0/go.mod h1:UOHUchuOm+lxM+EtvQ9h/IO88hK/ke7FHai4oGhhEoI= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= @@ -36,7 +34,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -oras.land/oras-go/v2 v2.2.0 h1:E1fqITD56Eg5neZbxBtAdZVgDHD6wBabJo6xESTcQyo= -oras.land/oras-go/v2 v2.2.0/go.mod h1:pXjn0+KfarspMHHNR3A56j3tgvr+mxArHuI8qVn59v8= oras.land/oras-go/v2 v2.2.1-0.20230531090906-7dd0378382c6 h1:2P1fjq1znGLo7tjy9PJsZrFF5L+qywbv28IgzKEX62E= oras.land/oras-go/v2 v2.2.1-0.20230531090906-7dd0378382c6/go.mod h1:pXjn0+KfarspMHHNR3A56j3tgvr+mxArHuI8qVn59v8= diff --git a/internal/tree/node.go b/internal/tree/node.go new file mode 100644 index 000000000..8ff1a53a9 --- /dev/null +++ b/internal/tree/node.go @@ -0,0 +1,67 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package tree pretty prints trees +package tree + +import "reflect" + +// Node represents a tree node. +type Node struct { + Value any + Nodes []*Node +} + +// New creates a new tree / root node. +func New(value any) *Node { + return &Node{ + Value: value, + } +} + +// Add adds a leaf node. +func (n *Node) Add(value any) *Node { + node := New(value) + n.Nodes = append(n.Nodes, node) + return node +} + +// AddPath adds a chain of nodes. +func (n *Node) AddPath(values ...any) *Node { + if len(values) == 0 { + return nil + } + + current := n + for _, value := range values { + if node := current.Find(value); node == nil { + current = current.Add(value) + } else { + current = node + } + } + return current +} + +// Find finds the child node with the target value. +// Nil if not found. +func (n *Node) Find(value any) *Node { + for _, node := range n.Nodes { + if reflect.DeepEqual(node.Value, value) { + return node + } + } + return nil +} diff --git a/internal/tree/node_test.go b/internal/tree/node_test.go new file mode 100644 index 000000000..685d5e874 --- /dev/null +++ b/internal/tree/node_test.go @@ -0,0 +1,171 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tree + +import ( + "bytes" + "reflect" + "testing" +) + +func TestNode_Add(t *testing.T) { + root := &Node{ + Value: "root", + } + + nodeNil := root.Add(nil) + want := &Node{} + if !reflect.DeepEqual(nodeNil, want) { + t.Errorf("Node.Add() = %v, want %v", nodeNil, want) + } + + nodeFoo := root.Add("foo") + want = &Node{ + Value: "foo", + } + if !reflect.DeepEqual(nodeFoo, want) { + t.Errorf("Node.Add() = %v, want %v", nodeFoo, want) + } + nodeBar := nodeFoo.Add("bar") + want = &Node{ + Value: "bar", + } + if !reflect.DeepEqual(nodeBar, want) { + t.Errorf("Node.Add() = %v, want %v", nodeBar, want) + } + + node42 := root.Add(42) + want = &Node{ + Value: 42, + } + if !reflect.DeepEqual(node42, want) { + t.Errorf("Node.Add() = %v, want %v", node42, want) + } + + buf := bytes.NewBuffer(nil) + printer := NewPrinter(buf) + if err := printer.Print(root); err != nil { + t.Fatalf("Printer.Print() error = %v", err) + } + gotPrint := buf.String() + // root + // ├── + // ├── foo + // │ └── bar + // └── 42 + wantPrint := "root\n├── \n├── foo\n│ └── bar\n└── 42\n" + if gotPrint != wantPrint { + t.Errorf("Node = %s, want %s", gotPrint, wantPrint) + } +} + +func TestNode_AddPath(t *testing.T) { + root := &Node{ + Value: "root", + } + + nodeNil := root.AddPath() + var want *Node + if !reflect.DeepEqual(nodeNil, want) { + t.Errorf("Node.AddPath() = %v, want %v", nodeNil, want) + } + + nodeBar := root.AddPath("foo", "bar") + want = &Node{ + Value: "bar", + } + if !reflect.DeepEqual(nodeBar, want) { + t.Errorf("Node.AddPath() = %v, want %v", nodeBar, want) + } + nodeBar2 := root.AddPath("foo", "bar2") + want = &Node{ + Value: "bar2", + } + if !reflect.DeepEqual(nodeBar2, want) { + t.Errorf("Node.AddPath() = %v, want %v", nodeBar2, want) + } + + node42 := root.AddPath(42) + want = &Node{ + Value: 42, + } + if !reflect.DeepEqual(node42, want) { + t.Errorf("Node.AddPath() = %v, want %v", node42, want) + } + + buf := bytes.NewBuffer(nil) + printer := NewPrinter(buf) + if err := printer.Print(root); err != nil { + t.Fatalf("Printer.Print() error = %v", err) + } + gotPrint := buf.String() + // root + // ├── foo + // │ ├── bar + // │ └── bar2 + // └── 42 + wantPrint := "root\n├── foo\n│ ├── bar\n│ └── bar2\n└── 42\n" + if gotPrint != wantPrint { + t.Errorf("Node = %s, want %s", gotPrint, wantPrint) + } +} + +func TestNode_Find(t *testing.T) { + root := &Node{ + Value: "root", + Nodes: []*Node{ + { + Value: "foo", + Nodes: []*Node{ + { + Value: "bar", + }, + }, + }, + { + Value: 42, + }, + }, + } + tests := []struct { + name string + value any + want *Node + }{ + { + name: "find existing node", + value: 42, + want: root.Nodes[1], + }, + { + name: "find non-existing node", + value: "hello", + want: nil, + }, + { + name: "find non-existing node but it is a grand child", + value: "bar", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := root.Find(tt.value); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Node.Find() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/tree/printer.go b/internal/tree/printer.go new file mode 100644 index 000000000..75a6f87db --- /dev/null +++ b/internal/tree/printer.go @@ -0,0 +1,82 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tree + +import ( + "fmt" + "io" + "os" +) + +// Box-drawing symbols +const ( + EdgeEmpty = " " + EdgePipe = "│ " + EdgeItem = "├── " + EdgeLast = "└── " +) + +// DefaultPrinter prints the tree to the stdout with default settings. +var DefaultPrinter = NewPrinter(os.Stdout) + +// Printer prints the tree. +type Printer struct { + writer io.Writer +} + +// NewPrinter create s a new printer. +func NewPrinter(writer io.Writer) *Printer { + return &Printer{ + writer: writer, + } +} + +// Print prints a tree. +func (p *Printer) Print(root *Node) error { + return p.print("", root) +} + +// print prints a tree recursively. +func (p *Printer) print(prefix string, n *Node) error { + if _, err := fmt.Fprintln(p.writer, n.Value); err != nil { + return err + } + size := len(n.Nodes) + if size == 0 { + return nil + } + + prefixItem := prefix + EdgeItem + prefixPipe := prefix + EdgePipe + last := size - 1 + for _, n := range n.Nodes[:last] { + if _, err := io.WriteString(p.writer, prefixItem); err != nil { + return err + } + if err := p.print(prefixPipe, n); err != nil { + return nil + } + } + if _, err := io.WriteString(p.writer, prefix+EdgeLast); err != nil { + return err + } + return p.print(prefix+EdgeEmpty, n.Nodes[last]) +} + +// Print prints the tree using the default printer. +func Print(root *Node) error { + return DefaultPrinter.Print(root) +} diff --git a/internal/tree/printer_test.go b/internal/tree/printer_test.go new file mode 100644 index 000000000..731867b82 --- /dev/null +++ b/internal/tree/printer_test.go @@ -0,0 +1,182 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tree + +import ( + "bytes" + "testing" +) + +func TestPrinter_Print(t *testing.T) { + tests := []struct { + name string + root *Node + want string + }{ + { + name: "single node tree", + root: &Node{ + Value: "root", + }, + want: "root\n", + }, + { + name: "single child", + root: &Node{ + Value: "root", + Nodes: []*Node{ + { + Value: "hello", + }, + }, + }, + want: "root\n└── hello\n", + }, + { + name: "multiple children", + root: &Node{ + Value: "root", + Nodes: []*Node{ + { + Value: "hello", + }, + { + Value: "world", + }, + }, + }, + want: "root\n├── hello\n└── world\n", + }, + { + name: "nested tree (beginning)", + root: &Node{ + Value: "root", + Nodes: []*Node{ + { + Value: "foo", + Nodes: []*Node{ + { + Value: "bar", + }, + { + Value: 42, + }, + }, + }, + { + Value: "hello", + }, + { + Value: "world", + }, + }, + }, + want: "root\n├── foo\n│ ├── bar\n│ └── 42\n├── hello\n└── world\n", + }, + { + name: "nested tree (middle)", + root: &Node{ + Value: "root", + Nodes: []*Node{ + { + Value: "hello", + }, + { + Value: "foo", + Nodes: []*Node{ + { + Value: "bar", + }, + { + Value: 42, + }, + }, + }, + { + Value: "world", + }, + }, + }, + want: "root\n├── hello\n├── foo\n│ ├── bar\n│ └── 42\n└── world\n", + }, + { + name: "nested tree (end)", + root: &Node{ + Value: "root", + Nodes: []*Node{ + { + Value: "hello", + }, + { + Value: "world", + }, + { + Value: "foo", + Nodes: []*Node{ + { + Value: "bar", + }, + { + Value: 42, + }, + }, + }, + }, + }, + want: "root\n├── hello\n├── world\n└── foo\n ├── bar\n └── 42\n", + }, + { + name: "double nested tree", + root: &Node{ + Value: "root", + Nodes: []*Node{ + { + Value: "hello", + }, + { + Value: "foo", + Nodes: []*Node{ + { + Value: "bar", + Nodes: []*Node{ + { + Value: 42, + }, + }, + }, + }, + }, + { + Value: "world", + }, + }, + }, + want: "root\n├── hello\n├── foo\n│ └── bar\n│ └── 42\n└── world\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + printer := NewPrinter(buf) + if err := printer.Print(tt.root); err != nil { + t.Fatalf("Printer.Print() error = %v", err) + } + if got := buf.String(); got != tt.want { + t.Errorf("Printer.Print() = %s, want %s", got, tt.want) + } + }) + } +}