ls-files: introduce "--format" option

Add a new option "--format" that outputs index entries
informations in a custom format, taking inspiration
from the option with the same name in the `git ls-tree`
command.

"--format" cannot used with "-s", "-o", "-k", "-t",
" --resolve-undo","--deduplicate" and "--eol".

Signed-off-by: ZheNing Hu <adlternative@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
ZheNing Hu 2022-07-23 06:44:45 +00:00 committed by Junio C Hamano
parent 8168d5e9c2
commit ce74de931d
3 changed files with 228 additions and 1 deletions

View File

@ -20,7 +20,7 @@ SYNOPSIS
[--exclude-standard]
[--error-unmatch] [--with-tree=<tree-ish>]
[--full-name] [--recurse-submodules]
[--abbrev[=<n>]] [--] [<file>...]
[--abbrev[=<n>]] [--format=<format>] [--] [<file>...]
DESCRIPTION
-----------
@ -192,6 +192,13 @@ followed by the ("attr/<eolattr>").
to the contained files. Sparse directories will be shown with a
trailing slash, such as "x/" for a sparse directory "x".
--format=<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).
--format cannot be combined with `-s`, `-o`, `-k`, `-t`, `--resolve-undo`
and `--eol`.
\--::
Do not interpret any more arguments as options.
@ -223,6 +230,36 @@ 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.
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-files --format='%(objectname) %(path)'
FIELD NAMES
-----------
The way each path is shown can be customized by using the
`--format=<format>` option, where the %(fieldname) in the
<format> string for various aspects of the index entry are
interpolated. The following "fieldname" are understood:
objectmode::
The mode of the file which is recorded in the index.
objectname::
The name of the file which is recorded in the index.
stage::
The stage of the file which is recorded in the index.
eolinfo:index::
eolinfo:worktree::
The <eolinfo> (see the description of the `--eol` option) of
the contents in the index or in the worktree for the path.
eolattr::
The <eolattr> (see the description of the `--eol` option)
that applies to the path.
path::
The pathname of the file which is recorded in the index.
EXCLUDE PATTERNS
----------------

View File

