Skip to content

Commit 455923e

Browse files
avargitster
authored andcommitted
ls-tree: introduce "--format" option
Add a --format option to ls-tree. It has an existing default output, and then --long and --name-only options to emit the default output along with the objectsize and, or to only emit object paths. Rather than add --type-only, --object-only etc. we can just support a --format using a strbuf_expand() similar to "for-each-ref --format". We might still add such options in the future for convenience. The --format implementation is slower than the existing code, but this change does not cause any performance regressions. We'll leave the existing show_tree() unchanged, and only run show_tree_fmt() in if a --format different than the hardcoded built-in ones corresponding to the existing modes is provided. I.e. something like the "--long" output would be much slower with this, mainly due to how we need to allocate various things to do with quote.c instead of spewing the output directly to stdout. The new option of '--format' comes from Ævar Arnfjörð Bjarmasonn's idea and suggestion, this commit makes modifications in terms of the original discussion on community [1]. In [1] there was a "GIT_TEST_LS_TREE_FORMAT_BACKEND" variable to ensure that we had test coverage for passing tests that would otherwise use show_tree() through show_tree_fmt(), and thus that the formatting mechanism could handle all the same cases as the non-formatting options. Somewhere in subsequent re-rolls of that we seem to have drifted away from what the goal of these tests should be. We're trying to ensure correctness of show_tree_fmt(). We can't tell if we "hit [the] fast-path" here, and instead of having an explicit test for that, we can just add it to something our "test_ls_tree_format" tests for. Here is the statistics about performance tests: 1. Default format (hitten the builtin formats): "git ls-tree <tree-ish>" vs "--format='%(mode) %(type) %(object)%x09%(file)'" $hyperfine --warmup=10 "/opt/git/master/bin/git ls-tree -r HEAD" Benchmark 1: /opt/git/master/bin/git ls-tree -r HEAD Time (mean ± σ): 105.2 ms ± 3.3 ms [User: 84.3 ms, System: 20.8 ms] Range (min … max): 99.2 ms … 113.2 ms 28 runs $hyperfine --warmup=10 "/opt/git/ls-tree-oid-only/bin/git ls-tree -r --format='%(mode) %(type) %(object)%x09%(file)' HEAD" Benchmark 1: /opt/git/ls-tree-oid-only/bin/git ls-tree -r --format='%(mode) %(type) %(object)%x09%(file)' HEAD Time (mean ± σ): 106.4 ms ± 2.7 ms [User: 86.1 ms, System: 20.2 ms] Range (min … max): 100.2 ms … 110.5 ms 29 runs 2. Default format includes object size (hitten the builtin formats): "git ls-tree -l <tree-ish>" vs "--format='%(mode) %(type) %(object) %(size:padded)%x09%(file)'" $hyperfine --warmup=10 "/opt/git/master/bin/git ls-tree -r -l HEAD" Benchmark 1: /opt/git/master/bin/git ls-tree -r -l HEAD Time (mean ± σ): 335.1 ms ± 6.5 ms [User: 304.6 ms, System: 30.4 ms] Range (min … max): 327.5 ms … 348.4 ms 10 runs $hyperfine --warmup=10 "/opt/git/ls-tree-oid-only/bin/git ls-tree -r --format='%(mode) %(type) %(object) %(size:padded)%x09%(file)' HEAD" Benchmark 1: /opt/git/ls-tree-oid-only/bin/git ls-tree -r --format='%(mode) %(type) %(object) %(size:padded)%x09%(file)' HEAD Time (mean ± σ): 337.2 ms ± 8.2 ms [User: 309.2 ms, System: 27.9 ms] Range (min … max): 328.8 ms … 349.4 ms 10 runs Links: [1] https://public-inbox.org/git/[email protected]/ [2] https://lore.kernel.org/git/cb717d08be87e3239117c6c667cb32caabaad33d.1646390152.git.dyroneteng@gmail.com/ Signed-off-by: Ævar Arnfjörð Bjarmason <[email protected]> Signed-off-by: Teng Long <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 22184af commit 455923e

