Skip to contentSkip to conversation

bringing attributes to pathspecs

PATCH Superseded Brandon Williams 5 participants 23 messages Mar 9, 2017 – Mar 21, 2017 Documentation

lore
Share & tools
lore
#0 lore raw

cover bringing attributes to pathspecs

This small series extends the pathspec magic to allow users to specify attributes that files must have in order for a pathspec to 'match' a file.

One potential use for this is to allow a repository to specify attributes for a set of files. The user can then specify that attribute as a pathspec to perform various operations only on that set of files. One simple example:

	git ls-files -- ":(attr:text)"

can be used to list all of the files with the 'text' attribute.

Brandon Williams (2):
pathspec: allow querying for attributes
pathspec: allow escaped query values

 Documentation/glossary-content.txt |  20 ++++
 attr.c                             |  17 ++++
 attr.h                             |   1 +
 dir.c                              |  43 ++++++++-
 pathspec.c                         | 165 ++++++++++++++++++++++++++++++--
 pathspec.h                         |  16 +++-
 t/t6135-pathspec-with-attrs.sh     | 190 +++++++++++++++++++++++++++++++++++++
 7 files changed, 442 insertions(+), 10 deletions(-)
 create mode 100755 t/t6135-pathspec-with-attrs.sh
Signature · 1 line
2.12.0.246.ga2ecc84866-goog
#1 lore raw

PATCH 1/2 pathspec: allow querying for attributes

The pathspec mechanism is extended via the new ":(attr:eol=input)pattern/to/match" syntax to filter paths so that it requires paths to not just match the given pattern but also have the specified attrs attached for them to be chosen.

Based on a patch by Stefan Beller <[email protected]>

---
 Documentation/glossary-content.txt |  20 ++++
 attr.c                             |  17 ++++
 attr.h                             |   1 +
 dir.c                              |  43 ++++++++-
 pathspec.c                         | 119 +++++++++++++++++++++++-
 pathspec.h                         |  16 +++-
 t/t6135-pathspec-with-attrs.sh     | 181 +++++++++++++++++++++++++++++++++++++
 7 files changed, 388 insertions(+), 9 deletions(-)
 create mode 100755 t/t6135-pathspec-with-attrs.sh
Documentation/glossary-content.txt +20 -0
index fc9320e59..5c32d1905 100644
@@ -384,6 +384,26 @@ full pathname may have special meaning:
 +
 Glob magic is incompatible with literal magic.
 
+attr;;
+After `attr:` comes a space separated list of "attribute
+requirements", all of which must be met in order for the
+path to be considered a match; this is in addition to the
+usual non-magic pathspec pattern matching.
++
+Each of the attribute requirements for the path takes one of
+these forms:
+
+- "`ATTR`" requires that the attribute `ATTR` must be set.
+
+- "`-ATTR`" requires that the attribute `ATTR` must be unset.
+
+- "`ATTR=VALUE`" requires that the attribute `ATTR` must be
+  set to the string `VALUE`.
+
+- "`!ATTR`" requires that the attribute `ATTR` must be
+  unspecified.
++
+
 exclude;;
 	After a path matches any non-exclude pathspec, it will be run
 	through all exclude pathspec (magic signature: `!` or its
attr.c +17 -0
index 5493bff22..7e2134471 100644
@@ -603,6 +603,23 @@ struct attr_check *attr_check_initl(const char *one, ...)
 	return check;
 }
 
+struct attr_check *attr_check_dup(const struct attr_check *check)
+{
+	struct attr_check *ret;
+
+	if (!check)
+		return NULL;
+
+	ret = attr_check_alloc();
+
+	ret->nr = check->nr;
+	ret->alloc = check->alloc;
+	ALLOC_ARRAY(ret->items, ret->nr);
+	COPY_ARRAY(ret->items, check->items, ret->nr);
+
+	return ret;
+}
+
 struct attr_check_item *attr_check_append(struct attr_check *check,
 					  const struct git_attr *attr)
 {
attr.h +1 -0
index 48ab3e1c2..442d464db 100644
@@ -44,6 +44,7 @@ struct attr_check {
 
 extern struct attr_check *attr_check_alloc(void);
 extern struct attr_check *attr_check_initl(const char *, ...);
+extern struct attr_check *attr_check_dup(const struct attr_check *check);
 
 extern struct attr_check_item *attr_check_append(struct attr_check *check,
 						 const struct git_attr *attr);
dir.c +40 -3

79 lines not on this page · raw message

pathspec.c +114 -5

201 lines not on this page · raw message

pathspec.h +15 -1
index 49fd823dd..83625f006 100644
@@ -8,13 +8,15 @@
 #define PATHSPEC_GLOB		(1<<3)
 #define PATHSPEC_ICASE		(1<<4)
 #define PATHSPEC_EXCLUDE	(1<<5)
+#define PATHSPEC_ATTR		(1<<6)
 #define PATHSPEC_ALL_MAGIC	  \
 	(PATHSPEC_FROMTOP	| \
 	 PATHSPEC_MAXDEPTH	| \
 	 PATHSPEC_LITERAL	| \
 	 PATHSPEC_GLOB		| \
 	 PATHSPEC_ICASE		| \
-	 PATHSPEC_EXCLUDE)
+	 PATHSPEC_EXCLUDE	| \
+	 PATHSPEC_ATTR)
 
 #define PATHSPEC_ONESTAR 1	/* the pathspec pattern satisfies GFNM_ONESTAR */
 
@@ -31,6 +33,18 @@ struct pathspec {
 		int len, prefix;
 		int nowildcard_len;
 		int flags;
+		int attr_match_nr;
+		int attr_match_alloc;
+		struct attr_match {
+			char *value;
+			enum attr_match_mode {
+				MATCH_SET,
+				MATCH_UNSET,
+				MATCH_VALUE,
+				MATCH_UNSPECIFIED
+			} match_mode;
+		} *attr_match;
+		struct attr_check *attr_check;
 	} *items;
 };
 
t/t6135-pathspec-with-attrs.sh new file +181 -0

181 lines not on this page · raw message

Signature · 1 line
2.12.0.246.ga2ecc84866-goog
#2 lore raw

Re: [PATCH 1/2] pathspec: allow querying for attributes

Quoted 8 lines · Brandon Williams
Documentation/glossary-content.txt +0 -0
index fc9320e59..5c32d1905 100644
@@ -384,6 +384,26 @@ full pathname may have special meaning:
 +
 Glob magic is incompatible with literal magic.
 

Documentation/glossary-content.txt full pathname may have special meaning: · line 396

Quoted 10 lines
Documentation/glossary-content.txt +10 -0
+attr;;
+After `attr:` comes a space separated list of "attribute
+requirements", all of which must be met in order for the
+path to be considered a match; this is in addition to the
+usual non-magic pathspec pattern matching.
++
+Each of the attribute requirements for the path takes one of
+these forms:
+
+- "`ATTR`" requires that the attribute `ATTR` must be set.

As a relative newcomer to attributes, I was confused by the fact that "set" and "set to a value" is different (and likewise "unset" and "unspecified"). Maybe it's worthwhile including a link to "gitattributes" to explain the different (exclusive) states that an attribute can be in.

Documentation/glossary-content.txt full pathname may have special meaning: · line 404

Quoted 8 lines
Documentation/glossary-content.txt +8 -0
+
+- "`-ATTR`" requires that the attribute `ATTR` must be unset.
+
+- "`ATTR=VALUE`" requires that the attribute `ATTR` must be
+  set to the string `VALUE`.
+
+- "`!ATTR`" requires that the attribute `ATTR` must be
+  unspecified.

It would read better to me if you omitted "must" in all 4 bullet points (and it is redundant anyway with "requires"), but I don't feel too strongly about this.

Quoted 7 lines
pathspec.c +0 -0
index b961f00c8..583ed5208 100644
@@ -87,6 +89,72 @@ static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic)
 	strbuf_addf(sb, ",prefix:%d)", prefixlen);
 }

pathspec.c static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) · line 100

Quoted 10 lines
pathspec.c +9 -0
 
