]> git.proxmox.com Git - libgit2.git/blob - src/attr_file.c
attr: Rename the `git_attr__` exports
[libgit2.git] / src / attr_file.c
1 #include "common.h"
2 #include "repository.h"
3 #include "filebuf.h"
4 #include "git2/blob.h"
5 #include "git2/tree.h"
6 #include <ctype.h>
7
8 const char *git_l_attr__true = "[internal]__TRUE__";
9 const char *git_l_attr__false = "[internal]__FALSE__";
10 const char *git_l_attr__unset = "[internal]__UNSET__";
11
12 static int sort_by_hash_and_name(const void *a_raw, const void *b_raw);
13 static void git_attr_rule__clear(git_attr_rule *rule);
14
15 int git_attr_file__new(
16 git_attr_file **attrs_ptr,
17 git_attr_file_source from,
18 const char *path,
19 git_pool *pool)
20 {
21 git_attr_file *attrs = NULL;
22
23 attrs = git__calloc(1, sizeof(git_attr_file));
24 GITERR_CHECK_ALLOC(attrs);
25
26 if (pool)
27 attrs->pool = pool;
28 else {
29 attrs->pool = git__calloc(1, sizeof(git_pool));
30 if (!attrs->pool || git_pool_init(attrs->pool, 1, 0) < 0)
31 goto fail;
32 attrs->pool_is_allocated = true;
33 }
34
35 if (path) {
36 size_t len = strlen(path);
37
38 attrs->key = git_pool_malloc(attrs->pool, (uint32_t)len + 3);
39 GITERR_CHECK_ALLOC(attrs->key);
40
41 attrs->key[0] = '0' + from;
42 attrs->key[1] = '#';
43 memcpy(&attrs->key[2], path, len);
44 attrs->key[len + 2] = '\0';
45 }
46
47 if (git_vector_init(&attrs->rules, 4, NULL) < 0)
48 goto fail;
49
50 *attrs_ptr = attrs;
51 return 0;
52
53 fail:
54 git_attr_file__free(attrs);
55 attrs_ptr = NULL;
56 return -1;
57 }
58
59 int git_attr_file__parse_buffer(
60 git_repository *repo, const char *buffer, git_attr_file *attrs)
61 {
62 int error = 0;
63 const char *scan = NULL;
64 char *context = NULL;
65 git_attr_rule *rule = NULL;
66
67 assert(buffer && attrs);
68
69 scan = buffer;
70
71 /* if subdir file path, convert context for file paths */
72 if (attrs->key && git__suffixcmp(attrs->key, "/" GIT_ATTR_FILE) == 0) {
73 context = attrs->key + 2;
74 context[strlen(context) - strlen(GIT_ATTR_FILE)] = '\0';
75 }
76
77 while (!error && *scan) {
78 /* allocate rule if needed */
79 if (!rule && !(rule = git__calloc(1, sizeof(git_attr_rule)))) {
80 error = -1;
81 break;
82 }
83
84 /* parse the next "pattern attr attr attr" line */
85 if (!(error = git_attr_fnmatch__parse(
86 &rule->match, attrs->pool, context, &scan)) &&
87 !(error = git_attr_assignment__parse(
88 repo, attrs->pool, &rule->assigns, &scan)))
89 {
90 if (rule->match.flags & GIT_ATTR_FNMATCH_MACRO)
91 /* should generate error/warning if this is coming from any
92 * file other than .gitattributes at repo root.
93 */
94 error = git_attr_cache__insert_macro(repo, rule);
95 else
96 error = git_vector_insert(&attrs->rules, rule);
97 }
98
99 /* if the rule wasn't a pattern, on to the next */
100 if (error < 0) {
101 git_attr_rule__clear(rule); /* reset rule contents */
102 if (error == GIT_ENOTFOUND)
103 error = 0;
104 } else {
105 rule = NULL; /* vector now "owns" the rule */
106 }
107 }
108
109 git_attr_rule__free(rule);
110
111 /* restore file path used for context */
112 if (context)
113 context[strlen(context)] = '.'; /* first char of GIT_ATTR_FILE */
114
115 return error;
116 }
117
118 int git_attr_file__new_and_load(
119 git_attr_file **attrs_ptr,
120 const char *path)
121 {
122 int error;
123 git_buf content = GIT_BUF_INIT;
124
125 if ((error = git_attr_file__new(attrs_ptr, 0, path, NULL)) < 0)
126 return error;
127
128 if (!(error = git_futils_readbuffer(&content, path)))
129 error = git_attr_file__parse_buffer(
130 NULL, git_buf_cstr(&content), *attrs_ptr);
131
132 git_buf_free(&content);
133
134 if (error) {
135 git_attr_file__free(*attrs_ptr);
136 *attrs_ptr = NULL;
137 }
138
139 return error;
140 }
141
142 void git_attr_file__clear_rules(git_attr_file *file)
143 {
144 unsigned int i;
145 git_attr_rule *rule;
146
147 git_vector_foreach(&file->rules, i, rule)
148 git_attr_rule__free(rule);
149
150 git_vector_free(&file->rules);
151 }
152
153 void git_attr_file__free(git_attr_file *file)
154 {
155 if (!file)
156 return;
157
158 git_attr_file__clear_rules(file);
159
160 if (file->pool_is_allocated) {
161 git_pool_clear(file->pool);
162 git__free(file->pool);
163 }
164 file->pool = NULL;
165
166 git__free(file);
167 }
168
169 uint32_t git_attr_file__name_hash(const char *name)
170 {
171 uint32_t h = 5381;
172 int c;
173 assert(name);
174 while ((c = (int)*name++) != 0)
175 h = ((h << 5) + h) + c;
176 return h;
177 }
178
179
180 int git_attr_file__lookup_one(
181 git_attr_file *file,
182 const git_attr_path *path,
183 const char *attr,
184 const char **value)
185 {
186 unsigned int i;
187 git_attr_name name;
188 git_attr_rule *rule;
189
190 *value = NULL;
191
192 name.name = attr;
193 name.name_hash = git_attr_file__name_hash(attr);
194
195 git_attr_file__foreach_matching_rule(file, path, i, rule) {
196 int pos = git_vector_bsearch(&rule->assigns, &name);
197
198 if (pos >= 0) {
199 *value = ((git_attr_assignment *)
200 git_vector_get(&rule->assigns, pos))->value;
201 break;
202 }
203 }
204
205 return 0;
206 }
207
208
209 bool git_attr_fnmatch__match(
210 git_attr_fnmatch *match,
211 const git_attr_path *path)
212 {
213 int fnm;
214
215 if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY && !path->is_dir)
216 return false;
217
218 if (match->flags & GIT_ATTR_FNMATCH_FULLPATH)
219 fnm = p_fnmatch(match->pattern, path->path, FNM_PATHNAME);
220 else if (path->is_dir)
221 fnm = p_fnmatch(match->pattern, path->basename, FNM_LEADING_DIR);
222 else
223 fnm = p_fnmatch(match->pattern, path->basename, 0);
224
225 return (fnm == FNM_NOMATCH) ? false : true;
226 }
227
228 bool git_attr_rule__match(
229 git_attr_rule *rule,
230 const git_attr_path *path)
231 {
232 bool matched = git_attr_fnmatch__match(&rule->match, path);
233
234 if (rule->match.flags & GIT_ATTR_FNMATCH_NEGATIVE)
235 matched = !matched;
236
237 return matched;
238 }
239
240
241 git_attr_assignment *git_attr_rule__lookup_assignment(
242 git_attr_rule *rule, const char *name)
243 {
244 int pos;
245 git_attr_name key;
246 key.name = name;
247 key.name_hash = git_attr_file__name_hash(name);
248
249 pos = git_vector_bsearch(&rule->assigns, &key);
250
251 return (pos >= 0) ? git_vector_get(&rule->assigns, pos) : NULL;
252 }
253
254 int git_attr_path__init(
255 git_attr_path *info, const char *path, const char *base)
256 {
257 /* build full path as best we can */
258 git_buf_init(&info->full, 0);
259
260 if (base != NULL && git_path_root(path) < 0) {
261 if (git_buf_joinpath(&info->full, base, path) < 0)
262 return -1;
263 info->path = info->full.ptr + strlen(base);
264 } else {
265 if (git_buf_sets(&info->full, path) < 0)
266 return -1;
267 info->path = info->full.ptr;
268 }
269
270 /* remove trailing slashes */
271 while (info->full.size > 0) {
272 if (info->full.ptr[info->full.size - 1] != '/')
273 break;
274 info->full.size--;
275 }
276 info->full.ptr[info->full.size] = '\0';
277
278 /* skip leading slashes in path */
279 while (*info->path == '/')
280 info->path++;
281
282 /* find trailing basename component */
283 info->basename = strrchr(info->path, '/');
284 if (info->basename)
285 info->basename++;
286 if (!info->basename || !*info->basename)
287 info->basename = info->path;
288
289 info->is_dir = (int)git_path_isdir(info->full.ptr);
290
291 return 0;
292 }
293
294 void git_attr_path__free(git_attr_path *info)
295 {
296 git_buf_free(&info->full);
297 info->path = NULL;
298 info->basename = NULL;
299 }
300
301
302 /*
303 * From gitattributes(5):
304 *
305 * Patterns have the following format:
306 *
307 * - A blank line matches no files, so it can serve as a separator for
308 * readability.
309 *
310 * - A line starting with # serves as a comment.
311 *
312 * - An optional prefix ! which negates the pattern; any matching file
313 * excluded by a previous pattern will become included again. If a negated
314 * pattern matches, this will override lower precedence patterns sources.
315 *
316 * - If the pattern ends with a slash, it is removed for the purpose of the
317 * following description, but it would only find a match with a directory. In
318 * other words, foo/ will match a directory foo and paths underneath it, but
319 * will not match a regular file or a symbolic link foo (this is consistent
320 * with the way how pathspec works in general in git).
321 *
322 * - If the pattern does not contain a slash /, git treats it as a shell glob
323 * pattern and checks for a match against the pathname without leading
324 * directories.
325 *
326 * - Otherwise, git treats the pattern as a shell glob suitable for consumption
327 * by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will
328 * not match a / in the pathname. For example, "Documentation/\*.html" matches
329 * "Documentation/git.html" but not "Documentation/ppc/ppc.html". A leading
330 * slash matches the beginning of the pathname; for example, "/\*.c" matches
331 * "cat-file.c" but not "mozilla-sha1/sha1.c".
332 */
333
334 /*
335 * This will return 0 if the spec was filled out,
336 * GIT_ENOTFOUND if the fnmatch does not require matching, or
337 * another error code there was an actual problem.
338 */
339 int git_attr_fnmatch__parse(
340 git_attr_fnmatch *spec,
341 git_pool *pool,
342 const char *source,
343 const char **base)
344 {
345 const char *pattern, *scan;
346 int slash_count, allow_space;
347
348 assert(spec && base && *base);
349
350 spec->flags = (spec->flags & GIT_ATTR_FNMATCH_ALLOWSPACE);
351 allow_space = (spec->flags != 0);
352
353 pattern = *base;
354
355 while (git__isspace(*pattern)) pattern++;
356 if (!*pattern || *pattern == '#') {
357 *base = git__next_line(pattern);
358 return GIT_ENOTFOUND;
359 }
360
361 if (*pattern == '[') {
362 if (strncmp(pattern, "[attr]", 6) == 0) {
363 spec->flags = spec->flags | GIT_ATTR_FNMATCH_MACRO;
364 pattern += 6;
365 }
366 /* else a character range like [a-e]* which is accepted */
367 }
368
369 if (*pattern == '!') {
370 spec->flags = spec->flags | GIT_ATTR_FNMATCH_NEGATIVE;
371 pattern++;
372 }
373
374 slash_count = 0;
375 for (scan = pattern; *scan != '\0'; ++scan) {
376 /* scan until (non-escaped) white space */
377 if (git__isspace(*scan) && *(scan - 1) != '\\') {
378 if (!allow_space || (*scan != ' ' && *scan != '\t'))
379 break;
380 }
381
382 if (*scan == '/') {
383 spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH;
384 slash_count++;
385 if (pattern == scan)
386 pattern++;
387 }
388 /* remember if we see an unescaped wildcard in pattern */
389 else if (git__iswildcard(*scan) &&
390 (scan == pattern || (*(scan - 1) != '\\')))
391 spec->flags = spec->flags | GIT_ATTR_FNMATCH_HASWILD;
392 }
393
394 *base = scan;
395
396 spec->length = scan - pattern;
397
398 if (pattern[spec->length - 1] == '/') {
399 spec->length--;
400 spec->flags = spec->flags | GIT_ATTR_FNMATCH_DIRECTORY;
401 if (--slash_count <= 0)
402 spec->flags = spec->flags & ~GIT_ATTR_FNMATCH_FULLPATH;
403 }
404
405 if ((spec->flags & GIT_ATTR_FNMATCH_FULLPATH) != 0 &&
406 source != NULL && git_path_root(pattern) < 0)
407 {
408 size_t sourcelen = strlen(source);
409 /* given an unrooted fullpath match from a file inside a repo,
410 * prefix the pattern with the relative directory of the source file
411 */
412 spec->pattern = git_pool_malloc(
413 pool, (uint32_t)(sourcelen + spec->length + 1));
414 if (spec->pattern) {
415 memcpy(spec->pattern, source, sourcelen);
416 memcpy(spec->pattern + sourcelen, pattern, spec->length);
417 spec->length += sourcelen;
418 spec->pattern[spec->length] = '\0';
419 }
420 } else {
421 spec->pattern = git_pool_strndup(pool, pattern, spec->length);
422 }
423
424 if (!spec->pattern) {
425 *base = git__next_line(pattern);
426 return -1;
427 } else {
428 /* strip '\' that might have be used for internal whitespace */
429 char *to = spec->pattern;
430 for (scan = spec->pattern; *scan; to++, scan++) {
431 if (*scan == '\\')
432 scan++; /* skip '\' but include next char */
433 if (to != scan)
434 *to = *scan;
435 }
436 if (to != scan) {
437 *to = '\0';
438 spec->length = (to - spec->pattern);
439 }
440 }
441
442 return 0;
443 }
444
445 static int sort_by_hash_and_name(const void *a_raw, const void *b_raw)
446 {
447 const git_attr_name *a = a_raw;
448 const git_attr_name *b = b_raw;
449
450 if (b->name_hash < a->name_hash)
451 return 1;
452 else if (b->name_hash > a->name_hash)
453 return -1;
454 else
455 return strcmp(b->name, a->name);
456 }
457
458 static void git_attr_assignment__free(git_attr_assignment *assign)
459 {
460 /* name and value are stored in a git_pool associated with the
461 * git_attr_file, so they do not need to be freed here
462 */
463 assign->name = NULL;
464 assign->value = NULL;
465 git__free(assign);
466 }
467
468 static int merge_assignments(void **old_raw, void *new_raw)
469 {
470 git_attr_assignment **old = (git_attr_assignment **)old_raw;
471 git_attr_assignment *new = (git_attr_assignment *)new_raw;
472
473 GIT_REFCOUNT_DEC(*old, git_attr_assignment__free);
474 *old = new;
475 return GIT_EEXISTS;
476 }
477
478 int git_attr_assignment__parse(
479 git_repository *repo,
480 git_pool *pool,
481 git_vector *assigns,
482 const char **base)
483 {
484 int error;
485 const char *scan = *base;
486 git_attr_assignment *assign = NULL;
487
488 assert(assigns && !assigns->length);
489
490 assigns->_cmp = sort_by_hash_and_name;
491
492 while (*scan && *scan != '\n') {
493 const char *name_start, *value_start;
494
495 /* skip leading blanks */
496 while (git__isspace(*scan) && *scan != '\n') scan++;
497
498 /* allocate assign if needed */
499 if (!assign) {
500 assign = git__calloc(1, sizeof(git_attr_assignment));
501 GITERR_CHECK_ALLOC(assign);
502 GIT_REFCOUNT_INC(assign);
503 }
504
505 assign->name_hash = 5381;
506 assign->value = git_l_attr__true;
507
508 /* look for magic name prefixes */
509 if (*scan == '-') {
510 assign->value = git_l_attr__false;
511 scan++;
512 } else if (*scan == '!') {
513 assign->value = git_l_attr__unset; /* explicit unspecified state */
514 scan++;
515 } else if (*scan == '#') /* comment rest of line */
516 break;
517
518 /* find the name */
519 name_start = scan;
520 while (*scan && !git__isspace(*scan) && *scan != '=') {
521 assign->name_hash =
522 ((assign->name_hash << 5) + assign->name_hash) + *scan;
523 scan++;
524 }
525 if (scan == name_start) {
526 /* must have found lone prefix (" - ") or leading = ("=foo")
527 * or end of buffer -- advance until whitespace and continue
528 */
529 while (*scan && !git__isspace(*scan)) scan++;
530 continue;
531 }
532
533 /* allocate permanent storage for name */
534 assign->name = git_pool_strndup(pool, name_start, scan - name_start);
535 GITERR_CHECK_ALLOC(assign->name);
536
537 /* if there is an equals sign, find the value */
538 if (*scan == '=') {
539 for (value_start = ++scan; *scan && !git__isspace(*scan); ++scan);
540
541 /* if we found a value, allocate permanent storage for it */
542 if (scan > value_start) {
543 assign->value = git_pool_strndup(pool, value_start, scan - value_start);
544 GITERR_CHECK_ALLOC(assign->value);
545 }
546 }
547
548 /* expand macros (if given a repo with a macro cache) */
549 if (repo != NULL && assign->value == git_l_attr__true) {
550 git_attr_rule *macro =
551 git_attr_cache__lookup_macro(repo, assign->name);
552
553 if (macro != NULL) {
554 unsigned int i;
555 git_attr_assignment *massign;
556
557 git_vector_foreach(&macro->assigns, i, massign) {
558 GIT_REFCOUNT_INC(massign);
559
560 error = git_vector_insert_sorted(
561 assigns, massign, &merge_assignments);
562 if (error < 0 && error != GIT_EEXISTS)
563 return error;
564 }
565 }
566 }
567
568 /* insert allocated assign into vector */
569 error = git_vector_insert_sorted(assigns, assign, &merge_assignments);
570 if (error < 0 && error != GIT_EEXISTS)
571 return error;
572
573 /* clear assign since it is now "owned" by the vector */
574 assign = NULL;
575 }
576
577 if (assign != NULL)
578 git_attr_assignment__free(assign);
579
580 *base = git__next_line(scan);
581
582 return (assigns->length == 0) ? GIT_ENOTFOUND : 0;
583 }
584
585 static void git_attr_rule__clear(git_attr_rule *rule)
586 {
587 unsigned int i;
588 git_attr_assignment *assign;
589
590 if (!rule)
591 return;
592
593 if (!(rule->match.flags & GIT_ATTR_FNMATCH_IGNORE)) {
594 git_vector_foreach(&rule->assigns, i, assign)
595 GIT_REFCOUNT_DEC(assign, git_attr_assignment__free);
596 git_vector_free(&rule->assigns);
597 }
598
599 /* match.pattern is stored in a git_pool, so no need to free */
600 rule->match.pattern = NULL;
601 rule->match.length = 0;
602 }
603
604 void git_attr_rule__free(git_attr_rule *rule)
605 {
606 git_attr_rule__clear(rule);
607 git__free(rule);
608 }
609