Skip to content
This repository was archived by the owner on Feb 16, 2023. It is now read-only.

Commit 5ccc9e9

Browse files
Merge pull request #319 from secrethub/feature/tree-flags
Add `-f`, `-i` and `--noreport` flags to tree command
2 parents 29212c7 + 50624b9 commit 5ccc9e9

2 files changed

Lines changed: 304 additions & 25 deletions

File tree

internals/secrethub/tree.go

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ import (
1313

1414
// TreeCommand lists the contents of a directory at a given path in a tree-like format.
1515
type TreeCommand struct {
16-
path api.DirPath
17-
io ui.IO
18-
newClient newClientFunc
16+
path api.DirPath
17+
io ui.IO
18+
fullPaths bool
19+
noIndentation bool
20+
noReport bool
21+
newClient newClientFunc
1922
}
2023

2124
// NewTreeCommand creates a new TreeCommand.
@@ -38,7 +41,7 @@ func (cmd *TreeCommand) Run() error {
3841
return err
3942
}
4043

41-
printTree(t, cmd.io.Output())
44+
cmd.printTree(t, cmd.io.Output())
4245
return nil
4346
}
4447

@@ -47,53 +50,90 @@ func (cmd *TreeCommand) Register(r command.Registerer) {
4750
clause := r.Command("tree", "List contents of a directory in a tree-like format.")
4851
clause.Arg("dir-path", "The path to to show contents for").Required().PlaceHolder(optionalDirPathPlaceHolder).SetValue(&cmd.path)
4952

53+
clause.Flag("full-paths", "Print the full path of each directory and secret.").Short('f').BoolVar(&cmd.fullPaths)
54+
clause.Flag("no-indentation", "Don't print indentation lines.").Short('i').BoolVar(&cmd.noIndentation)
55+
clause.Flag("no-report", "Turn off secret/directory count at end of tree listing.").BoolVar(&cmd.noReport)
56+
clause.Flag("noreport", "Turn off secret/directory count at end of tree listing.").Hidden().BoolVar(&cmd.noReport)
57+
5058
command.BindAction(clause, cmd.Run)
5159
}
5260

