@@ -1659,7 +1659,13 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
const char *dirname, int len, int baselen, int excluded,
const struct pathspec *pathspec)
{
- int nested_repo = 0;
+ /*
+ * WARNING: From this function, you can return path_recurse or you
+ * can call read_directory_recursive() (or neither), but
+ * you CAN'T DO BOTH.
+ */
+ enum path_treatment state;
+ int nested_repo = 0, old_ignored_nr, stop_early;
/* The "len-1" is to strip the final '/' */
enum exist_status status = directory_exists_in_index(istate, dirname, len-1);
@@ -1711,18 +1717,101 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
/* This is the "show_other_directories" case */
- if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
+ /*
+ * We only need to recurse into untracked/ignored directories if
+ * either of the following bits is set:
+ * - DIR_SHOW_IGNORED_TOO (because then we need to determine if
+ * there are ignored directories below)
+ * - DIR_HIDE_EMPTY_DIRECTORIES (because we have to determine if
+ * the directory is empty)
+ */
+ if (!(dir->flags & (DIR_SHOW_IGNORED_TOO | DIR_HIDE_EMPTY_DIRECTORIES)))
return excluded ? path_excluded : path_untracked;
+ /*
+ * If we only want to determine if dirname is empty, then we can
+ * stop at the first file we find underneath that directory rather
+ * than continuing to recurse beyond it. If DIR_SHOW_IGNORED_TOO
+ * is set, then we want MORE than just determining if dirname is
+ * empty.
+ */
+ stop_early = ((dir->flags & DIR_HIDE_EMPTY_DIRECTORIES) &&
+ !(dir->flags & DIR_SHOW_IGNORED_TOO));
+
+ /*
+ * If /every/ file within an untracked directory is ignored, then
+ * we want to treat the directory as ignored (for e.g. status
+ * --porcelain), without listing the individual ignored files
+ * underneath. To do so, we'll save the current ignored_nr, and
+ * pop all the ones added after it if it turns out the entire
+ * directory is ignored.
+ */
+ old_ignored_nr = dir->ignored_nr;
+
+ /* Actually recurse into dirname now, we'll fixup the state later. */
untracked = lookup_untracked(dir->untracked, untracked,
dirname + baselen, len - baselen);
+ state = read_directory_recursive(dir, istate, dirname, len, untracked,
+ stop_early, stop_early, pathspec);
+
+ /* There are a variety of reasons we may need to fixup the state... */
+ if (state == path_excluded) {
+ int i;
+
+ /*
+ * When stop_early is set, read_directory_recursive() will
+ * never return path_untracked regardless of whether
+ * underlying paths were untracked or ignored (because
+ * returning early means it excluded some paths, or
+ * something like that -- see commit 5aaa7fd39aaf ("Improve
+ * performance of git status --ignored", 2017-09-18)).
+ * However, we're not really concerned with the status of
+ * files under the directory, we just wanted to know
+ * whether the directory was empty (state == path_none) or
+ * not (state == path_excluded), and if not, we'd return
+ * our original status based on whether the untracked
+ * directory matched an exclusion pattern.
+ */
+ if (stop_early)
+ state = excluded ? path_excluded : path_untracked;
+
+ else {
+ /*
+ * When
+ * !stop_early && state == path_excluded
+ * then all paths under dirname were ignored. For
+ * this case, git status --porcelain wants to just
+ * list the directory itself as ignored and not
+ * list the individual paths underneath. Remove
+ * the individual paths underneath.
+ */
+ for (i = old_ignored_nr + 1; i<dir->ignored_nr; ++i)
+ free(dir->ignored[i]);
+ dir->ignored_nr = old_ignored_nr;
+ }
+ }
/*
- * If this is an excluded directory, then we only need to check if
- * the directory contains any files.
+ * If there is nothing under the current directory and we are not
+ * hiding empty directories, then we need to report on the
+ * untracked or ignored status of the directory itself.
*/
- return read_directory_recursive(dir, istate, dirname, len,
- untracked, 1, excluded, pathspec);
+ if (state == path_none && !(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
+ state = excluded ? path_excluded : path_untracked;
+
+ /*
+ * We can recurse into untracked directories that don't match any
+ * of the given pathspecs when some file underneath the directory
+ * might match one of the pathspecs. If so, we should make sure
+ * to note that the directory itself did not match.
+ */
+ if (pathspec &&
+ !match_pathspec(istate, pathspec, dirname, len,
+ 0 /* prefix */, NULL,
+ 0 /* do NOT special case dirs */))
+ state = path_none;
+
+ return state;
}
/*
@@ -1870,6 +1959,11 @@ static enum path_treatment treat_path_fast(struct dir_struct *dir,
int baselen,
const struct pathspec *pathspec)
{
+ /*
+ * WARNING: From this function, you can return path_recurse or you
+ * can call read_directory_recursive() (or neither), but
+ * you CAN'T DO BOTH.
+ */
strbuf_setlen(path, baselen);
if (!cdir->ucd) {
strbuf_addstr(path, cdir->file);
@@ -2102,7 +2196,7 @@ static int read_cached_dir(struct cached_dir *cdir)
return -1;
}
-static void close_cached_dir(struct cached_dir *cdir)
+static void close_cached_dir(struct cached_dir *cdir, int stop_at_first_file)
{
if (cdir->fdir)
closedir(cdir->fdir);
@@ -2110,7 +2204,7 @@ static void close_cached_dir(struct cached_dir *cdir)
* We have gone through this directory and found no untracked
* entries. Mark it valid.
*/
- if (cdir->untracked) {
+ if (!stop_at_first_file && cdir->untracked) {
cdir->untracked->valid = 1;
cdir->untracked->recurse = 1;
}
@@ -2175,14 +2269,10 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
int stop_at_first_file, const struct pathspec *pathspec)
{
/*
- * WARNING WARNING WARNING:
- *
- * Any updates to the traversal logic here may need corresponding
- * updates in treat_leading_path(). See the commit message for the
- * commit adding this warning as well as the commit preceding it
- * for details.
+ * WARNING: Do NOT recurse unless path_recurse is returned from
+ * treat_path(). Recursing on any other return value
+ * can result in exponential slowdown.
*/
-
struct cached_dir cdir;
enum path_treatment state, subdir_state, dir_state = path_none;
struct strbuf path = STRBUF_INIT;
@@ -2204,13 +2294,7 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
dir_state = state;
/* recurse into subdir if instructed by treat_path */
- if ((state == path_recurse) ||
- ((state == path_untracked) &&
- (resolve_dtype(cdir.d_type, istate, path.buf, path.len) == DT_DIR) &&
- ((dir->flags & DIR_SHOW_IGNORED_TOO) ||
- (pathspec &&
- do_match_pathspec(istate, pathspec, path.buf, path.len,
- baselen, NULL, DO_MATCH_LEADING_PATHSPEC) == MATCHED_RECURSIVELY_LEADING_PATHSPEC)))) {
+ if (state == path_recurse) {
struct untracked_cache_dir *ud;
ud = lookup_untracked(dir->untracked, untracked,
path.buf + baselen,
@@ -2266,7 +2350,7 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
istate, &path, baselen,
pathspec, state);
}
- close_cached_dir(&cdir);
+ close_cached_dir(&cdir, stop_at_first_file);
out:
strbuf_release(&path);
@@ -2294,15 +2378,6 @@ static int treat_leading_path(struct dir_struct *dir,
const char *path, int len,
const struct pathspec *pathspec)
{
- /*
- * WARNING WARNING WARNING:
- *
- * Any updates to the traversal logic here may need corresponding
- * updates in read_directory_recursive(). See 777b420347 (dir:
- * synchronize treat_leading_path() and read_directory_recursive(),
- * 2019-12-19) and its parent commit for details.
- */
-
struct strbuf sb = STRBUF_INIT;
struct strbuf subdir = STRBUF_INIT;
int prevlen, baselen;
@@ -2353,23 +2428,7 @@ static int treat_leading_path(struct dir_struct *dir,
strbuf_reset(&subdir);
strbuf_add(&subdir, path+prevlen, baselen-prevlen);
cdir.d_name = subdir.buf;
- state = treat_path(dir, NULL, &cdir, istate, &sb, prevlen,
- pathspec);
- if (state == path_untracked &&
- resolve_dtype(cdir.d_type, istate, sb.buf, sb.len) == DT_DIR &&
- (dir->flags & DIR_SHOW_IGNORED_TOO ||
- do_match_pathspec(istate, pathspec, sb.buf, sb.len,
- baselen, NULL, DO_MATCH_LEADING_PATHSPEC) == MATCHED_RECURSIVELY_LEADING_PATHSPEC)) {
- if (!match_pathspec(istate, pathspec, sb.buf, sb.len,
- 0 /* prefix */, NULL,
- 0 /* do NOT special case dirs */))
- state = path_none;
- add_path_to_appropriate_result_list(dir, NULL, &cdir,
- istate,
- &sb, baselen,
- pathspec, state);
- state = path_recurse;
- }
+ state = treat_path(dir, NULL, &cdir, istate, &sb, prevlen, pathspec);
if (state != path_recurse)
break; /* do not recurse into it */
@@ -84,10 +84,6 @@ dthree/
dtwo/
three
/done/ 0000000000000000000000000000000000000000 recurse valid
-/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
-three
-/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
-two
EOF
test_expect_success 'status first time (empty cache)' '
@@ -147,10 +143,10 @@ A two
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
-node creation: 0
+node creation: 2
gitignore invalidation: 0
directory invalidation: 1
-opendir: 1
+opendir: 3
EOF
test_cmp ../trace.expect ../trace
@@ -169,10 +165,6 @@ dtwo/
four
three
/done/ 0000000000000000000000000000000000000000 recurse valid
-/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
-three
-/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
-two
EOF
test_cmp ../expect ../actual
'
@@ -194,7 +186,7 @@ A two
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
-node creation: 0
+node creation: 2
gitignore invalidation: 1
directory invalidation: 1
opendir: 4
@@ -216,15 +208,11 @@ dthree/
dtwo/
three
/done/ 0000000000000000000000000000000000000000 recurse valid
-/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
-three
-/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
-two
EOF
test_cmp ../expect ../actual
'
-test_expect_failure 'new info/exclude invalidates everything' '
+test_expect_success 'new info/exclude invalidates everything' '
avoid_racy &&
echo three >>.git/info/exclude &&
: >../trace &&
@@ -240,7 +228,7 @@ A two
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
-node creation: 0
+node creation: 2
gitignore invalidation: 1
directory invalidation: 0
opendir: 4
@@ -248,7 +236,7 @@ EOF
test_cmp ../trace.expect ../trace
'
-test_expect_failure 'verify untracked cache dump' '
+test_expect_success 'verify untracked cache dump' '
test-tool dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
@@ -260,9 +248,6 @@ flags 00000006
dthree/
dtwo/
/done/ 0000000000000000000000000000000000000000 recurse valid
-/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
-/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
-two
EOF
test_cmp ../expect ../actual
'
@@ -277,14 +262,11 @@ exclude_per_dir .gitignore
flags 00000006
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse
/done/ 0000000000000000000000000000000000000000 recurse valid
-/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
-/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
-two
EOF
test_cmp ../expect ../actual
'
-test_expect_failure 'status after the move' '
+test_expect_success 'status after the move' '
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../actual &&
@@ -298,15 +280,15 @@ A one
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
-node creation: 0
+node creation: 2
gitignore invalidation: 0
directory invalidation: 0
-opendir: 1
+opendir: 3
EOF
test_cmp ../trace.expect ../trace
'
-test_expect_failure 'verify untracked cache dump' '
+test_expect_success 'verify untracked cache dump' '
test-tool dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
@@ -319,9 +301,6 @@ dthree/
dtwo/
two
/done/ 0000000000000000000000000000000000000000 recurse valid
-/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
-/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
-two
EOF
test_cmp ../expect ../actual
'
@@ -336,14 +315,11 @@ exclude_per_dir .gitignore
flags 00000006
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse
/done/ 0000000000000000000000000000000000000000 recurse valid
-/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
-/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
-two
EOF
test_cmp ../expect ../actual
'
-test_expect_failure 'status after the move' '
+test_expect_success 'status after the move' '
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../actual &&
@@ -357,15 +333,15 @@ A two
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
-node creation: 0
+node creation: 2
gitignore invalidation: 0
directory invalidation: 0
-opendir: 1
+opendir: 3
EOF
test_cmp ../trace.expect ../trace
'
-test_expect_failure 'verify untracked cache dump' '
+test_expect_success 'verify untracked cache dump' '
test-tool dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
@@ -377,9 +353,6 @@ flags 00000006
dthree/
dtwo/
/done/ 0000000000000000000000000000000000000000 recurse valid
-/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
-/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
-two
EOF
test_cmp ../expect ../actual
'
@@ -392,7 +365,7 @@ test_expect_success 'set up for sparse checkout testing' '
git commit -m "first commit"
'
-test_expect_failure 'status after commit' '
+test_expect_success 'status after commit' '
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../actual &&
@@ -403,15 +376,15 @@ test_expect_failure 'status after commit' '
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
-node creation: 0
+node creation: 2
gitignore invalidation: 0
directory invalidation: 0
-opendir: 2
+opendir: 4
EOF
test_cmp ../trace.expect ../trace
'
-test_expect_failure 'untracked cache correct after commit' '
+test_expect_success 'untracked cache correct after commit' '
test-tool dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
@@ -423,9 +396,6 @@ flags 00000006
dthree/
dtwo/
/done/ 0000000000000000000000000000000000000000 recurse valid
-/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
-/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
-two
EOF
test_cmp ../expect ../actual
'
@@ -450,7 +420,7 @@ test_expect_success 'create/modify files, some of which are gitignored' '
sync_mtime
'
-test_expect_failure 'test sparse status with untracked cache' '
+test_expect_success 'test sparse status with untracked cache' '
: >../trace &&
avoid_racy &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
@@ -464,15 +434,15 @@ test_expect_failure 'test sparse status with untracked cache' '
EOF
test_cmp ../status.expect ../status.actual &&
cat >../trace.expect <<EOF &&
-node creation: 0
+node creation: 2
gitignore invalidation: 1
directory invalidation: 2
-opendir: 2
+opendir: 4
EOF
test_cmp ../trace.expect ../trace
'
-test_expect_failure 'untracked cache correct after status' '
+test_expect_success 'untracked cache correct after status' '
test-tool dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
@@ -485,14 +455,11 @@ dthree/
dtwo/
/done/ 1946f0437f90c5005533cbe1736a6451ca301714 recurse valid
five
-/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
-/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
-two
EOF
test_cmp ../expect ../actual
'
-test_expect_failure 'test sparse status again with untracked cache' '
+test_expect_success 'test sparse status again with untracked cache' '
avoid_racy &&
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
@@ -520,7 +487,7 @@ test_expect_success 'set up for test of subdir and sparse checkouts' '
echo "sub" > done/sub/sub/file
'
-test_expect_failure 'test sparse status with untracked cache and subdir' '
+test_expect_success 'test sparse status with untracked cache and subdir' '
avoid_racy &&
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
@@ -543,7 +510,7 @@ EOF
test_cmp ../trace.expect ../trace
'
-test_expect_failure 'verify untracked cache dump (sparse/subdirs)' '
+test_expect_success 'verify untracked cache dump (sparse/subdirs)' '
test-tool dump-untracked-cache >../actual &&
cat >../expect-from-test-dump <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
@@ -557,18 +524,11 @@ dtwo/
/done/ 1946f0437f90c5005533cbe1736a6451ca301714 recurse valid
five
sub/
-/done/sub/ 0000000000000000000000000000000000000000 recurse check_only valid
-sub/
-/done/sub/sub/ 0000000000000000000000000000000000000000 recurse check_only valid
-file
-/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
-/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
-two
EOF
test_cmp ../expect-from-test-dump ../actual
'
-test_expect_failure 'test sparse status again with untracked cache and subdir' '
+test_expect_success 'test sparse status again with untracked cache and subdir' '
avoid_racy &&
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
@@ -583,7 +543,7 @@ EOF
test_cmp ../trace.expect ../trace
'
-test_expect_failure 'move entry in subdir from untracked to cached' '
+test_expect_success 'move entry in subdir from untracked to cached' '
git add dtwo/two &&
git status --porcelain >../status.actual &&
cat >../status.expect <<EOF &&
@@ -597,7 +557,7 @@ EOF
test_cmp ../status.expect ../status.actual
'
-test_expect_failure 'move entry in subdir from cached to untracked' '
+test_expect_success 'move entry in subdir from cached to untracked' '
git rm --cached dtwo/two &&
git status --porcelain >../status.actual &&
cat >../status.expect <<EOF &&
@@ -624,7 +584,7 @@ test_expect_success 'git status does not change anything' '
test_cmp ../expect-no-uc ../actual
'
-test_expect_failure 'setting core.untrackedCache to true and using git status creates the cache' '
+test_expect_success 'setting core.untrackedCache to true and using git status creates the cache' '
git config core.untrackedCache true &&
test-tool dump-untracked-cache >../actual &&
test_cmp ../expect-no-uc ../actual &&
@@ -657,7 +617,7 @@ test_expect_success 'using --untracked-cache does not fail when core.untrackedCa
test_cmp ../expect-empty ../actual
'
-test_expect_failure 'setting core.untrackedCache to keep' '
+test_expect_success 'setting core.untrackedCache to keep' '
git config core.untrackedCache keep &&
git update-index --untracked-cache &&
test-tool dump-untracked-cache >../actual &&