Skip to content

Commit a0f4afb

Browse files
committed
clean: require double -f options to nuke nested git repository and work tree
When you have an embedded git work tree in your work tree (be it an orphaned submodule, or an independent checkout of an unrelated project), "git clean -d -f" blindly descended into it and removed everything. This is rarely what the user wants. Signed-off-by: Junio C Hamano <[email protected]>
1 parent 0a53e9d commit a0f4afb

File tree

6 files changed

+63
-5
lines changed

6 files changed

+63
-5
lines changed

Documentation/git-clean.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ OPTIONS
2727
-------
2828
-d::
2929
Remove untracked directories in addition to untracked files.
30+
If an untracked directory is managed by a different git
31+
repository, it is not removed by default. Use -f option twice
32+
if you really want to remove such a directory.
3033

3134
-f::
3235
If the git configuration specifies clean.requireForce as true,

builtin-clean.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
3131
int i;
3232
int show_only = 0, remove_directories = 0, quiet = 0, ignored = 0;
3333
int ignored_only = 0, baselen = 0, config_set = 0, errors = 0;
34+
int rm_flags = REMOVE_DIR_KEEP_NESTED_GIT;
3435
struct strbuf directory = STRBUF_INIT;
3536
struct dir_struct dir;
3637
static const char **pathspec;
@@ -69,6 +70,9 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
6970
die("clean.requireForce%s set and -n or -f not given; "
7071
"refusing to clean", config_set ? "" : " not");
7172

73+
if (force > 1)
74+
rm_flags = 0;
75+
7276
dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
7377

7478
if (!ignored)
@@ -131,7 +135,8 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
131135
(matches == MATCHED_EXACTLY)) {
132136
if (!quiet)
133137
printf("Removing %s\n", qname);
134-
if (remove_dir_recursively(&directory, 0) != 0) {
138+
if (remove_dir_recursively(&directory,
139+
rm_flags) != 0) {
135140
warning("failed to remove '%s'", qname);
136141
errors++;
137142
}

dir.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -861,12 +861,20 @@ int is_empty_dir(const char *path)
861861
return ret;
862862
}
863863

864-
int remove_dir_recursively(struct strbuf *path, int only_empty)
864+
int remove_dir_recursively(struct strbuf *path, int flag)
865865
{
866-
DIR *dir = opendir(path->buf);
866+
DIR *dir;
867867
struct dirent *e;
868868
int ret = 0, original_len = path->len, len;
869+
int only_empty = (flag & REMOVE_DIR_EMPTY_ONLY);
870+
unsigned char submodule_head[20];
869871

872+
if ((flag & REMOVE_DIR_KEEP_NESTED_GIT) &&
873+
!resolve_gitlink_ref(path->buf, "HEAD", submodule_head))
874+
/* Do not descend and nuke a nested git work tree. */
875+
return 0;
876+
877+
dir = opendir(path->buf);
870878
if (!dir)
871879
return -1;
872880
if (path->buf[original_len - 1] != '/')

dir.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@ static inline int is_dot_or_dotdot(const char *name)
8888
extern int is_empty_dir(const char *dir);
8989

9090
extern void setup_standard_excludes(struct dir_struct *dir);
91-
extern int remove_dir_recursively(struct strbuf *path, int only_empty);
91+
92+
#define REMOVE_DIR_EMPTY_ONLY 01
93+
#define REMOVE_DIR_KEEP_NESTED_GIT 02
94+
extern int remove_dir_recursively(struct strbuf *path, int flag);
9295

9396
/* tries to remove the path with empty directories along it, ignores ENOENT */
9497
extern int remove_path(const char *path);

refs.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,7 @@ static int remove_empty_directories(const char *file)
821821
strbuf_init(&path, 20);
822822
strbuf_addstr(&path, file);
823823

824-
result = remove_dir_recursively(&path, 1);
824+
result = remove_dir_recursively(&path, REMOVE_DIR_EMPTY_ONLY);
825825

826826
strbuf_release(&path);
827827

t/t7300-clean.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,4 +380,43 @@ test_expect_success 'removal failure' '
380380
'
381381
chmod 755 foo
382382

383+
test_expect_success 'nested git work tree' '
384+
rm -fr foo bar &&
385+
mkdir foo bar &&
386+
(
387+
cd foo &&
388+
git init &&
389+
>hello.world
390+
git add . &&
391+
git commit -a -m nested
392+
) &&
393+
(
394+
cd bar &&
395+
>goodbye.people
396+
) &&
397+
git clean -f -d &&
398+
test -f foo/.git/index &&
399+
test -f foo/hello.world &&
400+
! test -d bar
401+
'
402+
403+
test_expect_success 'force removal of nested git work tree' '
404+
rm -fr foo bar &&
405+
mkdir foo bar &&
406+
(
407+
cd foo &&
408+
git init &&
409+
>hello.world
410+
git add . &&
411+
git commit -a -m nested
412+
) &&
413+
(
414+
cd bar &&
415+
>goodbye.people
416+
) &&
417+
git clean -f -f -d &&
418+
! test -d foo &&
419+
! test -d bar
420+
'
421+
383422
test_done

0 commit comments

Comments
 (0)