Git Mailing List Archive mirror
 help / color / mirror / Atom feed
* [PATCH 0/2] gitignore, re-inclusion fix
@ 2015-08-23 12:50 Nguyễn Thái Ngọc Duy
  2015-08-23 12:50 ` [PATCH 1/2] dir.c: make last_exclude_matching_from_list() run til the end Nguyễn Thái Ngọc Duy
                   ` (2 more replies)
  0 siblings, 3 replies; 10+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2015-08-23 12:50 UTC (permalink / raw
  To: git; +Cc: Eric Sunshine, Junio C Hamano,
	Nguyễn Thái Ngọc Duy

This is an old problem. I attempted once [1] and then was reminded [2]
with some more comments on the original patch. Let's try again.

The problem is this .gitignore currently does not work, but it should:

/abc
!/abc/def/ghi

This patch fixes that by realizing that the last rule may re-include
something in abc/def so it does not exclude "abc" and "abc/def"
outright to give the last rule a chance to match.

[1] http://article.gmane.org/gmane.comp.version-control.git/259870
[2] http://thread.gmane.org/gmane.comp.version-control.git/265901/focus=266227

Nguyễn Thái Ngọc Duy (2):
  dir.c: make last_exclude_matching_from_list() run til the end
  dir.c: don't exclude whole dir prematurely if neg pattern may match

 Documentation/gitignore.txt        | 21 ++++++---
 dir.c                              | 89 +++++++++++++++++++++++++++++++++++---
 t/t3001-ls-files-others-exclude.sh | 20 +++++++++
 3 files changed, 118 insertions(+), 12 deletions(-)

-- 
2.3.0.rc1.137.g477eb31

^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH 1/2] dir.c: make last_exclude_matching_from_list() run til the end
  2015-08-23 12:50 [PATCH 0/2] gitignore, re-inclusion fix Nguyễn Thái Ngọc Duy
@ 2015-08-23 12:50 ` Nguyễn Thái Ngọc Duy
  2015-08-25 20:28   ` Junio C Hamano
  2015-08-23 12:50 ` [PATCH 2/2] dir.c: don't exclude whole dir prematurely if neg pattern may match Nguyễn Thái Ngọc Duy
  2015-09-13  1:18 ` [PATCH v2 0/2] gitignore, re-inclusion fix Nguyễn Thái Ngọc Duy
  2 siblings, 1 reply; 10+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2015-08-23 12:50 UTC (permalink / raw
  To: git; +Cc: Eric Sunshine, Junio C Hamano,
	Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 dir.c | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/dir.c b/dir.c
index c00c7e2..3a7630a 100644
--- a/dir.c
+++ b/dir.c
@@ -901,6 +901,7 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
 						       int *dtype,
 						       struct exclude_list *el)
 {
+	struct exclude *exc = NULL; /* undecided */
 	int i;
 
 	if (!el->nr)
@@ -922,18 +923,22 @@ 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))
-			return x;
+				   exclude, prefix, x->patternlen, x->flags)) {
+			exc = x;
+			break;
+		}
 	}
