Labels were originally designed to manage large amount of submodules, the discussion steered this in a more general direction, such that other files can be labeled as well. Labels are meant to describe arbitrary set of files, which is not described via the tree layout. Signed-off-by: Stefan Beller <sbeller@xxxxxxxxxx> --- Documentation/gitattributes.txt | 12 +++ Documentation/glossary-content.txt | 4 + dir.c | 50 +++++++++++ pathspec.c | 64 ++++++++++++++- pathspec.h | 1 + t/t6134-pathspec-with-labels.sh | 164 +++++++++++++++++++++++++++++++++++++ 6 files changed, 291 insertions(+), 4 deletions(-) create mode 100755 t/t6134-pathspec-with-labels.sh diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt index af2c682..7fd764f 100644 --- a/Documentation/gitattributes.txt +++ b/Documentation/gitattributes.txt @@ -906,6 +906,18 @@ If this attribute is not set or has an invalid value, the value of the (See linkgit:git-config[1]). + +Attaching labels to path +~~~~~~~~~~~~~~~~~~~~~~~~ + +`label` +^^^^^^^^^^ +By the value of this attribute you can specify a list of arbitrary strings +to be attached to a path as labels. These labels can be used for +easier paths matching using pathspecs (linkgit:gitglossary[1]). +It is recommended to use only alphanumeric characters, dashes and +underscores for the labels. + USING MACRO ATTRIBUTES ---------------------- diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt index 8ad29e6..f990017 100644 --- a/Documentation/glossary-content.txt +++ b/Documentation/glossary-content.txt @@ -384,6 +384,10 @@ full pathname may have special meaning: + Glob magic is incompatible with literal magic. +label:<white space separated list>;; + Additionally to matching the pathspec, the path must habe a 'label' + attribute having set all labels listed here. + 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 656f272..a86e3ff 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" @@ -208,6 +209,52 @@ int within_depth(const char *name, int namelen, return 1; } +static int has_all_labels(const char *name, int namelen, + const struct string_list *required_labels) +{ + static struct git_attr *attr; + + struct git_attr_check check; + char *path; + int ret; + + if (!attr) + attr = git_attr("label"); + check.attr = attr; + + path = xmemdupz(name, namelen); + if (git_check_attr(path, 1, &check)) + die("internal bug: git_check_attr died."); + free(path); + + if (ATTR_TRUE(check.value)) + ret = 1; /* has all the labels */ + else if (ATTR_FALSE(check.value)) + ret = 0; /* has no labels */ + else if (ATTR_UNSET(check.value)) + /* + * If no label was specified this matches, otherwise + * there is a missing label. + */ + ret = (required_labels->nr > 0) ? 0 : 1; + else { + struct string_list_item *si; + struct string_list attr_labels = STRING_LIST_INIT_DUP; + string_list_split(&attr_labels, check.value, ',', -1); + string_list_sort(&attr_labels); + ret = 1; + for_each_string_list_item(si, required_labels) { + if (!string_list_has_string(&attr_labels, si->string)) { + ret = 0; + break; + } + } + string_list_clear(&attr_labels, 0); + } + + return ret; +} + #define DO_MATCH_EXCLUDE 1 #define DO_MATCH_DIRECTORY 2 @@ -263,6 +310,9 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix, strncmp(item->match, name - prefix, item->prefix)) return 0; + if (item->labels && !has_all_labels(name, namelen, item->labels)) + 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..c65428a 100644 --- a/pathspec.c +++ b/pathspec.c @@ -88,12 +88,40 @@ static void prefix_short_magic(struct strbuf *sb, int prefixlen, strbuf_addf(sb, ",prefix:%d)", prefixlen); } +static int check_valid_label_name(const char *label) +{ + if (!label || !strlen(label)) + return -1; + + if (!isalpha(*label)) + return -1; + + while (*label) { + if (!(isalnum(*label) || + *label == '-' || + *label == '_')) + return -1; + label++; + } + + return 0; +} + +static void validate_label_name(const char *label) +{ + if (check_valid_label_name(label)) + warning(_("Label '%s' discouraged. Recommended label names start " + "with an alphabetic character and use only alphanumeric " + "characters, dashes and underscores."), label); +} + 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 +136,30 @@ 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; } + /* The label token may have no argument, so no trailing ':' */ + if (skip_prefix(copyfrom, "label", &body)) { + struct strbuf sb = STRBUF_INIT; + skip_prefix(body, ":", &body); + strbuf_add(&sb, body, nextat - body); + if (!item->labels) { + item->labels = xmalloc(sizeof(*item->labels)); + string_list_init(item->labels, 1); + } else + die(_("multiple labels not supported in pathspec magic")); + string_list_split(item->labels, sb.buf, ' ', -1); + string_list_remove_empty_items(item->labels, 0); + strbuf_release(&sb); + 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 +468,7 @@ void parse_pathspec(struct pathspec *pathspec, for (i = 0; i < n; i++) { unsigned short_magic; entry = argv[i]; - + item[i].labels = NULL; item[i].magic = prefix_pathspec(item + i, &short_magic, argv + i, flags, prefix, prefixlen, entry); @@ -447,6 +490,12 @@ 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].labels) { + struct string_list_item *si; + for_each_string_list_item(si, item[i].labels) + validate_label_name(si->string); + } } if (nr_exclude == n) @@ -502,6 +551,13 @@ void copy_pathspec(struct pathspec *dst, const struct pathspec *src) void free_pathspec(struct pathspec *pathspec) { + int i; + for (i = 0; i < pathspec->nr; i++) { + if (pathspec->items[i].labels) + string_list_clear(pathspec->items[i].labels, 1); + free(pathspec->items[i].labels); + } + free(pathspec->items); pathspec->items = NULL; } diff --git a/pathspec.h b/pathspec.h index 0c11262..12ed2b8 100644 --- a/pathspec.h +++ b/pathspec.h @@ -32,6 +32,7 @@ struct pathspec { int len, prefix; int nowildcard_len; int flags; + struct string_list *labels; } *items; }; diff --git a/t/t6134-pathspec-with-labels.sh b/t/t6134-pathspec-with-labels.sh new file mode 100755 index 0000000..1fe5ed7 --- /dev/null +++ b/t/t6134-pathspec-with-labels.sh @@ -0,0 +1,164 @@ +#!/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; 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 +sub/fileA +sub/fileAB +sub/fileAC +sub/fileB +sub/fileBC +sub/fileC +sub/fileNoLabel +sub/fileSetLabel +sub/fileUnsetLabel +EOF + test_cmp expect actual +' + +test_expect_success 'pathspec with labels and non existent .gitattributes' ' + git ls-files ":(label)" >actual && + git ls-files >expect && + test_cmp expect actual +' + +test_expect_success 'pathspec with labels and non existent .gitattributes' ' + git ls-files ":(label:a)" >actual && + test_must_be_empty actual +' + +test_expect_success 'setup .gitattributes' ' + cat <<EOF >.gitattributes && +fileA label=labelA +fileB label=labelB +fileC label=labelC +fileAB label=labelA,labelB +fileAC label=labelA,labelC +fileBC label=labelB,labelC +fileUnsetLabel -label +fileSetLabel label +EOF + git add .gitattributes && + git commit -m "add attributes" +' + +test_expect_success 'check for any label' ' + cat <<EOF >expect && +.gitattributes +fileA +fileAB +fileAC +fileB +fileBC +fileC +fileNoLabel +fileSetLabel +sub/fileA +sub/fileAB +sub/fileAC +sub/fileB +sub/fileBC +sub/fileC +sub/fileNoLabel +sub/fileSetLabel +EOF + git ls-files ":(label)" >actual && + test_cmp expect actual +' + +test_expect_success 'check specific label' ' + cat <<EOF >expect && +fileA +fileAB +fileAC +fileSetLabel +sub/fileA +sub/fileAB +sub/fileAC +sub/fileSetLabel +EOF + git ls-files ":(label:labelA)" >actual && + test_cmp expect actual +' + +test_expect_success 'check label with 2 labels' ' + cat <<EOF >expect && +fileAB +fileSetLabel +sub/fileAB +sub/fileSetLabel +EOF + git ls-files ":(label:labelA labelB)" >actual && + test_cmp expect actual +' + +test_expect_success 'check label with more labels' ' + test_must_fail git ls-files ":(label:labelA,label:labelB)" 2>actual && + test_i18ngrep "not supported" actual +' + +test_expect_success 'check label with more labels but excluded path' ' + cat <<EOF >expect && +fileAB +fileB +fileBC +fileSetLabel +EOF + git ls-files ":(label:labelB)" ":(exclude)sub/" >actual && + test_cmp expect actual +' + +test_expect_success 'check label excluding other labels' ' + cat <<EOF >expect && +fileAB +fileB +fileBC +fileSetLabel +sub/fileAB +sub/fileB +EOF + git ls-files ":(label:labelB)" ":(exclude,label:labelC)sub/" >actual && + test_cmp expect actual +' + + +test_expect_success 'check for explicit unlabeled paths' ' + cat <<EOF >expect && +fileUnsetLabel +sub/fileUnsetLabel +EOF + git ls-files . ":(exclude,label)" >actual && + test_cmp expect actual +' + +test_expect_success 'check for paths that have no label' ' + cat <<EOF >expect && +.gitattributes +fileNoLabel +sub/fileNoLabel +EOF + git ls-files ":(label)" ":(exclude,label:labelA)" ":(exclude,label:labelB)" ":(exclude,label:labelC)" >actual && + test_cmp expect actual +' + +test_done -- 2.8.2.401.g6647087.dirty -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html