ignore.c 10.3 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 8 9

#define GIT_IGNORE_INTERNAL		"[internal]exclude"

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

12
static int parse_ignore_file(
13
	git_repository *repo, git_attr_file *attrs, const char *data)
14
{
15
	int error = 0;
16
	int ignore_case = false;
17 18
	const char *scan = data, *context = NULL;
	git_attr_fnmatch *match = NULL;
19

20
	if (git_repository__cvar(&ignore_case, repo, GIT_CVAR_IGNORECASE) < 0)
21
		giterr_clear();
22

23
	/* if subdir file path, convert context for file paths */
24 25 26 27
	if (attrs->entry &&
		git_path_root(attrs->entry->path) < 0 &&
		!git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE))
		context = attrs->entry->path;
28

29
	if (git_mutex_lock(&attrs->lock) < 0) {
30
		giterr_set(GITERR_OS, "Failed to lock ignore file");
31 32 33
		return -1;
	}

34
	while (!error && *scan) {
35 36 37
		if (!match && !(match = git__calloc(1, sizeof(*match)))) {
			error = -1;
			break;
38 39
		}

40
		match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
41

42
		if (!(error = git_attr_fnmatch__parse(
43
			match, &attrs->pool, context, &scan)))
44
		{
45 46 47 48 49
			match->flags |= GIT_ATTR_FNMATCH_IGNORE;

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

50
			scan = git__next_line(scan);
51
			error = git_vector_insert(&attrs->rules, match);
52 53
		}

54
		if (error != 0) {
55 56 57 58
			git__free(match->pattern);
			match->pattern = NULL;

			if (error == GIT_ENOTFOUND)
59
				error = 0;
60 61 62 63 64
		} else {
			match = NULL; /* vector now "owns" the match */
		}
	}

65
	git_mutex_unlock(&attrs->lock);
66
	git__free(match);
67 68 69 70

	return error;
}

71 72 73 74 75 76 77 78 79
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;

80
	error = git_attr_cache__get(
81 82
		&file, ignores->repo, GIT_ATTR_FILE__FROM_FILE,
		base, filename, parse_ignore_file);
83 84 85 86 87 88 89
	if (error < 0)
		return error;

	if (file != NULL) {
		if ((error = git_vector_insert(which_list, file)) < 0)
			git_attr_file__free(file);
	}
90 91 92

	return error;
}
93

94
static int push_one_ignore(void *payload, const char *path)
95
{
96
	git_ignores *ign = payload;
97
	ign->depth++;
98
	return push_ignore_file(ign, &ign->ign_path, path, GIT_IGNORE_FILE);
99 100
}

101
static int get_internal_ignores(git_attr_file **out, git_repository *repo)
102 103 104
{
	int error;

105 106 107 108
	if ((error = git_attr_cache__init(repo)) < 0)
		return error;

	error = git_attr_cache__get(
109
		out, repo, GIT_ATTR_FILE__IN_MEMORY, NULL, GIT_IGNORE_INTERNAL, NULL);
110

111 112
	/* if internal rules list is empty, insert default rules */
	if (!error && !(*out)->rules.length)
113
		error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES);
114 115 116 117

	return error;
}

118 119 120 121
int git_ignore__for_path(
	git_repository *repo,
	const char *path,
	git_ignores *ignores)
122
{
123
	int error = 0;
124
	const char *workdir = git_repository_workdir(repo);
125

126
	assert(ignores && path);
127

128
	memset(ignores, 0, sizeof(*ignores));
129 130
	ignores->repo = repo;

131 132 133
	/* Read the ignore_case flag */
	if ((error = git_repository__cvar(
			&ignores->ignore_case, repo, GIT_CVAR_IGNORECASE)) < 0)
134 135
		goto cleanup;

136
	if ((error = git_attr_cache__init(repo)) < 0)
137 138
		goto cleanup;

139 140 141 142
	/* 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
143
		error = git_buf_joinpath(&ignores->dir, path, "");
144
	if (error < 0)
145 146
		goto cleanup;

147 148 149
	if (workdir && !git__prefixcmp(ignores->dir.ptr, workdir))
		ignores->dir_root = strlen(workdir);

150
	/* set up internals */
151
	if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0)
152 153 154
		goto cleanup;

	/* load .gitignore up the path */
155 156
	if (workdir != NULL) {
		error = git_path_walk_up(
157
			&ignores->dir, workdir, push_one_ignore, ignores);
158 159 160
		if (error < 0)
			goto cleanup;
	}
161 162

	/* load .git/info/exclude */
163 164
	error = push_ignore_file(
		ignores, &ignores->ign_global,
165
		git_repository_path(repo), GIT_IGNORE_FILE_INREPO);
166
	if (error < 0)
167 168 169
		goto cleanup;

	/* load core.excludesfile */
170
	if (git_repository_attr_cache(repo)->cfg_excl_file != NULL)
171 172
		error = push_ignore_file(
			ignores, &ignores->ign_global, NULL,
173
			git_repository_attr_cache(repo)->cfg_excl_file);
174 175

cleanup:
176
	if (error < 0)
177
		git_ignore__free(ignores);
178

179 180 181 182 183
	return error;
}

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

187 188
	ign->depth++;

189
	return push_ignore_file(
190
		ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE);
191 192
}

