ignore.c 10 KB
Newer Older
1
#include "git2/ignore.h"
2
#include "common.h"
3
#include "ignore.h"
4
#include "attr.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, void *parsedata, const char *buffer, git_attr_file *ignores)
14
{
15
	int error = 0;
16 17
	git_attr_fnmatch *match = NULL;
	const char *scan = NULL;
18
	char *context = NULL;
19
	int ignore_case = false;
20

21 22 23 24 25 26
	/* Prefer to have the caller pass in a git_ignores as the parsedata
	 * object.  If they did not, then look up the value of ignore_case */
	if (parsedata != NULL)
		ignore_case = ((git_ignores *)parsedata)->ignore_case;
	else if (git_repository__cvar(&ignore_case, repo, GIT_CVAR_IGNORECASE) < 0)
		return error;
27

28 29 30
	if (ignores->key && git__suffixcmp(ignores->key, "/" GIT_IGNORE_FILE) == 0) {
		context = ignores->key + 2;
		context[strlen(context) - strlen(GIT_IGNORE_FILE)] = '\0';
31
	}
32

33
	scan = buffer;
34

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

41
		match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
42

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

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

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

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

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

66
	git__free(match);
67 68 69
	/* restore file path used for context */
	if (context)
		context[strlen(context)] = '.'; /* first char of GIT_IGNORE_FILE */
70 71 72 73

	return error;
}

74 75
#define push_ignore_file(R,IGN,S,B,F) \
	git_attr_cache__push_file((R),(B),(F),GIT_ATTR_FILE_FROM_FILE,parse_ignore_file,(IGN),(S))
76

77
static int push_one_ignore(void *payload, git_buf *path)
78
{
79
	git_ignores *ign = payload;
80

81 82
	return push_ignore_file(
		ign->repo, ign, &ign->ign_path, path->ptr, GIT_IGNORE_FILE);
83 84
}

85 86 87 88 89 90 91 92 93 94 95 96 97
static int get_internal_ignores(git_attr_file **ign, git_repository *repo)
{
	int error;

	if (!(error = git_attr_cache__init(repo)))
		error = git_attr_cache__internal_file(repo, GIT_IGNORE_INTERNAL, ign);

	if (!error && !(*ign)->rules.length)
		error = parse_ignore_file(repo, NULL, GIT_IGNORE_DEFAULT_RULES, *ign);

	return error;
}

98 99 100 101
int git_ignore__for_path(
	git_repository *repo,
	const char *path,
	git_ignores *ignores)
102
{
103
	int error = 0;
104
	const char *workdir = git_repository_workdir(repo);
105 106

	assert(ignores);
107

108 109 110 111
	ignores->repo = repo;
	git_buf_init(&ignores->dir, 0);
	ignores->ign_internal = NULL;

112 113 114
	/* Read the ignore_case flag */
	if ((error = git_repository__cvar(
			&ignores->ignore_case, repo, GIT_CVAR_IGNORECASE)) < 0)
115 116
		goto cleanup;

117 118 119
	if ((error = git_vector_init(&ignores->ign_path, 8, NULL)) < 0 ||
		(error = git_vector_init(&ignores->ign_global, 2, NULL)) < 0 ||
		(error = git_attr_cache__init(repo)) < 0)
120 121
		goto cleanup;

122 123 124 125 126 127
	/* 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
		error = git_buf_sets(&ignores->dir, path);
	if (error < 0)
128 129
		goto cleanup;

130
	/* set up internals */
131
	error = get_internal_ignores(&ignores->ign_internal, repo);
132
	if (error < 0)
133 134 135
		goto cleanup;

	/* load .gitignore up the path */
136 137
	if (workdir != NULL) {
		error = git_path_walk_up(
138
			&ignores->dir, workdir, push_one_ignore, ignores);
139 140 141
		if (error < 0)
			goto cleanup;
	}
142 143

	/* load .git/info/exclude */
144
	error = push_ignore_file(repo, ignores, &ignores->ign_global,
145
		git_repository_path(repo), GIT_IGNORE_FILE_INREPO);
146
	if (error < 0)
147 148 149
		goto cleanup;

	/* load core.excludesfile */
150
	if (git_repository_attr_cache(repo)->cfg_excl_file != NULL)
151
		error = push_ignore_file(repo, ignores, &ignores->ign_global, NULL,
152
			git_repository_attr_cache(repo)->cfg_excl_file);
153 154

cleanup:
155
	if (error < 0)
156
		git_ignore__free(ignores);
157

158 159 160 161 162
	return error;
}

int git_ignore__push_dir(git_ignores *ign, const char *dir)
{
163 164
	if (git_buf_joinpath(&ign->dir, ign->dir.ptr, dir) < 0)
		return -1;
165 166 167

	return push_ignore_file(
		ign->repo, ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE);
168 169
}

170 171 172 173
int git_ignore__pop_dir(git_ignores *ign)
{
	if (ign->ign_path.length > 0) {
		git_attr_file *file = git_vector_last(&ign->ign_path);
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
		const char *start, *end, *scan;
		size_t keylen;

		/* - ign->dir looks something like "a/b" (or "a/b/c/d")
		 * - file->key looks something like "0#a/b/.gitignore
		 *
		 * We are popping the last directory off ign->dir.  We also want to
		 * remove the file from the vector if the directory part of the key
		 * matches the ign->dir path.  We need to test if the "a/b" part of
		 * the file key matches the path we are about to pop.
		 */

		for (start = end = scan = &file->key[2]; *scan; ++scan)
			if (*scan == '/')
				end = scan; /* point 'end' to last '/' in key */
		keylen = (end - start) + 1;

		if (ign->dir.size >= keylen &&
			!memcmp(ign->dir.ptr + ign->dir.size - keylen, start, keylen))
193
			git_vector_pop(&ign->ign_path);
194

195 196
		git_buf_rtruncate_at_char(&ign->dir, '/');
	}
197
	return 0;
198 199
}

