Merge branch 'nd/negative-pathspec'

Introduce "negative pathspec" magic, to allow "git log -- . ':!dir'" to
tell us "I am interested in everything but 'dir' directory".

* nd/negative-pathspec:
  pathspec.c: support adding prefix magic to a pathspec with mnemonic magic
  Support pathspec magic :(exclude) and its short form :!
  glossary-content.txt: rephrase magic signature part
This commit is contained in:
Junio C Hamano 2014-01-10 10:31:48 -08:00
commit 010d81ae35
7 changed files with 354 additions and 40 deletions

View File

@ -323,24 +323,26 @@ including Documentation/chapter_1/figure_1.jpg.
A pathspec that begins with a colon `:` has special meaning. In the A pathspec that begins with a colon `:` has special meaning. In the
short form, the leading colon `:` is followed by zero or more "magic short form, the leading colon `:` is followed by zero or more "magic
signature" letters (which optionally is terminated by another colon `:`), signature" letters (which optionally is terminated by another colon `:`),
and the remainder is the pattern to match against the path. The optional and the remainder is the pattern to match against the path.
colon that terminates the "magic signature" can be omitted if the pattern The "magic signature" consists of ASCII symbols that are neither
begins with a character that cannot be a "magic signature" and is not a alphanumeric, glob, regex special charaters nor colon.
colon. The optional colon that terminates the "magic signature" can be
omitted if the pattern begins with a character that does not belong to
"magic signature" symbol set and is not a colon.
+ +
In the long form, the leading colon `:` is followed by a open In the long form, the leading colon `:` is followed by a open
parenthesis `(`, a comma-separated list of zero or more "magic words", parenthesis `(`, a comma-separated list of zero or more "magic words",
and a close parentheses `)`, and the remainder is the pattern to match and a close parentheses `)`, and the remainder is the pattern to match
against the path. against the path.
+ +
The "magic signature" consists of an ASCII symbol that is not A pathspec with only a colon means "there is no pathspec". This form
alphanumeric. should not be combined with other pathspec.
+ +
-- --
top `/`;; top;;
The magic word `top` (mnemonic: `/`) makes the pattern match The magic word `top` (magic signature: `/`) makes the pattern
from the root of the working tree, even when you are running match from the root of the working tree, even when you are
the command from inside a subdirectory. running the command from inside a subdirectory.
literal;; literal;;
Wildcards in the pattern such as `*` or `?` are treated Wildcards in the pattern such as `*` or `?` are treated
@ -377,14 +379,12 @@ full pathname may have special meaning:
- Other consecutive asterisks are considered invalid. - Other consecutive asterisks are considered invalid.
+ +
Glob magic is incompatible with literal magic. 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.
-- --
+
Currently only the slash `/` is recognized as the "magic signature",
but it is envisioned that we will support more types of magic in later
versions of Git.
+
A pathspec with only a colon means "there is no pathspec". This form
should not be combined with other pathspec.
[[def_parent]]parent:: [[def_parent]]parent::
A <<def_commit_object,commit object>> contains a (possibly empty) list A <<def_commit_object,commit object>> contains a (possibly empty) list

View File

