ls-files: optionally recurse into submodules

Allow ls-files to recognize submodules in order to retrieve a list of files from a repository's submodules. This is done by forking off a process to recursively call ls-files on all submodules. Use top-level --super-prefix option to pass a path to the submodule which it can use to prepend to output or pathspec matching logic. Signed-off-by: Brandon Williams <bmwill@google.com> Reviewed-by: Stefan Beller <sbeller@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Brandon Williams committed Oct 7, 2016 at 11:18 UTC e77aa336f116e8ff8a72d034494b3a476b78c3ee
4 files changed +208 -40
Documentation/git-ls-files.txt
+7 -1
@@ -18,7 +18,8 @@ SYNOPSIS
18 [--exclude-per-directory=<file>]
19 [--exclude-standard]
20 [--error-unmatch] [--with-tree=<tree-ish>]
21 - [--full-name] [--abbrev] [--] [<file>...]
21 + [--full-name] [--recurse-submodules]
22 + [--abbrev] [--] [<file>...]
23
24 DESCRIPTION
25 -----------
@@ -137,6 +138,11 @@ a space) at the start of each line:
138 option forces paths to be output relative to the project
139 top directory.
140
141 +--recurse-submodules::
142 + Recursively calls ls-files on each submodule in the repository.
143 + Currently there is only support for the --cached mode without a
144 + pathspec.
145 +
146 --abbrev[=<n>]::
147 Instead of showing the full 40-byte hexadecimal object
148 lines, show only a partial prefix.
builtin/ls-files.c
+100 -38
@@ -14,6 +14,7 @@
14 #include "resolve-undo.h"
15 #include "string-list.h"
16 #include "pathspec.h"
17 +#include "run-command.h"
18
19 static int abbrev;
20 static int show_deleted;
@@ -28,8 +29,10 @@ static int show_valid_bit;
29 static int line_terminator = '\n';
30 static int debug_mode;
31 static int show_eol;
32 +static int recurse_submodules;
33
34 static const char *prefix;
35 +static const char *super_prefix;
36 static int max_prefix_len;
37 static int prefix_len;
38 static struct pathspec pathspec;
@@ -67,12 +70,25 @@ static void write_eolinfo(const struct cache_entry *ce, const char *path)
70
71 static void write_name(const char *name)
72 {
73 + /*
74 + * Prepend the super_prefix to name to construct the full_name to be
75 + * written.
76 + */
77 + struct strbuf full_name = STRBUF_INIT;
78 + if (super_prefix) {
79 + strbuf_addstr(&full_name, super_prefix);
80 + strbuf_addstr(&full_name, name);
81 + name = full_name.buf;
82 + }
83 +
84 /*
85 * With "--full-name", prefix_len=0; this caller needs to pass
86 * an empty string in that case (a NULL is good for "").
87 */
88 write_name_quoted_relative(name, prefix_len ? prefix : NULL,
89 stdout, line_terminator);
90 +
91 + strbuf_release(&full_name);
92 }
93
94 static void show_dir_entry(const char *tag, struct dir_entry *ent)
@@ -152,55 +168,84 @@ static void show_killed_files(struct dir_struct *dir)
168 }
169 }
170
171 +/**
172 + * Recursively call ls-files on a submodule
173 + */
174 +static void show_gitlink(const struct cache_entry *ce)
175 +{
176 + struct child_process cp = CHILD_PROCESS_INIT;
177 + int status;
178 +
179 + argv_array_pushf(&cp.args, "--super-prefix=%s%s/",
180 + super_prefix ? super_prefix : "",
181 + ce->name);
182 + argv_array_push(&cp.args, "ls-files");
183 + argv_array_push(&cp.args, "--recurse-submodules");
184 +
185 + cp.git_cmd = 1;
186 + cp.dir = ce->name;
187 + status = run_command(&cp);
188 + if (status)
189 + exit(status);
190 +}
191 +
192 static void show_ce_entry(const char *tag, const struct cache_entry *ce)
193 {
194 + struct strbuf name = STRBUF_INIT;
195 int len = max_prefix_len;
196 + if (super_prefix)
197 + strbuf_addstr(&name, super_prefix);
198 + strbuf_addstr(&name, ce->name);
199
200 if (len >= ce_namelen(ce))
201 die("git ls-files: internal error - cache entry not superset of prefix");
202
162 - if (!match_pathspec(&pathspec, ce->name, ce_namelen(ce),
163 - len, ps_matched,
164 - S_ISDIR(ce->ce_mode) || S_ISGITLINK(ce->ce_mode)))
165 - return;
203 + if (recurse_submodules && S_ISGITLINK(ce->ce_mode)) {
204 + show_gitlink(ce);
205 + } else if (match_pathspec(&pathspec, name.buf, name.len,
206 + len, ps_matched,
207 + S_ISDIR(ce->ce_mode) ||
208 + S_ISGITLINK(ce->ce_mode))) {
209 + if (tag && *tag && show_valid_bit &&
210 + (ce->ce_flags & CE_VALID)) {
211 + static char alttag[4];
212 + memcpy(alttag, tag, 3);
213 + if (isalpha(tag[0]))
214 + alttag[0] = tolower(tag[0]);
215 + else if (tag[0] == '?')
216 + alttag[0] = '!';
217 + else {
218 + alttag[0] = 'v';
219 + alttag[1] = tag[0];
220 + alttag[2] = ' ';
221 + alttag[3] = 0;
222 + }
223 + tag = alttag;
224 + }
225
167 - if (tag && *tag && show_valid_bit &&
168 - (ce->ce_flags & CE_VALID)) {
169 - static char alttag[4];
170 - memcpy(alttag, tag, 3);
171 - if (isalpha(tag[0]))
172 - alttag[0] = tolower(tag[0]);
173 - else if (tag[0] == '?')
174 - alttag[0] = '!';
175 - else {
176 - alttag[0] = 'v';
177 - alttag[1] = tag[0];
178 - alttag[2] = ' ';
179 - alttag[3] = 0;
226 + if (!show_stage) {
227 + fputs(tag, stdout);
228 + } else {
229 + printf("%s%06o %s %d\t",
230 + tag,
231 + ce->ce_mode,
232 + find_unique_abbrev(ce->sha1,abbrev),
233 + ce_stage(ce));
234 + }
235 + write_eolinfo(ce, ce->name);
236 + write_name(ce->name);
237 + if (debug_mode) {
238 + const struct stat_data *sd = &ce->ce_stat_data;
239 +
240 + printf(" ctime: %d:%d\n", sd->sd_ctime.sec, sd->sd_ctime.nsec);
241 + printf(" mtime: %d:%d\n", sd->sd_mtime.sec, sd->sd_mtime.nsec);
242 + printf(" dev: %d\tino: %d\n", sd->sd_dev, sd->sd_ino);
243 + printf(" uid: %d\tgid: %d\n", sd->sd_uid, sd->sd_gid);
244 + printf(" size: %d\tflags: %x\n", sd->sd_size, ce->ce_flags);
245 }
181 - tag = alttag;
246 }
247
184 - if (!show_stage) {
185 - fputs(tag, stdout);
186 - } else {
187 - printf("%s%06o %s %d\t",
188 - tag,
189 - ce->ce_mode,
190 - find_unique_abbrev(ce->sha1,abbrev),
191 - ce_stage(ce));
192 - }
193 - write_eolinfo(ce, ce->name);
194 - write_name(ce->name);
195 - if (debug_mode) {
196 - const struct stat_data *sd = &ce->ce_stat_data;
197 -
198 - printf(" ctime: %d:%d\n", sd->sd_ctime.sec, sd->sd_ctime.nsec);
199 - printf(" mtime: %d:%d\n", sd->sd_mtime.sec, sd->sd_mtime.nsec);
200 - printf(" dev: %d\tino: %d\n", sd->sd_dev, sd->sd_ino);
201 - printf(" uid: %d\tgid: %d\n", sd->sd_uid, sd->sd_gid);
202 - printf(" size: %d\tflags: %x\n", sd->sd_size, ce->ce_flags);
203 - }
248 + strbuf_release(&name);
249 }
250
251 static void show_ru_info(void)
@@ -468,6 +513,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
513 { OPTION_SET_INT, 0, "full-name", &prefix_len, NULL,
514 N_("make the output relative to the project top directory"),
515 PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL },
516 + OPT_BOOL(0, "recurse-submodules", &recurse_submodules,
517 + N_("recurse through submodules")),
518 OPT_BOOL(0, "error-unmatch", &error_unmatch,
519 N_("if any <file> is not in the index, treat this as an error")),
520 OPT_STRING(0, "with-tree", &with_tree, N_("tree-ish"),
@@ -484,6 +531,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
531 prefix = cmd_prefix;
532 if (prefix)
533 prefix_len = strlen(prefix);
534 + super_prefix = get_super_prefix();
535 git_config(git_default_config, NULL);
536
537 if (read_cache() < 0)
@@ -519,6 +567,20 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
567 if (require_work_tree && !is_inside_work_tree())
568 setup_work_tree();
569
570 + if (recurse_submodules &&
571 + (show_stage || show_deleted || show_others || show_unmerged ||
572 + show_killed || show_modified || show_resolve_undo ||
573 + show_valid_bit || show_tag || show_eol || with_tree ||
574 + (line_terminator == '\0')))
575 + die("ls-files --recurse-submodules unsupported mode");
576 +
577 + if (recurse_submodules && error_unmatch)
578 + die("ls-files --recurse-submodules does not support "
579 + "--error-unmatch");
580 +
581 + if (recurse_submodules && argc)
582 + die("ls-files --recurse-submodules does not support pathspec");
583 +
584 parse_pathspec(&pathspec, 0,
585 PATHSPEC_PREFER_CWD |
586 PATHSPEC_STRIP_SUBMODULE_SLASH_CHEAP,
git.c
+1 -1
@@ -443,7 +443,7 @@ static struct cmd_struct commands[] = {
443 { "init-db", cmd_init_db },
444 { "interpret-trailers", cmd_interpret_trailers, RUN_SETUP_GENTLY },
445 { "log", cmd_log, RUN_SETUP },
446 - { "ls-files", cmd_ls_files, RUN_SETUP },
446 + { "ls-files", cmd_ls_files, RUN_SETUP | SUPPORT_SUPER_PREFIX },
447 { "ls-remote", cmd_ls_remote, RUN_SETUP_GENTLY },
448 { "ls-tree", cmd_ls_tree, RUN_SETUP },
449 { "mailinfo", cmd_mailinfo },
t/t3007-ls-files-recurse-submodules.sh new
+100
@@ -0,0 +1,100 @@
1 +#!/bin/sh
2 +
3 +test_description='Test ls-files recurse-submodules feature
4 +
5 +This test verifies the recurse-submodules feature correctly lists files from
6 +submodules.
7 +'
8 +
9 +. ./test-lib.sh
10 +
11 +test_expect_success 'setup directory structure and submodules' '
12 + echo a >a &&
13 + mkdir b &&
14 + echo b >b/b &&
15 + git add a b &&
16 + git commit -m "add a and b" &&
17 + git init submodule &&
18 + echo c >submodule/c &&
19 + git -C submodule add c &&
20 + git -C submodule commit -m "add c" &&
21 + git submodule add ./submodule &&
22 + git commit -m "added submodule"
23 +'
24 +
25 +test_expect_success 'ls-files correctly outputs files in submodule' '
26 + cat >expect <<-\EOF &&
27 + .gitmodules
28 + a
29 + b/b
30 + submodule/c
31 + EOF
32 +
33 + git ls-files --recurse-submodules >actual &&
34 + test_cmp expect actual
35 +'
36 +
37 +test_expect_success 'ls-files does not output files not added to a repo' '
38 + cat >expect <<-\EOF &&
39 + .gitmodules
40 + a
41 + b/b
42 + submodule/c
43 + EOF
44 +
45 + echo a >not_added &&
46 + echo b >b/not_added &&
47 + echo c >submodule/not_added &&
48 + git ls-files --recurse-submodules >actual &&
49 + test_cmp expect actual
50 +'
51 +
52 +test_expect_success 'ls-files recurses more than 1 level' '
53 + cat >expect <<-\EOF &&
54 + .gitmodules
55 + a
56 + b/b
57 + submodule/.gitmodules
58 + submodule/c
59 + submodule/subsub/d
60 + EOF
61 +
62 + git init submodule/subsub &&
63 + echo d >submodule/subsub/d &&
64 + git -C submodule/subsub add d &&
65 + git -C submodule/subsub commit -m "add d" &&
66 + git -C submodule submodule add ./subsub &&
67 + git -C submodule commit -m "added subsub" &&
68 + git ls-files --recurse-submodules >actual &&
69 + test_cmp expect actual
70 +'
71 +
72 +test_expect_success '--recurse-submodules does not support using path arguments' '
73 + test_must_fail git ls-files --recurse-submodules b 2>actual &&
74 + test_i18ngrep "does not support pathspec" actual
75 +'
76 +
77 +test_expect_success '--recurse-submodules does not support --error-unmatch' '
78 + test_must_fail git ls-files --recurse-submodules --error-unmatch 2>actual &&
79 + test_i18ngrep "does not support --error-unmatch" actual
80 +'
81 +
82 +test_incompatible_with_recurse_submodules () {
83 + test_expect_success "--recurse-submodules and $1 are incompatible" "
84 + test_must_fail git ls-files --recurse-submodules $1 2>actual &&
85 + test_i18ngrep 'unsupported mode' actual
86 + "
87 +}
88 +
89 +test_incompatible_with_recurse_submodules -z
90 +test_incompatible_with_recurse_submodules -v
91 +test_incompatible_with_recurse_submodules -t
92 +test_incompatible_with_recurse_submodules --deleted
93 +test_incompatible_with_recurse_submodules --modified
94 +test_incompatible_with_recurse_submodules --others
95 +test_incompatible_with_recurse_submodules --stage
96 +test_incompatible_with_recurse_submodules --killed
97 +test_incompatible_with_recurse_submodules --unmerged
98 +test_incompatible_with_recurse_submodules --eol
99 +
100 +test_done