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>

ZheNing Hu committed Jul 23, 2022 at 06:44 UTC ce74de931d7aea9746e37632534eacc63b0c1a90
3 files changed +228 -1
Documentation/git-ls-files.txt
+38 -1
@@ -20,7 +20,7 @@ SYNOPSIS
20 [--exclude-standard]
21 [--error-unmatch] [--with-tree=<tree-ish>]
22 [--full-name] [--recurse-submodules]
23 - [--abbrev[=<n>]] [--] [<file>...]
23 + [--abbrev[=<n>]] [--format=<format>] [--] [<file>...]
24
25 DESCRIPTION
26 -----------
@@ -192,6 +192,13 @@ followed by the ("attr/<eolattr>").
192 to the contained files. Sparse directories will be shown with a
193 trailing slash, such as "x/" for a sparse directory "x".
194
195 +--format=<format>::
196 + A string that interpolates `%(fieldname)` from the result being shown.
197 + It also interpolates `%%` to `%`, and `%xx` where `xx` are hex digits
198 + interpolates to character with hex code `xx`; for example `%00`
199 + interpolates to `\0` (NUL), `%09` to `\t` (TAB) and %0a to `\n` (LF).
200 + --format cannot be combined with `-s`, `-o`, `-k`, `-t`, `--resolve-undo`
201 + and `--eol`.
202 \--::
203 Do not interpret any more arguments as options.
204
@@ -223,6 +230,36 @@ quoted as explained for the configuration variable `core.quotePath`
230 (see linkgit:git-config[1]). Using `-z` the filename is output
231 verbatim and the line is terminated by a NUL byte.
232
233 +It is possible to print in a custom format by using the `--format`
234 +option, which is able to interpolate different fields using
235 +a `%(fieldname)` notation. For example, if you only care about the
236 +"objectname" and "path" fields, you can execute with a specific
237 +"--format" like
238 +
239 + git ls-files --format='%(objectname) %(path)'
240 +
241 +FIELD NAMES
242 +-----------
243 +The way each path is shown can be customized by using the
244 +`--format=<format>` option, where the %(fieldname) in the
245 +<format> string for various aspects of the index entry are
246 +interpolated. The following "fieldname" are understood:
247 +
248 +objectmode::
249 + The mode of the file which is recorded in the index.
250 +objectname::
251 + The name of the file which is recorded in the index.
252 +stage::
253 + The stage of the file which is recorded in the index.
254 +eolinfo:index::
255 +eolinfo:worktree::
256 + The <eolinfo> (see the description of the `--eol` option) of
257 + the contents in the index or in the worktree for the path.
258 +eolattr::
259 + The <eolattr> (see the description of the `--eol` option)
260 + that applies to the path.
261 +path::
262 + The pathname of the file which is recorded in the index.
263
264 EXCLUDE PATTERNS
265 ----------------
builtin/ls-files.c
+95
@@ -11,6 +11,7 @@
11 #include "quote.h"
12 #include "dir.h"
13 #include "builtin.h"
14 +#include "strbuf.h"
15 #include "tree.h"
16 #include "cache-tree.h"
17 #include "parse-options.h"
@@ -48,6 +49,7 @@ static char *ps_matched;
49 static const char *with_tree;
50 static int exc_given;
51 static int exclude_args;
52 +static const char *format;
53
54 static const char *tag_cached = "";
55 static const char *tag_unmerged = "";
@@ -85,6 +87,16 @@ static void write_name(const char *name)
87 stdout, line_terminator);
88 }
89
90 +static void write_name_to_buf(struct strbuf *sb, const char *name)
91 +{
92 + const char *rel = relative_path(name, prefix_len ? prefix : NULL, sb);
93 +
94 + if (line_terminator)
95 + quote_c_style(rel, sb, NULL, 0);
96 + else
97 + strbuf_addstr(sb, rel);
98 +}
99 +
100 static const char *get_tag(const struct cache_entry *ce, const char *tag)
101 {
102 static char alttag[4];
@@ -222,6 +234,73 @@ static void show_submodule(struct repository *superproject,
234 repo_clear(&subrepo);
235 }
236
237 +struct show_index_data {
238 + const char *pathname;
239 + struct index_state *istate;
240 + const struct cache_entry *ce;
241 +};
242 +
243 +static size_t expand_show_index(struct strbuf *sb, const char *start,
244 + void *context)
245 +{
246 + struct show_index_data *data = context;
247 + const char *end;
248 + const char *p;
249 + size_t len = strbuf_expand_literal_cb(sb, start, NULL);
250 + struct stat st;
251 +
252 + if (len)
253 + return len;
254 + if (*start != '(')
255 + die(_("bad ls-files format: element '%s' "
256 + "does not start with '('"), start);
257 +
258 + end = strchr(start + 1, ')');
259 + if (!end)
260 + die(_("bad ls-files format: element '%s'"
261 + "does not end in ')'"), start);
262 +
263 + len = end - start + 1;
264 + if (skip_prefix(start, "(objectmode)", &p))
265 + strbuf_addf(sb, "%06o", data->ce->ce_mode);
266 + else if (skip_prefix(start, "(objectname)", &p))
267 + strbuf_add_unique_abbrev(sb, &data->ce->oid, abbrev);
268 + else if (skip_prefix(start, "(stage)", &p))
269 + strbuf_addf(sb, "%d", ce_stage(data->ce));
270 + else if (skip_prefix(start, "(eolinfo:index)", &p))
271 + strbuf_addstr(sb, S_ISREG(data->ce->ce_mode) ?
272 + get_cached_convert_stats_ascii(data->istate,
273 + data->ce->name) : "");
274 + else if (skip_prefix(start, "(eolinfo:worktree)", &p))
275 + strbuf_addstr(sb, !lstat(data->pathname, &st) &&
276 + S_ISREG(st.st_mode) ?
277 + get_wt_convert_stats_ascii(data->pathname) : "");
278 + else if (skip_prefix(start, "(eolattr)", &p))
279 + strbuf_addstr(sb, get_convert_attr_ascii(data->istate,
280 + data->pathname));
281 + else if (skip_prefix(start, "(path)", &p))
282 + write_name_to_buf(sb, data->pathname);
283 + else
284 + die(_("bad ls-files format: %%%.*s"), (int)len, start);
285 +
286 + return len;
287 +}
288 +
289 +static void show_ce_fmt(struct repository *repo, const struct cache_entry *ce,
290 + const char *format, const char *fullname) {
291 + struct show_index_data data = {
292 + .pathname = fullname,
293 + .istate = repo->index,
294 + .ce = ce,
295 + };
296 + struct strbuf sb = STRBUF_INIT;
297 +
298 + strbuf_expand(&sb, format, expand_show_index, &data);
299 + strbuf_addch(&sb, line_terminator);
300 + fwrite(sb.buf, sb.len, 1, stdout);
301 + strbuf_release(&sb);
302 +}
303 +
304 static void show_ce(struct repository *repo, struct dir_struct *dir,
305 const struct cache_entry *ce, const char *fullname,
306 const char *tag)
@@ -236,6 +315,12 @@ static void show_ce(struct repository *repo, struct dir_struct *dir,
315 max_prefix_len, ps_matched,
316 S_ISDIR(ce->ce_mode) ||
317 S_ISGITLINK(ce->ce_mode))) {
318 + if (format) {
319 + show_ce_fmt(repo, ce, format, fullname);
320 + print_debug(ce);
321 + return;
322 + }
323 +
324 tag = get_tag(ce, tag);
325
326 if (!show_stage) {
@@ -675,6 +760,9 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
760 N_("suppress duplicate entries")),
761 OPT_BOOL(0, "sparse", &show_sparse_dirs,
762 N_("show sparse directories in the presence of a sparse index")),
763 + OPT_STRING_F(0, "format", &format, N_("format"),
764 + N_("format to use for the output"),
765 + PARSE_OPT_NONEG),
766 OPT_END()
767 };
768 int ret = 0;
@@ -699,6 +787,13 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
787 for (i = 0; i < exclude_list.nr; i++) {
788 add_pattern(exclude_list.items[i].string, "", 0, pl, --exclude_args);
789 }
790 +
791 + if (format && (show_stage || show_others || show_killed ||
792 + show_resolve_undo || skipping_duplicates || show_eol || show_tag))
793 + usage_msg_opt(_("--format cannot be used with -s, -o, -k, -t, "
794 + "--resolve-undo, --deduplicate, --eol"),
795 + ls_files_usage, builtin_ls_files_options);
796 +
797 if (show_tag || show_valid_bit || show_fsmonitor_bit) {
798 tag_cached = "H ";
799 tag_unmerged = "M ";
t/t3013-ls-files-format.sh new
+95
@@ -0,0 +1,95 @@
1 +#!/bin/sh
2 +
3 +test_description='git ls-files --format test'
4 +
5 +TEST_PASSES_SANITIZE_LEAK=true
6 +. ./test-lib.sh
7 +
8 +for flag in -s -o -k -t --resolve-undo --deduplicate --eol
9 +do
10 + test_expect_success "usage: --format is incompatible with $flag" '
11 + test_expect_code 129 git ls-files --format="%(objectname)" $flag
12 + '
13 +done
14 +
15 +test_expect_success 'setup' '
16 + printf "LINEONE\nLINETWO\nLINETHREE\n" >o1.txt &&
17 + printf "LINEONE\r\nLINETWO\r\nLINETHREE\r\n" >o2.txt &&
18 + printf "LINEONE\r\nLINETWO\nLINETHREE\n" >o3.txt &&
19 + git add o?.txt &&
20 + oid=$(git hash-object o1.txt) &&
21 + git update-index --add --cacheinfo 120000 $oid o4.txt &&
22 + git update-index --add --cacheinfo 160000 $oid o5.txt &&
23 + git update-index --add --cacheinfo 100755 $oid o6.txt &&
24 + git commit -m base
25 +'
26 +
27 +test_expect_success 'git ls-files --format objectmode v.s. -s' '
28 + git ls-files -s >files &&
29 + cut -d" " -f1 files >expect &&
30 + git ls-files --format="%(objectmode)" >actual &&
31 + test_cmp expect actual
32 +'
33 +
34 +test_expect_success 'git ls-files --format objectname v.s. -s' '
35 + git ls-files -s >files &&
36 + cut -d" " -f2 files >expect &&
37 + git ls-files --format="%(objectname)" >actual &&
38 + test_cmp expect actual
39 +'
40 +
41 +test_expect_success 'git ls-files --format v.s. --eol' '
42 + git ls-files --eol >tmp &&
43 + sed -e "s/ / /g" -e "s/ */ /g" tmp >expect 2>err &&
44 + test_must_be_empty err &&
45 + git ls-files --format="i/%(eolinfo:index) w/%(eolinfo:worktree) attr/%(eolattr) %(path)" >actual 2>err &&
46 + test_must_be_empty err &&
47 + test_cmp expect actual
48 +'
49 +
50 +test_expect_success 'git ls-files --format path v.s. -s' '
51 + git ls-files -s >files &&
52 + cut -f2 files >expect &&
53 + git ls-files --format="%(path)" >actual &&
54 + test_cmp expect actual
55 +'
56 +
57 +test_expect_success 'git ls-files --format with -m' '
58 + echo change >o1.txt &&
59 + cat >expect <<-\EOF &&
60 + o1.txt
61 + o4.txt
62 + o5.txt
63 + o6.txt
64 + EOF
65 + git ls-files --format="%(path)" -m >actual &&
66 + test_cmp expect actual
67 +'
68 +
69 +test_expect_success 'git ls-files --format with -d' '
70 + echo o7 >o7.txt &&
71 + git add o7.txt &&
72 + rm o7.txt &&
73 + cat >expect <<-\EOF &&
74 + o4.txt
75 + o5.txt
76 + o6.txt
77 + o7.txt
78 + EOF
79 + git ls-files --format="%(path)" -d >actual &&
80 + test_cmp expect actual
81 +'
82 +
83 +test_expect_success 'git ls-files --format v.s -s' '
84 + git ls-files --stage >expect &&
85 + git ls-files --format="%(objectmode) %(objectname) %(stage)%x09%(path)" >actual &&
86 + test_cmp expect actual
87 +'
88 +
89 +test_expect_success 'git ls-files --format with --debug' '
90 + git ls-files --debug >expect &&
91 + git ls-files --format="%(path)" --debug >actual &&
92 + test_cmp expect actual
93 +'
94 +
95 +test_done