1 /*
2 * Copyright (C) the libgit2 contributors. All rights reserved.
3 *
4 * This file is part of libgit2, distributed under the GNU GPL v2 with
5 * a Linking Exception. For full terms see the included COPYING file.
6 */
7
8 #include "attr.h"
9
10 #include "repository.h"
11 #include "sysdir.h"
12 #include "config.h"
13 #include "attr_file.h"
14 #include "ignore.h"
15 #include "git2/oid.h"
16 #include <ctype.h>
17
18 const char *git_attr__true = "[internal]__TRUE__";
19 const char *git_attr__false = "[internal]__FALSE__";
20 const char *git_attr__unset = "[internal]__UNSET__";
21
git_attr_value(const char * attr)22 git_attr_value_t git_attr_value(const char *attr)
23 {
24 if (attr == NULL || attr == git_attr__unset)
25 return GIT_ATTR_VALUE_UNSPECIFIED;
26
27 if (attr == git_attr__true)
28 return GIT_ATTR_VALUE_TRUE;
29
30 if (attr == git_attr__false)
31 return GIT_ATTR_VALUE_FALSE;
32
33 return GIT_ATTR_VALUE_STRING;
34 }
35
36 static int collect_attr_files(
37 git_repository *repo,
38 git_attr_session *attr_session,
39 git_attr_options *opts,
40 const char *path,
41 git_vector *files);
42
43 static void release_attr_files(git_vector *files);
44
git_attr_get_ext(const char ** value,git_repository * repo,git_attr_options * opts,const char * pathname,const char * name)45 int git_attr_get_ext(
46 const char **value,
47 git_repository *repo,
48 git_attr_options *opts,
49 const char *pathname,
50 const char *name)
51 {
52 int error;
53 git_attr_path path;
54 git_vector files = GIT_VECTOR_INIT;
55 size_t i, j;
56 git_attr_file *file;
57 git_attr_name attr;
58 git_attr_rule *rule;
59 git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
60
61 GIT_ASSERT_ARG(value);
62 GIT_ASSERT_ARG(repo);
63 GIT_ASSERT_ARG(name);
64 GIT_ERROR_CHECK_VERSION(opts, GIT_ATTR_OPTIONS_VERSION, "git_attr_options");
65
66 *value = NULL;
67
68 if (git_repository_is_bare(repo))
69 dir_flag = GIT_DIR_FLAG_FALSE;
70
71 if (git_attr_path__init(&path, pathname, git_repository_workdir(repo), dir_flag) < 0)
72 return -1;
73
74 if ((error = collect_attr_files(repo, NULL, opts, pathname, &files)) < 0)
75 goto cleanup;
76
77 memset(&attr, 0, sizeof(attr));
78 attr.name = name;
79 attr.name_hash = git_attr_file__name_hash(name);
80
81 git_vector_foreach(&files, i, file) {
82
83 git_attr_file__foreach_matching_rule(file, &path, j, rule) {
84 size_t pos;
85
86 if (!git_vector_bsearch(&pos, &rule->assigns, &attr)) {
87 *value = ((git_attr_assignment *)git_vector_get(
88 &rule->assigns, pos))->value;
89 goto cleanup;
90 }
91 }
92 }
93
94 cleanup:
95 release_attr_files(&files);
96 git_attr_path__free(&path);
97
98 return error;
99 }
100
git_attr_get(const char ** value,git_repository * repo,uint32_t flags,const char * pathname,const char * name)101 int git_attr_get(
102 const char **value,
103 git_repository *repo,
104 uint32_t flags,
105 const char *pathname,
106 const char *name)
107 {
108 git_attr_options opts = GIT_ATTR_OPTIONS_INIT;
109
110 opts.flags = flags;
111
112 return git_attr_get_ext(value, repo, &opts, pathname, name);
113 }
114
115
116 typedef struct {
117 git_attr_name name;
118 git_attr_assignment *found;
119 } attr_get_many_info;
120
git_attr_get_many_with_session(const char ** values,git_repository * repo,git_attr_session * attr_session,git_attr_options * opts,const char * pathname,size_t num_attr,const char ** names)121 int git_attr_get_many_with_session(
122 const char **values,
123 git_repository *repo,
124 git_attr_session *attr_session,
125 git_attr_options *opts,
126 const char *pathname,
127 size_t num_attr,
128 const char **names)
129 {
130 int error;
131 git_attr_path path;
132 git_vector files = GIT_VECTOR_INIT;
133 size_t i, j, k;
134 git_attr_file *file;
135 git_attr_rule *rule;
136 attr_get_many_info *info = NULL;
137 size_t num_found = 0;
138 git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
139
140 if (!num_attr)
141 return 0;
142
143 GIT_ASSERT_ARG(values);
144 GIT_ASSERT_ARG(repo);
145 GIT_ASSERT_ARG(pathname);
146 GIT_ASSERT_ARG(names);
147 GIT_ERROR_CHECK_VERSION(opts, GIT_ATTR_OPTIONS_VERSION, "git_attr_options");
148
149 if (git_repository_is_bare(repo))
150 dir_flag = GIT_DIR_FLAG_FALSE;
151
152 if (git_attr_path__init(&path, pathname, git_repository_workdir(repo), dir_flag) < 0)
153 return -1;
154
155 if ((error = collect_attr_files(repo, attr_session, opts, pathname, &files)) < 0)
156 goto cleanup;
157
158 info = git__calloc(num_attr, sizeof(attr_get_many_info));
159 GIT_ERROR_CHECK_ALLOC(info);
160
161 git_vector_foreach(&files, i, file) {
162
163 git_attr_file__foreach_matching_rule(file, &path, j, rule) {
164
165 for (k = 0; k < num_attr; k++) {
166 size_t pos;
167
168 if (info[k].found != NULL) /* already found assignment */
169 continue;
170
171 if (!info[k].name.name) {
172 info[k].name.name = names[k];
173 info[k].name.name_hash = git_attr_file__name_hash(names[k]);
174 }
175
176 if (!git_vector_bsearch(&pos, &rule->assigns, &info[k].name)) {
177 info[k].found = (git_attr_assignment *)
178 git_vector_get(&rule->assigns, pos);
179 values[k] = info[k].found->value;
180
181 if (++num_found == num_attr)
182 goto cleanup;
183 }
184 }
185 }
186 }
187
188 for (k = 0; k < num_attr; k++) {
189 if (!info[k].found)
190 values[k] = NULL;
191 }
192
193 cleanup:
194 release_attr_files(&files);
195 git_attr_path__free(&path);
196 git__free(info);
197
198 return error;
199 }
200
git_attr_get_many(const char ** values,git_repository * repo,uint32_t flags,const char * pathname,size_t num_attr,const char ** names)201 int git_attr_get_many(
202 const char **values,
203 git_repository *repo,
204 uint32_t flags,
205 const char *pathname,
206 size_t num_attr,
207 const char **names)
208 {
209 git_attr_options opts = GIT_ATTR_OPTIONS_INIT;
210
211 opts.flags = flags;
212
213 return git_attr_get_many_with_session(
214 values, repo, NULL, &opts, pathname, num_attr, names);
215 }
216
git_attr_get_many_ext(const char ** values,git_repository * repo,git_attr_options * opts,const char * pathname,size_t num_attr,const char ** names)217 int git_attr_get_many_ext(
218 const char **values,
219 git_repository *repo,
220 git_attr_options *opts,
221 const char *pathname,
222 size_t num_attr,
223 const char **names)
224 {
225 return git_attr_get_many_with_session(
226 values, repo, NULL, opts, pathname, num_attr, names);
227 }
228
git_attr_foreach(git_repository * repo,uint32_t flags,const char * pathname,int (* callback)(const char * name,const char * value,void * payload),void * payload)229 int git_attr_foreach(
230 git_repository *repo,
231 uint32_t flags,
232 const char *pathname,
233 int (*callback)(const char *name, const char *value, void *payload),
234 void *payload)
235 {
236 git_attr_options opts = GIT_ATTR_OPTIONS_INIT;
237
238 opts.flags = flags;
239
240 return git_attr_foreach_ext(repo, &opts, pathname, callback, payload);
241 }
242
git_attr_foreach_ext(git_repository * repo,git_attr_options * opts,const char * pathname,int (* callback)(const char * name,const char * value,void * payload),void * payload)243 int git_attr_foreach_ext(
244 git_repository *repo,
245 git_attr_options *opts,
246 const char *pathname,
247 int (*callback)(const char *name, const char *value, void *payload),
248 void *payload)
249 {
250 int error;
251 git_attr_path path;
252 git_vector files = GIT_VECTOR_INIT;
253 size_t i, j, k;
254 git_attr_file *file;
255 git_attr_rule *rule;
256 git_attr_assignment *assign;
257 git_strmap *seen = NULL;
258 git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
259
260 GIT_ASSERT_ARG(repo);
261 GIT_ASSERT_ARG(callback);
262 GIT_ERROR_CHECK_VERSION(opts, GIT_ATTR_OPTIONS_VERSION, "git_attr_options");
263
264 if (git_repository_is_bare(repo))
265 dir_flag = GIT_DIR_FLAG_FALSE;
266
267 if (git_attr_path__init(&path, pathname, git_repository_workdir(repo), dir_flag) < 0)
268 return -1;
269
270 if ((error = collect_attr_files(repo, NULL, opts, pathname, &files)) < 0 ||
271 (error = git_strmap_new(&seen)) < 0)
272 goto cleanup;
273
274 git_vector_foreach(&files, i, file) {
275
276 git_attr_file__foreach_matching_rule(file, &path, j, rule) {
277
278 git_vector_foreach(&rule->assigns, k, assign) {
279 /* skip if higher priority assignment was already seen */
280 if (git_strmap_exists(seen, assign->name))
281 continue;
282
283 if ((error = git_strmap_set(seen, assign->name, assign)) < 0)
284 goto cleanup;
285
286 error = callback(assign->name, assign->value, payload);
287 if (error) {
288 git_error_set_after_callback(error);
289 goto cleanup;
290 }
291 }
292 }
293 }
294
295 cleanup:
296 git_strmap_free(seen);
297 release_attr_files(&files);
298 git_attr_path__free(&path);
299
300 return error;
301 }
302
preload_attr_source(git_repository * repo,git_attr_session * attr_session,git_attr_file_source * source)303 static int preload_attr_source(
304 git_repository *repo,
305 git_attr_session *attr_session,
306 git_attr_file_source *source)
307 {
308 int error;
309 git_attr_file *preload = NULL;
310
311 if (!source)
312 return 0;
313
314 error = git_attr_cache__get(&preload, repo, attr_session, source,
315 git_attr_file__parse_buffer, true);
316
317 if (!error)
318 git_attr_file__free(preload);
319
320 return error;
321 }
322
preload_attr_file(git_repository * repo,git_attr_session * attr_session,const char * base,const char * filename)323 GIT_INLINE(int) preload_attr_file(
324 git_repository *repo,
325 git_attr_session *attr_session,
326 const char *base,
327 const char *filename)
328 {
329 git_attr_file_source source = { GIT_ATTR_FILE_SOURCE_FILE };
330
331 if (!filename)
332 return 0;
333
334 source.base = base;
335 source.filename = filename;
336
337 return preload_attr_source(repo, attr_session, &source);
338 }
339
system_attr_file(git_buf * out,git_attr_session * attr_session)340 static int system_attr_file(
341 git_buf *out,
342 git_attr_session *attr_session)
343 {
344 int error;
345
346 if (!attr_session) {
347 error = git_sysdir_find_system_file(out, GIT_ATTR_FILE_SYSTEM);
348
349 if (error == GIT_ENOTFOUND)
350 git_error_clear();
351
352 return error;
353 }
354
355 if (!attr_session->init_sysdir) {
356 error = git_sysdir_find_system_file(&attr_session->sysdir, GIT_ATTR_FILE_SYSTEM);
357
358 if (error == GIT_ENOTFOUND)
359 git_error_clear();
360 else if (error)
361 return error;
362
363 attr_session->init_sysdir = 1;
364 }
365
366 if (attr_session->sysdir.size == 0)
367 return GIT_ENOTFOUND;
368
369 /* We can safely provide a git_buf with no allocation (asize == 0) to
370 * a consumer. This allows them to treat this as a regular `git_buf`,
371 * but their call to `git_buf_dispose` will not attempt to free it.
372 */
373 git_buf_attach_notowned(
374 out, attr_session->sysdir.ptr, attr_session->sysdir.size);
375 return 0;
376 }
377
attr_setup(git_repository * repo,git_attr_session * attr_session,git_attr_options * opts)378 static int attr_setup(
379 git_repository *repo,
380 git_attr_session *attr_session,
381 git_attr_options *opts)
382 {
383 git_buf system = GIT_BUF_INIT, info = GIT_BUF_INIT;
384 git_attr_file_source index_source = { GIT_ATTR_FILE_SOURCE_INDEX, NULL, GIT_ATTR_FILE, NULL };
385 git_attr_file_source head_source = { GIT_ATTR_FILE_SOURCE_HEAD, NULL, GIT_ATTR_FILE, NULL };
386 git_attr_file_source commit_source = { GIT_ATTR_FILE_SOURCE_COMMIT, NULL, GIT_ATTR_FILE, NULL };
387 git_index *idx = NULL;
388 const char *workdir;
389 int error = 0;
390
391 if (attr_session && attr_session->init_setup)
392 return 0;
393
394 if ((error = git_attr_cache__init(repo)) < 0)
395 return error;
396
397 /*
398 * Preload attribute files that could contain macros so the
399 * definitions will be available for later file parsing.
400 */
401
402 if ((error = system_attr_file(&system, attr_session)) < 0 ||
403 (error = preload_attr_file(repo, attr_session, NULL, system.ptr)) < 0) {
404 if (error != GIT_ENOTFOUND)
405 goto out;
406
407 error = 0;
408 }
409
410 if ((error = preload_attr_file(repo, attr_session, NULL,
411 git_repository_attr_cache(repo)->cfg_attr_file)) < 0)
412 goto out;
413
414 if ((error = git_repository_item_path(&info, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 ||
415 (error = preload_attr_file(repo, attr_session, info.ptr, GIT_ATTR_FILE_INREPO)) < 0) {
416 if (error != GIT_ENOTFOUND)
417 goto out;
418
419 error = 0;
420 }
421
422 if ((workdir = git_repository_workdir(repo)) != NULL &&
423 (error = preload_attr_file(repo, attr_session, workdir, GIT_ATTR_FILE)) < 0)
424 goto out;
425
426 if ((error = git_repository_index__weakptr(&idx, repo)) < 0 ||
427 (error = preload_attr_source(repo, attr_session, &index_source)) < 0)
428 goto out;
429
430 if ((opts && (opts->flags & GIT_ATTR_CHECK_INCLUDE_HEAD) != 0) &&
431 (error = preload_attr_source(repo, attr_session, &head_source)) < 0)
432 goto out;
433
434 if ((opts && (opts->flags & GIT_ATTR_CHECK_INCLUDE_COMMIT) != 0)) {
435 #ifndef GIT_DEPRECATE_HARD
436 if (opts->commit_id)
437 commit_source.commit_id = opts->commit_id;
438 else
439 #endif
440 commit_source.commit_id = &opts->attr_commit_id;
441
442 if ((error = preload_attr_source(repo, attr_session, &commit_source)) < 0)
443 goto out;
444 }
445
446 if (attr_session)
447 attr_session->init_setup = 1;
448
449 out:
450 git_buf_dispose(&system);
451 git_buf_dispose(&info);
452
453 return error;
454 }
455
git_attr_add_macro(git_repository * repo,const char * name,const char * values)456 int git_attr_add_macro(
457 git_repository *repo,
458 const char *name,
459 const char *values)
460 {
461 int error;
462 git_attr_rule *macro = NULL;
463 git_pool *pool;
464
465 GIT_ASSERT_ARG(repo);
466 GIT_ASSERT_ARG(name);
467
468 if ((error = git_attr_cache__init(repo)) < 0)
469 return error;
470
471 macro = git__calloc(1, sizeof(git_attr_rule));
472 GIT_ERROR_CHECK_ALLOC(macro);
473
474 pool = &git_repository_attr_cache(repo)->pool;
475
476 macro->match.pattern = git_pool_strdup(pool, name);
477 GIT_ERROR_CHECK_ALLOC(macro->match.pattern);
478
479 macro->match.length = strlen(macro->match.pattern);
480 macro->match.flags = GIT_ATTR_FNMATCH_MACRO;
481
482 error = git_attr_assignment__parse(repo, pool, ¯o->assigns, &values);
483
484 if (!error)
485 error = git_attr_cache__insert_macro(repo, macro);
486
487 if (error < 0)
488 git_attr_rule__free(macro);
489
490 return error;
491 }
492
493 typedef struct {
494 git_repository *repo;
495 git_attr_session *attr_session;
496 git_attr_options *opts;
497 const char *workdir;
498 git_index *index;
499 git_vector *files;
500 } attr_walk_up_info;
501
attr_decide_sources(uint32_t flags,bool has_wd,bool has_index,git_attr_file_source_t * srcs)502 static int attr_decide_sources(
503 uint32_t flags,
504 bool has_wd,
505 bool has_index,
506 git_attr_file_source_t *srcs)
507 {
508 int count = 0;
509
510 switch (flags & 0x03) {
511 case GIT_ATTR_CHECK_FILE_THEN_INDEX:
512 if (has_wd)
513 srcs[count++] = GIT_ATTR_FILE_SOURCE_FILE;
514 if (has_index)
515 srcs[count++] = GIT_ATTR_FILE_SOURCE_INDEX;
516 break;
517 case GIT_ATTR_CHECK_INDEX_THEN_FILE:
518 if (has_index)
519 srcs[count++] = GIT_ATTR_FILE_SOURCE_INDEX;
520 if (has_wd)
521 srcs[count++] = GIT_ATTR_FILE_SOURCE_FILE;
522 break;
523 case GIT_ATTR_CHECK_INDEX_ONLY:
524 if (has_index)
525 srcs[count++] = GIT_ATTR_FILE_SOURCE_INDEX;
526 break;
527 }
528
529 if ((flags & GIT_ATTR_CHECK_INCLUDE_HEAD) != 0)
530 srcs[count++] = GIT_ATTR_FILE_SOURCE_HEAD;
531
532 if ((flags & GIT_ATTR_CHECK_INCLUDE_COMMIT) != 0)
533 srcs[count++] = GIT_ATTR_FILE_SOURCE_COMMIT;
534
535 return count;
536 }
537
push_attr_source(git_repository * repo,git_attr_session * attr_session,git_vector * list,git_attr_file_source * source,bool allow_macros)538 static int push_attr_source(
539 git_repository *repo,
540 git_attr_session *attr_session,
541 git_vector *list,
542 git_attr_file_source *source,
543 bool allow_macros)
544 {
545 int error = 0;
546 git_attr_file *file = NULL;
547
548 error = git_attr_cache__get(&file, repo, attr_session,
549 source,
550 git_attr_file__parse_buffer,
551 allow_macros);
552
553 if (error < 0)
554 return error;
555
556 if (file != NULL) {
557 if ((error = git_vector_insert(list, file)) < 0)
558 git_attr_file__free(file);
559 }
560
561 return error;
562 }
563
push_attr_file(git_repository * repo,git_attr_session * attr_session,git_vector * list,const char * base,const char * filename)564 GIT_INLINE(int) push_attr_file(
565 git_repository *repo,
566 git_attr_session *attr_session,
567 git_vector *list,
568 const char *base,
569 const char *filename)
570 {
571 git_attr_file_source source = { GIT_ATTR_FILE_SOURCE_FILE, base, filename };
572 return push_attr_source(repo, attr_session, list, &source, true);
573 }
574
push_one_attr(void * ref,const char * path)575 static int push_one_attr(void *ref, const char *path)
576 {
577 attr_walk_up_info *info = (attr_walk_up_info *)ref;
578 git_attr_file_source_t src[GIT_ATTR_FILE_NUM_SOURCES];
579 int error = 0, n_src, i;
580 bool allow_macros;
581
582 n_src = attr_decide_sources(info->opts ? info->opts->flags : 0,
583 info->workdir != NULL,
584 info->index != NULL,
585 src);
586
587 allow_macros = info->workdir ? !strcmp(info->workdir, path) : false;
588
589 for (i = 0; !error && i < n_src; ++i) {
590 git_attr_file_source source = { src[i], path, GIT_ATTR_FILE };
591
592 if (src[i] == GIT_ATTR_FILE_SOURCE_COMMIT && info->opts) {
593 #ifndef GIT_DEPRECATE_HARD
594 if (info->opts->commit_id)
595 source.commit_id = info->opts->commit_id;
596 else
597 #endif
598 source.commit_id = &info->opts->attr_commit_id;
599 }
600
601 error = push_attr_source(info->repo, info->attr_session, info->files,
602 &source, allow_macros);
603 }
604
605 return error;
606 }
607
release_attr_files(git_vector * files)608 static void release_attr_files(git_vector *files)
609 {
610 size_t i;
611 git_attr_file *file;
612
613 git_vector_foreach(files, i, file) {
614 git_attr_file__free(file);
615 files->contents[i] = NULL;
616 }
617 git_vector_free(files);
618 }
619
collect_attr_files(git_repository * repo,git_attr_session * attr_session,git_attr_options * opts,const char * path,git_vector * files)620 static int collect_attr_files(
621 git_repository *repo,
622 git_attr_session *attr_session,
623 git_attr_options *opts,
624 const char *path,
625 git_vector *files)
626 {
627 int error = 0;
628 git_buf dir = GIT_BUF_INIT, attrfile = GIT_BUF_INIT;
629 const char *workdir = git_repository_workdir(repo);
630 attr_walk_up_info info = { NULL };
631
632 GIT_ASSERT(!git_path_is_absolute(path));
633
634 if ((error = attr_setup(repo, attr_session, opts)) < 0)
635 return error;
636
637 /* Resolve path in a non-bare repo */
638 if (workdir != NULL) {
639 if (!(error = git_repository_workdir_path(&dir, repo, path)))
640 error = git_path_find_dir(&dir);
641 }
642 else {
643 error = git_path_dirname_r(&dir, path);
644 }
645
646 if (error < 0)
647 goto cleanup;
648
649 /* in precendence order highest to lowest:
650 * - $GIT_DIR/info/attributes
651 * - path components with .gitattributes
652 * - config core.attributesfile
653 * - $GIT_PREFIX/etc/gitattributes
654 */
655
656 if ((error = git_repository_item_path(&attrfile, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 ||
657 (error = push_attr_file(repo, attr_session, files, attrfile.ptr, GIT_ATTR_FILE_INREPO)) < 0) {
658 if (error != GIT_ENOTFOUND)
659 goto cleanup;
660 }
661
662 info.repo = repo;
663 info.attr_session = attr_session;
664 info.opts = opts;
665 info.workdir = workdir;
666 if (git_repository_index__weakptr(&info.index, repo) < 0)
667 git_error_clear(); /* no error even if there is no index */
668 info.files = files;
669
670 if (!strcmp(dir.ptr, "."))
671 error = push_one_attr(&info, "");
672 else
673 error = git_path_walk_up(&dir, workdir, push_one_attr, &info);
674
675 if (error < 0)
676 goto cleanup;
677
678 if (git_repository_attr_cache(repo)->cfg_attr_file != NULL) {
679 error = push_attr_file(repo, attr_session, files, NULL, git_repository_attr_cache(repo)->cfg_attr_file);
680 if (error < 0)
681 goto cleanup;
682 }
683
684 if (!opts || (opts->flags & GIT_ATTR_CHECK_NO_SYSTEM) == 0) {
685 error = system_attr_file(&dir, attr_session);
686
687 if (!error)
688 error = push_attr_file(repo, attr_session, files, NULL, dir.ptr);
689 else if (error == GIT_ENOTFOUND)
690 error = 0;
691 }
692
693 cleanup:
694 if (error < 0)
695 release_attr_files(files);
696 git_buf_dispose(&attrfile);
697 git_buf_dispose(&dir);
698
699 return error;
700 }
701