diff --git a/Documentation/git-status.txt b/Documentation/git-status.txt index 4acf690a8b..e78d745fc7 100644 --- a/Documentation/git-status.txt +++ b/Documentation/git-status.txt @@ -100,6 +100,11 @@ configuration variable documented in linkgit:git-config[1]. --ignored:: Show ignored files as well. +--show-ignored-directory:: + (EXPERIMENTAL) Show directories that are ignored, instead of individual files. + Does not recurse into excluded directories when listing all + untracked files. + -z:: Terminate entries with NUL, instead of LF. This implies the `--porcelain=v1` output format if no other format is given. diff --git a/Documentation/technical/api-directory-listing.txt b/Documentation/technical/api-directory-listing.txt index 6c77b4920c..cb772a2335 100644 --- a/Documentation/technical/api-directory-listing.txt +++ b/Documentation/technical/api-directory-listing.txt @@ -33,6 +33,12 @@ The notable options are: Similar to `DIR_SHOW_IGNORED`, but return ignored files in `ignored[]` in addition to untracked files in `entries[]`. +`DIR_SHOW_IGNORED_DIRECTORY`::: + + (EXPERIMENTAL) If this is set, non-empty directories that match an + ignore pattern are returned. The individual files contained in ignored + directories are not included. + `DIR_KEEP_UNTRACKED_CONTENTS`::: Only has meaning if `DIR_SHOW_IGNORED_TOO` is also set; if this is set, the diff --git a/builtin/commit.c b/builtin/commit.c index 57302f7f1d..789b626fc2 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1334,6 +1334,7 @@ static int git_status_config(const char *k, const char *v, void *cb) int cmd_status(int argc, const char **argv, const char *prefix) { static int no_lock_index = 0; + static int show_ignored_directory = 0; static struct wt_status s; int fd; struct object_id oid; @@ -1365,6 +1366,8 @@ int cmd_status(int argc, const char **argv, const char *prefix) OPT_COLUMN(0, "column", &s.colopts, N_("list untracked files in columns")), OPT_BOOL(0, "no-lock-index", &no_lock_index, N_("do not lock the index")), + OPT_BOOL(0, "show-ignored-directory", &show_ignored_directory, + N_("(EXPERIMENTAL) Only show directories that match an ignore pattern name.")), OPT_END(), }; @@ -1403,6 +1406,7 @@ int cmd_status(int argc, const char **argv, const char *prefix) s.ignore_submodule_arg = ignore_submodule_arg; s.status_format = status_format; s.verbose = verbose; + s.show_ignored_directory = show_ignored_directory; wt_status_collect(&s); diff --git a/dir.c b/dir.c index 00546e563b..c9a2a22896 100644 --- a/dir.c +++ b/dir.c @@ -49,7 +49,7 @@ struct cached_dir { static enum path_treatment read_directory_recursive(struct dir_struct *dir, struct index_state *istate, const char *path, int len, struct untracked_cache_dir *untracked, - int check_only, const struct pathspec *pathspec); + int check_only, int stop_at_first_file, const struct pathspec *pathspec); static int get_dtype(struct dirent *de, struct index_state *istate, const char *path, int len); @@ -1389,6 +1389,21 @@ static enum path_treatment treat_directory(struct dir_struct *dir, case index_nonexistent: if (dir->flags & DIR_SHOW_OTHER_DIRECTORIES) break; + if (exclude && + (dir->flags & DIR_SHOW_IGNORED_TOO) && + (dir->flags & DIR_SHOW_IGNORED_DIRECTORY)) { + + /* + * This is an excluded directory, and we are only + * showing the name of a excluded directory. + * Check to see if there are any contained files + * to determine if the directory is empty or not. + */ + if (read_directory_recursive(dir, istate, dirname, len, + untracked, 1, 1, pathspec) == path_excluded) + return path_excluded; + return path_none; + } if (!(dir->flags & DIR_NO_GITLINKS)) { unsigned char sha1[20]; if (resolve_gitlink_ref(dirname, "HEAD", sha1) == 0) @@ -1398,16 +1413,16 @@ static enum path_treatment treat_directory(struct dir_struct *dir, } /* This is the "show_other_directories" case */ - if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES)) return exclude ? path_excluded : path_untracked; untracked = lookup_untracked(dir->untracked, untracked, dirname + baselen, len - baselen); return read_directory_recursive(dir, istate, dirname, len, - untracked, 1, pathspec); + untracked, 1, 0, pathspec); } + /* * This is an inexact early pruning of any recursive directory * reading - if the path cannot possibly be in the pathspec, @@ -1633,7 +1648,7 @@ static enum path_treatment treat_path_fast(struct dir_struct *dir, * with check_only set. */ return read_directory_recursive(dir, istate, path->buf, path->len, - cdir->ucd, 1, pathspec); + cdir->ucd, 1, 0, pathspec); /* * We get path_recurse in the first run when * directory_exists_in_index() returns index_nonexistent. We @@ -1798,7 +1813,7 @@ static void close_cached_dir(struct cached_dir *cdir) static enum path_treatment read_directory_recursive(struct dir_struct *dir, struct index_state *istate, const char *base, int baselen, struct untracked_cache_dir *untracked, int check_only, - const struct pathspec *pathspec) + int stop_at_first_file, const struct pathspec *pathspec) { struct cached_dir cdir; enum path_treatment state, subdir_state, dir_state = path_none; @@ -1832,12 +1847,32 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir, subdir_state = read_directory_recursive(dir, istate, path.buf, path.len, ud, - check_only, pathspec); + check_only, stop_at_first_file, pathspec); if (subdir_state > dir_state) dir_state = subdir_state; } if (check_only) { + if (stop_at_first_file) { + /* + * In general, if we are stopping at the first found file, + * We can only signal that a path of at least "excluded" was + * found. If the first file we find is "excluded" - there might + * be other untracked files later on that will not be searched. + * + * In current usage of this function, stop_at_first_file will + * only be set when called from a directory that matches the + * exclude pattern - there should be no untracked files - + * all contents should be marked as excluded. + */ + if (dir_state == path_excluded) + break; + else if (dir_state > path_excluded) { + dir_state = path_excluded; + break; + } + } + /* abort early if maximum state has been reached */ if (dir_state == path_untracked) { if (cdir.fdir) @@ -2108,7 +2143,7 @@ int read_directory(struct dir_struct *dir, struct index_state *istate, */ dir->untracked = NULL; if (!len || treat_leading_path(dir, istate, path, len, pathspec)) - read_directory_recursive(dir, istate, path, len, untracked, 0, pathspec); + read_directory_recursive(dir, istate, path, len, untracked, 0, 0, pathspec); QSORT(dir->entries, dir->nr, cmp_dir_entry); QSORT(dir->ignored, dir->ignored_nr, cmp_dir_entry); diff --git a/dir.h b/dir.h index e3717055d1..b59eedf633 100644 --- a/dir.h +++ b/dir.h @@ -152,7 +152,8 @@ struct dir_struct { DIR_COLLECT_IGNORED = 1<<4, DIR_SHOW_IGNORED_TOO = 1<<5, DIR_COLLECT_KILLED_ONLY = 1<<6, - DIR_KEEP_UNTRACKED_CONTENTS = 1<<7 + DIR_KEEP_UNTRACKED_CONTENTS = 1<<7, + DIR_SHOW_IGNORED_DIRECTORY = 1<<8 } flags; struct dir_entry **entries; struct dir_entry **ignored; diff --git a/t/t7520-status-show-ignored-directory.sh b/t/t7520-status-show-ignored-directory.sh new file mode 100755 index 0000000000..856c00e43f --- /dev/null +++ b/t/t7520-status-show-ignored-directory.sh @@ -0,0 +1,149 @@ +#!/bin/sh +# +# + +test_description='git status collapse ignored' + +. ./test-lib.sh + + +cat >.gitignore <<\EOF +*.ign +ignored_dir/ +!*.unignore +EOF + +# commit initial ignore file +test_expect_success 'setup initial commit and ignore file' ' + git add . && + test_tick && + git commit -m "Initial commit" +' + +cat >expect <<\EOF +? expect +? output +! dir/ignored/ignored_1.ign +! dir/ignored/ignored_2.ign +! ignored/ignored_1.ign +! ignored/ignored_2.ign +EOF + +# Test status behavior on folder with ignored files +test_expect_success 'setup folder with ignored files' ' + mkdir -p ignored dir/ignored && + touch ignored/ignored_1.ign ignored/ignored_2.ign \ + dir/ignored/ignored_1.ign dir/ignored/ignored_2.ign +' + +test_expect_success 'Verify behavior of status on folders with ignored files' ' + test_when_finished "git clean -fdx" && + git status --porcelain=v2 --ignored --untracked-files=all --show-ignored-directory >output && + test_i18ncmp expect output +' + +# Test status bahavior on folder with tracked and ignored files +cat >expect <<\EOF +? expect +? output +! dir/tracked_ignored/ignored_1.ign +! dir/tracked_ignored/ignored_2.ign +! tracked_ignored/ignored_1.ign +! tracked_ignored/ignored_2.ign +EOF + +test_expect_success 'setup folder with tracked & ignored files' ' + mkdir -p tracked_ignored dir/tracked_ignored && + touch tracked_ignored/tracked_1 tracked_ignored/tracked_2 \ + tracked_ignored/ignored_1.ign tracked_ignored/ignored_2.ign \ + dir/tracked_ignored/tracked_1 dir/tracked_ignored/tracked_2 \ + dir/tracked_ignored/ignored_1.ign dir/tracked_ignored/ignored_2.ign && + + git add tracked_ignored/tracked_1 tracked_ignored/tracked_2 \ + dir/tracked_ignored/tracked_1 dir/tracked_ignored/tracked_2 && + test_tick && + git commit -m "commit tracked files" +' + +test_expect_success 'Verify status on folder with tracked & ignored files' ' + test_when_finished "git clean -fdx && git reset HEAD~1 --hard" && + git status --porcelain=v2 --ignored --untracked-files=all --show-ignored-directory >output && + test_i18ncmp expect output +' + + +# Test status behavior on folder with untracked and ignored files +cat >expect <<\EOF +? dir/untracked_ignored/untracked_1 +? dir/untracked_ignored/untracked_2 +? expect +? output +? untracked_ignored/untracked_1 +? untracked_ignored/untracked_2 +! dir/untracked_ignored/ignored_1.ign +! dir/untracked_ignored/ignored_2.ign +! untracked_ignored/ignored_1.ign +! untracked_ignored/ignored_2.ign +EOF + +test_expect_success 'setup folder with tracked & ignored files' ' + mkdir -p untracked_ignored dir/untracked_ignored && + touch untracked_ignored/untracked_1 untracked_ignored/untracked_2 \ + untracked_ignored/ignored_1.ign untracked_ignored/ignored_2.ign \ + dir/untracked_ignored/untracked_1 dir/untracked_ignored/untracked_2 \ + dir/untracked_ignored/ignored_1.ign dir/untracked_ignored/ignored_2.ign +' + +test_expect_success 'Verify status on folder with tracked & ignored files' ' + test_when_finished "git clean -fdx" && + git status --porcelain=v2 --ignored --untracked-files=all --show-ignored-directory >output && + test_i18ncmp expect output +' + +# Test status behavior on ignored folder +cat >expect <<\EOF +? expect +? output +! ignored_dir/ +EOF + +test_expect_success 'setup folder with tracked & ignored files' ' + mkdir ignored_dir && + touch ignored_dir/ignored_1 ignored_dir/ignored_2 \ + ignored_dir/ignored_1.ign ignored_dir/ignored_2.ign +' + +test_expect_success 'Verify status on folder with tracked & ignored files' ' + test_when_finished "git clean -fdx" && + git status --porcelain=v2 --ignored --untracked-files=all --show-ignored-directory >output && + test_i18ncmp expect output +' + +# Test status behavior on ignored folder with tracked file +cat >expect <<\EOF +? expect +? output +! ignored_dir/ignored_1 +! ignored_dir/ignored_1.ign +! ignored_dir/ignored_2 +! ignored_dir/ignored_2.ign +EOF + +test_expect_success 'setup folder with tracked & ignored files' ' + mkdir ignored_dir && + touch ignored_dir/ignored_1 ignored_dir/ignored_2 \ + ignored_dir/ignored_1.ign ignored_dir/ignored_2.ign \ + ignored_dir/tracked && + git add -f ignored_dir/tracked && + test_tick && + git commit -m "Force add file in ignored directory" +' + +test_expect_success 'Verify status on folder with tracked & ignored files' ' + test_when_finished "git clean -fdx && git reset HEAD~1 --hard" && + git status --porcelain=v2 --ignored --untracked-files=all --show-ignored-directory >output && + test_i18ncmp expect output +' + +test_done + diff --git a/wt-status.c b/wt-status.c index 77c27c5113..c3d17490a4 100644 --- a/wt-status.c +++ b/wt-status.c @@ -664,6 +664,10 @@ static void wt_status_collect_untracked(struct wt_status *s) dir.flags |= DIR_SHOW_IGNORED_TOO; else dir.untracked = the_index.untracked; + + if (s->show_ignored_directory) + dir.flags |= DIR_SHOW_IGNORED_DIRECTORY; + setup_standard_excludes(&dir); fill_directory(&dir, &the_index, &s->pathspec); diff --git a/wt-status.h b/wt-status.h index 64f4d33ea1..b9c1dffb92 100644 --- a/wt-status.h +++ b/wt-status.h @@ -72,6 +72,7 @@ struct wt_status { int submodule_summary; int show_ignored_files; enum untracked_status_type show_untracked_files; + int show_ignored_directory; const char *ignore_submodule_arg; char color_palette[WT_STATUS_MAXSLOT][COLOR_MAXLEN]; unsigned colopts;