-	return NULL; /* undecided */
+	return exc;
 }
 
 /*
-- 
2.3.0.rc1.137.g477eb31

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH 2/2] dir.c: don't exclude whole dir prematurely if neg pattern may match
  2015-08-23 12:50 [PATCH 0/2] gitignore, re-inclusion fix Nguyễn Thái Ngọc Duy
  2015-08-23 12:50 ` [PATCH 1/2] dir.c: make last_exclude_matching_from_list() run til the end Nguyễn Thái Ngọc Duy
@ 2015-08-23 12:50 ` Nguyễn Thái Ngọc Duy
  2015-09-13  1:18 ` [PATCH v2 0/2] gitignore, re-inclusion fix Nguyễn Thái Ngọc Duy
  2 siblings, 0 replies; 10+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2015-08-23 12:50 UTC (permalink / raw
  To: git; +Cc: Eric Sunshine, Junio C Hamano,
	Nguyễn Thái Ngọc Duy

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".

In order for it to detect that the directory under examination should
not be excluded right away, in other words it is a parent directory of a
negative pattern, the "directory path" of the negative pattern must be
literal. Patterns like "!f?o/bar" can't stop "foo" from being excluded.

Basename matching (i.e. "no slashes in the pattern") or must-be-dir
matching (i.e. "trailing slash in the pattern") does not work well with
this. For example, if we descend in "foo" and are examining "foo/abc",
current code for "foo/" pattern will check if path "foo/abc", not "foo",
is a directory. The same problem with basename matching. These may need
big code reorg to make it work.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/gitignore.txt        | 21 ++++++++---
 dir.c                              | 76 +++++++++++++++++++++++++++++++++++++-
 t/t3001-ls-files-others-exclude.sh | 20 ++++++++++
 3 files changed, 109 insertions(+), 8 deletions(-)

diff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt
index 473623d..889a72a 100644
--- a/Documentation/gitignore.txt
+++ b/Documentation/gitignore.txt
@@ -82,12 +82,9 @@ 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.
-   Put a backslash ("`\`") in front of the first "`!`" for patterns
-   that begin with a literal "`!`", for example, "`\!important!.txt`".
+   included again. It is possible to re-include a file if a parent
+   directory of that file is excluded, with restrictions. 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 +138,18 @@ not tracked by Git remain untracked.
 To stop tracking a file that is currently tracked, use
 'git rm --cached'.
 
+To re-include a file when its parent directory is excluded, the
+following conditions must match:
+
+ - 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 3a7630a..a1f711c 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
@@ -902,7 +963,7 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
 						       struct exclude_list *el)
 {
 	struct exclude *exc = NULL; /* undecided */
-	int i;
+	int i, matched_negative_path = 0;
 
 	if (!el->nr)
 		return NULL;	/* undefined */
@@ -937,7 +998,18 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
 			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))
+			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 3fc484e..9de49a6 100755
--- a/t/t3001-ls-files-others-exclude.sh
+++ b/t/t3001-ls-files-others-exclude.sh
@@ -305,4 +305,24 @@ 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 &&
+		/foo
+		!foo/bar/bar
+		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
-- 
2.3.0.rc1.137.g477eb31

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH 1/2] dir.c: make last_exclude_matching_from_list() run til the end
  2015-08-23 12:50 ` [PATCH 1/2] dir.c: make last_exclude_matching_from_list() run til the end Nguyễn Thái Ngọc Duy
@ 2015-08-25 20:28   ` Junio C Hamano
  2015-08-31 10:13     ` Duy Nguyen
  0 siblings, 1 reply; 10+ messages in thread
From: Junio C Hamano @ 2015-08-25 20:28 UTC (permalink / raw
  To: Nguyễn Thái Ngọc Duy; +Cc: git, Eric Sunshine

Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:

> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>

Because?  Title just tells what the patch meant to do (i.e. instead
of returning it keeps looping), but does not say why it is a good
idea.  Besides, this a no-op patch and does not make it keep looping.

> ---
>  dir.c | 15 ++++++++++-----
>  1 file changed, 10 insertions(+), 5 deletions(-)
>
> diff --git a/dir.c b/dir.c
> index c00c7e2..3a7630a 100644
> --- a/dir.c
> +++ b/dir.c
> @@ -901,6 +901,7 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
>  						       int *dtype,
>  						       struct exclude_list *el)
>  {
> +	struct exclude *exc = NULL; /* undecided */
>  	int i;
>  
>  	if (!el->nr)
> @@ -922,18 +923,22 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,

Note that we are in a big for() loop that scans backwards an array.

>  			if (match_basename(basename,
>  					   pathlen - (basename - pathname),
>  					   exclude, prefix, x->patternlen,
> -					   x->flags))
> -				return x;
> +					   x->flags)) {
> +				exc = x;
> +				break;
> +			}

We used to return x immediately; now we store x to exc and break,
i.e. leave the loop.

>  			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))
> -			return x;
> +				   exclude, prefix, x->patternlen, x->flags)) {
> +			exc = x;
> +			break;

We used to return x immediately; now we store x to exc and break,
i.e. leave the loop.

> +		}
>  	}
> -	return NULL; /* undecided */
> +	return exc;

And then we return exc.

>  }

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 1/2] dir.c: make last_exclude_matching_from_list() run til the end
  2015-08-25 20:28   ` Junio C Hamano
