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;
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 		/* if we found a match, we want to keep this rule */
145 		if ((wildmatch(git_buf_cstr(&buf), path, wildmatch_flags)) == WM_MATCH) {
146 			*out = 1;
147 			error = 0;
148 			goto out;
149 		}
150 	}
151 
152 	error = 0;
153 
154 out:
155 	git__free(path);
156 	git_buf_dispose(&buf);
157 	return error;
158 }
159 
parse_ignore_file(git_repository * repo,git_attr_file * attrs,const char * data,bool allow_macros)160 static int parse_ignore_file(
161 	git_repository *repo, git_attr_file *attrs, const char *data, bool allow_macros)
162 {
163 	int error = 0;
164 	int ignore_case = false;
165 	const char *scan = data, *context = NULL;
166 	git_attr_fnmatch *match = NULL;
167 
168 	GIT_UNUSED(allow_macros);
169 
170 	if (git_repository__configmap_lookup(&ignore_case, repo, GIT_CONFIGMAP_IGNORECASE) < 0)
171 		git_error_clear();
172 
173 	/* if subdir file path, convert context for file paths */
174 	if (attrs->entry &&
175 		git_path_root(attrs->entry->path) < 0 &&
176 		!git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE))
177 		context = attrs->entry->path;
178 
179 	if (git_mutex_lock(&attrs->lock) < 0) {
180 		git_error_set(GIT_ERROR_OS, "failed to lock ignore file");
181 		return -1;
182 	}
183 
184 	while (!error && *scan) {
185 		int valid_rule = 1;
186 
187 		if (!match && !(match = git__calloc(1, sizeof(*match)))) {
188 			error = -1;
189 			break;
190 		}
191 
192 		match->flags =
193 		    GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
194 
195 		if (!(error = git_attr_fnmatch__parse(
196 			match, &attrs->pool, context, &scan)))
197 		{
198 			match->flags |= GIT_ATTR_FNMATCH_IGNORE;
199 
200 			if (ignore_case)
201 				match->flags |= GIT_ATTR_FNMATCH_ICASE;
202 
203 			scan = git__next_line(scan);
204 
205 			/*
206 			 * If a negative match doesn't actually do anything,
207 			 * throw it away. As we cannot always verify whether a
208 			 * rule containing wildcards negates another rule, we
209 			 * do not optimize away these rules, though.
210 			 * */
211 			if (match->flags & GIT_ATTR_FNMATCH_NEGATIVE
212 			    && !(match->flags & GIT_ATTR_FNMATCH_HASWILD))
213 				error = does_negate_rule(&valid_rule, &attrs->rules, match);
214 
215 			if (!error && valid_rule)
216 				error = git_vector_insert(&attrs->rules, match);
217 		}
218 
219 		if (error != 0 || !valid_rule) {
220 			match->pattern = NULL;
221 
222 			if (error == GIT_ENOTFOUND)
223 				error = 0;
224 		} else {
225 			match = NULL; /* vector now "owns" the match */
226 		}
227 	}
228 
229 	git_mutex_unlock(&attrs->lock);
230 	git__free(match);
231 
232 	return error;
233 }
234 
push_ignore_file(git_ignores * ignores,git_vector * which_list,const char * base,const char * filename)235 static int push_ignore_file(
236 	git_ignores *ignores,
237 	git_vector *which_list,
238 	const char *base,
239 	const char *filename)
240 {
241 	int error = 0;
242 	git_attr_file *file = NULL;
243 
244 	error = git_attr_cache__get(&file, ignores->repo, NULL, GIT_ATTR_FILE__FROM_FILE,
245 				    base, filename, parse_ignore_file, false);
246 	if (error < 0)
247 		return error;
248 
249 	if (file != NULL) {
250 		if ((error = git_vector_insert(which_list, file)) < 0)
251 			git_attr_file__free(file);
252 	}
253 
254 	return error;
255 }
256 
push_one_ignore(void * payload,const char * path)257 static int push_one_ignore(void *payload, const char *path)
258 {
259 	git_ignores *ign = payload;
260 	ign->depth++;
261 	return push_ignore_file(ign, &ign->ign_path, path, GIT_IGNORE_FILE);
262 }
263 
get_internal_ignores(git_attr_file ** out,git_repository * repo)264 static int get_internal_ignores(git_attr_file **out, git_repository *repo)
265 {
266 	int error;
267 
268 	if ((error = git_attr_cache__init(repo)) < 0)
269 		return error;
270 
271 	error = git_attr_cache__get(out, repo, NULL, GIT_ATTR_FILE__IN_MEMORY, NULL,
272 				    GIT_IGNORE_INTERNAL, NULL, false);
273 
274 	/* if internal rules list is empty, insert default rules */
275 	if (!error && !(*out)->rules.length)
276 		error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES, false);
277 
278 	return error;
279 }
280 
git_ignore__for_path(git_repository * repo,const char * path,git_ignores * ignores)281 int git_ignore__for_path(
282 	git_repository *repo,
283 	const char *path,
284 	git_ignores *ignores)
285 {
286 	int error = 0;
287 	const char *workdir = git_repository_workdir(repo);
288 	git_buf infopath = GIT_BUF_INIT;
289 
290 	GIT_ASSERT_ARG(repo);
291 	GIT_ASSERT_ARG(ignores);
292 	GIT_ASSERT_ARG(path);
293 
294 	memset(ignores, 0, sizeof(*ignores));
295 	ignores->repo = repo;
296 
297 	/* Read the ignore_case flag */
298 	if ((error = git_repository__configmap_lookup(
299 			&ignores->ignore_case, repo, GIT_CONFIGMAP_IGNORECASE)) < 0)
300 		goto cleanup;
301 
302 	if ((error = git_attr_cache__init(repo)) < 0)
303 		goto cleanup;
304 
305 	/* given a unrooted path in a non-bare repo, resolve it */
306 	if (workdir && git_path_root(path) < 0) {
307 		git_buf local = GIT_BUF_INIT;
308 
309 		if ((error = git_path_dirname_r(&local, path)) < 0 ||
310 		    (error = git_path_resolve_relative(&local, 0)) < 0 ||
311 		    (error = git_path_to_dir(&local)) < 0 ||
312 		    (error = git_buf_joinpath(&ignores->dir, workdir, local.ptr)) < 0)
313 		{;} /* Nothing, we just want to stop on the first error */
314 		git_buf_dispose(&local);
315 	} else {
316 		error = git_buf_joinpath(&ignores->dir, path, "");
317 	}
318 	if (error < 0)
319 		goto cleanup;
320 
321 	if (workdir && !git__prefixcmp(ignores->dir.ptr, workdir))
322 		ignores->dir_root = strlen(workdir);
323 
324 	/* set up internals */
325 	if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0)
326 		goto cleanup;
327 
328 	/* load .gitignore up the path */
329 	if (workdir != NULL) {
330 		error = git_path_walk_up(
331 			&ignores->dir, workdir, push_one_ignore, ignores);
332 		if (error < 0)
333 			goto cleanup;
334 	}
335 
336 	/* load .git/info/exclude if possible */
337 	if ((error = git_repository_item_path(&infopath, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 ||
338 		(error = push_ignore_file(ignores, &ignores->ign_global, infopath.ptr, GIT_IGNORE_FILE_INREPO)) < 0) {
339 		if (error != GIT_ENOTFOUND)
340 			goto cleanup;
341 		error = 0;
342 	}
343 
344 	/* load core.excludesfile */
345 	if (git_repository_attr_cache(repo)->cfg_excl_file != NULL)
346 		error = push_ignore_file(
347 			ignores, &ignores->ign_global, NULL,
348 			git_repository_attr_cache(repo)->cfg_excl_file);
349 
350 cleanup:
351 	git_buf_dispose(&infopath);
352 	if (error < 0)
353 		git_ignore__free(ignores);
354 
355 	return error;
356 }
357 
git_ignore__push_dir(git_ignores * ign,const char * dir)358 int git_ignore__push_dir(git_ignores *ign, const char *dir)
359 {
360 	if (git_buf_joinpath(&ign->dir, ign->dir.ptr, dir) < 0)
361 		return -1;
362 
363 	ign->depth++;
364 
365 	return push_ignore_file(
366 		ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE);
367 }
368 
git_ignore__pop_dir(git_ignores * ign)369 int git_ignore__pop_dir(git_ignores *ign)
370 {
371 	if (ign->ign_path.length > 0) {
372 		git_attr_file *file = git_vector_last(&ign->ign_path);
373 		const char *start = file->entry->path, *end;
374 
375 		/* - ign->dir looks something like "/home/user/a/b/" (or "a/b/c/d/")
376 		 * - file->path looks something like "a/b/.gitignore
377 		 *
378 		 * We are popping the last directory off ign->dir.  We also want
379 		 * to remove the file from the vector if the popped directory
380 		 * matches the ignore path.  We need to test if the "a/b" part of
381 		 * the file key matches the path we are about to pop.
382 		 */
383 
384 		if ((end = strrchr(start, '/')) != NULL) {
385 			size_t dirlen = (end - start) + 1;
386 			const char *relpath = ign->dir.ptr + ign->dir_root;
387 			size_t pathlen = ign->dir.size - ign->dir_root;
388 
389 			if (pathlen == dirlen && !memcmp(relpath, start, dirlen)) {
390 				git_vector_pop(&ign->ign_path);
391 				git_attr_file__free(file);
392 			}
393 		}
394 	}
395 
396 	if (--ign->depth > 0) {
397 		git_buf_rtruncate_at_char(&ign->dir, '/');
398 		git_path_to_dir(&ign->dir);
399 	}
400 
401 	return 0;
402 }
403 
git_ignore__free(git_ignores * ignores)404 void git_ignore__free(git_ignores *ignores)
405 {
406 	unsigned int i;
407 	git_attr_file *file;
408 
409 	git_attr_file__free(ignores->ign_internal);
410 
411 	git_vector_foreach(&ignores->ign_path, i, file) {
412 		git_attr_file__free(file);
413 		ignores->ign_path.contents[i] = NULL;
414 	}
415 	git_vector_free(&ignores->ign_path);
416 
417 	git_vector_foreach(&ignores->ign_global, i, file) {
418 		git_attr_file__free(file);
419 		ignores->ign_global.contents[i] = NULL;
420 	}
421 	git_vector_free(&ignores->ign_global);
422 
423 	git_buf_dispose(&ignores->dir);
424 }
425 
ignore_lookup_in_rules(int * ignored,git_attr_file * file,git_attr_path * path)426 static bool ignore_lookup_in_rules(
427 	int *ignored, git_attr_file *file, git_attr_path *path)
428 {
429 	size_t j;
430 	git_attr_fnmatch *match;
431 
432 	git_vector_rforeach(&file->rules, j, match) {
433 		if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY &&
434 		    path->is_dir == GIT_DIR_FLAG_FALSE)
435 			continue;
436 		if (git_attr_fnmatch__match(match, path)) {
437 			*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
438 				GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;
439 			return true;
440 		}
441 	}
442 
443 	return false;
444 }
445 
git_ignore__lookup(int * out,git_ignores * ignores,const char * pathname,git_dir_flag dir_flag)446 int git_ignore__lookup(
447 	int *out, git_ignores *ignores, const char *pathname, git_dir_flag dir_flag)
448 {
449 	size_t i;
450 	git_attr_file *file;
451 	git_attr_path path;
452 
453 	*out = GIT_IGNORE_NOTFOUND;
454 
455 	if (git_attr_path__init(
456 		&path, pathname, git_repository_workdir(ignores->repo), dir_flag) < 0)
457 		return -1;
458 
459 	/* first process builtins - success means path was found */
460 	if (ignore_lookup_in_rules(out, ignores->ign_internal, &path))
461 		goto cleanup;
462 
463 	/* next process files in the path.
464 	 * this process has to process ignores in reverse order
465 	 * to ensure correct prioritization of rules
466 	 */
467 	git_vector_rforeach(&ignores->ign_path, i, file) {
468 		if (ignore_lookup_in_rules(out, file, &path))
469 			goto cleanup;
470 	}
471 
472 	/* last process global ignores */
473 	git_vector_foreach(&ignores->ign_global, i, file) {
474 		if (ignore_lookup_in_rules(out, file, &path))
475 			goto cleanup;
476 	}
477 
478 cleanup:
479 	git_attr_path__free(&path);
480 	return 0;
481 }
482 
git_ignore_add_rule(git_repository * repo,const char * rules)483 int git_ignore_add_rule(git_repository *repo, const char *rules)
484 {
485 	int error;
486 	git_attr_file *ign_internal = NULL;
487 
488 	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
489 		return error;
490 
491 	error = parse_ignore_file(repo, ign_internal, rules, false);
492 	git_attr_file__free(ign_internal);
493 
494 	return error;
495 }
496 
git_ignore_clear_internal_rules(git_repository * repo)497 int git_ignore_clear_internal_rules(git_repository *repo)
498 {
499 	int error;
500 	git_attr_file *ign_internal;
501 
502 	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
503 		return error;
504 
505 	if (!(error = git_attr_file__clear_rules(ign_internal, true)))
506 		error = parse_ignore_file(
507 				repo, ign_internal, GIT_IGNORE_DEFAULT_RULES, false);
508 
509 	git_attr_file__free(ign_internal);
510 	return error;
511 }
512 
git_ignore_path_is_ignored(int * ignored,git_repository * repo,const char * pathname)513 int git_ignore_path_is_ignored(
514 	int *ignored,
515 	git_repository *repo,
516 	const char *pathname)
517 {
518 	int error;
519 	const char *workdir;
520 	git_attr_path path;
521 	git_ignores ignores;
522 	unsigned int i;
523 	git_attr_file *file;
524 	git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
525 
526 	GIT_ASSERT_ARG(repo);
527 	GIT_ASSERT_ARG(ignored);
528 	GIT_ASSERT_ARG(pathname);
529 
530 	workdir = git_repository_workdir(repo);
531 
532 	memset(&path, 0, sizeof(path));
533 	memset(&ignores, 0, sizeof(ignores));
534 
535 	if (!git__suffixcmp(pathname, "/"))
536 		dir_flag = GIT_DIR_FLAG_TRUE;
537 	else if (git_repository_is_bare(repo))
538 		dir_flag = GIT_DIR_FLAG_FALSE;
539 
540 	if ((error = git_attr_path__init(&path, pathname, workdir, dir_flag)) < 0 ||
541 		(error = git_ignore__for_path(repo, path.path, &ignores)) < 0)
542 		goto cleanup;
543 
544 	while (1) {
545 		/* first process builtins - success means path was found */
546 		if (ignore_lookup_in_rules(ignored, ignores.ign_internal, &path))
547 			goto cleanup;
548 
549 		/* next process files in the path */
550 		git_vector_foreach(&ignores.ign_path, i, file) {
551 			if (ignore_lookup_in_rules(ignored, file, &path))
552 				goto cleanup;
553 		}
554 
555 		/* last process global ignores */
556 		git_vector_foreach(&ignores.ign_global, i, file) {
557 			if (ignore_lookup_in_rules(ignored, file, &path))
558 				goto cleanup;
559 		}
560 
561 		/* move up one directory */
562 		if (path.basename == path.path)
563 			break;
564 		path.basename[-1] = '\0';
565 		while (path.basename > path.path && *path.basename != '/')
566 			path.basename--;
567 		if (path.basename > path.path)
568 			path.basename++;
569 		path.is_dir = 1;
570 
571 		if ((error = git_ignore__pop_dir(&ignores)) < 0)
572 			break;
573 	}
574 
575 	*ignored = 0;
576 
577 cleanup:
578 	git_attr_path__free(&path);
579 	git_ignore__free(&ignores);
580 	return error;
581 }
582 
git_ignore__check_pathspec_for_exact_ignores(git_repository * repo,git_vector * vspec,bool no_fnmatch)583 int git_ignore__check_pathspec_for_exact_ignores(
584 	git_repository *repo,
585 	git_vector *vspec,
586 	bool no_fnmatch)
587 {
588 	int error = 0;
589 	size_t i;
590 	git_attr_fnmatch *match;
591 	int ignored;
592 	git_buf path = GIT_BUF_INIT;
593 	const char *wd, *filename;
594 	git_index *idx;
595 
596 	if ((error = git_repository__ensure_not_bare(
597 			repo, "validate pathspec")) < 0 ||
598 		(error = git_repository_index(&idx, repo)) < 0)
599 		return error;
600 
601 	wd = git_repository_workdir(repo);
602 
603 	git_vector_foreach(vspec, i, match) {
604 		/* skip wildcard matches (if they are being used) */
605 		if ((match->flags & GIT_ATTR_FNMATCH_HASWILD) != 0 &&
606 			!no_fnmatch)
607 			continue;
608 
609 		filename = match->pattern;
610 
611 		/* if file is already in the index, it's fine */
612 		if (git_index_get_bypath(idx, filename, 0) != NULL)
613 			continue;
614 
615 		if ((error = git_buf_joinpath(&path, wd, filename)) < 0)
616 			break;
617 
618 		/* is there a file on disk that matches this exactly? */
619 		if (!git_path_isfile(path.ptr))
620 			continue;
621 
622 		/* is that file ignored? */
623 		if ((error = git_ignore_path_is_ignored(&ignored, repo, filename)) < 0)
624 			break;
625 
626 		if (ignored) {
627 			git_error_set(GIT_ERROR_INVALID, "pathspec contains ignored file '%s'",
628 				filename);
629 			error = GIT_EINVALIDSPEC;
630 			break;
631 		}
632 	}
633 
634 	git_index_free(idx);
635 	git_buf_dispose(&path);
636 
637 	return error;
638 }
639 
640