ignore.c 14.4 KB
Newer Older
1
#include "git2/ignore.h"
2
#include "common.h"
3
#include "ignore.h"
4
#include "attrcache.h"
5
#include "path.h"
6
#include "config.h"
7
#include "fnmatch.h"
8 9 10

#define GIT_IGNORE_INTERNAL		"[internal]exclude"

11 12
#define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n"

13
/**
14 15 16
 * 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
17 18 19 20
 *
 * foo/bar
 * !bar
 *
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
 * 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.
40 41 42
 */
static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
{
43
	git_attr_fnmatch *longer, *shorter;
44 45 46 47
	char *p;

	if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0
		&& (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0) {
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68

		/* If lengths match we need to have an exact match */
		if (rule->length == neg->length) {
			return strcmp(rule->pattern, neg->pattern) == 0;
		} else if (rule->length < neg->length) {
			shorter = rule;
			longer = neg;
		} else {
			shorter = neg;
			longer = rule;
		}

		/* 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;

		if (p[-1] != '/')
			return false;
		if (memchr(shorter->pattern, '/', shorter->length) != NULL)
69 70
			return false;

71
		return memcmp(p, shorter->pattern, shorter->length) == 0;
72 73 74 75 76 77
	}

	return false;
}

/**
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
 * 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)
{
	int error = 0;
	size_t i;
	git_attr_fnmatch *rule;
	char *path;
	git_buf buf = GIT_BUF_INIT;

98 99
	*out = 0;

100 101 102 103 104 105 106 107 108 109
	/* 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) {
110 111
		if (!(rule->flags & GIT_ATTR_FNMATCH_HASWILD)) {
			if (does_negate_pattern(rule, match)) {
112
				error = 0;
113 114 115 116 117 118
				*out = 1;
				goto out;
			}
			else
				continue;
		}
119 120

	/*
121 122 123 124
	 * 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.
125 126 127 128 129
	 */
		git_buf_clear(&buf);
		if (rule->containing_dir) {
			git_buf_puts(&buf, rule->containing_dir);
		}
130 131 132 133 134

		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, "/*");
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159

		if (error < 0)
			goto out;

		if ((error = p_fnmatch(git_buf_cstr(&buf), path, FNM_PATHNAME)) < 0) {
			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;
}

160
static int parse_ignore_file(
161
	git_repository *repo, git_attr_file *attrs, const char *data)
162
{
163
	int error = 0;
164
	int ignore_case = false;
165 166
	const char *scan = data, *context = NULL;
	git_attr_fnmatch *match = NULL;
167

168
	if (git_repository__cvar(&ignore_case, repo, GIT_CVAR_IGNORECASE) < 0)
169
		giterr_clear();
170

171
	/* if subdir file path, convert context for file paths */
172 173 174 175
	if (attrs->entry &&
		git_path_root(attrs->entry->path) < 0 &&
		!git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE))
		context = attrs->entry->path;
176

177
	if (git_mutex_lock(&attrs->lock) < 0) {
178
		giterr_set(GITERR_OS, "failed to lock ignore file");
179 180 181
		return -1;
	}

182
	while (!error && *scan) {
183 184
		int valid_rule = 1;

185 186 187
		if (!match && !(match = git__calloc(1, sizeof(*match)))) {
			error = -1;
			break;
188 189
		}

190
		match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
191

192
		if (!(error = git_attr_fnmatch__parse(
193
			match, &attrs->pool, context, &scan)))
194
		{
195 196 197 198 199
			match->flags |= GIT_ATTR_FNMATCH_IGNORE;

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

200
			scan = git__next_line(scan);
201 202 203 204 205 206 207

			/* if a negative match doesn't actually do anything, throw it away */
			if (match->flags & GIT_ATTR_FNMATCH_NEGATIVE)
				error = does_negate_rule(&valid_rule, &attrs->rules, match);

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

210
		if (error != 0 || !valid_rule) {
211 212 213
			match->pattern = NULL;

			if (error == GIT_ENOTFOUND)
214
				error = 0;
215 216 217 218 219
		} else {
			match = NULL; /* vector now "owns" the match */
		}
	}

220
	git_mutex_unlock(&attrs->lock);
221
	git__free(match);
222 223 224 225

	return error;
}

226 227 228 229 230 231 232 233 234
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;

235
	error = git_attr_cache__get(
236
		&file, ignores->repo, NULL, GIT_ATTR_FILE__FROM_FILE,
237
		base, filename, parse_ignore_file);
238 239 240 241 242 243 244
	if (error < 0)
		return error;

	if (file != NULL) {
		if ((error = git_vector_insert(which_list, file)) < 0)
			git_attr_file__free(file);
	}
245 246 247

	return error;
}
248

