diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt index 80120ea14f..b7c0e6577e 100644 --- a/Documentation/gitattributes.txt +++ b/Documentation/gitattributes.txt @@ -56,6 +56,7 @@ When more than one pattern matches the path, a later line overrides an earlier line. This overriding is done per attribute. The rules how the pattern matches paths are the same as in `.gitignore` files; see linkgit:gitignore[5]. +Unlike `.gitignore`, negative patterns are forbidden. When deciding what attributes are assigned to a path, git consults `$GIT_DIR/info/attributes` file (which has the highest diff --git a/attr.c b/attr.c index e7caee4ddd..2fc6353628 100644 --- a/attr.c +++ b/attr.c @@ -115,6 +115,13 @@ struct attr_state { const char *setto; }; +struct pattern { + const char *pattern; + int patternlen; + int nowildcardlen; + int flags; /* EXC_FLAG_* */ +}; + /* * One rule, as from a .gitattributes file. * @@ -131,7 +138,7 @@ struct attr_state { */ struct match_attr { union { - char *pattern; + struct pattern pat; struct git_attr *attr; } u; char is_macro; @@ -241,9 +248,16 @@ static struct match_attr *parse_attr_line(const char *line, const char *src, if (is_macro) res->u.attr = git_attr_internal(name, namelen); else { - res->u.pattern = (char *)&(res->state[num_attr]); - memcpy(res->u.pattern, name, namelen); - res->u.pattern[namelen] = 0; + char *p = (char *)&(res->state[num_attr]); + memcpy(p, name, namelen); + res->u.pat.pattern = p; + parse_exclude_pattern(&res->u.pat.pattern, + &res->u.pat.patternlen, + &res->u.pat.flags, + &res->u.pat.nowildcardlen); + if (res->u.pat.flags & EXC_FLAG_NEGATIVE) + die(_("Negative patterns are forbidden in git attributes\n" + "Use '\\!' for literal leading exclamation.")); } res->is_macro = is_macro; res->num_attr = num_attr; @@ -640,25 +654,21 @@ static void prepare_attr_stack(const char *path) static int path_matches(const char *pathname, int pathlen, const char *basename, - const char *pattern, + const struct pattern *pat, const char *base, int baselen) { - if (!strchr(pattern, '/')) { - return (fnmatch_icase(pattern, basename, 0) == 0); + const char *pattern = pat->pattern; + int prefix = pat->nowildcardlen; + + if (pat->flags & EXC_FLAG_NODIR) { + return match_basename(basename, + pathlen - (basename - pathname), + pattern, prefix, + pat->patternlen, pat->flags); } - /* - * match with FNM_PATHNAME; the pattern has base implicitly - * in front of it. - */ - if (*pattern == '/') - pattern++; - if (pathlen < baselen || - (baselen && pathname[baselen] != '/') || - strncmp(pathname, base, baselen)) - return 0; - if (baselen != 0) - baselen++; - return fnmatch_icase(pattern, pathname + baselen, FNM_PATHNAME) == 0; + return match_pathname(pathname, pathlen, + base, baselen, + pattern, prefix, pat->patternlen, pat->flags); } static int macroexpand_one(int attr_nr, int rem); @@ -696,7 +706,7 @@ static int fill(const char *path, int pathlen, const char *basename, if (a->is_macro) continue; if (path_matches(path, pathlen, basename, - a->u.pattern, base, stk->originlen)) + &a->u.pat, base, stk->originlen)) rem = fill_one("fill", a, rem); } return rem; diff --git a/dir.c b/dir.c index c4e64a5bc9..ee8e7115a8 100644 --- a/dir.c +++ b/dir.c @@ -308,10 +308,10 @@ static int no_wildcard(const char *string) return string[simple_length(string)] == '\0'; } -static void parse_exclude_pattern(const char **pattern, - int *patternlen, - int *flags, - int *nowildcardlen) +void parse_exclude_pattern(const char **pattern, + int *patternlen, + int *flags, + int *nowildcardlen) { const char *p = *pattern; size_t i, len; @@ -530,9 +530,9 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen) dir->basebuf[baselen] = '\0'; } -static int match_basename(const char *basename, int basenamelen, - const char *pattern, int prefix, int patternlen, - int flags) +int match_basename(const char *basename, int basenamelen, + const char *pattern, int prefix, int patternlen, + int flags) { if (prefix == patternlen) { if (!strcmp_icase(pattern, basename)) @@ -549,10 +549,10 @@ static int match_basename(const char *basename, int basenamelen, return 0; } -static int match_pathname(const char *pathname, int pathlen, - const char *base, int baselen, - const char *pattern, int prefix, int patternlen, - int flags) +int match_pathname(const char *pathname, int pathlen, + const char *base, int baselen, + const char *pattern, int prefix, int patternlen, + int flags) { const char *name; int namelen; diff --git a/dir.h b/dir.h index 41ea32d957..f5c89e3b80 100644 --- a/dir.h +++ b/dir.h @@ -80,6 +80,16 @@ extern int excluded_from_list(const char *pathname, int pathlen, const char *bas int *dtype, struct exclude_list *el); struct dir_entry *dir_add_ignored(struct dir_struct *dir, const char *pathname, int len); +/* + * these implement the matching logic for dir.c:excluded_from_list and + * attr.c:path_matches() + */ +extern int match_basename(const char *, int, + const char *, int, int, int); +extern int match_pathname(const char *, int, + const char *, int, + const char *, int, int, int); + /* * The excluded() API is meant for callers that check each level of leading * directory hierarchies with excluded() to avoid recursing into excluded @@ -97,6 +107,7 @@ extern int path_excluded(struct path_exclude_check *, const char *, int namelen, extern int add_excludes_from_file_to_list(const char *fname, const char *base, int baselen, char **buf_p, struct exclude_list *which, int check_index); extern void add_excludes_from_file(struct dir_struct *, const char *fname); +extern void parse_exclude_pattern(const char **string, int *patternlen, int *flags, int *nowildcardlen); extern void add_exclude(const char *string, const char *base, int baselen, struct exclude_list *which); extern void free_excludes(struct exclude_list *el); diff --git a/t/t0003-attributes.sh b/t/t0003-attributes.sh index 51f3045ba4..f6c21ea4ea 100755 --- a/t/t0003-attributes.sh +++ b/t/t0003-attributes.sh @@ -206,6 +206,16 @@ test_expect_success 'root subdir attribute test' ' attr_check subdir/a/i unspecified ' +test_expect_success 'negative patterns' ' + echo "!f test=bar" >.gitattributes && + test_must_fail git check-attr test -- f +' + +test_expect_success 'patterns starting with exclamation' ' + echo "\!f test=foo" >.gitattributes && + attr_check "!f" foo +' + test_expect_success 'setup bare' ' git clone --bare . bare.git && cd bare.git