Re: What's cooking in git.git (Oct 2012, #01; Tue, 2)

10 messages, 3 authors, 2016-06-15 · open the first message on its own page

Re: What's cooking in git.git (Oct 2012, #01; Tue, 2)

From: Junio C Hamano <hidden>
Date: 2016-06-15 22:54:56

Nguyen Thai Ngoc Duy [off-list ref] writes:
quoted
I am guessing that the only sensible definition is that "**"
requires anything that comes before it (if exists) is at a proper
hierarchy boundary, and anything matches it is also at a proper
hierarchy boundary, so "x**y" matches "x/a/y"
and "x/y" too? (As opposed to "x/**/y" which does not)
Yeah, x**y would match x/y under that "sensible" semantics.
quoted
and not "xy", "xay",
nor "xa/by" in the above example.  If "x**y" can match "xy" or "xay"
(or "**foo" can match "afoo"), it would be unreasonable to say it
implies the pattern is anchored at any level, no?
Yeah. That makes things easier to reason, though not exactly what we're having.
It sounds like that "x**y" with the code you imported would match
"xy" and "xa/b/cy", and I do not think of a concise and good way to
describe what it does to the end users.

"matches anything including '/'" is not a useful description for the
purpose of allowing the user to intuitively understand why "x**y" is
anchored at the level (or is not anchored and can appear anywhere).

Perhaps the wildmatch code may not be what we want X-<.

[PATCH 3/6] attr: avoid searching for basename on every match

From: Nguyễn Thái Ngọc Duy <hidden>
Date: 2016-06-15 22:54:56

Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
 attr.c | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/attr.c b/attr.c
