diff --git a/Documentation/git-ls-tree.txt b/Documentation/git-ls-tree.txt index db02d6d79a..68bf1cf325 100644 --- a/Documentation/git-ls-tree.txt +++ b/Documentation/git-ls-tree.txt @@ -10,7 +10,7 @@ SYNOPSIS -------- [verse] 'git ls-tree' [-d] [-r] [-t] [-l] [-z] - [--name-only] [--name-status] [--full-name] [--full-tree] [--abbrev[=]] + [--name-only] [--name-status] [--full-name] [--full-tree] [--abbrev[=]] [--format=] [...] DESCRIPTION @@ -74,6 +74,16 @@ OPTIONS Do not limit the listing to the current working directory. Implies --full-name. +--format=:: + A string that interpolates `%(fieldname)` from the result + being shown. It also interpolates `%%` to `%`, and + `%xx` where `xx` are hex digits interpolates to character + with hex code `xx`; for example `%00` interpolates to + `\0` (NUL), `%09` to `\t` (TAB) and `%0a` to `\n` (LF). + When specified, `--format` cannot be combined with other + format-altering options, including `--long`, `--name-only` + and `--object-only`. + [...]:: When paths are given, show them (note that this isn't really raw pathnames, but rather a list of patterns to match). Otherwise @@ -82,16 +92,29 @@ OPTIONS Output Format ------------- - SP SP TAB + +The output format of `ls-tree` is determined by either the `--format` +option, or other format-altering options such as `--name-only` etc. +(see `--format` above). + +The use of certain `--format` directives is equivalent to using those +options, but invoking the full formatting machinery can be slower than +using an appropriate formatting option. + +In cases where the `--format` would exactly map to an existing option +`ls-tree` will use the appropriate faster path. Thus the default format +is equivalent to: + + %(objectmode) %(objecttype) %(objectname)%x09%(path) This output format is compatible with what `--index-info --stdin` of 'git update-index' expects. When the `-l` option is used, format changes to - SP SP SP TAB + %(objectmode) %(objecttype) %(objectname) %(objectsize:padded)%x09%(path) -Object size identified by is given in bytes, and right-justified +Object size identified by is given in bytes, and right-justified with minimum width of 7 characters. Object size is given only for blobs (file) entries; for other entries `-` character is used in place of size. @@ -100,6 +123,34 @@ quoted as explained for the configuration variable `core.quotePath` (see linkgit:git-config[1]). Using `-z` the filename is output verbatim and the line is terminated by a NUL byte. +Customized format: + +It is possible to print in a custom format by using the `--format` option, +which is able to interpolate different fields using a `%(fieldname)` notation. +For example, if you only care about the "objectname" and "path" fields, you +can execute with a specific "--format" like + + git ls-tree --format='%(objectname) %(path)' + +FIELD NAMES +----------- + +Various values from structured fields can be used to interpolate +into the resulting output. For each outputing line, the following +names can be used: + +objectmode:: + The mode of the object. +objecttype:: + The type of the object (`blob` or `tree`). +objectname:: + The name of the object. +objectsize[:padded]:: + The size of the object ("-" if it's a tree). + It also supports a padded format of size with "%(size:padded)". +path:: + The pathname of the object. + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/ls-tree.c b/builtin/ls-tree.c index edc8bf53a6..8a1bf2fa4d 100644 --- a/builtin/ls-tree.c +++ b/builtin/ls-tree.c @@ -23,6 +23,8 @@ static int ls_options; static struct pathspec pathspec; static int chomp_prefix; static const char *ls_tree_prefix; +static const char *format; + struct show_tree_data { unsigned mode; enum object_type type; @@ -37,10 +39,77 @@ static const char * const ls_tree_usage[] = { }; static enum ls_tree_cmdmode { - MODE_LONG = 1, + MODE_DEFAULT = 0, + MODE_LONG, MODE_NAME_ONLY, } cmdmode; +static void expand_objectsize(struct strbuf *line, const struct object_id *oid, + const enum object_type type, unsigned int padded) +{ + if (type == OBJ_BLOB) { + unsigned long size; + if (oid_object_info(the_repository, oid, &size) < 0) + die(_("could not get object info about '%s'"), + oid_to_hex(oid)); + if (padded) + strbuf_addf(line, "%7"PRIuMAX, (uintmax_t)size); + else + strbuf_addf(line, "%"PRIuMAX, (uintmax_t)size); + } else if (padded) { + strbuf_addf(line, "%7s", "-"); + } else { + strbuf_addstr(line, "-"); + } +} + +static size_t expand_show_tree(struct strbuf *sb, const char *start, + void *context) +{ + struct show_tree_data *data = context; + const char *end; + const char *p; + unsigned int errlen; + size_t len = strbuf_expand_literal_cb(sb, start, NULL); + + if (len) + return len; + if (*start != '(') + die(_("bad ls-tree format: element '%s' does not start with '('"), start); + + end = strchr(start + 1, ')'); + if (!end) + die(_("bad ls-tree format: element '%s' does not end in ')'"), start); + + len = end - start + 1; + if (skip_prefix(start, "(objectmode)", &p)) { + strbuf_addf(sb, "%06o", data->mode); + } else if (skip_prefix(start, "(objecttype)", &p)) { + strbuf_addstr(sb, type_name(data->type)); + } else if (skip_prefix(start, "(objectsize:padded)", &p)) { + expand_objectsize(sb, data->oid, data->type, 1); + } else if (skip_prefix(start, "(objectsize)", &p)) { + expand_objectsize(sb, data->oid, data->type, 0); + } else if (skip_prefix(start, "(objectname)", &p)) { + strbuf_add_unique_abbrev(sb, data->oid, abbrev); + } else if (skip_prefix(start, "(path)", &p)) { + const char *name = data->base->buf; + const char *prefix = chomp_prefix ? ls_tree_prefix : NULL; + struct strbuf quoted = STRBUF_INIT; + struct strbuf sbuf = STRBUF_INIT; + strbuf_addstr(data->base, data->pathname); + name = relative_path(data->base->buf, prefix, &sbuf); + quote_c_style(name, "ed, NULL, 0); + strbuf_addbuf(sb, "ed); + strbuf_release(&sbuf); + strbuf_release("ed); + } else { + errlen = (unsigned long)len; + die(_("bad ls-tree format: %%%.*s"), errlen, start); + } + return len; +} + static int show_recursive(const char *base, size_t baselen, const char *pathname) { int i; @@ -71,6 +140,38 @@ static int show_recursive(const char *base, size_t baselen, const char *pathname return 0; } +static int show_tree_fmt(const struct object_id *oid, struct strbuf *base, + const char *pathname, unsigned mode, void *context) +{ + size_t baselen; + int recurse = 0; + struct strbuf sb = STRBUF_INIT; + enum object_type type = object_type(mode); + + struct show_tree_data data = { + .mode = mode, + .type = type, + .oid = oid, + .pathname = pathname, + .base = base, + }; + + if (type == OBJ_TREE && show_recursive(base->buf, base->len, pathname)) + recurse = READ_TREE_RECURSIVE; + if (type == OBJ_TREE && recurse && !(ls_options & LS_SHOW_TREES)) + return recurse; + if (type == OBJ_BLOB && (ls_options & LS_TREE_ONLY)) + return 0; + + baselen = base->len; + strbuf_expand(&sb, format, expand_show_tree, &data); + strbuf_addch(&sb, line_termination); + fwrite(sb.buf, sb.len, 1, stdout); + strbuf_release(&sb); + strbuf_setlen(base, baselen); + return recurse; +} + static int show_default(struct show_tree_data *data) { size_t baselen = data->base->len; @@ -145,11 +246,33 @@ static int show_tree(const struct object_id *oid, struct strbuf *base, return recurse; } +struct ls_tree_cmdmode_to_fmt { + enum ls_tree_cmdmode mode; + const char *const fmt; +}; + +static struct ls_tree_cmdmode_to_fmt ls_tree_cmdmode_format[] = { + { + .mode = MODE_DEFAULT, + .fmt = "%(objectmode) %(objecttype) %(objectname)%x09%(path)", + }, + { + .mode = MODE_LONG, + .fmt = "%(objectmode) %(objecttype) %(objectname) %(objectsize:padded)%x09%(path)", + }, + { + .mode = MODE_NAME_ONLY, /* And MODE_NAME_STATUS */ + .fmt = "%(path)", + }, + { 0 }, +}; + int cmd_ls_tree(int argc, const char **argv, const char *prefix) { struct object_id oid; struct tree *tree; int i, full_tree = 0; + read_tree_fn_t fn = show_tree; const struct option ls_tree_options[] = { OPT_BIT('d', NULL, &ls_options, N_("only show trees"), LS_TREE_ONLY), @@ -170,6 +293,9 @@ int cmd_ls_tree(int argc, const char **argv, const char *prefix) OPT_BOOL(0, "full-tree", &full_tree, N_("list entire tree; not just current directory " "(implies --full-name)")), + OPT_STRING_F(0, "format", &format, N_("format"), + N_("format to use for the output"), + PARSE_OPT_NONEG), OPT__ABBREV(&abbrev), OPT_END() }; @@ -190,6 +316,10 @@ int cmd_ls_tree(int argc, const char **argv, const char *prefix) ((LS_TREE_ONLY|LS_RECURSIVE) & ls_options)) ls_options |= LS_SHOW_TREES; + if (format && cmdmode) + usage_msg_opt( + _("--format can't be combined with other format-altering options"), + ls_tree_usage, ls_tree_options); if (argc < 1) usage_with_options(ls_tree_usage, ls_tree_options); if (get_oid(argv[0], &oid)) @@ -211,6 +341,23 @@ int cmd_ls_tree(int argc, const char **argv, const char *prefix) tree = parse_tree_indirect(&oid); if (!tree) die("not a tree object"); - return !!read_tree(the_repository, tree, - &pathspec, show_tree, NULL); + /* + * The generic show_tree_fmt() is slower than show_tree(), so + * take the fast path if possible. + */ + if (format) { + struct ls_tree_cmdmode_to_fmt *m2f; + + fn = show_tree_fmt; + for (m2f = ls_tree_cmdmode_format; m2f->fmt; m2f++) { + if (strcmp(format, m2f->fmt)) + continue; + + cmdmode = m2f->mode; + fn = show_tree; + break; + } + } + + return !!read_tree(the_repository, tree, &pathspec, fn, NULL); } diff --git a/t/t3104-ls-tree-format.sh b/t/t3104-ls-tree-format.sh new file mode 100755 index 0000000000..7f1eb699d3 --- /dev/null +++ b/t/t3104-ls-tree-format.sh @@ -0,0 +1,67 @@ +#!/bin/sh + +test_description='ls-tree --format' + +TEST_PASSES_SANITIZE_LEAK=true +. ./test-lib.sh + +test_expect_success 'ls-tree --format usage' ' + test_expect_code 129 git ls-tree --format=fmt -l HEAD && + test_expect_code 129 git ls-tree --format=fmt --name-only HEAD && + test_expect_code 129 git ls-tree --format=fmt --name-status HEAD +' + +test_expect_success 'setup' ' + mkdir dir && + test_commit dir/sub-file && + test_commit top-file +' + +test_ls_tree_format () { + format=$1 && + opts=$2 && + fmtopts=$3 && + shift 2 && + + test_expect_success "ls-tree '--format=<$format>' is like options '$opts $fmtopts'" ' + git ls-tree $opts -r HEAD >expect && + git ls-tree --format="$format" -r $fmtopts HEAD >actual && + test_cmp expect actual + ' + + test_expect_success "ls-tree '--format=<$format>' on optimized v.s. non-optimized path" ' + git ls-tree --format="$format" -r $fmtopts HEAD >expect && + git ls-tree --format="> $format" -r $fmtopts HEAD >actual.raw && + sed "s/^> //" >actual