Skip to content

Commit

Permalink
Merge branch 'en/dir-traversal'
Browse files Browse the repository at this point in the history
"git clean" and "git ls-files -i" had confusion around working on
or showing ignored paths inside an ignored directory, which has
been corrected.

* en/dir-traversal:
  dir: introduce readdir_skip_dot_and_dotdot() helper
  dir: update stale description of treat_directory()
  dir: traverse into untracked directories if they may have ignored subfiles
  dir: avoid unnecessary traversal into ignored directory
  t3001, t7300: add testcase showcasing missed directory traversal
  t7300: add testcase showing unnecessary traversal into ignored directory
  ls-files: error out on -i unless -o or -c are specified
  dir: report number of visited directories and paths with trace2
  dir: convert trace calls to trace2 equivalents
  • Loading branch information
gitster committed May 19, 2021
2 parents 2e2ed74 + b548f0f commit 33be431
Show file tree
Hide file tree
Showing 18 changed files with 298 additions and 172 deletions.
4 changes: 1 addition & 3 deletions builtin/clean.c
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,
strbuf_complete(path, '/');

len = path->len;
while ((e = readdir(dir)) != NULL) {
while ((e = readdir_skip_dot_and_dotdot(dir)) != NULL) {
struct stat st;
if (is_dot_or_dotdot(e->d_name))
continue;

strbuf_setlen(path, len);
strbuf_addstr(path, e->d_name);
Expand Down
3 changes: 3 additions & 0 deletions builtin/ls-files.c
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,9 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
if (pathspec.nr && error_unmatch)
ps_matched = xcalloc(pathspec.nr, 1);

if ((dir.flags & DIR_SHOW_IGNORED) && !show_others && !show_cached)
die("ls-files -i must be used with either -o or -c");

if ((dir.flags & DIR_SHOW_IGNORED) && !exc_given)
die("ls-files --ignored needs some exclude pattern");

Expand Down
4 changes: 1 addition & 3 deletions builtin/worktree.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,8 @@ static void prune_worktrees(void)
struct dirent *d;
if (!dir)
return;
while ((d = readdir(dir)) != NULL) {
while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) {
char *path;
if (is_dot_or_dotdot(d->d_name))
continue;
strbuf_reset(&reason);
if (should_prune_worktree(d->d_name, &reason, &path, expire))
prune_worktree(d->d_name, reason.buf);
Expand Down
5 changes: 2 additions & 3 deletions diff-no-index.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ static int read_directory_contents(const char *path, struct string_list *list)
if (!(dir = opendir(path)))
return error("Could not open directory %s", path);

while ((e = readdir(dir)))
if (!is_dot_or_dotdot(e->d_name))
string_list_insert(list, e->d_name);
while ((e = readdir_skip_dot_and_dotdot(dir)))
string_list_insert(list, e->d_name);

closedir(dir);
return 0;
Expand Down
146 changes: 101 additions & 45 deletions dir.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ void dir_init(struct dir_struct *dir)
memset(dir, 0, sizeof(*dir));
}

struct dirent *
readdir_skip_dot_and_dotdot(DIR *dirp)
{
struct dirent *e;

while ((e = readdir(dirp)) != NULL) {
if (!is_dot_or_dotdot(e->d_name))
break;
}
return e;
}

int count_slashes(const char *s)
{
int cnt = 0;
Expand Down Expand Up @@ -1749,13 +1761,13 @@ static enum exist_status directory_exists_in_index(struct index_state *istate,
* Case 3: if we didn't have it in the index previously, we
* have a few sub-cases:
*
* (a) if "show_other_directories" is true, we show it as
* just a directory, unless "hide_empty_directories" is
* (a) if DIR_SHOW_OTHER_DIRECTORIES flag is set, we show it as
* just a directory, unless DIR_HIDE_EMPTY_DIRECTORIES is
* also true, in which case we need to check if it contains any
* untracked and / or ignored files.
* (b) if it looks like a git directory, and we don't have
* 'no_gitlinks' set we treat it as a gitlink, and show it
* as a directory.
* (b) if it looks like a git directory and we don't have the
* DIR_NO_GITLINKS flag, then we treat it as a gitlink, and
* show it as a directory.
* (c) otherwise, we recurse into it.
*/
static enum path_treatment treat_directory(struct dir_struct *dir,
Expand Down Expand Up @@ -1843,7 +1855,7 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
return path_recurse;
}

/* This is the "show_other_directories" case */
assert(dir->flags & DIR_SHOW_OTHER_DIRECTORIES);

/*
* If we have a pathspec which could match something _below_ this
Expand All @@ -1854,27 +1866,42 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
if (matches_how == MATCHED_RECURSIVELY_LEADING_PATHSPEC)
return path_recurse;

/* Special cases for where this directory is excluded/ignored */
if (excluded) {
/*
* If DIR_SHOW_OTHER_DIRECTORIES is set and we're not
* hiding empty directories, there is no need to
* recurse into an ignored directory.
*/
if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
return path_excluded;

/*
* Even if we are hiding empty directories, we can still avoid
* recursing into ignored directories for DIR_SHOW_IGNORED_TOO
* if DIR_SHOW_IGNORED_TOO_MODE_MATCHING is also set.
*/
if ((dir->flags & DIR_SHOW_IGNORED_TOO) &&
(dir->flags & DIR_SHOW_IGNORED_TOO_MODE_MATCHING))
return path_excluded;
}

/*
* Other than the path_recurse case immediately above, 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 entries below)
* Other than the path_recurse case above, we only need to
* recurse into untracked directories if any of the following
* bits is set:
* - DIR_SHOW_IGNORED (because then we need to determine if
* there are ignored entries below)
* - DIR_SHOW_IGNORED_TOO (same as above)
* - 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;

/*
* ...and even if DIR_SHOW_IGNORED_TOO is set, we can still avoid
* recursing into ignored directories if the path is excluded and
* DIR_SHOW_IGNORED_TOO_MODE_MATCHING is also set.
*/
if (excluded &&
(dir->flags & DIR_SHOW_IGNORED_TOO) &&
(dir->flags & DIR_SHOW_IGNORED_TOO_MODE_MATCHING))
return path_excluded;
if (!excluded &&
!(dir->flags & (DIR_SHOW_IGNORED |
DIR_SHOW_IGNORED_TOO |
DIR_HIDE_EMPTY_DIRECTORIES))) {
return path_untracked;
}

/*
* Even if we don't want to know all the paths under an untracked or
Expand Down Expand Up @@ -2326,7 +2353,7 @@ static int read_cached_dir(struct cached_dir *cdir)
struct dirent *de;

if (cdir->fdir) {
de = readdir(cdir->fdir);
de = readdir_skip_dot_and_dotdot(cdir->fdir);
if (!de) {
cdir->d_name = NULL;
cdir->d_type = DT_UNKNOWN;
Expand Down Expand Up @@ -2440,6 +2467,7 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,

if (open_cached_dir(&cdir, dir, untracked, istate, &path, check_only))
goto out;
dir->visited_directories++;

if (untracked)
untracked->check_only = !!check_only;
Expand All @@ -2448,6 +2476,7 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
/* check how the file or directory should be treated */
state = treat_path(dir, untracked, &cdir, istate, &path,
baselen, pathspec);
dir->visited_paths++;

if (state > dir_state)
dir_state = state;
Expand Down Expand Up @@ -2760,15 +2789,53 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d
return root;
}

static void emit_traversal_statistics(struct dir_struct *dir,
struct repository *repo,
const char *path,
int path_len)
{
if (!trace2_is_enabled())
return;

if (!path_len) {
trace2_data_string("read_directory", repo, "path", "");
} else {
struct strbuf tmp = STRBUF_INIT;
strbuf_add(&tmp, path, path_len);
trace2_data_string("read_directory", repo, "path", tmp.buf);
strbuf_release(&tmp);
}

trace2_data_intmax("read_directory", repo,
"directories-visited", dir->visited_directories);
trace2_data_intmax("read_directory", repo,
"paths-visited", dir->visited_paths);

if (!dir->untracked)
return;
trace2_data_intmax("read_directory", repo,
"node-creation", dir->untracked->dir_created);
trace2_data_intmax("read_directory", repo,
"gitignore-invalidation",
dir->untracked->gitignore_invalidated);
trace2_data_intmax("read_directory", repo,
"directory-invalidation",
dir->untracked->dir_invalidated);
trace2_data_intmax("read_directory", repo,
"opendir", dir->untracked->dir_opened);
}

int read_directory(struct dir_struct *dir, struct index_state *istate,
const char *path, int len, const struct pathspec *pathspec)
{
struct untracked_cache_dir *untracked;

trace_performance_enter();
trace2_region_enter("dir", "read_directory", istate->repo);
dir->visited_paths = 0;
dir->visited_directories = 0;

if (has_symlink_leading_path(path, len)) {
trace_performance_leave("read directory %.*s", len, path);
trace2_region_leave("dir", "read_directory", istate->repo);
return dir->nr;
}

Expand All @@ -2784,23 +2851,15 @@ int read_directory(struct dir_struct *dir, struct index_state *istate,
QSORT(dir->entries, dir->nr, cmp_dir_entry);
QSORT(dir->ignored, dir->ignored_nr, cmp_dir_entry);

trace_performance_leave("read directory %.*s", len, path);
emit_traversal_statistics(dir, istate->repo, path, len);

trace2_region_leave("dir", "read_directory", istate->repo);
if (dir->untracked) {
static int force_untracked_cache = -1;
static struct trace_key trace_untracked_stats = TRACE_KEY_INIT(UNTRACKED_STATS);

if (force_untracked_cache < 0)
force_untracked_cache =
git_env_bool("GIT_FORCE_UNTRACKED_CACHE", 0);
trace_printf_key(&trace_untracked_stats,
"node creation: %u\n"
"gitignore invalidation: %u\n"
"directory invalidation: %u\n"
"opendir: %u\n",
dir->untracked->dir_created,
dir->untracked->gitignore_invalidated,
dir->untracked->dir_invalidated,
dir->untracked->dir_opened);
if (force_untracked_cache &&
dir->untracked == istate->untracked &&
(dir->untracked->dir_opened ||
Expand All @@ -2811,6 +2870,7 @@ int read_directory(struct dir_struct *dir, struct index_state *istate,
FREE_AND_NULL(dir->untracked);
}
}

return dir->nr;
}

Expand Down Expand Up @@ -2892,11 +2952,9 @@ int is_empty_dir(const char *path)
if (!dir)
return 0;

while ((e = readdir(dir)) != NULL)
if (!is_dot_or_dotdot(e->d_name)) {
ret = 0;
break;
}
e = readdir_skip_dot_and_dotdot(dir);
if (e)
ret = 0;

closedir(dir);
return ret;
Expand Down Expand Up @@ -2936,10 +2994,8 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
strbuf_complete(path, '/');

len = path->len;
while ((e = readdir(dir)) != NULL) {
while ((e = readdir_skip_dot_and_dotdot(dir)) != NULL) {
struct stat st;
if (is_dot_or_dotdot(e->d_name))
continue;

strbuf_setlen(path, len);
strbuf_addstr(path, e->d_name);
Expand Down
6 changes: 6 additions & 0 deletions dir.h
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,14 @@ struct dir_struct {
struct oid_stat ss_info_exclude;
struct oid_stat ss_excludes_file;
unsigned unmanaged_exclude_files;

/* Stats about the traversal */
unsigned visited_paths;
unsigned visited_directories;
};

struct dirent *readdir_skip_dot_and_dotdot(DIR *dirp);

/*Count the number of slashes for string s*/
int count_slashes(const char *s);

Expand Down
5 changes: 1 addition & 4 deletions entry.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,9 @@ static void remove_subtree(struct strbuf *path)

if (!dir)
die_errno("cannot opendir '%s'", path->buf);
while ((de = readdir(dir)) != NULL) {
while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {
struct stat st;

if (is_dot_or_dotdot(de->d_name))
continue;

strbuf_addch(path, '/');
strbuf_addstr(path, de->d_name);
if (lstat(path->buf, &st))
Expand Down
5 changes: 1 addition & 4 deletions notes-merge.c
Original file line number Diff line number Diff line change
Expand Up @@ -695,13 +695,10 @@ int notes_merge_commit(struct notes_merge_options *o,

strbuf_addch(&path, '/');
baselen = path.len;
while ((e = readdir(dir)) != NULL) {
while ((e = readdir_skip_dot_and_dotdot(dir)) != NULL) {
struct stat st;
struct object_id obj_oid, blob_oid;

if (is_dot_or_dotdot(e->d_name))
continue;

if (get_oid_hex(e->d_name, &obj_oid)) {
if (o->verbosity >= 3)
printf("Skipping non-SHA1 entry '%s%s'\n",
Expand Down
4 changes: 1 addition & 3 deletions object-file.c
Original file line number Diff line number Diff line change
Expand Up @@ -2350,10 +2350,8 @@ int for_each_file_in_obj_subdir(unsigned int subdir_nr,
strbuf_addch(path, '/');
baselen = path->len;

while ((de = readdir(dir))) {
while ((de = readdir_skip_dot_and_dotdot(dir))) {
size_t namelen;
if (is_dot_or_dotdot(de->d_name))
continue;

namelen = strlen(de->d_name);
strbuf_setlen(path, baselen);
Expand Down
5 changes: 1 addition & 4 deletions packfile.c
Original file line number Diff line number Diff line change
Expand Up @@ -813,10 +813,7 @@ void for_each_file_in_pack_dir(const char *objdir,
}
strbuf_addch(&path, '/');
dirnamelen = path.len;
while ((de = readdir(dir)) != NULL) {
if (is_dot_or_dotdot(de->d_name))
continue;

while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {
strbuf_setlen(&path, dirnamelen);
strbuf_addstr(&path, de->d_name);

Expand Down
4 changes: 1 addition & 3 deletions rerere.c
Original file line number Diff line number Diff line change
Expand Up @@ -1190,13 +1190,11 @@ void rerere_gc(struct repository *r, struct string_list *rr)
if (!dir)
die_errno(_("unable to open rr-cache directory"));
/* Collect stale conflict IDs ... */
while ((e = readdir(dir))) {
while ((e = readdir_skip_dot_and_dotdot(dir))) {
struct rerere_dir *rr_dir;
struct rerere_id id;
int now_empty;

if (is_dot_or_dotdot(e->d_name))
continue;
if (!is_rr_cache_dirname(e->d_name))
continue; /* or should we remove e->d_name? */

Expand Down
2 changes: 1 addition & 1 deletion t/t1306-xdg-files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ test_expect_success 'Exclusion in a non-XDG global ignore file' '
test_expect_success 'Checking XDG ignore file when HOME is unset' '
(sane_unset HOME &&
git config --unset core.excludesfile &&
git ls-files --exclude-standard --ignored >actual) &&
git ls-files --exclude-standard --ignored --others >actual) &&
test_must_be_empty actual
'

Expand Down
5 changes: 5 additions & 0 deletions t/t3001-ls-files-others-exclude.sh
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ EOF
test_cmp expect actual
'

test_expect_success 'ls-files with "**" patterns and --directory' '
# Expectation same as previous test
git ls-files --directory -o -i --exclude "**/a.1" >actual &&
test_cmp expect actual
'

test_expect_success 'ls-files with "**" patterns and no slashes' '
git ls-files -o -i --exclude "one**a.1" >actual &&
Expand Down
Loading

0 comments on commit 33be431

Please sign in to comment.