2 #include "repository.h"
9 static int sort_by_hash_and_name(const void *a_raw
, const void *b_raw
);
10 static void git_attr_rule__clear(git_attr_rule
*rule
);
11 static bool parse_optimized_patterns(
12 git_attr_fnmatch
*spec
,
16 int git_attr_file__new(
17 git_attr_file
**attrs_ptr
,
18 git_attr_file_source from
,
22 git_attr_file
*attrs
= NULL
;
24 attrs
= git__calloc(1, sizeof(git_attr_file
));
25 GITERR_CHECK_ALLOC(attrs
);
30 attrs
->pool
= git__calloc(1, sizeof(git_pool
));
31 if (!attrs
->pool
|| git_pool_init(attrs
->pool
, 1, 0) < 0)
33 attrs
->pool_is_allocated
= true;
37 size_t len
= strlen(path
);
39 attrs
->key
= git_pool_malloc(attrs
->pool
, (uint32_t)len
+ 3);
40 GITERR_CHECK_ALLOC(attrs
->key
);
42 attrs
->key
[0] = '0' + from
;
44 memcpy(&attrs
->key
[2], path
, len
);
45 attrs
->key
[len
+ 2] = '\0';
48 if (git_vector_init(&attrs
->rules
, 4, NULL
) < 0)
55 git_attr_file__free(attrs
);
60 int git_attr_file__parse_buffer(
61 git_repository
*repo
, void *parsedata
, const char *buffer
, git_attr_file
*attrs
)
64 const char *scan
= NULL
;
66 git_attr_rule
*rule
= NULL
;
68 GIT_UNUSED(parsedata
);
70 assert(buffer
&& attrs
);
74 /* if subdir file path, convert context for file paths */
75 if (attrs
->key
&& git__suffixcmp(attrs
->key
, "/" GIT_ATTR_FILE
) == 0) {
76 context
= attrs
->key
+ 2;
77 context
[strlen(context
) - strlen(GIT_ATTR_FILE
)] = '\0';
80 while (!error
&& *scan
) {
81 /* allocate rule if needed */
82 if (!rule
&& !(rule
= git__calloc(1, sizeof(git_attr_rule
)))) {
87 /* parse the next "pattern attr attr attr" line */
88 if (!(error
= git_attr_fnmatch__parse_gitattr_format(
89 &rule
->match
, attrs
->pool
, context
, &scan
)) &&
90 !(error
= git_attr_assignment__parse(
91 repo
, attrs
->pool
, &rule
->assigns
, &scan
)))
93 if (rule
->match
.flags
& GIT_ATTR_FNMATCH_MACRO
)
94 /* should generate error/warning if this is coming from any
95 * file other than .gitattributes at repo root.
97 error
= git_attr_cache__insert_macro(repo
, rule
);
99 error
= git_vector_insert(&attrs
->rules
, rule
);
102 /* if the rule wasn't a pattern, on to the next */
104 git_attr_rule__clear(rule
); /* reset rule contents */
105 if (error
== GIT_ENOTFOUND
)
108 rule
= NULL
; /* vector now "owns" the rule */
112 git_attr_rule__free(rule
);
114 /* restore file path used for context */
116 context
[strlen(context
)] = '.'; /* first char of GIT_ATTR_FILE */
121 int git_attr_file__new_and_load(
122 git_attr_file
**attrs_ptr
,
126 git_buf content
= GIT_BUF_INIT
;
128 if ((error
= git_attr_file__new(attrs_ptr
, 0, path
, NULL
)) < 0)
131 if (!(error
= git_futils_readbuffer(&content
, path
)))
132 error
= git_attr_file__parse_buffer(
133 NULL
, NULL
, git_buf_cstr(&content
), *attrs_ptr
);
135 git_buf_free(&content
);
138 git_attr_file__free(*attrs_ptr
);
145 void git_attr_file__clear_rules(git_attr_file
*file
)
150 git_vector_foreach(&file
->rules
, i
, rule
)
151 git_attr_rule__free(rule
);
153 git_vector_free(&file
->rules
);
156 void git_attr_file__free(git_attr_file
*file
)
161 git_attr_file__clear_rules(file
);
163 if (file
->pool_is_allocated
) {
164 git_pool_clear(file
->pool
);
165 git__free(file
->pool
);
172 uint32_t git_attr_file__name_hash(const char *name
)
177 while ((c
= (int)*name
++) != 0)
178 h
= ((h
<< 5) + h
) + c
;
183 int git_attr_file__lookup_one(
185 const git_attr_path
*path
,
196 name
.name_hash
= git_attr_file__name_hash(attr
);
198 git_attr_file__foreach_matching_rule(file
, path
, i
, rule
) {
201 if (!git_vector_bsearch(&pos
, &rule
->assigns
, &name
)) {
202 *value
= ((git_attr_assignment
*)
203 git_vector_get(&rule
->assigns
, pos
))->value
;
212 bool git_attr_fnmatch__match(
213 git_attr_fnmatch
*match
,
214 const git_attr_path
*path
)
217 int icase_flags
= (match
->flags
& GIT_ATTR_FNMATCH_ICASE
) ? FNM_CASEFOLD
: 0;
219 if (match
->flags
& GIT_ATTR_FNMATCH_DIRECTORY
&& !path
->is_dir
)
222 if (match
->flags
& GIT_ATTR_FNMATCH_FULLPATH
)
223 fnm
= p_fnmatch(match
->pattern
, path
->path
, FNM_PATHNAME
| icase_flags
);
224 else if (path
->is_dir
)
225 fnm
= p_fnmatch(match
->pattern
, path
->basename
, FNM_LEADING_DIR
| icase_flags
);
227 fnm
= p_fnmatch(match
->pattern
, path
->basename
, icase_flags
);
229 return (fnm
== FNM_NOMATCH
) ? false : true;
232 bool git_attr_rule__match(
234 const git_attr_path
*path
)
236 bool matched
= git_attr_fnmatch__match(&rule
->match
, path
);
238 if (rule
->match
.flags
& GIT_ATTR_FNMATCH_NEGATIVE
)
245 git_attr_assignment
*git_attr_rule__lookup_assignment(
246 git_attr_rule
*rule
, const char *name
)
251 key
.name_hash
= git_attr_file__name_hash(name
);
253 if (git_vector_bsearch(&pos
, &rule
->assigns
, &key
))
256 return git_vector_get(&rule
->assigns
, pos
);
259 int git_attr_path__init(
260 git_attr_path
*info
, const char *path
, const char *base
)
264 /* build full path as best we can */
265 git_buf_init(&info
->full
, 0);
267 if (git_path_join_unrooted(&info
->full
, path
, base
, &root
) < 0)
270 info
->path
= info
->full
.ptr
+ root
;
272 /* remove trailing slashes */
273 while (info
->full
.size
> 0) {
274 if (info
->full
.ptr
[info
->full
.size
- 1] != '/')
278 info
->full
.ptr
[info
->full
.size
] = '\0';
280 /* skip leading slashes in path */
281 while (*info
->path
== '/')
284 /* find trailing basename component */
285 info
->basename
= strrchr(info
->path
, '/');
288 if (!info
->basename
|| !*info
->basename
)
289 info
->basename
= info
->path
;
291 info
->is_dir
= (int)git_path_isdir(info
->full
.ptr
);
296 void git_attr_path__free(git_attr_path
*info
)
298 git_buf_free(&info
->full
);
300 info
->basename
= NULL
;
304 * From gitattributes(5):
306 * Patterns have the following format:
308 * - A blank line matches no files, so it can serve as a separator for
311 * - A line starting with # serves as a comment.
313 * - An optional prefix ! which negates the pattern; any matching file
314 * excluded by a previous pattern will become included again. If a negated
315 * pattern matches, this will override lower precedence patterns sources.
317 * - If the pattern ends with a slash, it is removed for the purpose of the
318 * following description, but it would only find a match with a directory. In
319 * other words, foo/ will match a directory foo and paths underneath it, but
320 * will not match a regular file or a symbolic link foo (this is consistent
321 * with the way how pathspec works in general in git).
323 * - If the pattern does not contain a slash /, git treats it as a shell glob
324 * pattern and checks for a match against the pathname without leading
327 * - Otherwise, git treats the pattern as a shell glob suitable for consumption
328 * by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will
329 * not match a / in the pathname. For example, "Documentation/\*.html" matches
330 * "Documentation/git.html" but not "Documentation/ppc/ppc.html". A leading
331 * slash matches the beginning of the pathname; for example, "/\*.c" matches
332 * "cat-file.c" but not "mozilla-sha1/sha1.c".
336 * This will return 0 if the spec was filled out,
337 * GIT_ENOTFOUND if the fnmatch does not require matching, or
338 * another error code there was an actual problem.
340 int git_attr_fnmatch__parse_gitattr_format(
341 git_attr_fnmatch
*spec
,
348 assert(spec
&& base
&& *base
);
352 while (git__isspace(*pattern
)) pattern
++;
353 if (!*pattern
|| *pattern
== '#') {
354 *base
= git__next_line(pattern
);
355 return GIT_ENOTFOUND
;
358 if (*pattern
== '[') {
359 if (strncmp(pattern
, "[attr]", 6) == 0) {
360 spec
->flags
= spec
->flags
| GIT_ATTR_FNMATCH_MACRO
;
363 /* else a character range like [a-e]* which is accepted */
366 if (*pattern
== '!') {
367 spec
->flags
= spec
->flags
| GIT_ATTR_FNMATCH_NEGATIVE
;
371 if (git_attr_fnmatch__parse_shellglob_format(spec
, pool
,
372 source
, &pattern
) < 0)
381 * Fills a spec for the purpose of pure pathspec matching, not
382 * related to a gitattribute file parsing.
384 * This will return 0 if the spec was filled out, or
385 * another error code there was an actual problem.
387 int git_attr_fnmatch__parse_shellglob_format(
388 git_attr_fnmatch
*spec
,
393 const char *pattern
, *scan
;
394 int slash_count
, allow_space
;
396 assert(spec
&& base
&& *base
);
398 if (parse_optimized_patterns(spec
, pool
, *base
))
401 allow_space
= (spec
->flags
& GIT_ATTR_FNMATCH_ALLOWSPACE
) != 0;
405 for (scan
= pattern
; *scan
!= '\0'; ++scan
) {
406 /* scan until (non-escaped) white space */
407 if (git__isspace(*scan
) && *(scan
- 1) != '\\') {
408 if (!allow_space
|| (*scan
!= ' ' && *scan
!= '\t'))
413 spec
->flags
= spec
->flags
| GIT_ATTR_FNMATCH_FULLPATH
;
418 /* remember if we see an unescaped wildcard in pattern */
419 else if (git__iswildcard(*scan
) &&
420 (scan
== pattern
|| (*(scan
- 1) != '\\')))
421 spec
->flags
= spec
->flags
| GIT_ATTR_FNMATCH_HASWILD
;
426 spec
->length
= scan
- pattern
;
428 if (pattern
[spec
->length
- 1] == '/') {
430 spec
->flags
= spec
->flags
| GIT_ATTR_FNMATCH_DIRECTORY
;
431 if (--slash_count
<= 0)
432 spec
->flags
= spec
->flags
& ~GIT_ATTR_FNMATCH_FULLPATH
;
435 if ((spec
->flags
& GIT_ATTR_FNMATCH_FULLPATH
) != 0 &&
436 source
!= NULL
&& git_path_root(pattern
) < 0)
438 size_t sourcelen
= strlen(source
);
439 /* given an unrooted fullpath match from a file inside a repo,
440 * prefix the pattern with the relative directory of the source file
442 spec
->pattern
= git_pool_malloc(
443 pool
, (uint32_t)(sourcelen
+ spec
->length
+ 1));
445 memcpy(spec
->pattern
, source
, sourcelen
);
446 memcpy(spec
->pattern
+ sourcelen
, pattern
, spec
->length
);
447 spec
->length
+= sourcelen
;
448 spec
->pattern
[spec
->length
] = '\0';
451 spec
->pattern
= git_pool_strndup(pool
, pattern
, spec
->length
);
454 if (!spec
->pattern
) {
455 *base
= git__next_line(pattern
);
458 /* strip '\' that might have be used for internal whitespace */
459 spec
->length
= git__unescape(spec
->pattern
);
465 static bool parse_optimized_patterns(
466 git_attr_fnmatch
*spec
,
470 if (!pattern
[1] && (pattern
[0] == '*' || pattern
[0] == '.')) {
471 spec
->flags
= GIT_ATTR_FNMATCH_MATCH_ALL
;
472 spec
->pattern
= git_pool_strndup(pool
, pattern
, 1);
481 static int sort_by_hash_and_name(const void *a_raw
, const void *b_raw
)
483 const git_attr_name
*a
= a_raw
;
484 const git_attr_name
*b
= b_raw
;
486 if (b
->name_hash
< a
->name_hash
)
488 else if (b
->name_hash
> a
->name_hash
)
491 return strcmp(b
->name
, a
->name
);
494 static void git_attr_assignment__free(git_attr_assignment
*assign
)
496 /* name and value are stored in a git_pool associated with the
497 * git_attr_file, so they do not need to be freed here
500 assign
->value
= NULL
;
504 static int merge_assignments(void **old_raw
, void *new_raw
)
506 git_attr_assignment
**old
= (git_attr_assignment
**)old_raw
;
507 git_attr_assignment
*new = (git_attr_assignment
*)new_raw
;
509 GIT_REFCOUNT_DEC(*old
, git_attr_assignment__free
);
514 int git_attr_assignment__parse(
515 git_repository
*repo
,
521 const char *scan
= *base
;
522 git_attr_assignment
*assign
= NULL
;
524 assert(assigns
&& !assigns
->length
);
526 assigns
->_cmp
= sort_by_hash_and_name
;
528 while (*scan
&& *scan
!= '\n') {
529 const char *name_start
, *value_start
;
531 /* skip leading blanks */
532 while (git__isspace(*scan
) && *scan
!= '\n') scan
++;
534 /* allocate assign if needed */
536 assign
= git__calloc(1, sizeof(git_attr_assignment
));
537 GITERR_CHECK_ALLOC(assign
);
538 GIT_REFCOUNT_INC(assign
);
541 assign
->name_hash
= 5381;
542 assign
->value
= git_attr__true
;
544 /* look for magic name prefixes */
546 assign
->value
= git_attr__false
;
548 } else if (*scan
== '!') {
549 assign
->value
= git_attr__unset
; /* explicit unspecified state */
551 } else if (*scan
== '#') /* comment rest of line */
556 while (*scan
&& !git__isspace(*scan
) && *scan
!= '=') {
558 ((assign
->name_hash
<< 5) + assign
->name_hash
) + *scan
;
561 if (scan
== name_start
) {
562 /* must have found lone prefix (" - ") or leading = ("=foo")
563 * or end of buffer -- advance until whitespace and continue
565 while (*scan
&& !git__isspace(*scan
)) scan
++;
569 /* allocate permanent storage for name */
570 assign
->name
= git_pool_strndup(pool
, name_start
, scan
- name_start
);
571 GITERR_CHECK_ALLOC(assign
->name
);
573 /* if there is an equals sign, find the value */
575 for (value_start
= ++scan
; *scan
&& !git__isspace(*scan
); ++scan
);
577 /* if we found a value, allocate permanent storage for it */
578 if (scan
> value_start
) {
579 assign
->value
= git_pool_strndup(pool
, value_start
, scan
- value_start
);
580 GITERR_CHECK_ALLOC(assign
->value
);
584 /* expand macros (if given a repo with a macro cache) */
585 if (repo
!= NULL
&& assign
->value
== git_attr__true
) {
586 git_attr_rule
*macro
=
587 git_attr_cache__lookup_macro(repo
, assign
->name
);
591 git_attr_assignment
*massign
;
593 git_vector_foreach(¯o
->assigns
, i
, massign
) {
594 GIT_REFCOUNT_INC(massign
);
596 error
= git_vector_insert_sorted(
597 assigns
, massign
, &merge_assignments
);
598 if (error
< 0 && error
!= GIT_EEXISTS
)
604 /* insert allocated assign into vector */
605 error
= git_vector_insert_sorted(assigns
, assign
, &merge_assignments
);
606 if (error
< 0 && error
!= GIT_EEXISTS
)
609 /* clear assign since it is now "owned" by the vector */
614 git_attr_assignment__free(assign
);
616 *base
= git__next_line(scan
);
618 return (assigns
->length
== 0) ? GIT_ENOTFOUND
: 0;
621 static void git_attr_rule__clear(git_attr_rule
*rule
)
624 git_attr_assignment
*assign
;
629 if (!(rule
->match
.flags
& GIT_ATTR_FNMATCH_IGNORE
)) {
630 git_vector_foreach(&rule
->assigns
, i
, assign
)
631 GIT_REFCOUNT_DEC(assign
, git_attr_assignment__free
);
632 git_vector_free(&rule
->assigns
);
635 /* match.pattern is stored in a git_pool, so no need to free */
636 rule
->match
.pattern
= NULL
;
637 rule
->match
.length
= 0;
638 rule
->match
.flags
= 0;
641 void git_attr_rule__free(git_attr_rule
*rule
)
643 git_attr_rule__clear(rule
);