ignore.c 15 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
	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, 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

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

144
		if ((error = p_fnmatch(git_buf_cstr(&buf), path, fnflags)) < 0) {
145
			git_error_set(GIT_ERROR_INVALID, "error matching pattern");
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
			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);
161
	git_buf_dispose(&buf);
162 163 164
	return error;
}

165
static int parse_ignore_file(
166
	git_repository *repo, git_attr_file *attrs, const char *data)
167
{
168
	int error = 0;
169
	int ignore_case = false;
170 171
	const char *scan = data, *context = NULL;
	git_attr_fnmatch *match = NULL;
172

173
	if (git_repository__cvar(&ignore_case, repo, GIT_CVAR_IGNORECASE) < 0)
174
		git_error_clear();
175

176
	/* if subdir file path, convert context for file paths */
177 178 179 180
	if (attrs->entry &&
		git_path_root(attrs->entry->path) < 0 &&
		!git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE))
		context = attrs->entry->path;
181

182
	if (git_mutex_lock(&attrs->lock) < 0) {
183
		git_error_set(GIT_ERROR_OS, "failed to lock ignore file");
184 185 186
		return -1;
	}

187
	while (!error && *scan) {
188 189
		int valid_rule = 1;

190 191 192
		if (!match && !(match = git__calloc(1, sizeof(*match)))) {
			error = -1;
			break;
193 194
		}

195 196 197 198
		match->flags =
		    GIT_ATTR_FNMATCH_ALLOWSPACE |
		    GIT_ATTR_FNMATCH_ALLOWNEG |
		    GIT_ATTR_FNMATCH_NOLEADINGDIR;
199

200
		if (!(error = git_attr_fnmatch__parse(
201
			match, &attrs->pool, context, &scan)))
202
		{
203 204 205 206 207
			match->flags |= GIT_ATTR_FNMATCH_IGNORE;

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

208
			scan = git__next_line(scan);
209

210 211 212 213 214 215 216 217
			/*
			 * 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))
218 219 220 221
				error = does_negate_rule(&valid_rule, &attrs->rules, match);

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

224
		if (error != 0 || !valid_rule) {
225 226 227
			match->pattern = NULL;

			if (error == GIT_ENOTFOUND)
228
				error = 0;
229 230 231 232 233
		} else {
			match = NULL; /* vector now "owns" the match */
		}
	}

234
	git_mutex_unlock(&attrs->lock);
235
	git__free(match);
236 237 238 239

	return error;
}

240 241 242 243 244 245 246 247 248
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;

249
	error = git_attr_cache__get(
250
		&file, ignores->repo, NULL, GIT_ATTR_FILE__FROM_FILE,
251
		base, filename, parse_ignore_file);
252 253 254 255 256 257 258
	if (error < 0)
		return error;

	if (file != NULL) {
		if ((error = git_vector_insert(which_list, file)) < 0)
			git_attr_file__free(file);
	}
259 260 261

	return error;
}
262

263
static int push_one_ignore(void *payload, const char *path)
264
{
265
	git_ignores *ign = payload;
266
	ign->depth++;
267
	return push_ignore_file(ign, &ign->ign_path, path, GIT_IGNORE_FILE);
268 269
}

270
static int get_internal_ignores(git_attr_file **out, git_repository *repo)
271 272 273
{
	int error;

274 275 276 277
	if ((error = git_attr_cache__init(repo)) < 0)
		return error;

	error = git_attr_cache__get(
278
		out, repo, NULL, GIT_ATTR_FILE__IN_MEMORY, NULL, GIT_IGNORE_INTERNAL, NULL);
279

280 281
	/* if internal rules list is empty, insert default rules */
	if (!error && !(*out)->rules.length)
282
		error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES);
283 284 285 286

	return error;
}

287 288 289 290
int git_ignore__for_path(
	git_repository *repo,
	const char *path,
	git_ignores *ignores)
