ignore.c 12.2 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 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
/**
 * 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;

	/* 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) {
		/* no chance of matching w/o a wilcard */
		if (!(rule->flags & GIT_ATTR_FNMATCH_HASWILD))
			continue;

	/*
	 * If we're dealing with a directory (which we know via the
	 * strchr() check) we want to use 'dirname/<star>' as the
	 * pattern so p_fnmatch() honours FNM_PATHNAME
	 */
		git_buf_clear(&buf);
		if (rule->containing_dir) {
			git_buf_puts(&buf, rule->containing_dir);
		}
		if (!strchr(rule->pattern, '*'))
			error = git_buf_printf(&buf, "%s/*", rule->pattern);
		else
			error = git_buf_puts(&buf, rule->pattern);

		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;
		}
	}

	*out = 0;
	error = 0;

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

88
static int parse_ignore_file(
89
	git_repository *repo, git_attr_file *attrs, const char *data)
90
{
91
	int error = 0;
92
	int ignore_case = false;
93 94
	const char *scan = data, *context = NULL;
	git_attr_fnmatch *match = NULL;
95

96
	if (git_repository__cvar(&ignore_case, repo, GIT_CVAR_IGNORECASE) < 0)
97
		giterr_clear();
98

99
	/* if subdir file path, convert context for file paths */
100 101 102 103
	if (attrs->entry &&
		git_path_root(attrs->entry->path) < 0 &&
		!git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE))
		context = attrs->entry->path;
104

105
	if (git_mutex_lock(&attrs->lock) < 0) {
106
		giterr_set(GITERR_OS, "Failed to lock ignore file");
107 108 109
		return -1;
	}

110
	while (!error && *scan) {
111 112
		int valid_rule = 1;

113 114 115
		if (!match && !(match = git__calloc(1, sizeof(*match)))) {
			error = -1;
			break;
116 117
		}

118
		match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
119

120
		if (!(error = git_attr_fnmatch__parse(
121
			match, &attrs->pool, context, &scan)))
122
		{
123 124 125 126 127
			match->flags |= GIT_ATTR_FNMATCH_IGNORE;

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

128
			scan = git__next_line(scan);
129 130 131 132 133 134 135

			/* 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);
136 137
		}

138
		if (error != 0 || !valid_rule) {
139 140 141
			match->pattern = NULL;

			if (error == GIT_ENOTFOUND)
142
				error = 0;
143 144 145 146 147
		} else {
			match = NULL; /* vector now "owns" the match */
		}
	}

148
	git_mutex_unlock(&attrs->lock);
149
	git__free(match);
150 151 152 153

	return error;
}

154 155 156 157 158 159 160 161 162
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;

163
	error = git_attr_cache__get(
164
		&file, ignores->repo, NULL, GIT_ATTR_FILE__FROM_FILE,
165
		base, filename, parse_ignore_file);
166 167 168 169 170 171 172
	if (error < 0)
		return error;

	if (file != NULL) {
		if ((error = git_vector_insert(which_list, file)) < 0)
			git_attr_file__free(file);
	}
173 174 175

	return error;
}
176

177
static int push_one_ignore(void *payload, const char *path)
178
{
179
	git_ignores *ign = payload;
180
	ign->depth++;
181
	return push_ignore_file(ign, &ign->ign_path, path, GIT_IGNORE_FILE);
182 183
}

184
static int get_internal_ignores(git_attr_file **out, git_repository *repo)
185 186 187
{
	int error;

188 189 190 191
	if ((error = git_attr_cache__init(repo)) < 0)
		return error;

	error = git_attr_cache__get(
192
		out, repo, NULL, GIT_ATTR_FILE__IN_MEMORY, NULL, GIT_IGNORE_INTERNAL, NULL);
193

194 195
	/* if internal rules list is empty, insert default rules */
	if (!error && !(*out)->rules.length)
196
		error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES);
197 198 199 200

	return error;
}

201 202 203 204
int git_ignore__for_path(
	git_repository *repo,
	const char *path,
	git_ignores *ignores)