@ 2015-08-31 10:13     ` Duy Nguyen
  0 siblings, 0 replies; 10+ messages in thread
From: Duy Nguyen @ 2015-08-31 10:13 UTC (permalink / raw
  To: Junio C Hamano; +Cc: Git Mailing List, Eric Sunshine

On Wed, Aug 26, 2015 at 3:28 AM, Junio C Hamano <gitster@pobox.com> wrote:
> Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:
>
>> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
>
> Because?  Title just tells what the patch meant to do (i.e. instead
> of returning it keeps looping), but does not say why it is a good
> idea.  Besides, this a no-op patch and does not make it keep looping.

Because the next patch adds some post processing before returning the
value. Having all the paths come to the same point would simplify the
code. Will update the commit message.
-- 
Duy

^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH v2 0/2] gitignore, re-inclusion fix
  2015-08-23 12:50 [PATCH 0/2] gitignore, re-inclusion fix Nguyễn Thái Ngọc Duy
  2015-08-23 12:50 ` [PATCH 1/2] dir.c: make last_exclude_matching_from_list() run til the end Nguyễn Thái Ngọc Duy
  2015-08-23 12:50 ` [PATCH 2/2] dir.c: don't exclude whole dir prematurely if neg pattern may match Nguyễn Thái Ngọc Duy
@ 2015-09-13  1:18 ` Nguyễn Thái Ngọc Duy
  2015-09-13  1:19   ` [PATCH v2 1/2] dir.c: make last_exclude_matching_from_list() run til the end Nguyễn Thái Ngọc Duy
  2015-09-13  1:19   ` [PATCH v2 2/2] dir.c: don't exclude whole dir prematurely if neg pattern may match Nguyễn Thái Ngọc Duy
  2 siblings, 2 replies; 10+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2015-09-13  1:18 UTC (permalink / raw
  To: git; +Cc: Eric Sunshine, Junio C Hamano,
	Nguyễn Thái Ngọc Duy

No code change. Explain why 1/2 is needed in the commit message.

Nguyễn Thái Ngọc Duy (2):
  dir.c: make last_exclude_matching_from_list() run til the end
  dir.c: don't exclude whole dir prematurely if neg pattern may match

 Documentation/gitignore.txt        | 21 ++++++---
 dir.c                              | 89 +++++++++++++++++++++++++++++++++++---
 t/t3001-ls-files-others-exclude.sh | 20 +++++++++
 3 files changed, 118 insertions(+), 12 deletions(-)

-- 
2.3.0.rc1.137.g477eb31

^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH v2 1/2] dir.c: make last_exclude_matching_from_list() run til the end
  2015-09-13  1:18 ` [PATCH v2 0/2] gitignore, re-inclusion fix Nguyễn Thái Ngọc Duy
@ 2015-09-13  1:19   ` Nguyễn Thái Ngọc Duy
  2015-09-13  1:19   ` [PATCH v2 2/2] dir.c: don't exclude whole dir prematurely if neg pattern may match Nguyễn Thái Ngọc Duy
  1 sibling, 0 replies; 10+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2015-09-13  1:19 UTC (permalink / raw
  To: git; +Cc: Eric Sunshine, Junio C Hamano,
	Nguyễn Thái Ngọc Duy

The next patch adds some post processing to the result value before it's
returned to the caller. Make all branches reach the end of the function,
so we can do it all in one place.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 dir.c | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/dir.c b/dir.c
index c00c7e2..3a7630a 100644
--- a/dir.c
+++ b/dir.c
@@ -901,6 +901,7 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
 						       int *dtype,
 						       struct exclude_list *el)
 {
+	struct exclude *exc = NULL; /* undecided */
 	int i;
 
 	if (!el->nr)
@@ -922,18 +923,22 @@ 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))
-			return x;
+				   exclude, prefix, x->patternlen, x->flags)) {
+			exc = x;
+			break;
+		}
 	}
-	return NULL; /* undecided */
+	return exc;
 }
 
 /*
-- 
2.3.0.rc1.137.g477eb31

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 2/2] dir.c: don't exclude whole dir prematurely if neg pattern may match
  2015-09-13  1:18 ` [PATCH v2 0/2] gitignore, re-inclusion fix Nguyễn Thái Ngọc Duy
  2015-09-13  1:19   ` [PATCH v2 1/2] dir.c: make last_exclude_matching_from_list() run til the end Nguyễn Thái Ngọc Duy
@ 2015-09-13  1:19   ` Nguyễn Thái Ngọc Duy
  2015-09-14 17:15     ` Junio C Hamano
  1 sibling, 1 reply; 10+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2015-09-13  1:19 UTC (permalink / raw
  To: git; +Cc: Eric Sunshine, Junio C Hamano,
	Nguyễn Thái Ngọc Duy

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".

In order for it to detect that the directory under examination should
not be excluded right away, in other words it is a parent directory of a
negative pattern, the "directory path" of the negative pattern must be
literal. Patterns like "!f?o/bar" can't stop "foo" from being excluded.

Basename matching (i.e. "no slashes in the pattern") or must-be-dir
matching (i.e. "trailing slash in the pattern") does not work well with
this. For example, if we descend in "foo" and are examining "foo/abc",
current code for "foo/" pattern will check if path "foo/abc", not "foo",
is a directory. The same problem with basename matching. These may need
big code reorg to make it work.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/gitignore.txt        | 21 ++++++++---
 dir.c                              | 76 +++++++++++++++++++++++++++++++++++++-
 t/t3001-ls-files-others-exclude.sh | 20 ++++++++++
 3 files changed, 109 insertions(+), 8 deletions(-)

diff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt
index 473623d..889a72a 100644
--- a/Documentation/gitignore.txt
+++ b/Documentation/gitignore.txt
@@ -82,12 +82,9 @@ 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.
-   Put a backslash ("`\`") in front of the first "`!`" for patterns
-   that begin with a literal "`!`", for example, "`\!important!.txt`".
+   included again. It is possible to re-include a file if a parent
+   directory of that file is excluded, with restrictions. 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 +138,18 @@ not tracked by Git remain untracked.
 To stop tracking a file that is currently tracked, use
 'git rm --cached'.
 
+To re-include a file when its parent directory is excluded, the
+following conditions must be met:
+
+ - 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 3a7630a..a1f711c 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
@@ -902,7 +963,7 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
 						       struct exclude_list *el)
 {
 	struct exclude *exc = NULL; /* undecided */
-	int i;
+	int i, matched_negative_path = 0;
 
 	if (!el->nr)
 		return NULL;	/* undefined */
@@ -937,7 +998,18 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
 			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))
+			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 3fc484e..9de49a6 100755
--- a/t/t3001-ls-files-others-exclude.sh
+++ b/t/t3001-ls-files-others-exclude.sh
@@ -305,4 +305,24 @@ 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 &&
+		/foo
+		!foo/bar/bar
+		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
-- 
2.3.0.rc1.137.g477eb31

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH v2 2/2] dir.c: don't exclude whole dir prematurely if neg pattern may match
  2015-09-13  1:19   ` [PATCH v2 2/2] dir.c: don't exclude whole dir prematurely if neg pattern may match Nguyễn Thái Ngọc Duy
