ignore.c 15.2 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 "wildmatch.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
	if (neg->flags & GIT_ATTR_FNMATCH_ICASE)
		cmp = git__strncasecmp;
	else
62
		cmp = git__strncmp;
63

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, wildmatch_flags;
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
	wildmatch_flags = WM_PATHNAME;
113
	if (match->flags & GIT_ATTR_FNMATCH_ICASE)
114
		wildmatch_flags |= WM_CASEFOLD;
115

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

		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 143 144
			goto out;

		/* if we found a match, we want to keep this rule */
145
		if ((wildmatch(git_buf_cstr(&buf), path, wildmatch_flags)) == WM_MATCH) {
146 147 148 149 150 151 152 153 154 155
			*out = 1;
			error = 0;
			goto out;
		}
	}

	error = 0;

out:
	git__free(path);
156
	git_buf_dispose(&buf);
157 158 159
	return error;
}

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 166
	const char *scan = data, *context = NULL;
	git_attr_fnmatch *match = NULL;
167

168 169
	GIT_UNUSED(allow_macros);

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 175 176 177
	if (attrs->entry &&
		git_path_root(attrs->entry->path) < 0 &&
		!git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE))
		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 182 183
		return -1;
	}

184
	while (!error && *scan) {
185 186
		int valid_rule = 1;

187 188 189
		if (!match && !(match = git__calloc(1, sizeof(*match)))) {
			error = -1;
			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 199 200 201 202
			match->flags |= GIT_ATTR_FNMATCH_IGNORE;

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

203
			scan = git__next_line(scan);
204

205 206 207 208 209 210 211 212
			/*
			 * 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))
213 214 215 216
				error = does_negate_rule(&valid_rule, &attrs->rules, match);

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

219
		if (error != 0 || !valid_rule) {
220 221 222
			match->pattern = NULL;

			if (error == GIT_ENOTFOUND)
223
				error = 0;
224 225 226 227 228
		} else {
			match = NULL; /* vector now "owns" the match */
		}
	}

229
	git_mutex_unlock(&attrs->lock);
230
	git__free(match);
231 232 233 234

	return error;
}

235 236 237 238 239 240 241 242 243
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;

244 245
	error = git_attr_cache__get(&file, ignores->repo, NULL, GIT_ATTR_FILE__FROM_FILE,
				    base, filename, parse_ignore_file, false);
246 247 248 249 250 251 252
	if (error < 0)
		return error;

	if (file != NULL) {
		if ((error = git_vector_insert(which_list, file)) < 0)
			git_attr_file__free(file);
	}
253 254 255

	return error;
}
256

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
}

264
static int get_internal_ignores(git_attr_file **out, git_repository *repo)
265 266 267
{
	int error;

268 269 270
	if ((error = git_attr_cache__init(repo)) < 0)
		return error;

271 272
	error = git_attr_cache__get(out, repo, NULL, GIT_ATTR_FILE__IN_MEMORY, NULL,
				    GIT_IGNORE_INTERNAL, NULL, false);
273

274 275
	/* if internal rules list is empty, insert default rules */
	if (!error && !(*out)->rules.length)
276
		error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES, false);
277 278 279 280

	return error;
}

281 282 283 284
int git_ignore__for_path(
	git_repository *repo,
	const char *path,
	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 294
	ignores->repo = repo;

295
	/* Read the ignore_case flag */
296 297
	if ((error = git_repository__configmap_lookup(
			&ignores->ignore_case, repo, GIT_CONFIGMAP_IGNORECASE)) < 0)
298 299
		goto cleanup;

300
	if ((error = git_attr_cache__init(repo)) < 0)
301 302
		goto cleanup;

303
	/* given a unrooted path in a non-bare repo, resolve it */
304 305 306 307 308 309 310 311
	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 */
312
		git_buf_dispose(&local);
313
	} else {
314
		error = git_buf_joinpath(&ignores->dir, path, "");
315
	}
316
	if (error < 0)
317 318
		goto cleanup;

319 320 321
	if (workdir && !git__prefixcmp(ignores->dir.ptr, workdir))
		ignores->dir_root = strlen(workdir);

322
	/* set up internals */
323
	if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0)
324 325 326
		goto cleanup;

	/* load .gitignore up the path */
327 328
	if (workdir != NULL) {
		error = git_path_walk_up(
329
			&ignores->dir, workdir, push_one_ignore, ignores);
330 331 332
		if (error < 0)
			goto cleanup;
	}
333