File tree

3 files changed

+272
-7
lines changed

3 files changed

+272
-7
lines changed

Documentation/git-ls-tree.txt

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ SYNOPSIS
1010
--------
1111
[verse]
1212
'git ls-tree' [-d] [-r] [-t] [-l] [-z]
13-
[--name-only] [--name-status] [--full-name] [--full-tree] [--abbrev[=<n>]]
13+
[--name-only] [--name-status] [--full-name] [--full-tree] [--abbrev[=<n>]] [--format=<format>]
1414
<tree-ish> [<path>...]
1515

1616
DESCRIPTION
@@ -74,6 +74,16 @@ OPTIONS
7474
Do not limit the listing to the current working directory.
7575
Implies --full-name.
7676

77+
--format=<format>::
78+
A string that interpolates `%(fieldname)` from the result
79+
being shown. It also interpolates `%%` to `%`, and
80+
`%xx` where `xx` are hex digits interpolates to character
81+
with hex code `xx`; for example `%00` interpolates to
82+
`\0` (NUL), `%09` to `\t` (TAB) and `%0a` to `\n` (LF).
83+
When specified, `--format` cannot be combined with other
84+
format-altering options, including `--long`, `--name-only`
85+
and `--object-only`.
86+
7787
[<path>...]::
7888
When paths are given, show them (note that this isn't really raw
7989
pathnames, but rather a list of patterns to match). Otherwise
@@ -82,16 +92,29 @@ OPTIONS
8292

8393
Output Format
8494
-------------
85-
<mode> SP <type> SP <object> TAB <file>
95+
96+
The output format of `ls-tree` is determined by either the `--format`
97+
option, or other format-altering options such as `--name-only` etc.
98+
(see `--format` above).
99+
100+
The use of certain `--format` directives is equivalent to using those
101+
options, but invoking the full formatting machinery can be slower than
102+
using an appropriate formatting option.
103+
104+
In cases where the `--format` would exactly map to an existing option
105+
`ls-tree` will use the appropriate faster path. Thus the default format
106+
is equivalent to:
107+
108+
%(objectmode) %(objecttype) %(objectname)%x09%(path)
86109

87110
This output format is compatible with what `--index-info --stdin` of
88111
'git update-index' expects.
89112

90113
When the `-l` option is used, format changes to
91114

92-
<mode> SP <type> SP <object> SP <object size> TAB <file>
115+
%(objectmode) %(objecttype) %(objectname) %(objectsize:padded)%x09%(path)
93116

94-
Object size identified by <object> is given in bytes, and right-justified
117+
Object size identified by <objectname> is given in bytes, and right-justified
95118
with minimum width of 7 characters. Object size is given only for blobs
96119
(file) entries; for other entries `-` character is used in place of size.
97120

@@ -100,6 +123,34 @@ quoted as explained for the configuration variable `core.quotePath`
100123
(see linkgit:git-config[1]). Using `-z` the filename is output
101124
verbatim and the line is terminated by a NUL byte.
102125