@ 2015-09-14 17:15     ` Junio C Hamano
  2015-09-17 13:21       ` Duy Nguyen
  0 siblings, 1 reply; 10+ messages in thread
From: Junio C Hamano @ 2015-09-14 17:15 UTC (permalink / raw
  To: Nguyễn Thái Ngọc Duy; +Cc: git, Eric Sunshine

Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:

> diff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt
> index 473623d..889a72a 100644
> --- a/Documentation/gitignore.txt
> +++ b/Documentation/gitignore.txt
> @@ -82,12 +82,9 @@ PATTERN FORMAT
>  
>   - An optional prefix "`!`" which negates the pattern; any
>     matching file excluded by a previous pattern will become
> +   included again. It is possible to re-include a file if a parent
> +   directory of that file is excluded, with restrictions. See section
> +   NOTES for detail.

Sounds like a very useful thing.

>   - If the pattern ends with a slash, it is removed for the
>     purpose of the following description, but it would only find
> @@ -141,6 +138,18 @@ not tracked by Git remain untracked.
>  To stop tracking a file that is currently tracked, use
>  'git rm --cached'.
>  
> +To re-include a file when its parent directory is excluded, the
> +following conditions must be met:
> +
> + - 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.
> +

In this bulletted list, don't the readers also need to be told that
having "/abc" in .gitignore (but not "!/abc/anything" in .gitignore)
and "!foo" in abc/.gitignore would not cause us to descend into
"/abc" just to examine "abc/.gitignore" with pessimistic assumption
that there might be some exclusion in there?

> diff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh
> index 3fc484e..9de49a6 100755
> --- a/t/t3001-ls-files-others-exclude.sh
> +++ b/t/t3001-ls-files-others-exclude.sh
> @@ -305,4 +305,24 @@ 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 &&
> +		/foo
> +		!foo/bar/bar
> +		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
> +	)
> +'