249
static int push_one_ignore(void *payload, const char *path)
250
{
251
	git_ignores *ign = payload;
252
	ign->depth++;
253
	return push_ignore_file(ign, &ign->ign_path, path, GIT_IGNORE_FILE);
254 255
}

256
static int get_internal_ignores(git_attr_file **out, git_repository *repo)
257 258 259
{
	int error;

260 261 262 263
	if ((error = git_attr_cache__init(repo)) < 0)
		return error;

	error = git_attr_cache__get(
264
		out, repo, NULL, GIT_ATTR_FILE__IN_MEMORY, NULL, GIT_IGNORE_INTERNAL, NULL);
265

266 267
	/* if internal rules list is empty, insert default rules */
	if (!error && !(*out)->rules.length)
268
		error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES);
269 270 271 272

	return error;
}

273 274 275 276
int git_ignore__for_path(
	git_repository *repo,
	const char *path,
	git_ignores *ignores)
277
{
278
	int error = 0;
279
	const char *workdir = git_repository_workdir(repo);
280
	git_buf infopath = GIT_BUF_INIT;
281

282
	assert(repo && ignores && path);
283

284
	memset(ignores, 0, sizeof(*ignores));
285 286
	ignores->repo = repo;

287 288 289
	/* Read the ignore_case flag */
	if ((error = git_repository__cvar(
			&ignores->ignore_case, repo, GIT_CVAR_IGNORECASE)) < 0)
290 291
		goto cleanup;

292
	if ((error = git_attr_cache__init(repo)) < 0)
293 294
		goto cleanup;

295
	/* given a unrooted path in a non-bare repo, resolve it */
296 297 298 299 300 301 302 303 304 305
	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 {
306
		error = git_buf_joinpath(&ignores->dir, path, "");
307
	}
308
	if (error < 0)
309 310
		goto cleanup;

311 312 313
	if (workdir && !git__prefixcmp(ignores->dir.ptr, workdir))
		ignores->dir_root = strlen(workdir);

314
	/* set up internals */
315
	if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0)
316 317 318
		goto cleanup;

	/* load .gitignore up the path */
319 320
	if (workdir != NULL) {
		error = git_path_walk_up(
321
			&ignores->dir, workdir, push_one_ignore, ignores);
322 323 324
		if (error < 0)
			goto cleanup;
	}
325

326 327 328 329
	if ((error = git_repository_item_path(&infopath,
			repo, GIT_REPOSITORY_ITEM_INFO)) < 0)
		goto cleanup;

330
	/* load .git/info/exclude */
331 332
	error = push_ignore_file(
		ignores, &ignores->ign_global,
333
		infopath.ptr, GIT_IGNORE_FILE_INREPO);
334
	if (error < 0)
335 336 337
		goto cleanup;

	/* load core.excludesfile */
338
	if (git_repository_attr_cache(repo)->cfg_excl_file != NULL)
339 340
		error = push_ignore_file(
			ignores, &ignores->ign_global, NULL,
341
			git_repository_attr_cache(repo)->cfg_excl_file);
342 343

cleanup:
344
	git_buf_free(&infopath);
345
	if (error < 0)
346
		git_ignore__free(ignores);
347

348 349 350 351 352
	return error;
}

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

356 357
	ign->depth++;

358
	return push_ignore_file(
359
		ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE);
360 361
}

362 363 364 365
int git_ignore__pop_dir(git_ignores *ign)
{
	if (ign->ign_path.length > 0) {
		git_attr_file *file = git_vector_last(&ign->ign_path);
366
		const char *start = file->entry->path, *end;
367

368 369
		/* - ign->dir looks something like "/home/user/a/b/" (or "a/b/c/d/")
		 * - file->path looks something like "a/b/.gitignore
370
		 *
371 372 373
		 * 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
374 375 376
		 * the file key matches the path we are about to pop.
		 */

377 378
		if ((end = strrchr(start, '/')) != NULL) {
			size_t dirlen = (end - start) + 1;
379 380
			const char *relpath = ign->dir.ptr + ign->dir_root;
			size_t pathlen = ign->dir.size - ign->dir_root;
381

382
			if (pathlen == dirlen && !memcmp(relpath, start, dirlen)) {
383 384 385
				git_vector_pop(&ign->ign_path);
				git_attr_file__free(file);
			}
386
		}
387
	}
388

389
	if (--ign->depth > 0) {
390
		git_buf_rtruncate_at_char(&ign->dir, '/');
391
		git_path_to_dir(&ign->dir);
392
	}
393

394
	return 0;
395 396
}

