Support pathspec magic :(exclude) and its short form :!
authorNguyễn Thái Ngọc Duy <pclouds@gmail.com>
Fri, 6 Dec 2013 07:30:48 +0000 (14:30 +0700)
committerJunio C Hamano <gitster@pobox.com>
Fri, 6 Dec 2013 21:00:39 +0000 (13:00 -0800)
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/glossary-content.txt
builtin/add.c
dir.c
pathspec.c
pathspec.h
t/t6132-pathspec-exclude.sh [new file with mode: 0755]
tree-walk.c
index b7e7ab5849fa40f47b6bf1f3dc1e2f8a7967bdcd..378306f58162fdfee900b48b8bb6ad8171cbfca3 100644 (file)
@@ -379,6 +379,11 @@ full pathname may have special meaning:
  - Other consecutive asterisks are considered invalid.
 +
 Glob magic is incompatible with literal magic.
+
+exclude;;
+       After a path matches any non-exclude pathspec, it will be run
+       through all exclude pathspec (magic signature: `!`). If it
+       matches, the path is ignored.
 --
 
 [[def_parent]]parent::
index 226f758869358444ad1c39e5e0a05c961fb76859..0df73ae73596546966af18e7076d9a6f54964786 100644 (file)
@@ -540,10 +540,13 @@ int cmd_add(int argc, const char **argv, const char *prefix)
                               PATHSPEC_FROMTOP |
                               PATHSPEC_LITERAL |
                               PATHSPEC_GLOB |
