Skip to content

Commit 757dd4d

Browse files
committed
ls: implement --tree to show as a tree and --level to limit depth for recursion
1 parent 3651939 commit 757dd4d

File tree

7 files changed

+844
-85
lines changed

7 files changed

+844
-85
lines changed

dvc/commands/ls/__init__.py

+158-36
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Callable
2+
13
from dvc.cli import completion, formatter
24
from dvc.cli.command import CmdBaseNoRepo
35
from dvc.cli.utils import DictAction, append_doc_link
@@ -9,7 +11,18 @@
911
logger = logger.getChild(__name__)
1012

1113

12-
def _format_entry(entry, fmt, with_size=True, with_md5=False):
14+
def _get_formatter(with_color: bool = False) -> Callable[[dict], str]:
15+
def fmt(entry: dict) -> str:
16+
return entry["path"]
17+
18+
if with_color:
19+
ls_colors = LsColors()
20+
return ls_colors.format
21+
22+
return fmt
23+
24+
25+
def _format_entry(entry, name, with_size=True, with_hash=False):
1326
from dvc.utils.humanize import naturalsize
1427

1528
ret = []
@@ -20,60 +33,156 @@ def _format_entry(entry, fmt, with_size=True, with_md5=False):
2033
else:
2134
size = naturalsize(size)
2235
ret.append(size)
23-
if with_md5:
36+
if with_hash:
2437
md5 = entry.get("md5", "")
2538
ret.append(md5)
26-
ret.append(fmt(entry))
39+
ret.append(name)
2740
return ret
2841

2942

30-
def show_entries(entries, with_color=False, with_size=False, with_md5=False):
31-
if with_color:
32-
ls_colors = LsColors()
33-
fmt = ls_colors.format
34-
else:
35-
36-
def fmt(entry):
37-
return entry["path"]
38-
39-
if with_size or with_md5:
43+
def show_entries(entries, with_color=False, with_size=False, with_hash=False):
44+
fmt = _get_formatter(with_color)
45+
if with_size or with_hash:
46+
colalign = ("right",) if with_size else None
4047
ui.table(
4148
[
42-
_format_entry(entry, fmt, with_size=with_size, with_md5=with_md5)
49+
_format_entry(
50+
entry,
51+
fmt(entry),
52+
with_size=with_size,
53+
with_hash=with_hash,
54+
)
4355
for entry in entries
44-
]
56+
],
57+
colalign=colalign,
4558
)
4659
return
4760

4861
# NOTE: this is faster than ui.table for very large number of entries
4962
ui.write("\n".join(fmt(entry) for entry in entries))
5063

5164

65+
class TreePart:
66+
Edge = "├── "
67+
Line = "│ "
68+
Corner = "└── "
69+
Blank = " "
70+
71+
72+
def _build_tree_structure(
73+
entries, with_color=False, with_size=False, with_hash=False, _depth=0, _prefix=""
74+
):
75+
rows = []
76+
fmt = _get_formatter(with_color)
77+
78+
num_entries = len(entries)
79+
for i, (name, entry) in enumerate(entries.items()):
80+
entry["path"] = name
81+
is_last = i >= num_entries - 1
82+
tree_part = ""
83+
if _depth > 0:
84+
tree_part = TreePart.Corner if is_last else TreePart.Edge
85+
86+
row = _format_entry(
87+
entry,
88+
_prefix + tree_part + fmt(entry),
89+
with_size=with_size,
90+
with_hash=with_hash,
91+
)
92+
rows.append(row)
93+
94+
if contents := entry.get("contents"):
95+
new_prefix = _prefix
96+
if _depth > 0:
97+
new_prefix += TreePart.Blank if is_last else TreePart.Line
98+
new_rows = _build_tree_structure(
99+
contents,
100+
with_color=with_color,
101+
with_size=with_size,
102+
with_hash=with_hash,
103+
_depth=_depth + 1,
104+
_prefix=new_prefix,
105+
)
106+
rows.extend(new_rows)
107+
108+
return rows
109+
110+
111+
def show_tree(entries, with_color=False, with_size=False, with_hash=False):
112+
import tabulate
113+
114+
rows = _build_tree_structure(
115+
entries,
116+
with_color=with_color,
117+
with_size=with_size,
118+
with_hash=with_hash,
119+
)
120+
121+
colalign = ("right",) if with_size else None
122+
123+
_orig = tabulate.PRESERVE_WHITESPACE
124+
tabulate.PRESERVE_WHITESPACE = True
125+
try:
126+
ui.table(rows, colalign=colalign)
127+
finally:
128+
tabulate.PRESERVE_WHITESPACE = _orig
129+
130+
52131
class CmdList(CmdBaseNoRepo):
53-
def run(self):
132+
def _show_tree(self):
133+
from dvc.repo.ls import ls_tree
134+
135+
entries = ls_tree(
136+
self.args.url,
137+
self.args.path,
138+
rev=self.args.rev,
139+
dvc_only=self.args.dvc_only,
140+
config=self.args.config,
141+
remote=self.args.remote,
142+
remote_config=self.args.remote_config,
143+
maxdepth=self.args.level,
144+
)
145+
show_tree(
146+
entries,
147+
with_color=True,
148+
with_size=self.args.size,
149+
with_hash=self.args.show_hash,
150+
)
151+
return 0
152+
153+
def _show_list(self):
54154
from dvc.repo import Repo
55155

