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 "fnmatch.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, fnflags;
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 	fnflags = FNM_PATHNAME;
113 	if (match->flags & GIT_ATTR_FNMATCH_ICASE)
114 		fnflags |= FNM_IGNORECASE;
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 		/*
137 		 * When dealing with a directory, we add '/<star>' so
138 		 * p_fnmatch() honours FNM_PATHNAME. Checking for LEADINGDIR
139 		 * alone isn't enough as that's also set for nagations, so we
140 		 * need to check that NEGATIVE is off.
141 		 */
142 		git_buf_clear(&buf);
143 		if (rule->containing_dir) {
144 			git_buf_puts(&buf, rule->containing_dir);
145 		}
146 
147 		error = git_buf_puts(&buf, rule->pattern);
148 
149 		if ((rule->flags & (GIT_ATTR_FNMATCH_LEADINGDIR | GIT_ATTR_FNMATCH_NEGATIVE)) == GIT_ATTR_FNMATCH_LEADINGDIR)
150 			error = git_buf_PUTS(&buf, "/*");
151 
152 		if (error < 0)
153 			goto out;
154 
155 		if ((error = p_fnmatch(git_buf_cstr(&buf), path, fnflags)) < 0) {
156 			giterr_set(GITERR_INVALID, "error matching pattern");
157 			goto out;
158 		}
159 
160 		/* if we found a match, we want to keep this rule */
161 		if (error != FNM_NOMATCH) {
162 			*out = 1;
163 			error = 0;
164 			goto out;
165 		}
166 	}
167 
168 	error = 0;
169 
170 out:
171 	git__free(path);
172 	git_buf_free(&buf);
173 	return error;
174 }
175 
parse_ignore_file(git_repository * repo,git_attr_file * attrs,const char * data)176 static int parse_ignore_file(
177 	git_repository *repo, git_attr_file *attrs, const char *data)
178 {
179 	int error = 0;
180 	int ignore_case = false;
181 	const char *scan = data, *context = NULL;
182 	git_attr_fnmatch *match = NULL;
183 
184 	if (git_repository__cvar(&ignore_case, repo, GIT_CVAR_IGNORECASE) < 0)
185 		giterr_clear();
186 
187 	/* if subdir file path, convert context for file paths */
188 	if (attrs->entry &&
189 		git_path_root(attrs->entry->path) < 0 &&
190 		!git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE))
191 		context = attrs->entry->path;
192 
193 	if (git_mutex_lock(&attrs->lock) < 0) {
194 		giterr_set(GITERR_OS, "failed to lock ignore file");
195 		return -1;
196 	}
197 
198 	while (!error && *scan) {
199 		int valid_rule = 1;
200 
201 		if (!match && !(match = git__calloc(1, sizeof(*match)))) {
202 			error = -1;
203 			break;
204 		}
205 
206 		match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
207 
208 		if (!(error = git_attr_fnmatch__parse(
209 			match, &attrs->pool, context, &scan)))
210 		{
211 			match->flags |= GIT_ATTR_FNMATCH_IGNORE;
212 
213 			if (ignore_case)
214 				match->flags |= GIT_ATTR_FNMATCH_ICASE;
215 
216 			scan = git__next_line(scan);
217 
218 			/*
219 			 * If a negative match doesn't actually do anything,
220 			 * throw it away. As we cannot always verify whether a
221 			 * rule containing wildcards negates another rule, we
222 			 * do not optimize away these rules, though.
223 			 * */
224 			if (match->flags & GIT_ATTR_FNMATCH_NEGATIVE
225 			    && !(match->flags & GIT_ATTR_FNMATCH_HASWILD))
226 				error = does_negate_rule(&valid_rule, &attrs->rules, match);
227 
228 			if (!error && valid_rule)
229 				error = git_vector_insert(&attrs->rules, match);
230 		}
231 
232 		if (error != 0 || !valid_rule) {
233 			match->pattern = NULL;
234 
235 			if (error == GIT_ENOTFOUND)
236 				error = 0;
237 		} else {
238 			match = NULL; /* vector now "owns" the match */
239 		}
240 	}
241 
242 	git_mutex_unlock(&attrs->lock);
243 	git__free(match);
244 
245 	return error;
246 }
247 
push_ignore_file(git_ignores * ignores,git_vector * which_list,const char * base,const char * filename)248 static int push_ignore_file(
249 	git_ignores *ignores,
250 	git_vector *which_list,
251 	const char *base,
252 	const char *filename)
253 {
254 	int error = 0;
255 	git_attr_file *file = NULL;
256 
257 	error = git_attr_cache__get(
258 		&file, ignores->repo, NULL, GIT_ATTR_FILE__FROM_FILE,
259 		base, filename, parse_ignore_file);
260 	if (error < 0)
261 		return error;
262 
263 	if (file != NULL) {
264 		if ((error = git_vector_insert(which_list, file)) < 0)
265 			git_attr_file__free(file);
266 	}
267 
268 	return error;
269 }
270 
push_one_ignore(void * payload,const char * path)271 static int push_one_ignore(void *payload, const char *path)
272 {
273 	git_ignores *ign = payload;
274 	ign->depth++;
275 	return push_ignore_file(ign, &ign->ign_path, path, GIT_IGNORE_FILE);
276 }
277 
get_internal_ignores(git_attr_file ** out,git_repository * repo)278 static int get_internal_ignores(git_attr_file **out, git_repository *repo)
279 {
280 	int error;
281 
282 	if ((error = git_attr_cache__init(repo)) < 0)
283 		return error;
284 
285 	error = git_attr_cache__get(
286 		out, repo, NULL, GIT_ATTR_FILE__IN_MEMORY, NULL, GIT_IGNORE_INTERNAL, NULL);
287 
288 	/* if internal rules list is empty, insert default rules */
289 	if (!error && !(*out)->rules.length)
290 		error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES);
291 
292 	return error;
293 }
294 
git_ignore__for_path(git_repository * repo,const char * path,git_ignores * ignores)295 int git_ignore__for_path(
296 	git_repository *repo,
297 	const char *path,
298 	git_ignores *ignores)
299 {
300 	int error = 0;
301 	const char *workdir = git_repository_workdir(repo);
302 	git_buf infopath = GIT_BUF_INIT;
303 
304 	assert(repo && ignores && path);
305 
306 	memset(ignores, 0, sizeof(*ignores));
307 	ignores->repo = repo;
308 
309 	/* Read the ignore_case flag */
310 	if ((error = git_repository__cvar(
311 			&ignores->ignore_case, repo, GIT_CVAR_IGNORECASE)) < 0)
312 		goto cleanup;
313 
314 	if ((error = git_attr_cache__init(repo)) < 0)
315 		goto cleanup;
316 
317 	/* given a unrooted path in a non-bare repo, resolve it */
318 	if (workdir && git_path_root(path) < 0) {
319 		git_buf local = GIT_BUF_INIT;
320 
321 		if ((error = git_path_dirname_r(&local, path)) < 0 ||
322 		    (error = git_path_resolve_relative(&local, 0)) < 0 ||
323 		    (error = git_path_to_dir(&local)) < 0 ||
324 		    (error = git_buf_joinpath(&ignores->dir, workdir, local.ptr)) < 0)
325 		{;} /* Nothing, we just want to stop on the first error */
326 		git_buf_free(&local);
327 	} else {
328 		error = git_buf_joinpath(&ignores->dir, path, "");
329 	}
330 	if (error < 0)
331 		goto cleanup;
332 
333 	if (workdir && !git__prefixcmp(ignores->dir.ptr, workdir))
334 		ignores->dir_root = strlen(workdir);
335 
336 	/* set up internals */
337 	if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0)
338 		goto cleanup;
339 
340 	/* load .gitignore up the path */
341 	if (workdir != NULL) {
342 		error = git_path_walk_up(
343 			&ignores->dir, workdir, push_one_ignore, ignores);
344 		if (error < 0)
345 			goto cleanup;
346 	}
347 
348 	if ((error = git_repository_item_path(&infopath,
349 			repo, GIT_REPOSITORY_ITEM_INFO)) < 0)
350 		goto cleanup;
351 
352 	/* load .git/info/exclude */
353 	error = push_ignore_file(
354 		ignores, &ignores->ign_global,
355 		infopath.ptr, GIT_IGNORE_FILE_INREPO);
356 	if (error < 0)
357 		goto cleanup;
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_free(&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_free(&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 (git_attr_fnmatch__match(match, path)) {
449 			*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
450 				GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;
451 			return true;
452 		}
453 	}
454 
455 	return false;
456 }
457 
git_ignore__lookup(int * out,git_ignores * ignores,const char * pathname,git_dir_flag dir_flag)458 int git_ignore__lookup(
459 	int *out, git_ignores *ignores, const char *pathname, git_dir_flag dir_flag)
460 {
461 	unsigned int i;
462 	git_attr_file *file;
463 	git_attr_path path;
464 
465 	*out = GIT_IGNORE_NOTFOUND;
466 
467 	if (git_attr_path__init(
468 		&path, pathname, git_repository_workdir(ignores->repo), dir_flag) < 0)
469 		return -1;
470 
471 	/* first process builtins - success means path was found */
472 	if (ignore_lookup_in_rules(out, ignores->ign_internal, &path))
473 		goto cleanup;
474 
475 	/* next process files in the path */
476 	git_vector_foreach(&ignores->ign_path, i, file) {
477 		if (ignore_lookup_in_rules(out, file, &path))
478 			goto cleanup;
479 	}
480 
481 	/* last process global ignores */
482 	git_vector_foreach(&ignores->ign_global, i, file) {
483 		if (ignore_lookup_in_rules(out, file, &path))
484 			goto cleanup;
485 	}
486 
487 cleanup:
488 	git_attr_path__free(&path);
489 	return 0;
490 }
491 
git_ignore_add_rule(git_repository * repo,const char * rules)492 int git_ignore_add_rule(git_repository *repo, const char *rules)
493 {
494 	int error;
495 	git_attr_file *ign_internal = NULL;
496 
497 	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
498 		return error;
499 
500 	error = parse_ignore_file(repo, ign_internal, rules);
501 	git_attr_file__free(ign_internal);
502 
503 	return error;
504 }
505 
git_ignore_clear_internal_rules(git_repository * repo)506 int git_ignore_clear_internal_rules(git_repository *repo)
507 {
508 	int error;
509 	git_attr_file *ign_internal;
510 
511 	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
512 		return error;
513 
514 	if (!(error = git_attr_file__clear_rules(ign_internal, true)))
515 		error = parse_ignore_file(
516 			repo, ign_internal, GIT_IGNORE_DEFAULT_RULES);
517 
518 	git_attr_file__free(ign_internal);
519 	return error;
520 }
521 
git_ignore_path_is_ignored(int * ignored,git_repository * repo,const char * pathname)522 int git_ignore_path_is_ignored(
523 	int *ignored,
524 	git_repository *repo,
525 	const char *pathname)
526 {
527 	int error;
528 	const char *workdir;
529 	git_attr_path path;
530 	git_ignores ignores;
531 	unsigned int i;
532 	git_attr_file *file;
533 	git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
534 
535 	assert(repo && ignored && pathname);
536 
537 	workdir = git_repository_workdir(repo);
538 
539 	memset(&path, 0, sizeof(path));
540 	memset(&ignores, 0, sizeof(ignores));
541 
542 	if (git_repository_is_bare(repo))
543 		dir_flag = GIT_DIR_FLAG_FALSE;
544 
545 	if ((error = git_attr_path__init(&path, 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 *wd, *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 	wd = git_repository_workdir(repo);
607 
608 	git_vector_foreach(vspec, i, match) {
609 		/* skip wildcard matches (if they are being used) */
610 		if ((match->flags & GIT_ATTR_FNMATCH_HASWILD) != 0 &&
611 			!no_fnmatch)
612 			continue;
613 
614 		filename = match->pattern;
615 
616 		/* if file is already in the index, it's fine */
617 		if (git_index_get_bypath(idx, filename, 0) != NULL)
618 			continue;
619 
620 		if ((error = git_buf_joinpath(&path, wd, filename)) < 0)
621 			break;
622 
623 		/* is there a file on disk that matches this exactly? */
624 		if (!git_path_isfile(path.ptr))
625 			continue;
626 
627 		/* is that file ignored? */
628 		if ((error = git_ignore_path_is_ignored(&ignored, repo, filename)) < 0)
629 			break;
630 
631 		if (ignored) {
632 			giterr_set(GITERR_INVALID, "pathspec contains ignored file '%s'",
633 				filename);
634 			error = GIT_EINVALIDSPEC;
635 			break;
636 		}
637 	}
638 
639 	git_index_free(idx);
640 	git_buf_free(&path);
641 
642 	return error;
643 }
644 
645