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>
This commit is contained in:
Brandon Williams 2016-10-07 11:18:49 -07:00 committed by Junio C Hamano
parent 74866d7579
commit e77aa336f1
4 changed files with 209 additions and 41 deletions

View File

@ -18,7 +18,8 @@ SYNOPSIS
[--exclude-per-directory=<file>] [--exclude-per-directory=<file>]
[--exclude-standard] [--exclude-standard]
[--error-unmatch] [--with-tree=<tree-ish>] [--error-unmatch] [--with-tree=<tree-ish>]
[--full-name] [--abbrev] [--] [<file>...] [--full-name] [--recurse-submodules]
[--abbrev] [--] [<file>...]
DESCRIPTION DESCRIPTION
----------- -----------
@ -137,6 +138,11 @@ a space) at the start of each line:
option forces paths to be output relative to the project option forces paths to be output relative to the project
top directory. top directory.
--recurse-submodules::
Recursively calls ls-files on each submodule in the repository.
Currently there is only support for the --cached mode without a
pathspec.
--abbrev[=<n>]:: --abbrev[=<n>]::
Instead of showing the full 40-byte hexadecimal object Instead of showing the full 40-byte hexadecimal object
lines, show only a partial prefix. lines, show only a partial prefix.

View File

@ -14,6 +14,7 @@
#include "resolve-undo.h" #include "resolve-undo.h"
#include "string-list.h" #include "string-list.h"
#include "pathspec.h" #include "pathspec.h"
#include "run-command.h"
static int abbrev; static int abbrev;
static int show_deleted; static int show_deleted;
@ -28,8 +29,10 @@ static int show_valid_bit;
static int line_terminator = '\n'; static int line_terminator = '\n';
static int debug_mode; static int debug_mode;
static int show_eol; static int show_eol;
static int recurse_submodules;
static const char *prefix; static const char *prefix;
static const char *super_prefix;
static int max_prefix_len; static int max_prefix_len;
static int prefix_len; static int prefix_len;
static struct pathspec pathspec; static struct pathspec pathspec;
@ -67,12 +70,25 @@ static void write_eolinfo(const struct cache_entry *ce, const char *path)
static void write_name(const char *name) static void write_name(const char *name)
{ {
/*
* Prepend the super_prefix to name to construct the full_name to be
* written.
*/
struct strbuf full_name = STRBUF_INIT;
if (super_prefix) {
strbuf_addstr(&full_name, super_prefix);
strbuf_addstr(&full_name, name);
name = full_name.buf;
}
/* /*
* With "--full-name", prefix_len=0; this caller needs to pass * With "--full-name", prefix_len=0; this caller needs to pass
* an empty string in that case (a NULL is good for ""). * an empty string in that case (a NULL is good for "").
*/ */
write_name_quoted_relative(name, prefix_len ? prefix : NULL, write_name_quoted_relative(name, prefix_len ? prefix : NULL,
stdout, line_terminator); stdout, line_terminator);
strbuf_release(&full_name);
} }
static void show_dir_entry(const char *tag, struct dir_entry *ent) 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)
} }
} }
/**
* Recursively call ls-files on a submodule
*/
static void show_gitlink(const struct cache_entry *ce)
{
struct child_process cp = CHILD_PROCESS_INIT;
int status;
argv_array_pushf(&cp.args, "--super-prefix=%s%s/",
super_prefix ? super_prefix : "",
ce->name);
argv_array_push(&cp.args, "ls-files");
argv_array_push(&cp.args, "--recurse-submodules");
cp.git_cmd = 1;
cp.dir = ce->name;
status = run_command(&cp);
if (status)
exit(status);
}
static void show_ce_entry(const char *tag, const struct cache_entry *ce) static void show_ce_entry(const char *tag, const struct cache_entry *ce)
{ {
struct strbuf name = STRBUF_INIT;
int len = max_prefix_len; int len = max_prefix_len;
if (super_prefix)
strbuf_addstr(&name, super_prefix);
strbuf_addstr(&name, ce->name);
if (len >= ce_namelen(ce)) if (len >= ce_namelen(ce))
die("git ls-files: internal error - cache entry not superset of prefix"); die("git ls-files: internal error - cache entry not superset of prefix");
if (!match_pathspec(&pathspec, ce->name, ce_namelen(ce), if (recurse_submodules && S_ISGITLINK(ce->ce_mode)) {
len, ps_matched, show_gitlink(ce);
S_ISDIR(ce->ce_mode) || S_ISGITLINK(ce->ce_mode))) } else if (match_pathspec(&pathspec, name.buf, name.len,
return; len, ps_matched,
S_ISDIR(ce->ce_mode) ||
if (tag && *tag && show_valid_bit && S_ISGITLINK(ce->ce_mode))) {
(ce->ce_flags & CE_VALID)) { if (tag && *tag && show_valid_bit &&
static char alttag[4]; (ce->ce_flags & CE_VALID)) {
memcpy(alttag, tag, 3); static char alttag[4];
if (isalpha(tag[0])) memcpy(alttag, tag, 3);
alttag[0] = tolower(tag[0]); if (isalpha(tag[0]))
else if (tag[0] == '?') alttag[0] = tolower(tag[0]);
alttag[0] = '!'; else if (tag[0] == '?')
else { alttag[0] = '!';
alttag[0] = 'v'; else {
alttag[1] = tag[0]; alttag[0] = 'v';
alttag[2] = ' '; alttag[1] = tag[0];
alttag[3] = 0; alttag[2] = ' ';
alttag[3] = 0;
}
tag = alttag;
}
if (!show_stage) {
fputs(tag, stdout);
} else {
printf("%s%06o %s %d\t",
tag,
ce->ce_mode,
find_unique_abbrev(ce->sha1,abbrev),
ce_stage(ce));
}
write_eolinfo(ce, ce->name);
write_name(ce->name);
if (debug_mode) {
const struct stat_data *sd = &ce->ce_stat_data;
printf(" ctime: %d:%d\n", sd->sd_ctime.sec, sd->sd_ctime.nsec);
printf(" mtime: %d:%d\n", sd->sd_mtime.sec, sd->sd_mtime.nsec);
printf(" dev: %d\tino: %d\n", sd->sd_dev, sd->sd_ino);
printf(" uid: %d\tgid: %d\n", sd->sd_uid, sd->sd_gid);
printf(" size: %d\tflags: %x\n", sd->sd_size, ce->ce_flags);
} }
tag = alttag;
} }
if (!show_stage) { strbuf_release(&name);
fputs(tag, stdout);
} else {
printf("%s%06o %s %d\t",
tag,
ce->ce_mode,
find_unique_abbrev(ce->sha1,abbrev),
ce_stage(ce));
}
write_eolinfo(ce, ce->name);
write_name(ce->name);
if (debug_mode) {
const struct stat_data *sd = &ce->ce_stat_data;
printf(" ctime: %d:%d\n", sd->sd_ctime.sec, sd->sd_ctime.nsec);
printf(" mtime: %d:%d\n", sd->sd_mtime.sec, sd->sd_mtime.nsec);
printf(" dev: %d\tino: %d\n", sd->sd_dev, sd->sd_ino);
printf(" uid: %d\tgid: %d\n", sd->sd_uid, sd->sd_gid);
printf(" size: %d\tflags: %x\n", sd->sd_size, ce->ce_flags);
}
} }
static void show_ru_info(void) static void show_ru_info(void)
@ -468,6 +513,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
{ OPTION_SET_INT, 0, "full-name", &prefix_len, NULL, { OPTION_SET_INT, 0, "full-name", &prefix_len, NULL,
N_("make the output relative to the project top directory"), N_("make the output relative to the project top directory"),
PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL }, PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL },
OPT_BOOL(0, "recurse-submodules", &recurse_submodules,
N_("recurse through submodules")),
OPT_BOOL(0, "error-unmatch", &error_unmatch, OPT_BOOL(0, "error-unmatch", &error_unmatch,
N_("if any <file> is not in the index, treat this as an error")), N_("if any <file> is not in the index, treat this as an error")),
OPT_STRING(0, "with-tree", &with_tree, N_("tree-ish"), 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)
prefix = cmd_prefix; prefix = cmd_prefix;
if (prefix) if (prefix)
prefix_len = strlen(prefix); prefix_len = strlen(prefix);
super_prefix = get_super_prefix();
git_config(git_default_config, NULL); git_config(git_default_config, NULL);
if (read_cache() < 0) if (read_cache() < 0)
@ -519,6 +567,20 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
if (require_work_tree && !is_inside_work_tree()) if (require_work_tree && !is_inside_work_tree())
setup_work_tree(); setup_work_tree();
if (recurse_submodules &&
(show_stage || show_deleted || show_others || show_unmerged ||
show_killed || show_modified || show_resolve_undo ||
show_valid_bit || show_tag || show_eol || with_tree ||
(line_terminator == '\0')))
die("ls-files --recurse-submodules unsupported mode");
if (recurse_submodules && error_unmatch)
die("ls-files --recurse-submodules does not support "
"--error-unmatch");
if (recurse_submodules && argc)
die("ls-files --recurse-submodules does not support pathspec");
parse_pathspec(&pathspec, 0, parse_pathspec(&pathspec, 0,
PATHSPEC_PREFER_CWD | PATHSPEC_PREFER_CWD |
PATHSPEC_STRIP_SUBMODULE_SLASH_CHEAP, PATHSPEC_STRIP_SUBMODULE_SLASH_CHEAP,

2
git.c
View File

@ -443,7 +443,7 @@ static struct cmd_struct commands[] = {
{ "init-db", cmd_init_db }, { "init-db", cmd_init_db },
{ "interpret-trailers", cmd_interpret_trailers, RUN_SETUP_GENTLY }, { "interpret-trailers", cmd_interpret_trailers, RUN_SETUP_GENTLY },
{ "log", cmd_log, RUN_SETUP }, { "log", cmd_log, RUN_SETUP },
{ "ls-files", cmd_ls_files, RUN_SETUP }, { "ls-files", cmd_ls_files, RUN_SETUP | SUPPORT_SUPER_PREFIX },
{ "ls-remote", cmd_ls_remote, RUN_SETUP_GENTLY }, { "ls-remote", cmd_ls_remote, RUN_SETUP_GENTLY },
{ "ls-tree", cmd_ls_tree, RUN_SETUP }, { "ls-tree", cmd_ls_tree, RUN_SETUP },
{ "mailinfo", cmd_mailinfo }, { "mailinfo", cmd_mailinfo },

View File

@ -0,0 +1,100 @@
#!/bin/sh
test_description='Test ls-files recurse-submodules feature
This test verifies the recurse-submodules feature correctly lists files from
submodules.
'
. ./test-lib.sh
test_expect_success 'setup directory structure and submodules' '
echo a >a &&
mkdir b &&
echo b >b/b &&
git add a b &&
git commit -m "add a and b" &&
git init submodule &&
echo c >submodule/c &&
git -C submodule add c &&
git -C submodule commit -m "add c" &&
git submodule add ./submodule &&
git commit -m "added submodule"
'
test_expect_success 'ls-files correctly outputs files in submodule' '
cat >expect <<-\EOF &&
.gitmodules
a
b/b
submodule/c
EOF
git ls-files --recurse-submodules >actual &&
test_cmp expect actual
'
test_expect_success 'ls-files does not output files not added to a repo' '
cat >expect <<-\EOF &&
.gitmodules
a
b/b
submodule/c
EOF
echo a >not_added &&
echo b >b/not_added &&
echo c >submodule/not_added &&
git ls-files --recurse-submodules >actual &&
test_cmp expect actual
'
test_expect_success 'ls-files recurses more than 1 level' '
cat >expect <<-\EOF &&
.gitmodules
a
b/b
submodule/.gitmodules
submodule/c
submodule/subsub/d
EOF
git init submodule/subsub &&
echo d >submodule/subsub/d &&
git -C submodule/subsub add d &&
git -C submodule/subsub commit -m "add d" &&
git -C submodule submodule add ./subsub &&
git -C submodule commit -m "added subsub" &&
git ls-files --recurse-submodules >actual &&
test_cmp expect actual
'
test_expect_success '--recurse-submodules does not support using path arguments' '
test_must_fail git ls-files --recurse-submodules b 2>actual &&
test_i18ngrep "does not support pathspec" actual
'
test_expect_success '--recurse-submodules does not support --error-unmatch' '
test_must_fail git ls-files --recurse-submodules --error-unmatch 2>actual &&
test_i18ngrep "does not support --error-unmatch" actual
'
test_incompatible_with_recurse_submodules () {
test_expect_success "--recurse-submodules and $1 are incompatible" "
test_must_fail git ls-files --recurse-submodules $1 2>actual &&
test_i18ngrep 'unsupported mode' actual
"
}
test_incompatible_with_recurse_submodules -z
test_incompatible_with_recurse_submodules -v
test_incompatible_with_recurse_submodules -t
test_incompatible_with_recurse_submodules --deleted
test_incompatible_with_recurse_submodules --modified
test_incompatible_with_recurse_submodules --others
test_incompatible_with_recurse_submodules --stage
test_incompatible_with_recurse_submodules --killed
test_incompatible_with_recurse_submodules --unmerged
test_incompatible_with_recurse_submodules --eol
test_done