56-
try:
57-
entries = Repo.ls(
58-
self.args.url,
59-
self.args.path,
60-
rev=self.args.rev,
61-
recursive=self.args.recursive,
62-
dvc_only=self.args.dvc_only,
63-
config=self.args.config,
64-
remote=self.args.remote,
65-
remote_config=self.args.remote_config,
156+
entries = Repo.ls(
157+
self.args.url,
158+
self.args.path,
159+
rev=self.args.rev,
160+
recursive=self.args.recursive,
161+
dvc_only=self.args.dvc_only,
162+
config=self.args.config,
163+
remote=self.args.remote,
164+
remote_config=self.args.remote_config,
165+
maxdepth=self.args.level,
166+
)
167+
if self.args.json:
168+
ui.write_json(entries)
169+
elif entries:
170+
show_entries(
171+
entries,
172+
with_color=True,
173+
with_size=self.args.size,
174+
with_hash=self.args.show_hash,
66175
)
67-
if self.args.json:
68-
ui.write_json(entries)
69-
elif entries:
70-
show_entries(
71-
entries,
72-
with_color=True,
73-
with_size=self.args.size,
74-
with_md5=self.args.show_hash,
75-
)
76-
return 0
176+
return 0
177+
178+
def run(self):
179+
if self.args.tree and self.args.json:
180+
raise DvcException("Cannot use --tree and --json options together.")
181+
182+
try:
183+
if self.args.tree:
184+
return self._show_tree()
185+
return self._show_list()
77186
except FileNotFoundError:
78187
logger.exception("")
79188
return 1
@@ -102,6 +211,19 @@ def add_parser(subparsers, parent_parser):
102211
action="store_true",
103212
help="Recursively list files.",
104213
)
214+
list_parser.add_argument(
215+
"-T",
216+
"--tree",
217+
action="store_true",
218+
help="Recurse into directories as a tree.",
219+
)
220+
list_parser.add_argument(
221+
"-L",
222+
"--level",
223+
metavar="depth",
224+
type=int,
225+
help="Limit the depth of recursion.",
226+
)
105227
list_parser.add_argument(
106228
"--dvc-only", action="store_true", help="Show only DVC outputs."
107229
)

dvc/commands/ls/ls_colors.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ def format(self, entry):
3232
if entry.get("isexec", False):
3333
return self._format(text, code="ex")
3434

35-
_, ext = os.path.splitext(text)
35+
stem, ext = os.path.splitext(text)
36+
if not ext and stem.startswith("."):
37+
ext = stem
3638
return self._format(text, ext=ext)
3739

3840
def _format(self, text, code=None, ext=None):

0 commit comments

Comments
 (0)