Git fork

sparse-checkout: match some 'clean' behavior

The 'git sparse-checkout clean' subcommand is somewhat similar to 'git
clean' in that it will delete files that should not be in the worktree.
The big difference is that it focuses on the directories that should not
be in the worktree due to cone-mode sparse-checkout. It also does not
discriminate in the kinds of files and focuses on deleting entire
directories.

However, there are some restrictions that would be good to bring over
from 'git clean', specifically how it refuses to do anything without the
'-f'/'--force' or '-n'/'--dry-run' arguments. The 'clean.requireForce'
config can be set to 'false' to imply '--force'.

Add this behavior to avoid accidental deletion of files that cannot be
recovered from Git.

Signed-off-by: Derrick Stolee <stolee@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>

authored by

Derrick Stolee and committed by
Junio C Hamano
a8077c19 2520efd3

+76 -2
+9
Documentation/git-sparse-checkout.adoc
··· 127 127 This command can be used to be sure the sparse index works efficiently, 128 128 though it does not require enabling the sparse index feature via the 129 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. 130 139 131 140 'disable':: 132 141 Disable the `core.sparseCheckout` config setting, and restore the
+14 -1
builtin/sparse-checkout.c
··· 931 931 }; 932 932 933 933 static const char *msg_remove = N_("Removing %s\n"); 934 + static const char *msg_would_remove = N_("Would remove %s\n"); 934 935 935 936 static int sparse_checkout_clean(int argc, const char **argv, 936 937 const char *prefix, ··· 939 940 struct strbuf full_path = STRBUF_INIT; 940 941 const char *msg = msg_remove; 941 942 size_t worktree_len; 943 + int force = 0, dry_run = 0; 944 + int require_force = 1; 942 945 943 946 struct option builtin_sparse_checkout_clean_options[] = { 947 + OPT__DRY_RUN(&dry_run, N_("dry run")), 948 + OPT__FORCE(&force, N_("force"), PARSE_OPT_NOCOMPLETE), 944 949 OPT_END(), 945 950 }; 946 951 ··· 954 959 builtin_sparse_checkout_clean_options, 955 960 builtin_sparse_checkout_clean_usage, 0); 956 961 962 + repo_config_get_bool(repo, "clean.requireforce", &require_force); 963 + if (require_force && !force && !dry_run) 964 + die(_("for safety, refusing to clean without one of --force or --dry-run")); 965 + 966 + if (dry_run) 967 + msg = msg_would_remove; 968 + 957 969 if (repo_read_index(repo) < 0) 958 970 die(_("failed to read index")); 959 971 ··· 977 989 978 990 printf(msg, ce->name); 979 991 980 - if (remove_dir_recursively(&full_path, 0)) 992 + if (dry_run <= 0 && 993 + remove_dir_recursively(&full_path, 0)) 981 994 warning_errno(_("failed to remove '%s'"), ce->name); 982 995 } 983 996
+53 -1
t/t1091-sparse-checkout-builtin.sh
··· 1059 1059 touch repo/deep/deeper2/file && 1060 1060 touch repo/folder1/file && 1061 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 && 1078 + 1062 1079 cat >expect <<-\EOF && 1063 1080 Removing deep/deeper2/ 1064 1081 Removing folder1/ 1065 1082 EOF 1066 1083 1067 - git -C repo sparse-checkout clean >out && 1084 + git -C repo sparse-checkout clean -f >out && 1068 1085 test_cmp expect out && 1069 1086 1070 1087 test_path_is_missing repo/deep/deeper2 && ··· 1075 1092 test_when_finished git reset --hard && 1076 1093 git -C repo sparse-checkout set --cone deep/deeper1 && 1077 1094 mkdir repo/folder2 && 1095 + 1096 + # The previous test case checked the -f option, so 1097 + # test the config option in this one. 1098 + git -C repo config clean.requireForce false && 1078 1099 1079 1100 # create an untracked file and a modified file 1080 1101 touch repo/folder2/file && ··· 1152 1173 git -C repo sparse-checkout reapply && 1153 1174 git -C repo status -s >out && 1154 1175 test_must_be_empty out 1176 + ' 1177 + 1178 + test_expect_success 'clean with merge conflict status' ' 1179 + git clone repo clean-merge && 1180 + 1181 + echo dirty >clean-merge/deep/deeper2/a && 1182 + touch clean-merge/folder2/extra && 1183 + 1184 + cat >input <<-EOF && 1185 + 0 $ZERO_OID folder1/a 1186 + 100644 $(git -C clean-merge rev-parse HEAD:folder1/a) 1 folder1/a 1187 + EOF 1188 + git -C clean-merge update-index --index-info <input && 1189 + 1190 + git -C clean-merge sparse-checkout set deep/deeper1 && 1191 + 1192 + test_must_fail git -C clean-merge sparse-checkout clean -f 2>err && 1193 + grep "failed to convert index to a sparse index" err && 1194 + 1195 + mkdir -p clean-merge/folder1/ && 1196 + echo merged >clean-merge/folder1/a && 1197 + git -C clean-merge add --sparse folder1/a && 1198 + 1199 + # deletes folder2/ but leaves staged change in folder1 1200 + # and dirty change in deep/deeper2/ 1201 + cat >expect <<-\EOF && 1202 + Removing folder2/ 1203 + EOF 1204 + 1205 + git -C clean-merge sparse-checkout clean -f >out && 1206 + test_cmp expect out 1155 1207 ' 1156 1208 1157 1209 test_done