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