2 #include "repository.h"
11 static void attr_file_free(git_attr_file
*file
)
13 bool unlock
= !git_mutex_lock(&file
->lock
);
14 git_attr_file__clear_rules(file
, false);
15 git_pool_clear(&file
->pool
);
17 git_mutex_unlock(&file
->lock
);
18 git_mutex_free(&file
->lock
);
20 git__memzero(file
, sizeof(*file
));
24 int git_attr_file__new(
26 git_attr_file_entry
*entry
,
27 git_attr_file_source source
)
29 git_attr_file
*attrs
= git__calloc(1, sizeof(git_attr_file
));
30 GITERR_CHECK_ALLOC(attrs
);
32 if (git_mutex_init(&attrs
->lock
) < 0) {
33 giterr_set(GITERR_OS
, "Failed to initialize lock");
38 if (git_pool_init(&attrs
->pool
, 1, 0) < 0) {
39 attr_file_free(attrs
);
43 GIT_REFCOUNT_INC(attrs
);
45 attrs
->source
= source
;
50 int git_attr_file__clear_rules(git_attr_file
*file
, bool need_lock
)
55 if (need_lock
&& git_mutex_lock(&file
->lock
) < 0) {
56 giterr_set(GITERR_OS
, "Failed to lock attribute file");
60 git_vector_foreach(&file
->rules
, i
, rule
)
61 git_attr_rule__free(rule
);
62 git_vector_free(&file
->rules
);
65 git_mutex_unlock(&file
->lock
);
70 void git_attr_file__free(git_attr_file
*file
)
74 GIT_REFCOUNT_DEC(file
, attr_file_free
);
77 static int attr_file_oid_from_index(
78 git_oid
*oid
, git_repository
*repo
, const char *path
)
83 const git_index_entry
*entry
;
85 if ((error
= git_repository_index__weakptr(&idx
, repo
)) < 0 ||
86 (error
= git_index__find_pos(&pos
, idx
, path
, 0, 0)) < 0)
89 if (!(entry
= git_index_get_byindex(idx
, pos
)))
96 int git_attr_file__load(
99 git_attr_file_entry
*entry
,
100 git_attr_file_source source
,
101 git_attr_file_parser parser
)
104 git_blob
*blob
= NULL
;
105 git_buf content
= GIT_BUF_INIT
;
106 const char *data
= NULL
;
113 case GIT_ATTR_FILE__IN_MEMORY
:
114 /* in-memory attribute file doesn't need data */
116 case GIT_ATTR_FILE__FROM_INDEX
: {
119 if ((error
= attr_file_oid_from_index(&id
, repo
, entry
->path
)) < 0 ||
120 (error
= git_blob_lookup(&blob
, repo
, &id
)) < 0)
123 data
= git_blob_rawcontent(blob
);
126 case GIT_ATTR_FILE__FROM_FILE
: {
129 if (p_stat(entry
->fullpath
, &st
) < 0)
130 return git_path_set_error(errno
, entry
->fullpath
, "stat");
131 if (S_ISDIR(st
.st_mode
))
132 return GIT_ENOTFOUND
;
134 /* For open or read errors, return ENOTFOUND to skip item */
135 /* TODO: issue warning when warning API is available */
137 if ((fd
= git_futils_open_ro(entry
->fullpath
)) < 0)
138 return GIT_ENOTFOUND
;
140 error
= git_futils_readbuffer_fd(&content
, fd
, (size_t)st
.st_size
);
144 return GIT_ENOTFOUND
;
150 giterr_set(GITERR_INVALID
, "Unknown file source %d", source
);
154 if ((error
= git_attr_file__new(&file
, entry
, source
)) < 0)
157 if (parser
&& (error
= parser(repo
, file
, data
)) < 0) {
158 git_attr_file__free(file
);
162 /* write cache breaker */
163 if (source
== GIT_ATTR_FILE__FROM_INDEX
)
164 git_oid_cpy(&file
->cache_data
.oid
, git_blob_id(blob
));
165 else if (source
== GIT_ATTR_FILE__FROM_FILE
)
166 git_futils_filestamp_set_from_stat(&file
->cache_data
.stamp
, &st
);
167 /* else always cacheable */
173 git_buf_free(&content
);
178 int git_attr_file__out_of_date(git_repository
*repo
, git_attr_file
*file
)
183 switch (file
->source
) {
184 case GIT_ATTR_FILE__IN_MEMORY
:
187 case GIT_ATTR_FILE__FROM_FILE
:
188 return git_futils_filestamp_check(
189 &file
->cache_data
.stamp
, file
->entry
->fullpath
);
191 case GIT_ATTR_FILE__FROM_INDEX
: {
195 if ((error
= attr_file_oid_from_index(
196 &id
, repo
, file
->entry
->path
)) < 0)
199 return (git_oid__cmp(&file
->cache_data
.oid
, &id
) != 0);
203 giterr_set(GITERR_INVALID
, "Invalid file type %d", file
->source
);
208 static int sort_by_hash_and_name(const void *a_raw
, const void *b_raw
);
209 static void git_attr_rule__clear(git_attr_rule
*rule
);
210 static bool parse_optimized_patterns(
211 git_attr_fnmatch
*spec
,
213 const char *pattern
);
215 int git_attr_file__parse_buffer(
216 git_repository
*repo
, git_attr_file
*attrs
, const char *data
)
219 const char *scan
= data
, *context
= NULL
;
220 git_attr_rule
*rule
= NULL
;
222 /* if subdir file path, convert context for file paths */
224 git_path_root(attrs
->entry
->path
) < 0 &&
225 !git__suffixcmp(attrs
->entry
->path
, "/" GIT_ATTR_FILE
))
226 context
= attrs
->entry
->path
;
228 if (git_mutex_lock(&attrs
->lock
) < 0) {
229 giterr_set(GITERR_OS
, "Failed to lock attribute file");
233 while (!error
&& *scan
) {
234 /* allocate rule if needed */
235 if (!rule
&& !(rule
= git__calloc(1, sizeof(*rule
)))) {
241 GIT_ATTR_FNMATCH_ALLOWNEG
| GIT_ATTR_FNMATCH_ALLOWMACRO
;
243 /* parse the next "pattern attr attr attr" line */
244 if (!(error
= git_attr_fnmatch__parse(
245 &rule
->match
, &attrs
->pool
, context
, &scan
)) &&
246 !(error
= git_attr_assignment__parse(
247 repo
, &attrs
->pool
, &rule
->assigns
, &scan
)))
249 if (rule
->match
.flags
& GIT_ATTR_FNMATCH_MACRO
)
250 /* TODO: warning if macro found in file below repo root */
251 error
= git_attr_cache__insert_macro(repo
, rule
);
253 error
= git_vector_insert(&attrs
->rules
, rule
);
256 /* if the rule wasn't a pattern, on to the next */
258 git_attr_rule__clear(rule
); /* reset rule contents */
259 if (error
== GIT_ENOTFOUND
)
262 rule
= NULL
; /* vector now "owns" the rule */
266 git_mutex_unlock(&attrs
->lock
);
267 git_attr_rule__free(rule
);
272 uint32_t git_attr_file__name_hash(const char *name
)
277 while ((c
= (int)*name
++) != 0)
278 h
= ((h
<< 5) + h
) + c
;
282 int git_attr_file__lookup_one(
295 name
.name_hash
= git_attr_file__name_hash(attr
);
297 git_attr_file__foreach_matching_rule(file
, path
, i
, rule
) {
300 if (!git_vector_bsearch(&pos
, &rule
->assigns
, &name
)) {
301 *value
= ((git_attr_assignment
*)
302 git_vector_get(&rule
->assigns
, pos
))->value
;
310 int git_attr_file__load_standalone(git_attr_file
**out
, const char *path
)
314 git_buf content
= GIT_BUF_INIT
;
316 error
= git_attr_file__new(&file
, NULL
, GIT_ATTR_FILE__FROM_FILE
);
320 error
= git_attr_cache__alloc_file_entry(
321 &file
->entry
, NULL
, path
, &file
->pool
);
323 git_attr_file__free(file
);
326 /* because the cache entry is allocated from the file's own pool, we
327 * don't have to free it - freeing file+pool will free cache entry, too.
330 if (!(error
= git_futils_readbuffer(&content
, path
))) {
331 error
= git_attr_file__parse_buffer(NULL
, file
, content
.ptr
);
332 git_buf_free(&content
);
336 git_attr_file__free(file
);
343 bool git_attr_fnmatch__match(
344 git_attr_fnmatch
*match
,
347 const char *filename
;
350 if (match
->flags
& GIT_ATTR_FNMATCH_ICASE
)
351 flags
|= FNM_CASEFOLD
;
352 if (match
->flags
& GIT_ATTR_FNMATCH_LEADINGDIR
)
353 flags
|= FNM_LEADING_DIR
;
355 if (match
->flags
& GIT_ATTR_FNMATCH_FULLPATH
) {
356 filename
= path
->path
;
357 flags
|= FNM_PATHNAME
;
359 filename
= path
->basename
;
362 flags
|= FNM_LEADING_DIR
;
365 if ((match
->flags
& GIT_ATTR_FNMATCH_DIRECTORY
) && !path
->is_dir
) {
368 /* for attribute checks or root ignore checks, fail match */
369 if (!(match
->flags
& GIT_ATTR_FNMATCH_IGNORE
) ||
370 path
->basename
== path
->path
)
373 /* for ignore checks, use container of current item for check */
374 path
->basename
[-1] = '\0';
375 flags
|= FNM_LEADING_DIR
;
376 matchval
= p_fnmatch(match
->pattern
, path
->path
, flags
);
377 path
->basename
[-1] = '/';
378 return (matchval
!= FNM_NOMATCH
);
381 /* if path is a directory prefix of a negated pattern, then match */
382 if ((match
->flags
& GIT_ATTR_FNMATCH_NEGATIVE
) && path
->is_dir
) {
383 size_t pathlen
= strlen(path
->path
);
384 bool prefixed
= (pathlen
<= match
->length
) &&
385 ((match
->flags
& GIT_ATTR_FNMATCH_ICASE
) ?
386 !strncasecmp(match
->pattern
, path
->path
, pathlen
) :
387 !strncmp(match
->pattern
, path
->path
, pathlen
));
389 if (prefixed
&& git_path_at_end_of_segment(&match
->pattern
[pathlen
]))
393 return (p_fnmatch(match
->pattern
, filename
, flags
) != FNM_NOMATCH
);
396 bool git_attr_rule__match(
400 bool matched
= git_attr_fnmatch__match(&rule
->match
, path
);
402 if (rule
->match
.flags
& GIT_ATTR_FNMATCH_NEGATIVE
)
408 git_attr_assignment
*git_attr_rule__lookup_assignment(
409 git_attr_rule
*rule
, const char *name
)
414 key
.name_hash
= git_attr_file__name_hash(name
);
416 if (git_vector_bsearch(&pos
, &rule
->assigns
, &key
))
419 return git_vector_get(&rule
->assigns
, pos
);
422 int git_attr_path__init(
423 git_attr_path
*info
, const char *path
, const char *base
)
427 /* build full path as best we can */
428 git_buf_init(&info
->full
, 0);
430 if (git_path_join_unrooted(&info
->full
, path
, base
, &root
) < 0)
433 info
->path
= info
->full
.ptr
+ root
;
435 /* remove trailing slashes */
436 while (info
->full
.size
> 0) {
437 if (info
->full
.ptr
[info
->full
.size
- 1] != '/')
441 info
->full
.ptr
[info
->full
.size
] = '\0';
443 /* skip leading slashes in path */
444 while (*info
->path
== '/')
447 /* find trailing basename component */
448 info
->basename
= strrchr(info
->path
, '/');
451 if (!info
->basename
|| !*info
->basename
)
452 info
->basename
= info
->path
;
454 info
->is_dir
= (int)git_path_isdir(info
->full
.ptr
);
459 void git_attr_path__free(git_attr_path
*info
)
461 git_buf_free(&info
->full
);
463 info
->basename
= NULL
;
467 * From gitattributes(5):
469 * Patterns have the following format:
471 * - A blank line matches no files, so it can serve as a separator for
474 * - A line starting with # serves as a comment.
476 * - An optional prefix ! which negates the pattern; any matching file
477 * excluded by a previous pattern will become included again. If a negated
478 * pattern matches, this will override lower precedence patterns sources.
480 * - If the pattern ends with a slash, it is removed for the purpose of the
481 * following description, but it would only find a match with a directory. In
482 * other words, foo/ will match a directory foo and paths underneath it, but
483 * will not match a regular file or a symbolic link foo (this is consistent
484 * with the way how pathspec works in general in git).
486 * - If the pattern does not contain a slash /, git treats it as a shell glob
487 * pattern and checks for a match against the pathname without leading
490 * - Otherwise, git treats the pattern as a shell glob suitable for consumption
491 * by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will
492 * not match a / in the pathname. For example, "Documentation/\*.html" matches
493 * "Documentation/git.html" but not "Documentation/ppc/ppc.html". A leading
494 * slash matches the beginning of the pathname; for example, "/\*.c" matches
495 * "cat-file.c" but not "mozilla-sha1/sha1.c".
499 * This will return 0 if the spec was filled out,
500 * GIT_ENOTFOUND if the fnmatch does not require matching, or
501 * another error code there was an actual problem.
503 int git_attr_fnmatch__parse(
504 git_attr_fnmatch
*spec
,
509 const char *pattern
, *scan
;
510 int slash_count
, allow_space
;
512 assert(spec
&& base
&& *base
);
514 if (parse_optimized_patterns(spec
, pool
, *base
))
517 spec
->flags
= (spec
->flags
& GIT_ATTR_FNMATCH__INCOMING
);
518 allow_space
= ((spec
->flags
& GIT_ATTR_FNMATCH_ALLOWSPACE
) != 0);
522 while (git__isspace(*pattern
)) pattern
++;
523 if (!*pattern
|| *pattern
== '#') {
524 *base
= git__next_line(pattern
);
525 return GIT_ENOTFOUND
;
528 if (*pattern
== '[' && (spec
->flags
& GIT_ATTR_FNMATCH_ALLOWMACRO
) != 0) {
529 if (strncmp(pattern
, "[attr]", 6) == 0) {
530 spec
->flags
= spec
->flags
| GIT_ATTR_FNMATCH_MACRO
;
533 /* else a character range like [a-e]* which is accepted */
536 if (*pattern
== '!' && (spec
->flags
& GIT_ATTR_FNMATCH_ALLOWNEG
) != 0) {
537 spec
->flags
= spec
->flags
|
538 GIT_ATTR_FNMATCH_NEGATIVE
| GIT_ATTR_FNMATCH_LEADINGDIR
;
543 for (scan
= pattern
; *scan
!= '\0'; ++scan
) {
544 /* scan until (non-escaped) white space */
545 if (git__isspace(*scan
) && *(scan
- 1) != '\\') {
546 if (!allow_space
|| (*scan
!= ' ' && *scan
!= '\t'))
551 spec
->flags
= spec
->flags
| GIT_ATTR_FNMATCH_FULLPATH
;
556 /* remember if we see an unescaped wildcard in pattern */
557 else if (git__iswildcard(*scan
) &&
558 (scan
== pattern
|| (*(scan
- 1) != '\\')))
559 spec
->flags
= spec
->flags
| GIT_ATTR_FNMATCH_HASWILD
;
564 if ((spec
->length
= scan
- pattern
) == 0)
565 return GIT_ENOTFOUND
;
567 if (pattern
[spec
->length
- 1] == '/') {
569 spec
->flags
= spec
->flags
| GIT_ATTR_FNMATCH_DIRECTORY
;
570 if (--slash_count
<= 0)
571 spec
->flags
= spec
->flags
& ~GIT_ATTR_FNMATCH_FULLPATH
;
573 if ((spec
->flags
& GIT_ATTR_FNMATCH_NOLEADINGDIR
) == 0 &&
575 pattern
[spec
->length
- 1] == '*' &&
576 pattern
[spec
->length
- 2] == '/') {
578 spec
->flags
= spec
->flags
| GIT_ATTR_FNMATCH_LEADINGDIR
;
579 /* leave FULLPATH match on, however */
582 if ((spec
->flags
& GIT_ATTR_FNMATCH_FULLPATH
) != 0 &&
583 context
!= NULL
&& git_path_root(pattern
) < 0)
585 /* use context path minus the trailing filename */
586 char *slash
= strrchr(context
, '/');
587 size_t contextlen
= slash
? slash
- context
+ 1 : 0;
589 /* given an unrooted fullpath match from a file inside a repo,
590 * prefix the pattern with the relative directory of the source file
592 spec
->pattern
= git_pool_malloc(
593 pool
, (uint32_t)(contextlen
+ spec
->length
+ 1));
595 memcpy(spec
->pattern
, context
, contextlen
);
596 memcpy(spec
->pattern
+ contextlen
, pattern
, spec
->length
);
597 spec
->length
+= contextlen
;
598 spec
->pattern
[spec
->length
] = '\0';
601 spec
->pattern
= git_pool_strndup(pool
, pattern
, spec
->length
);
604 if (!spec
->pattern
) {
605 *base
= git__next_line(pattern
);
608 /* strip '\' that might have be used for internal whitespace */
609 spec
->length
= git__unescape(spec
->pattern
);
610 /* TODO: convert remaining '\' into '/' for POSIX ??? */
616 static bool parse_optimized_patterns(
617 git_attr_fnmatch
*spec
,
621 if (!pattern
[1] && (pattern
[0] == '*' || pattern
[0] == '.')) {
622 spec
->flags
= GIT_ATTR_FNMATCH_MATCH_ALL
;
623 spec
->pattern
= git_pool_strndup(pool
, pattern
, 1);
632 static int sort_by_hash_and_name(const void *a_raw
, const void *b_raw
)
634 const git_attr_name
*a
= a_raw
;
635 const git_attr_name
*b
= b_raw
;
637 if (b
->name_hash
< a
->name_hash
)
639 else if (b
->name_hash
> a
->name_hash
)
642 return strcmp(b
->name
, a
->name
);
645 static void git_attr_assignment__free(git_attr_assignment
*assign
)
647 /* name and value are stored in a git_pool associated with the
648 * git_attr_file, so they do not need to be freed here
651 assign
->value
= NULL
;
655 static int merge_assignments(void **old_raw
, void *new_raw
)
657 git_attr_assignment
**old
= (git_attr_assignment
**)old_raw
;
658 git_attr_assignment
*new = (git_attr_assignment
*)new_raw
;
660 GIT_REFCOUNT_DEC(*old
, git_attr_assignment__free
);
665 int git_attr_assignment__parse(
666 git_repository
*repo
,
672 const char *scan
= *base
;
673 git_attr_assignment
*assign
= NULL
;
675 assert(assigns
&& !assigns
->length
);
677 git_vector_set_cmp(assigns
, sort_by_hash_and_name
);
679 while (*scan
&& *scan
!= '\n') {
680 const char *name_start
, *value_start
;
682 /* skip leading blanks */
683 while (git__isspace(*scan
) && *scan
!= '\n') scan
++;
685 /* allocate assign if needed */
687 assign
= git__calloc(1, sizeof(git_attr_assignment
));
688 GITERR_CHECK_ALLOC(assign
);
689 GIT_REFCOUNT_INC(assign
);
692 assign
->name_hash
= 5381;
693 assign
->value
= git_attr__true
;
695 /* look for magic name prefixes */
697 assign
->value
= git_attr__false
;
699 } else if (*scan
== '!') {
700 assign
->value
= git_attr__unset
; /* explicit unspecified state */
702 } else if (*scan
== '#') /* comment rest of line */
707 while (*scan
&& !git__isspace(*scan
) && *scan
!= '=') {
709 ((assign
->name_hash
<< 5) + assign
->name_hash
) + *scan
;
712 if (scan
== name_start
) {
713 /* must have found lone prefix (" - ") or leading = ("=foo")
714 * or end of buffer -- advance until whitespace and continue
716 while (*scan
&& !git__isspace(*scan
)) scan
++;
720 /* allocate permanent storage for name */
721 assign
->name
= git_pool_strndup(pool
, name_start
, scan
- name_start
);
722 GITERR_CHECK_ALLOC(assign
->name
);
724 /* if there is an equals sign, find the value */
726 for (value_start
= ++scan
; *scan
&& !git__isspace(*scan
); ++scan
);
728 /* if we found a value, allocate permanent storage for it */
729 if (scan
> value_start
) {
730 assign
->value
= git_pool_strndup(pool
, value_start
, scan
- value_start
);
731 GITERR_CHECK_ALLOC(assign
->value
);
735 /* expand macros (if given a repo with a macro cache) */
736 if (repo
!= NULL
&& assign
->value
== git_attr__true
) {
737 git_attr_rule
*macro
=
738 git_attr_cache__lookup_macro(repo
, assign
->name
);
742 git_attr_assignment
*massign
;
744 git_vector_foreach(¯o
->assigns
, i
, massign
) {
745 GIT_REFCOUNT_INC(massign
);
747 error
= git_vector_insert_sorted(
748 assigns
, massign
, &merge_assignments
);
749 if (error
< 0 && error
!= GIT_EEXISTS
)
755 /* insert allocated assign into vector */
756 error
= git_vector_insert_sorted(assigns
, assign
, &merge_assignments
);
757 if (error
< 0 && error
!= GIT_EEXISTS
)
760 /* clear assign since it is now "owned" by the vector */
765 git_attr_assignment__free(assign
);
767 *base
= git__next_line(scan
);
769 return (assigns
->length
== 0) ? GIT_ENOTFOUND
: 0;
772 static void git_attr_rule__clear(git_attr_rule
*rule
)
775 git_attr_assignment
*assign
;
780 if (!(rule
->match
.flags
& GIT_ATTR_FNMATCH_IGNORE
)) {
781 git_vector_foreach(&rule
->assigns
, i
, assign
)
782 GIT_REFCOUNT_DEC(assign
, git_attr_assignment__free
);
783 git_vector_free(&rule
->assigns
);
786 /* match.pattern is stored in a git_pool, so no need to free */
787 rule
->match
.pattern
= NULL
;
788 rule
->match
.length
= 0;
791 void git_attr_rule__free(git_attr_rule
*rule
)
793 git_attr_rule__clear(rule
);