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 9 SYNOPSIS 10 10 -------- 11 11 [verse] 12 - 'git sparse-checkout' (init | list | set | add | reapply | disable | check-rules) [<options>] 12 + 'git sparse-checkout' (init | list | set | add | reapply | disable | check-rules | clean) [<options>] 13 13 14 14 15 15 DESCRIPTION ··· 110 110 flags, with the same meaning as the flags from the `set` command, in order 111 111 to change which sparsity mode you are using without needing to also respecify 112 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. 113 144 114 145 'disable':: 115 146 Disable the `core.sparseCheckout` config setting, and restore the
+160 -56
builtin/sparse-checkout.c
··· 2 2 #define DISABLE_SIGN_COMPARE_WARNINGS 3 3 4 4 #include "builtin.h" 5 + #include "abspath.h" 5 6 #include "config.h" 6 7 #include "dir.h" 7 8 #include "environment.h" ··· 23 24 static const char *empty_base = ""; 24 25 25 26 static char const * const builtin_sparse_checkout_usage[] = { 26 - N_("git sparse-checkout (init | list | set | add | reapply | disable | check-rules) [<options>]"), 27 + N_("git sparse-checkout (init | list | set | add | reapply | disable | check-rules | clean) [<options>]"), 27 28 NULL 28 29 }; 29 30 ··· 204 205 ensure_full_index(r->index); 205 206 } 206 207 207 - static int update_working_directory(struct pattern_list *pl) 208 + static int update_working_directory(struct repository *r, 209 + struct pattern_list *pl) 208 210 { 209 211 enum update_sparsity_result result; 210 212 struct unpack_trees_options o; 211 213 struct lock_file lock_file = LOCK_INIT; 212 - struct repository *r = the_repository; 213 214 struct pattern_list *old_pl; 214 215 215 216 /* If no branch has been checked out, there are no updates to make. */ ··· 327 328 string_list_clear(&sl, 0); 328 329 } 329 330 330 - static int write_patterns_and_update(struct pattern_list *pl) 331 + static int write_patterns_and_update(struct repository *repo, 332 + struct pattern_list *pl) 331 333 { 332 334 char *sparse_filename; 333 335 FILE *fp; ··· 336 338 337 339 sparse_filename = get_sparse_checkout_filename(); 338 340 339 - if (safe_create_leading_directories(the_repository, sparse_filename)) 341 + if (safe_create_leading_directories(repo, sparse_filename)) 340 342 die(_("failed to create directory for sparse-checkout file")); 341 343 342 344 hold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR); 343 345 344 - result = update_working_directory(pl); 346 + result = update_working_directory(repo, pl); 345 347 if (result) { 346 348 rollback_lock_file(&lk); 347 - update_working_directory(NULL); 349 + update_working_directory(repo, NULL); 348 350 goto out; 349 351 } 350 352 ··· 372 374 MODE_CONE_PATTERNS = 2, 373 375 }; 374 376 375 - static int set_config(enum sparse_checkout_mode mode) 377 + static int set_config(struct repository *repo, 378 + enum sparse_checkout_mode mode) 376 379 { 377 380 /* Update to use worktree config, if not already. */ 378 - if (init_worktree_config(the_repository)) { 381 + if (init_worktree_config(repo)) { 379 382 error(_("failed to initialize worktree config")); 380 383 return 1; 381 384 } 382 385 383 - if (repo_config_set_worktree_gently(the_repository, 386 + if (repo_config_set_worktree_gently(repo, 384 387 "core.sparseCheckout", 385 388 mode ? "true" : "false") || 386 - repo_config_set_worktree_gently(the_repository, 389 + repo_config_set_worktree_gently(repo, 387 390 "core.sparseCheckoutCone", 388 391 mode == MODE_CONE_PATTERNS ? 389 392 "true" : "false")) 390 393 return 1; 391 394 392 395 if (mode == MODE_NO_PATTERNS) 393 - return set_sparse_index_config(the_repository, 0); 396 + return set_sparse_index_config(repo, 0); 394 397 395 398 return 0; 396 399 } ··· 410 413 return MODE_ALL_PATTERNS; 411 414 } 412 415 413 - static int update_modes(int *cone_mode, int *sparse_index) 416 + static int update_modes(struct repository *repo, int *cone_mode, int *sparse_index) 414 417 { 415 418 int mode, record_mode; 416 419 ··· 418 421 record_mode = (*cone_mode != -1) || !core_apply_sparse_checkout; 419 422 420 423 mode = update_cone_mode(cone_mode); 421 - if (record_mode && set_config(mode)) 424 + if (record_mode && set_config(repo, mode)) 422 425 return 1; 423 426 424 427 /* Set sparse-index/non-sparse-index mode if specified */ 425 428 if (*sparse_index >= 0) { 426 - if (set_sparse_index_config(the_repository, *sparse_index) < 0) 429 + if (set_sparse_index_config(repo, *sparse_index) < 0) 427 430 die(_("failed to modify sparse-index config")); 428 431 429 432 /* force an index rewrite */ 430 - repo_read_index(the_repository); 431 - the_repository->index->updated_workdir = 1; 433 + repo_read_index(repo); 434 + repo->index->updated_workdir = 1; 432 435 433 436 if (!*sparse_index) 434 - ensure_full_index(the_repository->index); 437 + ensure_full_index(repo->index); 435 438 } 436 439 437 440 return 0; ··· 448 451 } init_opts; 449 452 450 453 static int sparse_checkout_init(int argc, const char **argv, const char *prefix, 451 - struct repository *repo UNUSED) 454 + struct repository *repo) 452 455 { 453 456 struct pattern_list pl; 454 457 char *sparse_filename; ··· 464 467 }; 465 468 466 469 setup_work_tree(); 467 - repo_read_index(the_repository); 470 + repo_read_index(repo); 468 471 469 472 init_opts.cone_mode = -1; 470 473 init_opts.sparse_index = -1; ··· 473 476 builtin_sparse_checkout_init_options, 474 477 builtin_sparse_checkout_init_usage, 0); 475 478 476 - if (update_modes(&init_opts.cone_mode, &init_opts.sparse_index)) 479 + if (update_modes(repo, &init_opts.cone_mode, &init_opts.sparse_index)) 477 480 return 1; 478 481 479 482 memset(&pl, 0, sizeof(pl)); ··· 485 488 if (res >= 0) { 486 489 free(sparse_filename); 487 490 clear_pattern_list(&pl); 488 - return update_working_directory(NULL); 491 + return update_working_directory(repo, NULL); 489 492 } 490 493 491 - if (repo_get_oid(the_repository, "HEAD", &oid)) { 494 + if (repo_get_oid(repo, "HEAD", &oid)) { 492 495 FILE *fp; 493 496 494 497 /* assume we are in a fresh repo, but update the sparse-checkout file */ 495 - if (safe_create_leading_directories(the_repository, sparse_filename)) 498 + if (safe_create_leading_directories(repo, sparse_filename)) 496 499 die(_("unable to create leading directories of %s"), 497 500 sparse_filename); 498 501 fp = xfopen(sparse_filename, "w"); ··· 511 514 add_pattern("!/*/", empty_base, 0, &pl, 0); 512 515 pl.use_cone_patterns = init_opts.cone_mode; 513 516 514 - return write_patterns_and_update(&pl); 517 + return write_patterns_and_update(repo, &pl); 515 518 } 516 519 517 520 static void insert_recursive_pattern(struct pattern_list *pl, struct strbuf *path) ··· 674 677 add_patterns_from_input(pl, argc, argv, use_stdin ? stdin : NULL); 675 678 } 676 679 677 - static int modify_pattern_list(struct strvec *args, int use_stdin, 680 + static int modify_pattern_list(struct repository *repo, 681 + struct strvec *args, int use_stdin, 678 682 enum modify_type m) 679 683 { 680 684 int result; ··· 696 700 } 697 701 698 702 if (!core_apply_sparse_checkout) { 699 - set_config(MODE_ALL_PATTERNS); 703 + set_config(repo, MODE_ALL_PATTERNS); 700 704 core_apply_sparse_checkout = 1; 701 705 changed_config = 1; 702 706 } 703 707 704 - result = write_patterns_and_update(pl); 708 + result = write_patterns_and_update(repo, pl); 705 709 706 710 if (result && changed_config) 707 - set_config(MODE_NO_PATTERNS); 711 + set_config(repo, MODE_NO_PATTERNS); 708 712 709 713 clear_pattern_list(pl); 710 714 free(pl); 711 715 return result; 712 716 } 713 717 714 - static void sanitize_paths(struct strvec *args, 718 + static void sanitize_paths(struct repository *repo, 719 + struct strvec *args, 715 720 const char *prefix, int skip_checks) 716 721 { 717 722 int i; ··· 752 757 753 758 for (i = 0; i < args->nr; i++) { 754 759 struct cache_entry *ce; 755 - struct index_state *index = the_repository->index; 760 + struct index_state *index = repo->index; 756 761 int pos = index_name_pos(index, args->v[i], strlen(args->v[i])); 757 762 758 763 if (pos < 0) ··· 779 784 } add_opts; 780 785 781 786 static int sparse_checkout_add(int argc, const char **argv, const char *prefix, 782 - struct repository *repo UNUSED) 787 + struct repository *repo) 783 788 { 784 789 static struct option builtin_sparse_checkout_add_options[] = { 785 790 OPT_BOOL_F(0, "skip-checks", &add_opts.skip_checks, ··· 796 801 if (!core_apply_sparse_checkout) 797 802 die(_("no sparse-checkout to add to")); 798 803 799 - repo_read_index(the_repository); 804 + repo_read_index(repo); 800 805 801 806 argc = parse_options(argc, argv, prefix, 802 807 builtin_sparse_checkout_add_options, ··· 804 809 805 810 for (int i = 0; i < argc; i++) 806 811 strvec_push(&patterns, argv[i]); 807 - sanitize_paths(&patterns, prefix, add_opts.skip_checks); 812 + sanitize_paths(repo, &patterns, prefix, add_opts.skip_checks); 808 813 809 - ret = modify_pattern_list(&patterns, add_opts.use_stdin, ADD); 814 + ret = modify_pattern_list(repo, &patterns, add_opts.use_stdin, ADD); 810 815 811 816 strvec_clear(&patterns); 812 817 return ret; ··· 825 830 } set_opts; 826 831 827 832 static int sparse_checkout_set(int argc, const char **argv, const char *prefix, 828 - struct repository *repo UNUSED) 833 + struct repository *repo) 829 834 { 830 835 int default_patterns_nr = 2; 831 836 const char *default_patterns[] = {"/*", "!/*/", NULL}; ··· 847 852 int ret; 848 853 849 854 setup_work_tree(); 850 - repo_read_index(the_repository); 855 + repo_read_index(repo); 851 856 852 857 set_opts.cone_mode = -1; 853 858 set_opts.sparse_index = -1; ··· 856 861 builtin_sparse_checkout_set_options, 857 862 builtin_sparse_checkout_set_usage, 0); 858 863 859 - if (update_modes(&set_opts.cone_mode, &set_opts.sparse_index)) 864 + if (update_modes(repo, &set_opts.cone_mode, &set_opts.sparse_index)) 860 865 return 1; 861 866 862 867 /* ··· 870 875 } else { 871 876 for (int i = 0; i < argc; i++) 872 877 strvec_push(&patterns, argv[i]); 873 - sanitize_paths(&patterns, prefix, set_opts.skip_checks); 878 + sanitize_paths(repo, &patterns, prefix, set_opts.skip_checks); 874 879 } 875 880 876 - ret = modify_pattern_list(&patterns, set_opts.use_stdin, REPLACE); 881 + ret = modify_pattern_list(repo, &patterns, set_opts.use_stdin, REPLACE); 877 882 878 883 strvec_clear(&patterns); 879 884 return ret; ··· 891 896 892 897 static int sparse_checkout_reapply(int argc, const char **argv, 893 898 const char *prefix, 894 - struct repository *repo UNUSED) 899 + struct repository *repo) 895 900 { 896 901 static struct option builtin_sparse_checkout_reapply_options[] = { 897 902 OPT_BOOL(0, "cone", &reapply_opts.cone_mode, ··· 912 917 builtin_sparse_checkout_reapply_options, 913 918 builtin_sparse_checkout_reapply_usage, 0); 914 919 915 - repo_read_index(the_repository); 920 + repo_read_index(repo); 916 921 917 - if (update_modes(&reapply_opts.cone_mode, &reapply_opts.sparse_index)) 922 + if (update_modes(repo, &reapply_opts.cone_mode, &reapply_opts.sparse_index)) 918 923 return 1; 919 924 920 - return update_working_directory(NULL); 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; 921 1021 } 922 1022 923 1023 static char const * const builtin_sparse_checkout_disable_usage[] = { ··· 927 1027 928 1028 static int sparse_checkout_disable(int argc, const char **argv, 929 1029 const char *prefix, 930 - struct repository *repo UNUSED) 1030 + struct repository *repo) 931 1031 { 932 1032 static struct option builtin_sparse_checkout_disable_options[] = { 933 1033 OPT_END(), ··· 955 1055 * are expecting to do that when disabling sparse-checkout. 956 1056 */ 957 1057 give_advice_on_expansion = 0; 958 - repo_read_index(the_repository); 1058 + repo_read_index(repo); 959 1059 960 1060 memset(&pl, 0, sizeof(pl)); 961 1061 hashmap_init(&pl.recursive_hashmap, pl_hashmap_cmp, NULL, 0); ··· 966 1066 add_pattern("/*", empty_base, 0, &pl, 0); 967 1067 968 1068 prepare_repo_settings(the_repository); 969 - the_repository->settings.sparse_index = 0; 1069 + repo->settings.sparse_index = 0; 970 1070 971 - if (update_working_directory(&pl)) 1071 + if (update_working_directory(repo, &pl)) 972 1072 die(_("error while refreshing working directory")); 973 1073 974 1074 clear_pattern_list(&pl); 975 - return set_config(MODE_NO_PATTERNS); 1075 + return set_config(repo, MODE_NO_PATTERNS); 976 1076 } 977 1077 978 1078 static char const * const builtin_sparse_checkout_check_rules_usage[] = { ··· 987 1087 char *rules_file; 988 1088 } check_rules_opts; 989 1089 990 - static int check_rules(struct pattern_list *pl, int null_terminated) { 1090 + static int check_rules(struct repository *repo, 1091 + struct pattern_list *pl, 1092 + int null_terminated) 1093 + { 991 1094 struct strbuf line = STRBUF_INIT; 992 1095 struct strbuf unquoted = STRBUF_INIT; 993 1096 char *path; 994 1097 int line_terminator = null_terminated ? 0 : '\n'; 995 1098 strbuf_getline_fn getline_fn = null_terminated ? strbuf_getline_nul 996 1099 : strbuf_getline; 997 - the_repository->index->sparse_checkout_patterns = pl; 1100 + repo->index->sparse_checkout_patterns = pl; 998 1101 while (!getline_fn(&line, stdin)) { 999 1102 path = line.buf; 1000 1103 if (!null_terminated && line.buf[0] == '"') { ··· 1006 1109 path = unquoted.buf; 1007 1110 } 1008 1111 1009 - if (path_in_sparse_checkout(path, the_repository->index)) 1112 + if (path_in_sparse_checkout(path, repo->index)) 1010 1113 write_name_quoted(path, stdout, line_terminator); 1011 1114 } 1012 1115 strbuf_release(&line); ··· 1016 1119 } 1017 1120 1018 1121 static int sparse_checkout_check_rules(int argc, const char **argv, const char *prefix, 1019 - struct repository *repo UNUSED) 1122 + struct repository *repo) 1020 1123 { 1021 1124 static struct option builtin_sparse_checkout_check_rules_options[] = { 1022 1125 OPT_BOOL('z', NULL, &check_rules_opts.null_termination, ··· 1055 1158 free(sparse_filename); 1056 1159 } 1057 1160 1058 - ret = check_rules(&pl, check_rules_opts.null_termination); 1161 + ret = check_rules(repo, &pl, check_rules_opts.null_termination); 1059 1162 clear_pattern_list(&pl); 1060 1163 free(check_rules_opts.rules_file); 1061 1164 return ret; ··· 1073 1176 OPT_SUBCOMMAND("set", &fn, sparse_checkout_set), 1074 1177 OPT_SUBCOMMAND("add", &fn, sparse_checkout_add), 1075 1178 OPT_SUBCOMMAND("reapply", &fn, sparse_checkout_reapply), 1179 + OPT_SUBCOMMAND("clean", &fn, sparse_checkout_clean), 1076 1180 OPT_SUBCOMMAND("disable", &fn, sparse_checkout_disable), 1077 1181 OPT_SUBCOMMAND("check-rules", &fn, sparse_checkout_check_rules), 1078 1182 OPT_END(), ··· 1084 1188 1085 1189 repo_config(the_repository, git_default_config, NULL); 1086 1190 1087 - prepare_repo_settings(the_repository); 1088 - the_repository->settings.command_requires_full_index = 0; 1191 + prepare_repo_settings(repo); 1192 + repo->settings.command_requires_full_index = 0; 1089 1193 1090 1194 return fn(argc, argv, prefix, repo); 1091 1195 }
+28
dir.c
··· 30 30 #include "read-cache-ll.h" 31 31 #include "setup.h" 32 32 #include "sparse-index.h" 33 + #include "strbuf.h" 33 34 #include "submodule-config.h" 34 35 #include "symlinks.h" 35 36 #include "trace2.h" ··· 85 86 break; 86 87 } 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; 88 116 } 89 117 90 118 int count_slashes(const char *s)
+14
dir.h
··· 537 537 int remove_dir_recursively(struct strbuf *path, int flag); 538 538 539 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 + /* 540 554 * Tries to remove the path, along with leading empty directories so long as 541 555 * those empty directories are not startup_info->original_cwd. Ignores 542 556 * ENOENT.
+3 -1
sparse-index.c
··· 32 32 "Your working directory likely has contents that are outside of\n" \ 33 33 "your sparse-checkout patterns. Use 'git sparse-checkout list' to\n" \ 34 34 "see your sparse-checkout definition and compare it to your working\n" \ 35 - "directory contents. Running 'git clean' may assist in this cleanup." 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." 36 38 37 39 struct modify_index_context { 38 40 struct index_state *write;
+175
t/t1091-sparse-checkout-builtin.sh
··· 1050 1050 test_cmp expect actual 1051 1051 ' 1052 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 + ' 1053 1228 1054 1229 test_done