summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/gitignore.txt23
-rw-r--r--dir.c89
-rwxr-xr-xt/t3001-ls-files-others-exclude.sh25
3 files changed, 127 insertions, 10 deletions
diff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt
index 473623d631..79a1948a0b 100644
--- a/Documentation/gitignore.txt
+++ b/Documentation/gitignore.txt
@@ -82,12 +82,12 @@ PATTERN FORMAT
- An optional prefix "`!`" which negates the pattern; any
matching file excluded by a previous pattern will become
- included again. It is not possible to re-include a file if a parent
- 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.
+ included again.
Put a backslash ("`\`") in front of the first "`!`" for patterns
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
purpose of the following description, but it would only find
@@ -141,6 +141,21 @@ not tracked by Git remain untracked.
To stop tracking a file that is currently tracked, use
'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)
+
+ - The rules to exclude the parent directory must not end with a
+ trailing slash.
+
+ - The rules to exclude the parent directory must have at least one
+ slash.
+
EXAMPLES
--------
diff --git a/dir.c b/dir.c
index 7b25634832..b90484acf8 100644
--- a/dir.c
+++ b/dir.c
@@ -882,6 +882,25 @@ int match_pathname(const char *pathname, int pathlen,
*/
if (!patternlen && !namelen)
return 1;
+ /*
+ * This can happen when we ignore some exclude rules
+ * on directories in other to see if negative rules
+ * may match. E.g.
+ *
+ * /abc
+ * !/abc/def/ghi
+ *
+ * The pattern of interest is "/abc". On the first
+ * try, we should match path "abc" with this pattern
+ * in the "if" statement right above, but the caller
+ * ignores it.
+ *
+ * On the second try with paths within "abc",
+ * e.g. "abc/xyz", we come here and try to match it
+ * with "/abc".
+ */
+ if (!patternlen && namelen && *name == '/')
+ return 1;
}
return fnmatch_icase_mem(pattern, patternlen,
@@ -890,6 +909,48 @@ int match_pathname(const char *pathname, int pathlen,
}
/*
+ * Return non-zero if pathname is a directory and an ancestor of the
+ * literal path in a (negative) pattern. This is used to keep
+ * descending in "foo" and "foo/bar" when the pattern is
+ * "!foo/bar/.gitignore". "foo/notbar" will not be descended however.
+ */
+static int match_neg_path(const char *pathname, int pathlen, int *dtype,
+ const char *base, int baselen,
+ const char *pattern, int prefix, int patternlen,
+ int flags)
+{
+ assert((flags & EXC_FLAG_NEGATIVE) && !(flags & EXC_FLAG_NODIR));
+
+ 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] == '/') &&
+ !strncmp_icase(pathname, pattern, pathlen)))
+ return 1;
+
+ return 0;
+}
+
+/*
* 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
* any, determines the fate. Returns the exclude_list element which
@@ -901,7 +962,8 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
int *dtype,
struct exclude_list *el)
{
- int i;
+ struct exclude *exc = NULL; /* undecided */
+ int i, matched_negative_path = 0;
if (!el->nr)
return NULL; /* undefined */
@@ -922,18 +984,33 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
if (match_basename(basename,
pathlen - (basename - pathname),
exclude, prefix, x->patternlen,
- x->flags))
- return x;
+ x->flags)) {
+ exc = x;
+ break;
+ }
continue;
}
assert(x->baselen == 0 || x->base[x->baselen - 1] == '/');
if (match_pathname(pathname, pathlen,
x->base, x->baselen ? x->baselen - 1 : 0,
+ exclude, prefix, x->patternlen, x->flags)) {
+ exc = x;
+ break;
+ }
+
+ if ((x->flags & EXC_FLAG_NEGATIVE) && !matched_negative_path &&
+ match_neg_path(pathname, pathlen, dtype, x->base,
+ x->baselen ? x->baselen - 1 : 0,
exclude, prefix, x->patternlen, x->flags))
- return x;
- }
- return NULL; /* undecided */
+ matched_negative_path = 1;
+ }
+ if (exc &&
+ !(exc->flags & EXC_FLAG_NEGATIVE) &&
+ !(exc->flags & EXC_FLAG_NODIR) &&
+ matched_negative_path)
+ exc = NULL;
+ return exc;
}
/*
diff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh
index 3fc484e8c3..da257c020f 100755
--- a/t/t3001-ls-files-others-exclude.sh
+++ b/t/t3001-ls-files-others-exclude.sh
@@ -305,4 +305,29 @@ test_expect_success 'ls-files with "**" patterns and no slashes' '
test_cmp expect actual
'
+test_expect_success 'negative patterns' '
+ git init reinclude &&
+ (
+ cd reinclude &&
+ cat >.gitignore <<-\EOF &&
+ /fooo
+ /foo
+ !foo/bar/bar
+ EOF
+ mkdir fooo &&
+ cat >fooo/.gitignore <<-\EOF &&
+ !/*
+ EOF
+ mkdir -p foo/bar &&
+ touch abc foo/def foo/bar/ghi foo/bar/bar &&
+ git ls-files -o --exclude-standard >../actual &&
+ cat >../expected <<-\EOF &&
+ .gitignore
+ abc
+ foo/bar/bar
+ EOF
+ test_cmp ../expected ../actual
+ )
+'
+
test_done