193 194 195 196
int git_ignore__pop_dir(git_ignores *ign)
{
	if (ign->ign_path.length > 0) {
		git_attr_file *file = git_vector_last(&ign->ign_path);
197
		const char *start = file->entry->path, *end;
198

199 200
		/* - ign->dir looks something like "/home/user/a/b/" (or "a/b/c/d/")
		 * - file->path looks something like "a/b/.gitignore
201
		 *
202 203 204
		 * 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
205 206 207
		 * the file key matches the path we are about to pop.
		 */

208 209
		if ((end = strrchr(start, '/')) != NULL) {
			size_t dirlen = (end - start) + 1;
210 211
			const char *relpath = ign->dir.ptr + ign->dir_root;
			size_t pathlen = ign->dir.size - ign->dir_root;
212

213
			if (pathlen == dirlen && !memcmp(relpath, start, dirlen)) {
214 215 216
				git_vector_pop(&ign->ign_path);
				git_attr_file__free(file);
			}
217
		}
218
	}
219

220
	if (--ign->depth > 0) {
221
		git_buf_rtruncate_at_char(&ign->dir, '/');
222
		git_path_to_dir(&ign->dir);
223
	}
224

225
	return 0;
226 227
}

228
void git_ignore__free(git_ignores *ignores)
229
{
230 231 232
	unsigned int i;
	git_attr_file *file;

233
	git_attr_file__free(ignores->ign_internal);
234 235 236 237 238

	git_vector_foreach(&ignores->ign_path, i, file) {
		git_attr_file__free(file);
		ignores->ign_path.contents[i] = NULL;
	}
239
	git_vector_free(&ignores->ign_path);
240 241 242 243 244

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

247 248 249
	git_buf_free(&ignores->dir);
}

250
static bool ignore_lookup_in_rules(
251
	int *ignored, git_attr_file *file, git_attr_path *path)
252
{
253
	size_t j;
254 255
	git_attr_fnmatch *match;

256
	git_vector_rforeach(&file->rules, j, match) {
257
		if (git_attr_fnmatch__match(match, path)) {
258 259
			*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
				GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;
260
			return true;
261 262 263
		}
	}

264
	return false;
265 266
}

267
int git_ignore__lookup(
268
	int *out, git_ignores *ignores, const char *pathname)
269
{
270
	unsigned int i;
271 272 273
	git_attr_file *file;
	git_attr_path path;

274 275
	*out = GIT_IGNORE_NOTFOUND;

276 277 278
	if (git_attr_path__init(
		&path, pathname, git_repository_workdir(ignores->repo)) < 0)
		return -1;
279

280
	/* first process builtins - success means path was found */
281
	if (ignore_lookup_in_rules(out, ignores->ign_internal, &path))
282
		goto cleanup;
283

284 285
	/* next process files in the path */
	git_vector_foreach(&ignores->ign_path, i, file) {
286
		if (ignore_lookup_in_rules(out, file, &path))
287
			goto cleanup;
288 289
	}

290 291
	/* last process global ignores */
	git_vector_foreach(&ignores->ign_global, i, file) {
292
		if (ignore_lookup_in_rules(out, file, &path))
293
			goto cleanup;
294 295
	}

296 297
cleanup:
	git_attr_path__free(&path);
298
	return 0;
299
}
300

301
int git_ignore_add_rule(git_repository *repo, const char *rules)
302 303
{
	int error;
304
	git_attr_file *ign_internal = NULL;
305

306 307 308 309 310
	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);
311 312 313 314

	return error;
}

315
int git_ignore_clear_internal_rules(git_repository *repo)
316 317 318 319
{
	int error;
	git_attr_file *ign_internal;

320 321
	if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
		return error;
322

323 324 325
	if (!(error = git_attr_file__clear_rules(ign_internal, true)))
		error = parse_ignore_file(
			repo, ign_internal, GIT_IGNORE_DEFAULT_RULES);
326

327
	git_attr_file__free(ign_internal);
328 329
	return error;
}
330 331 332 333

int git_ignore_path_is_ignored(
	int *ignored,
	git_repository *repo,
334
	const char *pathname)
335 336
{
	int error;
337 338
	const char *workdir;
	git_attr_path path;
339
	git_ignores ignores;
340 341
	unsigned int i;
	git_attr_file *file;
342

343 344 345 346
	assert(ignored && pathname);

	workdir = repo ? git_repository_workdir(repo) : NULL;

347 348
	memset(&path, 0, sizeof(path));
	memset(&ignores, 0, sizeof(ignores));
349

350 351 352
	if ((error = git_attr_path__init(&path, pathname, workdir)) < 0 ||
		(error = git_ignore__for_path(repo, path.path, &ignores)) < 0)
		goto cleanup;
353

354 355
	while (1) {
		/* first process builtins - success means path was found */
356
		if (ignore_lookup_in_rules(ignored, ignores.ign_internal, &path))
357 358 359 360
			goto cleanup;

		/* next process files in the path */
		git_vector_foreach(&ignores.ign_path, i, file) {
361
			if (ignore_lookup_in_rules(ignored, file, &path))
362 363 364 365 366
				goto cleanup;
		}

		/* last process global ignores */
		git_vector_foreach(&ignores.ign_global, i, file) {
367
			if (ignore_lookup_in_rules(ignored, file, &path))
368 369 370
				goto cleanup;
		}

371 372
		/* move up one directory */
		if (path.basename == path.path)
373
			break;
374 375 376 377 378 379 380 381
		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)
382
			break;
383 384 385 386 387 388
	}

	*ignored = 0;

cleanup:
	git_attr_path__free(&path);
389 390 391 392
	git_ignore__free(&ignores);
	return error;
}

393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
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;
}