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