Skip to content

Commit f4683b4

Browse files
committed
Merge branch 'sl/clean-d-ignored-fix' into maint
"git clean -d" used to clean directories that has ignored files, even though the command should not lose ignored ones without "-x". "git status --ignored" did not list ignored and untracked files without "-uall". These have been corrected. * sl/clean-d-ignored-fix: clean: teach clean -d to preserve ignored paths dir: expose cmp_name() and check_contains() dir: hide untracked contents of untracked dirs dir: recurse into untracked dirs for ignored files t7061: status --ignored should search untracked dirs t7300: clean -d should skip dirs with ignored files
2 parents f381e42 + 6b1db43 commit f4683b4

File tree

6 files changed

+109
-5
lines changed

6 files changed

+109
-5
lines changed

Documentation/technical/api-directory-listing.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ The notable options are:
3333
Similar to `DIR_SHOW_IGNORED`, but return ignored files in `ignored[]`
3434
in addition to untracked files in `entries[]`.
3535

36+
`DIR_KEEP_UNTRACKED_CONTENTS`:::
37+
38+
Only has meaning if `DIR_SHOW_IGNORED_TOO` is also set; if this is set, the
39+
untracked contents of untracked directories are also returned in
40+
`entries[]`.
41+
3642
`DIR_COLLECT_IGNORED`:::
3743

3844
Special mode for git-add. Return ignored files in `ignored[]` and

builtin/clean.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,38 @@ static void interactive_main_loop(void)
857857
}
858858
}
859859

860+
static void correct_untracked_entries(struct dir_struct *dir)
861+
{
862+
int src, dst, ign;
863+
864+
for (src = dst = ign = 0; src < dir->nr; src++) {
865+
/* skip paths in ignored[] that cannot be inside entries[src] */
866+
while (ign < dir->ignored_nr &&
867+
0 <= cmp_dir_entry(&dir->entries[src], &dir->ignored[ign]))
868+
ign++;
869+
870+
if (ign < dir->ignored_nr &&
871+
check_dir_entry_contains(dir->entries[src], dir->ignored[ign])) {
872+
/* entries[src] contains an ignored path, so we drop it */
873+
free(dir->entries[src]);
874+
} else {
875+
struct dir_entry *ent = dir->entries[src++];
876+
877+
/* entries[src] does not contain an ignored path, so we keep it */
878+
dir->entries[dst++] = ent;
879+
880+
/* then discard paths in entries[] contained inside entries[src] */
881+
while (src < dir->nr &&
882+
check_dir_entry_contains(ent, dir->entries[src]))
883+
free(dir->entries[src++]);
884+
885+
/* compensate for the outer loop's loop control */
886+
src--;
887+
}
888+
}
889+
dir->nr = dst;
890+
}
891+
860892
int cmd_clean(int argc, const char **argv, const char *prefix)
861893
{
862894
int i, res;
@@ -916,6 +948,9 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
916948

917949
dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
918950

951+
if (remove_directories)
952+
dir.flags |= DIR_SHOW_IGNORED_TOO | DIR_KEEP_UNTRACKED_CONTENTS;
953+
919954
if (read_cache() < 0)
920955
die(_("index file corrupt"));
921956

@@ -931,6 +966,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
931966
prefix, argv);
932967

933968
fill_directory(&dir, &pathspec);
969+
correct_untracked_entries(&dir);
934970

935971
for (i = 0; i < dir.nr; i++) {
936972
struct dir_entry *ent = dir.entries[i];
@@ -958,6 +994,12 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
958994
string_list_append(&del_list, rel);
959995
}
960996

997+
for (i = 0; i < dir.nr; i++)
998+
free(dir.entries[i]);
999+
1000+
for (i = 0; i < dir.ignored_nr; i++)
1001+
free(dir.ignored[i]);
1002+
9611003
if (interactive && del_list.nr > 0)
9621004
interactive_main_loop();
9631005

dir.c

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,7 +1784,10 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
17841784
dir_state = state;
17851785

