attrcache.c 11.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 "attrcache.h"

10 11 12 13 14
#include "repository.h"
#include "attr_file.h"
#include "config.h"
#include "sysdir.h"
#include "ignore.h"
15
#include "path.h"
16 17 18 19 20 21

GIT_INLINE(int) attr_cache_lock(git_attr_cache *cache)
{
	GIT_UNUSED(cache); /* avoid warning if threading is off */

	if (git_mutex_lock(&cache->lock) < 0) {
22
		git_error_set(GIT_ERROR_OS, "unable to get attr cache lock");
23 24 25 26 27 28 29 30 31 32 33
		return -1;
	}
	return 0;
}

GIT_INLINE(void) attr_cache_unlock(git_attr_cache *cache)
{
	GIT_UNUSED(cache); /* avoid warning if threading is off */
	git_mutex_unlock(&cache->lock);
}

34
GIT_INLINE(git_attr_file_entry *) attr_cache_lookup_entry(
35 36
	git_attr_cache *cache, const char *path)
{
37
	return git_strmap_get(cache->files, path);
38 39
}

40 41
int git_attr_cache__alloc_file_entry(
	git_attr_file_entry **out,
42
	git_repository *repo,
43 44 45 46
	const char *base,
	const char *path,
	git_pool *pool)
{
47
	git_str fullpath_str = GIT_STR_INIT;
48
	size_t baselen = 0, pathlen = strlen(path);
49 50
	size_t cachesize = sizeof(git_attr_file_entry) + pathlen + 1;
	git_attr_file_entry *ce;
51

52
	if (base != NULL && git_fs_path_root(path) < 0) {
53 54 55 56 57 58 59
		baselen = strlen(base);
		cachesize += baselen;

		if (baselen && base[baselen - 1] != '/')
			cachesize++;
	}

60
	ce = git_pool_mallocz(pool, cachesize);
61
	GIT_ERROR_CHECK_ALLOC(ce);
62

63
	if (baselen) {
64
		memcpy(ce->fullpath, base, baselen);
65 66 67 68

		if (base[baselen - 1] != '/')
			ce->fullpath[baselen++] = '/';
	}
69
	memcpy(&ce->fullpath[baselen], path, pathlen);
70

71 72 73 74
	fullpath_str.ptr = ce->fullpath;
	fullpath_str.size = pathlen + baselen;

	if (git_path_validate_str_length(repo, &fullpath_str) < 0)
75 76
		return -1;

77 78 79 80 81 82 83 84
	ce->path = &ce->fullpath[baselen];
	*out = ce;

	return 0;
}

/* call with attrcache locked */
static int attr_cache_make_entry(
85
	git_attr_file_entry **out, git_repository *repo, const char *path)
86 87
{
	git_attr_cache *cache = git_repository_attr_cache(repo);
88
	git_attr_file_entry *entry = NULL;
89
	int error;
90

91 92
	if ((error = git_attr_cache__alloc_file_entry(&entry, repo,
		git_repository_workdir(repo), path, &cache->pool)) < 0)
93
		return error;
94

95 96
	if ((error = git_strmap_set(cache->files, entry->path, entry)) < 0)
		return error;
97

98
	*out = entry;
99 100 101 102 103 104
	return error;
}

/* insert entry or replace existing if we raced with another thread */
static int attr_cache_upsert(git_attr_cache *cache, git_attr_file *file)
{
105
	git_attr_file_entry *entry;
106 107 108 109 110
	git_attr_file *old;

	if (attr_cache_lock(cache) < 0)
		return -1;

111
	entry = attr_cache_lookup_entry(cache, file->entry->path);
112

113
	GIT_REFCOUNT_OWN(file, entry);
114
	GIT_REFCOUNT_INC(file);
115

116 117 118 119
	/*
	 * Replace the existing value if another thread has
	 * created it in the meantime.
	 */
120
	old = git_atomic_swap(entry->file[file->source.type], file);
121 122 123 124 125 126 127 128 129 130 131 132 133

	if (old) {
		GIT_REFCOUNT_OWN(old, NULL);
		git_attr_file__free(old);
	}

	attr_cache_unlock(cache);
	return 0;
}

