dir: if our pathspec might match files under a dir, recurse into it

For git clean, if a directory is entirely untracked and the user did not specify -d (corresponding to DIR_SHOW_IGNORED_TOO), then we usually do not want to remove that directory and thus do not recurse into it. However, if the user manually specified specific (or even globbed) paths somewhere under that directory to remove, then we need to recurse into the directory to make sure we remove the relevant paths under that directory as the user requested. Note that this does not mean that the recursed-into directory will be added to dir->entries for later removal; as of a few commits earlier in this series, there is another more strict match check that is run after returning from a recursed-into directory before deciding to add it to the list of entries. Therefore, this will only result in files underneath the given directory which match one of the pathspecs being added to the entries list. Two notes of potential interest to future readers: * If we wanted to only recurse into a directory when it is specifically matched rather than matched-via-glob (e.g. '*.c'), then we could do so via making the final non-zero return in match_pathspec_item be MATCHED_RECURSIVELY instead of MATCHED_RECURSIVELY_LEADING_PATHSPEC. (Note that the relative order of MATCHED_RECURSIVELY_LEADING_PATHSPEC and MATCHED_RECURSIVELY are important for such a change.) I was leaving open that possibility while writing an RFC asking for the behavior we want, but even though we don't want it, that knowledge might help you understand the code flow better. * There is a growing amount of logic in read_directory_recursive() for deciding whether to recurse into a subdirectory. However, there is a comment immediately preceding this logic that says to recurse if instructed by treat_path(). It may be better for the logic in read_directory_recursive() to ultimately be moved to treat_path() (or another function it calls, such as treat_directory()), but I have left that for someone else to tackle in the future. Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Elijah Newren committed Sep 17, 2019 at 09:34 UTC 89a1f4aaf7650288b976c6022b2c5854950d52c6
3 files changed +11 -8
dir.c
+6 -4
@@ -360,7 +360,7 @@ static int match_pathspec_item(const struct index_state *istate,
360 if ((namelen < matchlen) &&
361 (match[namelen-offset] == '/') &&
362 !ps_strncmp(item, match, name, namelen))
363 - return MATCHED_RECURSIVELY;
363 + return MATCHED_RECURSIVELY_LEADING_PATHSPEC;
364
365 /* name" doesn't match up to the first wild character */
366 if (item->nowildcard_len < item->len &&
@@ -377,7 +377,7 @@ static int match_pathspec_item(const struct index_state *istate,
377 * The submodules themselves will be able to perform more
378 * accurate matching to determine if the pathspec matches.
379 */
380 - return MATCHED_RECURSIVELY;
380 + return MATCHED_RECURSIVELY_LEADING_PATHSPEC;
381 }
382
383 return 0;
@@ -1939,8 +1939,10 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
1939 /* recurse into subdir if instructed by treat_path */
1940 if ((state == path_recurse) ||
1941 ((state == path_untracked) &&
1942 - (dir->flags & DIR_SHOW_IGNORED_TOO) &&
1943 - (get_dtype(cdir.de, istate, path.buf, path.len) == DT_DIR))) {
1942 + (get_dtype(cdir.de, istate, path.buf, path.len) == DT_DIR) &&
1943 + ((dir->flags & DIR_SHOW_IGNORED_TOO) ||
1944 + do_match_pathspec(istate, pathspec, path.buf, path.len,
1945 + baselen, NULL, DO_MATCH_LEADING_PATHSPEC) == MATCHED_RECURSIVELY_LEADING_PATHSPEC))) {
1946 struct untracked_cache_dir *ud;
1947 ud = lookup_untracked(dir->untracked, untracked,
1948 path.buf + baselen,
dir.h
+3 -2
@@ -211,8 +211,9 @@ int count_slashes(const char *s);
211 * when populating the seen[] array.
212 */
213 #define MATCHED_RECURSIVELY 1
214 -#define MATCHED_FNMATCH 2
215 -#define MATCHED_EXACTLY 3
214 +#define MATCHED_RECURSIVELY_LEADING_PATHSPEC 2
215 +#define MATCHED_FNMATCH 3
216 +#define MATCHED_EXACTLY 4
217 int simple_length(const char *match);
218 int no_wildcard(const char *string);
219 char *common_prefix(const struct pathspec *pathspec);
t/t7300-clean.sh
+2 -2
@@ -691,7 +691,7 @@ test_expect_failure 'git clean -d skips nested repo containing ignored files' '
691 test_path_is_file nested-repo-with-ignored-file/file
692 '
693
694 -test_expect_failure 'git clean handles being told what to clean' '
694 +test_expect_success 'git clean handles being told what to clean' '
695 mkdir -p d1 d2 &&
696 touch d1/ut d2/ut &&
697 git clean -f */ut &&
@@ -707,7 +707,7 @@ test_expect_success 'git clean handles being told what to clean, with -d' '
707 test_path_is_missing d2/ut
708 '
709
710 -test_expect_failure 'git clean works if a glob is passed without -d' '
710 +test_expect_success 'git clean works if a glob is passed without -d' '
711 mkdir -p d1 d2 &&
712 touch d1/ut d2/ut &&
713 git clean -f "*ut" &&