[PATCHv8 0/5] pathspec magic extension to search for attributes

Subsystems: documentation, the rest

6 messages, 1 author, 2016-06-16 · open the first message on its own page

[PATCHv8 0/5] pathspec magic extension to search for attributes

From: Stefan Beller <hidden>
Date: 2016-06-16 02:19:30

This goes on top of origin/jc/attr, (396bf756f95, attr: expose validity check
for attribute names)

Patches 1 and 2 are small fixes, which could go independently as well.
Patches 3 and 4 are refactoring pathspec.c a little.
These did not change since v7


diff to v7:
diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt
index 866e8d8..aa9f220 100644
--- a/Documentation/glossary-content.txt
+++ b/Documentation/glossary-content.txt
@@ -394,11 +394,9 @@ you can query each attribute for certain states. The "`[mode]`" is a special
 character to indicate which attribute states are looked for. The following
 modes are available:

- - "`+`" the attribute must be set
+ - an empty "`[mode]`" matches if the attribute is set
  - "`-`" the attribute must be unset
- - "`~`" the attribute must be unspecified
- - "`?`" the attribute must not be unspecified, i.e. set, unset or value matches
- - an empty "`[mode]`" matches if the attribute is set or has a value
+ - "`!`" the attribute must be unspecified
  - an empty "`[mode]`" combined with "`[=value]`" matches if the attribute has
    the given value.
 +