static int attr_cache_remove(git_attr_cache *cache, git_attr_file *file)
{
	int error = 0;
134
	git_attr_file_entry *entry;
135
	git_attr_file *oldfile = NULL;
136 137 138

	if (!file)
		return 0;
139

140 141 142
	if ((error = attr_cache_lock(cache)) < 0)
		return error;

143
	if ((entry = attr_cache_lookup_entry(cache, file->entry->path)) != NULL)
144
		oldfile = git_atomic_compare_and_swap(&entry->file[file->source.type], file, NULL);
145 146 147

	attr_cache_unlock(cache);

148 149 150
	if (oldfile == file) {
		GIT_REFCOUNT_OWN(file, NULL);
		git_attr_file__free(file);
151
	}
152 153 154 155

	return error;
}

156 157 158 159 160
/* Look up cache entry and file.
 * - If entry is not present, create it while the cache is locked.
 * - If file is present, increment refcount before returning it, so the
 *   cache can be unlocked and it won't go away.
 */
161 162
static int attr_cache_lookup(
	git_attr_file **out_file,
163
	git_attr_file_entry **out_entry,
164
	git_repository *repo,
165
	git_attr_session *attr_session,
166
	git_attr_file_source *source)
167 168
{
	int error = 0;
169
	git_str path = GIT_STR_INIT;
170 171
	const char *wd = git_repository_workdir(repo);
	const char *filename;
172
	git_attr_cache *cache = git_repository_attr_cache(repo);
173
	git_attr_file_entry *entry = NULL;
174 175 176
	git_attr_file *file = NULL;

	/* join base and path as needed */
177
	if (source->base != NULL && git_fs_path_root(source->filename) < 0) {
178
		git_str *p = attr_session ? &attr_session->tmp : &path;
179

180
		if (git_str_joinpath(p, source->base, source->filename) < 0 ||
181
		    git_path_validate_str_length(repo, p) < 0)
182
			return -1;
183 184

		filename = p->ptr;
185 186
	} else {
		filename = source->filename;
187 188
	}

189 190
	if (wd && !git__prefixcmp(filename, wd))
		filename += strlen(wd);
191 192 193 194 195

	/* check cache for existing entry */
	if ((error = attr_cache_lock(cache)) < 0)
		goto cleanup;

196 197 198 199 200 201
	entry = attr_cache_lookup_entry(cache, filename);

	if (!entry) {
		error = attr_cache_make_entry(&entry, repo, filename);
	} else if (entry->file[source->type] != NULL) {
		file = entry->file[source->type];
202 203 204 205 206
		GIT_REFCOUNT_INC(file);
	}

	attr_cache_unlock(cache);

207
cleanup:
208 209
	*out_file  = file;
	*out_entry = entry;
210

211
	git_str_dispose(&path);
212 213 214 215 216 217
	return error;
}

int git_attr_cache__get(
	git_attr_file **out,
	git_repository *repo,
218
	git_attr_session *attr_session,
219
	git_attr_file_source *source,
220 221
	git_attr_file_parser parser,
	bool allow_macros)
