Browse Source

dir.c: don't exclude whole dir prematurely

If there is a pattern "!foo/bar", this patch makes it not exclude
"foo" right away. This gives us a chance to examine "foo" and
re-include "foo/bar".

Helped-by: brian m. carlson <sandals@crustytoothpaste.net>
Helped-by: Micha Wiedenmann <mw-u2@gmx.de>
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
maint
Nguyễn Thái Ngọc Duy 9 years ago committed by Junio C Hamano
parent
commit
d589a67ece
  1. 17
      Documentation/gitignore.txt
  2. 109
      dir.c
  3. 7
      t/t3001-ls-files-others-exclude.sh
  4. 153
      t/t3007-ls-files-other-negative.sh

17
Documentation/gitignore.txt

@ -82,12 +82,12 @@ PATTERN FORMAT


- An optional prefix "`!`" which negates the pattern; any - An optional prefix "`!`" which negates the pattern; any
matching file excluded by a previous pattern will become matching file excluded by a previous pattern will become
included again. It is not possible to re-include a file if a parent included again.
directory of that file is excluded. Git doesn't list excluded
directories for performance reasons, so any patterns on contained
files have no effect, no matter where they are defined.
Put a backslash ("`\`") in front of the first "`!`" for patterns Put a backslash ("`\`") in front of the first "`!`" for patterns
that begin with a literal "`!`", for example, "`\!important!.txt`". that begin with a literal "`!`", for example, "`\!important!.txt`".
It is possible to re-include a file if a parent directory of that
file is excluded if certain conditions are met. See section NOTES
for detail.


- If the pattern ends with a slash, it is removed for the - If the pattern ends with a slash, it is removed for the
purpose of the following description, but it would only find purpose of the following description, but it would only find
@ -141,6 +141,15 @@ not tracked by Git remain untracked.
To stop tracking a file that is currently tracked, use To stop tracking a file that is currently tracked, use
'git rm --cached'. 'git rm --cached'.


To re-include files or directories when their parent directory is
excluded, the following conditions must be met:

- The rules to exclude a directory and re-include a subset back must
be in the same .gitignore file.

- The directory part in the re-include rules must be literal (i.e. no
wildcards)

EXAMPLES EXAMPLES
-------- --------



109
dir.c

@ -930,6 +930,75 @@ static int match_sticky(struct exclude *exc, const char *pathname, int pathlen,
return 0; return 0;
} }


static inline int different_decisions(const struct exclude *a,
const struct exclude *b)
{
return (a->flags & EXC_FLAG_NEGATIVE) != (b->flags & EXC_FLAG_NEGATIVE);
}

/*
* Return non-zero if pathname is a directory and an ancestor of the
* literal path in a pattern.
*/
static int match_directory_part(const char *pathname, int pathlen,
int *dtype, struct exclude *x)
{
const char *base = x->base;
int baselen = x->baselen ? x->baselen - 1 : 0;
const char *pattern = x->pattern;
int prefix = x->nowildcardlen;
int patternlen = x->patternlen;

if (*dtype == DT_UNKNOWN)
*dtype = get_dtype(NULL, pathname, pathlen);
if (*dtype != DT_DIR)
return 0;

if (*pattern == '/') {
pattern++;
patternlen--;
prefix--;
}

if (baselen) {
if (((pathlen < baselen && base[pathlen] == '/') ||
pathlen == baselen) &&
!strncmp_icase(pathname, base, pathlen))
return 1;
pathname += baselen + 1;
pathlen -= baselen + 1;
}


if (prefix &&
(((pathlen < prefix && pattern[pathlen] == '/') ||
pathlen == prefix) &&
!strncmp_icase(pathname, pattern, pathlen)))
return 1;

return 0;
}

static struct exclude *should_descend(const char *pathname, int pathlen,
int *dtype, struct exclude_list *el,
struct exclude *exc)
{
int i;

for (i = el->nr - 1; 0 <= i; i--) {
struct exclude *x = el->excludes[i];

if (x == exc)
break;

if (!(x->flags & EXC_FLAG_NODIR) &&
different_decisions(x, exc) &&
match_directory_part(pathname, pathlen, dtype, x))
return x;
}
return NULL;
}

