builtin/sparse-checkout: add check-rules command

There exists no direct way to interrogate git about which paths are matched by a given set of sparsity rules. It is possible to get this information from git, but it includes checking out the commit that contains the paths, applying the sparse checkout patterns and then using something like 'git ls-files -t' to check if the skip worktree bit is set. This works in some case, but there are cases where it is awkward or infeasible to generate a checkout for this purpose. Exposing the pattern matching of sparse checkout enables more tooling to be built and avoids a situation where tools that want to reason about sparse checkouts start containing parallel implementation of the rules. To accommodate this, add a 'check-rules' subcommand to the 'sparse-checkout' builtin along the lines of the 'git check-ignore' and 'git check-attr' commands. The new command accepts a list of paths on stdin and outputs just the ones the match the sparse checkout. To allow for use in a bare repository and to allow for interrogating about other patterns than the current ones, include a '--rules-file' option which allows the caller to explicitly pass sparse checkout rules in the format accepted by 'sparse-checkout set --stdin'. To allow for reuse of the handling of input patterns for the '--rules-file' flag, modify 'add_patterns_from_input()' to be able to read from a 'FILE' instead of just stdin. To allow for reuse of the logic which decides whether or not rules should be interpreted as cone-mode patterns, split that part out of 'update_modes()' such that can be called without modifying the config. An alternative could have been to create a new 'check-sparsity' command. However, placing it under 'sparse-checkout' allows for a) more easily re-using the sparse checkout pattern matching and cone/non-code mode handling, and b) keeps the documentation for the command next to the experimental warning and the cone-mode discussion. Signed-off-by: William Sprent <williams@unity3d.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