222 223 224
{
	int error = 0;
	git_attr_cache *cache = git_repository_attr_cache(repo);
225
	git_attr_file_entry *entry = NULL;
Russell Belfer committed
226
	git_attr_file *file = NULL, *updated = NULL;
227

228
	if ((error = attr_cache_lookup(&file, &entry, repo, attr_session, source)) < 0)
Russell Belfer committed
229
		return error;
230

Russell Belfer committed
231
	/* load file if we don't have one or if existing one is out of date */
232 233
	if (!file ||
	    (error = git_attr_file__out_of_date(repo, attr_session, file, source)) > 0)
234
		error = git_attr_file__load(&updated, repo, attr_session,
235
		                            entry, source, parser,
236
		                            allow_macros);
Russell Belfer committed
237 238 239

	/* if we loaded the file, insert into and/or update cache */
	if (updated) {
240
		if ((error = attr_cache_upsert(cache, updated)) < 0) {
Russell Belfer committed
241
			git_attr_file__free(updated);
242
		} else {
Russell Belfer committed
243 244 245
			git_attr_file__free(file); /* offset incref from lookup */
			file = updated;
		}
246 247
	}

Russell Belfer committed
248 249 250 251 252
	/* if file could not be loaded */
	if (error < 0) {
		/* remove existing entry */
		if (file) {
			attr_cache_remove(cache, file);
253
			git_attr_file__free(file); /* offset incref from lookup */
Russell Belfer committed
254 255 256 257
			file = NULL;
		}
		/* no error if file simply doesn't exist */
		if (error == GIT_ENOTFOUND) {
258
			git_error_clear();
Russell Belfer committed
259 260
			error = 0;
		}
261 262
	}

263
	*out = file;
264 265 266 267 268
	return error;
}

bool git_attr_cache__is_cached(
	git_repository *repo,
269
	git_attr_file_source_t source_type,
270 271 272
	const char *filename)
{
	git_attr_cache *cache = git_repository_attr_cache(repo);
273
	git_attr_file_entry *entry;
274
	git_strmap *files;
275

276
	if (!cache || !(files = cache->files))
277 278
		return false;

279
	if ((entry = git_strmap_get(files, filename)) == NULL)
280 281
		return false;

282
	return entry && (entry->file[source_type] != NULL);
283 284 285 286 287 288
}


static int attr_cache__lookup_path(
	char **out, git_config *cfg, const char *key, const char *fallback)
{
289
	git_str buf = GIT_STR_INIT;
290
	int error;
291
	git_config_entry *entry = NULL;
292 293 294 295 296 297 298 299 300 301

	*out = NULL;

	if ((error = git_config__lookup_entry(&entry, cfg, key, false)) < 0)
		return error;

	if (entry) {
		const char *cfgval = entry->value;

		/* expand leading ~/ as needed */
302 303
		if (cfgval && cfgval[0] == '~' && cfgval[1] == '/') {
			if (! (error = git_sysdir_expand_global_file(&buf, &cfgval[2])))
304
				*out = git_str_detach(&buf);
305
		} else if (cfgval) {
306
			*out = git__strdup(cfgval);
307
		}
308
	}
309
	else if (!git_sysdir_find_xdg_file(&buf, fallback)) {
310
		*out = git_str_detach(&buf);
311
	}
312

313
	git_config_entry_free(entry);
314
	git_str_dispose(&buf);
315 316 317 318 319 320

	return error;
}

static void attr_cache__free(git_attr_cache *cache)
{
321 322
	bool unlock;

323 324 325
	if (!cache)
		return;

326
	unlock = (attr_cache_lock(cache) == 0);
327

328
	if (cache->files != NULL) {
329 330
		git_attr_file_entry *entry;
		git_attr_file *file;
331 332
		int i;

333 334
		git_strmap_foreach_value(cache->files, entry, {
			for (i = 0; i < GIT_ATTR_FILE_NUM_SOURCES; ++i) {
335
				if ((file = git_atomic_swap(entry->file[i], NULL)) != NULL) {
336 337
					GIT_REFCOUNT_OWN(file, NULL);
					git_attr_file__free(file);
338 339
				}
			}
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
		});
		git_strmap_free(cache->files);
	}

	if (cache->macros != NULL) {
		git_attr_rule *rule;

		git_strmap_foreach_value(cache->macros, rule, {
			git_attr_rule__free(rule);
		});
		git_strmap_free(cache->macros);
	}

	git_pool_clear(&cache->pool);

	git__free(cache->cfg_attr_file);
	cache->cfg_attr_file = NULL;

	git__free(cache->cfg_excl_file);
	cache->cfg_excl_file = NULL;

361
	if (unlock)
362
		attr_cache_unlock(cache);
363 364 365 366 367
	git_mutex_free(&cache->lock);

	git__free(cache);
}

