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 <derrickstolee@github.com> Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Elijah Newren committed Feb 19, 2022 at 16:44 UTC 4ce504360bc3b240e570281fabe00a85027532c3
2 files changed +51 -7
builtin/sparse-checkout.c
+36 -6
@@ -1,4 +1,5 @@
1 #include "builtin.h"
2 +#include "cache.h"
3 #include "config.h"
4 #include "dir.h"
5 #include "parse-options.h"
@@ -684,8 +685,11 @@ static int modify_pattern_list(int argc, const char **argv, int use_stdin,
685 return result;
686 }
687
687 -static void sanitize_paths(int argc, const char **argv, const char *prefix)
688 +static void sanitize_paths(int argc, const char **argv,
689 + const char *prefix, int skip_checks)
690 {
691 + int i;
692 +
693 if (!argc)
694 return;
695
@@ -694,30 +698,52 @@ static void sanitize_paths(int argc, const char **argv, const char *prefix)
698 * The args are not pathspecs, so unfortunately we
699 * cannot imitate how cmd_add() uses parse_pathspec().
700 */
697 - int i;
701 int prefix_len = strlen(prefix);
702
703 for (i = 0; i < argc; i++)
704 argv[i] = prefix_path(prefix, prefix_len, argv[i]);
705 }
706
707 + if (skip_checks)
708 + return;
709 +
710 if (prefix && *prefix && !core_sparse_checkout_cone)
711 die(_("please run from the toplevel directory in non-cone mode"));
712
713 + for (i = 0; i < argc; i++) {
714 + struct cache_entry *ce;
715 + struct index_state *index = the_repository->index;
716 + int pos = index_name_pos(index, argv[i], strlen(argv[i]));
717 +
718 + if (pos < 0)
719 + continue;
720 + ce = index->cache[pos];
721 + if (S_ISSPARSEDIR(ce->ce_mode))
722 + continue;
723 +
724 + if (core_sparse_checkout_cone)
725 + die(_("'%s' is not a directory; to treat it as a directory anyway, rerun with --skip-checks"), argv[i]);
726 + else
727 + 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]);
728 + }
729 }
730
731 static char const * const builtin_sparse_checkout_add_usage[] = {
710 - N_("git sparse-checkout add (--stdin | <patterns>)"),
732 + N_("git sparse-checkout add [--skip-checks] (--stdin | <patterns>)"),
733 NULL
734 };
735
736 static struct sparse_checkout_add_opts {
737 + int skip_checks;
738 int use_stdin;
739 } add_opts;
740
741 static int sparse_checkout_add(int argc, const char **argv, const char *prefix)
742 {
743 static struct option builtin_sparse_checkout_add_options[] = {
744 + OPT_BOOL_F(0, "skip-checks", &add_opts.skip_checks,
745 + N_("skip some sanity checks on the given paths that might give false positives"),
746 + PARSE_OPT_NONEG),
747 OPT_BOOL(0, "stdin", &add_opts.use_stdin,
748 N_("read patterns from standard in")),
749 OPT_END(),
@@ -733,19 +759,20 @@ static int sparse_checkout_add(int argc, const char **argv, const char *prefix)
759 builtin_sparse_checkout_add_usage,
760 PARSE_OPT_KEEP_UNKNOWN);
761
736 - sanitize_paths(argc, argv, prefix);
762 + sanitize_paths(argc, argv, prefix, add_opts.skip_checks);
763
764 return modify_pattern_list(argc, argv, add_opts.use_stdin, ADD);
765 }
766
767 static char const * const builtin_sparse_checkout_set_usage[] = {
742 - N_("git sparse-checkout set [--[no-]cone] [--[no-]sparse-index] (--stdin | <patterns>)"),
768 + N_("git sparse-checkout set [--[no-]cone] [--[no-]sparse-index] [--skip-checks] (--stdin | <patterns>)"),
769 NULL
770 };
771
772 static struct sparse_checkout_set_opts {
773 int cone_mode;
774 int sparse_index;
775 + int skip_checks;
776 int use_stdin;
777 } set_opts;
778
@@ -759,6 +786,9 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix)
786 N_("initialize the sparse-checkout in cone mode")),
787 OPT_BOOL(0, "sparse-index", &set_opts.sparse_index,
788 N_("toggle the use of a sparse index")),
789 + OPT_BOOL_F(0, "skip-checks", &set_opts.skip_checks,
790 + N_("skip some sanity checks on the given paths that might give false positives"),
791 + PARSE_OPT_NONEG),
792 OPT_BOOL_F(0, "stdin", &set_opts.use_stdin,
793 N_("read patterns from standard in"),
794 PARSE_OPT_NONEG),
@@ -787,7 +817,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix)
817 argv = default_patterns;
818 argc = default_patterns_nr;
819 } else {
790 - sanitize_paths(argc, argv, prefix);
820 + sanitize_paths(argc, argv, prefix, set_opts.skip_checks);
821 }
822
823 return modify_pattern_list(argc, argv, set_opts.use_stdin, REPLACE);
t/t1091-sparse-checkout-builtin.sh
+15 -1
@@ -562,7 +562,7 @@ test_expect_success 'different sparse-checkouts with worktrees' '
562 '
563
564 test_expect_success 'set using filename keeps file on-disk' '
565 - git -C repo sparse-checkout set a deep &&
565 + git -C repo sparse-checkout set --skip-checks a deep &&
566 cat >expect <<-\EOF &&
567 /*
568 !/*/
@@ -839,4 +839,18 @@ test_expect_success 'set from subdir in non-cone mode throws an error' '
839 grep "run from the toplevel directory in non-cone mode" error
840 '
841
842 +test_expect_success 'by default, cone mode will error out when passed files' '
843 + git -C repo sparse-checkout reapply --cone &&
844 + test_must_fail git -C repo sparse-checkout add .gitignore 2>error &&
845 +
846 + grep ".gitignore.*is not a directory" error
847 +'
848 +
849 +test_expect_success 'by default, non-cone mode will warn on individual files' '
850 + git -C repo sparse-checkout reapply --no-cone &&
851 + git -C repo sparse-checkout add .gitignore 2>warning &&
852 +
853 + grep "pass a leading slash before paths.*if you want a single file" warning
854 +'
855 +
856 test_done