attr: more matching optimizations from .gitignore

.gitattributes and .gitignore share the same pattern syntax but has separate matching implementation. Over the years, ignore's implementation accumulates more optimizations while attr's stays the same. This patch reuses the core matching functions that are also used by excluded_from_list. excluded_from_list and path_matches can't be merged due to differences in exclude and attr, for example: * "!pattern" syntax is forbidden in .gitattributes. As an attribute can be unset (i.e. set to a special value "false") or made back to unspecified (i.e. not even set to "false"), "!pattern attr" is unclear which one it means. * we support attaching attributes to directories, but git-core internally does not currently make use of attributes on directories. Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Nguyễn Thái Ngọc Duy committed Oct 15, 2012 at 13:24 UTC 82dce998c2028b6ee96691921b7037a6e182ec89
5 files changed +64 -32
Documentation/gitattributes.txt
+1
@@ -56,6 +56,7 @@ When more than one pattern matches the path, a later line
56 overrides an earlier line. This overriding is done per
57 attribute. The rules how the pattern matches paths are the
58 same as in `.gitignore` files; see linkgit:gitignore[5].
59 +Unlike `.gitignore`, negative patterns are forbidden.
60
61 When deciding what attributes are assigned to a path, git
62 consults `$GIT_DIR/info/attributes` file (which has the highest
attr.c
+31 -21
@@ -115,6 +115,13 @@ struct attr_state {
115 const char *setto;
116 };
117
118 +struct pattern {
119 + const char *pattern;
120 + int patternlen;
121 + int nowildcardlen;
122 + int flags; /* EXC_FLAG_* */
123 +};
124 +
125 /*
126 * One rule, as from a .gitattributes file.
127 *
@@ -131,7 +138,7 @@ struct attr_state {
138 */
139 struct match_attr {
140 union {
134 - char *pattern;
141 + struct pattern pat;
142 struct git_attr *attr;
143 } u;
144 char is_macro;
@@ -241,9 +248,16 @@ static struct match_attr *parse_attr_line(const char *line, const char *src,
248 if (is_macro)
249 res->u.attr = git_attr_internal(name, namelen);
250 else {
244 - res->u.pattern = (char *)&(res->state[num_attr]);
245 - memcpy(res->u.pattern, name, namelen);
246 - res->u.pattern[namelen] = 0;
251 + char *p = (char *)&(res->state[num_attr]);
252 + memcpy(p, name, namelen);
253 + res->u.pat.pattern = p;
254 + parse_exclude_pattern(&res->u.pat.pattern,
255 + &res->u.pat.patternlen,
256 + &res->u.pat.flags,
257 + &res->u.pat.nowildcardlen);
258 + if (res->u.pat.flags & EXC_FLAG_NEGATIVE)
259 + die(_("Negative patterns are forbidden in git attributes\n"
260 + "Use '\\!' for literal leading exclamation."));
261 }
262 res->is_macro = is_macro;
263 res->num_attr = num_attr;
@@ -640,25 +654,21 @@ static void prepare_attr_stack(const char *path)
654
655 static int path_matches(const char *pathname, int pathlen,
656 const char *basename,
643 - const char *pattern,
657 + const struct pattern *pat,
658 const char *base, int baselen)
659 {
646 - if (!strchr(pattern, '/')) {
647 - return (fnmatch_icase(pattern, basename, 0) == 0);
660 + const char *pattern = pat->pattern;
661 + int prefix = pat->nowildcardlen;
662 +
663 + if (pat->flags & EXC_FLAG_NODIR) {
664 + return match_basename(basename,
665 + pathlen - (basename - pathname),
666 + pattern, prefix,
667 + pat->patternlen, pat->flags);
668 }
649 - /*
650 - * match with FNM_PATHNAME; the pattern has base implicitly
651 - * in front of it.
652 - */
653 - if (*pattern == '/')
654 - pattern++;
655 - if (pathlen < baselen ||
656 - (baselen && pathname[baselen] != '/') ||
657 - strncmp(pathname, base, baselen))
658 - return 0;
659 - if (baselen != 0)
660 - baselen++;
661 - return fnmatch_icase(pattern, pathname + baselen, FNM_PATHNAME) == 0;
669 + return match_pathname(pathname, pathlen,
670 + base, baselen,
671 + pattern, prefix, pat->patternlen, pat->flags);
672 }
673
674 static int macroexpand_one(int attr_nr, int rem);
@@ -696,7 +706,7 @@ static int fill(const char *path, int pathlen, const char *basename,
706 if (a->is_macro)
707 continue;
708 if (path_matches(path, pathlen, basename,
699 - a->u.pattern, base, stk->originlen))
709 + &a->u.pat, base, stk->originlen))
710 rem = fill_one("fill", a, rem);
711 }
712 return rem;
dir.c
+11 -11
@@ -308,10 +308,10 @@ static int no_wildcard(const char *string)
308 return string[simple_length(string)] == '\0';
309 }
310
311 -static void parse_exclude_pattern(const char **pattern,
312 - int *patternlen,
313 - int *flags,
314 - int *nowildcardlen)
311 +void parse_exclude_pattern(const char **pattern,
312 + int *patternlen,
313 + int *flags,
314 + int *nowildcardlen)
315 {
316 const char *p = *pattern;
317 size_t i, len;
@@ -530,9 +530,9 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
530 dir->basebuf[baselen] = '\0';
531 }
532
533 -static int match_basename(const char *basename, int basenamelen,
534 - const char *pattern, int prefix, int patternlen,
535 - int flags)
533 +int match_basename(const char *basename, int basenamelen,
534 + const char *pattern, int prefix, int patternlen,
535 + int flags)
536 {
537 if (prefix == patternlen) {
538 if (!strcmp_icase(pattern, basename))
@@ -549,10 +549,10 @@ static int match_basename(const char *basename, int basenamelen,
549 return 0;
550 }
551
552 -static int match_pathname(const char *pathname, int pathlen,
553 - const char *base, int baselen,
554 - const char *pattern, int prefix, int patternlen,
555 - int flags)
552 +int match_pathname(const char *pathname, int pathlen,
553 + const char *base, int baselen,
554 + const char *pattern, int prefix, int patternlen,
555 + int flags)
556 {
557 const char *name;
558 int namelen;
dir.h
+11
@@ -80,6 +80,16 @@ extern int excluded_from_list(const char *pathname, int pathlen, const char *bas
80 int *dtype, struct exclude_list *el);
81 struct dir_entry *dir_add_ignored(struct dir_struct *dir, const char *pathname, int len);
82
83 +/*
84 + * these implement the matching logic for dir.c:excluded_from_list and
85 + * attr.c:path_matches()
86 + */
87 +extern int match_basename(const char *, int,
88 + const char *, int, int, int);
89 +extern int match_pathname(const char *, int,
90 + const char *, int,
91 + const char *, int, int, int);
92 +
93 /*
94 * The excluded() API is meant for callers that check each level of leading
95 * 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,
107 extern int add_excludes_from_file_to_list(const char *fname, const char *base, int baselen,
108 char **buf_p, struct exclude_list *which, int check_index);
109 extern void add_excludes_from_file(struct dir_struct *, const char *fname);
110 +extern void parse_exclude_pattern(const char **string, int *patternlen, int *flags, int *nowildcardlen);
111 extern void add_exclude(const char *string, const char *base,
112 int baselen, struct exclude_list *which);
113 extern void free_excludes(struct exclude_list *el);
t/t0003-attributes.sh
+10
@@ -206,6 +206,16 @@ test_expect_success 'root subdir attribute test' '
206 attr_check subdir/a/i unspecified
207 '
208
209 +test_expect_success 'negative patterns' '
210 + echo "!f test=bar" >.gitattributes &&
211 + test_must_fail git check-attr test -- f
212 +'
213 +
214 +test_expect_success 'patterns starting with exclamation' '
215 + echo "\!f test=foo" >.gitattributes &&
216 + attr_check "!f" foo
217 +'
218 +
219 test_expect_success 'setup bare' '
220 git clone --bare . bare.git &&
221 cd bare.git