5361
// printTree recursively prints the tree's contents in a tree-like structure.
54-
func printTree(t *api.Tree, w io.Writer) {
55-
name := colorizeByStatus(t.RootDir.Status, t.RootDir.Name)
56-
fmt.Fprintf(w, "%s/\n", name)
57-
58-
printDirContentsRecursively(t.RootDir, "", w)
62+
func (cmd *TreeCommand) printTree(t *api.Tree, w io.Writer) {
5963

60-
fmt.Fprintf(w,
61-
"\n%s, %s\n",
62-
pluralize("directory", "directories", t.DirCount()),
63-
pluralize("secret", "secrets", t.SecretCount()),
64-
)
64+
rootDirName := func() string {
65+
if cmd.fullPaths {
66+
return cmd.path.Value() + "/"
67+
}
68+
return t.RootDir.Name + "/"
69+
}()
70+
name := colorizeByStatus(t.RootDir.Status, rootDirName)
71+
fmt.Fprintf(w, "%s\n", name)
72+
73+
if cmd.fullPaths {
74+
cmd.printDirContentsRecursively(t.RootDir, "", w, cmd.path.Value())
75+
} else {
76+
cmd.printDirContentsRecursively(t.RootDir, "", w, "")
77+
}
78+
if !cmd.noReport {
79+
fmt.Fprintf(w,
80+
"\n%s, %s\n",
81+
pluralize("directory", "directories", t.DirCount()),
82+
pluralize("secret", "secrets", t.SecretCount()),
83+
)
84+
}
6585
}
6686

6787
// printDirContentsRecursively is a recursive function that prints the directory's contents
6888
// in a tree-like structure, subdirs first followed by secrets.
69-
func printDirContentsRecursively(dir *api.Dir, prefix string, w io.Writer) {
89+
func (cmd *TreeCommand) printDirContentsRecursively(dir *api.Dir, prefix string, w io.Writer, prevPath string) {
7090

7191
sort.Sort(api.SortDirByName(dir.SubDirs))
7292
sort.Sort(api.SortSecretByName(dir.Secrets))
7393

7494
total := len(dir.SubDirs) + len(dir.Secrets)
7595

96+
if cmd.fullPaths {
97+
prevPath += "/"
98+
} else {
99+
prevPath = ""
100+
}
101+
76102
i := 0
77103
for _, sub := range dir.SubDirs {
78-
name := colorizeByStatus(sub.Status, sub.Name)
79104

80-
if i == total-1 {
81-
fmt.Fprintf(w, "%s└── %s/\n", prefix, name)
82-
printDirContentsRecursively(sub, prefix+" ", w)
105+
name := sub.Name
106+
if cmd.fullPaths {
107+
name = prevPath + name
108+
}
109+
colorName := colorizeByStatus(sub.Status, name+"/")
110+
111+
if cmd.noIndentation {
112+
fmt.Fprintf(w, "%s\n", colorName)
113+
cmd.printDirContentsRecursively(sub, prefix, w, name)
114+
} else if i == total-1 {
115+
fmt.Fprintf(w, "%s└── %s\n", prefix, colorName)
116+
cmd.printDirContentsRecursively(sub, prefix+" ", w, name)
83117
} else {
84-
fmt.Fprintf(w, "%s├── %s/\n", prefix, name)
85-
printDirContentsRecursively(sub, prefix+"│ ", w)
118+
fmt.Fprintf(w, "%s├── %s\n", prefix, colorName)
119+
cmd.printDirContentsRecursively(sub, prefix+"│ ", w, name)
86120
}
87121
i++
88122
}
89123

90124
for _, secret := range dir.Secrets {
91-
name := colorizeByStatus(secret.Status, secret.Name)
125+
name := secret.Name
126+
if cmd.fullPaths {
127+
name = prevPath + name
128+
}
129+
colorName := colorizeByStatus(secret.Status, name)
92130

93-
if i == total-1 {
94-
fmt.Fprintf(w, "%s└── %s\n", prefix, name)
131+
if cmd.noIndentation {
132+
fmt.Fprintf(w, "%s\n", colorName)
133+
} else if i == total-1 {
134+
fmt.Fprintf(w, "%s└── %s\n", prefix, colorName)
95135
} else {
96-
fmt.Fprintf(w, "%s├── %s\n", prefix, name)
136+
fmt.Fprintf(w, "%s├── %s\n", prefix, colorName)
97137
}
98138
i++
99139
}

internals/secrethub/tree_test.go

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package secrethub
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/fatih/color"
8+
"github.com/secrethub/secrethub-cli/internals/cli/ui"
9+
"github.com/secrethub/secrethub-go/internals/api"
10+
"github.com/secrethub/secrethub-go/internals/api/uuid"
11+
"github.com/secrethub/secrethub-go/internals/assert"
12+
"github.com/secrethub/secrethub-go/pkg/secrethub"
13+
)
14+
15+
func TestSimpleTree(t *testing.T) {
16+
uuid0, _ := uuid.FromString("0")
17+
uuid1, _ := uuid.FromString("1")
18+
tree := &api.Tree{
19+
RootDir: &api.Dir{
20+
Name: "test/repo",
21+
DirID: uuid0,
22+
SubDirs: []*api.Dir{
23+
{
24+
Name: "secretFolder",
25+
DirID: uuid1,
26+
ParentID: &uuid0,
27+
Secrets: []*api.Secret{
28+
{Name: "found you"},
29+
},
30+
},
31+
},
32+
Secrets: []*api.Secret{
33+
{Name: "mySecret"},
34+
},
35+
},
36+
Dirs: map[uuid.UUID]*api.Dir{
37+
uuid.New(): {
38+
DirID: uuid0,
39+
ParentID: &uuid0,
40+
},
41+
uuid.New(): {
42+
Name: "secretFolder",
43+
DirID: uuid1,
44+
ParentID: &uuid0,
45+
Secrets: []*api.Secret{
46+
{Name: "found you"},
47+
},
48+
},
49+
},
50+
Secrets: map[uuid.UUID]*api.Secret{
51+
uuid.New(): {Name: "found you"},
52+
uuid.New(): {Name: "mySecret"},
53+
},
54+
}
55+
cases := map[string]struct {
56+
cmd *TreeCommand
57+
expectedOutput string
58+
}{
59+
"simple tree": {
60+
cmd: &TreeCommand{
61+
io: ui.NewUserIO(),
62+
newClient: func() (secrethub.ClientInterface, error) {
63+
return &secrethub.Client{}, nil
64+
},
65+
},
66+
expectedOutput: "test/repo/\n" +
67+
"├── secretFolder/\n" +
68+
"│ └── found you\n" +
69+
"└── mySecret\n\n" +
70+
"1 directory, 2 secrets\n",
71+
},
72+
"full path": {
73+
cmd: &TreeCommand{
74+
path: "test/repo",
75+
io: ui.NewUserIO(),
76+
fullPaths: true,
77+
newClient: func() (secrethub.ClientInterface, error) {
78+
return &secrethub.Client{}, nil
79+
},
80+
},
81+
expectedOutput: "test/repo/\n" +
82+
"├── test/repo/secretFolder/\n" +
83+
"│ └── test/repo/secretFolder/found you\n" +
84+
"└── test/repo/mySecret\n\n" +
85+
"1 directory, 2 secrets\n",
86+
},
87+
"no indent": {
88+
cmd: &TreeCommand{
89+
io: ui.NewUserIO(),
90+
noIndentation: true,
91+
newClient: func() (secrethub.ClientInterface, error) {
92+
return &secrethub.Client{}, nil
93+
},
94+
},
95+
expectedOutput: "test/repo/\n" +
96+
"secretFolder/\n" +
97+
"found you\n" +
98+
"mySecret\n\n" +
99+
"1 directory, 2 secrets\n",
100+
},
101+
"no report": {
102+
cmd: &TreeCommand{
103+
io: ui.NewUserIO(),
104+
noReport: true,
105+
newClient: func() (secrethub.ClientInterface, error) {
106+
return &secrethub.Client{}, nil
107+
},
108+
},
109+
expectedOutput: "test/repo/\n" +
110+
"├── secretFolder/\n" +
111+
"│ └── found you\n" +
112+
"└── mySecret\n",
113+
},
114+
"all flags": {
115+
cmd: &TreeCommand{
116+
path: "test/repo",
117+
io: ui.NewUserIO(),
118+
fullPaths: true,
119+
noIndentation: true,
120+
noReport: true,
121+
newClient: func() (secrethub.ClientInterface, error) {
122+
return &secrethub.Client{}, nil
123+
},
124+
},
125+
expectedOutput: "test/repo/\n" +
126+
"test/repo/secretFolder/\n" +
127+
"test/repo/secretFolder/found you\n" +
128+
"test/repo/mySecret\n",
129+
},
130+
}
131+
132+
for name, tc := range cases {
133+
t.Run(name, func(t *testing.T) {
134+
w := &bytes.Buffer{}
135+
tc.cmd.printTree(tree, w)
136+
assert.Equal(t, w.String(), tc.expectedOutput)
137+
})
138+
}
139+
}
140+
141+
func TestTreeColoring(t *testing.T) {
142+
color.NoColor = false
143+
uuid0, _ := uuid.FromString("0")
144+
uuid1, _ := uuid.FromString("1")
145+
uuid2, _ := uuid.FromString("2")
146+
tree := &api.Tree{
147+
RootDir: &api.Dir{
148+
Name: "test/repo",
149+
Status: api.StatusFlagged,
150+
DirID: uuid0,
151+
SubDirs: []*api.Dir{
152+
{
153+
Name: "happy",
154+
DirID: uuid1,
155+
ParentID: &uuid0,
156+
},
157+
{
158+
Name: "secretFolder",
159+
DirID: uuid2,
160+
ParentID: &uuid0,
161+
Status: api.StatusFlagged,
162+
Secrets: []*api.Secret{
163+
{
164+
Name: "found you",
165+
Status: api.StatusFlagged,
166+
},
167+
},
168+
},
169+
},
170+
Secrets: []*api.Secret{
171+
{Name: "mySecret"},
172+
},
173+
},
174+
Dirs: map[uuid.UUID]*api.Dir{
175+
uuid.New(): {
176+
DirID: uuid0,
177+
ParentID: &uuid0,
178+
},
179+
uuid.New(): {
180+
Name: "happy",
181+
DirID: uuid1,
182+
ParentID: &uuid0,
183+
},
184+
uuid.New(): {
185+
Name: "secretFolder",
186+
DirID: uuid2,
187+
ParentID: &uuid0,
188+
Secrets: []*api.Secret{
189+
{Name: "found you"},
190+
},
191+
},
192+
},
193+
Secrets: map[uuid.UUID]*api.Secret{
194+
uuid.New(): {Name: "found you"},
195+
uuid.New(): {Name: "mySecret"},
196+
},
197+
}
198+
cases := map[string]struct {
199+
cmd *TreeCommand
200+
expectedOutput string
201+
}{
202+
"simple tree": {
203+
cmd: &TreeCommand{
204+
newClient: func() (secrethub.ClientInterface, error) {
205+
return &secrethub.Client{}, nil
206+
},
207+
},
208+
expectedOutput: red.Sprint("test/repo/") + "\n" +
209+
"├── happy/\n" +
210+
"├── " + red.Sprint("secretFolder/") + "\n" +
211+
"│ └── " + red.Sprint("found you") + "\n" +
212+
"└── mySecret\n\n" +
213+
"2 directories, 2 secrets\n",
214+
},
215+
"full path": {
216+
cmd: &TreeCommand{
217+
path: "test/repo",
218+
fullPaths: true,
219+
newClient: func() (secrethub.ClientInterface, error) {
220+
return &secrethub.Client{}, nil
221+
},
222+
},
223+
expectedOutput: red.Sprint("test/repo/") + "\n" +
224+
"├── test/repo/happy/\n" +
225+
"├── " + red.Sprint("test/repo/secretFolder/") + "\n" +
226+
"│ └── " + red.Sprint("test/repo/secretFolder/found you") + "\n" +
227+
"└── test/repo/mySecret\n\n" +
228+
"2 directories, 2 secrets\n",
229+
},
230+
}
231+
232+
for name, tc := range cases {
233+
t.Run(name, func(t *testing.T) {
234+
w := new(bytes.Buffer)
235+
tc.cmd.printTree(tree, w)
236+
assert.Equal(t, w.String(), tc.expectedOutput)
237+
})
238+
}
239+
}

0 commit comments

Comments
 (0)