Skip to content

Commit 09487f2

Browse files
newrengitster
authored andcommitted
clean: avoid removing untracked files in a nested git repository
Users expect files in a nested git repository to be left alone unless sufficiently forced (with two -f's). Unfortunately, in certain circumstances, git would delete both tracked (and possibly dirty) files and untracked files within a nested repository. To explain how this happens, let's contrast a couple cases. First, take the following example setup (which assumes we are already within a git repo): git init nested cd nested >tracked git add tracked git commit -m init >untracked cd .. In this setup, everything works as expected; running 'git clean -fd' will result in fill_directory() returning the following paths: nested/ nested/tracked nested/untracked and then correct_untracked_entries() would notice this can be compressed to nested/ and then since "nested/" is a directory, we would call remove_dirs("nested/", ...), which would check is_nonbare_repository_dir() and then decide to skip it. However, if someone also creates an ignored file: >nested/ignored then running 'git clean -fd' would result in fill_directory() returning the same paths: nested/ nested/tracked nested/untracked but correct_untracked_entries() will notice that we had ignored entries under nested/ and thus simplify this list to nested/tracked nested/untracked Since these are not directories, we do not call remove_dirs() which was the only place that had the is_nonbare_repository_dir() safety check -- resulting in us deleting both the untracked file and the tracked (and possibly dirty) file. One possible fix for this issue would be walking the parent directories of each path and checking if they represent nonbare repositories, but that would be wasteful. Even if we added caching of some sort, it's still a waste because we should have been able to check that "nested/" represented a nonbare repository before even descending into it in the first place. Add a DIR_SKIP_NESTED_GIT flag to dir_struct.flags and use it to prevent fill_directory() and friends from descending into nested git repos. With this change, we also modify two regression tests added in commit 91479b9 ("t7300: add tests to document behavior of clean and nested git", 2015-06-15). That commit, nor its series, nor the six previous iterations of that series on the mailing list discussed why those tests coded the expectation they did. In fact, it appears their purpose was simply to test _existing_ behavior to make sure that the performance changes didn't change the behavior. However, these two tests directly contradicted the manpage's claims that two -f's were required to delete files/directories under a nested git repository. While one could argue that the user gave an explicit path which matched files/directories that were within a nested repository, there's a slippery slope that becomes very difficult for users to understand once you go down that route (e.g. what if they specified "git clean -f -d '*.c'"?) It would also be hard to explain what the exact behavior was; avoid such problems by making it really simple. Also, clean up some grammar errors describing this functionality in the git-clean manpage. Finally, there are still a couple bugs with -ffd not cleaning out enough (e.g. missing the nested .git) and with -ffdX possibly cleaning out the wrong files (paying attention to outer .gitignore instead of inner). This patch does not address these cases at all (and does not change the behavior relative to those flags), it only fixes the handling when given a single -f. See https://public-inbox.org/git/[email protected]/ for more discussion of the -ffd[X?] bugs. Signed-off-by: Elijah Newren <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent e86bbcf commit 09487f2

File tree

5 files changed

+22
-9
lines changed

5 files changed

+22
-9
lines changed

Documentation/git-clean.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ OPTIONS
3737
--force::
3838
If the Git configuration variable clean.requireForce is not set
3939
to false, 'git clean' will refuse to delete files or directories
40-
unless given -f or -i. Git will refuse to delete directories
41-
with .git sub directory or file unless a second -f
42-
is given.
40+
unless given -f or -i. Git will refuse to modify untracked
41+
nested git repositories (directories with a .git subdirectory)
42+
unless a second -f is given.
4343

4444
-i::
4545
--interactive::

builtin/clean.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,8 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
946946

947947
if (force > 1)
948948
rm_flags = 0;
949+
else
950+
dir.flags |= DIR_SKIP_NESTED_GIT;
949951

950952
dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
951953

dir.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1451,6 +1451,16 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
14511451
return path_none;
14521452

14531453
case index_nonexistent:
1454+
if (dir->flags & DIR_SKIP_NESTED_GIT) {
1455+
int nested_repo;
1456+
struct strbuf sb = STRBUF_INIT;
1457+
strbuf_addstr(&sb, dirname);
1458+
nested_repo = is_nonbare_repository_dir(&sb);
1459+
strbuf_release(&sb);
1460+
if (nested_repo)
1461+
return path_none;
1462+
}
1463+
14541464
if (dir->flags & DIR_SHOW_OTHER_DIRECTORIES)
14551465
break;
14561466
if (exclude &&

dir.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ struct dir_struct {
156156
DIR_SHOW_IGNORED_TOO = 1<<5,
157157
DIR_COLLECT_KILLED_ONLY = 1<<6,
158158
DIR_KEEP_UNTRACKED_CONTENTS = 1<<7,
159-
DIR_SHOW_IGNORED_TOO_MODE_MATCHING = 1<<8
159+
DIR_SHOW_IGNORED_TOO_MODE_MATCHING = 1<<8,
160+
DIR_SKIP_NESTED_GIT = 1<<9
160161
} flags;
161162
struct dir_entry **entries;
162163
struct dir_entry **ignored;

t/t7300-clean.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ test_expect_failure 'nested (non-empty) bare repositories should be cleaned even
549549
test_path_is_missing strange_bare
550550
'
551551

552-
test_expect_success 'giving path in nested git work tree will remove it' '
552+
test_expect_success 'giving path in nested git work tree will NOT remove it' '
553553
rm -fr repo &&
554554
mkdir repo &&
555555
(
@@ -561,7 +561,7 @@ test_expect_success 'giving path in nested git work tree will remove it' '
561561
git clean -f -d repo/bar/baz &&
562562
test_path_is_file repo/.git/HEAD &&
563563
test_path_is_dir repo/bar/ &&
564-
test_path_is_missing repo/bar/baz
564+
test_path_is_file repo/bar/baz/hello.world
565565
'
566566

567567
test_expect_success 'giving path to nested .git will not remove it' '
@@ -579,7 +579,7 @@ test_expect_success 'giving path to nested .git will not remove it' '
579579
test_path_is_dir untracked/
580580
'
581581

582-
test_expect_success 'giving path to nested .git/ will remove contents' '
582+
test_expect_success 'giving path to nested .git/ will NOT remove contents' '
583583
rm -fr repo untracked &&
584584
mkdir repo untracked &&
585585
(
@@ -589,7 +589,7 @@ test_expect_success 'giving path to nested .git/ will remove contents' '
589589
) &&
590590
git clean -f -d repo/.git/ &&
591591
test_path_is_dir repo/.git &&
592-
test_dir_is_empty repo/.git &&
592+
test_path_is_file repo/.git/HEAD &&
593593
test_path_is_dir untracked/
594594
'
595595

@@ -671,7 +671,7 @@ test_expect_success 'git clean -d skips untracked dirs containing ignored files'
671671
test_path_is_missing foo/b/bb
672672
'
673673

674-
test_expect_failure 'git clean -d skips nested repo containing ignored files' '
674+
test_expect_success 'git clean -d skips nested repo containing ignored files' '
675675
test_when_finished "rm -rf nested-repo-with-ignored-file" &&
676676
677677
git init nested-repo-with-ignored-file &&

0 commit comments

Comments
 (0)