205
{
206
	int error = 0;
207
	const char *workdir = git_repository_workdir(repo);
208

209
	assert(ignores && path);
210

211
	memset(ignores, 0, sizeof(*ignores));
212 213
	ignores->repo = repo;

214 215 216
	/* Read the ignore_case flag */
	if ((error = git_repository__cvar(
			&ignores->ignore_case, repo, GIT_CVAR_IGNORECASE)) < 0)
217 218
		goto cleanup;

219
	if ((error = git_attr_cache__init(repo)) < 0)
220 221
		goto cleanup;

222 223 224 225
	/* given a unrooted path in a non-bare repo, resolve it */
	if (workdir && git_path_root(path) < 0)
		error = git_path_find_dir(&ignores->dir, path, workdir);
	else
226
		error = git_buf_joinpath(&ignores->dir, path, "");
227
	if (error < 0)
228 229
		goto cleanup;

230 231 232
	if (workdir && !git__prefixcmp(ignores->dir.ptr, workdir))
		ignores->dir_root = strlen(workdir);

233
	/* set up internals */
234
	if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0)
235 236 237
		goto cleanup;

	/* load .gitignore up the path */
238 239
	if (workdir != NULL) {
		error = git_path_walk_up(
240
			&ignores->dir, workdir, push_one_ignore, ignores);
241 242 243
		if (error < 0)
			goto cleanup;
	}
244 245

	/* load .git/info/exclude */
246 247
	error = push_ignore_file(
		ignores, &ignores->ign_global,
248
		git_repository_path(repo), GIT_IGNORE_FILE_INREPO);
249
	if (error < 0)
250 251 252
		goto cleanup;

	/* load core.excludesfile */
253
	if (git_repository_attr_cache(repo)->cfg_excl_file != NULL)
254 255
		error = push_ignore_file(
			ignores, &ignores->ign_global, NULL,
256
			git_repository_attr_cache(repo)->cfg_excl_file);
257 258

cleanup:
259
	if (error < 0)
260
		git_ignore__free(ignores);
261

262 263 264 265 266
	return error;
}

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

270 271
	ign->depth++;

272
	return push_ignore_file(
273
		ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE);
274 275
}

276 277 278 279
int git_ignore__pop_dir(git_ignores *ign)
{
	if (ign->ign_path.length > 0) {
		git_attr_file *file = git_vector_last(&ign->ign_path);
280
		const char *start = file->entry->path, *end;
281

282 283
		/* - ign->dir looks something like "/home/user/a/b/" (or "a/b/c/d/")
		 * - file->path looks something like "a/b/.gitignore
284
		 *
285 286 287
		 * 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
288 289 290
		 * the file key matches the path we are about to pop.
		 */

291 292
		if ((end = strrchr(start, '/')) != NULL) {
			size_t dirlen = (end - start) + 1;
293 294
			const char *relpath = ign->dir.ptr + ign->dir_root;
			size_t pathlen = ign->dir.size - ign->dir_root;
295

296
			if (pathlen == dirlen && !memcmp(relpath, start, dirlen)) {
297 298 299
				git_vector_pop(&ign->ign_path);
				git_attr_file__free(file);
			}
300
		}
301
	}
302

303
	if (--ign->depth > 0) {
304
		git_buf_rtruncate_at_char(&ign->dir, '/');
305
		git_path_to_dir(&ign->dir);
306
	}
307

308
	return 0;
309 310
}

311
void git_ignore__free(git_ignores *ignores)
312
{
313 314 315
	unsigned int i;
	git_attr_file *file;

316
	git_attr_file__free(ignores->ign_internal);
317 318 319 320 321

	git_vector_foreach(&ignores->ign_path, i, file) {
		git_attr_file__free(file);
		ignores->ign_path.contents[i] = NULL;
	}
322
	git_vector_free(&ignores->ign_path);
323 324 325 326 327

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

330 331 332
	git_buf_free(&ignores->dir);
}

