libgit2/src/attr.c
Russell Belfer 974774c7b0 Add attributes to filters and fix registry
The filter registry as implemented was too primitive to actually
work once multiple filters were coming into play.  This expands
the implementation of the registry to handle multiple prioritized
filters correctly.

Additionally, this adds an "attributes" field to a filter that
makes it really really easy to implement filters that are based
on one or more attribute values.  The lookup and even simple value
checking can all happen automatically without custom filter code.

Lastly, with the registry improvements, this fills out the filter
lifecycle callbacks, with initialize and shutdown callbacks that
will be called before the filter is first used and after it is
last invoked.  This allows for system-wide initialization and
cleanup by the filter.
2013-09-17 09:31:44 -07:00

744 lines
16 KiB
C

#include "repository.h"
#include "fileops.h"
#include "config.h"
#include "attr.h"
#include "ignore.h"
#include "git2/oid.h"
#include <ctype.h>
GIT__USE_STRMAP;
const char *git_attr__true = "[internal]__TRUE__";
const char *git_attr__false = "[internal]__FALSE__";
const char *git_attr__unset = "[internal]__UNSET__";
git_attr_t git_attr_value(const char *attr)
{
if (attr == NULL || attr == git_attr__unset)
return GIT_ATTR_UNSPECIFIED_T;
if (attr == git_attr__true)
return GIT_ATTR_TRUE_T;
if (attr == git_attr__false)
return GIT_ATTR_FALSE_T;
return GIT_ATTR_VALUE_T;
}
static int collect_attr_files(
git_repository *repo,
uint32_t flags,
const char *path,
git_vector *files);
int git_attr_get(
const char **value,
git_repository *repo,
uint32_t flags,
const char *pathname,
const char *name)
{
int error;
git_attr_path path;
git_vector files = GIT_VECTOR_INIT;
size_t i, j;
git_attr_file *file;
git_attr_name attr;
git_attr_rule *rule;
*value = NULL;
if (git_attr_path__init(&path, pathname, git_repository_workdir(repo)) < 0)
return -1;
if ((error = collect_attr_files(repo, flags, pathname, &files)) < 0)
goto cleanup;
attr.name = name;
attr.name_hash = git_attr_file__name_hash(name);
git_vector_foreach(&files, i, file) {
git_attr_file__foreach_matching_rule(file, &path, j, rule) {
size_t pos;
if (!git_vector_bsearch(&pos, &rule->assigns, &attr)) {
*value = ((git_attr_assignment *)git_vector_get(
&rule->assigns, pos))->value;
goto cleanup;
}
}
}
cleanup:
git_vector_free(&files);
git_attr_path__free(&path);
return error;
}
typedef struct {
git_attr_name name;
git_attr_assignment *found;
} attr_get_many_info;
int git_attr_get_many(
const char **values,
git_repository *repo,
uint32_t flags,
const char *pathname,
size_t num_attr,
const char **names)
{
int error;
git_attr_path path;
git_vector files = GIT_VECTOR_INIT;
size_t i, j, k;
git_attr_file *file;
git_attr_rule *rule;
attr_get_many_info *info = NULL;
size_t num_found = 0;
if (git_attr_path__init(&path, pathname, git_repository_workdir(repo)) < 0)
return -1;
if ((error = collect_attr_files(repo, flags, pathname, &files)) < 0)
goto cleanup;
info = git__calloc(num_attr, sizeof(attr_get_many_info));
GITERR_CHECK_ALLOC(info);
git_vector_foreach(&files, i, file) {
git_attr_file__foreach_matching_rule(file, &path, j, rule) {
for (k = 0; k < num_attr; k++) {
size_t pos;
if (info[k].found != NULL) /* already found assignment */
continue;
if (!info[k].name.name) {
info[k].name.name = names[k];
info[k].name.name_hash = git_attr_file__name_hash(names[k]);
}
if (!git_vector_bsearch(&pos, &rule->assigns, &info[k].name)) {
info[k].found = (git_attr_assignment *)
git_vector_get(&rule->assigns, pos);
values[k] = info[k].found->value;
if (++num_found == num_attr)
goto cleanup;
}
}
}
}
for (k = 0; k < num_attr; k++) {
if (!info[k].found)
values[k] = NULL;
}
cleanup:
git_vector_free(&files);
git_attr_path__free(&path);
git__free(info);
return error;
}
int git_attr_foreach(
git_repository *repo,
uint32_t flags,
const char *pathname,
int (*callback)(const char *name, const char *value, void *payload),
void *payload)
{
int error;
git_attr_path path;
git_vector files = GIT_VECTOR_INIT;
size_t i, j, k;
git_attr_file *file;
git_attr_rule *rule;
git_attr_assignment *assign;
git_strmap *seen = NULL;
if (git_attr_path__init(&path, pathname, git_repository_workdir(repo)) < 0)
return -1;
if ((error = collect_attr_files(repo, flags, pathname, &files)) < 0)
goto cleanup;
seen = git_strmap_alloc();
GITERR_CHECK_ALLOC(seen);
git_vector_foreach(&files, i, file) {
git_attr_file__foreach_matching_rule(file, &path, j, rule) {
git_vector_foreach(&rule->assigns, k, assign) {
/* skip if higher priority assignment was already seen */
if (git_strmap_exists(seen, assign->name))
continue;
git_strmap_insert(seen, assign->name, assign, error);
if (error < 0)
goto cleanup;
error = callback(assign->name, assign->value, payload);
if (error) {
giterr_clear();
error = GIT_EUSER;
goto cleanup;
}
}
}
}
cleanup:
git_strmap_free(seen);
git_vector_free(&files);
git_attr_path__free(&path);
return error;
}
int git_attr_add_macro(
git_repository *repo,
const char *name,
const char *values)
{
int error;
git_attr_rule *macro = NULL;
git_pool *pool;
if (git_attr_cache__init(repo) < 0)
return -1;
macro = git__calloc(1, sizeof(git_attr_rule));
GITERR_CHECK_ALLOC(macro);
pool = &git_repository_attr_cache(repo)->pool;
macro->match.pattern = git_pool_strdup(pool, name);
GITERR_CHECK_ALLOC(macro->match.pattern);
macro->match.length = strlen(macro->match.pattern);
macro->match.flags = GIT_ATTR_FNMATCH_MACRO;
error = git_attr_assignment__parse(repo, pool, &macro->assigns, &values);
if (!error)
error = git_attr_cache__insert_macro(repo, macro);
if (error < 0)
git_attr_rule__free(macro);
return error;
}
bool git_attr_cache__is_cached(
git_repository *repo, git_attr_file_source source, const char *path)
{
git_buf cache_key = GIT_BUF_INIT;
git_strmap *files = git_repository_attr_cache(repo)->files;
const char *workdir = git_repository_workdir(repo);
bool rval;
if (workdir && git__prefixcmp(path, workdir) == 0)
path += strlen(workdir);
if (git_buf_printf(&cache_key, "%d#%s", (int)source, path) < 0)
return false;
rval = git_strmap_exists(files, git_buf_cstr(&cache_key));
git_buf_free(&cache_key);
return rval;
}
static int load_attr_file(
const char **data,
git_futils_filestamp *stamp,
const char *filename)
{
int error;
git_buf content = GIT_BUF_INIT;
error = git_futils_filestamp_check(stamp, filename);
if (error < 0)
return error;
/* if error == 0, then file is up to date. By returning GIT_ENOTFOUND,
* we tell the caller not to reparse this file...
*/
if (!error)
return GIT_ENOTFOUND;
error = git_futils_readbuffer(&content, filename);
if (error < 0) {
/* convert error into ENOTFOUND so failed permissions / invalid
* file type don't actually stop the operation in progress.
*/
return GIT_ENOTFOUND;
/* TODO: once warnings are available, issue a warning callback */
}
*data = git_buf_detach(&content);
return 0;
}
static int load_attr_blob_from_index(
const char **content,
git_blob **blob,
git_repository *repo,
const git_oid *old_oid,
const char *relfile)
{
int error;
size_t pos;
git_index *index;
const git_index_entry *entry;
if ((error = git_repository_index__weakptr(&index, repo)) < 0 ||
(error = git_index_find(&pos, index, relfile)) < 0)
return error;
entry = git_index_get_byindex(index, pos);
if (old_oid && git_oid__cmp(old_oid, &entry->oid) == 0)
return GIT_ENOTFOUND;
if ((error = git_blob_lookup(blob, repo, &entry->oid)) < 0)
return error;
*content = git_blob_rawcontent(*blob);
return 0;
}
static int load_attr_from_cache(
git_attr_file **file,
git_attr_cache *cache,
git_attr_file_source source,
const char *relative_path)
{
git_buf cache_key = GIT_BUF_INIT;
khiter_t cache_pos;
*file = NULL;
if (!cache || !cache->files)
return 0;
if (git_buf_printf(&cache_key, "%d#%s", (int)source, relative_path) < 0)
return -1;
cache_pos = git_strmap_lookup_index(cache->files, cache_key.ptr);
git_buf_free(&cache_key);
if (git_strmap_valid_index(cache->files, cache_pos))
*file = git_strmap_value_at(cache->files, cache_pos);
return 0;
}
int git_attr_cache__internal_file(
git_repository *repo,
const char *filename,
git_attr_file **file)
{
int error = 0;
git_attr_cache *cache = git_repository_attr_cache(repo);
khiter_t cache_pos = git_strmap_lookup_index(cache->files, filename);
if (git_strmap_valid_index(cache->files, cache_pos)) {
*file = git_strmap_value_at(cache->files, cache_pos);
return 0;
}
if (git_attr_file__new(file, 0, filename, &cache->pool) < 0)
return -1;
git_strmap_insert(cache->files, (*file)->key + 2, *file, error);
if (error > 0)
error = 0;
return error;
}
int git_attr_cache__push_file(
git_repository *repo,
const char *base,
const char *filename,
git_attr_file_source source,
git_attr_file_parser parse,
void* parsedata,
git_vector *stack)
{
int error = 0;
git_buf path = GIT_BUF_INIT;
const char *workdir = git_repository_workdir(repo);
const char *relfile, *content = NULL;
git_attr_cache *cache = git_repository_attr_cache(repo);
git_attr_file *file = NULL;
git_blob *blob = NULL;
git_futils_filestamp stamp;
assert(filename && stack);
/* join base and path as needed */
if (base != NULL && git_path_root(filename) < 0) {
if (git_buf_joinpath(&path, base, filename) < 0)
return -1;
filename = path.ptr;
}
relfile = filename;
if (workdir && git__prefixcmp(relfile, workdir) == 0)
relfile += strlen(workdir);
/* check cache */
if (load_attr_from_cache(&file, cache, source, relfile) < 0)
return -1;
/* if not in cache, load data, parse, and cache */
if (source == GIT_ATTR_FILE_FROM_FILE) {
git_futils_filestamp_set(
&stamp, file ? &file->cache_data.stamp : NULL);
error = load_attr_file(&content, &stamp, filename);
} else {
error = load_attr_blob_from_index(&content, &blob,
repo, file ? &file->cache_data.oid : NULL, relfile);
}
if (error) {
/* not finding a file is not an error for this function */
if (error == GIT_ENOTFOUND) {
giterr_clear();
error = 0;
}
goto finish;
}
/* if we got here, we have to parse and/or reparse the file */
if (file)
git_attr_file__clear_rules(file);
else {
error = git_attr_file__new(&file, source, relfile, &cache->pool);
if (error < 0)
goto finish;
}
if (parse && (error = parse(repo, parsedata, content, file)) < 0)
goto finish;
git_strmap_insert(cache->files, file->key, file, error); //-V595
if (error > 0)
error = 0;
/* remember "cache buster" file signature */
if (blob)
git_oid_cpy(&file->cache_data.oid, git_object_id((git_object *)blob));
else
git_futils_filestamp_set(&file->cache_data.stamp, &stamp);
finish:
/* push file onto vector if we found one*/
if (!error && file != NULL)
error = git_vector_insert(stack, file);
if (error != 0)
git_attr_file__free(file);
if (blob)
git_blob_free(blob);
else
git__free((void *)content);
git_buf_free(&path);
return error;
}
#define push_attr_file(R,S,B,F) \
git_attr_cache__push_file((R),(B),(F),GIT_ATTR_FILE_FROM_FILE,git_attr_file__parse_buffer,NULL,(S))
typedef struct {
git_repository *repo;
uint32_t flags;
const char *workdir;
git_index *index;
git_vector *files;
} attr_walk_up_info;
int git_attr_cache__decide_sources(
uint32_t flags, bool has_wd, bool has_index, git_attr_file_source *srcs)
{
int count = 0;
switch (flags & 0x03) {
case GIT_ATTR_CHECK_FILE_THEN_INDEX:
if (has_wd)
srcs[count++] = GIT_ATTR_FILE_FROM_FILE;
if (has_index)
srcs[count++] = GIT_ATTR_FILE_FROM_INDEX;
break;
case GIT_ATTR_CHECK_INDEX_THEN_FILE:
if (has_index)
srcs[count++] = GIT_ATTR_FILE_FROM_INDEX;
if (has_wd)
srcs[count++] = GIT_ATTR_FILE_FROM_FILE;
break;
case GIT_ATTR_CHECK_INDEX_ONLY:
if (has_index)
srcs[count++] = GIT_ATTR_FILE_FROM_INDEX;
break;
}
return count;
}
static int push_one_attr(void *ref, git_buf *path)
{
int error = 0, n_src, i;
attr_walk_up_info *info = (attr_walk_up_info *)ref;
git_attr_file_source src[2];
n_src = git_attr_cache__decide_sources(
info->flags, info->workdir != NULL, info->index != NULL, src);
for (i = 0; !error && i < n_src; ++i)
error = git_attr_cache__push_file(
info->repo, path->ptr, GIT_ATTR_FILE, src[i],
git_attr_file__parse_buffer, NULL, info->files);
return error;
}
static int collect_attr_files(
git_repository *repo,
uint32_t flags,
const char *path,
git_vector *files)
{
int error;
git_buf dir = GIT_BUF_INIT;
const char *workdir = git_repository_workdir(repo);
attr_walk_up_info info;
if (git_attr_cache__init(repo) < 0 ||
git_vector_init(files, 4, NULL) < 0)
return -1;
/* Resolve path in a non-bare repo */
if (workdir != NULL)
error = git_path_find_dir(&dir, path, workdir);
else
error = git_path_dirname_r(&dir, path);
if (error < 0)
goto cleanup;
/* in precendence order highest to lowest:
* - $GIT_DIR/info/attributes
* - path components with .gitattributes
* - config core.attributesfile
* - $GIT_PREFIX/etc/gitattributes
*/
error = push_attr_file(
repo, files, git_repository_path(repo), GIT_ATTR_FILE_INREPO);
if (error < 0)
goto cleanup;
info.repo = repo;
info.flags = flags;
info.workdir = workdir;
if (git_repository_index__weakptr(&info.index, repo) < 0)
giterr_clear(); /* no error even if there is no index */
info.files = files;
error = git_path_walk_up(&dir, workdir, push_one_attr, &info);
if (error < 0)
goto cleanup;
if (git_repository_attr_cache(repo)->cfg_attr_file != NULL) {
error = push_attr_file(
repo, files, NULL, git_repository_attr_cache(repo)->cfg_attr_file);
if (error < 0)
goto cleanup;
}
if ((flags & GIT_ATTR_CHECK_NO_SYSTEM) == 0) {
error = git_futils_find_system_file(&dir, GIT_ATTR_FILE_SYSTEM);
if (!error)
error = push_attr_file(repo, files, NULL, dir.ptr);
else if (error == GIT_ENOTFOUND) {
giterr_clear();
error = 0;
}
}
cleanup:
if (error < 0)
git_vector_free(files);
git_buf_free(&dir);
return error;
}
static int attr_cache__lookup_path(
char **out, git_config *cfg, const char *key, const char *fallback)
{
git_buf buf = GIT_BUF_INIT;
int error;
const char *cfgval = NULL;
*out = NULL;
if (!(error = git_config_get_string(&cfgval, cfg, key))) {
/* expand leading ~/ as needed */
if (cfgval && cfgval[0] == '~' && cfgval[1] == '/' &&
!git_futils_find_global_file(&buf, &cfgval[2]))
*out = git_buf_detach(&buf);
else if (cfgval)
*out = git__strdup(cfgval);
} else if (error == GIT_ENOTFOUND) {
giterr_clear();
error = 0;
if (!git_futils_find_xdg_file(&buf, fallback))
*out = git_buf_detach(&buf);
}
git_buf_free(&buf);
return error;
}
int git_attr_cache__init(git_repository *repo)
{
int ret;
git_attr_cache *cache = git_repository_attr_cache(repo);
git_config *cfg;
if (cache->initialized)
return 0;
/* cache config settings for attributes and ignores */
if (git_repository_config__weakptr(&cfg, repo) < 0)
return -1;
ret = attr_cache__lookup_path(
&cache->cfg_attr_file, cfg, GIT_ATTR_CONFIG, GIT_ATTR_FILE_XDG);
if (ret < 0)
return ret;
ret = attr_cache__lookup_path(
&cache->cfg_excl_file, cfg, GIT_IGNORE_CONFIG, GIT_IGNORE_FILE_XDG);
if (ret < 0)
return ret;
/* allocate hashtable for attribute and ignore file contents */
if (cache->files == NULL) {
cache->files = git_strmap_alloc();
GITERR_CHECK_ALLOC(cache->files);
}
/* allocate hashtable for attribute macros */
if (cache->macros == NULL) {
cache->macros = git_strmap_alloc();
GITERR_CHECK_ALLOC(cache->macros);
}
/* allocate string pool */
if (git_pool_init(&cache->pool, 1, 0) < 0)
return -1;
cache->initialized = 1;
/* insert default macros */
return git_attr_add_macro(repo, "binary", "-diff -crlf -text");
}
void git_attr_cache_flush(
git_repository *repo)
{
git_attr_cache *cache;
if (!repo)
return;
cache = git_repository_attr_cache(repo);
if (cache->files != NULL) {
git_attr_file *file;
git_strmap_foreach_value(cache->files, file, {
git_attr_file__free(file);
});
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;
cache->initialized = 0;
}
int git_attr_cache__insert_macro(git_repository *repo, git_attr_rule *macro)
{
git_strmap *macros = git_repository_attr_cache(repo)->macros;
int error;
/* TODO: generate warning log if (macro->assigns.length == 0) */
if (macro->assigns.length == 0)
return 0;
git_strmap_insert(macros, macro->match.pattern, macro, error);
return (error < 0) ? -1 : 0;
}
git_attr_rule *git_attr_cache__lookup_macro(
git_repository *repo, const char *name)
{
git_strmap *macros = git_repository_attr_cache(repo)->macros;
khiter_t pos;
pos = git_strmap_lookup_index(macros, name);
if (!git_strmap_valid_index(macros, pos))
return NULL;
return (git_attr_rule *)git_strmap_value_at(macros, pos);
}