200
void git_ignore__free(git_ignores *ignores)
201
{
202 203 204 205 206 207
	/* don't need to free ignores->ign_internal since it is in cache */
	git_vector_free(&ignores->ign_path);
	git_vector_free(&ignores->ign_global);
	git_buf_free(&ignores->dir);
}

208
static bool ignore_lookup_in_rules(
209 210
	git_vector *rules, git_attr_path *path, int *ignored)
{
211
	size_t j;
212 213 214
	git_attr_fnmatch *match;

	git_vector_rforeach(rules, j, match) {
215
		if (git_attr_fnmatch__match(match, path)) {
216
			*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0);
217
			return true;
218 219 220
		}
	}

221
	return false;
222 223
}

224 225
int git_ignore__lookup(
	git_ignores *ignores, const char *pathname, int *ignored)
226
{
227
	unsigned int i;
228 229 230
	git_attr_file *file;
	git_attr_path path;

231 232 233
	if (git_attr_path__init(
		&path, pathname, git_repository_workdir(ignores->repo)) < 0)
		return -1;
234

235 236 237
	/* first process builtins - success means path was found */
	if (ignore_lookup_in_rules(
			&ignores->ign_internal->rules, &path, ignored))
238
		goto cleanup;
239

240 241
	/* next process files in the path */
	git_vector_foreach(&ignores->ign_path, i, file) {
242
		if (ignore_lookup_in_rules(&file->rules, &path, ignored))
243
			goto cleanup;
244 245
	}

246 247
	/* last process global ignores */
	git_vector_foreach(&ignores->ign_global, i, file) {
248
		if (ignore_lookup_in_rules(&file->rules, &path, ignored))
249
			goto cleanup;
250 251 252
	}

	*ignored = 0;
253 254 255

cleanup:
	git_attr_path__free(&path);
256
	return 0;
257
}
258 259 260 261 262 263 264 265

int git_ignore_add_rule(
	git_repository *repo,
	const char *rules)
{
	int error;
	git_attr_file *ign_internal;

266
	if (!(error = get_internal_ignores(&ign_internal, repo)))
267
		error = parse_ignore_file(repo, NULL, rules, ign_internal);
268 269 270 271 272 273 274 275 276 277

	return error;
}

int git_ignore_clear_internal_rules(
	git_repository *repo)
{
	int error;
	git_attr_file *ign_internal;

278
	if (!(error = get_internal_ignores(&ign_internal, repo))) {
279 280
		git_attr_file__clear_rules(ign_internal);

281 282 283 284
		return parse_ignore_file(
			repo, NULL, GIT_IGNORE_DEFAULT_RULES, ign_internal);
	}

285 286
	return error;
}
287 288 289 290

int git_ignore_path_is_ignored(
	int *ignored,
	git_repository *repo,
291
	const char *pathname)
292 293
{
	int error;
294 295 296 297
	const char *workdir;
	git_attr_path path;
	char *tail, *end;
	bool full_is_dir;
298
	git_ignores ignores;
299 300
	unsigned int i;
	git_attr_file *file;
301

302 303 304 305 306 307 308 309 310 311
	assert(ignored && pathname);

	workdir = repo ? git_repository_workdir(repo) : NULL;

	if ((error = git_attr_path__init(&path, pathname, workdir)) < 0)
		return error;

	tail = path.path;
	end  = &path.full.ptr[path.full.size];
	full_is_dir = path.is_dir;
312

313 314 315 316 317 318 319 320 321 322
	while (1) {
		/* advance to next component of path */
		path.basename = tail;

		while (tail < end && *tail != '/') tail++;
		*tail = '\0';

		path.full.size = (tail - path.full.ptr);
		path.is_dir = (tail == end) ? full_is_dir : true;

323 324 325
		/* initialize ignores the first time through */
		if (path.basename == path.path &&
			(error = git_ignore__for_path(repo, path.path, &ignores)) < 0)
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
			break;

		/* first process builtins - success means path was found */
		if (ignore_lookup_in_rules(
				&ignores.ign_internal->rules, &path, ignored))
			goto cleanup;

		/* next process files in the path */
		git_vector_foreach(&ignores.ign_path, i, file) {
			if (ignore_lookup_in_rules(&file->rules, &path, ignored))
				goto cleanup;
		}

		/* last process global ignores */
		git_vector_foreach(&ignores.ign_global, i, file) {
			if (ignore_lookup_in_rules(&file->rules, &path, ignored))
				goto cleanup;
		}

		/* if we found no rules before reaching the end, we're done */
		if (tail == end)
			break;

349 350 351 352
		/* now add this directory to list of ignores */
		if ((error = git_ignore__push_dir(&ignores, path.path)) < 0)
			break;

353 354 355 356 357 358 359 360 361
		/* reinstate divider in path */
		*tail = '/';
		while (*tail == '/') tail++;
	}

	*ignored = 0;

cleanup:
	git_attr_path__free(&path);
362 363 364 365
	git_ignore__free(&ignores);
	return error;
}

366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 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

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