/* /*
* Scan the given exclude list in reverse to see whether pathname * Scan the given exclude list in reverse to see whether pathname
* should be ignored. The first match (i.e. the last on the list), if * should be ignored. The first match (i.e. the last on the list), if
@ -943,7 +1012,7 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
struct exclude_list *el) struct exclude_list *el)
{ {
struct exclude *exc = NULL; /* undecided */ struct exclude *exc = NULL; /* undecided */
int i; int i, maybe_descend = 0;


if (!el->nr) if (!el->nr)
return NULL; /* undefined */ return NULL; /* undefined */
@ -955,6 +1024,10 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
const char *exclude = x->pattern; const char *exclude = x->pattern;
int prefix = x->nowildcardlen; int prefix = x->nowildcardlen;


if (!maybe_descend && i < el->nr - 1 &&
different_decisions(x, el->excludes[i+1]))
maybe_descend = 1;

if (x->sticky_paths.nr) { if (x->sticky_paths.nr) {
if (*dtype == DT_UNKNOWN) if (*dtype == DT_UNKNOWN)
*dtype = get_dtype(NULL, pathname, pathlen); *dtype = get_dtype(NULL, pathname, pathlen);
@ -998,6 +1071,34 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
return NULL; return NULL;
} }


/*
* We have found a matching pattern "exc" that may exclude whole
* directory. We also found that there may be a pattern that matches
* something inside the directory and reincludes stuff.
*
* Go through the patterns again, find that pattern and double check.
* If it's true, return "undecided" and keep descending in. "exc" is
* marked sticky so that it continues to match inside the directory.
*/
if (!(exc->flags & EXC_FLAG_NEGATIVE) && maybe_descend) {
struct exclude *x;

if (*dtype == DT_UNKNOWN)
*dtype = get_dtype(NULL, pathname, pathlen);

if (*dtype == DT_DIR &&
(x = should_descend(pathname, pathlen, dtype, el, exc))) {
add_sticky(exc, pathname, pathlen);
trace_printf_key(&trace_exclude,
"exclude: %.*s vs %s at line %d => %s,"
" forced open by %s at line %d => n/a\n",
pathlen, pathname, exc->pattern, exc->srcpos,
exc->flags & EXC_FLAG_NEGATIVE ? "no" : "yes",
x->pattern, x->srcpos);
return NULL;
}
}

