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