ignore.c 15.3 KB
Newer Older
1 2 3 4 5 6 7
/*
 * Copyright (C) the libgit2 contributors. All rights reserved.
 *
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
 */

8 9
#include "ignore.h"

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 18

#define GIT_IGNORE_INTERNAL		"[internal]exclude"

19 20
#define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n"

21
/**
22 23 24
 * A negative ignore pattern can negate a positive one without
 * wildcards if it is a basename only and equals the basename of
 * the positive pattern. Thus
25 26 27 28
 *
 * foo/bar
 * !bar
 *
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
 * would result in foo/bar being unignored again while
 *
 * moo/foo/bar
 * !foo/bar
 *
 * would do nothing. The reverse also holds true: a positive
 * basename pattern can be negated by unignoring the basename in
 * subdirectories. Thus
 *
 * bar
 * !foo/bar
 *
 * would result in foo/bar being unignored again. As with the
 * first case,
 *
 * foo/bar
 * !moo/foo/bar
 *
 * would do nothing, again.
48 49 50
 */
static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
{
51
	int (*cmp)(const char *, const char *, size_t);
52
	git_attr_fnmatch *longer, *shorter;
53 54
	char *p;

55 56 57
	if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0
	    || (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0)
		return false;
58

59 60 61 62 63
	if (neg->flags & GIT_ATTR_FNMATCH_ICASE)
		cmp = git__strncasecmp;
	else
		cmp = strncmp;

64 65
	/* If lengths match we need to have an exact match */
	if (rule->length == neg->length) {
66
		return cmp(rule->pattern, neg->pattern, rule->length) == 0;
67 68 69 70 71 72 73
	} else if (rule->length < neg->length) {
		shorter = rule;
		longer = neg;
	} else {
		shorter = neg;
		longer = rule;
	}
74

75 76 77 78 79
	/* Otherwise, we need to check if the shorter
	 * rule is a basename only (that is, it contains
	 * no path separator) and, if so, if it
	 * matches the tail of the longer rule */
	p = longer->pattern + longer->length - shorter->length;
80

81 82 83 84
	if (p[-1] != '/')
		return false;
	if (memchr(shorter->pattern, '/', shorter->length) != NULL)
		return false;
85

86
	return cmp(p, shorter->pattern, shorter->length) == 0;
87 88 89
}

/**
90 91 92 93 94 95 96 97 98 99 100 101 102 103
 * A negative ignore can only unignore a file which is given explicitly before, thus
 *
 *    foo
 *    !foo/bar
 *
 * does not unignore 'foo/bar' as it's not in the list. However
 *
 *    foo/<star>
 *    !foo/bar
 *
 * does unignore 'foo/bar', as it is contained within the 'foo/<star>' rule.
 */