diff --git a/dir.c b/dir.c
index 3141a5a..f60a503 100644
--- a/dir.c
+++ b/dir.c
@@ -219,42 +219,31 @@ int within_depth(const char *name, int namelen,
 static int match_attrs(const char *name, int namelen,
 		       const struct pathspec_item *item)
 {
-	char *path;
 	int i;

-	path = xmemdupz(name, namelen);
-	git_check_attr(path, item->attr_check);
-
+	git_check_attr_counted(name, namelen, item->attr_check);
 	for (i = 0; i < item->attr_match_nr; i++) {
 		const char *value;
 		int matched;
 		enum attr_match_mode match_mode;

 		value = item->attr_check->check[i].value;
-
 		match_mode = item->attr_match[i].match_mode;

-		if (ATTR_TRUE(value)) {
-			matched = match_mode == MATCH_SET ||
-				  match_mode == MATCH_SET_OR_VALUE ||
-				  match_mode == MATCH_NOT_UNSPECIFIED;
-		} else if (ATTR_FALSE(value)) {
-			matched = match_mode == MATCH_UNSET ||
-				  match_mode == MATCH_NOT_UNSPECIFIED;
-		} else if (ATTR_UNSET(value)) {
+		if (ATTR_TRUE(value))
+			matched = match_mode == MATCH_SET;
+		else if (ATTR_FALSE(value))
+			matched = match_mode == MATCH_UNSET;
+		else if (ATTR_UNSET(value))
 			matched = match_mode == MATCH_UNSPECIFIED;
-		} else {
-			matched = match_mode == MATCH_NOT_UNSPECIFIED ||
-				  match_mode == MATCH_SET_OR_VALUE ||
-				  (match_mode == MATCH_VALUE &&
+		else
+			matched = (match_mode == MATCH_VALUE &&
 				   !strcmp(item->attr_match[i].value, value));
-		}
+
 		if (!matched)
 			return 0;
 	}

-	free(path);
-
 	return 1;
 }
diff --git a/pathspec.c b/pathspec.c
index 32fb6a8..b795a9c 100644
--- a/pathspec.c
+++ b/pathspec.c
@@ -96,66 +96,58 @@ static void parse_pathspec_attr_match(struct pathspec_item *item, const char *va


 	if (!value || !strlen(value))
-		goto err;
+		die(_("attr spec must not be empty"));

 	string_list_split(&list, value, ' ', -1);
 	string_list_remove_empty_items(&list, 0);

 	if (!item->attr_check)
 		item->attr_check = git_attr_check_alloc();
+	else
+		die(_("Only one 'attr:' specification is allowed."));

 	ALLOC_GROW(item->attr_match, item->attr_match_nr + list.nr, item->attr_match_alloc);

 	for_each_string_list_item(si, &list) {
-		size_t val_len;
+		size_t attr_len;

 		int j = item->attr_match_nr++;
-		const char *val = si->string;
+		const char *attr = si->string;
 		struct attr_match *am = &item->attr_match[j];

-		if (val[0] == '?')
-			am->match_mode = MATCH_NOT_UNSPECIFIED;
-		else if (val[0] == '~')
+		if (attr[0] == '!')
 			am->match_mode = MATCH_UNSPECIFIED;
-		else if (val[0] == '+')
-			am->match_mode = MATCH_SET;
-		else if (val[0] == '-')
+		else if (attr[0] == '-')
 			am->match_mode = MATCH_UNSET;
 		else
-			am->match_mode = MATCH_SET_OR_VALUE;
+			am->match_mode = MATCH_SET;

-		if (am->match_mode != MATCH_SET_OR_VALUE)
+		if (am->match_mode != MATCH_SET)
 			/* skip first character */
-			val++;
+			attr++;

-		val_len = strcspn(val, "=,)");
-		if (val[val_len] == '=') {
+		attr_len = strcspn(attr, "=");
+		if (attr[attr_len] == '=') {
 			am->match_mode = MATCH_VALUE;
-			am->value = xstrdup(&val[val_len + 1]);
-			/*
-			 * NEEDSWORK:
-			 * Do we want to allow escaped commas to search
-			 * for comma separated values?
-			 */
+			am->value = xstrdup(&attr[attr_len + 1]);
 			if (strchr(am->value, '\\'))
 				die(_("attr spec values must not contain backslashes"));
 		} else
 			am->value = NULL;

-		if (invalid_attr_name(val, val_len)) {
+		if (!attr_name_valid(attr, attr_len)) {
+			struct strbuf sb = STRBUF_INIT;
 			am->match_mode = INVALID_ATTR;
-			goto err;
+			invalid_attr_name_message(&sb, attr, attr_len);
+			die(_("invalid attribute in '%s': '%s'"), value, sb.buf);
 		}

-		am->attr = git_attr(xmemdupz(val, val_len));
+		am->attr = git_attr_counted(attr, attr_len);
 		git_attr_check_append(item->attr_check, am->attr);
 	}

 	string_list_clear(&list, 0);
 	return;
-err:
-	die(_("attr spec '%s': attrs must not start with '-' and "
-	      "be composed of [-A-Za-z0-9_.]."), value);
 }

 static void eat_long_magic(struct pathspec_item *item, const char *elt,
@@ -188,9 +180,9 @@ static void eat_long_magic(struct pathspec_item *item, const char *elt,
 		}

 		if (skip_prefix(copyfrom, "attr:", &body)) {
-			char *pass = xmemdupz(body, len - strlen("attr:"));
-			parse_pathspec_attr_match(item, pass);
-			free(pass);
+			char *attr_body = xmemdupz(body, len - strlen("attr:"));
+			parse_pathspec_attr_match(item, attr_body);
+			free(attr_body);
 			continue;
 		}
@@ -591,6 +583,8 @@ void free_pathspec(struct pathspec *pathspec)
 {
 	int i, j;
 	for (i = 0; i < pathspec->nr; i++) {
+		if (!pathspec->items[i].attr_match_nr)
+			continue;
 		for (j = 0; j < pathspec->items[j].attr_match_nr; j++)
 			free(pathspec->items[i].attr_match[j].value);
 		free(pathspec->items[i].attr_match);
diff --git a/t/t6134-pathspec-with-labels.sh b/t/t6134-pathspec-with-labels.sh
index 35b3ab2..c0d8cda 100755
--- a/t/t6134-pathspec-with-labels.sh
+++ b/t/t6134-pathspec-with-labels.sh
@@ -74,7 +74,7 @@ test_expect_success 'check specific set attr' '
 fileSetLabel
 sub/fileSetLabel
 EOF
-	git ls-files ":(attr:+label)" >actual &&
+	git ls-files ":(attr:label)" >actual &&
 	test_cmp expect actual
 '
@@ -98,52 +98,41 @@ EOF
 	test_must_be_empty actual
 '

-test_expect_success 'check set or value attr' '
-	cat <<EOF >expect &&
-fileSetLabel
-fileValue
-sub/fileSetLabel
-sub/fileValue
-EOF
-	git ls-files ":(attr:label)" >actual &&
-	test_cmp expect actual
-'
-
 test_expect_success 'check unspecified attr' '
 	cat <<EOF >expect &&
 .gitattributes
+fileA
+fileAB
+fileAC
+fileB
+fileBC
 fileC
 fileNoLabel
 fileWrongLabel
+sub/fileA
+sub/fileAB
+sub/fileAC
+sub/fileB
+sub/fileBC
 sub/fileC
 sub/fileNoLabel
 sub/fileWrongLabel
 EOF
-	git ls-files ":(attr:~label,attr:~labelA,attr:~labelB)" >actual &&
+	git ls-files :\(attr:\!label\) >actual &&
 	test_cmp expect actual
 '

-test_expect_success 'check not unspecified attr' '
+test_expect_success 'check multiple unspecified attr' '
 	cat <<EOF >expect &&
-fileSetLabel
-fileUnsetLabel
-fileValue
-sub/fileSetLabel
-sub/fileUnsetLabel
-sub/fileValue
-EOF
-	git ls-files ":(attr:?label)" >actual &&
-	test_cmp expect actual
-'
-
-test_expect_success 'check label with 2 labels' '
-	cat <<EOF >expect &&
-fileAB
-sub/fileAB
+.gitattributes
+fileC
+fileNoLabel
+fileWrongLabel
+sub/fileC
+sub/fileNoLabel
+sub/fileWrongLabel
 EOF
-	git ls-files ":(attr:labelA labelB)" >actual &&
-	test_cmp expect actual &&
-	git ls-files ":(attr:labelA,attr:labelB)" >actual &&
+	git ls-files :\(attr:\!labelB\ \!labelA\ \!label\) >actual &&
 	test_cmp expect actual
 '

[PATCHv8 1/5] string list: improve comment

From: Stefan Beller <hidden>
Date: 2016-06-16 02:19:30

Signed-off-by: Stefan Beller <redacted>
---
 string-list.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/string-list.h b/string-list.h
index d3809a1..465a1f0 100644
--- a/string-list.h
+++ b/string-list.h
@@ -106,7 +106,7 @@ void unsorted_string_list_delete_item(struct string_list *list, int i, int free_
  * list->strdup_strings must be set, as new memory needs to be
  * allocated to hold the substrings.  If maxsplit is non-negative,
  * then split at most maxsplit times.  Return the number of substrings
- * appended to list.
+ * appended to list. The list may be non-empty already.
  *
  * Examples:
  *   string_list_split(l, "foo:bar:baz", ':', -1) -> ["foo", "bar", "baz"]
-- 
2.8.2.123.g3bde101

[PATCHv8 2/5] Documentation: fix a typo

From: Stefan Beller <hidden>
Date: 2016-06-16 02:19:30

Signed-off-by: Stefan Beller <redacted>
---
 Documentation/gitattributes.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt
index e3b1de8..af2c682 100644
--- a/Documentation/gitattributes.txt
+++ b/Documentation/gitattributes.txt
@@ -86,7 +86,7 @@ is either not set or empty, $HOME/.config/git/attributes is used instead.
 Attributes for all users on a system should be placed in the
 `$(prefix)/etc/gitattributes` file.
 
-Sometimes you would need to override an setting of an attribute
+Sometimes you would need to override a setting of an attribute
 for a path to `Unspecified` state.  This can be done by listing
 the name of the attribute prefixed with an exclamation point `!`.
 
-- 
2.8.2.123.g3bde101

[PATCHv8 3/5] pathspec: move long magic parsing out of prefix_pathspec

From: Stefan Beller <hidden>
Date: 2016-06-16 02:19:30

`prefix_pathspec` is quite a lengthy function and we plan on adding more.
Split it up for better readability. As we want to add code into the
inner loop of the long magic parsing, we also benefit from lower
indentation.

Signed-off-by: Stefan Beller <redacted>
---
 pathspec.c | 84 +++++++++++++++++++++++++++++++++++---------------------------
 1 file changed, 47 insertions(+), 37 deletions(-)
diff --git a/pathspec.c b/pathspec.c
index c9e9b6c..eba37c2 100644
--- a/pathspec.c
+++ b/pathspec.c
@@ -88,6 +88,52 @@ static void prefix_short_magic(struct strbuf *sb, int prefixlen,
 	strbuf_addf(sb, ",prefix:%d)", prefixlen);
 }
 
+static void eat_long_magic(struct pathspec_item *item, const char *elt,
+		unsigned *magic, int *pathspec_prefix,
+		const char **copyfrom_, const char **long_magic_end)
+{
+	int i;
+	const char *copyfrom = *copyfrom_;
+	/* longhand */
+	const char *nextat;
+	for (copyfrom = elt + 2;
+	     *copyfrom && *copyfrom != ')';
+	     copyfrom = nextat) {
+		size_t len = strcspn(copyfrom, ",)");
+		if (copyfrom[len] == ',')
+			nextat = copyfrom + len + 1;
+		else
+			/* handle ')' and '\0' */
+			nextat = copyfrom + len;
+		if (!len)
+			continue;
+		for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {
+			if (strlen(pathspec_magic[i].name) == len &&
+			    !strncmp(pathspec_magic[i].name, copyfrom, len)) {
+				*magic |= pathspec_magic[i].bit;
+				break;
+			}
+			if (starts_with(copyfrom, "prefix:")) {
+				char *endptr;
+				*pathspec_prefix = strtol(copyfrom + 7,
+							  &endptr, 10);
+				if (endptr - copyfrom != len)
+					die(_("invalid parameter for pathspec magic 'prefix'"));
+				/* "i" would be wrong, but it does not matter */
+				break;
+			}
+		}
+		if (ARRAY_SIZE(pathspec_magic) <= i)
+			die(_("Invalid pathspec magic '%.*s' in '%s'"),
+			    (int) len, copyfrom, elt);
+	}
+	if (*copyfrom != ')')
+		die(_("Missing ')' at the end of pathspec magic in '%s'"), elt);
+	*long_magic_end = copyfrom;
+	copyfrom++;
+	*copyfrom_ = copyfrom;
+}
+
 /*
  * Take an element of a pathspec and check for magic signatures.
  * Append the result to the prefix. Return the magic bitmap.
@@ -150,43 +196,7 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
 	    (flags & PATHSPEC_LITERAL_PATH)) {
 		; /* nothing to do */
 	} else if (elt[1] == '(') {
-		/* longhand */
-		const char *nextat;
-		for (copyfrom = elt + 2;
-		     *copyfrom && *copyfrom != ')';
-		     copyfrom = nextat) {
-			size_t len = strcspn(copyfrom, ",)");
-			if (copyfrom[len] == ',')
-				nextat = copyfrom + len + 1;
-			else
-				/* handle ')' and '\0' */
-				nextat = copyfrom + len;
-			if (!len)
-				continue;
-			for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {
-				if (strlen(pathspec_magic[i].name) == len &&
-				    !strncmp(pathspec_magic[i].name, copyfrom, len)) {
-					magic |= pathspec_magic[i].bit;
-					break;
-				}
-				if (starts_with(copyfrom, "prefix:")) {
-					char *endptr;
-					pathspec_prefix = strtol(copyfrom + 7,
-								 &endptr, 10);
-					if (endptr - copyfrom != len)
-						die(_("invalid parameter for pathspec magic 'prefix'"));
-					/* "i" would be wrong, but it does not matter */
-					break;
-				}
-			}
-			if (ARRAY_SIZE(pathspec_magic) <= i)
-				die(_("Invalid pathspec magic '%.*s' in '%s'"),
-				    (int) len, copyfrom, elt);
-		}
-		if (*copyfrom != ')')
-			die(_("Missing ')' at the end of pathspec magic in '%s'"), elt);
-		long_magic_end = copyfrom;
-		copyfrom++;
+		eat_long_magic(item, elt, &magic, &pathspec_prefix, &copyfrom, &long_magic_end);
 	} else {
 		/* shorthand */
 		for (copyfrom = elt + 1;
-- 
2.8.2.123.g3bde101

[PATCHv8 4/5] pathspec: move prefix check out of the inner loop

From: Stefan Beller <hidden>
Date: 2016-06-16 02:19:30

The prefix check is not related the check of pathspec magic; also there
is no code that is relevant after we'd break the loop on a match for
"prefix:". So move the check before the loop and shortcircuit the outer
loop.

Signed-off-by: Stefan Beller <redacted>
---
 pathspec.c | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/pathspec.c b/pathspec.c
index eba37c2..4dff252 100644
--- a/pathspec.c
+++ b/pathspec.c
@@ -107,21 +107,22 @@ static void eat_long_magic(struct pathspec_item *item, const char *elt,
 			nextat = copyfrom + len;
 		if (!len)
 			continue;
+
+		if (starts_with(copyfrom, "prefix:")) {
+			char *endptr;
+			*pathspec_prefix = strtol(copyfrom + 7,
+						  &endptr, 10);
+			if (endptr - copyfrom != len)
+				die(_("invalid parameter for pathspec magic 'prefix'"));
+			continue;
+		}
+
 		for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {
 			if (strlen(pathspec_magic[i].name) == len &&
 			    !strncmp(pathspec_magic[i].name, copyfrom, len)) {
 				*magic |= pathspec_magic[i].bit;
 				break;
 			}
-			if (starts_with(copyfrom, "prefix:")) {
-				char *endptr;
-				*pathspec_prefix = strtol(copyfrom + 7,
-							  &endptr, 10);
-				if (endptr - copyfrom != len)
-					die(_("invalid parameter for pathspec magic 'prefix'"));
-				/* "i" would be wrong, but it does not matter */
-				break;
-			}
 		}
 		if (ARRAY_SIZE(pathspec_magic) <= i)
 			die(_("Invalid pathspec magic '%.*s' in '%s'"),
-- 
2.8.2.123.g3bde101

[PATCHv8 5/5] pathspec: allow querying for attributes

From: Stefan Beller <hidden>
Date: 2016-06-16 02:19:30

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.

Signed-off-by: Stefan Beller <redacted>
---
 Documentation/glossary-content.txt |  17 ++++
 dir.c                              |  35 ++++++++
 pathspec.c                         |  97 +++++++++++++++++++++-
 pathspec.h                         |  16 ++++
 t/t6134-pathspec-with-labels.sh    | 166 +++++++++++++++++++++++++++++++++++++
 5 files changed, 327 insertions(+), 4 deletions(-)
 create mode 100755 t/t6134-pathspec-with-labels.sh
diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt
index cafc284..aa9f220 100644
--- a/Documentation/glossary-content.txt
+++ b/Documentation/glossary-content.txt
@@ -384,6 +384,23 @@ full pathname may have special meaning:
 +
 Glob magic is incompatible with literal magic.
 
+attr;;
+	Additionally to matching the pathspec, the path must have the
+	attribute as specified. The syntax for specifying the required
+	attributes is "`attr: [mode] <attribute name> [=value]`"
++
+Attributes can have 4 states (Set, Unset, Set to a value, unspecified) and
+you can query each attribute for certain states. The "`[mode]`" is a special
+character to indicate which attribute states are looked for. The following
+modes are available:
+
+ - an empty "`[mode]`" matches if the attribute is set
+ - "`-`" the attribute must be unset
+ - "`!`" the attribute must be unspecified
+ - an empty "`[mode]`" combined with "`[=value]`" matches if the attribute has
+   the given value.
++
+
 exclude;;
 	After a path matches any non-exclude pathspec, it will be run
 	through all exclude pathspec (magic signature: `!`). If it
diff --git a/dir.c b/dir.c
index 996653b..f60a503 100644
--- a/dir.c
+++ b/dir.c
@@ -9,6 +9,7 @@
  */
 #include "cache.h"
 #include "dir.h"
+#include "attr.h"
 #include "refs.h"
 #include "wildmatch.h"
 #include "pathspec.h"
@@ -215,6 +216,37 @@ int within_depth(const char *name, int namelen,
 	return 1;
 }
 
+static int match_attrs(const char *name, int namelen,
+		       const struct pathspec_item *item)
+{
+	int i;
+
+	git_check_attr_counted(name, namelen, item->attr_check);
+	for (i = 0; i < item->attr_match_nr; i++) {
+		const char *value;
+		int matched;
+		enum attr_match_mode match_mode;
+
+		value = item->attr_check->check[i].value;
+		match_mode = item->attr_match[i].match_mode;
+
+		if (ATTR_TRUE(value))
+			matched = match_mode == MATCH_SET;
+		else if (ATTR_FALSE(value))
+			matched = match_mode == MATCH_UNSET;
+		else if (ATTR_UNSET(value))
+			matched = match_mode == MATCH_UNSPECIFIED;
+		else
+			matched = (match_mode == MATCH_VALUE &&
+				   !strcmp(item->attr_match[i].value, value));
+
+		if (!matched)
+			return 0;
+	}
+
+	return 1;
+}
+
 #define DO_MATCH_EXCLUDE   1
 #define DO_MATCH_DIRECTORY 2
 
@@ -270,6 +302,9 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix,
 	    strncmp(item->match, name - prefix, item->prefix))
 		return 0;
 
+	if (item->attr_match_nr && !match_attrs(name, namelen, item))
+		return 0;
+
 	/* If the match was just the prefix, we matched */
 	if (!*match)
 		return MATCHED_RECURSIVELY;
diff --git a/pathspec.c b/pathspec.c
index 4dff252..b795a9c 100644
--- a/pathspec.c
+++ b/pathspec.c
@@ -1,6 +1,7 @@
 #include "cache.h"
 #include "dir.h"
 #include "pathspec.h"
+#include "attr.h"
 
 /*
  * Finds which of the given pathspecs match items in the index.
@@ -88,12 +89,74 @@ static void prefix_short_magic(struct strbuf *sb, int prefixlen,
 	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 (!value || !strlen(value))
+		die(_("attr spec must not be empty"));
+
+	string_list_split(&list, value, ' ', -1);
+	string_list_remove_empty_items(&list, 0);
+
+	if (!item->attr_check)
+		item->attr_check = git_attr_check_alloc();
+	else
+		die(_("Only one 'attr:' specification is allowed."));
+
+	ALLOC_GROW(item->attr_match, item->attr_match_nr + list.nr, item->attr_match_alloc);
+
+	for_each_string_list_item(si, &list) {
+		size_t attr_len;
+
+		int j = item->attr_match_nr++;
+		const char *attr = si->string;
+		struct attr_match *am = &item->attr_match[j];
+
+		if (attr[0] == '!')
+			am->match_mode = MATCH_UNSPECIFIED;
+		else if (attr[0] == '-')
+			am->match_mode = MATCH_UNSET;
+		else
+			am->match_mode = MATCH_SET;
+
+		if (am->match_mode != MATCH_SET)
+			/* skip first character */
+			attr++;
+
+		attr_len = strcspn(attr, "=");
+		if (attr[attr_len] == '=') {
+			am->match_mode = MATCH_VALUE;
+			am->value = xstrdup(&attr[attr_len + 1]);
+			if (strchr(am->value, '\\'))
+				die(_("attr spec values must not contain backslashes"));
+		} else
+			am->value = NULL;
+
+		if (!attr_name_valid(attr, attr_len)) {
+			struct strbuf sb = STRBUF_INIT;
+			am->match_mode = INVALID_ATTR;
+			invalid_attr_name_message(&sb, attr, attr_len);
+			die(_("invalid attribute in '%s': '%s'"), value, sb.buf);
+		}
+
+		am->attr = git_attr_counted(attr, attr_len);
+		git_attr_check_append(item->attr_check, am->attr);
+	}
+
+	string_list_clear(&list, 0);
+	return;
+}
+
 static void eat_long_magic(struct pathspec_item *item, const char *elt,
 		unsigned *magic, int *pathspec_prefix,
 		const char **copyfrom_, const char **long_magic_end)
 {
 	int i;
 	const char *copyfrom = *copyfrom_;
+	const char *body;
 	/* longhand */
 	const char *nextat;
 	for (copyfrom = elt + 2;
@@ -108,15 +171,21 @@ static void eat_long_magic(struct pathspec_item *item, const char *elt,
 		if (!len)
 			continue;
 
-		if (starts_with(copyfrom, "prefix:")) {
+		if (skip_prefix(copyfrom, "prefix:", &body)) {
 			char *endptr;
-			*pathspec_prefix = strtol(copyfrom + 7,
-						  &endptr, 10);
+			*pathspec_prefix = strtol(body, &endptr, 10);
 			if (endptr - copyfrom != len)
 				die(_("invalid parameter for pathspec magic 'prefix'"));
 			continue;
 		}
 
+		if (skip_prefix(copyfrom, "attr:", &body)) {
+			char *attr_body = xmemdupz(body, len - strlen("attr:"));
+			parse_pathspec_attr_match(item, attr_body);
+			free(attr_body);
+			continue;
+		}
+
 		for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {
 			if (strlen(pathspec_magic[i].name) == len &&
 			    !strncmp(pathspec_magic[i].name, copyfrom, len)) {
@@ -425,7 +494,10 @@ void parse_pathspec(struct pathspec *pathspec,
 	for (i = 0; i < n; i++) {
 		unsigned short_magic;
 		entry = argv[i];
-
+		item[i].attr_check = NULL;
+		item[i].attr_match = NULL;
+		item[i].attr_match_nr = 0;
+		item[i].attr_match_alloc = 0;
 		item[i].magic = prefix_pathspec(item + i, &short_magic,
 						argv + i, flags,
 						prefix, prefixlen, entry);
@@ -447,6 +519,13 @@ 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_match_nr) {
+			int j;
+			for (j = 0; j < item[i].attr_match_nr; j++)
+				if (item[i].attr_match[j].match_mode == INVALID_ATTR)
+					die(_("attribute spec in the wrong syntax are prohibited."));
+		}
 	}
 
 	if (nr_exclude == n)
@@ -502,6 +581,16 @@ void copy_pathspec(struct pathspec *dst, const struct pathspec *src)
 
 void free_pathspec(struct pathspec *pathspec)
 {
+	int i, j;
+	for (i = 0; i < pathspec->nr; i++) {
+		if (!pathspec->items[i].attr_match_nr)
+			continue;
+		for (j = 0; j < pathspec->items[j].attr_match_nr; j++)
+			free(pathspec->items[i].attr_match[j].value);
+		free(pathspec->items[i].attr_match);
+		git_attr_check_free(pathspec->items[i].attr_check);
+	}
+
 	free(pathspec->items);
 	pathspec->items = NULL;
 }
diff --git a/pathspec.h b/pathspec.h
index 0c11262..5308137 100644
--- a/pathspec.h
+++ b/pathspec.h
@@ -32,6 +32,22 @@ struct pathspec {
 		int len, prefix;
 		int nowildcard_len;
 		int flags;
+		int attr_match_nr;
+		int attr_match_alloc;
+		struct attr_match {
+			struct git_attr *attr;
+			char *value;
+			enum attr_match_mode {
+				MATCH_SET,
+				MATCH_UNSET,
+				MATCH_VALUE,
+				MATCH_UNSPECIFIED,
+				MATCH_NOT_UNSPECIFIED,
+				MATCH_SET_OR_VALUE,
+				INVALID_ATTR
+			} match_mode;
+		} *attr_match;
+		struct git_attr_check *attr_check;
 	} *items;
 };
 
diff --git a/t/t6134-pathspec-with-labels.sh b/t/t6134-pathspec-with-labels.sh
new file mode 100755
index 0000000..c0d8cda
--- /dev/null
+++ b/t/t6134-pathspec-with-labels.sh
@@ -0,0 +1,166 @@
+#!/bin/sh
+
+test_description='test labels in pathspecs'
+. ./test-lib.sh
+
+test_expect_success 'setup a tree' '
+	mkdir sub &&
+	for p in fileA fileB fileC fileAB fileAC fileBC fileNoLabel fileUnsetLabel fileSetLabel fileValue fileWrongLabel; do
+		: >$p &&
+		git add $p &&
+		: >sub/$p
+		git add sub/$p
+	done &&
+	git commit -m $p &&
+	git ls-files >actual &&
+	cat <<EOF >expect &&
+fileA
+fileAB
+fileAC
+fileB
+fileBC
+fileC
+fileNoLabel
+fileSetLabel
+fileUnsetLabel
+fileValue
+fileWrongLabel
+sub/fileA
+sub/fileAB
+sub/fileAC
+sub/fileB
+sub/fileBC
+sub/fileC
+sub/fileNoLabel
+sub/fileSetLabel
+sub/fileUnsetLabel
+sub/fileValue
+sub/fileWrongLabel
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'pathspec with no attr' '
+	test_must_fail git ls-files ":(attr:)" 2>actual &&
+	test_i18ngrep fatal actual
+'
+
+test_expect_success 'pathspec with labels and non existent .gitattributes' '
+	git ls-files ":(attr:label)" >actual &&
+	test_must_be_empty actual
+'
+
+test_expect_success 'setup .gitattributes' '
+	cat <<EOF >.gitattributes &&
+fileA labelA
+fileB labelB
+fileC labelC
+fileAB labelA labelB
+fileAC labelA labelC
+fileBC labelB labelC
+fileUnsetLabel -label
+fileSetLabel label
+fileValue label=foo
+fileWrongLabel label☺
+EOF
+	git add .gitattributes &&
+	git commit -m "add attributes"
+'
+
+sq="'"
+
+test_expect_success 'check specific set attr' '
+	cat <<EOF >expect &&
+fileSetLabel
+sub/fileSetLabel
+EOF
+	git ls-files ":(attr:label)" >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'check specific unset attr' '
+	cat <<EOF >expect &&
+fileUnsetLabel
+sub/fileUnsetLabel
+EOF
+	git ls-files ":(attr:-label)" >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'check specific value attr' '
+	cat <<EOF >expect &&
+fileValue
+sub/fileValue
+EOF
+	git ls-files ":(attr:label=foo)" >actual &&
+	test_cmp expect actual &&
+	git ls-files ":(attr:label=bar)" >actual &&
+	test_must_be_empty actual
+'
+
+test_expect_success 'check unspecified attr' '
+	cat <<EOF >expect &&
+.gitattributes
+fileA
+fileAB
+fileAC
+fileB
+fileBC
+fileC
+fileNoLabel
+fileWrongLabel
+sub/fileA
+sub/fileAB
+sub/fileAC
+sub/fileB
+sub/fileBC
+sub/fileC
+sub/fileNoLabel
+sub/fileWrongLabel
+EOF
+	git ls-files :\(attr:\!label\) >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'check multiple unspecified attr' '
+	cat <<EOF >expect &&
+.gitattributes
+fileC
+fileNoLabel
+fileWrongLabel
+sub/fileC
+sub/fileNoLabel
+sub/fileWrongLabel
+EOF
+	git ls-files :\(attr:\!labelB\ \!labelA\ \!label\) >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'check label with more labels but excluded path' '
+	cat <<EOF >expect &&
+fileAB
+fileB
+fileBC
+EOF
+	git ls-files ":(attr:labelB)" ":(exclude)sub/" >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'check label excluding other labels' '
+	cat <<EOF >expect &&
+fileAB
+fileB
+fileBC
+sub/fileAB
+sub/fileB
+EOF
+	git ls-files ":(attr:labelB)" ":(exclude,attr:labelC)sub/" >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'abort on giving invalid label on the command line' '
+	test_must_fail git ls-files . ":(attr:☺)" 2>actual &&
+	test_i18ngrep "fatal" actual
+'
+
+test_done
-- 
2.8.2.123.g3bde101
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help