And another test here may want to explicitly ensure that we are not
overly pessimising the ignore processing, so that later changes will
not break it, I think.  Or do we already have such a case covered by
an existing test?

Thanks.

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v2 2/2] dir.c: don't exclude whole dir prematurely if neg pattern may match
  2015-09-14 17:15     ` Junio C Hamano
@ 2015-09-17 13:21       ` Duy Nguyen
  0 siblings, 0 replies; 10+ messages in thread
From: Duy Nguyen @ 2015-09-17 13:21 UTC (permalink / raw
  To: Junio C Hamano; +Cc: Git Mailing List, Eric Sunshine

On Tue, Sep 15, 2015 at 12:15 AM, Junio C Hamano <gitster@pobox.com> wrote:
> Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:
>
>> diff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt
>> index 473623d..889a72a 100644
>> --- a/Documentation/gitignore.txt
>> +++ b/Documentation/gitignore.txt
>> @@ -82,12 +82,9 @@ PATTERN FORMAT
>>
>>   - An optional prefix "`!`" which negates the pattern; any
>>     matching file excluded by a previous pattern will become
>> +   included again. It is possible to re-include a file if a parent
>> +   directory of that file is excluded, with restrictions. See section
>> +   NOTES for detail.
>
> Sounds like a very useful thing.
>
>>   - If the pattern ends with a slash, it is removed for the
>>     purpose of the following description, but it would only find
>> @@ -141,6 +138,18 @@ not tracked by Git remain untracked.
>>  To stop tracking a file that is currently tracked, use
>>  'git rm --cached'.
>>
>> +To re-include a file when its parent directory is excluded, the
>> +following conditions must be met:
>> +
>> + - 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.
>> +
>
> In this bulletted list, don't the readers also need to be told that
> having "/abc" in .gitignore (but not "!/abc/anything" in .gitignore)
> and "!foo" in abc/.gitignore would not cause us to descend into
> "/abc" just to examine "abc/.gitignore" with pessimistic assumption
> that there might be some exclusion in there?

Yeah I thought it was already mentioned. I guess it's just in my mind.
Will update.

>> diff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh
>> index 3fc484e..9de49a6 100755
>> --- a/t/t3001-ls-files-others-exclude.sh
>> +++ b/t/t3001-ls-files-others-exclude.sh
>> @@ -305,4 +305,24 @@ 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 &&
>> +             /foo
>> +             !foo/bar/bar
>> +             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
>> +     )
>> +'
>
> And another test here may want to explicitly ensure that we are not
> overly pessimising the ignore processing, so that later changes will
> not break it, I think.

Yep. Will do.
-- 
Duy

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2015-09-17 13:22 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2015-08-23 12:50 [PATCH 0/2] gitignore, re-inclusion fix Nguyễn Thái Ngọc Duy
2015-08-23 12:50 ` [PATCH 1/2] dir.c: make last_exclude_matching_from_list() run til the end Nguyễn Thái Ngọc Duy
2015-08-25 20:28   ` Junio C Hamano
2015-08-31 10:13     ` Duy Nguyen
2015-08-23 12:50 ` [PATCH 2/2] dir.c: don't exclude whole dir prematurely if neg pattern may match Nguyễn Thái Ngọc Duy
2015-09-13  1:18 ` [PATCH v2 0/2] gitignore, re-inclusion fix Nguyễn Thái Ngọc Duy
2015-09-13  1:19   ` [PATCH v2 1/2] dir.c: make last_exclude_matching_from_list() run til the end Nguyễn Thái Ngọc Duy
2015-09-13  1:19   ` [PATCH v2 2/2] dir.c: don't exclude whole dir prematurely if neg pattern may match Nguyễn Thái Ngọc Duy
2015-09-14 17:15     ` Junio C Hamano
2015-09-17 13:21       ` Duy Nguyen

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).