static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match)
{
104
	int error = 0, fnflags;
105 106 107 108 109
	size_t i;
	git_attr_fnmatch *rule;
	char *path;
	git_buf buf = GIT_BUF_INIT;

110 111
	*out = 0;

112 113 114 115
	fnflags = FNM_PATHNAME;
	if (match->flags & GIT_ATTR_FNMATCH_ICASE)
		fnflags |= FNM_IGNORECASE;

116 117 118 119 120 121 122 123 124 125
	/* path of the file relative to the workdir, so we match the rules in subdirs */
	if (match->containing_dir) {
		git_buf_puts(&buf, match->containing_dir);
	}
	if (git_buf_puts(&buf, match->pattern) < 0)
		return -1;

	path = git_buf_detach(&buf);

	git_vector_foreach(rules, i, rule) {
126 127
		if (!(rule->flags & GIT_ATTR_FNMATCH_HASWILD)) {
			if (does_negate_pattern(rule, match)) {
128
				error = 0;
129 130 131 132 133 134
				*out = 1;
				goto out;
			}
			else
				continue;
		}
135

136 137 138 139 140 141
		/*
		 * When dealing with a directory, we add '/<star>' so
		 * p_fnmatch() honours FNM_PATHNAME. Checking for LEADINGDIR
		 * alone isn't enough as that's also set for nagations, so we
		 * need to check that NEGATIVE is off.
		 */
142 143 144 145
		git_buf_clear(&buf);
		if (rule->containing_dir) {
			git_buf_puts(&buf, rule->containing_dir);
		}
146 147 148 149 150

		error = git_buf_puts(&buf, rule->pattern);

		if ((rule->flags & (GIT_ATTR_FNMATCH_LEADINGDIR | GIT_ATTR_FNMATCH_NEGATIVE)) == GIT_ATTR_FNMATCH_LEADINGDIR)
			error = git_buf_PUTS(&buf, "/*");
151 152 153 154

		if (error < 0)
			goto out;

155
		if ((error = p_fnmatch(git_buf_cstr(&buf), path, fnflags)) < 0) {
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
			giterr_set(GITERR_INVALID, "error matching pattern");
			goto out;
		}

		/* if we found a match, we want to keep this rule */
		if (error != FNM_NOMATCH) {
			*out = 1;
			error = 0;
			goto out;
		}
	}

	error = 0;

out:
	git__free(path);
	git_buf_free(&buf);
	return error;
}

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 182
	const char *scan = data, *context = NULL;
	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 189 190 191
	if (attrs->entry &&
		git_path_root(attrs->entry->path) < 0 &&
		!git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE))
		context = attrs->entry->path;
192

193
	if (git_mutex_lock(&attrs->lock) < 0) {
194
		giterr_set(GITERR_OS, "failed to lock ignore file");
195 196 197
		return -1;
	}

198
	while (!error && *scan) {
199 200
		int valid_rule = 1;

201 202 203
		if (!match && !(match = git__calloc(1, sizeof(*match)))) {
			error = -1;
			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 212 213 214 215
			match->flags |= GIT_ATTR_FNMATCH_IGNORE;

			if (ignore_case)
				match->flags |= GIT_ATTR_FNMATCH_ICASE;

216 217 218 219 220 221 222 223 224 225
			while (match->length > 0) {
				if (match->pattern[match->length - 1] == ' ' ||
				    match->pattern[match->length - 1] == '\t') {
					match->pattern[match->length - 1] = 0;
					match->length --;
				} else {
					break;
				}
			}

226
			scan = git__next_line(scan);
227

228 229 230 231 232 233 234 235
			/*
			 * If a negative match doesn't actually do anything,
			 * throw it away. As we cannot always verify whether a
			 * rule containing wildcards negates another rule, we
			 * do not optimize away these rules, though.
			 * */
			if (match->flags & GIT_ATTR_FNMATCH_NEGATIVE
			    && !(match->flags & GIT_ATTR_FNMATCH_HASWILD))
236 237 238 239
				error = does_negate_rule(&valid_rule, &attrs->rules, match);

			if (!error && valid_rule)
				error = git_vector_insert(&attrs->rules, match);
240 241
		}

242
		if (error != 0 || !valid_rule) {
243 244 245
			match->pattern = NULL;

			if (error == GIT_ENOTFOUND)
246
				error = 0;
247 248 249 250 251
		} else {
			match = NULL; /* vector now "owns" the match */
		}
	}

252
	git_mutex_unlock(&attrs->lock);
253
	git__free(match);
254 255 256 257

	return error;
}

258 259 260 261 262 263 264 265 266
static int push_ignore_file(
	git_ignores *ignores,
	git_vector *which_list,
	const char *base,
	const char *filename)
{
	int error = 0;
	git_attr_file *file = NULL;

267
	error = git_attr_cache__get(
268
		&file, ignores->repo, NULL, GIT_ATTR_FILE__FROM_FILE,
269
		base, filename, parse_ignore_file);
270 271 272 273 274 275 276
	if (error < 0)
		return error;

	if (file != NULL) {
		if ((error = git_vector_insert(which_list, file)) < 0)
			git_attr_file__free(file);
	}
277 278 279

	return error;
}
280

