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