126+
Customized format:
127+
128+
It is possible to print in a custom format by using the `--format` option,
129+
which is able to interpolate different fields using a `%(fieldname)` notation.
130+
For example, if you only care about the "objectname" and "path" fields, you
131+
can execute with a specific "--format" like
132+
133+
git ls-tree --format='%(objectname) %(path)' <tree-ish>
134+
135+
FIELD NAMES
136+
-----------
137+
138+
Various values from structured fields can be used to interpolate
139+
into the resulting output. For each outputing line, the following
140+
names can be used:
141+
142+
objectmode::
143+
The mode of the object.
144+
objecttype::
145+
The type of the object (`blob` or `tree`).
146+
objectname::
147+
The name of the object.
148+
objectsize[:padded]::
149+
The size of the object ("-" if it's a tree).
150+
It also supports a padded format of size with "%(size:padded)".
151+
path::
152+
The pathname of the object.
153+
103154
GIT
104155
---
105156
Part of the linkgit:git[1] suite

builtin/ls-tree.c

Lines changed: 150 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ static int ls_options;
2323
static struct pathspec pathspec;
2424
static int chomp_prefix;
2525
static const char *ls_tree_prefix;
26+
static const char *format;
27+
2628
struct show_tree_data {
2729
unsigned mode;
2830
enum object_type type;
@@ -37,10 +39,77 @@ static const char * const ls_tree_usage[] = {
3739
};
3840

3941
static enum ls_tree_cmdmode {
40-
MODE_LONG = 1,
42+
MODE_DEFAULT = 0,
43+
MODE_LONG,
4144
MODE_NAME_ONLY,
4245
} cmdmode;
4346

47+
static void expand_objectsize(struct strbuf *line, const struct object_id *oid,
48+
const enum object_type type, unsigned int padded)
49+
{
50+
if (type == OBJ_BLOB) {
51+
unsigned long size;
52+
if (oid_object_info(the_repository, oid, &size) < 0)
53+
die(_("could not get object info about '%s'"),
54+
oid_to_hex(oid));
55+
if (padded)
56+
strbuf_addf(line, "%7"PRIuMAX, (uintmax_t)size);
57+
else
58+
strbuf_addf(line, "%"PRIuMAX, (uintmax_t)size);
59+
} else if (padded) {
60+
strbuf_addf(line, "%7s", "-");
61+
} else {
62+
strbuf_addstr(line, "-");
63+
}
64+
}
65+
66+
static size_t expand_show_tree(struct strbuf *sb, const char *start,
67+
void *context)
68+
{
69+
struct show_tree_data *data = context;
70+
const char *end;
71+
const char *p;
72+
unsigned int errlen;
73+
size_t len = strbuf_expand_literal_cb(sb, start, NULL);
74+
75+
if (len)
76+
return len;
77+
if (*start != '(')
78+
die(_("bad ls-tree format: element '%s' does not start with '('"), start);
79+
80+
end = strchr(start + 1, ')');
81+
if (!end)
82+
die(_("bad ls-tree format: element '%s' does not end in ')'"), start);
83+
84+
len = end - start + 1;
85+
if (skip_prefix(start, "(objectmode)", &p)) {
86+
strbuf_addf(sb, "%06o", data->mode);
87+
} else if (skip_prefix(start, "(objecttype)", &p)) {
88+
strbuf_addstr(sb, type_name(data->type));
89+
} else if (skip_prefix(start, "(objectsize:padded)", &p)) {
90+
expand_objectsize(sb, data->oid, data->type, 1);
91+
} else if (skip_prefix(start, "(objectsize)", &p)) {
92+
expand_objectsize(sb, data->oid, data->type, 0);
93+
} else if (skip_prefix(start, "(objectname)", &p)) {
94+
strbuf_add_unique_abbrev(sb, data->oid, abbrev);
95+
} else if (skip_prefix(start, "(path)", &p)) {
96+
const char *name = data->base->buf;
97+
const char *prefix = chomp_prefix ? ls_tree_prefix : NULL;
98+
struct strbuf quoted = STRBUF_INIT;
99+
struct strbuf sbuf = STRBUF_INIT;
100+
strbuf_addstr(data->base, data->pathname);
101+
name = relative_path(data->base->buf, prefix, &sbuf);
102+
quote_c_style(name, &quoted, NULL, 0);
103+
strbuf_addbuf(sb, &quoted);
104+
strbuf_release(&sbuf);
105+
strbuf_release(&quoted);
106+
} else {
107+
errlen = (unsigned long)len;
108+
die(_("bad ls-tree format: %%%.*s"), errlen, start);
109+
}
110+
return len;
111+
}
112+
44113
static int show_recursive(const char *base, size_t baselen, const char *pathname)
45114
{
46115
int i;
@@ -71,6 +140,38 @@ static int show_recursive(const char *base, size_t baselen, const char *pathname
71140
return 0;
72141
}
73142

143+
static int show_tree_fmt(const struct object_id *oid, struct strbuf *base,
144+
const char *pathname, unsigned mode, void *context)
145+
{
146+
size_t baselen;
147+
int recurse = 0;
148+
struct strbuf sb = STRBUF_INIT;
149+
enum object_type type = object_type(mode);
150+
151+
struct show_tree_data data = {
152+
.mode = mode,
153+
.type = type,
154+
.oid = oid,
155+
.pathname = pathname,
156+
.base = base,
157+
};
158+
159+
if (type == OBJ_TREE && show_recursive(base->buf, base->len, pathname))
160+
recurse = READ_TREE_RECURSIVE;
161+
if (type == OBJ_TREE && recurse && !(ls_options & LS_SHOW_TREES))
162+
return recurse;
163+
if (type == OBJ_BLOB && (ls_options & LS_TREE_ONLY))
164+
return 0;
165+
166+
baselen = base->len;
167+
strbuf_expand(&sb, format, expand_show_tree, &data);
168+
strbuf_addch(&sb, line_termination);
169+
fwrite(sb.buf, sb.len, 1, stdout);
170+
strbuf_release(&sb);
171+
strbuf_setlen(base, baselen);
172+
return recurse;
173+
}
174+
74175
static int show_default(struct show_tree_data *data)
75176
{
76177
size_t baselen = data->base->len;
@@ -145,11 +246,33 @@ static int show_tree(const struct object_id *oid, struct strbuf *base,
145246
return recurse;
146247
}
147248

249+
struct ls_tree_cmdmode_to_fmt {
250+
enum ls_tree_cmdmode mode;
251+
const char *const fmt;
252+
};
253+
254+
static struct ls_tree_cmdmode_to_fmt ls_tree_cmdmode_format[] = {
255+
{
256+
.mode = MODE_DEFAULT,
257+
.fmt = "%(objectmode) %(objecttype) %(objectname)%x09%(path)",
258+
},
259+
{
260+
.mode = MODE_LONG,
261+
.fmt = "%(objectmode) %(objecttype) %(objectname) %(objectsize:padded)%x09%(path)",
262+
},
263+
{
264+
.mode = MODE_NAME_ONLY, /* And MODE_NAME_STATUS */
265+
.fmt = "%(path)",
266+
},
267+
{ 0 },
268+
};
269+
148270
int cmd_ls_tree(int argc, const char **argv, const char *prefix)
149271
{
150272
struct object_id oid;
151273
struct tree *tree;
152274
int i, full_tree = 0;
275+
read_tree_fn_t fn = show_tree;
153276
const struct option ls_tree_options[] = {
154277
OPT_BIT('d', NULL, &ls_options, N_("only show trees"),
155278
LS_TREE_ONLY),
@@ -170,6 +293,9 @@ int cmd_ls_tree(int argc, const char **argv, const char *prefix)
170293
OPT_BOOL(0, "full-tree", &full_tree,
171294
N_("list entire tree; not just current directory "
172295
"(implies --full-name)")),
296+
OPT_STRING_F(0, "format", &format, N_("format"),
297+
N_("format to use for the output"),
298+
PARSE_OPT_NONEG),
173299
OPT__ABBREV(&abbrev),
174300
OPT_END()
175301
};
@@ -190,6 +316,10 @@ int cmd_ls_tree(int argc, const char **argv, const char *prefix)
190316
((LS_TREE_ONLY|LS_RECURSIVE) & ls_options))
191317
ls_options |= LS_SHOW_TREES;
192318

319+
if (format && cmdmode)
320+
usage_msg_opt(
321+
_("--format can't be combined with other format-altering options"),
322+
ls_tree_usage, ls_tree_options);
193323
if (argc < 1)
194324
usage_with_options(ls_tree_usage, ls_tree_options);
195325
if (get_oid(argv[0], &oid))
@@ -211,6 +341,23 @@ int cmd_ls_tree(int argc, const char **argv, const char *prefix)
211341
tree = parse_tree_indirect(&oid);
212342
if (!tree)
213343
die("not a tree object");
214-
return !!read_tree(the_repository, tree,
215-
&pathspec, show_tree, NULL);
344+
/*
345+
* The generic show_tree_fmt() is slower than show_tree(), so
346+
* take the fast path if possible.
347+
*/
348+
if (format) {
349+
struct ls_tree_cmdmode_to_fmt *m2f;
350+
351+
fn = show_tree_fmt;
352+
for (m2f = ls_tree_cmdmode_format; m2f->fmt; m2f++) {
353+
if (strcmp(format, m2f->fmt))
354+
continue;
355+
356+
cmdmode = m2f->mode;
357+
fn = show_tree;
358+
break;
359+
}
360+
}
361+
362+
return !!read_tree(the_repository, tree, &pathspec, fn, NULL);
216363
}

t/t3104-ls-tree-format.sh

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/bin/sh
2+
3+
test_description='ls-tree --format'
4+
5+
TEST_PASSES_SANITIZE_LEAK=true
6+
. ./test-lib.sh
7+
8+
test_expect_success 'ls-tree --format usage' '
9+
test_expect_code 129 git ls-tree --format=fmt -l HEAD &&
10+
test_expect_code 129 git ls-tree --format=fmt --name-only HEAD &&
11+
test_expect_code 129 git ls-tree --format=fmt --name-status HEAD
12+
'
13+
14+
test_expect_success 'setup' '
15+
mkdir dir &&
16+
test_commit dir/sub-file &&
17+
test_commit top-file
18+
'
19+
20+
test_ls_tree_format () {
21+
format=$1 &&
22+
opts=$2 &&
23+
fmtopts=$3 &&
24+
shift 2 &&
25+
26+
test_expect_success "ls-tree '--format=<$format>' is like options '$opts $fmtopts'" '
27+
git ls-tree $opts -r HEAD >expect &&
28+
git ls-tree --format="$format" -r $fmtopts HEAD >actual &&
29+
test_cmp expect actual
30+
'
31+
32+
test_expect_success "ls-tree '--format=<$format>' on optimized v.s. non-optimized path" '
33+
git ls-tree --format="$format" -r $fmtopts HEAD >expect &&
34+
git ls-tree --format="> $format" -r $fmtopts HEAD >actual.raw &&
35+
sed "s/^> //" >actual <actual.raw &&
36+
test_cmp expect actual
37+
'
38+
}
39+
40+
test_ls_tree_format \
41+
"%(objectmode) %(objecttype) %(objectname)%x09%(path)" \
42+
""
43+
44+
test_ls_tree_format \
45+
"%(objectmode) %(objecttype) %(objectname) %(objectsize:padded)%x09%(path)" \
46+
"--long"
47+
48+
test_ls_tree_format \
49+
"%(path)" \
50+
"--name-only"
51+
52+
test_ls_tree_format \
53+
"%(objectmode) %(objecttype) %(objectname)%x09%(path)" \
54+
"-t" \
55+
"-t"
56+
57+
test_ls_tree_format \
58+
"%(objectmode) %(objecttype) %(objectname)%x09%(path)" \
59+
"--full-name" \
60+
"--full-name"
61+
62+
test_ls_tree_format \
63+
"%(objectmode) %(objecttype) %(objectname)%x09%(path)" \
64+
"--full-tree" \
65+
"--full-tree"
66+
67+
test_done

0 commit comments

Comments
 (0)