281
static int push_one_ignore(void *payload, const char *path)
282
{
283
	git_ignores *ign = payload;
284
	ign->depth++;
285
	return push_ignore_file(ign, &ign->ign_path, path, GIT_IGNORE_FILE);
286 287
}

288
static int get_internal_ignores(git_attr_file **out, git_repository *repo)
289 290 291
{
	int error;

292 293 294 295
	if ((error = git_attr_cache__init(repo)) < 0)
		return error;

	error = git_attr_cache__get(
296
		out, repo, NULL, GIT_ATTR_FILE__IN_MEMORY, NULL, GIT_IGNORE_INTERNAL, NULL);
297

298 299
	/* if internal rules list is empty, insert default rules */
	if (!error && !(*out)->rules.length)
300
		error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES);
301 302 303 304

	return error;
}

305 306 307 308
int git_ignore__for_path(
	git_repository *repo,
	const char *path,
	git_ignores *ignores)
309
{
310
	int error = 0;
311
	const char *workdir = git_repository_workdir(repo);
312
	git_buf infopath = GIT_BUF_INIT;
313

314
	assert(repo && ignores && path);
315

316
	memset(ignores, 0, sizeof(*ignores));
317 318
	ignores->repo = repo;

319 320 321
	/* Read the ignore_case flag */
	if ((error = git_repository__cvar(
			&ignores->ignore_case, repo, GIT_CVAR_IGNORECASE)) < 0)
322 323
		goto cleanup;

324
	if ((error = git_attr_cache__init(repo)) < 0)
325 326
		goto cleanup;

327
	/* given a unrooted path in a non-bare repo, resolve it */
328 329 330 331 332 333 334 335 336 337
	if (workdir && git_path_root(path) < 0) {
		git_buf local = GIT_BUF_INIT;

		if ((error = git_path_dirname_r(&local, path)) < 0 ||
		    (error = git_path_resolve_relative(&local, 0)) < 0 ||
		    (error = git_path_to_dir(&local)) < 0 ||
		    (error = git_buf_joinpath(&ignores->dir, workdir, local.ptr)) < 0)
		{;} /* Nothing, we just want to stop on the first error */
		git_buf_free(&local);
	} else {
338
		error = git_buf_joinpath(&ignores->dir, path, "");
339
	}
340
	if (error < 0)
341 342
		goto cleanup;

343 344 345
	if (workdir && !git__prefixcmp(ignores->dir.ptr, workdir))
		ignores->dir_root = strlen(workdir);

346
	/* set up internals */
347
	if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0)
348 349 350
		goto cleanup;

	/* load .gitignore up the path */
351 352
	if (workdir != NULL) {
		error = git_path_walk_up(
353
			&ignores->dir, workdir, push_one_ignore, ignores);
354 355 356
		if (error < 0)
			goto cleanup;
	}
357

358 359 360 361
	if ((error = git_repository_item_path(&infopath,
			repo, GIT_REPOSITORY_ITEM_INFO)) < 0)
		goto cleanup;

362
	/* load .git/info/exclude */
363 364
	error = push_ignore_file(
		ignores, &ignores->ign_global,
365
		infopath.ptr, GIT_IGNORE_FILE_INREPO);
366
	if (error < 0)
367 368 369
		goto cleanup;

	/* load core.excludesfile */
370
	if (git_repository_attr_cache(repo)->cfg_excl_file != NULL)
371 372
		error = push_ignore_file(
			ignores, &ignores->ign_global, NULL,
373
			git_repository_attr_cache(repo)->cfg_excl_file);
374 375

cleanup:
376
	git_buf_free(&infopath);
377
	if (error < 0)
378
		git_ignore__free(ignores);
379

380 381 382 383 384
	return error;
}

int git_ignore__push_dir(git_ignores *ign, const char *dir)
{
385 386
	if (git_buf_joinpath(&ign->dir, ign->dir.ptr, dir) < 0)
		return -1;
387

388 389
	ign->depth++;

390
	return push_ignore_file(
391
		ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE);
392 393
}