William Sprent committed Mar 27, 2023 at 07:55 UTC 00408adeac13f6c16f93a3856961f7b327485c6b
3 files changed +267 -23
Documentation/git-sparse-checkout.txt
+24 -1
@@ -9,7 +9,7 @@ git-sparse-checkout - Reduce your working tree to a subset of tracked files
9 SYNOPSIS
10 --------
11 [verse]
12 -'git sparse-checkout' (init | list | set | add | reapply | disable) [<options>]
12 +'git sparse-checkout' (init | list | set | add | reapply | disable | check-rules) [<options>]
13
14
15 DESCRIPTION
@@ -135,6 +135,29 @@ paths to pass to a subsequent 'set' or 'add' command. However,
135 the disable command, so the easy restore of calling a plain `init`
136 decreased in utility.
137
138 +'check-rules'::
139 + Check whether sparsity rules match one or more paths.
140 ++
141 +By default `check-rules` reads a list of paths from stdin and outputs only
142 +the ones that match the current sparsity rules. The input is expected to consist
143 +of one path per line, matching the output of `git ls-tree --name-only` including
144 +that pathnames that begin with a double quote (") are interpreted as C-style
145 +quoted strings.
146 ++
147 +When called with the `--rules-file <file>` flag the input files are matched
148 +against the sparse checkout rules found in `<file>` instead of the current ones.
149 +The rules in the files are expected to be in the same form as accepted by `git
150 +sparse-checkout set --stdin` (in particular, they must be newline-delimited).
151 ++
152 +By default, the rules passed to the `--rules-file` option are interpreted as
153 +cone mode directories. To pass non-cone mode patterns with `--rules-file`,
154 +combine the option with the `--no-cone` option.
155 ++
156 +When called with the `-z` flag, the format of the paths input on stdin as well
157 +as the output paths are \0 terminated and not quoted. Note that this does not
158 +apply to the format of the rules passed with the `--rules-file` option.
159 +
160 +
161 EXAMPLES
162 --------
163 `git sparse-checkout set MY/DIR1 SUB/DIR2`::
builtin/sparse-checkout.c
+111 -20
@@ -20,7 +20,7 @@
20 static const char *empty_base = "";
21
22 static char const * const builtin_sparse_checkout_usage[] = {
23 - N_("git sparse-checkout (init | list | set | add | reapply | disable) [<options>]"),
23 + N_("git sparse-checkout (init | list | set | add | reapply | disable | check-rules) [<options>]"),
24 NULL
25 };
26
@@ -382,13 +382,7 @@ static int set_config(enum sparse_checkout_mode mode)
382 return 0;
383 }
384
385 -static int update_modes(int *cone_mode, int *sparse_index)
386 -{
387 - int mode, record_mode;
388 -
389 - /* Determine if we need to record the mode; ensure sparse checkout on */
390 - record_mode = (*cone_mode != -1) || !core_apply_sparse_checkout;
391 -
385 +static enum sparse_checkout_mode update_cone_mode(int *cone_mode) {
386 /* If not specified, use previous definition of cone mode */
387 if (*cone_mode == -1 && core_apply_sparse_checkout)
388 *cone_mode = core_sparse_checkout_cone;
@@ -396,12 +390,21 @@ static int update_modes(int *cone_mode, int *sparse_index)
390 /* Set cone/non-cone mode appropriately */
391 core_apply_sparse_checkout = 1;
392 if (*cone_mode == 1 || *cone_mode == -1) {
399 - mode = MODE_CONE_PATTERNS;
393 core_sparse_checkout_cone = 1;
401 - } else {
402 - mode = MODE_ALL_PATTERNS;
403 - core_sparse_checkout_cone = 0;
394 + return MODE_CONE_PATTERNS;
395 }
396 + core_sparse_checkout_cone = 0;
397 + return MODE_ALL_PATTERNS;
398 +}
399 +
400 +static int update_modes(int *cone_mode, int *sparse_index)
401 +{
402 + int mode, record_mode;
403 +
404 + /* Determine if we need to record the mode; ensure sparse checkout on */
405 + record_mode = (*cone_mode != -1) || !core_apply_sparse_checkout;
406 +
407 + mode = update_cone_mode(cone_mode);
408 if (record_mode && set_config(mode))
409 return 1;
410
@@ -545,7 +548,7 @@ static void strbuf_to_cone_pattern(struct strbuf *line, struct pattern_list *pl)
548
549 static void add_patterns_from_input(struct pattern_list *pl,
550 int argc, const char **argv,
548 - int use_stdin)
551 + FILE *file)
552 {
553 int i;
554 if (core_sparse_checkout_cone) {
@@ -555,9 +558,9 @@ static void add_patterns_from_input(struct pattern_list *pl,
558 hashmap_init(&pl->parent_hashmap, pl_hashmap_cmp, NULL, 0);
559 pl->use_cone_patterns = 1;
560
558 - if (use_stdin) {
561 + if (file) {
562 struct strbuf unquoted = STRBUF_INIT;
560 - while (!strbuf_getline(&line, stdin)) {
563 + while (!strbuf_getline(&line, file)) {
564 if (line.buf[0] == '"') {
565 strbuf_reset(&unquoted);
566 if (unquote_c_style(&unquoted, line.buf, NULL))
@@ -579,10 +582,10 @@ static void add_patterns_from_input(struct pattern_list *pl,
582 }
583 }
584 } else {
582 - if (use_stdin) {
585 + if (file) {
586 struct strbuf line = STRBUF_INIT;
587
585 - while (!strbuf_getline(&line, stdin)) {
588 + while (!strbuf_getline(&line, file)) {
589 size_t len;
590 char *buf = strbuf_detach(&line, &len);
591 add_pattern(buf, empty_base, 0, pl, 0);
@@ -609,7 +612,8 @@ static void add_patterns_cone_mode(int argc, const char **argv,
612 struct pattern_list existing;
613 char *sparse_filename = get_sparse_checkout_filename();
614
612 - add_patterns_from_input(pl, argc, argv, use_stdin);
615 + add_patterns_from_input(pl, argc, argv,
616 + use_stdin ? stdin : NULL);
617
618 memset(&existing, 0, sizeof(existing));
619 existing.use_cone_patterns = core_sparse_checkout_cone;
@@ -646,7 +650,7 @@ static void add_patterns_literal(int argc, const char **argv,
650 pl, NULL, 0))
651 die(_("unable to load existing sparse-checkout patterns"));
652 free(sparse_filename);
649 - add_patterns_from_input(pl, argc, argv, use_stdin);
653 + add_patterns_from_input(pl, argc, argv, use_stdin ? stdin : NULL);
654 }
655
656 static int modify_pattern_list(int argc, const char **argv, int use_stdin,
@@ -665,7 +669,8 @@ static int modify_pattern_list(int argc, const char **argv, int use_stdin,
669 break;
670
671 case REPLACE:
668 - add_patterns_from_input(pl, argc, argv, use_stdin);
672 + add_patterns_from_input(pl, argc, argv,
673 + use_stdin ? stdin : NULL);
674 break;
675 }
676
@@ -927,6 +932,91 @@ static int sparse_checkout_disable(int argc, const char **argv,
932 return set_config(MODE_NO_PATTERNS);
933 }
934
935 +static char const * const builtin_sparse_checkout_check_rules_usage[] = {
936 + N_("git sparse-checkout check-rules [-z] [--skip-checks]"
937 + "[--[no-]cone] [--rules-file <file>]"),
938 + NULL
939 +};
940 +
941 +static struct sparse_checkout_check_rules_opts {
942 + int cone_mode;
943 + int null_termination;
944 + char *rules_file;
945 +} check_rules_opts;
946 +
947 +static int check_rules(struct pattern_list *pl, int null_terminated) {
948 + struct strbuf line = STRBUF_INIT;
949 + struct strbuf unquoted = STRBUF_INIT;
950 + char *path;
951 + int line_terminator = null_terminated ? 0 : '\n';
952 + strbuf_getline_fn getline_fn = null_terminated ? strbuf_getline_nul
953 + : strbuf_getline;
954 + the_repository->index->sparse_checkout_patterns = pl;
955 + while (!getline_fn(&line, stdin)) {
956 + path = line.buf;
957 + if (!null_terminated && line.buf[0] == '"') {
958 + strbuf_reset(&unquoted);
959 + if (unquote_c_style(&unquoted, line.buf, NULL))
960 + die(_("unable to unquote C-style string '%s'"),
961 + line.buf);
962 +
963 + path = unquoted.buf;
964 + }
965 +
966 + if (path_in_sparse_checkout(path, the_repository->index))
967 + write_name_quoted(path, stdout, line_terminator);
968 + }
969 + strbuf_release(&line);
970 + strbuf_release(&unquoted);
971 +
972 + return 0;
973 +}
974 +
975 +static int sparse_checkout_check_rules(int argc, const char **argv, const char *prefix)
976 +{
977 + static struct option builtin_sparse_checkout_check_rules_options[] = {
978 + OPT_BOOL('z', NULL, &check_rules_opts.null_termination,
979 + N_("terminate input and output files by a NUL character")),
980 + OPT_BOOL(0, "cone", &check_rules_opts.cone_mode,
981 + N_("when used with --rules-file interpret patterns as cone mode patterns")),
982 + OPT_FILENAME(0, "rules-file", &check_rules_opts.rules_file,
983 + N_("use patterns in <file> instead of the current ones.")),
984 + OPT_END(),
985 + };
986 +
987 + FILE *fp;
988 + int ret;
989 + struct pattern_list pl = {0};
990 + char *sparse_filename;
991 + check_rules_opts.cone_mode = -1;
992 +
993 + argc = parse_options(argc, argv, prefix,
994 + builtin_sparse_checkout_check_rules_options,
995 + builtin_sparse_checkout_check_rules_usage,
996 + PARSE_OPT_KEEP_UNKNOWN_OPT);
997 +
998 + if (check_rules_opts.rules_file && check_rules_opts.cone_mode < 0)
999 + check_rules_opts.cone_mode = 1;
1000 +
1001 + update_cone_mode(&check_rules_opts.cone_mode);
1002 + pl.use_cone_patterns = core_sparse_checkout_cone;
1003 + if (check_rules_opts.rules_file) {
1004 + fp = xfopen(check_rules_opts.rules_file, "r");
1005 + add_patterns_from_input(&pl, argc, argv, fp);
1006 + fclose(fp);
1007 + } else {
1008 + sparse_filename = get_sparse_checkout_filename();
1009 + if (add_patterns_from_file_to_list(sparse_filename, "", 0, &pl,
1010 + NULL, 0))
1011 + die(_("unable to load existing sparse-checkout patterns"));
1012 + free(sparse_filename);
1013 + }
1014 +
1015 + ret = check_rules(&pl, check_rules_opts.null_termination);
1016 + clear_pattern_list(&pl);
1017 + return ret;
1018 +}
1019 +
1020 int cmd_sparse_checkout(int argc, const char **argv, const char *prefix)
1021 {
1022 parse_opt_subcommand_fn *fn = NULL;
@@ -937,6 +1027,7 @@ int cmd_sparse_checkout(int argc, const char **argv, const char *prefix)
1027 OPT_SUBCOMMAND("add", &fn, sparse_checkout_add),
1028 OPT_SUBCOMMAND("reapply", &fn, sparse_checkout_reapply),
1029 OPT_SUBCOMMAND("disable", &fn, sparse_checkout_disable),
1030 + OPT_SUBCOMMAND("check-rules", &fn, sparse_checkout_check_rules),
1031 OPT_END(),
1032 };
1033
t/t1091-sparse-checkout-builtin.sh
+132 -2
@@ -555,7 +555,7 @@ test_expect_success 'cone mode: set with core.ignoreCase=true' '
555 check_files repo a folder1
556 '
557
558 -test_expect_success 'interaction with submodules' '
558 +test_expect_success 'setup submodules' '
559 git clone repo super &&
560 (
561 cd super &&
@@ -566,11 +566,22 @@ test_expect_success 'interaction with submodules' '
566 git commit -m "add submodule" &&
567 git sparse-checkout init --cone &&
568 git sparse-checkout set folder1
569 - ) &&
569 + )
570 +'
571 +
572 +test_expect_success 'interaction with submodules' '
573 check_files super a folder1 modules &&
574 check_files super/modules/child a deep folder1 folder2
575 '
576
577 +test_expect_success 'check-rules interaction with submodules' '
578 + git -C super ls-tree --name-only -r HEAD >all-files &&
579 + git -C super sparse-checkout check-rules >check-rules-matches <all-files &&
580 +
581 + test_i18ngrep ! "modules/" check-rules-matches &&
582 + test_i18ngrep "folder1/" check-rules-matches
583 +'
584 +
585 test_expect_success 'different sparse-checkouts with worktrees' '
586 git -C repo sparse-checkout set --cone deep folder1 &&
587 git -C repo worktree add --detach ../worktree &&
@@ -915,4 +926,123 @@ test_expect_success 'disable fails outside work tree' '
926 test_i18ngrep "this operation must be run in a work tree" err
927 '
928
929 +test_expect_success 'setup clean' '
930 + git -C repo clean -fdx
931 +'
932 +
933 +test_expect_success 'check-rules cone mode' '
934 + cat >rules <<-\EOF &&
935 + folder1
936 + deep/deeper1/deepest
937 + EOF
938 +
939 + git -C bare ls-tree -r --name-only HEAD >all-files &&
940 + git -C bare sparse-checkout check-rules --cone \
941 + --rules-file ../rules >check-rules-file <all-files &&
942 +
943 + git -C repo sparse-checkout set --cone --stdin <rules&&
944 + git -C repo ls-files -t >out &&
945 + sed -n "/^S /!s/^. //p" out >ls-files &&
946 +
947 + git -C repo sparse-checkout check-rules >check-rules-default <all-files &&
948 +
949 + test_i18ngrep "deep/deeper1/deepest/a" check-rules-file &&
950 + test_i18ngrep ! "deep/deeper2" check-rules-file &&
951 +
952 + test_cmp check-rules-file ls-files &&
953 + test_cmp check-rules-file check-rules-default
954 +'
955 +
956 +test_expect_success 'check-rules non-cone mode' '
957 + cat >rules <<-\EOF &&
958 + deep/deeper1/deepest/a
959 + EOF
960 +
961 + git -C bare ls-tree -r --name-only HEAD >all-files &&
962 + git -C bare sparse-checkout check-rules --no-cone --rules-file ../rules\
963 + >check-rules-file <all-files &&
964 +
965 + cat rules | git -C repo sparse-checkout set --no-cone --stdin &&
966 + git -C repo ls-files -t >out &&
967 + sed -n "/^S /!s/^. //p" out >ls-files &&
968 +
969 + git -C repo sparse-checkout check-rules >check-rules-default <all-files &&
970 +
971 + cat >expect <<-\EOF &&
972 + deep/deeper1/deepest/a
973 + EOF
974 +
975 + test_cmp expect check-rules-file &&
976 + test_cmp check-rules-file ls-files &&
977 + test_cmp check-rules-file check-rules-default
978 +'
979 +
980 +test_expect_success 'check-rules cone mode is default' '
981 + cat >rules <<-\EOF &&
982 + folder1
983 + EOF
984 +
985 + cat >all-files <<-\EOF &&
986 + toplevel
987 + folder2/file
988 + folder1/file
989 + EOF
990 +
991 + cat >expect <<-\EOF &&
992 + toplevel
993 + folder1/file
994 + EOF
995 +
996 + git -C repo sparse-checkout set --no-cone &&
997 + git -C repo sparse-checkout check-rules \
998 + --rules-file ../rules >actual <all-files &&
999 +
1000 + git -C bare sparse-checkout check-rules \
1001 + --rules-file ../rules >actual-bare <all-files &&
1002 +
1003 + test_cmp expect actual &&
1004 + test_cmp expect actual-bare
1005 +'
1006 +
1007 +test_expect_success 'check-rules quoting' '
1008 + cat >rules <<-EOF &&
1009 + "folder\" a"
1010 + EOF
1011 + cat >files <<-EOF &&
1012 + "folder\" a/file"
1013 + "folder\" b/file"
1014 + EOF
1015 + cat >expect <<-EOF &&
1016 + "folder\" a/file"
1017 + EOF
1018 + git sparse-checkout check-rules --cone \
1019 + --rules-file rules >actual <files &&
1020 +
1021 + test_cmp expect actual
1022 +'
1023 +
1024 +test_expect_success 'check-rules null termination' '
1025 + cat >rules <<-EOF &&
1026 + "folder\" a"
1027 + EOF
1028 +
1029 + lf_to_nul >files <<-EOF &&
1030 + folder" a/a
1031 + folder" a/b
1032 + folder" b/fileQ
1033 + EOF
1034 +
1035 + cat >expect <<-EOF &&
1036 + folder" a/aQfolder" a/bQ
1037 + EOF
1038 +
1039 + git sparse-checkout check-rules --cone -z \
1040 + --rules-file rules >actual.nul <files &&
1041 + nul_to_q <actual.nul >actual &&
1042 + echo >>actual &&
1043 +
1044 + test_cmp expect actual
1045 +'
1046 +
1047 +
1048 test_done