@ -540,10 +540,13 @@ int cmd_add(int argc, const char **argv, const char *prefix)
PATHSPEC_FROMTOP | PATHSPEC_FROMTOP |
PATHSPEC_LITERAL | PATHSPEC_LITERAL |
PATHSPEC_GLOB | PATHSPEC_GLOB |
PATHSPEC_ICASE); PATHSPEC_ICASE |
PATHSPEC_EXCLUDE);
for (i = 0; i < pathspec.nr; i++) { for (i = 0; i < pathspec.nr; i++) {
const char *path = pathspec.items[i].match; const char *path = pathspec.items[i].match;
if (pathspec.items[i].magic & PATHSPEC_EXCLUDE)
continue;
if (!seen[i] && if (!seen[i] &&
((pathspec.items[i].magic & ((pathspec.items[i].magic &
(PATHSPEC_GLOB | PATHSPEC_ICASE)) || (PATHSPEC_GLOB | PATHSPEC_ICASE)) ||

47
dir.c
View File

@ -126,10 +126,13 @@ static size_t common_prefix_len(const struct pathspec *pathspec)
PATHSPEC_MAXDEPTH | PATHSPEC_MAXDEPTH |
PATHSPEC_LITERAL | PATHSPEC_LITERAL |
PATHSPEC_GLOB | PATHSPEC_GLOB |
PATHSPEC_ICASE); PATHSPEC_ICASE |
PATHSPEC_EXCLUDE);
for (n = 0; n < pathspec->nr; n++) { for (n = 0; n < pathspec->nr; n++) {
size_t i = 0, len = 0, item_len; size_t i = 0, len = 0, item_len;
if (pathspec->items[n].magic & PATHSPEC_EXCLUDE)
continue;
if (pathspec->items[n].magic & PATHSPEC_ICASE) if (pathspec->items[n].magic & PATHSPEC_ICASE)
item_len = pathspec->items[n].prefix; item_len = pathspec->items[n].prefix;
else 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 * pathspec did not match any names, which could indicate that the
* user mistyped the nth pathspec. * user mistyped the nth pathspec.
*/ */
int match_pathspec_depth(const struct pathspec *ps, static int match_pathspec_depth_1(const struct pathspec *ps,
const char *name, int namelen, const char *name, int namelen,
int prefix, char *seen) int prefix, char *seen,
int exclude)
{ {
int i, retval = 0; int i, retval = 0;
@ -290,7 +294,8 @@ int match_pathspec_depth(const struct pathspec *ps,
PATHSPEC_MAXDEPTH | PATHSPEC_MAXDEPTH |
PATHSPEC_LITERAL | PATHSPEC_LITERAL |
PATHSPEC_GLOB | PATHSPEC_GLOB |
PATHSPEC_ICASE); PATHSPEC_ICASE |
PATHSPEC_EXCLUDE);
if (!ps->nr) { if (!ps->nr) {
if (!ps->recursive || if (!ps->recursive ||
@ -309,8 +314,19 @@ int match_pathspec_depth(const struct pathspec *ps,
for (i = ps->nr - 1; i >= 0; i--) { for (i = ps->nr - 1; i >= 0; i--) {
int how; int how;
if ((!exclude && ps->items[i].magic & PATHSPEC_EXCLUDE) ||
( exclude && !(ps->items[i].magic & PATHSPEC_EXCLUDE)))
continue;
if (seen && seen[i] == MATCHED_EXACTLY) if (seen && seen[i] == MATCHED_EXACTLY)
continue; 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); how = match_pathspec_item(ps->items+i, prefix, name, namelen);
if (ps->recursive && if (ps->recursive &&
(ps->magic & PATHSPEC_MAXDEPTH) && (ps->magic & PATHSPEC_MAXDEPTH) &&
@ -334,6 +350,18 @@ int match_pathspec_depth(const struct pathspec *ps,
return retval; 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. * 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_MAXDEPTH |
PATHSPEC_LITERAL | PATHSPEC_LITERAL |
PATHSPEC_GLOB | PATHSPEC_GLOB |
PATHSPEC_ICASE); PATHSPEC_ICASE |
PATHSPEC_EXCLUDE);
if (has_symlink_leading_path(path, len)) if (has_symlink_leading_path(path, len))
return dir->nr; 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); simplify = create_simplify(pathspec ? pathspec->_raw : NULL);
if (!len || treat_leading_path(dir, path, len, simplify)) if (!len || treat_leading_path(dir, path, len, simplify))
read_directory_recursive(dir, path, len, 0, simplify); read_directory_recursive(dir, path, len, 0, simplify);

View File

@ -71,8 +71,23 @@ static struct pathspec_magic {
{ PATHSPEC_LITERAL, 0, "literal" }, { PATHSPEC_LITERAL, 0, "literal" },
{ PATHSPEC_GLOB, '\0', "glob" }, { PATHSPEC_GLOB, '\0', "glob" },
{ PATHSPEC_ICASE, '\0', "icase" }, { PATHSPEC_ICASE, '\0', "icase" },
{ PATHSPEC_EXCLUDE, '!', "exclude" },
}; };
static void prefix_short_magic(struct strbuf *sb, int prefixlen,
unsigned short_magic)
{
int i;
strbuf_addstr(sb, ":(");
for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++)
if (short_magic & pathspec_magic[i].bit) {
if (sb->buf[sb->len - 1] != '(')
strbuf_addch(sb, ',');
strbuf_addstr(sb, pathspec_magic[i].name);
}
strbuf_addf(sb, ",prefix:%d)", prefixlen);
}
/* /*
* Take an element of a pathspec and check for magic signatures. * Take an element of a pathspec and check for magic signatures.
* Append the result to the prefix. Return the magic bitmap. * Append the result to the prefix. Return the magic bitmap.
@ -232,22 +247,16 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
*/ */
if (flags & PATHSPEC_PREFIX_ORIGIN) { if (flags & PATHSPEC_PREFIX_ORIGIN) {
struct strbuf sb = STRBUF_INIT; struct strbuf sb = STRBUF_INIT;
const char *start = elt;
if (prefixlen && !literal_global) { if (prefixlen && !literal_global) {
/* Preserve the actual prefix length of each pattern */ /* Preserve the actual prefix length of each pattern */
if (short_magic) if (short_magic)
die("BUG: prefixing on short magic is not supported"); prefix_short_magic(&sb, prefixlen, short_magic);
else if (long_magic_end) { else if (long_magic_end) {
strbuf_add(&sb, start, long_magic_end - start); strbuf_add(&sb, elt, long_magic_end - elt);
strbuf_addf(&sb, ",prefix:%d", prefixlen); strbuf_addf(&sb, ",prefix:%d)", prefixlen);
start = long_magic_end; } else
} else {
if (*start == ':')
start++;
strbuf_addf(&sb, ":(prefix:%d)", prefixlen); strbuf_addf(&sb, ":(prefix:%d)", prefixlen);
}
} }
strbuf_add(&sb, start, copyfrom - start);
strbuf_addstr(&sb, match); strbuf_addstr(&sb, match);
item->original = strbuf_detach(&sb, NULL); item->original = strbuf_detach(&sb, NULL);
} else } else
@ -355,7 +364,7 @@ void parse_pathspec(struct pathspec *pathspec,
{ {
struct pathspec_item *item; struct pathspec_item *item;
const char *entry = argv ? *argv : NULL; const char *entry = argv ? *argv : NULL;
int i, n, prefixlen; int i, n, prefixlen, nr_exclude = 0;
memset(pathspec, 0, sizeof(*pathspec)); memset(pathspec, 0, sizeof(*pathspec));
@ -412,6 +421,8 @@ void parse_pathspec(struct pathspec *pathspec,
if ((flags & PATHSPEC_LITERAL_PATH) && if ((flags & PATHSPEC_LITERAL_PATH) &&
!(magic_mask & PATHSPEC_LITERAL)) !(magic_mask & PATHSPEC_LITERAL))
item[i].magic |= PATHSPEC_LITERAL; item[i].magic |= PATHSPEC_LITERAL;
if (item[i].magic & PATHSPEC_EXCLUDE)
nr_exclude++;
if (item[i].magic & magic_mask) if (item[i].magic & magic_mask)
unsupported_magic(entry, unsupported_magic(entry,
item[i].magic & magic_mask, item[i].magic & magic_mask,
@ -427,6 +438,10 @@ void parse_pathspec(struct pathspec *pathspec,
pathspec->magic |= item[i].magic; 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 (pathspec->magic & PATHSPEC_MAXDEPTH) {
if (flags & PATHSPEC_KEEP_ORDER) if (flags & PATHSPEC_KEEP_ORDER)

View File

@ -7,12 +7,14 @@
#define PATHSPEC_LITERAL (1<<2) #define PATHSPEC_LITERAL (1<<2)
#define PATHSPEC_GLOB (1<<3) #define PATHSPEC_GLOB (1<<3)
#define PATHSPEC_ICASE (1<<4) #define PATHSPEC_ICASE (1<<4)
#define PATHSPEC_EXCLUDE (1<<5)
#define PATHSPEC_ALL_MAGIC \ #define PATHSPEC_ALL_MAGIC \
(PATHSPEC_FROMTOP | \ (PATHSPEC_FROMTOP | \
PATHSPEC_MAXDEPTH | \ PATHSPEC_MAXDEPTH | \
PATHSPEC_LITERAL | \ PATHSPEC_LITERAL | \
PATHSPEC_GLOB | \ PATHSPEC_GLOB | \
PATHSPEC_ICASE) PATHSPEC_ICASE | \
PATHSPEC_EXCLUDE)
#define PATHSPEC_ONESTAR 1 /* the pathspec pattern satisfies GFNM_ONESTAR */ #define PATHSPEC_ONESTAR 1 /* the pathspec pattern satisfies GFNM_ONESTAR */

184
t/t6132-pathspec-exclude.sh Executable file
View File

@ -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

View 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) * Pre-condition: either baselen == base_offset (i.e. empty path)
* or base[baselen-1] == '/' (i.e. with trailing slash). * or base[baselen-1] == '/' (i.e. with trailing slash).
*/ */
enum interesting tree_entry_interesting(const struct name_entry *entry, static enum interesting do_match(const struct name_entry *entry,
struct strbuf *base, int base_offset, struct strbuf *base, int base_offset,
const struct pathspec *ps) const struct pathspec *ps,
int exclude)
{ {
int i; int i;
int pathlen, baselen = base->len - base_offset; int pathlen, baselen = base->len - base_offset;
@ -676,7 +677,8 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
PATHSPEC_MAXDEPTH | PATHSPEC_MAXDEPTH |
PATHSPEC_LITERAL | PATHSPEC_LITERAL |
PATHSPEC_GLOB | PATHSPEC_GLOB |
PATHSPEC_ICASE); PATHSPEC_ICASE |
PATHSPEC_EXCLUDE);
if (!ps->nr) { if (!ps->nr) {
if (!ps->recursive || 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; const char *base_str = base->buf + base_offset;
int matchlen = item->len, matched = 0; int matchlen = item->len, matched = 0;
if ((!exclude && item->magic & PATHSPEC_EXCLUDE) ||
( exclude && !(item->magic & PATHSPEC_EXCLUDE)))
continue;
if (baselen >= matchlen) { if (baselen >= matchlen) {
/* If it doesn't match, move along... */ /* If it doesn't match, move along... */
if (!match_dir_prefix(item, base_str, match, matchlen)) if (!match_dir_prefix(item, base_str, match, matchlen))
@ -782,3 +788,72 @@ match_wildcards:
} }
return never_interesting; /* No matches */ 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 */
}