394 395 396 397
int git_ignore__pop_dir(git_ignores *ign)
{
	if (ign->ign_path.length > 0) {
		git_attr_file *file = git_vector_last(&ign->ign_path);
398
		const char *start = file->entry->path, *end;
399

400 401
		/* - ign->dir looks something like "/home/user/a/b/" (or "a/b/c/d/")
		 * - file->path looks something like "a/b/.gitignore
402
		 *
403 404 405
		 * We are popping the last directory off ign->dir.  We also want
		 * to remove the file from the vector if the popped directory
		 * matches the ignore path.  We need to test if the "a/b" part of
406 407 408
		 * the file key matches the path we are about to pop.
		 */

409 410
		if ((end = strrchr(start, '/')) != NULL) {
			size_t dirlen = (end - start) + 1;
411 412
			const char *relpath = ign->dir.ptr + ign->dir_root;
			size_t pathlen = ign->dir.size - ign->dir_root;
413

414
			if (pathlen == dirlen && !memcmp(relpath, start, dirlen)) {
415 416 417
				git_vector_pop(&ign->ign_path);
				git_attr_file__free(file);
			}
418
		}
419
	}
420

421
	if (--ign->depth > 0) {
422
		git_buf_rtruncate_at_char(&ign->dir, '/');
423
		git_path_to_dir(&ign->dir);
424
	}
425

426
	return 0;
427 428
}

429
void git_ignore__free(git_ignores *ignores)
430
{
431 432 433
	unsigned int i;
	git_attr_file *file;

434
	git_attr_file__free(ignores->ign_internal);
435 436 437 438 439

	git_vector_foreach(&ignores->ign_path, i, file) {
		git_attr_file__free(file);
		ignores->ign_path.contents[i] = NULL;
	}
440
	git_vector_free(&ignores->ign_path);
441 442 443 444 445

	git_vector_foreach(&ignores->ign_global, i, file) {
		git_attr_file__free(file);
		ignores->ign_global.contents[i] = NULL;
	}
446
	git_vector_free(&ignores->ign_global);
447

448 449 450
	git_buf_free(&ignores->dir);
}

451
static bool ignore_lookup_in_rules(
452
	int *ignored, git_attr_file *file, git_attr_path *path)
453
{
454
	size_t j;
455 456
	git_attr_fnmatch *match;

457
	git_vector_rforeach(&file->rules, j, match) {
458
		if (git_attr_fnmatch__match(match, path)) {
459 460
			*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
				GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;
461
			return true;
462 463 464
		}
	}

465
	return false;
466 467
}

468
int git_ignore__lookup(
469
	int *out, git_ignores *ignores, const char *pathname, git_dir_flag dir_flag)
470
{
471
	unsigned int i;
472 473 474
	git_attr_file *file;
	git_attr_path path;

475 476
	*out = GIT_IGNORE_NOTFOUND;

477
	if (git_attr_path__init(
478
		&path, pathname, git_repository_workdir(ignores->repo), dir_flag) < 0)
479
		return -1;
480

481
	/* first process builtins - success means path was found */
482
	if (ignore_lookup_in_rules(out, ignores->ign_internal, &path))
483
		goto cleanup;
484

485 486
	/* next process files in the path */
	git_vector_foreach(&ignores->ign_path, i, file) {
487
		if (ignore_lookup_in_rules(out, file, &path))
488
			goto cleanup;
489 490
	}

491 492
	/* last process global ignores */
	git_vector_foreach(&ignores->ign_global, i, file) {
493
		if (ignore_lookup_in_rules(out, file, &path))
494
			goto cleanup;
495 496
	}

497 498
cleanup:
	git_attr_path__free(&path);
499
	return 0;
500
}
501

502
int git_ignore_add_rule(git_repository *repo, const char *rules)
503 504
{
	int error;
505
	git_attr_file *ign_internal = NULL;
506

507 508 509 510 511
	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
		return error;

	error = parse_ignore_file(repo, ign_internal, rules);
	git_attr_file__free(ign_internal);
512 513 514 515

	return error;
}

