File Coverage

deps/libgit2/src/libgit2/ignore.c
Criterion Covered Total %
statement 165 298 55.3
branch 91 220 41.3
condition n/a
subroutine n/a
pod n/a
total 256 518 49.4


line stmt bran cond sub pod time code
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 "ignore.h"
9              
10             #include "git2/ignore.h"
11             #include "common.h"
12             #include "attrcache.h"
13             #include "fs_path.h"
14             #include "config.h"
15             #include "wildmatch.h"
16             #include "path.h"
17              
18             #define GIT_IGNORE_INTERNAL "[internal]exclude"
19              
20             #define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n"
21              
22             /**
23             * A negative ignore pattern can negate a positive one without
24             * wildcards if it is a basename only and equals the basename of
25             * the positive pattern. Thus
26             *
27             * foo/bar
28             * !bar
29             *
30             * would result in foo/bar being unignored again while
31             *
32             * moo/foo/bar
33             * !foo/bar
34             *
35             * would do nothing. The reverse also holds true: a positive
36             * basename pattern can be negated by unignoring the basename in
37             * subdirectories. Thus
38             *
39             * bar
40             * !foo/bar
41             *
42             * would result in foo/bar being unignored again. As with the
43             * first case,
44             *
45             * foo/bar
46             * !moo/foo/bar
47             *
48             * would do nothing, again.
49             */
50 0           static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
51             {
52             int (*cmp)(const char *, const char *, size_t);
53             git_attr_fnmatch *longer, *shorter;
54             char *p;
55              
56 0 0         if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0
57 0 0         || (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0)
58 0           return false;
59              
60 0 0         if (neg->flags & GIT_ATTR_FNMATCH_ICASE)
61 0           cmp = git__strncasecmp;
62             else
63 0           cmp = git__strncmp;
64              
65             /* If lengths match we need to have an exact match */
66 0 0         if (rule->length == neg->length) {
67 0           return cmp(rule->pattern, neg->pattern, rule->length) == 0;
68 0 0         } else if (rule->length < neg->length) {
69 0           shorter = rule;
70 0           longer = neg;
71             } else {
72 0           shorter = neg;
73 0           longer = rule;
74             }
75              
76             /* Otherwise, we need to check if the shorter
77             * rule is a basename only (that is, it contains
78             * no path separator) and, if so, if it
79             * matches the tail of the longer rule */
80 0           p = longer->pattern + longer->length - shorter->length;
81              
82 0 0         if (p[-1] != '/')
83 0           return false;
84 0 0         if (memchr(shorter->pattern, '/', shorter->length) != NULL)
85 0           return false;
86              
87 0           return cmp(p, shorter->pattern, shorter->length) == 0;
88             }
89              
90             /**
91             * A negative ignore can only unignore a file which is given explicitly before, thus
92             *
93             * foo
94             * !foo/bar
95             *
96             * does not unignore 'foo/bar' as it's not in the list. However
97             *
98             * foo/
99             * !foo/bar
100             *
101             * does unignore 'foo/bar', as it is contained within the 'foo/' rule.
102             */
103 0           static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match)
104             {
105 0           int error = 0, wildmatch_flags, effective_flags;
106             size_t i;
107             git_attr_fnmatch *rule;
108             char *path;
109 0           git_str buf = GIT_STR_INIT;
110              
111 0           *out = 0;
112              
113 0           wildmatch_flags = WM_PATHNAME;
114 0 0         if (match->flags & GIT_ATTR_FNMATCH_ICASE)
115 0           wildmatch_flags |= WM_CASEFOLD;
116              
117             /* path of the file relative to the workdir, so we match the rules in subdirs */
118 0 0         if (match->containing_dir) {
119 0           git_str_puts(&buf, match->containing_dir);
120             }
121 0 0         if (git_str_puts(&buf, match->pattern) < 0)
122 0           return -1;
123              
124 0           path = git_str_detach(&buf);
125              
126 0 0         git_vector_foreach(rules, i, rule) {
127 0 0         if (!(rule->flags & GIT_ATTR_FNMATCH_HASWILD)) {
128 0 0         if (does_negate_pattern(rule, match)) {
129 0           error = 0;
130 0           *out = 1;
131 0           goto out;
132             }
133             else
134 0           continue;
135             }
136              
137 0           git_str_clear(&buf);
138 0 0         if (rule->containing_dir)
139 0           git_str_puts(&buf, rule->containing_dir);
140 0           git_str_puts(&buf, rule->pattern);
141              
142 0 0         if (git_str_oom(&buf))
143 0           goto out;
144              
145             /*
146             * if rule isn't for full path we match without PATHNAME flag
147             * as lines like *.txt should match something like dir/test.txt
148             * requiring * to also match /
149             */
150 0           effective_flags = wildmatch_flags;
151 0 0         if (!(rule->flags & GIT_ATTR_FNMATCH_FULLPATH))
152 0           effective_flags &= ~WM_PATHNAME;
153              
154             /* if we found a match, we want to keep this rule */
155 0 0         if ((wildmatch(git_str_cstr(&buf), path, effective_flags)) == WM_MATCH) {
156 0           *out = 1;
157 0           error = 0;
158 0           goto out;
159             }
160             }
161              
162 0           error = 0;
163              
164             out:
165 0           git__free(path);
166 0           git_str_dispose(&buf);
167 0           return error;
168             }
169              
170 369           static int parse_ignore_file(
171             git_repository *repo, git_attr_file *attrs, const char *data, bool allow_macros)
172             {
173 369           int error = 0;
174 369           int ignore_case = false;
175 369           const char *scan = data, *context = NULL;
176 369           git_attr_fnmatch *match = NULL;
177              
178 369           GIT_UNUSED(allow_macros);
179              
180 369 50         if (git_repository__configmap_lookup(&ignore_case, repo, GIT_CONFIGMAP_IGNORECASE) < 0)
181 0           git_error_clear();
182              
183             /* if subdir file path, convert context for file paths */
184 738           if (attrs->entry &&
185 736 100         git_fs_path_root(attrs->entry->path) < 0 &&
186 367           !git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE))
187 176           context = attrs->entry->path;
188              
189 369 50         if (git_mutex_lock(&attrs->lock) < 0) {
190 0           git_error_set(GIT_ERROR_OS, "failed to lock ignore file");
191 0           return -1;
192             }
193              
194 441 50         while (!error && *scan) {
    100          
195 72           int valid_rule = 1;
196              
197 72 100         if (!match && !(match = git__calloc(1, sizeof(*match)))) {
    50          
198 0           error = -1;
199 0           break;
200             }
201              
202 72           match->flags =
203             GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
204              
205 72 100         if (!(error = git_attr_fnmatch__parse(
206             match, &attrs->pool, context, &scan)))
207             {
208 44           match->flags |= GIT_ATTR_FNMATCH_IGNORE;
209              
210 44 50         if (ignore_case)
211 0           match->flags |= GIT_ATTR_FNMATCH_ICASE;
212              
213 44           scan = git__next_line(scan);
214              
215             /*
216             * If a negative match doesn't actually do anything,
217             * throw it away. As we cannot always verify whether a
218             * rule containing wildcards negates another rule, we
219             * do not optimize away these rules, though.
220             * */
221 44 50         if (match->flags & GIT_ATTR_FNMATCH_NEGATIVE
222 0 0         && !(match->flags & GIT_ATTR_FNMATCH_HASWILD))
223 0           error = does_negate_rule(&valid_rule, &attrs->rules, match);
224              
225 44 50         if (!error && valid_rule)
    50          
226 44           error = git_vector_insert(&attrs->rules, match);
227             }
228              
229 72 100         if (error != 0 || !valid_rule) {
    50          
230 28           match->pattern = NULL;
231              
232 56 50         if (error == GIT_ENOTFOUND)
233 28           error = 0;
234             } else {
235 72           match = NULL; /* vector now "owns" the match */
236             }
237             }
238              
239 369           git_mutex_unlock(&attrs->lock);
240 369           git__free(match);
241              
242 369           return error;
243             }
244              
245 540           static int push_ignore_file(
246             git_ignores *ignores,
247             git_vector *which_list,
248             const char *base,
249             const char *filename)
250             {
251 540           git_attr_file_source source = { GIT_ATTR_FILE_SOURCE_FILE, base, filename };
252 540           git_attr_file *file = NULL;
253 540           int error = 0;
254              
255 540           error = git_attr_cache__get(&file, ignores->repo, NULL, &source, parse_ignore_file, false);
256              
257 540 50         if (error < 0)
258 0           return error;
259              
260 540 50         if (file != NULL) {
261 540 50         if ((error = git_vector_insert(which_list, file)) < 0)
262 0           git_attr_file__free(file);
263             }
264              
265 540           return error;
266             }
267              
268 182           static int push_one_ignore(void *payload, const char *path)
269             {
270 182           git_ignores *ign = payload;
271 182           ign->depth++;
272 182           return push_ignore_file(ign, &ign->ign_path, path, GIT_IGNORE_FILE);
273             }
274              
275 184           static int get_internal_ignores(git_attr_file **out, git_repository *repo)
276             {
277 184           git_attr_file_source source = { GIT_ATTR_FILE_SOURCE_MEMORY, NULL, GIT_IGNORE_INTERNAL };
278             int error;
279              
280 184 50         if ((error = git_attr_cache__init(repo)) < 0)
281 0           return error;
282              
283 184           error = git_attr_cache__get(out, repo, NULL, &source, NULL, false);
284              
285             /* if internal rules list is empty, insert default rules */
286 184 50         if (!error && !(*out)->rules.length)
    100          
287 14           error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES, false);
288              
289 184           return error;
290             }
291              
292 182           int git_ignore__for_path(
293             git_repository *repo,
294             const char *path,
295             git_ignores *ignores)
296             {
297 182           int error = 0;
298 182           const char *workdir = git_repository_workdir(repo);
299 182           git_str infopath = GIT_STR_INIT;
300              
301 182 50         GIT_ASSERT_ARG(repo);
302 182 50         GIT_ASSERT_ARG(ignores);
303 182 50         GIT_ASSERT_ARG(path);
304              
305 182           memset(ignores, 0, sizeof(*ignores));
306 182           ignores->repo = repo;
307              
308             /* Read the ignore_case flag */
309 182 50         if ((error = git_repository__configmap_lookup(
310             &ignores->ignore_case, repo, GIT_CONFIGMAP_IGNORECASE)) < 0)
311 0           goto cleanup;
312              
313 182 50         if ((error = git_attr_cache__init(repo)) < 0)
314 0           goto cleanup;
315              
316             /* given a unrooted path in a non-bare repo, resolve it */
317 364 50         if (workdir && git_fs_path_root(path) < 0) {
    50          
318 182           git_str local = GIT_STR_INIT;
319              
320 182 50         if ((error = git_fs_path_dirname_r(&local, path)) < 0 ||
    50          
321 182 50         (error = git_fs_path_resolve_relative(&local, 0)) < 0 ||
322 182 50         (error = git_fs_path_to_dir(&local)) < 0 ||
323 182           (error = git_str_joinpath(&ignores->dir, workdir, local.ptr)) < 0 ||
324 182           (error = git_path_validate_str_length(repo, &ignores->dir)) < 0) {
325             /* Nothing, we just want to stop on the first error */
326             }
327              
328 182           git_str_dispose(&local);
329             } else {
330 0 0         if (!(error = git_str_joinpath(&ignores->dir, path, "")))
331 0           error = git_path_validate_str_length(NULL, &ignores->dir);
332             }
333              
334 182 50         if (error < 0)
335 0           goto cleanup;
336              
337 182 50         if (workdir && !git__prefixcmp(ignores->dir.ptr, workdir))
    50          
338 182           ignores->dir_root = strlen(workdir);
339              
340             /* set up internals */
341 182 50         if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0)
342 0           goto cleanup;
343              
344             /* load .gitignore up the path */
345 182 50         if (workdir != NULL) {
346 182           error = git_fs_path_walk_up(
347             &ignores->dir, workdir, push_one_ignore, ignores);
348 182 50         if (error < 0)
349 0           goto cleanup;
350             }
351              
352             /* load .git/info/exclude if possible */
353 182 50         if ((error = git_repository__item_path(&infopath, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 ||
    50          
354 182           (error = push_ignore_file(ignores, &ignores->ign_global, infopath.ptr, GIT_IGNORE_FILE_INREPO)) < 0) {
355 0 0         if (error != GIT_ENOTFOUND)
356 0           goto cleanup;
357 0           error = 0;
358             }
359              
360             /* load core.excludesfile */
361 182 50         if (git_repository_attr_cache(repo)->cfg_excl_file != NULL)
362 0           error = push_ignore_file(
363             ignores, &ignores->ign_global, NULL,
364 0           git_repository_attr_cache(repo)->cfg_excl_file);
365              
366             cleanup:
367 182           git_str_dispose(&infopath);
368 182 50         if (error < 0)
369 0           git_ignore__free(ignores);
370              
371 182           return error;
372             }
373              
374 176           int git_ignore__push_dir(git_ignores *ign, const char *dir)
375             {
376 176 50         if (git_str_joinpath(&ign->dir, ign->dir.ptr, dir) < 0)
377 0           return -1;
378              
379 176           ign->depth++;
380              
381 176           return push_ignore_file(
382 176           ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE);
383             }
384              
385 355           int git_ignore__pop_dir(git_ignores *ign)
386             {
387 355 50         if (ign->ign_path.length > 0) {
388 355           git_attr_file *file = git_vector_last(&ign->ign_path);
389 355           const char *start = file->entry->path, *end;
390              
391             /* - ign->dir looks something like "/home/user/a/b/" (or "a/b/c/d/")
392             * - file->path looks something like "a/b/.gitignore
393             *
394             * We are popping the last directory off ign->dir. We also want
395             * to remove the file from the vector if the popped directory
396             * matches the ignore path. We need to test if the "a/b" part of
397             * the file key matches the path we are about to pop.
398             */
399              
400 355 100         if ((end = strrchr(start, '/')) != NULL) {
401 176           size_t dirlen = (end - start) + 1;
402 176           const char *relpath = ign->dir.ptr + ign->dir_root;
403 176           size_t pathlen = ign->dir.size - ign->dir_root;
404              
405 176 50         if (pathlen == dirlen && !memcmp(relpath, start, dirlen)) {
    50          
406 176           git_vector_pop(&ign->ign_path);
407 176           git_attr_file__free(file);
408             }
409             }
410             }
411              
412 355 100         if (--ign->depth > 0) {
413 176           git_str_rtruncate_at_char(&ign->dir, '/');
414 176           git_fs_path_to_dir(&ign->dir);
415             }
416              
417 355           return 0;
418             }
419              
420 213           void git_ignore__free(git_ignores *ignores)
421             {
422             unsigned int i;
423             git_attr_file *file;
424              
425 213           git_attr_file__free(ignores->ign_internal);
426              
427 395 100         git_vector_foreach(&ignores->ign_path, i, file) {
428 182           git_attr_file__free(file);
429 182           ignores->ign_path.contents[i] = NULL;
430             }
431 213           git_vector_free(&ignores->ign_path);
432              
433 395 100         git_vector_foreach(&ignores->ign_global, i, file) {
434 182           git_attr_file__free(file);
435 182           ignores->ign_global.contents[i] = NULL;
436             }
437 213           git_vector_free(&ignores->ign_global);
438              
439 213           git_str_dispose(&ignores->dir);
440 213           }
441              
442 2677           static bool ignore_lookup_in_rules(
443             int *ignored, git_attr_file *file, git_attr_path *path)
444             {
445             size_t j;
446             git_attr_fnmatch *match;
447              
448 4996 100         git_vector_rforeach(&file->rules, j, match) {
449 2330 50         if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY &&
    0          
450 0           path->is_dir == GIT_DIR_FLAG_FALSE)
451 0           continue;
452 2330 100         if (git_attr_fnmatch__match(match, path)) {
453 22           *ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
454 11           GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;
455 11           return true;
456             }
457             }
458              
459 2666           return false;
460             }
461              
462 760           int git_ignore__lookup(
463             int *out, git_ignores *ignores, const char *pathname, git_dir_flag dir_flag)
464             {
465             size_t i;
466             git_attr_file *file;
467             git_attr_path path;
468              
469 760           *out = GIT_IGNORE_NOTFOUND;
470              
471 760 50         if (git_attr_path__init(
472 760           &path, pathname, git_repository_workdir(ignores->repo), dir_flag) < 0)
473 0           return -1;
474              
475             /* first process builtins - success means path was found */
476 760 100         if (ignore_lookup_in_rules(out, ignores->ign_internal, &path))
477 10           goto cleanup;
478              
479             /* next process files in the path.
480             * this process has to process ignores in reverse order
481             * to ensure correct prioritization of rules
482             */
483 1910 100         git_vector_rforeach(&ignores->ign_path, i, file) {
484 1160 50         if (ignore_lookup_in_rules(out, file, &path))
485 0           goto cleanup;
486             }
487              
488             /* last process global ignores */
489 1500 100         git_vector_foreach(&ignores->ign_global, i, file) {
490 750 50         if (ignore_lookup_in_rules(out, file, &path))
491 0           goto cleanup;
492             }
493              
494             cleanup:
495 760           git_attr_path__free(&path);
496 760           return 0;
497             }
498              
499 2           int git_ignore_add_rule(git_repository *repo, const char *rules)
500             {
501             int error;
502 2           git_attr_file *ign_internal = NULL;
503              
504 2 50         if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
505 0           return error;
506              
507 2           error = parse_ignore_file(repo, ign_internal, rules, false);
508 2           git_attr_file__free(ign_internal);
509              
510 2           return error;
511             }
512              
513 0           int git_ignore_clear_internal_rules(git_repository *repo)
514             {
515             int error;
516             git_attr_file *ign_internal;
517              
518 0 0         if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
519 0           return error;
520              
521 0 0         if (!(error = git_attr_file__clear_rules(ign_internal, true)))
522 0           error = parse_ignore_file(
523             repo, ign_internal, GIT_IGNORE_DEFAULT_RULES, false);
524              
525 0           git_attr_file__free(ign_internal);
526 0           return error;
527             }
528              
529 3           int git_ignore_path_is_ignored(
530             int *ignored,
531             git_repository *repo,
532             const char *pathname)
533             {
534             int error;
535             const char *workdir;
536             git_attr_path path;
537             git_ignores ignores;
538             unsigned int i;
539             git_attr_file *file;
540 3           git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
541              
542 3 50         GIT_ASSERT_ARG(repo);
543 3 50         GIT_ASSERT_ARG(ignored);
544 3 50         GIT_ASSERT_ARG(pathname);
545              
546 3           workdir = git_repository_workdir(repo);
547              
548 3           memset(&path, 0, sizeof(path));
549 3           memset(&ignores, 0, sizeof(ignores));
550              
551 3 50         if (!git__suffixcmp(pathname, "/"))
552 0           dir_flag = GIT_DIR_FLAG_TRUE;
553 3 50         else if (git_repository_is_bare(repo))
554 0           dir_flag = GIT_DIR_FLAG_FALSE;
555              
556 3 50         if ((error = git_attr_path__init(&path, pathname, workdir, dir_flag)) < 0 ||
    50          
557 3           (error = git_ignore__for_path(repo, path.path, &ignores)) < 0)
558             goto cleanup;
559              
560             while (1) {
561             /* first process builtins - success means path was found */
562 3 100         if (ignore_lookup_in_rules(ignored, ignores.ign_internal, &path))
563 1           goto cleanup;
564              
565             /* next process files in the path */
566 4 100         git_vector_foreach(&ignores.ign_path, i, file) {
567 2 50         if (ignore_lookup_in_rules(ignored, file, &path))
568 0           goto cleanup;
569             }
570              
571             /* last process global ignores */
572 4 100         git_vector_foreach(&ignores.ign_global, i, file) {
573 2 50         if (ignore_lookup_in_rules(ignored, file, &path))
574 0           goto cleanup;
575             }
576              
577             /* move up one directory */
578 2 50         if (path.basename == path.path)
579 2           break;
580 0           path.basename[-1] = '\0';
581 0 0         while (path.basename > path.path && *path.basename != '/')
    0          
582 0           path.basename--;
583 0 0         if (path.basename > path.path)
584 0           path.basename++;
585 0           path.is_dir = 1;
586              
587 0 0         if ((error = git_ignore__pop_dir(&ignores)) < 0)
588 0           break;
589 0           }
590              
591 2           *ignored = 0;
592              
593             cleanup:
594 3           git_attr_path__free(&path);
595 3           git_ignore__free(&ignores);
596 3           return error;
597             }
598              
599 0           int git_ignore__check_pathspec_for_exact_ignores(
600             git_repository *repo,
601             git_vector *vspec,
602             bool no_fnmatch)
603             {
604 0           int error = 0;
605             size_t i;
606             git_attr_fnmatch *match;
607             int ignored;
608 0           git_str path = GIT_STR_INIT;
609             const char *filename;
610             git_index *idx;
611              
612 0 0         if ((error = git_repository__ensure_not_bare(
613 0 0         repo, "validate pathspec")) < 0 ||
614             (error = git_repository_index(&idx, repo)) < 0)
615 0           return error;
616              
617 0 0         git_vector_foreach(vspec, i, match) {
618             /* skip wildcard matches (if they are being used) */
619 0 0         if ((match->flags & GIT_ATTR_FNMATCH_HASWILD) != 0 &&
    0          
620 0           !no_fnmatch)
621 0           continue;
622              
623 0           filename = match->pattern;
624              
625             /* if file is already in the index, it's fine */
626 0 0         if (git_index_get_bypath(idx, filename, 0) != NULL)
627 0           continue;
628              
629 0 0         if ((error = git_repository_workdir_path(&path, repo, filename)) < 0)
630 0           break;
631              
632             /* is there a file on disk that matches this exactly? */
633 0 0         if (!git_fs_path_isfile(path.ptr))
634 0           continue;
635              
636             /* is that file ignored? */
637 0 0         if ((error = git_ignore_path_is_ignored(&ignored, repo, filename)) < 0)
638 0           break;
639              
640 0 0         if (ignored) {
641 0           git_error_set(GIT_ERROR_INVALID, "pathspec contains ignored file '%s'",
642             filename);
643 0           error = GIT_EINVALIDSPEC;
644 0           break;
645             }
646             }
647              
648 0           git_index_free(idx);
649 0           git_str_dispose(&path);
650              
651 0           return error;
652             }