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