diff options
author | Russell Belfer <arrbee@arrbee.com> | 2011-12-16 22:56:43 +0400 |
---|---|---|
committer | Russell Belfer <arrbee@arrbee.com> | 2011-12-21 04:32:58 +0400 |
commit | ee1f0b1aed7798908d9e038b006b66f868613fc3 (patch) | |
tree | c60350029b9e4bb14811ac13caf59ad86424f33e /src/attr_file.c | |
parent | be00b00dd1468f1c625ca3fadc61f2a16edfb8d5 (diff) |
Add APIs for git attributes
This adds APIs for querying git attributes. In addition to
the new API in include/git2/attr.h, most of the action is in
src/attr_file.[hc] which contains utilities for dealing with
a single attributes file, and src/attr.[hc] which contains
the implementation of the APIs that merge all applicable
attributes files.
Diffstat (limited to 'src/attr_file.c')
-rw-r--r-- | src/attr_file.c | 456 |
1 files changed, 456 insertions, 0 deletions
diff --git a/src/attr_file.c b/src/attr_file.c new file mode 100644 index 000000000..5d159db00 --- /dev/null +++ b/src/attr_file.c @@ -0,0 +1,456 @@ +#include "common.h" +#include "attr_file.h" +#include "filebuf.h" +#include <ctype.h> + +const char *git_attr__true = "[internal]__TRUE__"; +const char *git_attr__false = "[internal]__FALSE__"; + +static int parse_fnmatch(git_attr_fnmatch *spec, const char **base); +static int parse_assigns(git_vector *assigns, const char **base); +static int free_rule(git_attr_rule *rule); +static int sort_by_hash_and_name(const void *a_raw, const void *b_raw); + +int git_attr_file__from_buffer(git_attr_file **out, const char *buffer) +{ + int error = GIT_SUCCESS; + git_attr_file *attrs = NULL; + const char *scan = NULL; + git_attr_rule *rule = NULL; + + *out = NULL; + + attrs = git__calloc(1, sizeof(git_attr_file)); + if (attrs == NULL) + return git__throw(GIT_ENOMEM, "Could not allocate attribute storage"); + + attrs->path = NULL; + + error = git_vector_init(&attrs->rules, 4, NULL); + if (error != GIT_SUCCESS) { + git__rethrow(error, "Could not initialize attribute storage"); + goto cleanup; + } + + scan = buffer; + + while (error == GIT_SUCCESS && *scan) { + /* allocate rule if needed */ + if (!rule && !(rule = git__calloc(1, sizeof(git_attr_rule)))) { + error = GIT_ENOMEM; + break; + } + + /* parse the next "pattern attr attr attr" line */ + if (!(error = parse_fnmatch(&rule->match, &scan)) && + !(error = parse_assigns(&rule->assigns, &scan))) + error = git_vector_insert(&attrs->rules, rule); + + /* if the rule wasn't a pattern, on to the next */ + if (error != GIT_SUCCESS) { + free_rule(rule); /* release anything partially allocated */ + if (error == GIT_ENOTFOUND) + error = GIT_SUCCESS; + } else { + rule = NULL; /* vector now "owns" the rule */ + } + } + +cleanup: + if (error != GIT_SUCCESS) { + git_attr_file__free(attrs); + git__free(attrs); + } else { + *out = attrs; + } + + return error; +} + +int git_attr_file__from_file(git_attr_file **out, const char *path) +{ + int error = GIT_SUCCESS; + git_fbuffer fbuf = GIT_FBUFFER_INIT; + + *out = NULL; + + if ((error = git_futils_readbuffer(&fbuf, path)) < GIT_SUCCESS || + (error = git_attr_file__from_buffer(out, fbuf.data)) < GIT_SUCCESS) + { + git__rethrow(error, "Could not open attribute file '%s'", path); + } else { + /* save path (okay to fail) */ + (*out)->path = git__strdup(path); + } + + git_futils_freebuffer(&fbuf); + + return error; +} + +void git_attr_file__free(git_attr_file *file) +{ + unsigned int i; + git_attr_rule *rule; + + if (!file) + return; + + git_vector_foreach(&file->rules, i, rule) { + free_rule(rule); + } + + git_vector_free(&file->rules); + + git__free(file->path); + file->path = NULL; +} + +unsigned long git_attr_file__name_hash(const char *name) +{ + unsigned long h = 5381; + int c; + assert(name); + while ((c = (int)*name++) != 0) + h = ((h << 5) + h) + c; + return h; +} + + +int git_attr_file__lookup_one( + git_attr_file *file, + const git_attr_path *path, + const char *attr, + const char **value) +{ + unsigned int i; + git_attr_name name; + git_attr_rule *rule; + + *value = NULL; + + name.name = attr; + name.name_hash = git_attr_file__name_hash(attr); + + git_attr_file__foreach_matching_rule(file, path, i, rule) { + int pos = git_vector_bsearch(&rule->assigns, &name); + git_clearerror(); /* okay if search failed */ + + if (pos >= 0) { + *value = ((git_attr_assignment *) + git_vector_get(&rule->assigns, pos))->value; + break; + } + } + + return GIT_SUCCESS; +} + + +int git_attr_rule__match_path( + git_attr_rule *rule, + const git_attr_path *path) +{ + int matched = FNM_NOMATCH; + + if (rule->match.directory && !path->is_dir) + return matched; + + if (rule->match.fullpath) + matched = p_fnmatch(rule->match.pattern, path->path, FNM_PATHNAME); + else + matched = p_fnmatch(rule->match.pattern, path->basename, 0); + + if (rule->match.negative) + matched = (matched == GIT_SUCCESS) ? FNM_NOMATCH : GIT_SUCCESS; + + return matched; +} + +git_attr_assignment *git_attr_rule__lookup_assignment( + git_attr_rule *rule, const char *name) +{ + int pos; + git_attr_name key; + key.name = name; + key.name_hash = git_attr_file__name_hash(name); + + pos = git_vector_bsearch(&rule->assigns, &key); + git_clearerror(); /* okay if search failed */ + + return (pos >= 0) ? git_vector_get(&rule->assigns, pos) : NULL; +} + +int git_attr_path__init( + git_attr_path *info, const char *path) +{ + info->path = path; + info->basename = strrchr(path, '/'); + if (info->basename) + info->basename++; + if (!info->basename || !*info->basename) + info->basename = path; + info->is_dir = (git_futils_isdir(path) == GIT_SUCCESS); + return GIT_SUCCESS; +} + + +/* + * From gitattributes(5): + * + * Patterns have the following format: + * + * - A blank line matches no files, so it can serve as a separator for + * readability. + * + * - A line starting with # serves as a comment. + * + * - An optional prefix ! which negates the pattern; any matching file + * excluded by a previous pattern will become included again. If a negated + * pattern matches, this will override lower precedence patterns sources. + * + * - If the pattern ends with a slash, it is removed for the purpose of the + * following description, but it would only find a match with a directory. In + * other words, foo/ will match a directory foo and paths underneath it, but + * will not match a regular file or a symbolic link foo (this is consistent + * with the way how pathspec works in general in git). + * + * - If the pattern does not contain a slash /, git treats it as a shell glob + * pattern and checks for a match against the pathname without leading + * directories. + * + * - Otherwise, git treats the pattern as a shell glob suitable for consumption + * by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will + * not match a / in the pathname. For example, "Documentation/\*.html" matches + * "Documentation/git.html" but not "Documentation/ppc/ppc.html". A leading + * slash matches the beginning of the pathname; for example, "/\*.c" matches + * "cat-file.c" but not "mozilla-sha1/sha1.c". + */ + +/* + * This will return GIT_SUCCESS if the spec was filled out, + * GIT_ENOTFOUND if the fnmatch does not require matching, or + * another error code there was an actual problem. + */ +static int parse_fnmatch( + git_attr_fnmatch *spec, + const char **base) +{ + const char *pattern; + const char *scan; + int slash_count; + int error = GIT_SUCCESS; + + assert(base && *base); + + pattern = *base; + + while (isspace(*pattern)) pattern++; + if (!*pattern || *pattern == '#') { + error = GIT_ENOTFOUND; + goto skip_to_eol; + } + + if (*pattern == '!') { + spec->negative = 1; + pattern++; + } else { + spec->negative = 0; + } + + spec->fullpath = 0; + slash_count = 0; + for (scan = pattern; *scan != '\0'; ++scan) { + if (isspace(*scan) && *(scan - 1) != '\\') + break; + + if (*scan == '/') { + spec->fullpath = 1; + slash_count++; + } + } + + *base = scan; + spec->length = scan - pattern; + spec->pattern = git__strndup(pattern, spec->length); + + if (!spec->pattern) { + error = GIT_ENOMEM; + goto skip_to_eol; + } else { + char *from = spec->pattern, *to = spec->pattern; + while (*from) { + if (*from == '\\') { + from++; + spec->length--; + } + *to++ = *from++; + } + *to = '\0'; + } + + if (pattern[spec->length - 1] == '/') { + spec->length--; + spec->pattern[spec->length] = '\0'; + spec->directory = 1; + if (--slash_count <= 0) + spec->fullpath = 0; + } else { + spec->directory = 0; + } + + return GIT_SUCCESS; + +skip_to_eol: + /* skip to end of line */ + while (*pattern && *pattern != '\n') pattern++; + if (*pattern == '\n') pattern++; + *base = pattern; + + return error; +} + +static int sort_by_hash_and_name(const void *a_raw, const void *b_raw) +{ + const git_attr_name *a = a_raw; + const git_attr_name *b = b_raw; + + if (b->name_hash < a->name_hash) + return 1; + else if (b->name_hash > a->name_hash) + return -1; + else + return strcmp(b->name, a->name); +} + +static int parse_assigns( + git_vector *assigns, + const char **base) +{ + int error = GIT_SUCCESS; + const char *scan = *base; + git_attr_assignment *assign = NULL; + + assert(assigns && !assigns->length); + + while (*scan && *scan != '\n') { + const char *name_start, *value_start; + + /* skip leading blanks */ + while (isspace(*scan) && *scan != '\n') scan++; + + /* allocate assign if needed */ + if (!assign) { + assign = git__calloc(1, sizeof(git_attr_assignment)); + if (!assign) { + error = GIT_ENOMEM; + break; + } + } + + assign->name_hash = 5381; + assign->value = GIT_ATTR_TRUE; + assign->is_allocated = 0; + + /* look for magic name prefixes */ + if (*scan == '-') { + assign->value = GIT_ATTR_FALSE; + scan++; + } else if (*scan == '!') { + assign->value = NULL; /* explicit unspecified state */ + scan++; + } else if (*scan == '#') /* comment rest of line */ + break; + + /* find the name */ + name_start = scan; + while (*scan && !isspace(*scan) && *scan != '=') { + assign->name_hash = + ((assign->name_hash << 5) + assign->name_hash) + *scan; + scan++; + } + assign->name_len = scan - name_start; + if (assign->name_len <= 0) { + /* must have found lone prefix (" - ") or leading = ("=foo") + * or end of buffer -- advance until whitespace and continue + */ + while (*scan && !isspace(*scan)) scan++; + continue; + } + + /* if there is an equals sign, find the value */ + if (*scan == '=') { + for (value_start = ++scan; *scan && !isspace(*scan); ++scan); + + /* if we found a value, allocate permanent storage for it */ + if (scan > value_start) { + assign->value = git__strndup(value_start, scan - value_start); + if (!assign->value) { + error = GIT_ENOMEM; + break; + } else { + assign->is_allocated = 1; + } + } + } + + /* allocate permanent storage for name */ + assign->name = git__strndup(name_start, assign->name_len); + if (!assign->name) { + error = GIT_ENOMEM; + break; + } + + /* insert allocated assign into vector */ + error = git_vector_insert(assigns, assign); + if (error < GIT_SUCCESS) + break; + + /* clear assign since it is now "owned" by the vector */ + assign = NULL; + } + + if (!assigns->length) + error = git__throw(GIT_ENOTFOUND, "No attribute assignments found for rule"); + else { + assigns->_cmp = sort_by_hash_and_name; + git_vector_sort(assigns); + } + + if (assign != NULL) { + git__free(assign->name); + if (assign->is_allocated) + git__free((void *)assign->value); + git__free(assign); + } + + while (*scan && *scan != '\n') scan++; + *base = scan; + + return error; +} + +static int free_rule(git_attr_rule *rule) +{ + unsigned int i; + git_attr_assignment *assign; + + if (!rule) + return GIT_SUCCESS; + + git__free(rule->match.pattern); + rule->match.pattern = NULL; + rule->match.length = 0; + + git_vector_foreach(&rule->assigns, i, assign) { + git__free(assign->name); + assign->name = NULL; + + if (assign->is_allocated) { + git__free((void *)assign->value); + assign->value = NULL; + } + } + + return GIT_SUCCESS; +} |