516
int git_ignore_clear_internal_rules(git_repository *repo)
517 518 519 520
{
	int error;
	git_attr_file *ign_internal;

521 522
	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
		return error;
523

524 525 526
	if (!(error = git_attr_file__clear_rules(ign_internal, true)))
		error = parse_ignore_file(
			repo, ign_internal, GIT_IGNORE_DEFAULT_RULES);
527

528
	git_attr_file__free(ign_internal);
529 530
	return error;
}
531 532 533 534

int git_ignore_path_is_ignored(
	int *ignored,
	git_repository *repo,
535
	const char *pathname)
536 537
{
	int error;
538 539
	const char *workdir;
	git_attr_path path;
540
	git_ignores ignores;
541 542
	unsigned int i;
	git_attr_file *file;
543

544
	assert(repo && ignored && pathname);
545

546
	workdir = git_repository_workdir(repo);
547

548 549
	memset(&path, 0, sizeof(path));
	memset(&ignores, 0, sizeof(ignores));
550

551
	if ((error = git_attr_path__init(&path, pathname, workdir, GIT_DIR_FLAG_UNKNOWN)) < 0 ||
552 553
		(error = git_ignore__for_path(repo, path.path, &ignores)) < 0)
		goto cleanup;
554

555 556
	while (1) {
		/* first process builtins - success means path was found */
557
		if (ignore_lookup_in_rules(ignored, ignores.ign_internal, &path))
558 559 560 561
			goto cleanup;

		/* next process files in the path */
		git_vector_foreach(&ignores.ign_path, i, file) {
562
			if (ignore_lookup_in_rules(ignored, file, &path))
563 564 565 566 567
				goto cleanup;
		}

		/* last process global ignores */
		git_vector_foreach(&ignores.ign_global, i, file) {
568
			if (ignore_lookup_in_rules(ignored, file, &path))
569 570 571
				goto cleanup;
		}

572 573
		/* move up one directory */
		if (path.basename == path.path)
574
			break;
575 576 577 578 579 580 581 582
		path.basename[-1] = '\0';
		while (path.basename > path.path && *path.basename != '/')
			path.basename--;
		if (path.basename > path.path)
			path.basename++;
		path.is_dir = 1;

		if ((error = git_ignore__pop_dir(&ignores)) < 0)
583
			break;
584 585 586 587 588 589
	}

	*ignored = 0;

cleanup:
	git_attr_path__free(&path);
590 591 592 593
	git_ignore__free(&ignores);
	return error;
}

594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650
int git_ignore__check_pathspec_for_exact_ignores(
	git_repository *repo,
	git_vector *vspec,
	bool no_fnmatch)
{
	int error = 0;
	size_t i;
	git_attr_fnmatch *match;
	int ignored;
	git_buf path = GIT_BUF_INIT;
	const char *wd, *filename;
	git_index *idx;

	if ((error = git_repository__ensure_not_bare(
			repo, "validate pathspec")) < 0 ||
		(error = git_repository_index(&idx, repo)) < 0)
		return error;

	wd = git_repository_workdir(repo);

	git_vector_foreach(vspec, i, match) {
		/* skip wildcard matches (if they are being used) */
		if ((match->flags & GIT_ATTR_FNMATCH_HASWILD) != 0 &&
			!no_fnmatch)
			continue;

		filename = match->pattern;

		/* if file is already in the index, it's fine */
		if (git_index_get_bypath(idx, filename, 0) != NULL)
			continue;

		if ((error = git_buf_joinpath(&path, wd, filename)) < 0)
			break;

		/* is there a file on disk that matches this exactly? */
		if (!git_path_isfile(path.ptr))
			continue;

		/* is that file ignored? */
		if ((error = git_ignore_path_is_ignored(&ignored, repo, filename)) < 0)
			break;

		if (ignored) {
			giterr_set(GITERR_INVALID, "pathspec contains ignored file '%s'",
				filename);
			error = GIT_EINVALIDSPEC;
			break;
		}
	}

	git_index_free(idx);
	git_buf_free(&path);

	return error;
}