397
void git_ignore__free(git_ignores *ignores)
398
{
399 400 401
	unsigned int i;
	git_attr_file *file;

402
	git_attr_file__free(ignores->ign_internal);
403 404 405 406 407

	git_vector_foreach(&ignores->ign_path, i, file) {
		git_attr_file__free(file);
		ignores->ign_path.contents[i] = NULL;
	}
408
	git_vector_free(&ignores->ign_path);
409 410 411 412 413

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

416 417 418
	git_buf_free(&ignores->dir);
}

419
static bool ignore_lookup_in_rules(
420
	int *ignored, git_attr_file *file, git_attr_path *path)
421
{
422
	size_t j;
423 424
	git_attr_fnmatch *match;

425
	git_vector_rforeach(&file->rules, j, match) {
426
		if (git_attr_fnmatch__match(match, path)) {
427 428
			*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
				GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;
429
			return true;
430 431 432
		}
	}

433
	return false;
434 435
}

436
int git_ignore__lookup(
437
	int *out, git_ignores *ignores, const char *pathname, git_dir_flag dir_flag)
438
{
439
	unsigned int i;
440 441 442
	git_attr_file *file;
	git_attr_path path;

443 444
	*out = GIT_IGNORE_NOTFOUND;

445
	if (git_attr_path__init(
446
		&path, pathname, git_repository_workdir(ignores->repo), dir_flag) < 0)
447
		return -1;
448

449
	/* first process builtins - success means path was found */
450
	if (ignore_lookup_in_rules(out, ignores->ign_internal, &path))
451
		goto cleanup;
452

453 454
	/* next process files in the path */
	git_vector_foreach(&ignores->ign_path, i, file) {
455
		if (ignore_lookup_in_rules(out, file, &path))
456
			goto cleanup;
457 458
	}

459 460
	/* last process global ignores */
	git_vector_foreach(&ignores->ign_global, i, file) {
461
		if (ignore_lookup_in_rules(out, file, &path))
462
			goto cleanup;
463 464
	}

465 466
cleanup:
	git_attr_path__free(&path);
467
	return 0;
468
}
469

470
int git_ignore_add_rule(git_repository *repo, const char *rules)
471 472
{
	int error;
473
	git_attr_file *ign_internal = NULL;
474

475 476 477 478 479
	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);
480 481 482 483

	return error;
}

484
int git_ignore_clear_internal_rules(git_repository *repo)
485 486 487 488
{
	int error;
	git_attr_file *ign_internal;

489 490
	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
		return error;
491

492 493 494
	if (!(error = git_attr_file__clear_rules(ign_internal, true)))
		error = parse_ignore_file(
			repo, ign_internal, GIT_IGNORE_DEFAULT_RULES);
495

496
	git_attr_file__free(ign_internal);
497 498
	return error;
}
499 500 501 502

int git_ignore_path_is_ignored(
	int *ignored,
	git_repository *repo,
503
	const char *pathname)
504 505
{
	int error;
506 507
	const char *workdir;
	git_attr_path path;
508
	git_ignores ignores;
509 510
	unsigned int i;
	git_attr_file *file;
511

512
	assert(repo && ignored && pathname);
513

514
	workdir = git_repository_workdir(repo);
515

516 517
	memset(&path, 0, sizeof(path));
	memset(&ignores, 0, sizeof(ignores));
518

519
	if ((error = git_attr_path__init(&path, pathname, workdir, GIT_DIR_FLAG_UNKNOWN)) < 0 ||
520 521
		(error = git_ignore__for_path(repo, path.path, &ignores)) < 0)
		goto cleanup;
522

523 524
	while (1) {
		/* first process builtins - success means path was found */
525
		if (ignore_lookup_in_rules(ignored, ignores.ign_internal, &path))
526 527 528 529
			goto cleanup;

		/* next process files in the path */
		git_vector_foreach(&ignores.ign_path, i, file) {
530
			if (ignore_lookup_in_rules(ignored, file, &path))
531 532 533 534 535
				goto cleanup;
		}

		/* last process global ignores */
		git_vector_foreach(&ignores.ign_global, i, file) {
536
			if (ignore_lookup_in_rules(ignored, file, &path))
537 538 539
				goto cleanup;
		}

540 541
		/* move up one directory */
		if (path.basename == path.path)
542
			break;
543 544 545 546 547 548 549 550
		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)
551
			break;
552 553 554 555 556 557
	}

	*ignored = 0;

cleanup:
	git_attr_path__free(&path);
558 559 560 561
	git_ignore__free(&ignores);
	return error;
}

562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 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
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;
}