368
int git_attr_cache__init(git_repository *repo)
369 370 371
{
	int ret = 0;
	git_attr_cache *cache = git_repository_attr_cache(repo);
372
	git_config *cfg = NULL;
373 374 375 376 377

	if (cache)
		return 0;

	cache = git__calloc(1, sizeof(git_attr_cache));
378
	GIT_ERROR_CHECK_ALLOC(cache);
379 380 381

	/* set up lock */
	if (git_mutex_init(&cache->lock) < 0) {
382
		git_error_set(GIT_ERROR_OS, "unable to initialize lock for attr cache");
383 384 385 386
		git__free(cache);
		return -1;
	}

387 388 389
	if ((ret = git_repository_config_snapshot(&cfg, repo)) < 0)
		goto cancel;

390 391 392 393 394 395 396 397 398 399 400 401 402 403
	/* cache config settings for attributes and ignores */
	ret = attr_cache__lookup_path(
		&cache->cfg_attr_file, cfg, GIT_ATTR_CONFIG, GIT_ATTR_FILE_XDG);
	if (ret < 0)
		goto cancel;

	ret = attr_cache__lookup_path(
		&cache->cfg_excl_file, cfg, GIT_IGNORE_CONFIG, GIT_IGNORE_FILE_XDG);
	if (ret < 0)
		goto cancel;

	/* allocate hashtable for attribute and ignore file contents,
	 * hashtable for attribute macros, and string pool
	 */
404
	if ((ret = git_strmap_new(&cache->files)) < 0 ||
405 406
	    (ret = git_strmap_new(&cache->macros)) < 0 ||
	    (ret = git_pool_init(&cache->pool, 1)) < 0)
407 408
		goto cancel;

409
	if (git_atomic_compare_and_swap(&repo->attrcache, NULL, cache) != NULL)
410 411
		goto cancel; /* raced with another thread, free this but no error */

412 413
	git_config_free(cfg);

414
	/* insert default macros */
415
	return git_attr_add_macro(repo, "binary", "-diff -merge -text -crlf");
416 417 418

cancel:
	attr_cache__free(cache);
419
	git_config_free(cfg);
420 421 422
	return ret;
}

423
int git_attr_cache_flush(git_repository *repo)
424 425 426 427 428 429
{
	git_attr_cache *cache;

	/* this could be done less expensively, but for now, we'll just free
	 * the entire attrcache and let the next use reinitialize it...
	 */
430
	if (repo && (cache = git_atomic_swap(repo->attrcache, NULL)) != NULL)
431
		attr_cache__free(cache);
432 433

	return 0;
434 435 436 437 438
}

int git_attr_cache__insert_macro(git_repository *repo, git_attr_rule *macro)
{
	git_attr_cache *cache = git_repository_attr_cache(repo);
439 440 441
	git_attr_rule *preexisting;
	bool locked = false;
	int error = 0;
442

443 444 445 446 447 448 449 450 451 452
	/*
	 * Callers assume that if we return success, that the
	 * macro will have been adopted by the attributes cache.
	 * Thus, we have to free the macro here if it's not being
	 * added to the cache.
	 *
	 * TODO: generate warning log if (macro->assigns.length == 0)
	 */
	if (macro->assigns.length == 0) {
		git_attr_rule__free(macro);
453
		goto out;
454
	}
455

456 457 458 459 460 461
	if ((error = attr_cache_lock(cache)) < 0)
		goto out;
	locked = true;

	if ((preexisting = git_strmap_get(cache->macros, macro->match.pattern)) != NULL)
	    git_attr_rule__free(preexisting);
462

463 464 465 466 467 468
	if ((error = git_strmap_set(cache->macros, macro->match.pattern, macro)) < 0)
	    goto out;

out:
	if (locked)
		attr_cache_unlock(cache);
469
	return error;
470 471 472 473 474 475 476
}

git_attr_rule *git_attr_cache__lookup_macro(
	git_repository *repo, const char *name)
{
	git_strmap *macros = git_repository_attr_cache(repo)->macros;

477
	return git_strmap_get(macros, name);
478
}