index 66b96d9..eb576ac 100644
--- a/attr.c
+++ b/attr.c
@@ -644,13 +644,11 @@ static void prepare_attr_stack(const char *path)
 }
 
 static int path_matches(const char *pathname, int pathlen,
+			const char *basename,
 			const char *pattern,
 			const char *base, int baselen)
 {
 	if (!strchr(pattern, '/')) {
-		/* match basename */
-		const char *basename = strrchr(pathname, '/');
-		basename = basename ? basename + 1 : pathname;
 		return (fnmatch_icase(pattern, basename, 0) == 0);
 	}
 	/*
@@ -693,7 +691,8 @@ static int fill_one(const char *what, struct match_attr *a, int rem)
 	return rem;
 }
 
-static int fill(const char *path, int pathlen, struct attr_stack *stk, int rem)
+static int fill(const char *path, int pathlen, const char *basename,
+		struct attr_stack *stk, int rem)
 {
 	int i;
 	const char *base = stk->origin ? stk->origin : "";
@@ -702,7 +701,7 @@ static int fill(const char *path, int pathlen, struct attr_stack *stk, int rem)
 		struct match_attr *a = stk->attrs[i];
 		if (a->is_macro)
 			continue;
-		if (path_matches(path, pathlen,
+		if (path_matches(path, pathlen, basename,
 				 a->pattern, base, stk->originlen))
 			rem = fill_one("fill", a, rem);
 	}
@@ -741,15 +740,19 @@ static void collect_all_attrs(const char *path)
 {
 	struct attr_stack *stk;
 	int i, pathlen, rem;
+	const char *basename;
 
 	prepare_attr_stack(path);
 	for (i = 0; i < attr_nr; i++)
 		check_all_attr[i].value = ATTR__UNKNOWN;
 
+	basename = strrchr(path, '/');
+	basename = basename ? basename + 1 : path;
+
 	pathlen = strlen(path);
 	rem = attr_nr;
 	for (stk = attr_stack; 0 < rem && stk; stk = stk->prev)
-		rem = fill(path, pathlen, stk, rem);
+		rem = fill(path, pathlen, basename, stk, rem);
 }
 
 int git_check_attr(const char *path, int num, struct git_attr_check *check)
-- 
1.7.12.1.405.gb727dc9

[PATCH 4/6] attr: more matching optimizations from .gitignore

From: Nguyễn Thái Ngọc Duy <hidden>
Date: 2016-06-15 22:54:56

.gitattributes and .gitignore share the same pattern syntax but has
separate matching implementation. Over the years, ignore's
implementation accumulates more optimizations while attr's stays the
same.

This patch adds those optimizations to .gitattributes. Basically it
tries to avoid fnmatch/wildmatch in favor of strncmp as much as
possible.

Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
 attr.c | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++----------
 dir.c  |  4 ++--
 dir.h  |  2 ++
 3 files changed, 57 insertions(+), 12 deletions(-)
diff --git a/attr.c b/attr.c
index eb576ac..3fde9fa 100644
--- a/attr.c
+++ b/attr.c
@@ -116,6 +116,13 @@ struct attr_state {
 	const char *setto;
 };
 
+struct pattern {
+	const char *pattern;
+	int patternlen;
+	int nowildcardlen;
+	int flags;		/* EXC_FLAG_* */
+};
+
 /*
  * One rule, as from a .gitattributes file.
  *
@@ -131,7 +138,7 @@ struct attr_state {
  * listed as they appear in the file (macros unexpanded).
  */
 struct match_attr {
-	const char *pattern;
+	struct pattern pat;
 	struct git_attr *attr;
 	char is_macro;
 	unsigned num_attr;
@@ -243,7 +250,13 @@ static struct match_attr *parse_attr_line(const char *line, const char *src,
 		char *p = (char *)&(res->state[num_attr]);
 		memcpy(p, name, namelen);
 		p[namelen] = 0;
-		res->pattern = p;
+		res->pat.pattern = p;
+		res->pat.patternlen = strlen(p);
+		res->pat.nowildcardlen = simple_length(p);
+		if (!strchr(p, '/'))
+			res->pat.flags |= EXC_FLAG_NODIR;
+		if (*p == '*' && no_wildcard(p+1))
+			res->pat.flags |= EXC_FLAG_ENDSWITH;
 	}
 	res->is_macro = is_macro;
 	res->num_attr = num_attr;
@@ -645,26 +658,56 @@ static void prepare_attr_stack(const char *path)
 
 static int path_matches(const char *pathname, int pathlen,
 			const char *basename,
-			const char *pattern,
+			const struct pattern *pat,
 			const char *base, int baselen)
 {
-	if (!strchr(pattern, '/')) {
+	const char *pattern = pat->pattern;
+	int prefix = pat->nowildcardlen;
+	const char *name;
+	int namelen;
+
+	if (pat->flags & EXC_FLAG_NODIR) {
+		if (prefix == pat->patternlen &&
+		    !strcmp_icase(pattern, basename))
+			return 1;
+
+		if (pat->flags & EXC_FLAG_ENDSWITH &&
+		    pat->patternlen - 1 <= pathlen &&
+		    !strcmp_icase(pattern + 1, pathname +
+				  pathlen - pat->patternlen + 1))
+			return 1;
+
 		return (fnmatch_icase(pattern, basename, 0) == 0);
 	}
 	/*
 	 * match with FNM_PATHNAME; the pattern has base implicitly
 	 * in front of it.
 	 */
-	if (*pattern == '/')
+	if (*pattern == '/') {
 		pattern++;
+		prefix--;
+	}
+
+	/*
+	 * note: unlike excluded_from_list, baselen here does not
+	 * contain the trailing slash
+	 */
+
 	if (pathlen < baselen ||
 	    (baselen && pathname[baselen] != '/') ||
 	    strncmp(pathname, base, baselen))
 		return 0;
-	if (baselen != 0)
-		baselen++;
-	return (ignore_case && iwildmatch(pattern, pathname + baselen)) ||
-		(!ignore_case && wildmatch(pattern, pathname + baselen));
+
+	namelen = baselen ? pathlen - baselen - 1 : pathlen;
+	name = pathname + pathlen - namelen;
+
+	/* if the non-wildcard part is longer than the remaining
+	   pathname, surely it cannot match */
+	if (!namelen || prefix > namelen)
+		return 0;
+
+	return (ignore_case && iwildmatch(pattern, name)) ||
+		(!ignore_case && wildmatch(pattern, name));
 }
 
 static int macroexpand_one(int attr_nr, int rem);
@@ -702,7 +745,7 @@ static int fill(const char *path, int pathlen, const char *basename,
 		if (a->is_macro)
 			continue;
 		if (path_matches(path, pathlen, basename,
-				 a->pattern, base, stk->originlen))
+				 &a->pat, base, stk->originlen))
 			rem = fill_one("fill", a, rem);
 	}
 	return rem;
