Merge branch 'sl/clean-d-ignored-fix' into maint
authorJunio C Hamano <gitster@pobox.com>
Tue, 13 Jun 2017 20:27:01 +0000 (13:27 -0700)
committerJunio C Hamano <gitster@pobox.com>
Tue, 13 Jun 2017 20:27:02 +0000 (13:27 -0700)
"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

Documentation/technical/api-directory-listing.txt
builtin/clean.c
dir.c
dir.h
t/t7061-wtstatus-ignore.sh
t/t7300-clean.sh
index 7f8e78d916c0b38ed6757aee6578a53cab30d9d0..6c77b4920c92a0b327fb88d2b461ec0d918ec99a 100644 (file)
@@ -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_KEEP_UNTRACKED_CONTENTS`:::
+
+       Only has meaning if `DIR_SHOW_IGNORED_TOO` is also set; if this is set, the
+       untracked contents of untracked directories are also returned in
+       `entries[]`.
+
 `DIR_COLLECT_IGNORED`:::
 
        Special mode for git-add. Return ignored files in `ignored[]` and
index d861f836a29bf98d248f27632a8c47711a5da3a5..937eb17b66ca8984a040e32b575eca43cfccb0e2 100644 (file)
@@ -857,6 +857,38 @@ static void interactive_main_loop(void)
        }
 }
 
+static void correct_untracked_entries(struct dir_struct *dir)
+{
+       int src, dst, ign;
+
+       for (src = dst = ign = 0; src < dir->nr; src++) {
+               /* skip paths in ignored[] that cannot be inside entries[src] */
+               while (ign < dir->ignored_nr &&
+                      0 <= cmp_dir_entry(&dir->entries[src], &dir->ignored[ign]))
+                       ign++;
+
+               if (ign < dir->ignored_nr &&
+                   check_dir_entry_contains(dir->entries[src], dir->ignored[ign])) {
+                       /* entries[src] contains an ignored path, so we drop it */
+                       free(dir->entries[src]);
+               } else {
+                       struct dir_entry *ent = dir->entries[src++];
+
+                       /* entries[src] does not contain an ignored path, so we keep it */
+                       dir->entries[dst++] = ent;
+
+                       /* then discard paths in entries[] contained inside entries[src] */
+                       while (src < dir->nr &&
+                              check_dir_entry_contains(ent, dir->entries[src]))
+                               free(dir->entries[src++]);
+
+                       /* compensate for the outer loop's loop control */
+                       src--;
+               }
+       }
+       dir->nr = dst;
+}
+
 int cmd_clean(int argc, const char **argv, const char *prefix)
 {
        int i, res;
@@ -916,6 +948,9 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
 
        dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
 
+       if (remove_directories)
+               dir.flags |= DIR_SHOW_IGNORED_TOO | DIR_KEEP_UNTRACKED_CONTENTS;
+
        if (read_cache() < 0)
                die(_("index file corrupt"));
 
@@ -931,6 +966,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
                       prefix, argv);
 
        fill_directory(&dir, &pathspec);
+       correct_untracked_entries(&dir);
 
        for (i = 0; i < dir.nr; i++) {
                struct dir_entry *ent = dir.entries[i];
@@ -958,6 +994,12 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
                string_list_append(&del_list, rel);
        }
 
+       for (i = 0; i < dir.nr; i++)
+               free(dir.entries[i]);
+
+       for (i = 0; i < dir.ignored_nr; i++)
+               free(dir.ignored[i]);
+
        if (interactive && del_list.nr > 0)
                interactive_main_loop();
 
diff --git a/dir.c b/dir.c
index f451bfa48c0a0e7905d4c2adf4e3e05a8d272a8a..31f9343f9fcf57a89cab519f284cf2e0a767744d 100644 (file)
--- a/dir.c
+++ b/dir.c
@@ -1784,7 +1784,10 @@ 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) {
+               if ((state == path_recurse) ||
+                       ((state == path_untracked) &&
+                        (dir->flags & DIR_SHOW_IGNORED_TOO) &&
+                        (get_dtype(cdir.de, path.buf, path.len) == DT_DIR))) {
                        struct untracked_cache_dir *ud;
                        ud = lookup_untracked(dir->untracked, untracked,
                                              path.buf + baselen,
@@ -1839,7 +1842,7 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
        return dir_state;
 }
 
-static int cmp_name(const void *p1, const void *p2)
+int cmp_dir_entry(const void *p1, const void *p2)
 {
        const struct dir_entry *e1 = *(const struct dir_entry **)p1;
        const struct dir_entry *e2 = *(const struct dir_entry **)p2;
@@ -1847,6 +1850,14 @@ static int cmp_name(const void *p1, const void *p2)
        return name_compare(e1->name, e1->len, e2->name, e2->len);
 }
 
+/* check if *out lexically strictly contains *in */
+int check_dir_entry_contains(const struct dir_entry *out, const struct dir_entry *in)
+{
+       return (out->len < in->len) &&
+               (out->name[out->len - 1] == '/') &&
+               !memcmp(out->name, in->name, out->len);
+}
+
 static int treat_leading_path(struct dir_struct *dir,
                              const char *path, int len,
                              const struct pathspec *pathspec)
@@ -2060,8 +2071,32 @@ int read_directory(struct dir_struct *dir, const char *path,
                dir->untracked = NULL;
        if (!len || treat_leading_path(dir, path, len, pathspec))
                read_directory_recursive(dir, path, len, untracked, 0, pathspec);
-       QSORT(dir->entries, dir->nr, cmp_name);
-       QSORT(dir->ignored, dir->ignored_nr, cmp_name);
+       QSORT(dir->entries, dir->nr, cmp_dir_entry);
+       QSORT(dir->ignored, dir->ignored_nr, cmp_dir_entry);
+
+       /*
+        * If DIR_SHOW_IGNORED_TOO is set, read_directory_recursive() will
+        * also pick up untracked contents of untracked dirs; by default
+        * we discard these, but given DIR_KEEP_UNTRACKED_CONTENTS we do not.
+        */
+       if ((dir->flags & DIR_SHOW_IGNORED_TOO) &&
+                    !(dir->flags & DIR_KEEP_UNTRACKED_CONTENTS)) {
+               int i, j;
+
+               /* remove from dir->entries untracked contents of untracked dirs */
+               for (i = j = 0; j < dir->nr; j++) {
+                       if (i &&
+                           check_dir_entry_contains(dir->entries[i - 1], dir->entries[j])) {
+                               free(dir->entries[j]);
+                               dir->entries[j] = NULL;
+                       } else {
+                               dir->entries[i++] = dir->entries[j];
+                       }
+               }
+
+               dir->nr = i;
+       }
+
        if (dir->untracked) {
                static struct trace_key trace_untracked_stats = TRACE_KEY_INIT(UNTRACKED_STATS);
                trace_printf_key(&trace_untracked_stats,
diff --git a/dir.h b/dir.h
index bf23a470af57775c8f0099597f7a403995bf3a89..edb5fda586444762e50397409815962e65f22415 100644 (file)
--- a/dir.h
+++ b/dir.h
@@ -151,7 +151,8 @@ struct dir_struct {
                DIR_NO_GITLINKS = 1<<3,
                DIR_COLLECT_IGNORED = 1<<4,
                DIR_SHOW_IGNORED_TOO = 1<<5,
-               DIR_COLLECT_KILLED_ONLY = 1<<6
+               DIR_COLLECT_KILLED_ONLY = 1<<6,
+               DIR_KEEP_UNTRACKED_CONTENTS = 1<<7
        } flags;
        struct dir_entry **entries;
        struct dir_entry **ignored;
@@ -326,6 +327,9 @@ static inline int dir_path_match(const struct dir_entry *ent,
                              has_trailing_dir);
 }
 
+int cmp_dir_entry(const void *p1, const void *p2);
+int check_dir_entry_contains(const struct dir_entry *out, const struct dir_entry *in);
+
 void untracked_cache_invalidate_path(struct index_state *, const char *);
 void untracked_cache_remove_from_index(struct index_state *, const char *);
 void untracked_cache_add_to_index(struct index_state *, const char *);
index cdc0747bf01b0598a2e864073887896162815019..fc6013ba3c83aa7243f15595bc7b6ddd8b8f46dc 100755 (executable)
@@ -9,6 +9,7 @@ cat >expected <<\EOF
 ?? actual
 ?? expected
 ?? untracked/
+!! untracked/ignored
 EOF
 
 test_expect_success 'status untracked directory with --ignored' '
index b89fd2a6ada025fc0e68b1a1c3d7e739aacec688..7b36954d63d70e79149cf1558a93586e1b39c867 100755 (executable)
@@ -653,4 +653,20 @@ test_expect_success 'git clean -d respects pathspecs (pathspec is prefix of dir)
        test_path_is_dir foobar
 '
 
+test_expect_success 'git clean -d skips untracked dirs containing ignored files' '
+       echo /foo/bar >.gitignore &&
+       echo ignoreme >>.gitignore &&
+       rm -rf foo &&
+       mkdir -p foo/a/aa/aaa foo/b/bb/bbb &&
+       touch foo/bar foo/baz foo/a/aa/ignoreme foo/b/ignoreme foo/b/bb/1 foo/b/bb/2 &&
+       git clean -df &&
+       test_path_is_dir foo &&
+       test_path_is_file foo/bar &&
+       test_path_is_missing foo/baz &&
+       test_path_is_file foo/a/aa/ignoreme &&
+       test_path_is_missing foo/a/aa/aaa &&
+       test_path_is_file foo/b/ignoreme &&
+       test_path_is_missing foo/b/bb
+'
+
 test_done