291
{
292
	int error = 0;
293
	const char *workdir = git_repository_workdir(repo);
294
	git_buf infopath = GIT_BUF_INIT;
295

296
	assert(repo && ignores && path);
297

298
	memset(ignores, 0, sizeof(*ignores));
299 300
	ignores->repo = repo;

301 302 303
	/* Read the ignore_case flag */
	if ((error = git_repository__cvar(
			&ignores->ignore_case, repo, GIT_CVAR_IGNORECASE)) < 0)
304 305
		goto cleanup;

306
	if ((error = git_attr_cache__init(repo)) < 0)
307 308
		goto cleanup;

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

325 326 327
	if (workdir && !git__prefixcmp(ignores->dir.ptr, workdir))
		ignores->dir_root = strlen(workdir);

328
	/* set up internals */
329
	if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0)
330 331 332
		goto cleanup;

	/* load .gitignore up the path */
333 334
	if (workdir != NULL) {
		error = git_path_walk_up(
335
			&ignores->dir, workdir, push_one_ignore, ignores);
336 337 338
		if (error < 0)
			goto cleanup;
	}
339

340 341 342 343
	if ((error = git_repository_item_path(&infopath,
			repo, GIT_REPOSITORY_ITEM_INFO)) < 0)
		goto cleanup;

344
	/* load .git/info/exclude */
345 346
	error = push_ignore_file(
		ignores, &ignores->ign_global,
347
		infopath.ptr, GIT_IGNORE_FILE_INREPO);
348
	if (error < 0)
349 350 351
		goto cleanup;

	/* load core.excludesfile */
352
	if (git_repository_attr_cache(repo)->cfg_excl_file != NULL)
353 354
		error = push_ignore_file(
			ignores, &ignores->ign_global, NULL,
355
			git_repository_attr_cache(repo)->cfg_excl_file);
356 357

cleanup:
358
	git_buf_dispose(&infopath);
359
	if (error < 0)
360
		git_ignore__free(ignores);
361

362 363 364 365 366
	return error;
}

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

370 371
	ign->depth++;

372
	return push_ignore_file(
373
		ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE);
374 375
}

376 377 378 379
int git_ignore__pop_dir(git_ignores *ign)
{
	if (ign->ign_path.length > 0) {
		git_attr_file *file = git_vector_last(&ign->ign_path);
380
		const char *start = file->entry->path, *end;
381

382 383
		/* - ign->dir looks something like "/home/user/a/b/" (or "a/b/c/d/")
		 * - file->path looks something like "a/b/.gitignore
384
		 *
385 386 387
		 * 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
388 389 390
		 * the file key matches the path we are about to pop.
		 */

391 392
		if ((end = strrchr(start, '/')) != NULL) {
			size_t dirlen = (end - start) + 1;
393 394
			const char *relpath = ign->dir.ptr + ign->dir_root;
			size_t pathlen = ign->dir.size - ign->dir_root;
395

396
			if (pathlen == dirlen && !memcmp(relpath, start, dirlen)) {
397 398 399
				git_vector_pop(&ign->ign_path);
				git_attr_file__free(file);
			}
400
		}
401
	}
402

403
	if (--ign->depth > 0) {
404
		git_buf_rtruncate_at_char(&ign->dir, '/');
405
		git_path_to_dir(&ign->dir);
406
	}
407

408
	return 0;
409 410
}

411
void git_ignore__free(git_ignores *ignores)
412
{
413 414 415
	unsigned int i;
	git_attr_file *file;

416
	git_attr_file__free(ignores->ign_internal);
417 418 419 420 421

	git_vector_foreach(&ignores->ign_path, i, file) {
		git_attr_file__free(file);
		ignores->ign_path.contents[i] = NULL;
	}
422
	git_vector_free(&ignores->ign_path);
423 424 425 426 427

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

430
	git_buf_dispose(&ignores->dir);
431 432
}

433
static bool ignore_lookup_in_rules(
434
	int *ignored, git_attr_file *file, git_attr_path *path)
435
{
436
	size_t j;
437 438
	git_attr_fnmatch *match;

439
	git_vector_rforeach(&file->rules, j, match) {
440 441 442
		if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY &&
		    path->is_dir == GIT_DIR_FLAG_FALSE)
			continue;
443
		if (git_attr_fnmatch__match(match, path)) {
444 445
			*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
				GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;
446
			return true;
447 448 449
		}
	}

450
	return false;
451 452
}

