clean: preserve nested git worktree in subdirectories

remove_dir_recursively() has a check to avoid removing the directory it was asked to remove without recursing into it and report success when the directory is the top level of a working tree of a nested git repository, to protect such a repository from "clean -f" (without double -f). If a working tree of a nested git repository is in a subdirectory of a toplevel project, however, this protection did not apply by mistake; we forgot to pass the REMOVE_DIR_KEEP_NESTED_GIT down to the recursive removal codepath. This requires us to also teach the higher level not to remove the directory it is asked to remove, when the recursed invocation did not remove the directory it was asked to remove due to a nested git repository, as it is not an error to leave the parent directories of such a nested repository. Signed-off-by: Junio C Hamano <gitster@pobox.com>

Junio C Hamano committed Mar 15, 2012 at 01:04 UTC ae2f203ef7be7b33ff5ec61646c8662363eccd62
2 files changed +43 -11
dir.c
+21 -6
@@ -1172,23 +1172,27 @@ int is_empty_dir(const char *path)
1172 return ret;
1173 }
1174
1175 -int remove_dir_recursively(struct strbuf *path, int flag)
1175 +static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
1176 {
1177 DIR *dir;
1178 struct dirent *e;
1179 - int ret = 0, original_len = path->len, len;
1179 + int ret = 0, original_len = path->len, len, kept_down = 0;
1180 int only_empty = (flag & REMOVE_DIR_EMPTY_ONLY);
1181 int keep_toplevel = (flag & REMOVE_DIR_KEEP_TOPLEVEL);
1182 unsigned char submodule_head[20];
1183
1184 if ((flag & REMOVE_DIR_KEEP_NESTED_GIT) &&
1185 - !resolve_gitlink_ref(path->buf, "HEAD", submodule_head))
1185 + !resolve_gitlink_ref(path->buf, "HEAD", submodule_head)) {
1186 /* Do not descend and nuke a nested git work tree. */
1187 + if (kept_up)
1188 + *kept_up = 1;
1189 return 0;
1190 + }
1191
1189 - flag &= ~(REMOVE_DIR_KEEP_TOPLEVEL|REMOVE_DIR_KEEP_NESTED_GIT);
1192 + flag &= ~REMOVE_DIR_KEEP_TOPLEVEL;
1193 dir = opendir(path->buf);
1194 if (!dir) {
1195 + /* an empty dir could be removed even if it is unreadble */
1196 if (!keep_toplevel)
1197 return rmdir(path->buf);
1198 else
@@ -1208,7 +1212,7 @@ int remove_dir_recursively(struct strbuf *path, int flag)
1212 if (lstat(path->buf, &st))
1213 ; /* fall thru */
1214 else if (S_ISDIR(st.st_mode)) {
1211 - if (!remove_dir_recursively(path, flag))
1215 + if (!remove_dir_recurse(path, flag, &kept_down))
1216 continue; /* happy */
1217 } else if (!only_empty && !unlink(path->buf))
1218 continue; /* happy, too */
@@ -1220,11 +1224,22 @@ int remove_dir_recursively(struct strbuf *path, int flag)
1224 closedir(dir);
1225
1226 strbuf_setlen(path, original_len);
1223 - if (!ret && !keep_toplevel)
1227 + if (!ret && !keep_toplevel && !kept_down)
1228 ret = rmdir(path->buf);
1229 + else if (kept_up)
1230 + /*
1231 + * report the uplevel that it is not an error that we
1232 + * did not rmdir() our directory.
1233 + */
1234 + *kept_up = !ret;
1235 return ret;
1236 }
1237
1238 +int remove_dir_recursively(struct strbuf *path, int flag)
1239 +{
1240 + return remove_dir_recurse(path, flag, NULL);
1241 +}
1242 +
1243 void setup_standard_excludes(struct dir_struct *dir)
1244 {
1245 const char *path;
t/t7300-clean.sh
+22 -5
@@ -399,8 +399,8 @@ test_expect_success SANITY 'removal failure' '
399 '
400
401 test_expect_success 'nested git work tree' '
402 - rm -fr foo bar &&
403 - mkdir foo bar &&
402 + rm -fr foo bar baz &&
403 + mkdir -p foo bar baz/boo &&
404 (
405 cd foo &&
406 git init &&
@@ -412,15 +412,24 @@ test_expect_success 'nested git work tree' '
412 cd bar &&
413 >goodbye.people
414 ) &&
415 + (
416 + cd baz/boo &&
417 + git init &&
418 + >deeper.world
419 + git add . &&
420 + git commit -a -m deeply.nested
421 + ) &&
422 git clean -f -d &&
423 test -f foo/.git/index &&
424 test -f foo/hello.world &&
425 + test -f baz/boo/.git/index &&
426 + test -f baz/boo/deeper.world &&
427 ! test -d bar
428 '
429
430 test_expect_success 'force removal of nested git work tree' '
422 - rm -fr foo bar &&
423 - mkdir foo bar &&
431 + rm -fr foo bar baz &&
432 + mkdir -p foo bar baz/boo &&
433 (
434 cd foo &&
435 git init &&
@@ -432,9 +441,17 @@ test_expect_success 'force removal of nested git work tree' '
441 cd bar &&
442 >goodbye.people
443 ) &&
444 + (
445 + cd baz/boo &&
446 + git init &&
447 + >deeper.world
448 + git add . &&
449 + git commit -a -m deeply.nested
450 + ) &&
451 git clean -f -f -d &&
452 ! test -d foo &&
437 - ! test -d bar
453 + ! test -d bar &&
454 + ! test -d baz
455 '
456
457 test_expect_success 'git clean -e' '