333
static bool ignore_lookup_in_rules(
334
	int *ignored, git_attr_file *file, git_attr_path *path)
335
{
336
	size_t j;
337 338
	git_attr_fnmatch *match;

339
	git_vector_rforeach(&file->rules, j, match) {
340
		if (git_attr_fnmatch__match(match, path)) {
341 342
			*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
				GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;
343
			return true;
344 345 346
		}
	}

347
	return false;
348 349
}

350
int git_ignore__lookup(
351
	int *out, git_ignores *ignores, const char *pathname)
352
{
353
	unsigned int i;
354 355 356
	git_attr_file *file;
	git_attr_path path;

357 358
	*out = GIT_IGNORE_NOTFOUND;

359 360 361
	if (git_attr_path__init(
		&path, pathname, git_repository_workdir(ignores->repo)) < 0)
		return -1;
362

363
	/* first process builtins - success means path was found */
364
	if (ignore_lookup_in_rules(out, ignores->ign_internal, &path))
365
		goto cleanup;
366

367 368
	/* next process files in the path */
	git_vector_foreach(&ignores->ign_path, i, file) {
369
		if (ignore_lookup_in_rules(out, file, &path))
370
			goto cleanup;
371 372
	}

373 374
	/* last process global ignores */
	git_vector_foreach(&ignores->ign_global, i, file) {
375
		if (ignore_lookup_in_rules(out, file, &path))
376
			goto cleanup;
377 378
	}

379 380
cleanup:
	git_attr_path__free(&path);
381
	return 0;
382
}
383

384
int git_ignore_add_rule(git_repository *repo, const char *rules)
385 386
{
	int error;
387
	git_attr_file *ign_internal = NULL;
388

389 390 391 392 393
	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);
394 395 396 397

	return error;
}

398
int git_ignore_clear_internal_rules(git_repository *repo)
399 400 401 402
{
	int error;
	git_attr_file *ign_internal;

403 404
	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
		return error;
405

406 407 408
	if (!(error = git_attr_file__clear_rules(ign_internal, true)))
		error = parse_ignore_file(
			repo, ign_internal, GIT_IGNORE_DEFAULT_RULES);
409

410
	git_attr_file__free(ign_internal);
411 412
	return error;
}
413 414 415 416

int git_ignore_path_is_ignored(
	int *ignored,
	git_repository *repo,
417
	const char *pathname)
418 419
{
	int error;
420 421
	const char *workdir;
	git_attr_path path;
422
	git_ignores ignores;
423 424
	unsigned int i;
	git_attr_file *file;
425

426 427 428 429
	assert(ignored && pathname);

	workdir = repo ? git_repository_workdir(repo) : NULL;

430 431
	memset(&path, 0, sizeof(path));
	memset(&ignores, 0, sizeof(ignores));
432

433 434 435
	if ((error = git_attr_path__init(&path, pathname, workdir)) < 0 ||
		(error = git_ignore__for_path(repo, path.path, &ignores)) < 0)
		goto cleanup;
436

437 438
	while (1) {
		/* first process builtins - success means path was found */
439
		if (ignore_lookup_in_rules(ignored, ignores.ign_internal, &path))
440 441 442 443
			goto cleanup;

		/* next process files in the path */
		git_vector_foreach(&ignores.ign_path, i, file) {
444
			if (ignore_lookup_in_rules(ignored, file, &path))
445 446 447 448 449
				goto cleanup;
		}

		/* last process global ignores */
		git_vector_foreach(&ignores.ign_global, i, file) {
450
			if (ignore_lookup_in_rules(ignored, file, &path))
451 452 453
				goto cleanup;
		}

454 455
		/* move up one directory */
		if (path.basename == path.path)
456
			break;
457 458 459 460 461 462 463 464
		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)
465
			break;
466 467 468 469 470 471
	}

	*ignored = 0;

cleanup:
	git_attr_path__free(&path);
472 473 474 475
	git_ignore__free(&ignores);
	return error;
}

476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532
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;
}