453
int git_ignore__lookup(
454
	int *out, git_ignores *ignores, const char *pathname, git_dir_flag dir_flag)
455
{
456
	unsigned int i;
457 458 459
	git_attr_file *file;
	git_attr_path path;

460 461
	*out = GIT_IGNORE_NOTFOUND;

462
	if (git_attr_path__init(
463
		&path, pathname, git_repository_workdir(ignores->repo), dir_flag) < 0)
464
		return -1;
465

466
	/* first process builtins - success means path was found */
467
	if (ignore_lookup_in_rules(out, ignores->ign_internal, &path))
468
		goto cleanup;
469

470 471
	/* next process files in the path */
	git_vector_foreach(&ignores->ign_path, i, file) {
472
		if (ignore_lookup_in_rules(out, file, &path))
473
			goto cleanup;
474 475
	}

476 477
	/* last process global ignores */
	git_vector_foreach(&ignores->ign_global, i, file) {
478
		if (ignore_lookup_in_rules(out, file, &path))
479
			goto cleanup;
480 481
	}

482 483
cleanup:
	git_attr_path__free(&path);
484
	return 0;
485
}
486

487
int git_ignore_add_rule(git_repository *repo, const char *rules)
488 489
{
	int error;
490
	git_attr_file *ign_internal = NULL;
491

492 493 494 495 496
	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);
497 498 499 500

	return error;
}

501
int git_ignore_clear_internal_rules(git_repository *repo)
502 503 504 505
{
	int error;
	git_attr_file *ign_internal;

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

509 510 511
	if (!(error = git_attr_file__clear_rules(ign_internal, true)))
		error = parse_ignore_file(
			repo, ign_internal, GIT_IGNORE_DEFAULT_RULES);
512

513
	git_attr_file__free(ign_internal);
514 515
	return error;
}
516 517 518 519

int git_ignore_path_is_ignored(
	int *ignored,
	git_repository *repo,
520
	const char *pathname)
521 522
{
	int error;
523 524
	const char *workdir;
	git_attr_path path;
525
	git_ignores ignores;
526 527
	unsigned int i;
	git_attr_file *file;
528
	git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
529

530
	assert(repo && ignored && pathname);
531

532
	workdir = git_repository_workdir(repo);
533

534 535
	memset(&path, 0, sizeof(path));
	memset(&ignores, 0, sizeof(ignores));
536

537 538 539
	if (!git__suffixcmp(pathname, "/"))
		dir_flag = GIT_DIR_FLAG_TRUE;
	else if (git_repository_is_bare(repo))
540 541 542
		dir_flag = GIT_DIR_FLAG_FALSE;

	if ((error = git_attr_path__init(&path, pathname, workdir, dir_flag)) < 0 ||
543 544
		(error = git_ignore__for_path(repo, path.path, &ignores)) < 0)
		goto cleanup;
545

546 547
	while (1) {
		/* first process builtins - success means path was found */
548
		if (ignore_lookup_in_rules(ignored, ignores.ign_internal, &path))
549 550 551 552
			goto cleanup;

		/* next process files in the path */
		git_vector_foreach(&ignores.ign_path, i, file) {
553
			if (ignore_lookup_in_rules(ignored, file, &path))
554 555 556 557 558
				goto cleanup;
		}

		/* last process global ignores */
		git_vector_foreach(&ignores.ign_global, i, file) {
559
			if (ignore_lookup_in_rules(ignored, file, &path))
560 561 562
				goto cleanup;
		}

563 564
		/* move up one directory */
		if (path.basename == path.path)
565
			break;
566 567 568 569 570 571 572 573
		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)
574
			break;
575 576 577 578 579 580
	}

	*ignored = 0;

cleanup:
	git_attr_path__free(&path);
581 582 583 584
	git_ignore__free(&ignores);
	return error;
}

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 623 624 625 626 627 628
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) {
629
			git_error_set(GIT_ERROR_INVALID, "pathspec contains ignored file '%s'",
630 631 632 633 634 635 636
				filename);
			error = GIT_EINVALIDSPEC;
			break;
		}
	}

	git_index_free(idx);
637
	git_buf_dispose(&path);
638 639 640 641

	return error;
}