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