334 335 336 337 338 339 340
	/* load .git/info/exclude if possible */
	if ((error = git_repository_item_path(&infopath, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 ||
		(error = push_ignore_file(ignores, &ignores->ign_global, infopath.ptr, GIT_IGNORE_FILE_INREPO)) < 0) {
		if (error != GIT_ENOTFOUND)
			goto cleanup;
		error = 0;
	}
341 342

	/* load core.excludesfile */
343
	if (git_repository_attr_cache(repo)->cfg_excl_file != NULL)
344 345
		error = push_ignore_file(
			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 354 355 356 357
	return error;
}

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

361 362
	ign->depth++;

363
	return push_ignore_file(
364
		ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE);
365 366
}

367 368 369 370
int git_ignore__pop_dir(git_ignores *ign)
{
	if (ign->ign_path.length > 0) {
		git_attr_file *file = git_vector_last(&ign->ign_path);
371
		const char *start = file->entry->path, *end;
372

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

382 383
		if ((end = strrchr(start, '/')) != NULL) {
			size_t dirlen = (end - start) + 1;
384 385
			const char *relpath = ign->dir.ptr + ign->dir_root;
			size_t pathlen = ign->dir.size - ign->dir_root;
386

387
			if (pathlen == dirlen && !memcmp(relpath, start, dirlen)) {
388 389 390
				git_vector_pop(&ign->ign_path);
				git_attr_file__free(file);
			}
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
}

402
void git_ignore__free(git_ignores *ignores)
403
{
404 405 406
	unsigned int i;
	git_attr_file *file;

407
	git_attr_file__free(ignores->ign_internal);
408 409 410 411 412

	git_vector_foreach(&ignores->ign_path, i, file) {
		git_attr_file__free(file);
		ignores->ign_path.contents[i] = NULL;
	}
413
	git_vector_free(&ignores->ign_path);
414 415 416 417 418

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

421
	git_buf_dispose(&ignores->dir);
422 423
}

424
static bool ignore_lookup_in_rules(
425
	int *ignored, git_attr_file *file, git_attr_path *path)
426
{
427
	size_t j;
428 429
	git_attr_fnmatch *match;

430
	git_vector_rforeach(&file->rules, j, match) {
431 432 433
		if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY &&
		    path->is_dir == GIT_DIR_FLAG_FALSE)
			continue;
434
		if (git_attr_fnmatch__match(match, path)) {
435 436
			*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
				GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;
437
			return true;
438 439 440
		}
	}

441
	return false;
442 443
}

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 449 450
	git_attr_file *file;
	git_attr_path path;

451 452
	*out = GIT_IGNORE_NOTFOUND;

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 462 463 464 465
	/* next process files in the path.
	 * this process has to process ignores in reverse order
	 * to ensure correct prioritization of rules
	 */
	git_vector_rforeach(&ignores->ign_path, i, file) {
466
		if (ignore_lookup_in_rules(out, file, &path))
467
			goto cleanup;
468 469
	}

470 471
	/* last process global ignores */
	git_vector_foreach(&ignores->ign_global, i, file) {
472
		if (ignore_lookup_in_rules(out, file, &path))
473
			goto cleanup;
474 475
	}

476 477
cleanup:
	git_attr_path__free(&path);
478
	return 0;
479
}
480

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 487 488
	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
		return error;

489
	error = parse_ignore_file(repo, ign_internal, rules, false);
490
	git_attr_file__free(ign_internal);
491 492 493 494

	return error;
}

495
int git_ignore_clear_internal_rules(git_repository *repo)
496 497 498 499
{
	int error;
	git_attr_file *ign_internal;

500 501
	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
		return error;
502

503 504
	if (!(error = git_attr_file__clear_rules(ign_internal, true)))
		error = parse_ignore_file(
505
				repo, ign_internal, GIT_IGNORE_DEFAULT_RULES, false);
506

507
	git_attr_file__free(ign_internal);
508 509
	return error;
}
510 511 512 513

int git_ignore_path_is_ignored(
	int *ignored,
	git_repository *repo,
514
	const char *pathname)
515 516
{
	int error;
517 518
	const char *workdir;
	git_attr_path path;
519
	git_ignores ignores;
520 521
	unsigned int i;
	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 529
	memset(&path, 0, sizeof(path));
	memset(&ignores, 0, sizeof(ignores));
530

531 532 533
	if (!git__suffixcmp(pathname, "/"))
		dir_flag = GIT_DIR_FLAG_TRUE;
	else if (git_repository_is_bare(repo))
534 535 536
		dir_flag = GIT_DIR_FLAG_FALSE;

	if ((error = git_attr_path__init(&path, pathname, workdir, dir_flag)) < 0 ||
537 538
		(error = git_ignore__for_path(repo, path.path, &ignores)) < 0)
		goto cleanup;
539

540 541
	while (1) {
		/* first process builtins - success means path was found */
542
		if (ignore_lookup_in_rules(ignored, ignores.ign_internal, &path))
543 544 545 546
			goto cleanup;

		/* next process files in the path */
		git_vector_foreach(&ignores.ign_path, i, file) {
547
			if (ignore_lookup_in_rules(ignored, file, &path))
548 549 550 551 552
				goto cleanup;
		}

		/* last process global ignores */
		git_vector_foreach(&ignores.ign_global, i, file) {
553
			if (ignore_lookup_in_rules(ignored, file, &path))
554 555 556
				goto cleanup;
		}

557 558
		/* move up one directory */
		if (path.basename == path.path)
559
			break;
560 561 562 563 564 565 566 567
		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)
568
			break;
569 570 571 572 573 574
	}

	*ignored = 0;

cleanup:
	git_attr_path__free(&path);
575 576 577 578
	git_ignore__free(&ignores);
	return error;
}

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 619 620 621 622
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) {
623
			git_error_set(GIT_ERROR_INVALID, "pathspec contains ignored file '%s'",
624 625 626 627 628 629 630
				filename);
			error = GIT_EINVALIDSPEC;
			break;
		}
	}

	git_index_free(idx);
631
	git_buf_dispose(&path);
632 633 634 635

	return error;
}