trace_printf_key(&trace_exclude, "exclude: %.*s vs %s at line %d => %s%s\n", trace_printf_key(&trace_exclude, "exclude: %.*s vs %s at line %d => %s%s\n",
pathlen, pathname, exc->pattern, exc->srcpos, pathlen, pathname, exc->pattern, exc->srcpos,
exc->flags & EXC_FLAG_NEGATIVE ? "no" : "yes", exc->flags & EXC_FLAG_NEGATIVE ? "no" : "yes",
@ -2096,6 +2197,12 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
if (has_symlink_leading_path(path, len)) if (has_symlink_leading_path(path, len))
return dir->nr; return dir->nr;


/*
* Stay on the safe side. if read_directory() has run once on
* "dir", some sticky flag may have been left. Clear them all.
*/
clear_sticky(dir);

/* /*
* exclude patterns are treated like positive ones in * exclude patterns are treated like positive ones in
* create_simplify. Usually exclude patterns should be a * create_simplify. Usually exclude patterns should be a

7
t/t3001-ls-files-others-exclude.sh

@ -175,13 +175,10 @@ test_expect_success 'negated exclude matches can override previous ones' '
grep "^a.1" output grep "^a.1" output
' '


test_expect_success 'excluded directory overrides content patterns' ' test_expect_success 'excluded directory does not override content patterns' '


git ls-files --others --exclude="one" --exclude="!one/a.1" >output && git ls-files --others --exclude="one" --exclude="!one/a.1" >output &&
if grep "^one/a.1" output grep "^one/a.1" output
then
false
fi
' '


test_expect_success 'negated directory doesn'\''t affect content patterns' ' test_expect_success 'negated directory doesn'\''t affect content patterns' '

153
t/t3007-ls-files-other-negative.sh

@ -0,0 +1,153 @@
#!/bin/sh

test_description='test re-include patterns'

. ./test-lib.sh

test_expect_success 'setup' '
mkdir -p fooo foo/bar tmp &&
touch abc foo/def foo/bar/ghi foo/bar/bar
'

test_expect_success 'no match, do not enter subdir and waste cycles' '
cat >.gitignore <<-\EOF &&
/tmp
/foo
!fooo/bar/bar
EOF
GIT_TRACE_EXCLUDE="$(pwd)/tmp/trace" git ls-files -o --exclude-standard >tmp/actual &&
! grep "enter .foo/.\$" tmp/trace &&
cat >tmp/expected <<-\EOF &&
.gitignore
abc
EOF
test_cmp tmp/expected tmp/actual
'

test_expect_success 'match, excluded by literal pathname pattern' '
cat >.gitignore <<-\EOF &&
/tmp
/fooo
/foo
!foo/bar/bar
EOF
cat >fooo/.gitignore <<-\EOF &&
!/*
EOF git ls-files -o --exclude-standard >tmp/actual &&
cat >tmp/expected <<-\EOF &&
.gitignore
abc
foo/bar/bar
EOF
test_cmp tmp/expected tmp/actual
'

test_expect_success 'match, excluded by wildcard pathname pattern' '
cat >.gitignore <<-\EOF &&
/tmp
/fooo
/fo?
!foo/bar/bar
EOF
git ls-files -o --exclude-standard >tmp/actual &&
cat >tmp/expected <<-\EOF &&
.gitignore
abc
foo/bar/bar
EOF
test_cmp tmp/expected tmp/actual
'

test_expect_success 'match, excluded by literal basename pattern' '
cat >.gitignore <<-\EOF &&
/tmp
/fooo
foo
!foo/bar/bar
EOF
git ls-files -o --exclude-standard >tmp/actual &&
cat >tmp/expected <<-\EOF &&
.gitignore
abc
foo/bar/bar
EOF
test_cmp tmp/expected tmp/actual
'

test_expect_success 'match, excluded by wildcard basename pattern' '
cat >.gitignore <<-\EOF &&
/tmp
/fooo
fo?
!foo/bar/bar
EOF
git ls-files -o --exclude-standard >tmp/actual &&
cat >tmp/expected <<-\EOF &&
.gitignore
abc
foo/bar/bar
EOF
test_cmp tmp/expected tmp/actual
'

test_expect_success 'match, excluded by literal mustbedir, basename pattern' '
cat >.gitignore <<-\EOF &&
/tmp
/fooo
foo/
!foo/bar/bar
EOF
git ls-files -o --exclude-standard >tmp/actual &&
cat >tmp/expected <<-\EOF &&
.gitignore
abc
foo/bar/bar
EOF
test_cmp tmp/expected tmp/actual
'

test_expect_success 'match, excluded by literal mustbedir, pathname pattern' '
cat >.gitignore <<-\EOF &&
/tmp
/fooo
/foo/
!foo/bar/bar
EOF
git ls-files -o --exclude-standard >tmp/actual &&
cat >tmp/expected <<-\EOF &&
.gitignore
abc
foo/bar/bar
EOF
test_cmp tmp/expected tmp/actual
'

test_expect_success 'prepare for nested negatives' '
cat >.git/info/exclude <<-\EOF &&
/.gitignore
/tmp
/foo
/abc
EOF
git ls-files -o --exclude-standard >tmp/actual &&
test_must_be_empty tmp/actual &&
mkdir -p 1/2/3/4 &&
touch 1/f 1/2/f 1/2/3/f 1/2/3/4/f
'

test_expect_success 'match, literal pathname, nested negatives' '
cat >.gitignore <<-\EOF &&
/1
!1/2
1/2/3
!1/2/3/4
EOF
git ls-files -o --exclude-standard >tmp/actual &&
cat >tmp/expected <<-\EOF &&
1/2/3/4/f
1/2/f
EOF
test_cmp tmp/expected tmp/actual
'

test_done
Loading…
Cancel
Save