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 		    (error = git_path_validate_workdir_buf(repo, &ignores->dir)) < 0) {
314 			/* Nothing, we just want to stop on the first error */
315 		}
316 
317 		git_buf_dispose(&local);
318 	} else {
319 		if (!(error = git_buf_joinpath(&ignores->dir, path, "")))
320 		    error = git_path_validate_filesystem(ignores->dir.ptr, ignores->dir.size);
321 	}
322 
323 	if (error < 0)
324 		goto cleanup;
325 
326 	if (workdir && !git__prefixcmp(ignores->dir.ptr, workdir))
327 		ignores->dir_root = strlen(workdir);
328 
329 	/* set up internals */
330 	if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0)
331 		goto cleanup;
332 
333 	/* load .gitignore up the path */
334 	if (workdir != NULL) {
335 		error = git_path_walk_up(
336 			&ignores->dir, workdir, push_one_ignore, ignores);
337 		if (error < 0)
338 			goto cleanup;
339 	}
340 
341 	/* load .git/info/exclude if possible */
342 	if ((error = git_repository_item_path(&infopath, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 ||
343 		(error = push_ignore_file(ignores, &ignores->ign_global, infopath.ptr, GIT_IGNORE_FILE_INREPO)) < 0) {
344 		if (error != GIT_ENOTFOUND)
345 			goto cleanup;
346 		error = 0;
347 	}
348 
349 	/* load core.excludesfile */
350 	if (git_repository_attr_cache(repo)->cfg_excl_file != NULL)
351 		error = push_ignore_file(
352 			ignores, &ignores->ign_global, NULL,
353 			git_repository_attr_cache(repo)->cfg_excl_file);
354 
355 cleanup:
356 	git_buf_dispose(&infopath);
357 	if (error < 0)
358 		git_ignore__free(ignores);
359 
360 	return error;
361 }
362 
git_ignore__push_dir(git_ignores * ign,const char * dir)363 int git_ignore__push_dir(git_ignores *ign, const char *dir)
364 {
365 	if (git_buf_joinpath(&ign->dir, ign->dir.ptr, dir) < 0)
366 		return -1;
367 
368 	ign->depth++;
369 
370 	return push_ignore_file(
371 		ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE);
372 }
373 
git_ignore__pop_dir(git_ignores * ign)374 int git_ignore__pop_dir(git_ignores *ign)
375 {
376 	if (ign->ign_path.length > 0) {
377 		git_attr_file *file = git_vector_last(&ign->ign_path);
378 		const char *start = file->entry->path, *end;
379 
380 		/* - ign->dir looks something like "/home/user/a/b/" (or "a/b/c/d/")
381 		 * - file->path looks something like "a/b/.gitignore
382 		 *
383 		 * We are popping the last directory off ign->dir.  We also want
384 		 * to remove the file from the vector if the popped directory
385 		 * matches the ignore path.  We need to test if the "a/b" part of
386 		 * the file key matches the path we are about to pop.
387 		 */
388 
389 		if ((end = strrchr(start, '/')) != NULL) {
390 			size_t dirlen = (end - start) + 1;
391 			const char *relpath = ign->dir.ptr + ign->dir_root;
392 			size_t pathlen = ign->dir.size - ign->dir_root;
393 
394 			if (pathlen == dirlen && !memcmp(relpath, start, dirlen)) {
395 				git_vector_pop(&ign->ign_path);
396 				git_attr_file__free(file);
397 			}
398 		}
399 	}
400 
401 	if (--ign->depth > 0) {
402 		git_buf_rtruncate_at_char(&ign->dir, '/');
403 		git_path_to_dir(&ign->dir);
404 	}
405 
406 	return 0;
407 }
408 
git_ignore__free(git_ignores * ignores)409 void git_ignore__free(git_ignores *ignores)
410 {
411 	unsigned int i;
412 	git_attr_file *file;
413 
414 	git_attr_file__free(ignores->ign_internal);
415 
416 	git_vector_foreach(&ignores->ign_path, i, file) {
417 		git_attr_file__free(file);
418 		ignores->ign_path.contents[i] = NULL;
419 	}
420 	git_vector_free(&ignores->ign_path);
421 
422 	git_vector_foreach(&ignores->ign_global, i, file) {
423 		git_attr_file__free(file);
424 		ignores->ign_global.contents[i] = NULL;
425 	}
426 	git_vector_free(&ignores->ign_global);
427 
428 	git_buf_dispose(&ignores->dir);
429 }
430 
ignore_lookup_in_rules(int * ignored,git_attr_file * file,git_attr_path * path)431 static bool ignore_lookup_in_rules(
432 	int *ignored, git_attr_file *file, git_attr_path *path)
433 {
434 	size_t j;
435 	git_attr_fnmatch *match;
436 
437 	git_vector_rforeach(&file->rules, j, match) {
438 		if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY &&
439 		    path->is_dir == GIT_DIR_FLAG_FALSE)
440 			continue;
441 		if (git_attr_fnmatch__match(match, path)) {
442 			*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
443 				GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;
444 			return true;
445 		}
446 	}
447 
448 	return false;
449 }
450 
git_ignore__lookup(int * out,git_ignores * ignores,const char * pathname,git_dir_flag dir_flag)451 int git_ignore__lookup(
452 	int *out, git_ignores *ignores, const char *pathname, git_dir_flag dir_flag)
453 {
454 	size_t i;
455 	git_attr_file *file;
456 	git_attr_path path;
457 
458 	*out = GIT_IGNORE_NOTFOUND;
459 
460 	if (git_attr_path__init(
461 		&path, ignores->repo, pathname, git_repository_workdir(ignores->repo), dir_flag) < 0)
462 		return -1;
463 
464 	/* first process builtins - success means path was found */
465 	if (ignore_lookup_in_rules(out, ignores->ign_internal, &path))
466 		goto cleanup;
467 
468 	/* next process files in the path.
469 	 * this process has to process ignores in reverse order
470 	 * to ensure correct prioritization of rules
471 	 */
472 	git_vector_rforeach(&ignores->ign_path, i, file) {
473 		if (ignore_lookup_in_rules(out, file, &path))
474 			goto cleanup;
475 	}
476 
477 	/* last process global ignores */
478 	git_vector_foreach(&ignores->ign_global, i, file) {
479 		if (ignore_lookup_in_rules(out, file, &path))
480 			goto cleanup;
481 	}
482 
483 cleanup:
484 	git_attr_path__free(&path);
485 	return 0;
486 }
487 
git_ignore_add_rule(git_repository * repo,const char * rules)488 int git_ignore_add_rule(git_repository *repo, const char *rules)
489 {
490 	int error;
491 	git_attr_file *ign_internal = NULL;
492 
493 	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
494 		return error;
495 
496 	error = parse_ignore_file(repo, ign_internal, rules, false);
497 	git_attr_file__free(ign_internal);
498 
499 	return error;
500 }
501 
git_ignore_clear_internal_rules(git_repository * repo)502 int git_ignore_clear_internal_rules(git_repository *repo)
503 {
504 	int error;
505 	git_attr_file *ign_internal;
506 
507 	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
508 		return error;
509 
510 	if (!(error = git_attr_file__clear_rules(ign_internal, true)))
511 		error = parse_ignore_file(
512 				repo, ign_internal, GIT_IGNORE_DEFAULT_RULES, false);
513 
514 	git_attr_file__free(ign_internal);
515 	return error;
516 }
517 
git_ignore_path_is_ignored(int * ignored,git_repository * repo,const char * pathname)518 int git_ignore_path_is_ignored(
519 	int *ignored,
520 	git_repository *repo,
521 	const char *pathname)
522 {
523 	int error;
524 	const char *workdir;
525 	git_attr_path path;
526 	git_ignores ignores;
527 	unsigned int i;
528 	git_attr_file *file;
529 	git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
530 
531 	GIT_ASSERT_ARG(repo);
532 	GIT_ASSERT_ARG(ignored);
533 	GIT_ASSERT_ARG(pathname);
534 
535 	workdir = git_repository_workdir(repo);
536 
537 	memset(&path, 0, sizeof(path));
538 	memset(&ignores, 0, sizeof(ignores));
539 
540 	if (!git__suffixcmp(pathname, "/"))
541 		dir_flag = GIT_DIR_FLAG_TRUE;
542 	else if (git_repository_is_bare(repo))
543 		dir_flag = GIT_DIR_FLAG_FALSE;
544 
545 	if ((error = git_attr_path__init(&path, repo, pathname, workdir, dir_flag)) < 0 ||
546 		(error = git_ignore__for_path(repo, path.path, &ignores)) < 0)
547 		goto cleanup;
548 
549 	while (1) {
550 		/* first process builtins - success means path was found */
551 		if (ignore_lookup_in_rules(ignored, ignores.ign_internal, &path))
552 			goto cleanup;
553 
554 		/* next process files in the path */
555 		git_vector_foreach(&ignores.ign_path, i, file) {
556 			if (ignore_lookup_in_rules(ignored, file, &path))
557 				goto cleanup;
558 		}
559 
560 		/* last process global ignores */
561 		git_vector_foreach(&ignores.ign_global, i, file) {
562 			if (ignore_lookup_in_rules(ignored, file, &path))
563 				goto cleanup;
564 		}
565 
566 		/* move up one directory */
567 		if (path.basename == path.path)
568 			break;
569 		path.basename[-1] = '\0';
570 		while (path.basename > path.path && *path.basename != '/')
571 			path.basename--;
572 		if (path.basename > path.path)
573 			path.basename++;
574 		path.is_dir = 1;
575 
576 		if ((error = git_ignore__pop_dir(&ignores)) < 0)
577 			break;
578 	}
579 
580 	*ignored = 0;
581 
582 cleanup:
583 	git_attr_path__free(&path);
584 	git_ignore__free(&ignores);
585 	return error;
586 }
587 
git_ignore__check_pathspec_for_exact_ignores(git_repository * repo,git_vector * vspec,bool no_fnmatch)588 int git_ignore__check_pathspec_for_exact_ignores(
589 	git_repository *repo,
590 	git_vector *vspec,
591 	bool no_fnmatch)
592 {
593 	int error = 0;
594 	size_t i;
595 	git_attr_fnmatch *match;
596 	int ignored;
597 	git_buf path = GIT_BUF_INIT;
598 	const char *filename;
599 	git_index *idx;
600 
601 	if ((error = git_repository__ensure_not_bare(
602 			repo, "validate pathspec")) < 0 ||
603 		(error = git_repository_index(&idx, repo)) < 0)
604 		return error;
605 
606 	git_vector_foreach(vspec, i, match) {
607 		/* skip wildcard matches (if they are being used) */
608 		if ((match->flags & GIT_ATTR_FNMATCH_HASWILD) != 0 &&
609 			!no_fnmatch)
610 			continue;
611 
612 		filename = match->pattern;
613 
614 		/* if file is already in the index, it's fine */
615 		if (git_index_get_bypath(idx, filename, 0) != NULL)
616 			continue;
617 
618 		if ((error = git_repository_workdir_path(&path, repo, filename)) < 0)
619 			break;
620 
621 		/* is there a file on disk that matches this exactly? */
622 		if (!git_path_isfile(path.ptr))
623 			continue;
624 
625 		/* is that file ignored? */
626 		if ((error = git_ignore_path_is_ignored(&ignored, repo, filename)) < 0)
627 			break;
628 
629 		if (ignored) {
630 			git_error_set(GIT_ERROR_INVALID, "pathspec contains ignored file '%s'",
631 				filename);
632 			error = GIT_EINVALIDSPEC;
633 			break;
634 		}
635 	}
636 
637 	git_index_free(idx);
638 	git_buf_dispose(&path);
639 
640 	return error;
641 }
642 
643