Skip to content

Commit

Permalink
fix: properly print tree for oras discover (#1005)
Browse files Browse the repository at this point in the history
Signed-off-by: Shiwei Zhang <[email protected]>
  • Loading branch information
shizhMSFT authored Jul 6, 2023
1 parent 42650e5 commit 34390f9
Show file tree
Hide file tree
Showing 7 changed files with 504 additions and 7 deletions.
4 changes: 2 additions & 2 deletions cmd/oras/root/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
67 changes: 67 additions & 0 deletions internal/tree/node.go
Original file line number Diff line number Diff line change
@@ -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
}
171 changes: 171 additions & 0 deletions internal/tree/node_test.go
Original file line number Diff line number Diff line change
@@ -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
// ├── <nil>
// ├── foo
// │ └── bar
// └── 42
wantPrint := "root\n├── <nil>\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)
}
})
}
}
82 changes: 82 additions & 0 deletions internal/tree/printer.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 34390f9

Please sign in to comment.