+static void parse_pathspec_attr_match(struct pathspec_item *item, const char *value)
+{
+	struct string_list_item *si;
+	struct string_list list = STRING_LIST_INIT_DUP;
+
+	if (item->attr_check)
+		die(_("Only one 'attr:' specification is allowed."));
+
+	if (!value || !strlen(value))

You can write !*value instead of !strlen(value).

pathspec.c static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) · line 103

Quoted 3 lines
pathspec.c +3 -0
+		die(_("attr spec must not be empty"));
+
+	string_list_split(&list, value, ' ', -1);

You could avoid some allocations by using the in-place variant (since value is a newly allocated string not used elsewhere) but it is probably not that important in this argument parsing case.

pathspec.c static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) · line 109

Quoted 6 lines
pathspec.c +6 -0
+	string_list_remove_empty_items(&list, 0);
+
+	item->attr_check = attr_check_alloc();
+	ALLOC_GROW(item->attr_match,
+		   item->attr_match_nr + list.nr,
+		   item->attr_match_alloc);

Is there a time when this function is called while item->attr_match_nr is not zero?

Quoted 36 lines
pathspec.c +36 -0
+
+	for_each_string_list_item(si, &list) {
+		size_t attr_len;
+		char *attr_name;
+		const struct git_attr *a;
+
+		int j = item->attr_match_nr++;
+		const char *attr = si->string;
+		struct attr_match *am = &item->attr_match[j];
+
+		switch (*attr) {
+		case '!':
+			am->match_mode = MATCH_UNSPECIFIED;
+			attr++;
+			attr_len = strlen(attr);
+			break;
+		case '-':
+			am->match_mode = MATCH_UNSET;
+			attr++;
+			attr_len = strlen(attr);
+			break;
+		default:
+			attr_len = strcspn(attr, "=");
+			if (attr[attr_len] != '=')
+				am->match_mode = MATCH_SET;
+			else {
+				am->match_mode = MATCH_VALUE;
+				am->value = xstrdup(&attr[attr_len + 1]);
+				if (strchr(am->value, '\\'))
+					die(_("attr spec values must not contain backslashes"));
+			}
+			break;
+		}
+
+		attr_name = xmemdupz(attr, attr_len);
+		a = git_attr(attr_name);

pathspec.c static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) · line 155

Quoted 10 lines
pathspec.c +10 -0
+		if (!a)
+			die(_("invalid attribute name %s"), attr_name);
+
+		attr_check_append(item->attr_check, a);
+
+		free(attr_name);
+	}
+
+	string_list_clear(&list, 0);
+	return;

Redundant return?

pathspec.c void parse_pathspec(struct pathspec *pathspec, · line 634

Quoted 8 lines
pathspec.c +4 -0
@@ -544,6 +628,10 @@ void parse_pathspec(struct pathspec *pathspec,
 		if (item[i].nowildcard_len < item[i].len)
 			pathspec->has_wildcard = 1;
 		pathspec->magic |= item[i].magic;
+
+		if (item[i].attr_check &&
+		    item[i].attr_check->nr != item[i].attr_match_nr)
+			die("BUG: should have same number of entries");

I'm not sure if this check is giving us any benefit - I would expect this type of code before some other code that assumed that the numbers matched, and that will potentially segfault if not.

#3 lore raw

Re: [PATCH 1/2] pathspec: allow querying for attributes

Quoted 9 lines · Jonathan Tan
Quoted 8 lines · Brandon Williams
Documentation/glossary-content.txt +1 -0
index fc9320e59..5c32d1905 100644
@@ -384,6 +384,26 @@ full pathname may have special meaning:
+

Glob magic is incompatible with literal magic.

Quoted 16 lines

Documentation/glossary-content.txt full pathname may have special meaning: · line 396

Quoted 10 lines
Documentation/glossary-content.txt +10 -0
+attr;;
+After `attr:` comes a space separated list of "attribute
+requirements", all of which must be met in order for the
+path to be considered a match; this is in addition to the
+usual non-magic pathspec pattern matching.
++
+Each of the attribute requirements for the path takes one of
+these forms:
+
+- "`ATTR`" requires that the attribute `ATTR` must be set.

As a relative newcomer to attributes, I was confused by the fact that "set" and "set to a value" is different (and likewise "unset" and "unspecified"). Maybe it's worthwhile including a link to "gitattributes" to explain the different (exclusive) states that an attribute can be in.

Good idea! I'll add in a link to gitattributes.

Quoted 13 lines

Documentation/glossary-content.txt full pathname may have special meaning: · line 404

Quoted 9 lines
Documentation/glossary-content.txt +8 -0
+
+- "`-ATTR`" requires that the attribute `ATTR` must be unset.
+
+- "`ATTR=VALUE`" requires that the attribute `ATTR` must be
+  set to the string `VALUE`.
+
+- "`!ATTR`" requires that the attribute `ATTR` must be
+  unspecified.

It would read better to me if you omitted "must" in all 4 bullet points (and it is redundant anyway with "requires"), but I don't feel too strongly about this.

I agree, the first paragraph already says "must" so it reads better without repeating must over and over again.

Quoted 9 lines
Quoted 9 lines
pathspec.c +0 -0
index b961f00c8..583ed5208 100644
@@ -87,6 +89,72 @@ static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic)
 	strbuf_addf(sb, ",prefix:%d)", prefixlen);
 }
 
Quoted 12 lines

pathspec.c static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) · line 100

Quoted 9 lines
pathspec.c +9 -0
+static void parse_pathspec_attr_match(struct pathspec_item *item, const char *value)
+{
+	struct string_list_item *si;
+	struct string_list list = STRING_LIST_INIT_DUP;
+
+	if (item->attr_check)
+		die(_("Only one 'attr:' specification is allowed."));
+
+	if (!value || !strlen(value))

You can write !*value instead of !strlen(value).

Done.

Quoted 9 lines

pathspec.c static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) · line 109

Quoted 6 lines
pathspec.c +6 -0
+	string_list_remove_empty_items(&list, 0);
+
+	item->attr_check = attr_check_alloc();
+	ALLOC_GROW(item->attr_match,
+		   item->attr_match_nr + list.nr,
+		   item->attr_match_alloc);

Is there a time when this function is called while item->attr_match_nr is not zero?

Nope, it pretty much has to be zero. I'll change this to just use list.nr. item->attr_match_nr will be incremented up to list.nr over the course of the for loop and I'll move the equality check to the end of this function.

Quoted 4 lines

pathspec.c static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) · line 155

Quoted 2 lines
pathspec.c +2 -0
+	string_list_clear(&list, 0);
+	return;

Redundant return?

I'll remove it.

Quoted 13 lines