diff --git a/dir.c b/dir.c
index 92cda82..fd49336 100644
--- a/dir.c
+++ b/dir.c
@@ -292,7 +292,7 @@ int match_pathspec_depth(const struct pathspec *ps,
 /*
  * Return the length of the "simple" part of a path match limiter.
  */
-static int simple_length(const char *match)
+int simple_length(const char *match)
 {
 	int len = -1;
 
@@ -304,7 +304,7 @@ static int simple_length(const char *match)
 	}
 }
 
-static int no_wildcard(const char *string)
+int no_wildcard(const char *string)
 {
 	return string[simple_length(string)] == '\0';
 }
diff --git a/dir.h b/dir.h
index 893465a..7ea8678 100644
--- a/dir.h
+++ b/dir.h
@@ -101,6 +101,8 @@ extern void add_exclude(const char *string, const char *base,
 			int baselen, struct exclude_list *which);
 extern void free_excludes(struct exclude_list *el);
 extern int file_exists(const char *);
+extern int simple_length(const char *match);
+extern int no_wildcard(const char *string);
 
 extern int is_inside_dir(const char *dir);
 extern int dir_inside_of(const char *subdir, const char *dir);
-- 
1.7.12.1.405.gb727dc9

[PATCH 5/6] gitignore: do not do basename match with patterns that have '**'

From: Nguyễn Thái Ngọc Duy <hidden>
Date: 2016-06-15 22:54:56

"**" can match slashes, not like "*". "ab**ef" should be able to match
"ab/cd/ef", or "ab/c/d/ef" and so on. Turn off the EXC_FLAG_NODIR in
this case otherwise the pattern is only checked against the base
name. This behavior is in sync with rsync.

Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
 Documentation/gitignore.txt        | 10 +++++-----
 attr.c                             |  2 +-
 dir.c                              |  2 +-
 t/t0003-attributes.sh              | 16 ++++++++++++++++
 t/t3001-ls-files-others-exclude.sh | 10 ++++++++++
 5 files changed, 33 insertions(+), 7 deletions(-)
diff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt
index eb81d31..4dfe8bd 100644
--- a/Documentation/gitignore.txt
+++ b/Documentation/gitignore.txt
@@ -81,11 +81,11 @@ PATTERN FORMAT
    regular file or a symbolic link `foo` (this is consistent
    with the way how pathspec works in general in git).
 
- - If the pattern does not contain a slash '/', git treats it as
-   a shell glob pattern and checks for a match against the
-   pathname relative to the location of the `.gitignore` file
-   (relative to the toplevel of the work tree if not from a
-   `.gitignore` file).
+ - If the pattern does not contain a slash '/' nor '**', git
+   treats it as a shell glob pattern and checks for a match
+   against the pathname relative to the location of the
+   `.gitignore` file (relative to the toplevel of the work tree
+   if not from a `.gitignore` file).
 
  - Otherwise, git treats the pattern as a shell glob suitable
    for consumption by fnmatch(3) with the FNM_PATHNAME flag:
diff --git a/attr.c b/attr.c
index 3fde9fa..634b39c 100644
--- a/attr.c
+++ b/attr.c
@@ -253,7 +253,7 @@ static struct match_attr *parse_attr_line(const char *line, const char *src,
 		res->pat.pattern = p;
 		res->pat.patternlen = strlen(p);
 		res->pat.nowildcardlen = simple_length(p);
-		if (!strchr(p, '/'))
+		if (!strchr(p, '/') && !strstr(p, "**"))
 			res->pat.flags |= EXC_FLAG_NODIR;
 		if (*p == '*' && no_wildcard(p+1))
 			res->pat.flags |= EXC_FLAG_ENDSWITH;
diff --git a/dir.c b/dir.c
index fd49336..6a5de98 100644
--- a/dir.c
+++ b/dir.c
@@ -340,7 +340,7 @@ void add_exclude(const char *string, const char *base,
 	x->base = base;
 	x->baselen = baselen;
 	x->flags = flags;
-	if (!strchr(string, '/'))
+	if (!strchr(string, '/') && !strstr(string, "**"))
 		x->flags |= EXC_FLAG_NODIR;
 	x->nowildcardlen = simple_length(string);
 	if (*string == '*' && no_wildcard(string+1))
diff --git a/t/t0003-attributes.sh b/t/t0003-attributes.sh
index 6c3c554..9b534a0 100755
--- a/t/t0003-attributes.sh
+++ b/t/t0003-attributes.sh
@@ -249,4 +249,20 @@ EOF
 	test_line_count = 0 err
 '
 
+test_expect_success '"**" with no slashes test' '
+	echo "a**f foo=bar" >.gitattributes &&
+	cat <<\EOF >expect &&
+f: foo: unspecified
+a/f: foo: bar
+a/b/f: foo: bar
+a/b/c/f: foo: bar
+EOF
+	git check-attr foo -- "f" >actual 2>err &&
+	git check-attr foo -- "a/f" >>actual 2>>err &&
+	git check-attr foo -- "a/b/f" >>actual 2>>err &&
+	git check-attr foo -- "a/b/c/f" >>actual 2>>err &&
+	test_cmp expect actual &&
+	test_line_count = 0 err
+'
+
 test_done
diff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh
index 67c8bcf..6a5a4ab 100755
--- a/t/t3001-ls-files-others-exclude.sh
+++ b/t/t3001-ls-files-others-exclude.sh
@@ -225,4 +225,14 @@ EOF
 	test_cmp expect actual
 '
 
+
+test_expect_success 'ls-files with "**" patterns and no slashes' '
+	cat <<\EOF >expect &&
+one/a.1
+one/two/a.1
+EOF
+	git ls-files -o -i --exclude "one**a.1" >actual
+	test_cmp expect actual
+'
+
 test_done
-- 
1.7.12.1.405.gb727dc9

[PATCH 6/6] t3001: note about expected "**" behavior

From: Nguyễn Thái Ngọc Duy <hidden>
Date: 2016-06-15 22:54:56

"**" currently matches any characters including slashes. It's probably
too powerful. A more sensible definition may be match any characters
that the but the whole match must be wrapped by slashes. So "**" can
match none, "/", "/aaa/", "/aa/bb/" and so on but not "aa/bb".

Note it in the test suite.

Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
 t/t3001-ls-files-others-exclude.sh | 8 ++++++++
 1 file changed, 8 insertions(+)
diff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh
index 6a5a4ab..99b5f5c 100755
--- a/t/t3001-ls-files-others-exclude.sh
+++ b/t/t3001-ls-files-others-exclude.sh
@@ -235,4 +235,12 @@ EOF
 	test_cmp expect actual
 '
 
+# We might want ** to match at directory boundary, e.g. a**b matches
+# a/b, a/x/b, a/x/x/b... but not ax/xb.
+test_expect_failure 'ls-files with "**" patterns and no slashes' '
+	: >expect &&
+	git ls-files -o -i --exclude "o**a.1" >actual
+	test_cmp expect actual
+'
+
 test_done
-- 
1.7.12.1.405.gb727dc9

[PATCH 2/6] attr: avoid strlen() on every match

From: Nguyễn Thái Ngọc Duy <hidden>
Date: 2016-06-15 22:54:56

Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
 attr.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/attr.c b/attr.c
index 48df800..66b96d9 100644
--- a/attr.c
+++ b/attr.c
@@ -277,6 +277,7 @@ static struct match_attr *parse_attr_line(const char *line, const char *src,
 static struct attr_stack {
 	struct attr_stack *prev;
 	char *origin;
+	size_t originlen;
 	unsigned num_matches;
 	unsigned alloc;
 	struct match_attr **attrs;
@@ -532,6 +533,7 @@ static void bootstrap_attr_stack(void)
 	if (!is_bare_repository() || direction == GIT_ATTR_INDEX) {
 		elem = read_attr(GITATTRIBUTES_FILE, 1);
 		elem->origin = xstrdup("");
+		elem->originlen = 0;
 		elem->prev = attr_stack;
 		attr_stack = elem;
 		debug_push(elem);
@@ -625,7 +627,7 @@ static void prepare_attr_stack(const char *path)
 			strbuf_addstr(&pathbuf, GITATTRIBUTES_FILE);
 			elem = read_attr(pathbuf.buf, 0);
 			strbuf_setlen(&pathbuf, cp - path);
-			elem->origin = strbuf_detach(&pathbuf, NULL);
+			elem->origin = strbuf_detach(&pathbuf, &elem->originlen);
 			elem->prev = attr_stack;
 			attr_stack = elem;
 			debug_push(elem);
@@ -701,7 +703,7 @@ static int fill(const char *path, int pathlen, struct attr_stack *stk, int rem)
 		if (a->is_macro)
 			continue;
 		if (path_matches(path, pathlen,
-				 a->pattern, base, strlen(base)))
+				 a->pattern, base, stk->originlen))
 			rem = fill_one("fill", a, rem);
 	}
 	return rem;
-- 
1.7.12.1.405.gb727dc9

[PATCH 0/6] wildmatch part 2

From: Nguyễn Thái Ngọc Duy <hidden>
Date: 2016-06-15 22:54:56

On Thu, Oct 4, 2012 at 1:01 PM, Junio C Hamano [off-list ref] wrote:
Perhaps the wildmatch code may not be what we want X-<.
When I imported wildmatch I was hoping to make minimum changes to it.
But wildmatch is probably the only practical way to support "**" even
if we later need to change it the way we want. Other options are base
our work on top of compat/fnmatch.c, which is an #ifdef spaghetti
mess, or write a new fnmatch()-compatible function. Both unattractive
to me.

Anyway, this is on top of nd/wildmatch, which makes "ab**cd" match
full pathname.

attr patches port .gitignore optimizations over. In long term, we
should probably have a shared matching implementation instead. I tried
that road once and failed so I won't attempt again any time soon. If
we drop wildmatch, I can split these attr patches out as a separate
series. It's a good thing to do anyway.

The last patch just reflects that current "**" is not exactly what we
want. I'm not sure if I could look into wildmatch.c and change it.
Anybody is welcome to step up, of course.

Nguyễn Thái Ngọc Duy (6):
  attr: remove the union in struct match_attr
  attr: avoid strlen() on every match
  attr: avoid searching for basename on every match
  attr: more matching optimizations from .gitignore
  gitignore: do not do basename match with patterns that have '**'
  t3001: note about expected "**" behavior

 Documentation/gitignore.txt        |  10 ++--
 attr.c                             | 101 +++++++++++++++++++++++++++----------
 dir.c                              |   6 +--
 dir.h                              |   2 +
 t/t0003-attributes.sh              |  16 ++++++
 t/t3001-ls-files-others-exclude.sh |  18 +++++++
 6 files changed, 118 insertions(+), 35 deletions(-)

-- 
1.7.12.1.405.gb727dc9

[PATCH 1/6] attr: remove the union in struct match_attr

From: Nguyễn Thái Ngọc Duy <hidden>
Date: 2016-06-15 22:54:56

We're going to add more attributes to u.pattern so it'll become bigger
in size than a pointer. There's no point in sharing the same room with
u.attr.

Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
 attr.c | 25 ++++++++++++-------------
 1 file changed, 12 insertions(+), 13 deletions(-)
diff --git a/attr.c b/attr.c
index 15ebaa1..48df800 100644
--- a/attr.c
+++ b/attr.c
@@ -119,10 +119,10 @@ struct attr_state {
 /*
  * One rule, as from a .gitattributes file.
  *
- * If is_macro is true, then u.attr is a pointer to the git_attr being
+ * If is_macro is true, then attr is a pointer to the git_attr being
  * defined.
  *
- * If is_macro is false, then u.pattern points at the filename pattern
+ * If is_macro is false, then pattern points at the filename pattern
  * to which the rule applies.  (The memory pointed to is part of the
  * memory block allocated for the match_attr instance.)
  *
@@ -131,10 +131,8 @@ struct attr_state {
  * listed as they appear in the file (macros unexpanded).
  */
 struct match_attr {
-	union {
-		char *pattern;
-		struct git_attr *attr;
-	} u;
+	const char *pattern;
+	struct git_attr *attr;
 	char is_macro;
 	unsigned num_attr;
 	struct attr_state state[FLEX_ARRAY];
@@ -240,11 +238,12 @@ static struct match_attr *parse_attr_line(const char *line, const char *src,
 		      sizeof(struct attr_state) * num_attr +
 		      (is_macro ? 0 : namelen + 1));
 	if (is_macro)
-		res->u.attr = git_attr_internal(name, namelen);
+		res->attr = git_attr_internal(name, namelen);
 	else {
-		res->u.pattern = (char *)&(res->state[num_attr]);
-		memcpy(res->u.pattern, name, namelen);
-		res->u.pattern[namelen] = 0;
+		char *p = (char *)&(res->state[num_attr]);
+		memcpy(p, name, namelen);
+		p[namelen] = 0;
+		res->pattern = p;
 	}
 	res->is_macro = is_macro;
 	res->num_attr = num_attr;
@@ -682,7 +681,7 @@ static int fill_one(const char *what, struct match_attr *a, int rem)
 
 		if (*n == ATTR__UNKNOWN) {
 			debug_set(what,
-				  a->is_macro ? a->u.attr->name : a->u.pattern,
+				  a->is_macro ? a->attr->name : a->pattern,
 				  attr, v);
 			*n = v;
 			rem--;
@@ -702,7 +701,7 @@ static int fill(const char *path, int pathlen, struct attr_stack *stk, int rem)
 		if (a->is_macro)
 			continue;
 		if (path_matches(path, pathlen,
-				 a->u.pattern, base, strlen(base)))
+				 a->pattern, base, strlen(base)))
 			rem = fill_one("fill", a, rem);
 	}
 	return rem;
@@ -722,7 +721,7 @@ static int macroexpand_one(int attr_nr, int rem)
 			struct match_attr *ma = stk->attrs[i];
 			if (!ma->is_macro)
 				continue;
-			if (ma->u.attr->attr_nr == attr_nr)
+			if (ma->attr->attr_nr == attr_nr)
 				a = ma;
 		}
 
-- 
1.7.12.1.405.gb727dc9

Re: [PATCH 5/6] gitignore: do not do basename match with patterns that have '**'

From: Johannes Sixt <hidden>
Date: 2016-06-15 22:54:57

Am 10/4/2012 9:39, schrieb Nguyễn Thái Ngọc Duy:
- - If the pattern does not contain a slash '/', git treats it as
-   a shell glob pattern and checks for a match against the
-   pathname relative to the location of the `.gitignore` file
-   (relative to the toplevel of the work tree if not from a
-   `.gitignore` file).
+ - If the pattern does not contain a slash '/' nor '**', git
+   treats it as a shell glob pattern and checks for a match
+   against the pathname relative to the location of the
+   `.gitignore` file (relative to the toplevel of the work tree
+   if not from a `.gitignore` file).
+test_expect_success '"**" with no slashes test' '
+	echo "a**f foo=bar" >.gitattributes &&
+	cat <<\EOF >expect &&
+f: foo: unspecified
+a/f: foo: bar
+a/b/f: foo: bar
+a/b/c/f: foo: bar
+EOF
Should the above .gitattributes match nested paths, such as b/a/c/f?

I think it should, because the user can easily say "/a**f" that nested
paths should not be matched.

But if it does not match, as your documentation update implies, which
options does the user have to match nested paths? Only to add more
patterns for each nested directory, such as "b/a**f".

-- Hannes

Re: [PATCH 5/6] gitignore: do not do basename match with patterns that have '**'

From: Nguyen Thai Ngoc Duy <hidden>
Date: 2016-06-15 22:54:57

On Fri, Oct 5, 2012 at 2:01 PM, Johannes Sixt [off-list ref] wrote:
Am 10/4/2012 9:39, schrieb Nguyễn Thái Ngọc Duy:
quoted
- - If the pattern does not contain a slash '/', git treats it as
-   a shell glob pattern and checks for a match against the
-   pathname relative to the location of the `.gitignore` file
-   (relative to the toplevel of the work tree if not from a
-   `.gitignore` file).
+ - If the pattern does not contain a slash '/' nor '**', git
+   treats it as a shell glob pattern and checks for a match
+   against the pathname relative to the location of the
+   `.gitignore` file (relative to the toplevel of the work tree
+   if not from a `.gitignore` file).
I think in the latest round, we forbid this case (i.e. a/**, **/b and
a/**/b are ok, but a**b is not), exactly because it's hard to define
how it should do. Thanks for another example.
quoted
+test_expect_success '"**" with no slashes test' '
+     echo "a**f foo=bar" >.gitattributes &&
+     cat <<\EOF >expect &&
+f: foo: unspecified
+a/f: foo: bar
+a/b/f: foo: bar
+a/b/c/f: foo: bar
+EOF
Should the above .gitattributes match nested paths, such as b/a/c/f?

I think it should, because the user can easily say "/a**f" that nested
paths should not be matched.
The user can also say **/a/**f to match b/a/c/f.
-- 
Duy
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help