17861786
/* recurse into subdir if instructed by treat_path */
1787-
if (state == path_recurse) {
1787+
if ((state == path_recurse) ||
1788+
((state == path_untracked) &&
1789+
(dir->flags & DIR_SHOW_IGNORED_TOO) &&
1790+
(get_dtype(cdir.de, path.buf, path.len) == DT_DIR))) {
17881791
struct untracked_cache_dir *ud;
17891792
ud = lookup_untracked(dir->untracked, untracked,
17901793
path.buf + baselen,
@@ -1839,14 +1842,22 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
18391842
return dir_state;
18401843
}
18411844

1842-
static int cmp_name(const void *p1, const void *p2)
1845+
int cmp_dir_entry(const void *p1, const void *p2)
18431846
{
18441847
const struct dir_entry *e1 = *(const struct dir_entry **)p1;
18451848
const struct dir_entry *e2 = *(const struct dir_entry **)p2;
18461849

18471850
return name_compare(e1->name, e1->len, e2->name, e2->len);
18481851
}
18491852

1853+
/* check if *out lexically strictly contains *in */
1854+
int check_dir_entry_contains(const struct dir_entry *out, const struct dir_entry *in)
1855+
{
1856+
return (out->len < in->len) &&
1857+
(out->name[out->len - 1] == '/') &&
1858+
!memcmp(out->name, in->name, out->len);
1859+
}
1860+
18501861
static int treat_leading_path(struct dir_struct *dir,
18511862
const char *path, int len,
18521863
const struct pathspec *pathspec)
@@ -2060,8 +2071,32 @@ int read_directory(struct dir_struct *dir, const char *path,
20602071
dir->untracked = NULL;
20612072
if (!len || treat_leading_path(dir, path, len, pathspec))
20622073
read_directory_recursive(dir, path, len, untracked, 0, pathspec);
2063-
QSORT(dir->entries, dir->nr, cmp_name);
2064-
QSORT(dir->ignored, dir->ignored_nr, cmp_name);
2074+
QSORT(dir->entries, dir->nr, cmp_dir_entry);
2075+
QSORT(dir->ignored, dir->ignored_nr, cmp_dir_entry);
2076+
2077+
/*
2078+
* If DIR_SHOW_IGNORED_TOO is set, read_directory_recursive() will
2079+
* also pick up untracked contents of untracked dirs; by default
2080+
* we discard these, but given DIR_KEEP_UNTRACKED_CONTENTS we do not.
2081+
*/
2082+
if ((dir->flags & DIR_SHOW_IGNORED_TOO) &&
2083+
!(dir->flags & DIR_KEEP_UNTRACKED_CONTENTS)) {
2084+
int i, j;
2085+
2086+
/* remove from dir->entries untracked contents of untracked dirs */
2087+
for (i = j = 0; j < dir->nr; j++) {
2088+
if (i &&
2089+
check_dir_entry_contains(dir->entries[i - 1], dir->entries[j])) {
2090+
free(dir->entries[j]);
2091+
dir->entries[j] = NULL;
2092+
} else {
2093+
dir->entries[i++] = dir->entries[j];
2094+
}
2095+
}
2096+
2097+
dir->nr = i;
2098+
}
2099+
20652100
if (dir->untracked) {
20662101
static struct trace_key trace_untracked_stats = TRACE_KEY_INIT(UNTRACKED_STATS);
20672102
trace_printf_key(&trace_untracked_stats,

dir.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ struct dir_struct {
151151
DIR_NO_GITLINKS = 1<<3,
152152
DIR_COLLECT_IGNORED = 1<<4,
153153
DIR_SHOW_IGNORED_TOO = 1<<5,
154-
DIR_COLLECT_KILLED_ONLY = 1<<6
154+
DIR_COLLECT_KILLED_ONLY = 1<<6,
155+
DIR_KEEP_UNTRACKED_CONTENTS = 1<<7
155156
} flags;
156157
struct dir_entry **entries;
157158
struct dir_entry **ignored;
@@ -326,6 +327,9 @@ static inline int dir_path_match(const struct dir_entry *ent,
326327
has_trailing_dir);
327328
}
328329

330+
int cmp_dir_entry(const void *p1, const void *p2);
331+
int check_dir_entry_contains(const struct dir_entry *out, const struct dir_entry *in);
332+
329333
void untracked_cache_invalidate_path(struct index_state *, const char *);
330334
void untracked_cache_remove_from_index(struct index_state *, const char *);
331335
void untracked_cache_add_to_index(struct index_state *, const char *);

t/t7061-wtstatus-ignore.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ cat >expected <<\EOF
99
?? actual
1010
?? expected
1111
?? untracked/
12+
!! untracked/ignored
1213
EOF
1314

1415
test_expect_success 'status untracked directory with --ignored' '

t/t7300-clean.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,4 +653,20 @@ test_expect_success 'git clean -d respects pathspecs (pathspec is prefix of dir)
653653
test_path_is_dir foobar
654654
'
655655

656+
test_expect_success 'git clean -d skips untracked dirs containing ignored files' '
657+
echo /foo/bar >.gitignore &&
658+
echo ignoreme >>.gitignore &&
659+
rm -rf foo &&
660+
mkdir -p foo/a/aa/aaa foo/b/bb/bbb &&
661+
touch foo/bar foo/baz foo/a/aa/ignoreme foo/b/ignoreme foo/b/bb/1 foo/b/bb/2 &&
662+
git clean -df &&
663+
test_path_is_dir foo &&
664+
test_path_is_file foo/bar &&
665+
test_path_is_missing foo/baz &&
666+
test_path_is_file foo/a/aa/ignoreme &&
667+
test_path_is_missing foo/a/aa/aaa &&
668+
test_path_is_file foo/b/ignoreme &&
669+
test_path_is_missing foo/b/bb
670+
'
671+
656672
test_done

0 commit comments

Comments
 (0)