Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Support pathspec magic :(exclude) and its short form :!
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
  • Loading branch information
Nguyễn Thái Ngọc Duy authored and Junio C Hamano committed Dec 6, 2013
1 parent 8b7cb51 commit ef79b1f
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 13 deletions.
5 changes: 5 additions & 0 deletions Documentation/glossary-content.txt
Expand Up @@ -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::
Expand Down
5 changes: 4 additions & 1 deletion builtin/add.c
Expand Up @@ -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)) ||
Expand Down
47 changes: 41 additions & 6 deletions dir.c
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand All @@ -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 ||
Expand All @@ -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) &&
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 8 additions & 1 deletion pathspec.c
Expand Up @@ -71,6 +71,7 @@ static struct pathspec_magic {
{ PATHSPEC_LITERAL, 0, "literal" },
{ PATHSPEC_GLOB, '\0', "glob" },
{ PATHSPEC_ICASE, '\0', "icase" },
{ PATHSPEC_EXCLUDE, '!', "exclude" },
};

/*
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion pathspec.h
Expand Up @@ -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 */

Expand Down
184 changes: 184 additions & 0 deletions t/t6132-pathspec-exclude.sh
@@ -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

0 comments on commit ef79b1f

Please sign in to comment.