@ -11,6 +11,7 @@
#include "quote.h"
#include "dir.h"
#include "builtin.h"
#include "strbuf.h"
#include "tree.h"
#include "cache-tree.h"
#include "parse-options.h"
@ -48,6 +49,7 @@ static char *ps_matched;
static const char *with_tree;
static int exc_given;
static int exclude_args;
static const char *format;
static const char *tag_cached = "";
static const char *tag_unmerged = "";
@ -85,6 +87,16 @@ static void write_name(const char *name)
stdout, line_terminator);
}
static void write_name_to_buf(struct strbuf *sb, const char *name)
{
const char *rel = relative_path(name, prefix_len ? prefix : NULL, sb);
if (line_terminator)
quote_c_style(rel, sb, NULL, 0);
else
strbuf_addstr(sb, rel);
}
static const char *get_tag(const struct cache_entry *ce, const char *tag)
{
static char alttag[4];
@ -222,6 +234,73 @@ static void show_submodule(struct repository *superproject,
repo_clear(&subrepo);
}
struct show_index_data {
const char *pathname;
struct index_state *istate;
const struct cache_entry *ce;
};
static size_t expand_show_index(struct strbuf *sb, const char *start,
void *context)
{
struct show_index_data *data = context;
const char *end;
const char *p;
size_t len = strbuf_expand_literal_cb(sb, start, NULL);
struct stat st;
if (len)
return len;
if (*start != '(')
die(_("bad ls-files format: element '%s' "
"does not start with '('"), start);
end = strchr(start + 1, ')');
if (!end)
die(_("bad ls-files format: element '%s'"
"does not end in ')'"), start);
len = end - start + 1;
if (skip_prefix(start, "(objectmode)", &p))
strbuf_addf(sb, "%06o", data->ce->ce_mode);
else if (skip_prefix(start, "(objectname)", &p))
strbuf_add_unique_abbrev(sb, &data->ce->oid, abbrev);
else if (skip_prefix(start, "(stage)", &p))
strbuf_addf(sb, "%d", ce_stage(data->ce));
else if (skip_prefix(start, "(eolinfo:index)", &p))
strbuf_addstr(sb, S_ISREG(data->ce->ce_mode) ?
get_cached_convert_stats_ascii(data->istate,
data->ce->name) : "");
else if (skip_prefix(start, "(eolinfo:worktree)", &p))
strbuf_addstr(sb, !lstat(data->pathname, &st) &&
S_ISREG(st.st_mode) ?
get_wt_convert_stats_ascii(data->pathname) : "");
else if (skip_prefix(start, "(eolattr)", &p))
strbuf_addstr(sb, get_convert_attr_ascii(data->istate,
data->pathname));
else if (skip_prefix(start, "(path)", &p))
write_name_to_buf(sb, data->pathname);
else
die(_("bad ls-files format: %%%.*s"), (int)len, start);
return len;
}
static void show_ce_fmt(struct repository *repo, const struct cache_entry *ce,
const char *format, const char *fullname) {
struct show_index_data data = {
.pathname = fullname,
.istate = repo->index,
.ce = ce,
};
struct strbuf sb = STRBUF_INIT;
strbuf_expand(&sb, format, expand_show_index, &data);
strbuf_addch(&sb, line_terminator);
fwrite(sb.buf, sb.len, 1, stdout);
strbuf_release(&sb);
}
static void show_ce(struct repository *repo, struct dir_struct *dir,
const struct cache_entry *ce, const char *fullname,
const char *tag)
@ -236,6 +315,12 @@ static void show_ce(struct repository *repo, struct dir_struct *dir,
max_prefix_len, ps_matched,
S_ISDIR(ce->ce_mode) ||
S_ISGITLINK(ce->ce_mode))) {
if (format) {
show_ce_fmt(repo, ce, format, fullname);
print_debug(ce);
return;
}
tag = get_tag(ce, tag);
if (!show_stage) {
@ -675,6 +760,9 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
N_("suppress duplicate entries")),
OPT_BOOL(0, "sparse", &show_sparse_dirs,
N_("show sparse directories in the presence of a sparse index")),
OPT_STRING_F(0, "format", &format, N_("format"),
N_("format to use for the output"),
PARSE_OPT_NONEG),
OPT_END()
};
int ret = 0;
@ -699,6 +787,13 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
for (i = 0; i < exclude_list.nr; i++) {
add_pattern(exclude_list.items[i].string, "", 0, pl, --exclude_args);
}
if (format && (show_stage || show_others || show_killed ||
show_resolve_undo || skipping_duplicates || show_eol || show_tag))
usage_msg_opt(_("--format cannot be used with -s, -o, -k, -t, "
"--resolve-undo, --deduplicate, --eol"),
ls_files_usage, builtin_ls_files_options);
if (show_tag || show_valid_bit || show_fsmonitor_bit) {
tag_cached = "H ";
tag_unmerged = "M ";

95
t/t3013-ls-files-format.sh Executable file
View File

@ -0,0 +1,95 @@
#!/bin/sh
test_description='git ls-files --format test'
TEST_PASSES_SANITIZE_LEAK=true
. ./test-lib.sh
for flag in -s -o -k -t --resolve-undo --deduplicate --eol
do
test_expect_success "usage: --format is incompatible with $flag" '
test_expect_code 129 git ls-files --format="%(objectname)" $flag
'
done
test_expect_success 'setup' '
printf "LINEONE\nLINETWO\nLINETHREE\n" >o1.txt &&
printf "LINEONE\r\nLINETWO\r\nLINETHREE\r\n" >o2.txt &&
printf "LINEONE\r\nLINETWO\nLINETHREE\n" >o3.txt &&
git add o?.txt &&
oid=$(git hash-object o1.txt) &&
git update-index --add --cacheinfo 120000 $oid o4.txt &&
git update-index --add --cacheinfo 160000 $oid o5.txt &&
git update-index --add --cacheinfo 100755 $oid o6.txt &&
git commit -m base
'
test_expect_success 'git ls-files --format objectmode v.s. -s' '
git ls-files -s >files &&
cut -d" " -f1 files >expect &&
git ls-files --format="%(objectmode)" >actual &&
test_cmp expect actual
'
test_expect_success 'git ls-files --format objectname v.s. -s' '
git ls-files -s >files &&
cut -d" " -f2 files >expect &&
git ls-files --format="%(objectname)" >actual &&
test_cmp expect actual
'
test_expect_success 'git ls-files --format v.s. --eol' '
git ls-files --eol >tmp &&
sed -e "s/ / /g" -e "s/ */ /g" tmp >expect 2>err &&
test_must_be_empty err &&
git ls-files --format="i/%(eolinfo:index) w/%(eolinfo:worktree) attr/%(eolattr) %(path)" >actual 2>err &&
test_must_be_empty err &&
test_cmp expect actual
'
test_expect_success 'git ls-files --format path v.s. -s' '
git ls-files -s >files &&
cut -f2 files >expect &&
git ls-files --format="%(path)" >actual &&
test_cmp expect actual
'
test_expect_success 'git ls-files --format with -m' '
echo change >o1.txt &&
cat >expect <<-\EOF &&
o1.txt
o4.txt
o5.txt
o6.txt
EOF
git ls-files --format="%(path)" -m >actual &&
test_cmp expect actual
'
test_expect_success 'git ls-files --format with -d' '
echo o7 >o7.txt &&
git add o7.txt &&
rm o7.txt &&
cat >expect <<-\EOF &&
o4.txt
o5.txt
o6.txt
o7.txt
EOF
git ls-files --format="%(path)" -d >actual &&
test_cmp expect actual
'
test_expect_success 'git ls-files --format v.s -s' '
git ls-files --stage >expect &&
git ls-files --format="%(objectmode) %(objectname) %(stage)%x09%(path)" >actual &&
test_cmp expect actual
'
test_expect_success 'git ls-files --format with --debug' '
git ls-files --debug >expect &&
git ls-files --format="%(path)" --debug >actual &&
test_cmp expect actual
'
test_done