-                              PATHSPEC_ICASE);
+                              PATHSPEC_ICASE |
+                              PATHSPEC_EXCLUDE);
 
                for (i = 0; i < pathspec.nr; i++) {
                        const char *path = pathspec.items[i].match;
+                       if (pathspec.items[i].magic & PATHSPEC_EXCLUDE)
+                               continue;
                        if (!seen[i] &&
                            ((pathspec.items[i].magic &
                              (PATHSPEC_GLOB | PATHSPEC_ICASE)) ||
diff --git a/dir.c b/dir.c
index 23b6de47036839ca4297a9b93d1a9794547ccff1..d10a63f731020aab99f9b25a2b96a029d772410d 100644 (file)
--- a/dir.c
+++ b/dir.c
@@ -126,10 +126,13 @@ static size_t common_prefix_len(const struct pathspec *pathspec)
                       PATHSPEC_MAXDEPTH |
                       PATHSPEC_LITERAL |
                       PATHSPEC_GLOB |
-                      PATHSPEC_ICASE);
+                      PATHSPEC_ICASE |
+                      PATHSPEC_EXCLUDE);
 
        for (n = 0; n < pathspec->nr; n++) {
                size_t i = 0, len = 0, item_len;
+               if (pathspec->items[n].magic & PATHSPEC_EXCLUDE)
+                       continue;
                if (pathspec->items[n].magic & PATHSPEC_ICASE)
                        item_len = pathspec->items[n].prefix;
                else
@@ -279,9 +282,10 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix,
  * pathspec did not match any names, which could indicate that the
  * user mistyped the nth pathspec.
  */
-int match_pathspec_depth(const struct pathspec *ps,
-                        const char *name, int namelen,
-                        int prefix, char *seen)
+static int match_pathspec_depth_1(const struct pathspec *ps,
+                                 const char *name, int namelen,
+                                 int prefix, char *seen,
+                                 int exclude)
 {
        int i, retval = 0;
 
@@ -290,7 +294,8 @@ int match_pathspec_depth(const struct pathspec *ps,
                       PATHSPEC_MAXDEPTH |
                       PATHSPEC_LITERAL |
                       PATHSPEC_GLOB |
-                      PATHSPEC_ICASE);
+                      PATHSPEC_ICASE |
+                      PATHSPEC_EXCLUDE);
 
        if (!ps->nr) {
                if (!ps->recursive ||
@@ -309,8 +314,19 @@ int match_pathspec_depth(const struct pathspec *ps,
 
        for (i = ps->nr - 1; i >= 0; i--) {
                int how;
+
+               if ((!exclude &&   ps->items[i].magic & PATHSPEC_EXCLUDE) ||
+                   ( exclude && !(ps->items[i].magic & PATHSPEC_EXCLUDE)))
+                       continue;
+
                if (seen && seen[i] == MATCHED_EXACTLY)
                        continue;
+               /*
+                * Make exclude patterns optional and never report
+                * "pathspec ':(exclude)foo' matches no files"
+                */
+               if (seen && ps->items[i].magic & PATHSPEC_EXCLUDE)
+                       seen[i] = MATCHED_FNMATCH;
                how = match_pathspec_item(ps->items+i, prefix, name, namelen);
                if (ps->recursive &&
                    (ps->magic & PATHSPEC_MAXDEPTH) &&
@@ -334,6 +350,18 @@ int match_pathspec_depth(const struct pathspec *ps,
        return retval;
 }
 
+int match_pathspec_depth(const struct pathspec *ps,
+                        const char *name, int namelen,
+                        int prefix, char *seen)
+{
+       int positive, negative;
+       positive = match_pathspec_depth_1(ps, name, namelen, prefix, seen, 0);
+       if (!(ps->magic & PATHSPEC_EXCLUDE) || !positive)
+               return positive;
+       negative = match_pathspec_depth_1(ps, name, namelen, prefix, seen, 1);
+       return negative ? 0 : positive;
+}
+
 /*
  * Return the length of the "simple" part of a path match limiter.
  */
@@ -1375,11 +1403,18 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
                               PATHSPEC_MAXDEPTH |
                               PATHSPEC_LITERAL |
                               PATHSPEC_GLOB |
-                              PATHSPEC_ICASE);
+                              PATHSPEC_ICASE |
+                              PATHSPEC_EXCLUDE);
 
        if (has_symlink_leading_path(path, len))
                return dir->nr;
 
+       /*
+        * exclude patterns are treated like positive ones in
+        * create_simplify. Usually exclude patterns should be a
+        * subset of positive ones, which has no impacts on
+        * create_simplify().
+        */
        simplify = create_simplify(pathspec ? pathspec->_raw : NULL);
        if (!len || treat_leading_path(dir, path, len, simplify))
                read_directory_recursive(dir, path, len, 0, simplify);
index 87b3b82f1fd9baee7758159e5e3119b6da0655e3..4e6a727570a3d9c7511009bcc71f8ae0ed2d5fa1 100644 (file)
@@ -71,6 +71,7 @@ static struct pathspec_magic {
        { PATHSPEC_LITERAL,   0, "literal" },
        { PATHSPEC_GLOB,   '\0', "glob" },
        { PATHSPEC_ICASE,  '\0', "icase" },
+       { PATHSPEC_EXCLUDE, '!', "exclude" },
 };
 
 /*
@@ -355,7 +356,7 @@ void parse_pathspec(struct pathspec *pathspec,
 {
        struct pathspec_item *item;
        const char *entry = argv ? *argv : NULL;
-       int i, n, prefixlen;
+       int i, n, prefixlen, nr_exclude = 0;
 
        memset(pathspec, 0, sizeof(*pathspec));
 
@@ -412,6 +413,8 @@ void parse_pathspec(struct pathspec *pathspec,
                if ((flags & PATHSPEC_LITERAL_PATH) &&
                    !(magic_mask & PATHSPEC_LITERAL))
                        item[i].magic |= PATHSPEC_LITERAL;
+               if (item[i].magic & PATHSPEC_EXCLUDE)
+                       nr_exclude++;
                if (item[i].magic & magic_mask)
                        unsupported_magic(entry,
                                          item[i].magic & magic_mask,
@@ -427,6 +430,10 @@ void parse_pathspec(struct pathspec *pathspec,
                pathspec->magic |= item[i].magic;
        }
 
+       if (nr_exclude == n)
+               die(_("There is nothing to exclude from by :(exclude) patterns.\n"
+                     "Perhaps you forgot to add either ':/' or '.' ?"));
+
 
        if (pathspec->magic & PATHSPEC_MAXDEPTH) {
                if (flags & PATHSPEC_KEEP_ORDER)
index a75e9242d1bea7e9318426d6786bd5eae9dc6215..0c1126264a394e62cfbed47b2feae85e9b5d1410 100644 (file)
@@ -7,12 +7,14 @@
 #define PATHSPEC_LITERAL       (1<<2)
 #define PATHSPEC_GLOB          (1<<3)
 #define PATHSPEC_ICASE         (1<<4)
+#define PATHSPEC_EXCLUDE       (1<<5)
 #define PATHSPEC_ALL_MAGIC       \
        (PATHSPEC_FROMTOP       | \
         PATHSPEC_MAXDEPTH      | \
         PATHSPEC_LITERAL       | \
         PATHSPEC_GLOB          | \
-        PATHSPEC_ICASE)
+        PATHSPEC_ICASE         | \
+        PATHSPEC_EXCLUDE)
 
 #define PATHSPEC_ONESTAR 1     /* the pathspec pattern satisfies GFNM_ONESTAR */
 
diff --git a/t/t6132-pathspec-exclude.sh b/t/t6132-pathspec-exclude.sh
new file mode 100755 (executable)
index 0000000..62049be
--- /dev/null
@@ -0,0 +1,184 @@
+#!/bin/sh
+
+test_description='test case exclude pathspec'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+       for p in file sub/file sub/sub/file sub/file2 sub/sub/sub/file sub2/file; do
+               if echo $p | grep /; then
+                       mkdir -p `dirname $p`
+               fi &&
+               : >$p &&
+               git add $p &&
+               git commit -m $p
+       done &&
+       git log --oneline --format=%s >actual &&
+       cat <<EOF >expect &&
+sub2/file
+sub/sub/sub/file
+sub/file2
+sub/sub/file
+sub/file
+file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'exclude only should error out' '
+       test_must_fail git log --oneline --format=%s -- ":(exclude)sub"
+'
+
+test_expect_success 't_e_i() exclude sub' '
+       git log --oneline --format=%s -- . ":(exclude)sub" >actual
+       cat <<EOF >expect &&
+sub2/file
+file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 't_e_i() exclude sub/sub/file' '
+       git log --oneline --format=%s -- . ":(exclude)sub/sub/file" >actual
+       cat <<EOF >expect &&
+sub2/file
+sub/sub/sub/file
+sub/file2
+sub/file
+file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 't_e_i() exclude sub using mnemonic' '
+       git log --oneline --format=%s -- . ":!sub" >actual
+       cat <<EOF >expect &&
+sub2/file
+file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 't_e_i() exclude :(icase)SUB' '
+       git log --oneline --format=%s -- . ":(exclude,icase)SUB" >actual
+       cat <<EOF >expect &&
+sub2/file
+file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 't_e_i() exclude sub2 from sub' '
+       (
+       cd sub &&
+       git log --oneline --format=%s -- :/ ":/!sub2" >actual
+       cat <<EOF >expect &&
+sub/sub/sub/file
+sub/file2
+sub/sub/file
+sub/file
+file
+EOF
+       test_cmp expect actual
+       )
+'
+
+test_expect_success 't_e_i() exclude sub/*file' '
+       git log --oneline --format=%s -- . ":(exclude)sub/*file" >actual
+       cat <<EOF >expect &&
+sub2/file
+sub/file2
+file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 't_e_i() exclude :(glob)sub/*/file' '
+       git log --oneline --format=%s -- . ":(exclude,glob)sub/*/file" >actual
+       cat <<EOF >expect &&
+sub2/file
+sub/sub/sub/file
+sub/file2
+sub/file
+file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'm_p_d() exclude sub' '
+       git ls-files -- . ":(exclude)sub" >actual
+       cat <<EOF >expect &&
+file
+sub2/file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'm_p_d() exclude sub/sub/file' '
+       git ls-files -- . ":(exclude)sub/sub/file" >actual
+       cat <<EOF >expect &&
+file
+sub/file
+sub/file2
+sub/sub/sub/file
+sub2/file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'm_p_d() exclude sub using mnemonic' '
+       git ls-files -- . ":!sub" >actual
+       cat <<EOF >expect &&
+file
+sub2/file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'm_p_d() exclude :(icase)SUB' '
+       git ls-files -- . ":(exclude,icase)SUB" >actual
+       cat <<EOF >expect &&
+file
+sub2/file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'm_p_d() exclude sub2 from sub' '
+       (
+       cd sub &&
+       git ls-files -- :/ ":/!sub2" >actual
+       cat <<EOF >expect &&
+../file
+file
+file2
+sub/file
+sub/sub/file
+EOF
+       test_cmp expect actual
+       )
+'
+
+test_expect_success 'm_p_d() exclude sub/*file' '
+       git ls-files -- . ":(exclude)sub/*file" >actual
+       cat <<EOF >expect &&
+file
+sub/file2
+sub2/file
+EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'm_p_d() exclude :(glob)sub/*/file' '
+       git ls-files -- . ":(exclude,glob)sub/*/file" >actual
+       cat <<EOF >expect &&
+file
+sub/file
+sub/file2
+sub/sub/sub/file
+sub2/file
+EOF
+       test_cmp expect actual
+'
+
+test_done
index 5ece8c3477b11f3bf0c54196924ac24568309225..680afda060e28fd15c0064bd51cb829dc2469b1c 100644 (file)
@@ -662,9 +662,10 @@ static int match_wildcard_base(const struct pathspec_item *item,
  * Pre-condition: either baselen == base_offset (i.e. empty path)
  * or base[baselen-1] == '/' (i.e. with trailing slash).
  */
-enum interesting tree_entry_interesting(const struct name_entry *entry,
-                                       struct strbuf *base, int base_offset,
-                                       const struct pathspec *ps)
+static enum interesting do_match(const struct name_entry *entry,
+                                struct strbuf *base, int base_offset,
+                                const struct pathspec *ps,
+                                int exclude)
 {
        int i;
        int pathlen, baselen = base->len - base_offset;
@@ -676,7 +677,8 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
                       PATHSPEC_MAXDEPTH |
                       PATHSPEC_LITERAL |
                       PATHSPEC_GLOB |
-                      PATHSPEC_ICASE);
+                      PATHSPEC_ICASE |
+                      PATHSPEC_EXCLUDE);
 
        if (!ps->nr) {
                if (!ps->recursive ||
@@ -697,6 +699,10 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
                const char *base_str = base->buf + base_offset;
                int matchlen = item->len, matched = 0;
 
+               if ((!exclude &&   item->magic & PATHSPEC_EXCLUDE) ||
+                   ( exclude && !(item->magic & PATHSPEC_EXCLUDE)))
+                       continue;
+
                if (baselen >= matchlen) {
                        /* If it doesn't match, move along... */
                        if (!match_dir_prefix(item, base_str, match, matchlen))
@@ -782,3 +788,72 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
        }
        return never_interesting; /* No matches */
 }
+
+/*
+ * Is a tree entry interesting given the pathspec we have?
+ *
+ * Pre-condition: either baselen == base_offset (i.e. empty path)
+ * or base[baselen-1] == '/' (i.e. with trailing slash).
+ */
+enum interesting tree_entry_interesting(const struct name_entry *entry,
+                                       struct strbuf *base, int base_offset,
+                                       const struct pathspec *ps)
+{
+       enum interesting positive, negative;
+       positive = do_match(entry, base, base_offset, ps, 0);
+
+       /*
+        * case | entry | positive | negative | result
+        * -----+-------+----------+----------+-------
+        *   1  |  file |   -1     |  -1..2   |  -1
+        *   2  |  file |    0     |  -1..2   |   0
+        *   3  |  file |    1     |   -1     |   1
+        *   4  |  file |    1     |    0     |   1
+        *   5  |  file |    1     |    1     |   0
+        *   6  |  file |    1     |    2     |   0
+        *   7  |  file |    2     |   -1     |   2
+        *   8  |  file |    2     |    0     |   2
+        *   9  |  file |    2     |    1     |   0
+        *  10  |  file |    2     |    2     |  -1
+        * -----+-------+----------+----------+-------
+        *  11  |  dir  |   -1     |  -1..2   |  -1
+        *  12  |  dir  |    0     |  -1..2   |   0
+        *  13  |  dir  |    1     |   -1     |   1
+        *  14  |  dir  |    1     |    0     |   1
+        *  15  |  dir  |    1     |    1     |   1 (*)
+        *  16  |  dir  |    1     |    2     |   0
+        *  17  |  dir  |    2     |   -1     |   2
+        *  18  |  dir  |    2     |    0     |   2
+        *  19  |  dir  |    2     |    1     |   1 (*)
+        *  20  |  dir  |    2     |    2     |  -1
+        *
+        * (*) An exclude pattern interested in a directory does not
+        * necessarily mean it will exclude all of the directory. In
+        * wildcard case, it can't decide until looking at individual
+        * files inside. So don't write such directories off yet.
+        */
+
+       if (!(ps->magic & PATHSPEC_EXCLUDE) ||
+           positive <= entry_not_interesting) /* #1, #2, #11, #12 */
+               return positive;
+
+       negative = do_match(entry, base, base_offset, ps, 1);
+
+       /* #3, #4, #7, #8, #13, #14, #17, #18 */
+       if (negative <= entry_not_interesting)
+               return positive;
+
+       /* #15, #19 */
+       if (S_ISDIR(entry->mode) &&
+           positive >= entry_interesting &&
+           negative == entry_interesting)
+               return entry_interesting;
+
+       if ((positive == entry_interesting &&
+            negative >= entry_interesting) || /* #5, #6, #16 */
+           (positive == all_entries_interesting &&
+            negative == entry_interesting)) /* #9 */
+               return entry_not_interesting;
+
+       return all_entries_not_interesting; /* #10, #20 */
+}