Merge branch 'ws/sparse-check-rules'

"git sparse-checkout" command learns a debugging aid for the sparse
rule definitions.

* ws/sparse-check-rules:
  builtin/sparse-checkout: add check-rules command
  builtin/sparse-checkout: remove NEED_WORK_TREE flag
This commit is contained in:
Junio C Hamano 2023-04-11 13:49:12 -07:00
commit d02343b599
4 changed files with 307 additions and 24 deletions

View File

@ -9,7 +9,7 @@ git-sparse-checkout - Reduce your working tree to a subset of tracked files
SYNOPSIS SYNOPSIS
-------- --------
[verse] [verse]
'git sparse-checkout' (init | list | set | add | reapply | disable) [<options>] 'git sparse-checkout' (init | list | set | add | reapply | disable | check-rules) [<options>]
DESCRIPTION DESCRIPTION
@ -135,6 +135,29 @@ paths to pass to a subsequent 'set' or 'add' command. However,
the disable command, so the easy restore of calling a plain `init` the disable command, so the easy restore of calling a plain `init`
decreased in utility. decreased in utility.
'check-rules'::
Check whether sparsity rules match one or more paths.
+
By default `check-rules` reads a list of paths from stdin and outputs only
the ones that match the current sparsity rules. The input is expected to consist
of one path per line, matching the output of `git ls-tree --name-only` including
that pathnames that begin with a double quote (") are interpreted as C-style
quoted strings.
+
When called with the `--rules-file <file>` flag the input files are matched
against the sparse checkout rules found in `<file>` instead of the current ones.
The rules in the files are expected to be in the same form as accepted by `git
sparse-checkout set --stdin` (in particular, they must be newline-delimited).
+
By default, the rules passed to the `--rules-file` option are interpreted as
cone mode directories. To pass non-cone mode patterns with `--rules-file`,
combine the option with the `--no-cone` option.
+
When called with the `-z` flag, the format of the paths input on stdin as well
as the output paths are \0 terminated and not quoted. Note that this does not
apply to the format of the rules passed with the `--rules-file` option.
EXAMPLES EXAMPLES
-------- --------
`git sparse-checkout set MY/DIR1 SUB/DIR2`:: `git sparse-checkout set MY/DIR1 SUB/DIR2`::

View File

@ -23,7 +23,7 @@
static const char *empty_base = ""; static const char *empty_base = "";
static char const * const builtin_sparse_checkout_usage[] = { static char const * const builtin_sparse_checkout_usage[] = {
N_("git sparse-checkout (init | list | set | add | reapply | disable) [<options>]"), N_("git sparse-checkout (init | list | set | add | reapply | disable | check-rules) [<options>]"),
NULL NULL
}; };
@ -60,6 +60,7 @@ static int sparse_checkout_list(int argc, const char **argv, const char *prefix)
char *sparse_filename; char *sparse_filename;
int res; int res;
setup_work_tree();
if (!core_apply_sparse_checkout) if (!core_apply_sparse_checkout)
die(_("this worktree is not sparse")); die(_("this worktree is not sparse"));
@ -384,13 +385,7 @@ static int set_config(enum sparse_checkout_mode mode)
return 0; return 0;
} }
static int update_modes(int *cone_mode, int *sparse_index) static enum sparse_checkout_mode update_cone_mode(int *cone_mode) {
{
int mode, record_mode;
/* Determine if we need to record the mode; ensure sparse checkout on */
record_mode = (*cone_mode != -1) || !core_apply_sparse_checkout;
/* If not specified, use previous definition of cone mode */ /* If not specified, use previous definition of cone mode */
if (*cone_mode == -1 && core_apply_sparse_checkout) if (*cone_mode == -1 && core_apply_sparse_checkout)
*cone_mode = core_sparse_checkout_cone; *cone_mode = core_sparse_checkout_cone;
@ -398,12 +393,21 @@ static int update_modes(int *cone_mode, int *sparse_index)
/* Set cone/non-cone mode appropriately */ /* Set cone/non-cone mode appropriately */
core_apply_sparse_checkout = 1; core_apply_sparse_checkout = 1;
if (*cone_mode == 1 || *cone_mode == -1) { if (*cone_mode == 1 || *cone_mode == -1) {
mode = MODE_CONE_PATTERNS;
core_sparse_checkout_cone = 1; core_sparse_checkout_cone = 1;
} else { return MODE_CONE_PATTERNS;
mode = MODE_ALL_PATTERNS;
core_sparse_checkout_cone = 0;
} }
core_sparse_checkout_cone = 0;
return MODE_ALL_PATTERNS;
}
static int update_modes(int *cone_mode, int *sparse_index)
{
int mode, record_mode;
/* Determine if we need to record the mode; ensure sparse checkout on */
record_mode = (*cone_mode != -1) || !core_apply_sparse_checkout;
mode = update_cone_mode(cone_mode);
if (record_mode && set_config(mode)) if (record_mode && set_config(mode))
return 1; return 1;
@ -449,6 +453,7 @@ static int sparse_checkout_init(int argc, const char **argv, const char *prefix)
OPT_END(), OPT_END(),
}; };
setup_work_tree();
repo_read_index(the_repository); repo_read_index(the_repository);
init_opts.cone_mode = -1; init_opts.cone_mode = -1;
@ -546,7 +551,7 @@ static void strbuf_to_cone_pattern(struct strbuf *line, struct pattern_list *pl)
static void add_patterns_from_input(struct pattern_list *pl, static void add_patterns_from_input(struct pattern_list *pl,
int argc, const char **argv, int argc, const char **argv,
int use_stdin) FILE *file)
{ {
int i; int i;
if (core_sparse_checkout_cone) { if (core_sparse_checkout_cone) {
@ -556,9 +561,9 @@ static void add_patterns_from_input(struct pattern_list *pl,
hashmap_init(&pl->parent_hashmap, pl_hashmap_cmp, NULL, 0); hashmap_init(&pl->parent_hashmap, pl_hashmap_cmp, NULL, 0);
pl->use_cone_patterns = 1; pl->use_cone_patterns = 1;
if (use_stdin) { if (file) {
struct strbuf unquoted = STRBUF_INIT; struct strbuf unquoted = STRBUF_INIT;
while (!strbuf_getline(&line, stdin)) { while (!strbuf_getline(&line, file)) {
if (line.buf[0] == '"') { if (line.buf[0] == '"') {
strbuf_reset(&unquoted); strbuf_reset(&unquoted);
if (unquote_c_style(&unquoted, line.buf, NULL)) if (unquote_c_style(&unquoted, line.buf, NULL))
@ -580,10 +585,10 @@ static void add_patterns_from_input(struct pattern_list *pl,
} }
} }
} else { } else {
if (use_stdin) { if (file) {
struct strbuf line = STRBUF_INIT; struct strbuf line = STRBUF_INIT;
while (!strbuf_getline(&line, stdin)) { while (!strbuf_getline(&line, file)) {
size_t len; size_t len;
char *buf = strbuf_detach(&line, &len); char *buf = strbuf_detach(&line, &len);
add_pattern(buf, empty_base, 0, pl, 0); add_pattern(buf, empty_base, 0, pl, 0);
@ -610,7 +615,8 @@ static void add_patterns_cone_mode(int argc, const char **argv,
struct pattern_list existing; struct pattern_list existing;
char *sparse_filename = get_sparse_checkout_filename(); char *sparse_filename = get_sparse_checkout_filename();
add_patterns_from_input(pl, argc, argv, use_stdin); add_patterns_from_input(pl, argc, argv,
use_stdin ? stdin : NULL);
memset(&existing, 0, sizeof(existing)); memset(&existing, 0, sizeof(existing));
existing.use_cone_patterns = core_sparse_checkout_cone; existing.use_cone_patterns = core_sparse_checkout_cone;
@ -647,7 +653,7 @@ static void add_patterns_literal(int argc, const char **argv,
pl, NULL, 0)) pl, NULL, 0))
die(_("unable to load existing sparse-checkout patterns")); die(_("unable to load existing sparse-checkout patterns"));
free(sparse_filename); free(sparse_filename);
add_patterns_from_input(pl, argc, argv, use_stdin); add_patterns_from_input(pl, argc, argv, use_stdin ? stdin : NULL);
} }
static int modify_pattern_list(int argc, const char **argv, int use_stdin, static int modify_pattern_list(int argc, const char **argv, int use_stdin,
@ -666,7 +672,8 @@ static int modify_pattern_list(int argc, const char **argv, int use_stdin,
break; break;
case REPLACE: case REPLACE:
add_patterns_from_input(pl, argc, argv, use_stdin); add_patterns_from_input(pl, argc, argv,
use_stdin ? stdin : NULL);
break; break;
} }
@ -761,6 +768,7 @@ static int sparse_checkout_add(int argc, const char **argv, const char *prefix)
OPT_END(), OPT_END(),
}; };
setup_work_tree();
if (!core_apply_sparse_checkout) if (!core_apply_sparse_checkout)
die(_("no sparse-checkout to add to")); die(_("no sparse-checkout to add to"));
@ -807,6 +815,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix)
OPT_END(), OPT_END(),
}; };
setup_work_tree();
repo_read_index(the_repository); repo_read_index(the_repository);
set_opts.cone_mode = -1; set_opts.cone_mode = -1;
@ -856,6 +865,7 @@ static int sparse_checkout_reapply(int argc, const char **argv,
OPT_END(), OPT_END(),
}; };
setup_work_tree();
if (!core_apply_sparse_checkout) if (!core_apply_sparse_checkout)
die(_("must be in a sparse-checkout to reapply sparsity patterns")); die(_("must be in a sparse-checkout to reapply sparsity patterns"));
@ -899,6 +909,7 @@ static int sparse_checkout_disable(int argc, const char **argv,
* forcibly return to a dense checkout regardless of initial state. * forcibly return to a dense checkout regardless of initial state.
*/ */
setup_work_tree();
argc = parse_options(argc, argv, prefix, argc = parse_options(argc, argv, prefix,
builtin_sparse_checkout_disable_options, builtin_sparse_checkout_disable_options,
builtin_sparse_checkout_disable_usage, 0); builtin_sparse_checkout_disable_usage, 0);
@ -924,6 +935,91 @@ static int sparse_checkout_disable(int argc, const char **argv,
return set_config(MODE_NO_PATTERNS); return set_config(MODE_NO_PATTERNS);
} }
static char const * const builtin_sparse_checkout_check_rules_usage[] = {
N_("git sparse-checkout check-rules [-z] [--skip-checks]"
"[--[no-]cone] [--rules-file <file>]"),
NULL
};
static struct sparse_checkout_check_rules_opts {
int cone_mode;
int null_termination;
char *rules_file;
} check_rules_opts;
static int check_rules(struct pattern_list *pl, int null_terminated) {
struct strbuf line = STRBUF_INIT;
struct strbuf unquoted = STRBUF_INIT;
char *path;
int line_terminator = null_terminated ? 0 : '\n';
strbuf_getline_fn getline_fn = null_terminated ? strbuf_getline_nul
: strbuf_getline;
the_repository->index->sparse_checkout_patterns = pl;
while (!getline_fn(&line, stdin)) {
path = line.buf;
if (!null_terminated && line.buf[0] == '"') {
strbuf_reset(&unquoted);
if (unquote_c_style(&unquoted, line.buf, NULL))
die(_("unable to unquote C-style string '%s'"),
line.buf);
path = unquoted.buf;
}
if (path_in_sparse_checkout(path, the_repository->index))
write_name_quoted(path, stdout, line_terminator);
}
strbuf_release(&line);
strbuf_release(&unquoted);
return 0;
}
static int sparse_checkout_check_rules(int argc, const char **argv, const char *prefix)
{
static struct option builtin_sparse_checkout_check_rules_options[] = {
OPT_BOOL('z', NULL, &check_rules_opts.null_termination,
N_("terminate input and output files by a NUL character")),
OPT_BOOL(0, "cone", &check_rules_opts.cone_mode,
N_("when used with --rules-file interpret patterns as cone mode patterns")),
OPT_FILENAME(0, "rules-file", &check_rules_opts.rules_file,
N_("use patterns in <file> instead of the current ones.")),
OPT_END(),
};
FILE *fp;
int ret;
struct pattern_list pl = {0};
char *sparse_filename;
check_rules_opts.cone_mode = -1;
argc = parse_options(argc, argv, prefix,
builtin_sparse_checkout_check_rules_options,
builtin_sparse_checkout_check_rules_usage,
PARSE_OPT_KEEP_UNKNOWN_OPT);
if (check_rules_opts.rules_file && check_rules_opts.cone_mode < 0)
check_rules_opts.cone_mode = 1;
update_cone_mode(&check_rules_opts.cone_mode);
pl.use_cone_patterns = core_sparse_checkout_cone;
if (check_rules_opts.rules_file) {
fp = xfopen(check_rules_opts.rules_file, "r");
add_patterns_from_input(&pl, argc, argv, fp);
fclose(fp);
} else {
sparse_filename = get_sparse_checkout_filename();
if (add_patterns_from_file_to_list(sparse_filename, "", 0, &pl,
NULL, 0))
die(_("unable to load existing sparse-checkout patterns"));
free(sparse_filename);
}
ret = check_rules(&pl, check_rules_opts.null_termination);
clear_pattern_list(&pl);
return ret;
}
int cmd_sparse_checkout(int argc, const char **argv, const char *prefix) int cmd_sparse_checkout(int argc, const char **argv, const char *prefix)
{ {
parse_opt_subcommand_fn *fn = NULL; parse_opt_subcommand_fn *fn = NULL;
@ -934,6 +1030,7 @@ int cmd_sparse_checkout(int argc, const char **argv, const char *prefix)
OPT_SUBCOMMAND("add", &fn, sparse_checkout_add), OPT_SUBCOMMAND("add", &fn, sparse_checkout_add),
OPT_SUBCOMMAND("reapply", &fn, sparse_checkout_reapply), OPT_SUBCOMMAND("reapply", &fn, sparse_checkout_reapply),
OPT_SUBCOMMAND("disable", &fn, sparse_checkout_disable), OPT_SUBCOMMAND("disable", &fn, sparse_checkout_disable),
OPT_SUBCOMMAND("check-rules", &fn, sparse_checkout_check_rules),
OPT_END(), OPT_END(),
}; };

2
git.c
View File

@ -587,7 +587,7 @@ static struct cmd_struct commands[] = {
{ "show-branch", cmd_show_branch, RUN_SETUP }, { "show-branch", cmd_show_branch, RUN_SETUP },
{ "show-index", cmd_show_index, RUN_SETUP_GENTLY }, { "show-index", cmd_show_index, RUN_SETUP_GENTLY },
{ "show-ref", cmd_show_ref, RUN_SETUP }, { "show-ref", cmd_show_ref, RUN_SETUP },
{ "sparse-checkout", cmd_sparse_checkout, RUN_SETUP | NEED_WORK_TREE }, { "sparse-checkout", cmd_sparse_checkout, RUN_SETUP },
{ "stage", cmd_add, RUN_SETUP | NEED_WORK_TREE }, { "stage", cmd_add, RUN_SETUP | NEED_WORK_TREE },
{ "stash", cmd_stash, RUN_SETUP | NEED_WORK_TREE }, { "stash", cmd_stash, RUN_SETUP | NEED_WORK_TREE },
{ "status", cmd_status, RUN_SETUP | NEED_WORK_TREE }, { "status", cmd_status, RUN_SETUP | NEED_WORK_TREE },

View File

@ -555,7 +555,7 @@ test_expect_success 'cone mode: set with core.ignoreCase=true' '
check_files repo a folder1 check_files repo a folder1
' '
test_expect_success 'interaction with submodules' ' test_expect_success 'setup submodules' '
git clone repo super && git clone repo super &&
( (
cd super && cd super &&
@ -566,11 +566,22 @@ test_expect_success 'interaction with submodules' '
git commit -m "add submodule" && git commit -m "add submodule" &&
git sparse-checkout init --cone && git sparse-checkout init --cone &&
git sparse-checkout set folder1 git sparse-checkout set folder1
) && )
'
test_expect_success 'interaction with submodules' '
check_files super a folder1 modules && check_files super a folder1 modules &&
check_files super/modules/child a deep folder1 folder2 check_files super/modules/child a deep folder1 folder2
' '
test_expect_success 'check-rules interaction with submodules' '
git -C super ls-tree --name-only -r HEAD >all-files &&
git -C super sparse-checkout check-rules >check-rules-matches <all-files &&
test_i18ngrep ! "modules/" check-rules-matches &&
test_i18ngrep "folder1/" check-rules-matches
'
test_expect_success 'different sparse-checkouts with worktrees' ' test_expect_success 'different sparse-checkouts with worktrees' '
git -C repo sparse-checkout set --cone deep folder1 && git -C repo sparse-checkout set --cone deep folder1 &&
git -C repo worktree add --detach ../worktree && git -C repo worktree add --detach ../worktree &&
@ -882,4 +893,156 @@ test_expect_success 'by default, non-cone mode will warn on individual files' '
grep "pass a leading slash before paths.*if you want a single file" warning grep "pass a leading slash before paths.*if you want a single file" warning
' '
test_expect_success 'setup bare repo' '
git clone --bare "file://$(pwd)/repo" bare
'
test_expect_success 'list fails outside work tree' '
test_must_fail git -C bare sparse-checkout list 2>err &&
test_i18ngrep "this operation must be run in a work tree" err
'
test_expect_success 'add fails outside work tree' '
test_must_fail git -C bare sparse-checkout add deeper 2>err &&
test_i18ngrep "this operation must be run in a work tree" err
'
test_expect_success 'set fails outside work tree' '
test_must_fail git -C bare sparse-checkout set deeper 2>err &&
test_i18ngrep "this operation must be run in a work tree" err
'
test_expect_success 'init fails outside work tree' '
test_must_fail git -C bare sparse-checkout init 2>err &&
test_i18ngrep "this operation must be run in a work tree" err
'
test_expect_success 'reapply fails outside work tree' '
test_must_fail git -C bare sparse-checkout reapply 2>err &&
test_i18ngrep "this operation must be run in a work tree" err
'
test_expect_success 'disable fails outside work tree' '
test_must_fail git -C bare sparse-checkout disable 2>err &&
test_i18ngrep "this operation must be run in a work tree" err
'
test_expect_success 'setup clean' '
git -C repo clean -fdx
'
test_expect_success 'check-rules cone mode' '
cat >rules <<-\EOF &&
folder1
deep/deeper1/deepest
EOF
git -C bare ls-tree -r --name-only HEAD >all-files &&
git -C bare sparse-checkout check-rules --cone \
--rules-file ../rules >check-rules-file <all-files &&
git -C repo sparse-checkout set --cone --stdin <rules&&
git -C repo ls-files -t >out &&
sed -n "/^S /!s/^. //p" out >ls-files &&
git -C repo sparse-checkout check-rules >check-rules-default <all-files &&
test_i18ngrep "deep/deeper1/deepest/a" check-rules-file &&
test_i18ngrep ! "deep/deeper2" check-rules-file &&
test_cmp check-rules-file ls-files &&
test_cmp check-rules-file check-rules-default
'
test_expect_success 'check-rules non-cone mode' '
cat >rules <<-\EOF &&
deep/deeper1/deepest/a
EOF
git -C bare ls-tree -r --name-only HEAD >all-files &&
git -C bare sparse-checkout check-rules --no-cone --rules-file ../rules\
>check-rules-file <all-files &&
cat rules | git -C repo sparse-checkout set --no-cone --stdin &&
git -C repo ls-files -t >out &&
sed -n "/^S /!s/^. //p" out >ls-files &&
git -C repo sparse-checkout check-rules >check-rules-default <all-files &&
cat >expect <<-\EOF &&
deep/deeper1/deepest/a
EOF
test_cmp expect check-rules-file &&
test_cmp check-rules-file ls-files &&
test_cmp check-rules-file check-rules-default
'
test_expect_success 'check-rules cone mode is default' '
cat >rules <<-\EOF &&
folder1
EOF
cat >all-files <<-\EOF &&
toplevel
folder2/file
folder1/file
EOF
cat >expect <<-\EOF &&
toplevel
folder1/file
EOF
git -C repo sparse-checkout set --no-cone &&
git -C repo sparse-checkout check-rules \
--rules-file ../rules >actual <all-files &&
git -C bare sparse-checkout check-rules \
--rules-file ../rules >actual-bare <all-files &&
test_cmp expect actual &&
test_cmp expect actual-bare
'
test_expect_success 'check-rules quoting' '
cat >rules <<-EOF &&
"folder\" a"
EOF
cat >files <<-EOF &&
"folder\" a/file"
"folder\" b/file"
EOF
cat >expect <<-EOF &&
"folder\" a/file"
EOF
git sparse-checkout check-rules --cone \
--rules-file rules >actual <files &&
test_cmp expect actual
'
test_expect_success 'check-rules null termination' '
cat >rules <<-EOF &&
"folder\" a"
EOF
lf_to_nul >files <<-EOF &&
folder" a/a
folder" a/b
folder" b/fileQ
EOF
cat >expect <<-EOF &&
folder" a/aQfolder" a/bQ
EOF
git sparse-checkout check-rules --cone -z \
--rules-file rules >actual.nul <files &&
nul_to_q <actual.nul >actual &&
echo >>actual &&
test_cmp expect actual
'
test_done test_done