Git fork

Merge branch 'ds/sparse-checkout-clean'

"git sparse-checkout" subcommand learned a new "clean" action to
prune otherwise unused working-tree files that are outside the
areas of interest.

* ds/sparse-checkout-clean:
sparse-index: improve advice message instructions
t: expand tests around sparse merges and clean
sparse-index: point users to new 'clean' action
sparse-checkout: add --verbose option to 'clean'
dir: add generic "walk all files" helper
sparse-checkout: match some 'clean' behavior
sparse-checkout: add basics of 'clean' command
sparse-checkout: remove use of the_repository

+412 -58
+32 -1
Documentation/git-sparse-checkout.adoc
··· 9 SYNOPSIS 10 -------- 11 [verse] 12 - 'git sparse-checkout' (init | list | set | add | reapply | disable | check-rules) [<options>] 13 14 15 DESCRIPTION ··· 110 flags, with the same meaning as the flags from the `set` command, in order 111 to change which sparsity mode you are using without needing to also respecify 112 all sparsity paths. 113 114 'disable':: 115 Disable the `core.sparseCheckout` config setting, and restore the
··· 9 SYNOPSIS 10 -------- 11 [verse] 12 + 'git sparse-checkout' (init | list | set | add | reapply | disable | check-rules | clean) [<options>] 13 14 15 DESCRIPTION ··· 110 flags, with the same meaning as the flags from the `set` command, in order 111 to change which sparsity mode you are using without needing to also respecify 112 all sparsity paths. 113 + 114 + 'clean':: 115 + Opportunistically remove files outside of the sparse-checkout 116 + definition. This command requires cone mode to use recursive 117 + directory matches to determine which files should be removed. A 118 + file is considered for removal if it is contained within a tracked 119 + directory that is outside of the sparse-checkout definition. 120 + + 121 + Some special cases, such as merge conflicts or modified files outside of 122 + the sparse-checkout definition could lead to keeping files that would 123 + otherwise be removed. Resolve conflicts, stage modifications, and use 124 + `git sparse-checkout reapply` in conjunction with `git sparse-checkout 125 + clean` to resolve these cases. 126 + + 127 + This command can be used to be sure the sparse index works efficiently, 128 + though it does not require enabling the sparse index feature via the 129 + `index.sparse=true` configuration. 130 + + 131 + To prevent accidental deletion of worktree files, the `clean` subcommand 132 + will not delete any files without the `-f` or `--force` option, unless 133 + the `clean.requireForce` config option is set to `false`. 134 + + 135 + The `--dry-run` option will list the directories that would be removed 136 + without deleting them. Running in this mode can be helpful to predict the 137 + behavior of the clean comand or to determine which kinds of files are left 138 + in the sparse directories. 139 + + 140 + The `--verbose` option will list every file within the directories that 141 + are considered for removal. This option is helpful to determine if those 142 + files are actually important or perhaps to explain why the directory is 143 + still present despite the current sparse-checkout. 144 145 'disable':: 146 Disable the `core.sparseCheckout` config setting, and restore the
+160 -56
builtin/sparse-checkout.c
··· 2 #define DISABLE_SIGN_COMPARE_WARNINGS 3 4 #include "builtin.h" 5 #include "config.h" 6 #include "dir.h" 7 #include "environment.h" ··· 23 static const char *empty_base = ""; 24 25 static char const * const builtin_sparse_checkout_usage[] = { 26 - N_("git sparse-checkout (init | list | set | add | reapply | disable | check-rules) [<options>]"), 27 NULL 28 }; 29 ··· 204 ensure_full_index(r->index); 205 } 206 207 - static int update_working_directory(struct pattern_list *pl) 208 { 209 enum update_sparsity_result result; 210 struct unpack_trees_options o; 211 struct lock_file lock_file = LOCK_INIT; 212 - struct repository *r = the_repository; 213 struct pattern_list *old_pl; 214 215 /* If no branch has been checked out, there are no updates to make. */ ··· 327 string_list_clear(&sl, 0); 328 } 329 330 - static int write_patterns_and_update(struct pattern_list *pl) 331 { 332 char *sparse_filename; 333 FILE *fp; ··· 336 337 sparse_filename = get_sparse_checkout_filename(); 338 339 - if (safe_create_leading_directories(the_repository, sparse_filename)) 340 die(_("failed to create directory for sparse-checkout file")); 341 342 hold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR); 343 344 - result = update_working_directory(pl); 345 if (result) { 346 rollback_lock_file(&lk); 347 - update_working_directory(NULL); 348 goto out; 349 } 350 ··· 372 MODE_CONE_PATTERNS = 2, 373 }; 374 375 - static int set_config(enum sparse_checkout_mode mode) 376 { 377 /* Update to use worktree config, if not already. */ 378 - if (init_worktree_config(the_repository)) { 379 error(_("failed to initialize worktree config")); 380 return 1; 381 } 382 383 - if (repo_config_set_worktree_gently(the_repository, 384 "core.sparseCheckout", 385 mode ? "true" : "false") || 386 - repo_config_set_worktree_gently(the_repository, 387 "core.sparseCheckoutCone", 388 mode == MODE_CONE_PATTERNS ? 389 "true" : "false")) 390 return 1; 391 392 if (mode == MODE_NO_PATTERNS) 393 - return set_sparse_index_config(the_repository, 0); 394 395 return 0; 396 } ··· 410 return MODE_ALL_PATTERNS; 411 } 412 413 - static int update_modes(int *cone_mode, int *sparse_index) 414 { 415 int mode, record_mode; 416 ··· 418 record_mode = (*cone_mode != -1) || !core_apply_sparse_checkout; 419 420 mode = update_cone_mode(cone_mode); 421 - if (record_mode && set_config(mode)) 422 return 1; 423 424 /* Set sparse-index/non-sparse-index mode if specified */ 425 if (*sparse_index >= 0) { 426 - if (set_sparse_index_config(the_repository, *sparse_index) < 0) 427 die(_("failed to modify sparse-index config")); 428 429 /* force an index rewrite */ 430 - repo_read_index(the_repository); 431 - the_repository->index->updated_workdir = 1; 432 433 if (!*sparse_index) 434 - ensure_full_index(the_repository->index); 435 } 436 437 return 0; ··· 448 } init_opts; 449 450 static int sparse_checkout_init(int argc, const char **argv, const char *prefix, 451 - struct repository *repo UNUSED) 452 { 453 struct pattern_list pl; 454 char *sparse_filename; ··· 464 }; 465 466 setup_work_tree(); 467 - repo_read_index(the_repository); 468 469 init_opts.cone_mode = -1; 470 init_opts.sparse_index = -1; ··· 473 builtin_sparse_checkout_init_options, 474 builtin_sparse_checkout_init_usage, 0); 475 476 - if (update_modes(&init_opts.cone_mode, &init_opts.sparse_index)) 477 return 1; 478 479 memset(&pl, 0, sizeof(pl)); ··· 485 if (res >= 0) { 486 free(sparse_filename); 487 clear_pattern_list(&pl); 488 - return update_working_directory(NULL); 489 } 490 491 - if (repo_get_oid(the_repository, "HEAD", &oid)) { 492 FILE *fp; 493 494 /* assume we are in a fresh repo, but update the sparse-checkout file */ 495 - if (safe_create_leading_directories(the_repository, sparse_filename)) 496 die(_("unable to create leading directories of %s"), 497 sparse_filename); 498 fp = xfopen(sparse_filename, "w"); ··· 511 add_pattern("!/*/", empty_base, 0, &pl, 0); 512 pl.use_cone_patterns = init_opts.cone_mode; 513 514 - return write_patterns_and_update(&pl); 515 } 516 517 static void insert_recursive_pattern(struct pattern_list *pl, struct strbuf *path) ··· 674 add_patterns_from_input(pl, argc, argv, use_stdin ? stdin : NULL); 675 } 676 677 - static int modify_pattern_list(struct strvec *args, int use_stdin, 678 enum modify_type m) 679 { 680 int result; ··· 696 } 697 698 if (!core_apply_sparse_checkout) { 699 - set_config(MODE_ALL_PATTERNS); 700 core_apply_sparse_checkout = 1; 701 changed_config = 1; 702 } 703 704 - result = write_patterns_and_update(pl); 705 706 if (result && changed_config) 707 - set_config(MODE_NO_PATTERNS); 708 709 clear_pattern_list(pl); 710 free(pl); 711 return result; 712 } 713 714 - static void sanitize_paths(struct strvec *args, 715 const char *prefix, int skip_checks) 716 { 717 int i; ··· 752 753 for (i = 0; i < args->nr; i++) { 754 struct cache_entry *ce; 755 - struct index_state *index = the_repository->index; 756 int pos = index_name_pos(index, args->v[i], strlen(args->v[i])); 757 758 if (pos < 0) ··· 779 } add_opts; 780 781 static int sparse_checkout_add(int argc, const char **argv, const char *prefix, 782 - struct repository *repo UNUSED) 783 { 784 static struct option builtin_sparse_checkout_add_options[] = { 785 OPT_BOOL_F(0, "skip-checks", &add_opts.skip_checks, ··· 796 if (!core_apply_sparse_checkout) 797 die(_("no sparse-checkout to add to")); 798 799 - repo_read_index(the_repository); 800 801 argc = parse_options(argc, argv, prefix, 802 builtin_sparse_checkout_add_options, ··· 804 805 for (int i = 0; i < argc; i++) 806 strvec_push(&patterns, argv[i]); 807 - sanitize_paths(&patterns, prefix, add_opts.skip_checks); 808 809 - ret = modify_pattern_list(&patterns, add_opts.use_stdin, ADD); 810 811 strvec_clear(&patterns); 812 return ret; ··· 825 } set_opts; 826 827 static int sparse_checkout_set(int argc, const char **argv, const char *prefix, 828 - struct repository *repo UNUSED) 829 { 830 int default_patterns_nr = 2; 831 const char *default_patterns[] = {"/*", "!/*/", NULL}; ··· 847 int ret; 848 849 setup_work_tree(); 850 - repo_read_index(the_repository); 851 852 set_opts.cone_mode = -1; 853 set_opts.sparse_index = -1; ··· 856 builtin_sparse_checkout_set_options, 857 builtin_sparse_checkout_set_usage, 0); 858 859 - if (update_modes(&set_opts.cone_mode, &set_opts.sparse_index)) 860 return 1; 861 862 /* ··· 870 } else { 871 for (int i = 0; i < argc; i++) 872 strvec_push(&patterns, argv[i]); 873 - sanitize_paths(&patterns, prefix, set_opts.skip_checks); 874 } 875 876 - ret = modify_pattern_list(&patterns, set_opts.use_stdin, REPLACE); 877 878 strvec_clear(&patterns); 879 return ret; ··· 891 892 static int sparse_checkout_reapply(int argc, const char **argv, 893 const char *prefix, 894 - struct repository *repo UNUSED) 895 { 896 static struct option builtin_sparse_checkout_reapply_options[] = { 897 OPT_BOOL(0, "cone", &reapply_opts.cone_mode, ··· 912 builtin_sparse_checkout_reapply_options, 913 builtin_sparse_checkout_reapply_usage, 0); 914 915 - repo_read_index(the_repository); 916 917 - if (update_modes(&reapply_opts.cone_mode, &reapply_opts.sparse_index)) 918 return 1; 919 920 - return update_working_directory(NULL); 921 } 922 923 static char const * const builtin_sparse_checkout_disable_usage[] = { ··· 927 928 static int sparse_checkout_disable(int argc, const char **argv, 929 const char *prefix, 930 - struct repository *repo UNUSED) 931 { 932 static struct option builtin_sparse_checkout_disable_options[] = { 933 OPT_END(), ··· 955 * are expecting to do that when disabling sparse-checkout. 956 */ 957 give_advice_on_expansion = 0; 958 - repo_read_index(the_repository); 959 960 memset(&pl, 0, sizeof(pl)); 961 hashmap_init(&pl.recursive_hashmap, pl_hashmap_cmp, NULL, 0); ··· 966 add_pattern("/*", empty_base, 0, &pl, 0); 967 968 prepare_repo_settings(the_repository); 969 - the_repository->settings.sparse_index = 0; 970 971 - if (update_working_directory(&pl)) 972 die(_("error while refreshing working directory")); 973 974 clear_pattern_list(&pl); 975 - return set_config(MODE_NO_PATTERNS); 976 } 977 978 static char const * const builtin_sparse_checkout_check_rules_usage[] = { ··· 987 char *rules_file; 988 } check_rules_opts; 989 990 - static int check_rules(struct pattern_list *pl, int null_terminated) { 991 struct strbuf line = STRBUF_INIT; 992 struct strbuf unquoted = STRBUF_INIT; 993 char *path; 994 int line_terminator = null_terminated ? 0 : '\n'; 995 strbuf_getline_fn getline_fn = null_terminated ? strbuf_getline_nul 996 : strbuf_getline; 997 - the_repository->index->sparse_checkout_patterns = pl; 998 while (!getline_fn(&line, stdin)) { 999 path = line.buf; 1000 if (!null_terminated && line.buf[0] == '"') { ··· 1006 path = unquoted.buf; 1007 } 1008 1009 - if (path_in_sparse_checkout(path, the_repository->index)) 1010 write_name_quoted(path, stdout, line_terminator); 1011 } 1012 strbuf_release(&line); ··· 1016 } 1017 1018 static int sparse_checkout_check_rules(int argc, const char **argv, const char *prefix, 1019 - struct repository *repo UNUSED) 1020 { 1021 static struct option builtin_sparse_checkout_check_rules_options[] = { 1022 OPT_BOOL('z', NULL, &check_rules_opts.null_termination, ··· 1055 free(sparse_filename); 1056 } 1057 1058 - ret = check_rules(&pl, check_rules_opts.null_termination); 1059 clear_pattern_list(&pl); 1060 free(check_rules_opts.rules_file); 1061 return ret; ··· 1073 OPT_SUBCOMMAND("set", &fn, sparse_checkout_set), 1074 OPT_SUBCOMMAND("add", &fn, sparse_checkout_add), 1075 OPT_SUBCOMMAND("reapply", &fn, sparse_checkout_reapply), 1076 OPT_SUBCOMMAND("disable", &fn, sparse_checkout_disable), 1077 OPT_SUBCOMMAND("check-rules", &fn, sparse_checkout_check_rules), 1078 OPT_END(), ··· 1084 1085 repo_config(the_repository, git_default_config, NULL); 1086 1087 - prepare_repo_settings(the_repository); 1088 - the_repository->settings.command_requires_full_index = 0; 1089 1090 return fn(argc, argv, prefix, repo); 1091 }
··· 2 #define DISABLE_SIGN_COMPARE_WARNINGS 3 4 #include "builtin.h" 5 + #include "abspath.h" 6 #include "config.h" 7 #include "dir.h" 8 #include "environment.h" ··· 24 static const char *empty_base = ""; 25 26 static char const * const builtin_sparse_checkout_usage[] = { 27 + N_("git sparse-checkout (init | list | set | add | reapply | disable | check-rules | clean) [<options>]"), 28 NULL 29 }; 30 ··· 205 ensure_full_index(r->index); 206 } 207 208 + static int update_working_directory(struct repository *r, 209 + struct pattern_list *pl) 210 { 211 enum update_sparsity_result result; 212 struct unpack_trees_options o; 213 struct lock_file lock_file = LOCK_INIT; 214 struct pattern_list *old_pl; 215 216 /* If no branch has been checked out, there are no updates to make. */ ··· 328 string_list_clear(&sl, 0); 329 } 330 331 + static int write_patterns_and_update(struct repository *repo, 332 + struct pattern_list *pl) 333 { 334 char *sparse_filename; 335 FILE *fp; ··· 338 339 sparse_filename = get_sparse_checkout_filename(); 340 341 + if (safe_create_leading_directories(repo, sparse_filename)) 342 die(_("failed to create directory for sparse-checkout file")); 343 344 hold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR); 345 346 + result = update_working_directory(repo, pl); 347 if (result) { 348 rollback_lock_file(&lk); 349 + update_working_directory(repo, NULL); 350 goto out; 351 } 352 ··· 374 MODE_CONE_PATTERNS = 2, 375 }; 376 377 + static int set_config(struct repository *repo, 378 + enum sparse_checkout_mode mode) 379 { 380 /* Update to use worktree config, if not already. */ 381 + if (init_worktree_config(repo)) { 382 error(_("failed to initialize worktree config")); 383 return 1; 384 } 385 386 + if (repo_config_set_worktree_gently(repo, 387 "core.sparseCheckout", 388 mode ? "true" : "false") || 389 + repo_config_set_worktree_gently(repo, 390 "core.sparseCheckoutCone", 391 mode == MODE_CONE_PATTERNS ? 392 "true" : "false")) 393 return 1; 394 395 if (mode == MODE_NO_PATTERNS) 396 + return set_sparse_index_config(repo, 0); 397 398 return 0; 399 } ··· 413 return MODE_ALL_PATTERNS; 414 } 415 416 + static int update_modes(struct repository *repo, int *cone_mode, int *sparse_index) 417 { 418 int mode, record_mode; 419 ··· 421 record_mode = (*cone_mode != -1) || !core_apply_sparse_checkout; 422 423 mode = update_cone_mode(cone_mode); 424 + if (record_mode && set_config(repo, mode)) 425 return 1; 426 427 /* Set sparse-index/non-sparse-index mode if specified */ 428 if (*sparse_index >= 0) { 429 + if (set_sparse_index_config(repo, *sparse_index) < 0) 430 die(_("failed to modify sparse-index config")); 431 432 /* force an index rewrite */ 433 + repo_read_index(repo); 434 + repo->index->updated_workdir = 1; 435 436 if (!*sparse_index) 437 + ensure_full_index(repo->index); 438 } 439 440 return 0; ··· 451 } init_opts; 452 453 static int sparse_checkout_init(int argc, const char **argv, const char *prefix, 454 + struct repository *repo) 455 { 456 struct pattern_list pl; 457 char *sparse_filename; ··· 467 }; 468 469 setup_work_tree(); 470 + repo_read_index(repo); 471 472 init_opts.cone_mode = -1; 473 init_opts.sparse_index = -1; ··· 476 builtin_sparse_checkout_init_options, 477 builtin_sparse_checkout_init_usage, 0); 478 479 + if (update_modes(repo, &init_opts.cone_mode, &init_opts.sparse_index)) 480 return 1; 481 482 memset(&pl, 0, sizeof(pl)); ··· 488 if (res >= 0) { 489 free(sparse_filename); 490 clear_pattern_list(&pl); 491 + return update_working_directory(repo, NULL); 492 } 493 494 + if (repo_get_oid(repo, "HEAD", &oid)) { 495 FILE *fp; 496 497 /* assume we are in a fresh repo, but update the sparse-checkout file */ 498 + if (safe_create_leading_directories(repo, sparse_filename)) 499 die(_("unable to create leading directories of %s"), 500 sparse_filename); 501 fp = xfopen(sparse_filename, "w"); ··· 514 add_pattern("!/*/", empty_base, 0, &pl, 0); 515 pl.use_cone_patterns = init_opts.cone_mode; 516 517 + return write_patterns_and_update(repo, &pl); 518 } 519 520 static void insert_recursive_pattern(struct pattern_list *pl, struct strbuf *path) ··· 677 add_patterns_from_input(pl, argc, argv, use_stdin ? stdin : NULL); 678 } 679 680 + static int modify_pattern_list(struct repository *repo, 681 + struct strvec *args, int use_stdin, 682 enum modify_type m) 683 { 684 int result; ··· 700 } 701 702 if (!core_apply_sparse_checkout) { 703 + set_config(repo, MODE_ALL_PATTERNS); 704 core_apply_sparse_checkout = 1; 705 changed_config = 1; 706 } 707 708 + result = write_patterns_and_update(repo, pl); 709 710 if (result && changed_config) 711 + set_config(repo, MODE_NO_PATTERNS); 712 713 clear_pattern_list(pl); 714 free(pl); 715 return result; 716 } 717 718 + static void sanitize_paths(struct repository *repo, 719 + struct strvec *args, 720 const char *prefix, int skip_checks) 721 { 722 int i; ··· 757 758 for (i = 0; i < args->nr; i++) { 759 struct cache_entry *ce; 760 + struct index_state *index = repo->index; 761 int pos = index_name_pos(index, args->v[i], strlen(args->v[i])); 762 763 if (pos < 0) ··· 784 } add_opts; 785 786 static int sparse_checkout_add(int argc, const char **argv, const char *prefix, 787 + struct repository *repo) 788 { 789 static struct option builtin_sparse_checkout_add_options[] = { 790 OPT_BOOL_F(0, "skip-checks", &add_opts.skip_checks, ··· 801 if (!core_apply_sparse_checkout) 802 die(_("no sparse-checkout to add to")); 803 804 + repo_read_index(repo); 805 806 argc = parse_options(argc, argv, prefix, 807 builtin_sparse_checkout_add_options, ··· 809 810 for (int i = 0; i < argc; i++) 811 strvec_push(&patterns, argv[i]); 812 + sanitize_paths(repo, &patterns, prefix, add_opts.skip_checks); 813 814 + ret = modify_pattern_list(repo, &patterns, add_opts.use_stdin, ADD); 815 816 strvec_clear(&patterns); 817 return ret; ··· 830 } set_opts; 831 832 static int sparse_checkout_set(int argc, const char **argv, const char *prefix, 833 + struct repository *repo) 834 { 835 int default_patterns_nr = 2; 836 const char *default_patterns[] = {"/*", "!/*/", NULL}; ··· 852 int ret; 853 854 setup_work_tree(); 855 + repo_read_index(repo); 856 857 set_opts.cone_mode = -1; 858 set_opts.sparse_index = -1; ··· 861 builtin_sparse_checkout_set_options, 862 builtin_sparse_checkout_set_usage, 0); 863 864 + if (update_modes(repo, &set_opts.cone_mode, &set_opts.sparse_index)) 865 return 1; 866 867 /* ··· 875 } else { 876 for (int i = 0; i < argc; i++) 877 strvec_push(&patterns, argv[i]); 878 + sanitize_paths(repo, &patterns, prefix, set_opts.skip_checks); 879 } 880 881 + ret = modify_pattern_list(repo, &patterns, set_opts.use_stdin, REPLACE); 882 883 strvec_clear(&patterns); 884 return ret; ··· 896 897 static int sparse_checkout_reapply(int argc, const char **argv, 898 const char *prefix, 899 + struct repository *repo) 900 { 901 static struct option builtin_sparse_checkout_reapply_options[] = { 902 OPT_BOOL(0, "cone", &reapply_opts.cone_mode, ··· 917 builtin_sparse_checkout_reapply_options, 918 builtin_sparse_checkout_reapply_usage, 0); 919 920 + repo_read_index(repo); 921 922 + if (update_modes(repo, &reapply_opts.cone_mode, &reapply_opts.sparse_index)) 923 return 1; 924 925 + return update_working_directory(repo, NULL); 926 + } 927 + 928 + static char const * const builtin_sparse_checkout_clean_usage[] = { 929 + "git sparse-checkout clean [-n|--dry-run]", 930 + NULL 931 + }; 932 + 933 + static int list_file_iterator(const char *path, const void *data) 934 + { 935 + const char *msg = data; 936 + 937 + printf(msg, path); 938 + return 0; 939 + } 940 + 941 + static void list_every_file_in_dir(const char *msg, 942 + const char *directory) 943 + { 944 + struct strbuf path = STRBUF_INIT; 945 + 946 + strbuf_addstr(&path, directory); 947 + for_each_file_in_dir(&path, list_file_iterator, msg); 948 + strbuf_release(&path); 949 + } 950 + 951 + static const char *msg_remove = N_("Removing %s\n"); 952 + static const char *msg_would_remove = N_("Would remove %s\n"); 953 + 954 + static int sparse_checkout_clean(int argc, const char **argv, 955 + const char *prefix, 956 + struct repository *repo) 957 + { 958 + struct strbuf full_path = STRBUF_INIT; 959 + const char *msg = msg_remove; 960 + size_t worktree_len; 961 + int force = 0, dry_run = 0, verbose = 0; 962 + int require_force = 1; 963 + 964 + struct option builtin_sparse_checkout_clean_options[] = { 965 + OPT__DRY_RUN(&dry_run, N_("dry run")), 966 + OPT__FORCE(&force, N_("force"), PARSE_OPT_NOCOMPLETE), 967 + OPT__VERBOSE(&verbose, N_("report each affected file, not just directories")), 968 + OPT_END(), 969 + }; 970 + 971 + setup_work_tree(); 972 + if (!core_apply_sparse_checkout) 973 + die(_("must be in a sparse-checkout to clean directories")); 974 + if (!core_sparse_checkout_cone) 975 + die(_("must be in a cone-mode sparse-checkout to clean directories")); 976 + 977 + argc = parse_options(argc, argv, prefix, 978 + builtin_sparse_checkout_clean_options, 979 + builtin_sparse_checkout_clean_usage, 0); 980 + 981 + repo_config_get_bool(repo, "clean.requireforce", &require_force); 982 + if (require_force && !force && !dry_run) 983 + die(_("for safety, refusing to clean without one of --force or --dry-run")); 984 + 985 + if (dry_run) 986 + msg = msg_would_remove; 987 + 988 + if (repo_read_index(repo) < 0) 989 + die(_("failed to read index")); 990 + 991 + if (convert_to_sparse(repo->index, SPARSE_INDEX_MEMORY_ONLY) || 992 + repo->index->sparse_index == INDEX_EXPANDED) 993 + die(_("failed to convert index to a sparse index; resolve merge conflicts and try again")); 994 + 995 + strbuf_addstr(&full_path, repo->worktree); 996 + strbuf_addch(&full_path, '/'); 997 + worktree_len = full_path.len; 998 + 999 + for (size_t i = 0; i < repo->index->cache_nr; i++) { 1000 + struct cache_entry *ce = repo->index->cache[i]; 1001 + if (!S_ISSPARSEDIR(ce->ce_mode)) 1002 + continue; 1003 + strbuf_setlen(&full_path, worktree_len); 1004 + strbuf_add(&full_path, ce->name, ce->ce_namelen); 1005 + 1006 + if (!is_directory(full_path.buf)) 1007 + continue; 1008 + 1009 + if (verbose) 1010 + list_every_file_in_dir(msg, ce->name); 1011 + else 1012 + printf(msg, ce->name); 1013 + 1014 + if (dry_run <= 0 && 1015 + remove_dir_recursively(&full_path, 0)) 1016 + warning_errno(_("failed to remove '%s'"), ce->name); 1017 + } 1018 + 1019 + strbuf_release(&full_path); 1020 + return 0; 1021 } 1022 1023 static char const * const builtin_sparse_checkout_disable_usage[] = { ··· 1027 1028 static int sparse_checkout_disable(int argc, const char **argv, 1029 const char *prefix, 1030 + struct repository *repo) 1031 { 1032 static struct option builtin_sparse_checkout_disable_options[] = { 1033 OPT_END(), ··· 1055 * are expecting to do that when disabling sparse-checkout. 1056 */ 1057 give_advice_on_expansion = 0; 1058 + repo_read_index(repo); 1059 1060 memset(&pl, 0, sizeof(pl)); 1061 hashmap_init(&pl.recursive_hashmap, pl_hashmap_cmp, NULL, 0); ··· 1066 add_pattern("/*", empty_base, 0, &pl, 0); 1067 1068 prepare_repo_settings(the_repository); 1069 + repo->settings.sparse_index = 0; 1070 1071 + if (update_working_directory(repo, &pl)) 1072 die(_("error while refreshing working directory")); 1073 1074 clear_pattern_list(&pl); 1075 + return set_config(repo, MODE_NO_PATTERNS); 1076 } 1077 1078 static char const * const builtin_sparse_checkout_check_rules_usage[] = { ··· 1087 char *rules_file; 1088 } check_rules_opts; 1089 1090 + static int check_rules(struct repository *repo, 1091 + struct pattern_list *pl, 1092 + int null_terminated) 1093 + { 1094 struct strbuf line = STRBUF_INIT; 1095 struct strbuf unquoted = STRBUF_INIT; 1096 char *path; 1097 int line_terminator = null_terminated ? 0 : '\n'; 1098 strbuf_getline_fn getline_fn = null_terminated ? strbuf_getline_nul 1099 : strbuf_getline; 1100 + repo->index->sparse_checkout_patterns = pl; 1101 while (!getline_fn(&line, stdin)) { 1102 path = line.buf; 1103 if (!null_terminated && line.buf[0] == '"') { ··· 1109 path = unquoted.buf; 1110 } 1111 1112 + if (path_in_sparse_checkout(path, repo->index)) 1113 write_name_quoted(path, stdout, line_terminator); 1114 } 1115 strbuf_release(&line); ··· 1119 } 1120 1121 static int sparse_checkout_check_rules(int argc, const char **argv, const char *prefix, 1122 + struct repository *repo) 1123 { 1124 static struct option builtin_sparse_checkout_check_rules_options[] = { 1125 OPT_BOOL('z', NULL, &check_rules_opts.null_termination, ··· 1158 free(sparse_filename); 1159 } 1160 1161 + ret = check_rules(repo, &pl, check_rules_opts.null_termination); 1162 clear_pattern_list(&pl); 1163 free(check_rules_opts.rules_file); 1164 return ret; ··· 1176 OPT_SUBCOMMAND("set", &fn, sparse_checkout_set), 1177 OPT_SUBCOMMAND("add", &fn, sparse_checkout_add), 1178 OPT_SUBCOMMAND("reapply", &fn, sparse_checkout_reapply), 1179 + OPT_SUBCOMMAND("clean", &fn, sparse_checkout_clean), 1180 OPT_SUBCOMMAND("disable", &fn, sparse_checkout_disable), 1181 OPT_SUBCOMMAND("check-rules", &fn, sparse_checkout_check_rules), 1182 OPT_END(), ··· 1188 1189 repo_config(the_repository, git_default_config, NULL); 1190 1191 + prepare_repo_settings(repo); 1192 + repo->settings.command_requires_full_index = 0; 1193 1194 return fn(argc, argv, prefix, repo); 1195 }
+28
dir.c
··· 30 #include "read-cache-ll.h" 31 #include "setup.h" 32 #include "sparse-index.h" 33 #include "submodule-config.h" 34 #include "symlinks.h" 35 #include "trace2.h" ··· 85 break; 86 } 87 return e; 88 } 89 90 int count_slashes(const char *s)
··· 30 #include "read-cache-ll.h" 31 #include "setup.h" 32 #include "sparse-index.h" 33 + #include "strbuf.h" 34 #include "submodule-config.h" 35 #include "symlinks.h" 36 #include "trace2.h" ··· 86 break; 87 } 88 return e; 89 + } 90 + 91 + int for_each_file_in_dir(struct strbuf *path, file_iterator fn, const void *data) 92 + { 93 + struct dirent *e; 94 + int res = 0; 95 + size_t baselen = path->len; 96 + DIR *dir = opendir(path->buf); 97 + 98 + if (!dir) 99 + return 0; 100 + 101 + while (!res && (e = readdir_skip_dot_and_dotdot(dir)) != NULL) { 102 + unsigned char dtype = get_dtype(e, path, 0); 103 + strbuf_setlen(path, baselen); 104 + strbuf_addstr(path, e->d_name); 105 + 106 + if (dtype == DT_REG) { 107 + res = fn(path->buf, data); 108 + } else if (dtype == DT_DIR) { 109 + strbuf_addch(path, '/'); 110 + res = for_each_file_in_dir(path, fn, data); 111 + } 112 + } 113 + 114 + closedir(dir); 115 + return res; 116 } 117 118 int count_slashes(const char *s)
+14
dir.h
··· 537 int remove_dir_recursively(struct strbuf *path, int flag); 538 539 /* 540 * Tries to remove the path, along with leading empty directories so long as 541 * those empty directories are not startup_info->original_cwd. Ignores 542 * ENOENT.
··· 537 int remove_dir_recursively(struct strbuf *path, int flag); 538 539 /* 540 + * This function pointer type is called on each file discovered in 541 + * for_each_file_in_dir. The iteration stops if this method returns 542 + * non-zero. 543 + */ 544 + typedef int (*file_iterator)(const char *path, const void *data); 545 + 546 + struct strbuf; 547 + /* 548 + * Given a directory path, recursively visit each file within, including 549 + * within subdirectories. 550 + */ 551 + int for_each_file_in_dir(struct strbuf *path, file_iterator fn, const void *data); 552 + 553 + /* 554 * Tries to remove the path, along with leading empty directories so long as 555 * those empty directories are not startup_info->original_cwd. Ignores 556 * ENOENT.
+3 -1
sparse-index.c
··· 32 "Your working directory likely has contents that are outside of\n" \ 33 "your sparse-checkout patterns. Use 'git sparse-checkout list' to\n" \ 34 "see your sparse-checkout definition and compare it to your working\n" \ 35 - "directory contents. Running 'git clean' may assist in this cleanup." 36 37 struct modify_index_context { 38 struct index_state *write;
··· 32 "Your working directory likely has contents that are outside of\n" \ 33 "your sparse-checkout patterns. Use 'git sparse-checkout list' to\n" \ 34 "see your sparse-checkout definition and compare it to your working\n" \ 35 + "directory contents. Cleaning up any merge conflicts or staged\n" \ 36 + "changes before running 'git sparse-checkout clean' or 'git\n" \ 37 + "sparse-checkout reapply' may assist in this cleanup." 38 39 struct modify_index_context { 40 struct index_state *write;
+175
t/t1091-sparse-checkout-builtin.sh
··· 1050 test_cmp expect actual 1051 ' 1052 1053 1054 test_done
··· 1050 test_cmp expect actual 1051 ' 1052 1053 + test_expect_success 'clean' ' 1054 + git -C repo sparse-checkout set --cone deep/deeper1 && 1055 + git -C repo sparse-checkout reapply && 1056 + mkdir -p repo/deep/deeper2 repo/folder1/extra/inside && 1057 + 1058 + # Add untracked files 1059 + touch repo/deep/deeper2/file && 1060 + touch repo/folder1/extra/inside/file && 1061 + 1062 + test_must_fail git -C repo sparse-checkout clean 2>err && 1063 + grep "refusing to clean" err && 1064 + 1065 + git -C repo config clean.requireForce true && 1066 + test_must_fail git -C repo sparse-checkout clean 2>err && 1067 + grep "refusing to clean" err && 1068 + 1069 + cat >expect <<-\EOF && 1070 + Would remove deep/deeper2/ 1071 + Would remove folder1/ 1072 + EOF 1073 + 1074 + git -C repo sparse-checkout clean --dry-run >out && 1075 + test_cmp expect out && 1076 + test_path_exists repo/deep/deeper2 && 1077 + test_path_exists repo/folder1/extra/inside/file && 1078 + 1079 + cat >expect <<-\EOF && 1080 + Would remove deep/deeper2/file 1081 + Would remove folder1/extra/inside/file 1082 + EOF 1083 + 1084 + git -C repo sparse-checkout clean --dry-run --verbose >out && 1085 + test_cmp expect out && 1086 + 1087 + cat >expect <<-\EOF && 1088 + Removing deep/deeper2/ 1089 + Removing folder1/ 1090 + EOF 1091 + 1092 + git -C repo sparse-checkout clean -f >out && 1093 + test_cmp expect out && 1094 + 1095 + test_path_is_missing repo/deep/deeper2 && 1096 + test_path_is_missing repo/folder1 1097 + ' 1098 + 1099 + test_expect_success 'clean with sparse file states' ' 1100 + test_when_finished git reset --hard && 1101 + git -C repo sparse-checkout set --cone deep/deeper1 && 1102 + mkdir repo/folder2 && 1103 + 1104 + # The previous test case checked the -f option, so 1105 + # test the config option in this one. 1106 + git -C repo config clean.requireForce false && 1107 + 1108 + # create an untracked file and a modified file 1109 + touch repo/folder2/file && 1110 + echo dirty >repo/folder2/a && 1111 + 1112 + # First clean/reapply pass will do nothing. 1113 + git -C repo sparse-checkout clean >out && 1114 + test_must_be_empty out && 1115 + test_path_exists repo/folder2/a && 1116 + test_path_exists repo/folder2/file && 1117 + 1118 + git -C repo sparse-checkout reapply 2>err && 1119 + test_grep folder2 err && 1120 + test_path_exists repo/folder2/a && 1121 + test_path_exists repo/folder2/file && 1122 + 1123 + # Now, stage the change to the tracked file. 1124 + git -C repo add --sparse folder2/a && 1125 + 1126 + # Clean will continue not doing anything. 1127 + git -C repo sparse-checkout clean >out && 1128 + test_line_count = 0 out && 1129 + test_path_exists repo/folder2/a && 1130 + test_path_exists repo/folder2/file && 1131 + 1132 + # But we can reapply to remove the staged change. 1133 + git -C repo sparse-checkout reapply 2>err && 1134 + test_grep folder2 err && 1135 + test_path_is_missing repo/folder2/a && 1136 + test_path_exists repo/folder2/file && 1137 + 1138 + # We can clean now. 1139 + cat >expect <<-\EOF && 1140 + Removing folder2/ 1141 + EOF 1142 + git -C repo sparse-checkout clean >out && 1143 + test_cmp expect out && 1144 + test_path_is_missing repo/folder2 && 1145 + 1146 + # At the moment, the file is staged. 1147 + cat >expect <<-\EOF && 1148 + M folder2/a 1149 + EOF 1150 + 1151 + git -C repo status -s >out && 1152 + test_cmp expect out && 1153 + 1154 + # Reapply persists the modified state. 1155 + git -C repo sparse-checkout reapply && 1156 + cat >expect <<-\EOF && 1157 + M folder2/a 1158 + EOF 1159 + git -C repo status -s >out && 1160 + test_cmp expect out && 1161 + 1162 + # Committing the change leads to resolved status. 1163 + git -C repo commit -m "modified" && 1164 + git -C repo status -s >out && 1165 + test_must_be_empty out && 1166 + 1167 + # Repeat, but this time commit before reapplying. 1168 + mkdir repo/folder2/ && 1169 + echo dirtier >repo/folder2/a && 1170 + git -C repo add --sparse folder2/a && 1171 + git -C repo sparse-checkout clean >out && 1172 + test_must_be_empty out && 1173 + test_path_exists repo/folder2/a && 1174 + 1175 + # Committing without reapplying makes it look like a deletion 1176 + # due to no skip-worktree bit. 1177 + git -C repo commit -m "dirtier" && 1178 + git -C repo status -s >out && 1179 + test_must_be_empty out && 1180 + 1181 + git -C repo sparse-checkout reapply && 1182 + git -C repo status -s >out && 1183 + test_must_be_empty out 1184 + ' 1185 + 1186 + test_expect_success 'sparse-checkout operations with merge conflicts' ' 1187 + git clone repo merge && 1188 + 1189 + ( 1190 + cd merge && 1191 + mkdir -p folder1/even/more/dirs && 1192 + echo base >folder1/even/more/dirs/file && 1193 + git add folder1 && 1194 + git commit -m "base" && 1195 + 1196 + git checkout -b right&& 1197 + echo right >folder1/even/more/dirs/file && 1198 + git commit -a -m "right" && 1199 + 1200 + git checkout -b left HEAD~1 && 1201 + echo left >folder1/even/more/dirs/file && 1202 + git commit -a -m "left" && 1203 + 1204 + git checkout -b merge && 1205 + git sparse-checkout set deep/deeper1 && 1206 + 1207 + test_must_fail git merge -m "will-conflict" right && 1208 + 1209 + test_must_fail git sparse-checkout clean -f 2>err && 1210 + grep "failed to convert index to a sparse index" err && 1211 + 1212 + echo merged >folder1/even/more/dirs/file && 1213 + git add --sparse folder1 && 1214 + git merge --continue && 1215 + 1216 + test_path_exists folder1/even/more/dirs/file && 1217 + 1218 + # clean does not remove the file, because the 1219 + # SKIP_WORKTREE bit was not cleared by the merge command. 1220 + git sparse-checkout clean -f >out && 1221 + test_line_count = 0 out && 1222 + test_path_exists folder1/even/more/dirs/file && 1223 + 1224 + git sparse-checkout reapply && 1225 + test_path_is_missing folder1 1226 + ) 1227 + ' 1228 1229 test_done