pathspec.c void parse_pathspec(struct pathspec *pathspec, · line 634

Quoted 9 lines
pathspec.c +4 -0
@@ -544,6 +628,10 @@ void parse_pathspec(struct pathspec *pathspec,
 		if (item[i].nowildcard_len < item[i].len)
 			pathspec->has_wildcard = 1;
 		pathspec->magic |= item[i].magic;
+
+		if (item[i].attr_check &&
+		    item[i].attr_check->nr != item[i].attr_match_nr)
+			die("BUG: should have same number of entries");

I'm not sure if this check is giving us any benefit - I would expect this type of code before some other code that assumed that the numbers matched, and that will potentially segfault if not.

I'll push the check to right after the object creation (see comment above).

Signature · 1 line
Brandon Williams
#4 lore raw

Re: [PATCH 1/2] pathspec: allow querying for attributes

In reply to Brandon Williams
Quoted 6 lines · Brandon Williams
attr.c +6 -0
+struct attr_check *attr_check_dup(const struct attr_check *check)
+{
+	struct attr_check *ret;
+
+	if (!check)
+		return NULL;

attr.c struct attr_check *attr_check_initl(const char *one, ...) · line 621

Quoted 10 lines
attr.c +10 -0
+
+	ret = attr_check_alloc();
+
+	ret->nr = check->nr;
+	ret->alloc = check->alloc;
+	ALLOC_ARRAY(ret->items, ret->nr);
+	COPY_ARRAY(ret->items, check->items, ret->nr);
+
+	return ret;
+}

Because an attr_check instance cannot be shared and used by multiple threads, we expect that the callers that go multi-thread to copy pathspec to each worker, and preload_index(), which is an existing example of such a caller, already does so with copy_pathspec().

Makes sense.

Quoted 16 lines
pathspec.c +3 -3
@@ -565,26 +653,47 @@ void parse_pathspec(struct pathspec *pathspec,
 
 void copy_pathspec(struct pathspec *dst, const struct pathspec *src)
 {
-	int i;
+	int i, j;
 
 	*dst = *src;
 	ALLOC_ARRAY(dst->items, dst->nr);
 	COPY_ARRAY(dst->items, src->items, dst->nr);
 
 	for (i = 0; i < dst->nr; i++) {
-		dst->items[i].match = xstrdup(src->items[i].match);
-		dst->items[i].original = xstrdup(src->items[i].original);
+		struct pathspec_item *d = &dst->items[i];
+		struct pathspec_item *s = &src->items[i];

pathspec.c void parse_pathspec(struct pathspec *pathspec, · line 674

Quoted 10 lines
pathspec.c +10 -0
+
+		d->match = xstrdup(s->match);
+		d->original = xstrdup(s->original);
+
+		ALLOC_ARRAY(d->attr_match, d->attr_match_nr);
+		COPY_ARRAY(d->attr_match, s->attr_match, d->attr_match_nr);
+		for (j = 0; j < d->attr_match_nr; j++) {
+			const char *value = s->attr_match[j].value;
+			if (value)
+				d->attr_match[j].value = xstrdup(value);

We have xstrdup_or_null(), which may help here.

Quoted 16 lines
pathspec.c +6 -1
+		}
+
+		d->attr_check = attr_check_dup(s->attr_check);
 	}
 }
 
 void clear_pathspec(struct pathspec *pathspec)
 {
-	int i;
+	int i, j;
 
 	for (i = 0; i < pathspec->nr; i++) {
 		free(pathspec->items[i].match);
 		free(pathspec->items[i].original);
+
+		for (j = 0; j < pathspec->items[j].attr_match_nr; j++)

pathspec.c void parse_pathspec(struct pathspec *pathspec, · line 699

Quoted 10 lines
pathspec.c +6 -0
+			free(pathspec->items[i].attr_match[j].value);
+		free(pathspec->items[i].attr_match);
+
+		if (pathspec->items[i].attr_check)
+			attr_check_free(pathspec->items[i].attr_check);
 	}
+
 	free(pathspec->items);
 	pathspec->items = NULL;
 	pathspec->nr = 0;

OK, makes sense.

t/t6135-pathspec-with-attrs.sh line 7

Quoted 13 lines
t/t6135-pathspec-with-attrs.sh new file +7 -0
new file mode 100755
index 000000000..b5e5a0607
@@ -0,0 +1,181 @@
+#!/bin/sh
+
+test_description='test labels in pathspecs'
+. ./test-lib.sh
+
+test_expect_success 'setup a tree' '
+	cat <<-EOF >expect &&

Minor style nit. Quote the 'EOF' marker and you signal to readers that what they'll see are literally the values, and they do not have to worry about $variable_interpolation. I.e.

	cat <<-\EOF >expect &&

t/t6135-pathspec-with-attrs.sh line 167

Quoted 6 lines
t/t6135-pathspec-with-attrs.sh new file +6 -0
+test_expect_success 'fail if attr magic is used places not implemented' '
+	# The main purpose of this test is to check that we actually fail
+	# when you attempt to use attr magic in commands that do not implement
+	# attr magic. This test does not advocate git-add to stay that way,
+	# though, but git-add is convenient as it has its own internal pathspec
+	# parsing.

That's thought-provoking ;-) Would it help to add a test-pathspec helper, similar to test-config helper, that serves as a vehicle to test this?

t/t6135-pathspec-with-attrs.sh line 170

Quoted 3 lines
t/t6135-pathspec-with-attrs.sh new file +3 -0
+	test_must_fail git add ":(attr:labelB)" 2>actual &&
+	test_i18ngrep "unsupported magic" actual
+'

Thanks.

#5 lore raw

Re: [PATCH 1/2] pathspec: allow querying for attributes

Quoted 10 lines

t/t6135-pathspec-with-attrs.sh line 167

Quoted 6 lines
t/t6135-pathspec-with-attrs.sh new file +6 -0
+test_expect_success 'fail if attr magic is used places not implemented' '
+     # The main purpose of this test is to check that we actually fail
+     # when you attempt to use attr magic in commands that do not implement
+     # attr magic. This test does not advocate git-add to stay that way,
+     # though, but git-add is convenient as it has its own internal pathspec
+     # parsing.

That's thought-provoking ;-) Would it help to add a test-pathspec helper, similar to test-config helper, that serves as a vehicle to test this?

I think that is a very good idea; looking through files mentioned by "ls t/ |grep pathspec" most of the tests for pathspecs are done with git-log currently, which also helps with DWIM checking, e.g. if the pathspec part can be interpreted as a ref instead.

So I wonder if such a test-pathspec helper would first only cover this test case of not supporting pathspecs, which sounds a bit counter intuitive.

Thanks,
Stefan

#6 lore raw

PATCH 2/2 pathspec: allow escaped query values

In reply to Brandon Williams

In our own .gitattributes file we have attributes such as:

*.[ch] whitespace=indent,trail,space

When querying for attributes we want to be able to ask for the exact value, i.e.

git ls-files :(attr:whitespace=indent,trail,space)

should work, but the commas are used in the attr magic to introduce the next attr, such that this query currently fails with

fatal: Invalid pathspec magic 'trail' in ':(attr:whitespace=indent,trail,space)'

This change allows escaping characters by a backslash, such that the query

git ls-files :(attr:whitespace=indent\,trail\,space)

will match all path that have the value "indent,trail,space" for the whitespace attribute. To accomplish this, we need to modify two places. First parse_long_magic needs to not stop early upon seeing a comma or closing paren that is escaped. As a second step we need to remove any escaping from the attr value.

Based on a patch by Stefan Beller <[email protected]>

---
 pathspec.c                     | 52 ++++++++++++++++++++++++++++++++++++++----
 t/t6135-pathspec-with-attrs.sh |  9 ++++++++
 2 files changed, 57 insertions(+), 4 deletions(-)
pathspec.c +48 -4
index 583ed5208..f89f84a83 100644
@@ -89,6 +89,51 @@ static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic)
 	strbuf_addf(sb, ",prefix:%d)", prefixlen);
 }
 
+static size_t strcspn_escaped(const char *s, const char *stop)
+{
+	const char *i;
+
+	for (i = s; *i; i++) {
+		/* skip the escaped character */
+		if (i[0] == '\\' && i[1]) {
+			i++;
+			continue;
+		}
+
+		if (strchr(stop, *i))
+			break;
+	}
+	return i - s;
+}
+
+static inline int invalid_value_char(const char ch)
+{
+	if (isalnum(ch) || strchr(",-_", ch))
+		return 0;
+	return -1;
+}
+
+static char *attr_value_unescape(const char *value)
+{
+	const char *src;
+	char *dst, *ret;
+
+	ret = xmallocz(strlen(value));
+	for (src = value, dst = ret; *src; src++, dst++) {
+		if (*src == '\\') {
+			if (!src[1])
+				die(_("Escape character '\\' not allowed as "
+				      "last character in attr value"));
+			src++;
+		}
+		if (invalid_value_char(*src))
+			die("cannot use '%c' for value matching", *src);
+		*dst = *src;
+	}
+	*dst = '\0';
+	return ret;
+}
+
 static void parse_pathspec_attr_match(struct pathspec_item *item, const char *value)
 {
 	struct string_list_item *si;
@@ -133,10 +178,9 @@ static void parse_pathspec_attr_match(struct pathspec_item *item, const char *va
 			if (attr[attr_len] != '=')
 				am->match_mode = MATCH_SET;
 			else {
+				const char *v = &attr[attr_len + 1];
 				am->match_mode = MATCH_VALUE;
-				am->value = xstrdup(&attr[attr_len + 1]);
-				if (strchr(am->value, '\\'))
-					die(_("attr spec values must not contain backslashes"));
+				am->value = attr_value_unescape(v);
 			}
 			break;
 		}
@@ -239,7 +283,7 @@ static const char *parse_long_magic(unsigned *magic, int *prefix_len,
 	const char *nextat;
 
 	for (pos = elem + 2; *pos && *pos != ')'; pos = nextat) {
-		size_t len = strcspn(pos, ",)");
+		size_t len = strcspn_escaped(pos, ",)");
 		int i;
 
 		if (pos[len] == ',')
t/t6135-pathspec-with-attrs.sh +9 -0
index b5e5a0607..585d17bad 100755
@@ -178,4 +178,13 @@ test_expect_success 'abort on asking for wrong magic' '
 	test_must_fail git ls-files . ":(attr:!label=foo)"
 '
 
+test_expect_success 'check attribute list' '
+	cat <<-EOF >>.gitattributes &&
+	* whitespace=indent,trail,space
+	EOF
+	git ls-files ":(attr:whitespace=indent\,trail\,space)" >actual &&
+	git ls-files >expect &&
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
2.12.0.246.ga2ecc84866-goog
#7 lore raw

Re: [PATCH 2/2] pathspec: allow escaped query values

Quoted 8 lines · Brandon Williams
t/t6135-pathspec-with-attrs.sh +0 -0
index b5e5a0607..585d17bad 100755
@@ -178,4 +178,13 @@ test_expect_success 'abort on asking for wrong magic' '
 	test_must_fail git ls-files . ":(attr:!label=foo)"
 '
 

t/t6135-pathspec-with-attrs.sh test_expect_success 'abort on asking for wrong magic' ' · line 190

Quoted 10 lines
t/t6135-pathspec-with-attrs.sh +9 -0
+test_expect_success 'check attribute list' '
+	cat <<-EOF >>.gitattributes &&
+	* whitespace=indent,trail,space
+	EOF
+	git ls-files ":(attr:whitespace=indent\,trail\,space)" >actual &&
+	git ls-files >expect &&
+	test_cmp expect actual
+'
+
 test_done

Is there a way to verify that \, is not escaped by the shell into ,?

Maybe also add tests that show \ as the last character and \ escaping another \.

#8 lore raw

Re: [PATCH 2/2] pathspec: allow escaped query values

Quoted 9 lines · Jonathan Tan
Quoted 8 lines · Brandon Williams
t/t6135-pathspec-with-attrs.sh +0 -0
index b5e5a0607..585d17bad 100755
@@ -178,4 +178,13 @@ test_expect_success 'abort on asking for wrong magic' '
 	test_must_fail git ls-files . ":(attr:!label=foo)"

'

Quoted 12 lines
Quoted 10 lines
t/t6135-pathspec-with-attrs.sh +9 -0
+test_expect_success 'check attribute list' '
+	cat <<-EOF >>.gitattributes &&
+	* whitespace=indent,trail,space
+	EOF
+	git ls-files ":(attr:whitespace=indent\,trail\,space)" >actual &&
+	git ls-files >expect &&
+	test_cmp expect actual
+'
+

test_done

Is there a way to verify that \, is not escaped by the shell into ,?

You can run with GIT_TRACE=1 to see the actual string passed to git. '\,' is indeed passed to git with no problems.

Quoted 3 lines

Maybe also add tests that show \ as the last character and \
escaping another \.

Done

Signature · 1 line
Brandon Williams
#9 lore raw
In reply to Brandon Williams
Quoted 10 lines · Brandon Williams

This small series extends the pathspec magic to allow users to specify attributes that files must have in order for a pathspec to 'match' a file.

One potential use for this is to allow a repository to specify attributes for a set of files. The user can then specify that attribute as a pathspec to perform various operations only on that set of files. One simple example:

        git ls-files -- ":(attr:text)"

can be used to list all of the files with the 'text' attribute.

Thanks for reviving these patches.

I have read through both of them and they look good to me.

Thanks,
Stefan

#10 lore raw

v2 cover bringing attributes to pathspecs

In reply to Brandon Williams

v2 addresses the comments made by Jonathan.

Brandon Williams (2):
pathspec: allow querying for attributes
pathspec: allow escaped query values

 Documentation/glossary-content.txt |  21 ++++
 attr.c                             |  17 ++++
 attr.h                             |   1 +
 dir.c                              |  43 +++++++-
 pathspec.c                         | 163 ++++++++++++++++++++++++++++--
 pathspec.h                         |  16 ++-
 t/t6135-pathspec-with-attrs.sh     | 200 +++++++++++++++++++++++++++++++++++++
 7 files changed, 451 insertions(+), 10 deletions(-)
 create mode 100755 t/t6135-pathspec-with-attrs.sh
Signature · 1 line
2.12.0.246.ga2ecc84866-goog
#11 lore raw

PATCH v2 1/2 pathspec: allow querying for attributes

The pathspec mechanism is extended via the new ":(attr:eol=input)pattern/to/match" syntax to filter paths so that it requires paths to not just match the given pattern but also have the specified attrs attached for them to be chosen.

Based on a patch by Stefan Beller <[email protected]>

---
 Documentation/glossary-content.txt |  21 +++++
 attr.c                             |  17 ++++
 attr.h                             |   1 +
 dir.c                              |  43 ++++++++-
 pathspec.c                         | 117 +++++++++++++++++++++++-
 pathspec.h                         |  16 +++-
 t/t6135-pathspec-with-attrs.sh     | 181 +++++++++++++++++++++++++++++++++++++
 7 files changed, 387 insertions(+), 9 deletions(-)
 create mode 100755 t/t6135-pathspec-with-attrs.sh
Documentation/glossary-content.txt +21 -0
index fc9320e59..6e991c246 100644
@@ -384,6 +384,27 @@ full pathname may have special meaning:
 +
 Glob magic is incompatible with literal magic.
 
+attr;;
+After `attr:` comes a space separated list of "attribute
+requirements", all of which must be met in order for the
+path to be considered a match; this is in addition to the
+usual non-magic pathspec pattern matching.
+See linkgit:gitattributes[5].
++
+Each of the attribute requirements for the path takes one of
+these forms:
+
+- "`ATTR`" requires that the attribute `ATTR` be set.
+
+- "`-ATTR`" requires that the attribute `ATTR` be unset.
+
+- "`ATTR=VALUE`" requires that the attribute `ATTR` be
+  set to the string `VALUE`.
+
+- "`!ATTR`" requires that the attribute `ATTR` be
+  unspecified.
++
+
 exclude;;
 	After a path matches any non-exclude pathspec, it will be run
 	through all exclude pathspec (magic signature: `!` or its
attr.c +17 -0
index 5493bff22..7e2134471 100644
@@ -603,6 +603,23 @@ struct attr_check *attr_check_initl(const char *one, ...)
 	return check;
 }
 
+struct attr_check *attr_check_dup(const struct attr_check *check)
+{
+	struct attr_check *ret;
+
+	if (!check)
+		return NULL;
+
+	ret = attr_check_alloc();
+
+	ret->nr = check->nr;
+	ret->alloc = check->alloc;
+	ALLOC_ARRAY(ret->items, ret->nr);
+	COPY_ARRAY(ret->items, check->items, ret->nr);
+
+	return ret;
+}
+
 struct attr_check_item *attr_check_append(struct attr_check *check,
 					  const struct git_attr *attr)
 {
attr.h +1 -0
index 48ab3e1c2..442d464db 100644
@@ -44,6 +44,7 @@ struct attr_check {
 
 extern struct attr_check *attr_check_alloc(void);
 extern struct attr_check *attr_check_initl(const char *, ...);
+extern struct attr_check *attr_check_dup(const struct attr_check *check);
 
 extern struct attr_check_item *attr_check_append(struct attr_check *check,
 						 const struct git_attr *attr);
dir.c +40 -3

79 lines not on this page · raw message

pathspec.c +112 -5

193 lines not on this page · raw message

pathspec.h +15 -1
index 49fd823dd..83625f006 100644
@@ -8,13 +8,15 @@
 #define PATHSPEC_GLOB		(1<<3)
 #define PATHSPEC_ICASE		(1<<4)
 #define PATHSPEC_EXCLUDE	(1<<5)
+#define PATHSPEC_ATTR		(1<<6)
 #define PATHSPEC_ALL_MAGIC	  \
 	(PATHSPEC_FROMTOP	| \
 	 PATHSPEC_MAXDEPTH	| \
 	 PATHSPEC_LITERAL	| \
 	 PATHSPEC_GLOB		| \
 	 PATHSPEC_ICASE		| \
-	 PATHSPEC_EXCLUDE)
+	 PATHSPEC_EXCLUDE	| \
+	 PATHSPEC_ATTR)
 
 #define PATHSPEC_ONESTAR 1	/* the pathspec pattern satisfies GFNM_ONESTAR */
 
@@ -31,6 +33,18 @@ struct pathspec {
 		int len, prefix;
 		int nowildcard_len;
 		int flags;
+		int attr_match_nr;
+		int attr_match_alloc;
+		struct attr_match {
+			char *value;
+			enum attr_match_mode {
+				MATCH_SET,
+				MATCH_UNSET,
+				MATCH_VALUE,
+				MATCH_UNSPECIFIED
+			} match_mode;
+		} *attr_match;
+		struct attr_check *attr_check;
 	} *items;
 };
 
t/t6135-pathspec-with-attrs.sh new file +181 -0

181 lines not on this page · raw message

Signature · 1 line
2.12.0.246.ga2ecc84866-goog
#12 lore raw

Re: [PATCH v2 1/2] pathspec: allow querying for attributes

Thanks - I don't think I have any more comments on this patch set after these.

Quoted 16 lines · Brandon Williams
pathspec.c +8 -0
index b961f00c8..7cd5f6e3d 100644
@@ -87,6 +89,74 @@ static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic)
 	strbuf_addf(sb, ",prefix:%d)", prefixlen);
 }
 
+static void parse_pathspec_attr_match(struct pathspec_item *item, const char *value)
+{
+	struct string_list_item *si;
+	struct string_list list = STRING_LIST_INIT_DUP;
+
+	if (item->attr_check)
+		die(_("Only one 'attr:' specification is allowed."));
+

pathspec.c static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) · line 109

Quoted 10 lines
pathspec.c +10 -0
+	if (!value || !*value)
+		die(_("attr spec must not be empty"));
+
+	string_list_split(&list, value, ' ', -1);
+	string_list_remove_empty_items(&list, 0);
+
+	item->attr_check = attr_check_alloc();
+	ALLOC_GROW(item->attr_match,
+		   list.nr,
+		   item->attr_match_alloc);

If item->attr_match always starts empty, then I think an xmalloc or xcalloc suffices (and we don't need item->attr_match_alloc anymore).

We should probably also check item->attr_match above - that is, if (item->attr_check || item->attr_match).

Quoted 36 lines
pathspec.c +36 -0
+
+	for_each_string_list_item(si, &list) {
+		size_t attr_len;
+		char *attr_name;
+		const struct git_attr *a;
+
+		int j = item->attr_match_nr++;
+		const char *attr = si->string;
+		struct attr_match *am = &item->attr_match[j];
+
+		switch (*attr) {
+		case '!':
+			am->match_mode = MATCH_UNSPECIFIED;
+			attr++;
+			attr_len = strlen(attr);
+			break;
+		case '-':
+			am->match_mode = MATCH_UNSET;
+			attr++;
+			attr_len = strlen(attr);
+			break;
+		default:
+			attr_len = strcspn(attr, "=");
+			if (attr[attr_len] != '=')
+				am->match_mode = MATCH_SET;
+			else {
+				am->match_mode = MATCH_VALUE;
+				am->value = xstrdup(&attr[attr_len + 1]);
+				if (strchr(am->value, '\\'))
+					die(_("attr spec values must not contain backslashes"));
+			}
+			break;
+		}
+
+		attr_name = xmemdupz(attr, attr_len);
+		a = git_attr(attr_name);

pathspec.c static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) · line 155

Quoted 10 lines
pathspec.c +10 -0
+		if (!a)
+			die(_("invalid attribute name %s"), attr_name);
+
+		attr_check_append(item->attr_check, a);
+
+		free(attr_name);
+	}
+
+	if (item->attr_check->nr != item->attr_match_nr)
+		die("BUG: should have same number of entries");

I think such postcondition checks are usually not worth it, but others might differ.

Quoted 7 lines
pathspec.c +4 -0
+
+	string_list_clear(&list, 0);
+}
+
 static inline int get_literal_global(void)
 {
 	static int literal = -1;
#13 lore raw

Re: [PATCH v2 1/2] pathspec: allow querying for attributes

Quoted 34 lines · Jonathan Tan

Thanks - I don't think I have any more comments on this patch set after these.

Quoted 16 lines · Brandon Williams
pathspec.c +8 -0
index b961f00c8..7cd5f6e3d 100644
@@ -87,6 +89,74 @@ static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic)
 	strbuf_addf(sb, ",prefix:%d)", prefixlen);
 }
 
+static void parse_pathspec_attr_match(struct pathspec_item *item, const char *value)
+{
+	struct string_list_item *si;
+	struct string_list list = STRING_LIST_INIT_DUP;
+
+	if (item->attr_check)
+		die(_("Only one 'attr:' specification is allowed."));
+

pathspec.c static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) · line 109

Quoted 10 lines
pathspec.c +10 -0
+	if (!value || !*value)
+		die(_("attr spec must not be empty"));
+
+	string_list_split(&list, value, ' ', -1);
+	string_list_remove_empty_items(&list, 0);
+
+	item->attr_check = attr_check_alloc();
+	ALLOC_GROW(item->attr_match,
+		   list.nr,
+		   item->attr_match_alloc);

If item->attr_match always starts empty, then I think an xmalloc or xcalloc suffices (and we don't need item->attr_match_alloc anymore).

Quoted 2 lines

We should probably also check item->attr_match above - that is, if (item->attr_check || item->attr_match).

Correct, I'll make these changes.

Quoted 42 lines
Quoted 42 lines
pathspec.c +41 -0
+
+	for_each_string_list_item(si, &list) {
+		size_t attr_len;
+		char *attr_name;
+		const struct git_attr *a;
+
+		int j = item->attr_match_nr++;
+		const char *attr = si->string;
+		struct attr_match *am = &item->attr_match[j];
+
+		switch (*attr) {
+		case '!':
+			am->match_mode = MATCH_UNSPECIFIED;
+			attr++;
+			attr_len = strlen(attr);
+			break;
+		case '-':
+			am->match_mode = MATCH_UNSET;
+			attr++;
+			attr_len = strlen(attr);
+			break;
+		default:
+			attr_len = strcspn(attr, "=");
+			if (attr[attr_len] != '=')
+				am->match_mode = MATCH_SET;
+			else {
+				am->match_mode = MATCH_VALUE;
+				am->value = xstrdup(&attr[attr_len + 1]);
+				if (strchr(am->value, '\\'))
+					die(_("attr spec values must not contain backslashes"));
+			}
+			break;
+		}
+
+		attr_name = xmemdupz(attr, attr_len);
+		a = git_attr(attr_name);
+		if (!a)
+			die(_("invalid attribute name %s"), attr_name);
+
+		attr_check_append(item->attr_check, a);
+
Quoted 8 lines

pathspec.c static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) · line 155

Quoted 5 lines
pathspec.c +5 -0
+		free(attr_name);
+	}
+
+	if (item->attr_check->nr != item->attr_match_nr)
+		die("BUG: should have same number of entries");

I think such postcondition checks are usually not worth it, but others might differ.

yeah probably not, but its just an assert check for just in case so I'll leave it in.

Quoted 8 lines
Quoted 8 lines
pathspec.c +4 -0
+
+	string_list_clear(&list, 0);
+}
+
 static inline int get_literal_global(void)
 {
 	static int literal = -1;
Signature · 1 line
Brandon Williams
#14 lore raw

PATCH v2 2/2 pathspec: allow escaped query values

In reply to Brandon Williams

In our own .gitattributes file we have attributes such as:

*.[ch] whitespace=indent,trail,space

When querying for attributes we want to be able to ask for the exact value, i.e.

git ls-files :(attr:whitespace=indent,trail,space)

should work, but the commas are used in the attr magic to introduce the next attr, such that this query currently fails with

fatal: Invalid pathspec magic 'trail' in ':(attr:whitespace=indent,trail,space)'

This change allows escaping characters by a backslash, such that the query

git ls-files :(attr:whitespace=indent\,trail\,space)

will match all path that have the value "indent,trail,space" for the whitespace attribute. To accomplish this, we need to modify two places. First parse_long_magic needs to not stop early upon seeing a comma or closing paren that is escaped. As a second step we need to remove any escaping from the attr value.

Based on a patch by Stefan Beller <[email protected]>

---
 pathspec.c                     | 52 ++++++++++++++++++++++++++++++++++++++----
 t/t6135-pathspec-with-attrs.sh | 19 +++++++++++++++
 2 files changed, 67 insertions(+), 4 deletions(-)
pathspec.c +48 -4
index 7cd5f6e3d..d7956f6bf 100644
@@ -89,6 +89,51 @@ static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic)
 	strbuf_addf(sb, ",prefix:%d)", prefixlen);
 }
 
+static size_t strcspn_escaped(const char *s, const char *stop)
+{
+	const char *i;
+
+	for (i = s; *i; i++) {
+		/* skip the escaped character */
+		if (i[0] == '\\' && i[1]) {
+			i++;
+			continue;
+		}
+
+		if (strchr(stop, *i))
+			break;
+	}
+	return i - s;
+}
+
+static inline int invalid_value_char(const char ch)
+{
+	if (isalnum(ch) || strchr(",-_", ch))
+		return 0;
+	return -1;
+}
+
+static char *attr_value_unescape(const char *value)
+{
+	const char *src;
+	char *dst, *ret;
+
+	ret = xmallocz(strlen(value));
+	for (src = value, dst = ret; *src; src++, dst++) {
+		if (*src == '\\') {
+			if (!src[1])
+				die(_("Escape character '\\' not allowed as "
+				      "last character in attr value"));
+			src++;
+		}
+		if (invalid_value_char(*src))
+			die("cannot use '%c' for value matching", *src);
+		*dst = *src;
+	}
+	*dst = '\0';
+	return ret;
+}
+
 static void parse_pathspec_attr_match(struct pathspec_item *item, const char *value)
 {
 	struct string_list_item *si;
@@ -133,10 +178,9 @@ static void parse_pathspec_attr_match(struct pathspec_item *item, const char *va
 			if (attr[attr_len] != '=')
 				am->match_mode = MATCH_SET;
 			else {
+				const char *v = &attr[attr_len + 1];
 				am->match_mode = MATCH_VALUE;
-				am->value = xstrdup(&attr[attr_len + 1]);
-				if (strchr(am->value, '\\'))
-					die(_("attr spec values must not contain backslashes"));
+				am->value = attr_value_unescape(v);
 			}
 			break;
 		}
@@ -241,7 +285,7 @@ static const char *parse_long_magic(unsigned *magic, int *prefix_len,
 	const char *nextat;
 
 	for (pos = elem + 2; *pos && *pos != ')'; pos = nextat) {
-		size_t len = strcspn(pos, ",)");
+		size_t len = strcspn_escaped(pos, ",)");
 		int i;
 
 		if (pos[len] == ',')
t/t6135-pathspec-with-attrs.sh +19 -0
index b5e5a0607..f60af29a9 100755
@@ -178,4 +178,23 @@ test_expect_success 'abort on asking for wrong magic' '
 	test_must_fail git ls-files . ":(attr:!label=foo)"
 '
 
+test_expect_success 'check attribute list' '
+	cat <<-EOF >>.gitattributes &&
+	* whitespace=indent,trail,space
+	EOF
+	git ls-files ":(attr:whitespace=indent\,trail\,space)" >actual &&
+	git ls-files >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'backslash cannot be the last character' '
+	test_must_fail git ls-files ":(attr:label=foo\\ labelA=bar)" 2>actual &&
+	test_i18ngrep "not allowed as last character in attr value" actual
+'
+
+test_expect_success 'backslash cannot be used as a value' '
+	test_must_fail git ls-files ":(attr:label=f\\\oo)" 2>actual &&
+	test_i18ngrep "for value matching" actual
+'
+
 test_done
Signature · 1 line
2.12.0.246.ga2ecc84866-goog
#15 lore raw

v3 cover bringing attributes to pathspecs

In reply to Brandon Williams

v3 fixes some nits in style in the test script (using <<-\EOF instead of <<-EOF) as well as fixing a few other minor things reported by Junio and Jonathan.

Brandon Williams (2):
pathspec: allow querying for attributes
pathspec: allow escaped query values

 Documentation/glossary-content.txt |  21 ++++
 attr.c                             |  17 ++++
 attr.h                             |   1 +
 dir.c                              |  43 +++++++-
 pathspec.c                         | 159 +++++++++++++++++++++++++++--
 pathspec.h                         |  15 ++-
 t/t6135-pathspec-with-attrs.sh     | 200 +++++++++++++++++++++++++++++++++++++
 7 files changed, 446 insertions(+), 10 deletions(-)
 create mode 100755 t/t6135-pathspec-with-attrs.sh

--- interdiff with bw/attr-pathspec

pathspec.c +3 -7
index d7956f6bf..303efda83 100644
@@ -139,7 +139,7 @@ static void parse_pathspec_attr_match(struct pathspec_item *item, const char *va
 	struct string_list_item *si;
 	struct string_list list = STRING_LIST_INIT_DUP;
 
-	if (item->attr_check)
+	if (item->attr_check || item->attr_match)
 		die(_("Only one 'attr:' specification is allowed."));
 
 	if (!value || !*value)
@@ -149,9 +149,7 @@ static void parse_pathspec_attr_match(struct pathspec_item *item, const char *va
 	string_list_remove_empty_items(&list, 0);
 
 	item->attr_check = attr_check_alloc();
-	ALLOC_GROW(item->attr_match,
-		   list.nr,
-		   item->attr_match_alloc);
+	item->attr_match = xcalloc(list.nr, sizeof(struct attr_match));
 
 	for_each_string_list_item(si, &list) {
 		size_t attr_len;
@@ -462,7 +460,6 @@ static void init_pathspec_item(struct pathspec_item *item, unsigned flags,
 	item->attr_check = NULL;
 	item->attr_match = NULL;
 	item->attr_match_nr = 0;
-	item->attr_match_alloc = 0;
 
 	/* PATHSPEC_LITERAL_PATH ignores magic */
 	if (flags & PATHSPEC_LITERAL_PATH) {
@@ -712,8 +709,7 @@ void copy_pathspec(struct pathspec *dst, const struct pathspec *src)
 		COPY_ARRAY(d->attr_match, s->attr_match, d->attr_match_nr);
 		for (j = 0; j < d->attr_match_nr; j++) {
 			const char *value = s->attr_match[j].value;
-			if (value)
-				d->attr_match[j].value = xstrdup(value);
+			d->attr_match[j].value = xstrdup_or_null(value);
 		}
 
 		d->attr_check = attr_check_dup(s->attr_check);
pathspec.h +0 -1
index 83625f006..55e976972 100644
@@ -34,7 +34,6 @@ struct pathspec {
 		int nowildcard_len;
 		int flags;
 		int attr_match_nr;
-		int attr_match_alloc;
 		struct attr_match {
 			char *value;
 			enum attr_match_mode {
t/t6135-pathspec-with-attrs.sh +9 -9
index f60af29a9..77b8cef66 100755
@@ -4,7 +4,7 @@ test_description='test labels in pathspecs'
 . ./test-lib.sh
 
 test_expect_success 'setup a tree' '
-	cat <<-EOF >expect &&
+	cat <<-\EOF >expect &&
 	fileA
 	fileAB
 	fileAC
@@ -49,7 +49,7 @@ test_expect_success 'pathspec with labels and non existent .gitattributes' '
 '
 
 test_expect_success 'setup .gitattributes' '
-	cat <<-EOF >.gitattributes &&
+	cat <<-\EOF >.gitattributes &&
 	fileA labelA
 	fileB labelB
 	fileC labelC
@@ -66,7 +66,7 @@ test_expect_success 'setup .gitattributes' '
 '
 
 test_expect_success 'check specific set attr' '
-	cat <<-EOF >expect &&
+	cat <<-\EOF >expect &&
 	fileSetLabel
 	sub/fileSetLabel
 	EOF
@@ -75,7 +75,7 @@ test_expect_success 'check specific set attr' '
 '
 
 test_expect_success 'check specific unset attr' '
-	cat <<-EOF >expect &&
+	cat <<-\EOF >expect &&
 	fileUnsetLabel
 	sub/fileUnsetLabel
 	EOF
@@ -84,7 +84,7 @@ test_expect_success 'check specific unset attr' '
 '
 
 test_expect_success 'check specific value attr' '
-	cat <<-EOF >expect &&
+	cat <<-\EOF >expect &&
 	fileValue
 	sub/fileValue
 	EOF
@@ -95,7 +95,7 @@ test_expect_success 'check specific value attr' '
 '
 
 test_expect_success 'check unspecified attr' '
-	cat <<-EOF >expect &&
+	cat <<-\EOF >expect &&
 	.gitattributes
 	fileA
 	fileAB
@@ -119,7 +119,7 @@ test_expect_success 'check unspecified attr' '
 '
 
 test_expect_success 'check multiple unspecified attr' '
-	cat <<-EOF >expect &&
+	cat <<-\EOF >expect &&
 	.gitattributes
 	fileC
 	fileNoLabel
@@ -133,7 +133,7 @@ test_expect_success 'check multiple unspecified attr' '
 '
 
 test_expect_success 'check label with more labels but excluded path' '
-	cat <<-EOF >expect &&
+	cat <<-\EOF >expect &&
 	fileAB
 	fileB
 	fileBC
@@ -143,7 +143,7 @@ test_expect_success 'check label with more labels but excluded path' '
 '
 
 test_expect_success 'check label excluding other labels' '
-	cat <<-EOF >expect &&
+	cat <<-\EOF >expect &&
 	fileAB
 	fileB
 	fileBC
Signature · 1 line
2.12.0.246.ga2ecc84866-goog
#16 lore raw

PATCH v3 1/2 pathspec: allow querying for attributes

The pathspec mechanism is extended via the new ":(attr:eol=input)pattern/to/match" syntax to filter paths so that it requires paths to not just match the given pattern but also have the specified attrs attached for them to be chosen.

Based on a patch by Stefan Beller <[email protected]>

---
 Documentation/glossary-content.txt |  21 +++++
 attr.c                             |  17 ++++
 attr.h                             |   1 +
 dir.c                              |  43 ++++++++-
 pathspec.c                         | 113 ++++++++++++++++++++++-
 pathspec.h                         |  15 ++-
 t/t6135-pathspec-with-attrs.sh     | 181 +++++++++++++++++++++++++++++++++++++
 7 files changed, 382 insertions(+), 9 deletions(-)
 create mode 100755 t/t6135-pathspec-with-attrs.sh
Documentation/glossary-content.txt +21 -0
index fc9320e59..6e991c246 100644
@@ -384,6 +384,27 @@ full pathname may have special meaning:
 +
 Glob magic is incompatible with literal magic.
 
+attr;;
+After `attr:` comes a space separated list of "attribute
+requirements", all of which must be met in order for the
+path to be considered a match; this is in addition to the
+usual non-magic pathspec pattern matching.
+See linkgit:gitattributes[5].
++
+Each of the attribute requirements for the path takes one of
+these forms:
+
+- "`ATTR`" requires that the attribute `ATTR` be set.
+
+- "`-ATTR`" requires that the attribute `ATTR` be unset.
+
+- "`ATTR=VALUE`" requires that the attribute `ATTR` be
+  set to the string `VALUE`.
+
+- "`!ATTR`" requires that the attribute `ATTR` be
+  unspecified.
++
+
 exclude;;
 	After a path matches any non-exclude pathspec, it will be run
 	through all exclude pathspec (magic signature: `!` or its
attr.c +17 -0
index 5493bff22..7e2134471 100644
@@ -603,6 +603,23 @@ struct attr_check *attr_check_initl(const char *one, ...)
 	return check;
 }
 
+struct attr_check *attr_check_dup(const struct attr_check *check)
+{
+	struct attr_check *ret;
+
+	if (!check)
+		return NULL;
+
+	ret = attr_check_alloc();
+
+	ret->nr = check->nr;
+	ret->alloc = check->alloc;
+	ALLOC_ARRAY(ret->items, ret->nr);
+	COPY_ARRAY(ret->items, check->items, ret->nr);
+
+	return ret;
+}
+
 struct attr_check_item *attr_check_append(struct attr_check *check,
 					  const struct git_attr *attr)
 {
attr.h +1 -0
index 48ab3e1c2..442d464db 100644
@@ -44,6 +44,7 @@ struct attr_check {
 
 extern struct attr_check *attr_check_alloc(void);
 extern struct attr_check *attr_check_initl(const char *, ...);
+extern struct attr_check *attr_check_dup(const struct attr_check *check);
 
 extern struct attr_check_item *attr_check_append(struct attr_check *check,
 						 const struct git_attr *attr);
dir.c +40 -3

79 lines not on this page · raw message

pathspec.c +108 -5

189 lines not on this page · raw message

pathspec.h +14 -1
index 49fd823dd..55e976972 100644
@@ -8,13 +8,15 @@
 #define PATHSPEC_GLOB		(1<<3)
 #define PATHSPEC_ICASE		(1<<4)
 #define PATHSPEC_EXCLUDE	(1<<5)
+#define PATHSPEC_ATTR		(1<<6)
 #define PATHSPEC_ALL_MAGIC	  \
 	(PATHSPEC_FROMTOP	| \
 	 PATHSPEC_MAXDEPTH	| \
 	 PATHSPEC_LITERAL	| \
 	 PATHSPEC_GLOB		| \
 	 PATHSPEC_ICASE		| \
-	 PATHSPEC_EXCLUDE)
+	 PATHSPEC_EXCLUDE	| \
+	 PATHSPEC_ATTR)
 
 #define PATHSPEC_ONESTAR 1	/* the pathspec pattern satisfies GFNM_ONESTAR */
 
@@ -31,6 +33,17 @@ struct pathspec {
 		int len, prefix;
 		int nowildcard_len;
 		int flags;
+		int attr_match_nr;
+		struct attr_match {
+			char *value;
+			enum attr_match_mode {
+				MATCH_SET,
+				MATCH_UNSET,
+				MATCH_VALUE,
+				MATCH_UNSPECIFIED
+			} match_mode;
+		} *attr_match;
+		struct attr_check *attr_check;
 	} *items;
 };
 
t/t6135-pathspec-with-attrs.sh new file +181 -0

181 lines not on this page · raw message

Signature · 1 line
2.12.0.246.ga2ecc84866-goog
#17 lore raw

PATCH v3 2/2 pathspec: allow escaped query values

In reply to Brandon Williams

In our own .gitattributes file we have attributes such as:

*.[ch] whitespace=indent,trail,space

When querying for attributes we want to be able to ask for the exact value, i.e.

git ls-files :(attr:whitespace=indent,trail,space)

should work, but the commas are used in the attr magic to introduce the next attr, such that this query currently fails with

fatal: Invalid pathspec magic 'trail' in ':(attr:whitespace=indent,trail,space)'

This change allows escaping characters by a backslash, such that the query

git ls-files :(attr:whitespace=indent\,trail\,space)

will match all path that have the value "indent,trail,space" for the whitespace attribute. To accomplish this, we need to modify two places. First parse_long_magic needs to not stop early upon seeing a comma or closing paren that is escaped. As a second step we need to remove any escaping from the attr value.

Based on a patch by Stefan Beller <[email protected]>

---
 pathspec.c                     | 52 ++++++++++++++++++++++++++++++++++++++----
 t/t6135-pathspec-with-attrs.sh | 19 +++++++++++++++
 2 files changed, 67 insertions(+), 4 deletions(-)
pathspec.c +48 -4
index 608511c52..303efda83 100644
@@ -89,6 +89,51 @@ static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic)
 	strbuf_addf(sb, ",prefix:%d)", prefixlen);
 }
 
+static size_t strcspn_escaped(const char *s, const char *stop)
+{
+	const char *i;
+
+	for (i = s; *i; i++) {
+		/* skip the escaped character */
+		if (i[0] == '\\' && i[1]) {
+			i++;
+			continue;
+		}
+
+		if (strchr(stop, *i))
+			break;
+	}
+	return i - s;
+}
+
+static inline int invalid_value_char(const char ch)
+{
+	if (isalnum(ch) || strchr(",-_", ch))
+		return 0;
+	return -1;
+}
+
+static char *attr_value_unescape(const char *value)
+{
+	const char *src;
+	char *dst, *ret;
+
+	ret = xmallocz(strlen(value));
+	for (src = value, dst = ret; *src; src++, dst++) {
+		if (*src == '\\') {
+			if (!src[1])
+				die(_("Escape character '\\' not allowed as "
+				      "last character in attr value"));
+			src++;
+		}
+		if (invalid_value_char(*src))
+			die("cannot use '%c' for value matching", *src);
+		*dst = *src;
+	}
+	*dst = '\0';
+	return ret;
+}
+
 static void parse_pathspec_attr_match(struct pathspec_item *item, const char *value)
 {
 	struct string_list_item *si;
@@ -131,10 +176,9 @@ static void parse_pathspec_attr_match(struct pathspec_item *item, const char *va
 			if (attr[attr_len] != '=')
 				am->match_mode = MATCH_SET;
 			else {
+				const char *v = &attr[attr_len + 1];
 				am->match_mode = MATCH_VALUE;
-				am->value = xstrdup(&attr[attr_len + 1]);
-				if (strchr(am->value, '\\'))
-					die(_("attr spec values must not contain backslashes"));
+				am->value = attr_value_unescape(v);
 			}
 			break;
 		}
@@ -239,7 +283,7 @@ static const char *parse_long_magic(unsigned *magic, int *prefix_len,
 	const char *nextat;
 
 	for (pos = elem + 2; *pos && *pos != ')'; pos = nextat) {
-		size_t len = strcspn(pos, ",)");
+		size_t len = strcspn_escaped(pos, ",)");
 		int i;
 
 		if (pos[len] == ',')
t/t6135-pathspec-with-attrs.sh +19 -0
index 3f06fcf8d..77b8cef66 100755
@@ -178,4 +178,23 @@ test_expect_success 'abort on asking for wrong magic' '
 	test_must_fail git ls-files . ":(attr:!label=foo)"
 '
 
+test_expect_success 'check attribute list' '
+	cat <<-EOF >>.gitattributes &&
+	* whitespace=indent,trail,space
+	EOF
+	git ls-files ":(attr:whitespace=indent\,trail\,space)" >actual &&
+	git ls-files >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'backslash cannot be the last character' '
+	test_must_fail git ls-files ":(attr:label=foo\\ labelA=bar)" 2>actual &&
+	test_i18ngrep "not allowed as last character in attr value" actual
+'
+
+test_expect_success 'backslash cannot be used as a value' '
+	test_must_fail git ls-files ":(attr:label=f\\\oo)" 2>actual &&
+	test_i18ngrep "for value matching" actual
+'
+
 test_done
Signature · 1 line
2.12.0.246.ga2ecc84866-goog
#18 lore raw
In reply to Brandon Williams
Quoted 2 lines · Brandon Williams

v3 fixes some nits in style in the test script (using <<-\EOF instead of <<-EOF) as well as fixing a few other minor things reported by Junio and Jonathan.

Thanks. Will replace.

I think this is ready for 'next', so let's ask reviewers to really pay attention to this round, wait for a few days and merge it by the end of the week at the latest.

Thanks.

#19 lore raw
Quoted 10 lines · Junio C Hamano
Quoted 2 lines · Brandon Williams

v3 fixes some nits in style in the test script (using <<-\EOF instead of <<-EOF) as well as fixing a few other minor things reported by Junio and Jonathan.

Thanks. Will replace.

I think this is ready for 'next', so let's ask reviewers to really pay attention to this round, wait for a few days and merge it by the end of the week at the latest.

Sounds good to me, Thanks!

Signature · 1 line
Brandon Williams
#20 lore raw
In reply to Brandon Williams
Quoted 2 lines · Brandon Williams

v3 fixes some nits in style in the test script (using <<-\EOF instead of <<-EOF) as well as fixing a few other minor things reported by Junio and Jonathan.

I'm slowly digging through the pile of mails in the past weeks... I know this has landed on 'master' (thanks!). Just wanted to check something.

The series updated match_pathspec(), but that's only one of two pathspec filtering functions. The other is tree_entry_interesting() (e.g. for "git grep <tree>"). Do you have plans to support :(attr) there too? "No" is a perfectly fine answer (and it will end up in my forever growing backlog).

The thing about tree_entry_interesting() is, we would want to stop traversing subtrees as soon as possible. Naively implemented, we would need to traverse all subtrees so we can call match_attrs(). That's not great. Oii I'm rambling.. I don't know yet how to implement this thing efficiently.

Signature · 1 line
Duy
#21 lore raw
Quoted 11 lines · Duy Nguyen

The series updated match_pathspec(), but that's only one of two pathspec filtering functions. The other is tree_entry_interesting() (e.g. for "git grep <tree>"). Do you have plans to support :(attr) there too? "No" is a perfectly fine answer (and it will end up in my forever growing backlog).

The thing about tree_entry_interesting() is, we would want to stop traversing subtrees as soon as possible. Naively implemented, we would need to traverse all subtrees so we can call match_attrs(). That's not great. Oii I'm rambling.. I don't know yet how to implement this thing efficiently.

Thanks for great insights.

It indeed will become issue when an overly broad pathspec pattern is combined with an attribute requirement, e.g. ".:(attr=X)", and we may have to devise a way to tell that there won't be any paths with that satisfy the attribute requirement before descending into a tree as an optimization.

#22 lore raw
In reply to Duy Nguyen
Quoted 14 lines · Duy Nguyen
Quoted 2 lines · Brandon Williams

v3 fixes some nits in style in the test script (using <<-\EOF instead of <<-EOF) as well as fixing a few other minor things reported by Junio and Jonathan.

I'm slowly digging through the pile of mails in the past weeks... I know this has landed on 'master' (thanks!). Just wanted to check something.

The series updated match_pathspec(), but that's only one of two pathspec filtering functions. The other is tree_entry_interesting() (e.g. for "git grep <tree>"). Do you have plans to support :(attr) there too? "No" is a perfectly fine answer (and it will end up in my forever growing backlog).

Quoted 5 lines

The thing about tree_entry_interesting() is, we would want to stop traversing subtrees as soon as possible. Naively implemented, we would need to traverse all subtrees so we can call match_attrs(). That's not great. Oii I'm rambling.. I don't know yet how to implement this thing efficiently.

I expect that this should be supported in tree_entry_interesting() at some point, though I didn't have any immediate plans to do that yet. One reason for that is I just haven't thought through all the cases to make it performant (as you've pointed out). So for now it'll probably be appended to the backlog (yours, mine, or both) and if there ends up being a larger demand for it then we can increase the priority for it.

Signature · 1 line
Brandon Williams