From f748012e01109d04d3e956e26557a7fb4f92ce47 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 19 Feb 2022 16:44:41 +0000 Subject: [PATCH 1/5] sparse-checkout: correct reapply's handling of options Commit 4e256731d6 ("sparse-checkout: enable reapply to take --[no-]{cone,sparse-index}", 2021-12-14) made it so that reapply could take additional options but added no tests. Tests would have shown that the feature doesn't work because the initial values are set AFTER parsing the command line options instead of before. Add a test and set the initial value at the appropriate time. Reviewed-by: Derrick Stolee Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/sparse-checkout.c | 6 +++--- t/t1091-sparse-checkout-builtin.sh | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c index a311483a7d..fcd574f5fc 100644 --- a/builtin/sparse-checkout.c +++ b/builtin/sparse-checkout.c @@ -789,15 +789,15 @@ static int sparse_checkout_reapply(int argc, const char **argv) if (!core_apply_sparse_checkout) die(_("must be in a sparse-checkout to reapply sparsity patterns")); + reapply_opts.cone_mode = -1; + reapply_opts.sparse_index = -1; + argc = parse_options(argc, argv, NULL, builtin_sparse_checkout_reapply_options, builtin_sparse_checkout_reapply_usage, 0); repo_read_index(the_repository); - reapply_opts.cone_mode = -1; - reapply_opts.sparse_index = -1; - if (update_modes(&reapply_opts.cone_mode, &reapply_opts.sparse_index)) return 1; diff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh index 3592d12442..ce5e7c19ef 100755 --- a/t/t1091-sparse-checkout-builtin.sh +++ b/t/t1091-sparse-checkout-builtin.sh @@ -495,6 +495,34 @@ test_expect_failure 'sparse-checkout reapply' ' git -C tweak sparse-checkout disable ' +test_expect_success 'reapply can handle config options' ' + git -C repo sparse-checkout init --cone --no-sparse-index && + git -C repo config --worktree --list >actual && + cat >expect <<-\EOF && + core.sparsecheckout=true + core.sparsecheckoutcone=true + EOF + test_cmp expect actual && + + git -C repo sparse-checkout reapply --no-cone --no-sparse-index && + git -C repo config --worktree --list >actual && + cat >expect <<-\EOF && + core.sparsecheckout=true + EOF + test_cmp expect actual && + + git -C repo sparse-checkout reapply --cone --sparse-index && + git -C repo config --worktree --list >actual && + cat >expect <<-\EOF && + core.sparsecheckout=true + core.sparsecheckoutcone=true + index.sparse=true + EOF + test_cmp expect actual && + + git -C repo sparse-checkout disable +' + test_expect_success 'cone mode: set with core.ignoreCase=true' ' rm repo/.git/info/sparse-checkout && git -C repo sparse-checkout init --cone && From d526b4dbe1ab5bb7dfa9502237da81b940be876c Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 19 Feb 2022 16:44:42 +0000 Subject: [PATCH 2/5] sparse-checkout: correctly set non-cone mode when expected commit f2e3a218e8 ("sparse-checkout: enable `set` to initialize sparse-checkout mode", 2021-12-14) made the `set` command able to initialize sparse-checkout mode, but it also had to function when sparse-checkout mode was already setup and the user just wanted to change the sparsity paths. So, if the user passed --cone or --no-cone, then we should override the current setting, but if they didn't pass either, we should use whatever the current cone mode setting is. Unfortunately, there was a small error in the logic in that it would not set the in-memory cone mode value (core_sparse_checkout_one) when --no-cone was specified, but since it did set the config setting on disk, any subsequent git invocation would correctly get non-cone mode. As such, the error did not previously matter. However, a subsequent commit will add some logic that depends on core_sparse_checkout_cone being set to the correct mode, so make sure it is set consistently with the config values we will be writing to disk. Reviewed-by: Derrick Stolee Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/sparse-checkout.c | 1 + 1 file changed, 1 insertion(+) diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c index fcd574f5fc..fb85a1459c 100644 --- a/builtin/sparse-checkout.c +++ b/builtin/sparse-checkout.c @@ -403,6 +403,7 @@ static int update_modes(int *cone_mode, int *sparse_index) core_sparse_checkout_cone = 1; } else { mode = MODE_ALL_PATTERNS; + core_sparse_checkout_cone = 0; } if (record_mode && set_config(mode)) return 1; From bb8b5e9a90d607ec6144527c5019f56b6a81d862 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 19 Feb 2022 16:44:43 +0000 Subject: [PATCH 3/5] sparse-checkout: pay attention to prefix for {set, add} In cone mode, non-option arguments to set & add are clearly paths, and as such, we should pay attention to prefix. In non-cone mode, it is not clear that folks intend to provide paths since the inputs are gitignore-style patterns. Paying attention to prefix would prevent folks from doing things like git sparse-checkout add /.gitattributes git sparse-checkout add '/toplevel-dir/*' In fact, the former will result in fatal: '/.gitattributes' is outside repository... while the later will result in fatal: Invalid path '/toplevel-dir': No such file or directory despite the fact that both are valid gitignore-style patterns that would select real files if added to the sparse-checkout file. This might lead people to just use the path without the leading slash, potentially resulting in them grabbing files with the same name throughout the directory hierarchy contrary to their expectations. See also [1] and [2]. Adding prefix seems to just be fraught with error; so for now simply throw an error in non-cone mode when sparse-checkout set/add are run from a subdirectory. [1] https://lore.kernel.org/git/e1934710-e228-adc4-d37c-f706883bd27c@gmail.com/ [2] https://lore.kernel.org/git/CABPp-BHXZ-XLxY0a3wCATfdq=6-EjW62RzbxKAoFPeXfJswD2w@mail.gmail.com/ Helped-by: Junio Hamano Reviewed-by: Derrick Stolee Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/sparse-checkout.c | 26 +++++++++++++++++++ t/t1091-sparse-checkout-builtin.sh | 41 ++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c index fb85a1459c..3f828feb1f 100644 --- a/builtin/sparse-checkout.c +++ b/builtin/sparse-checkout.c @@ -684,6 +684,28 @@ static int modify_pattern_list(int argc, const char **argv, int use_stdin, return result; } +static void sanitize_paths(int argc, const char **argv, const char *prefix) +{ + if (!argc) + return; + + if (prefix && *prefix && core_sparse_checkout_cone) { + /* + * The args are not pathspecs, so unfortunately we + * cannot imitate how cmd_add() uses parse_pathspec(). + */ + int i; + int prefix_len = strlen(prefix); + + for (i = 0; i < argc; i++) + argv[i] = prefix_path(prefix, prefix_len, argv[i]); + } + + if (prefix && *prefix && !core_sparse_checkout_cone) + die(_("please run from the toplevel directory in non-cone mode")); + +} + static char const * const builtin_sparse_checkout_add_usage[] = { N_("git sparse-checkout add (--stdin | )"), NULL @@ -711,6 +733,8 @@ static int sparse_checkout_add(int argc, const char **argv, const char *prefix) builtin_sparse_checkout_add_usage, PARSE_OPT_KEEP_UNKNOWN); + sanitize_paths(argc, argv, prefix); + return modify_pattern_list(argc, argv, add_opts.use_stdin, ADD); } @@ -762,6 +786,8 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix) if (!core_sparse_checkout_cone && argc == 0) { argv = default_patterns; argc = default_patterns_nr; + } else { + sanitize_paths(argc, argv, prefix); } return modify_pattern_list(argc, argv, set_opts.use_stdin, REPLACE); diff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh index ce5e7c19ef..c1f86b0e02 100755 --- a/t/t1091-sparse-checkout-builtin.sh +++ b/t/t1091-sparse-checkout-builtin.sh @@ -798,4 +798,45 @@ test_expect_success 'malformed cone-mode patterns' ' grep "warning: disabling cone pattern matching" err ' +test_expect_success 'set from subdir pays attention to prefix' ' + git -C repo sparse-checkout disable && + git -C repo/deep sparse-checkout set --cone deeper2 ../folder1 && + + git -C repo sparse-checkout list >actual && + + cat >expect <<-\EOF && + deep/deeper2 + folder1 + EOF + test_cmp expect actual +' + +test_expect_success 'add from subdir pays attention to prefix' ' + git -C repo sparse-checkout set --cone deep/deeper2 && + git -C repo/deep sparse-checkout add deeper1/deepest ../folder1 && + + git -C repo sparse-checkout list >actual && + + cat >expect <<-\EOF && + deep/deeper1/deepest + deep/deeper2 + folder1 + EOF + test_cmp expect actual +' + +test_expect_success 'set from subdir in non-cone mode throws an error' ' + git -C repo sparse-checkout disable && + test_must_fail git -C repo/deep sparse-checkout set --no-cone deeper2 ../folder1 2>error && + + grep "run from the toplevel directory in non-cone mode" error +' + +test_expect_success 'set from subdir in non-cone mode throws an error' ' + git -C repo sparse-checkout set --no-cone deep/deeper2 && + test_must_fail git -C repo/deep sparse-checkout add deeper1/deepest ../folder1 2>error && + + grep "run from the toplevel directory in non-cone mode" error +' + test_done From 4ce504360bc3b240e570281fabe00a85027532c3 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 19 Feb 2022 16:44:44 +0000 Subject: [PATCH 4/5] sparse-checkout: error or warn when given individual files The set and add subcommands accept multiple positional arguments. The meaning of these arguments differs slightly in the two modes: Cone mode only accepts directories. If given a file, it would previously treat it as a directory, causing not just the file itself to be included but all sibling files as well -- likely against users' expectations. Throw an error if the specified path is a file in the index. Provide a --skip-checks argument to allow users to override (e.g. for the case when the given path IS a directory on another branch). Non-cone mode accepts general gitignore patterns. There are many reasons to avoid this mode, but one possible reason to use it instead of cone mode: to be able to select individual files within a directory. However, if a file is passed to set/add in non-cone mode, you won't be selecting a single file, you'll be selecting a file with the same name in any directory. Thus users will likely want to prefix any paths they specify with a leading '/' character; warn users if the patterns they specify exactly name a file because it means they are likely missing such a leading slash. Reviewed-by: Derrick Stolee Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/sparse-checkout.c | 42 +++++++++++++++++++++++++----- t/t1091-sparse-checkout-builtin.sh | 16 +++++++++++- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c index 3f828feb1f..38ac37d9c6 100644 --- a/builtin/sparse-checkout.c +++ b/builtin/sparse-checkout.c @@ -1,4 +1,5 @@ #include "builtin.h" +#include "cache.h" #include "config.h" #include "dir.h" #include "parse-options.h" @@ -684,8 +685,11 @@ static int modify_pattern_list(int argc, const char **argv, int use_stdin, return result; } -static void sanitize_paths(int argc, const char **argv, const char *prefix) +static void sanitize_paths(int argc, const char **argv, + const char *prefix, int skip_checks) { + int i; + if (!argc) return; @@ -694,30 +698,52 @@ static void sanitize_paths(int argc, const char **argv, const char *prefix) * The args are not pathspecs, so unfortunately we * cannot imitate how cmd_add() uses parse_pathspec(). */ - int i; int prefix_len = strlen(prefix); for (i = 0; i < argc; i++) argv[i] = prefix_path(prefix, prefix_len, argv[i]); } + if (skip_checks) + return; + if (prefix && *prefix && !core_sparse_checkout_cone) die(_("please run from the toplevel directory in non-cone mode")); + for (i = 0; i < argc; i++) { + struct cache_entry *ce; + struct index_state *index = the_repository->index; + int pos = index_name_pos(index, argv[i], strlen(argv[i])); + + if (pos < 0) + continue; + ce = index->cache[pos]; + if (S_ISSPARSEDIR(ce->ce_mode)) + continue; + + if (core_sparse_checkout_cone) + die(_("'%s' is not a directory; to treat it as a directory anyway, rerun with --skip-checks"), argv[i]); + else + warning(_("pass a leading slash before paths such as '%s' if you want a single file (see NON-CONE PROBLEMS in the git-sparse-checkout manual)."), argv[i]); + } } static char const * const builtin_sparse_checkout_add_usage[] = { - N_("git sparse-checkout add (--stdin | )"), + N_("git sparse-checkout add [--skip-checks] (--stdin | )"), NULL }; static struct sparse_checkout_add_opts { + int skip_checks; int use_stdin; } add_opts; static int sparse_checkout_add(int argc, const char **argv, const char *prefix) { static struct option builtin_sparse_checkout_add_options[] = { + OPT_BOOL_F(0, "skip-checks", &add_opts.skip_checks, + N_("skip some sanity checks on the given paths that might give false positives"), + PARSE_OPT_NONEG), OPT_BOOL(0, "stdin", &add_opts.use_stdin, N_("read patterns from standard in")), OPT_END(), @@ -733,19 +759,20 @@ static int sparse_checkout_add(int argc, const char **argv, const char *prefix) builtin_sparse_checkout_add_usage, PARSE_OPT_KEEP_UNKNOWN); - sanitize_paths(argc, argv, prefix); + sanitize_paths(argc, argv, prefix, add_opts.skip_checks); return modify_pattern_list(argc, argv, add_opts.use_stdin, ADD); } static char const * const builtin_sparse_checkout_set_usage[] = { - N_("git sparse-checkout set [--[no-]cone] [--[no-]sparse-index] (--stdin | )"), + N_("git sparse-checkout set [--[no-]cone] [--[no-]sparse-index] [--skip-checks] (--stdin | )"), NULL }; static struct sparse_checkout_set_opts { int cone_mode; int sparse_index; + int skip_checks; int use_stdin; } set_opts; @@ -759,6 +786,9 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix) N_("initialize the sparse-checkout in cone mode")), OPT_BOOL(0, "sparse-index", &set_opts.sparse_index, N_("toggle the use of a sparse index")), + OPT_BOOL_F(0, "skip-checks", &set_opts.skip_checks, + N_("skip some sanity checks on the given paths that might give false positives"), + PARSE_OPT_NONEG), OPT_BOOL_F(0, "stdin", &set_opts.use_stdin, N_("read patterns from standard in"), PARSE_OPT_NONEG), @@ -787,7 +817,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix) argv = default_patterns; argc = default_patterns_nr; } else { - sanitize_paths(argc, argv, prefix); + sanitize_paths(argc, argv, prefix, set_opts.skip_checks); } return modify_pattern_list(argc, argv, set_opts.use_stdin, REPLACE); diff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh index c1f86b0e02..3b39329266 100755 --- a/t/t1091-sparse-checkout-builtin.sh +++ b/t/t1091-sparse-checkout-builtin.sh @@ -562,7 +562,7 @@ test_expect_success 'different sparse-checkouts with worktrees' ' ' test_expect_success 'set using filename keeps file on-disk' ' - git -C repo sparse-checkout set a deep && + git -C repo sparse-checkout set --skip-checks a deep && cat >expect <<-\EOF && /* !/*/ @@ -839,4 +839,18 @@ test_expect_success 'set from subdir in non-cone mode throws an error' ' grep "run from the toplevel directory in non-cone mode" error ' +test_expect_success 'by default, cone mode will error out when passed files' ' + git -C repo sparse-checkout reapply --cone && + test_must_fail git -C repo sparse-checkout add .gitignore 2>error && + + grep ".gitignore.*is not a directory" error +' + +test_expect_success 'by default, non-cone mode will warn on individual files' ' + git -C repo sparse-checkout reapply --no-cone && + git -C repo sparse-checkout add .gitignore 2>warning && + + grep "pass a leading slash before paths.*if you want a single file" warning +' + test_done From 8dd7c4739bded62175bea1f7518d993b39b51f90 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 19 Feb 2022 16:44:45 +0000 Subject: [PATCH 5/5] sparse-checkout: reject arguments in cone-mode that look like patterns In sparse-checkout add/set under cone mode, the arguments passed are supposed to be directories rather than gitignore-style patterns. However, given the amount of effort spent in the manual discussing patterns, it is easy for users to assume they need to pass patterns such as /foo/* or !/bar/*/ or perhaps they really do ignore the directory rule and specify a random gitignore-style pattern like *.c To help catch such mistakes, throw an error if any of the positional arguments: * starts with any of '/!' * contains any of '*?[]' Inform users they can pass --skip-checks if they have a directory that really does have such special characters in its name. (We exclude '\' because of sparse-checkout's special handling of backslashes; see the MINGW test in t1091.46.) Reviewed-by: Derrick Stolee Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/sparse-checkout.c | 11 +++++++++++ t/t1091-sparse-checkout-builtin.sh | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c index 38ac37d9c6..a0c8a1499a 100644 --- a/builtin/sparse-checkout.c +++ b/builtin/sparse-checkout.c @@ -710,6 +710,17 @@ static void sanitize_paths(int argc, const char **argv, if (prefix && *prefix && !core_sparse_checkout_cone) die(_("please run from the toplevel directory in non-cone mode")); + if (core_sparse_checkout_cone) { + for (i = 0; i < argc; i++) { + if (argv[i][0] == '/') + die(_("specify directories rather than patterns (no leading slash)")); + if (argv[i][0] == '!') + die(_("specify directories rather than patterns. If your directory starts with a '!', pass --skip-checks")); + if (strpbrk(argv[i], "*?[]")) + die(_("specify directories rather than patterns. If your directory really has any of '*?[]\\' in it, pass --skip-checks")); + } + } + for (i = 0; i < argc; i++) { struct cache_entry *ce; struct index_state *index = the_repository->index; diff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh index 3b39329266..421127d99c 100755 --- a/t/t1091-sparse-checkout-builtin.sh +++ b/t/t1091-sparse-checkout-builtin.sh @@ -673,7 +673,7 @@ test_expect_success BSLASHPSPEC 'pattern-checks: escaped characters' ' git -C escaped reset --hard $COMMIT && check_files escaped "a deep folder1 folder2 zbad\\dir zdoes*exist" zglob[!a]? && git -C escaped sparse-checkout init --cone && - git -C escaped sparse-checkout set zbad\\dir/bogus "zdoes*not*exist" "zdoes*exist" "zglob[!a]?" && + git -C escaped sparse-checkout set --skip-checks zbad\\dir/bogus "zdoes*not*exist" "zdoes*exist" "zglob[!a]?" && cat >expect <<-\EOF && /* !/*/