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

fix: properly print tree for oras discover #1005

Merged
merged 7 commits into from
Jul 6, 2023
Merged
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
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