Skip to content

Commit e77aa33

Browse files
bmwillgitster
authored andcommitted
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 <[email protected]> Reviewed-by: Stefan Beller <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 74866d7 commit e77aa33

File tree

4 files changed

+208
-40
lines changed

4 files changed

+208
-40
lines changed

Documentation/git-ls-files.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ SYNOPSIS
1818
[--exclude-per-directory=<file>]
1919
[--exclude-standard]
2020
[--error-unmatch] [--with-tree=<tree-ish>]
21-
[--full-name] [--abbrev] [--] [<file>...]
21+
[--full-name] [--recurse-submodules]
22+
[--abbrev] [--] [<file>...]
2223

2324
DESCRIPTION
2425
-----------
@@ -137,6 +138,11 @@ a space) at the start of each line:
137138
option forces paths to be output relative to the project
138139
top directory.
139140

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+
140146
--abbrev[=<n>]::
141147
Instead of showing the full 40-byte hexadecimal object
142148
lines, show only a partial prefix.

builtin/ls-files.c

Lines changed: 100 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "resolve-undo.h"
1515
#include "string-list.h"
1616
#include "pathspec.h"
17+
#include "run-command.h"
1718

1819
static int abbrev;
1920
static int show_deleted;
@@ -28,8 +29,10 @@ static int show_valid_bit;
2829
static int line_terminator = '\n';
2930
static int debug_mode;
3031
static int show_eol;
32+
static int recurse_submodules;
3133

3234
static const char *prefix;
35+
static const char *super_prefix;
3336
static int max_prefix_len;
3437
static int prefix_len;
3538
static struct pathspec pathspec;
@@ -67,12 +70,25 @@ static void write_eolinfo(const struct cache_entry *ce, const char *path)
6770

6871
static void write_name(const char *name)
6972
{
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+
7084
/*
7185
* With "--full-name", prefix_len=0; this caller needs to pass
7286
* an empty string in that case (a NULL is good for "").
7387
*/
7488
write_name_quoted_relative(name, prefix_len ? prefix : NULL,
7589
stdout, line_terminator);
90+
91+
strbuf_release(&full_name);
7692
}
7793

7894
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)
152168
}
153169
}
154170

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+
155192
static void show_ce_entry(const char *tag, const struct cache_entry *ce)
156193
{
194+
struct strbuf name = STRBUF_INIT;
157195
int len = max_prefix_len;
196+
if (super_prefix)
197+
strbuf_addstr(&name, super_prefix);
198+
strbuf_addstr(&name, ce->name);
158199

159200
if (len >= ce_namelen(ce))
160201
die("git ls-files: internal error - cache entry not superset of prefix");
161202

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+
}
166225

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);
180245
}
181-
tag = alttag;
182246
}
183247

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);
204249
}
205250

206251
static void show_ru_info(void)
@@ -468,6 +513,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
468513
{ OPTION_SET_INT, 0, "full-name", &prefix_len, NULL,
469514
N_("make the output relative to the project top directory"),
470515
PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL },
516+
OPT_BOOL(0, "recurse-submodules", &recurse_submodules,
517+
N_("recurse through submodules")),
471518
OPT_BOOL(0, "error-unmatch", &error_unmatch,
472519
N_("if any <file> is not in the index, treat this as an error")),
473520
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)
484531
prefix = cmd_prefix;
485532
if (prefix)
486533
prefix_len = strlen(prefix);
534+
super_prefix = get_super_prefix();
487535
git_config(git_default_config, NULL);
488536

489537
if (read_cache() < 0)
@@ -519,6 +567,20 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
519567
if (require_work_tree && !is_inside_work_tree())
520568
setup_work_tree();
521569

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+
522584
parse_pathspec(&pathspec, 0,
523585
PATHSPEC_PREFER_CWD |
524586
PATHSPEC_STRIP_SUBMODULE_SLASH_CHEAP,

git.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ static struct cmd_struct commands[] = {
443443
{ "init-db", cmd_init_db },
444444
{ "interpret-trailers", cmd_interpret_trailers, RUN_SETUP_GENTLY },
445445
{ "log", cmd_log, RUN_SETUP },
446-
{ "ls-files", cmd_ls_files, RUN_SETUP },
446+
{ "ls-files", cmd_ls_files, RUN_SETUP | SUPPORT_SUPER_PREFIX },
447447
{ "ls-remote", cmd_ls_remote, RUN_SETUP_GENTLY },
448448
{ "ls-tree", cmd_ls_tree, RUN_SETUP },
449449
{ "mailinfo", cmd_mailinfo },
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)