summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRussell Belfer <arrbee@arrbee.com>2011-12-16 10:56:43 -0800
committerRussell Belfer <arrbee@arrbee.com>2011-12-20 16:32:58 -0800
commitee1f0b1aed7798908d9e038b006b66f868613fc3 (patch)
treec60350029b9e4bb14811ac13caf59ad86424f33e
parentbe00b00dd1468f1c625ca3fadc61f2a16edfb8d5 (diff)
downloadlibgit2-ee1f0b1aed7798908d9e038b006b66f868613fc3.tar.gz
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.
-rw-r--r--include/git2/attr.h56
-rw-r--r--src/attr.c311
-rw-r--r--src/attr.h21
-rw-r--r--src/attr_file.c456
-rw-r--r--src/attr_file.h87
-rw-r--r--src/hashtable.c14
-rw-r--r--src/hashtable.h7
-rw-r--r--src/refs.c15
-rw-r--r--src/repository.c1
-rw-r--r--src/repository.h2
-rw-r--r--src/util.h1
-rw-r--r--src/vector.c5
-rw-r--r--src/vector.h5
-rw-r--r--tests-clay/attr/file.c236
-rw-r--r--tests-clay/attr/lookup.c237
-rw-r--r--tests-clay/attr/repo.c140
-rw-r--r--tests-clay/clay.h13
-rw-r--r--tests-clay/clay_main.c39
-rw-r--r--tests/resources/attr/.gitattributes4
-rw-r--r--tests/resources/attr/.gitted/HEAD1
-rw-r--r--tests/resources/attr/.gitted/config6
-rw-r--r--tests/resources/attr/.gitted/description1
-rw-r--r--tests/resources/attr/.gitted/indexbin0 -> 1072 bytes
-rw-r--r--tests/resources/attr/.gitted/info/attributes2
-rw-r--r--tests/resources/attr/.gitted/info/exclude6
-rw-r--r--tests/resources/attr/.gitted/logs/HEAD1
-rw-r--r--tests/resources/attr/.gitted/logs/refs/heads/master1
-rw-r--r--tests/resources/attr/.gitted/objects/29/29de282ce999e95183aedac6451d3384559c4bbin0 -> 58 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/2c/66e14f77196ea763fb1e41612c1aa2bc2d8ed2bin0 -> 316 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/2d/e7dfe3588f3c7e9ad59e7d50ba90e3329df9d9bin0 -> 124 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/3b/74db7ab381105dc0d28f8295a77f6a82989292bin0 -> 276 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/45/141a79a77842c59a63229403220a4e4be74e3dbin0 -> 36 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/55/6f8c827b8e4a02ad5cab77dca2bcb3e226b0b3bin0 -> 24 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/6b/ab5c79cd5140d0f800917f550eb2a3dc32b0da3
-rw-r--r--tests/resources/attr/.gitted/objects/c0/091889c0c77142b87a1fa5123a6398a61d33e7bin0 -> 290 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/c4/85abe35abd4aa6fd83b076a78bbea9e2e7e06cbin0 -> 129 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/c7/aadd770d5907a8475c29e9ee21a27b88bf675dbin0 -> 60 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/dc/cada462d3df8ac6de596fb8c896aba9344f941bin0 -> 35 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/e5/63cf4758f0d646f1b14b76016aa17fa9e549a4bin0 -> 39 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/f2/c6d717cf4a5a3e6b02684155ab07b766982165bin0 -> 44 bytes
-rw-r--r--tests/resources/attr/.gitted/objects/fb/5067b1aef3ac1ada4b379dbcb7d17255df7d78bin0 -> 28 bytes
-rw-r--r--tests/resources/attr/.gitted/refs/heads/master1
-rw-r--r--tests/resources/attr/attr01
-rw-r--r--tests/resources/attr/attr129
-rw-r--r--tests/resources/attr/attr221
-rw-r--r--tests/resources/attr/attr34
-rw-r--r--tests/resources/attr/root_test11
-rw-r--r--tests/resources/attr/root_test21
-rw-r--r--tests/resources/attr/root_test31
-rw-r--r--tests/resources/attr/root_test4.txt1
-rw-r--r--tests/resources/attr/subdir/.gitattributes3
-rw-r--r--tests/resources/attr/subdir/subdir_test12
-rw-r--r--tests/resources/attr/subdir/subdir_test2.txt1
-rw-r--r--tests/resources/attr/subdir2/subdir2_test11
54 files changed, 1722 insertions, 16 deletions
diff --git a/include/git2/attr.h b/include/git2/attr.h
new file mode 100644
index 000000000..d585937b7
--- /dev/null
+++ b/include/git2/attr.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009-2011 the libgit2 contributors
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+#ifndef INCLUDE_git_attr_h__
+#define INCLUDE_git_attr_h__
+
+#include "common.h"
+#include "types.h"
+
+/**
+ * @file git2/attr.h
+ * @brief Git attribute management routines
+ * @defgroup git_attr Git attribute management routines
+ * @ingroup Git
+ * @{
+ */
+GIT_BEGIN_DECL
+
+#define GIT_ATTR_TRUE git_attr__true
+#define GIT_ATTR_FALSE git_attr__false
+#define GIT_ATTR_UNSPECIFIED NULL
+
+GIT_EXTERN(const char *)git_attr__true;
+GIT_EXTERN(const char *)git_attr__false;
+
+
+/**
+ * Lookup attribute for path returning string caller must free
+ */
+GIT_EXTERN(int) git_attr_get(
+ git_repository *repo, const char *path, const char *name,
+ const char **value);
+
+/**
+ * Lookup list of attributes for path, populating array of strings
+ */
+GIT_EXTERN(int) git_attr_get_many(
+ git_repository *repo, const char *path,
+ size_t num_attr, const char **names,
+ const char **values);
+
+/**
+ * Perform an operation on each attribute of a path.
+ */
+GIT_EXTERN(int) git_attr_foreach(
+ git_repository *repo, const char *path,
+ int (*callback)(const char *name, const char *value, void *payload),
+ void *payload);
+
+/** @} */
+GIT_END_DECL
+#endif
+
diff --git a/src/attr.c b/src/attr.c
new file mode 100644
index 000000000..d8e7095b1
--- /dev/null
+++ b/src/attr.c
@@ -0,0 +1,311 @@
+#include "attr.h"
+#include "buffer.h"
+#include "fileops.h"
+#include "config.h"
+#include <ctype.h>
+
+#define GIT_ATTR_FILE_INREPO "info/attributes"
+#define GIT_ATTR_FILE ".gitattributes"
+#define GIT_ATTR_FILE_SYSTEM "/etc/gitattributes"
+#if GIT_WIN32
+#define GIT_ATTR_FILE_WIN32 L"%PROGRAMFILES%\\Git\\etc\\gitattributes"
+#endif
+
+static int collect_attr_files(
+ git_repository *repo, const char *path, git_vector *files);
+
+
+int git_attr_get(
+ git_repository *repo, const char *pathname,
+ const char *name, const char **value)
+{
+ int error;
+ git_attr_path path;
+ git_vector files = GIT_VECTOR_INIT;
+ unsigned int i, j;
+ git_attr_file *file;
+ git_attr_name attr;
+ git_attr_rule *rule;
+
+ *value = NULL;
+
+ if ((error = git_attr_path__init(&path, pathname)) < GIT_SUCCESS ||
+ (error = collect_attr_files(repo, pathname, &files)) < GIT_SUCCESS)
+ return git__rethrow(error, "Could not get attribute for %s", pathname);
+
+ 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) {
+ int pos = git_vector_bsearch(&rule->assigns, &attr);
+ git_clearerror(); /* okay if search failed */
+
+ if (pos >= 0) {
+ *value = ((git_attr_assignment *)git_vector_get(
+ &rule->assigns, pos))->value;
+ goto found;
+ }
+ }
+ }
+
+found:
+ git_vector_free(&files);
+
+ return error;
+}
+
+
+typedef struct {
+ git_attr_name name;
+ git_attr_assignment *found;
+} attr_get_many_info;
+
+int git_attr_get_many(
+ git_repository *repo, const char *pathname,
+ size_t num_attr, const char **names, const char **values)
+{
+ int error;
+ git_attr_path path;
+ git_vector files = GIT_VECTOR_INIT;
+ unsigned int i, j, k;
+ git_attr_file *file;
+ git_attr_rule *rule;
+ attr_get_many_info *info = NULL;
+ size_t num_found = 0;
+
+ memset(values, 0, sizeof(const char *) * num_attr);
+
+ if ((error = git_attr_path__init(&path, pathname)) < GIT_SUCCESS ||
+ (error = collect_attr_files(repo, pathname, &files)) < GIT_SUCCESS)
+ return git__rethrow(error, "Could not get attributes for %s", pathname);
+
+ if ((info = git__calloc(num_attr, sizeof(attr_get_many_info))) == NULL) {
+ git__rethrow(GIT_ENOMEM, "Could not get attributes for %s", pathname);
+ goto cleanup;
+ }
+
+ git_vector_foreach(&files, i, file) {
+
+ git_attr_file__foreach_matching_rule(file, &path, j, rule) {
+
+ for (k = 0; k < num_attr; k++) {
+ int 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]);
+ }
+
+ pos = git_vector_bsearch(&rule->assigns, &info[k].name);
+ git_clearerror(); /* okay if search failed */
+
+ if (pos >= 0) {
+ 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;
+ }
+ }
+ }
+ }
+
+cleanup:
+ git_vector_free(&files);
+ git__free(info);
+
+ return error;
+}
+
+
+int git_attr_foreach(
+ git_repository *repo, 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;
+ unsigned int i, j, k;
+ git_attr_file *file;
+ git_attr_rule *rule;
+ git_attr_assignment *assign;
+ git_hashtable *seen = NULL;
+
+ if ((error = git_attr_path__init(&path, pathname)) < GIT_SUCCESS ||
+ (error = collect_attr_files(repo, pathname, &files)) < GIT_SUCCESS)
+ return git__rethrow(error, "Could not get attributes for %s", pathname);
+
+ seen = git_hashtable_alloc(8, git_hash__strhash_cb, git_hash__strcmp_cb);
+ if (!seen) {
+ error = GIT_ENOMEM;
+ goto cleanup;
+ }
+
+ 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_hashtable_lookup(seen, assign->name))
+ continue;
+
+ error = git_hashtable_insert(seen, assign->name, assign);
+ if (error != GIT_SUCCESS)
+ goto cleanup;
+
+ error = callback(assign->name, assign->value, payload);
+ if (error != GIT_SUCCESS)
+ goto cleanup;
+ }
+ }
+ }
+
+cleanup:
+ if (seen)
+ git_hashtable_free(seen);
+ git_vector_free(&files);
+
+ if (error != GIT_SUCCESS)
+ (void)git__rethrow(error, "Could not get attributes for %s", pathname);
+
+ return error;
+}
+
+
+/* add git_attr_file to vector of files, loading if needed */
+static int push_attrs(
+ git_repository *repo,
+ git_vector *files,
+ const char *base,
+ const char *filename)
+{
+ int error = GIT_SUCCESS;
+ git_attr_cache *cache = &repo->attrcache;
+ git_buf path = GIT_BUF_INIT;
+ git_attr_file *file;
+ int add_to_cache = 0;
+
+ if (cache->files == NULL) {
+ cache->files = git_hashtable_alloc(
+ 8, git_hash__strhash_cb, git_hash__strcmp_cb);
+ if (!cache->files)
+ return git__throw(GIT_ENOMEM, "Could not create attribute cache");
+ }
+
+ if ((error = git_path_prettify(&path, filename, base)) < GIT_SUCCESS) {
+ if (error == GIT_EOSERR)
+ /* file was not found -- ignore error */
+ error = GIT_SUCCESS;
+ goto cleanup;
+ }
+
+ /* either get attr_file from cache or read from disk */
+ file = git_hashtable_lookup(cache->files, path.ptr);
+ if (file == NULL) {
+ error = git_attr_file__from_file(&file, path.ptr);
+ add_to_cache = (error == GIT_SUCCESS);
+ }
+
+ if (file != NULL) {
+ /* add file to vector, if we found it */
+ error = git_vector_insert(files, file);
+
+ /* add file to cache, if it is new */
+ /* do this after above step b/c it is not critical */
+ if (error == GIT_SUCCESS && add_to_cache && file->path != NULL)
+ error = git_hashtable_insert(cache->files, file->path, file);
+ }
+
+cleanup:
+ git_buf_free(&path);
+ return error;
+}
+
+
+static int collect_attr_files(
+ git_repository *repo, const char *path, git_vector *files)
+{
+ int error = GIT_SUCCESS;
+ git_buf dir = GIT_BUF_INIT;
+ git_config *cfg;
+ const char *workdir = git_repository_workdir(repo);
+
+ if ((error = git_vector_init(files, 4, NULL)) < GIT_SUCCESS)
+ goto cleanup;
+
+ if ((error = git_path_prettify(&dir, path, workdir)) < GIT_SUCCESS)
+ goto cleanup;
+
+ if (git_futils_isdir(dir.ptr) != GIT_SUCCESS) {
+ git_path_dirname_r(&dir, dir.ptr);
+ git_path_to_dir(&dir);
+ if ((error = git_buf_lasterror(&dir)) < GIT_SUCCESS)
+ 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_attrs(repo, files, repo->path_repository, GIT_ATTR_FILE_INREPO);
+ if (error < GIT_SUCCESS)
+ goto cleanup;
+
+ if (workdir && git__prefixcmp(dir.ptr, workdir) == 0) {
+ ssize_t rootlen = (ssize_t)strlen(workdir);
+
+ do {
+ error = push_attrs(repo, files, dir.ptr, GIT_ATTR_FILE);
+ if (error == GIT_SUCCESS) {
+ git_path_dirname_r(&dir, dir.ptr);
+ git_path_to_dir(&dir);
+ error = git_buf_lasterror(&dir);
+ }
+ } while (!error && dir.size >= rootlen);
+ } else {
+ error = push_attrs(repo, files, dir.ptr, GIT_ATTR_FILE);
+ }
+ if (error < GIT_SUCCESS)
+ goto cleanup;
+
+ if (git_repository_config(&cfg, repo) == GIT_SUCCESS) {
+ const char *core_attribs = NULL;
+ git_config_get_string(cfg, "core.attributesfile", &core_attribs);
+ git_clearerror(); /* don't care if attributesfile is not set */
+ if (core_attribs)
+ error = push_attrs(repo, files, NULL, core_attribs);
+ git_config_free(cfg);
+ }
+
+ if (error == GIT_SUCCESS)
+ error = push_attrs(repo, files, NULL, GIT_ATTR_FILE_SYSTEM);
+
+ cleanup:
+ if (error < GIT_SUCCESS) {
+ git__rethrow(error, "Could not get attributes for '%s'", path);
+ git_vector_free(files);
+ }
+ git_buf_free(&dir);
+
+ return error;
+}
+
+
+void git_repository__attr_cache_free(git_attr_cache *attrs)
+{
+ if (attrs && attrs->files) {
+ git_hashtable_free(attrs->files);
+ attrs->files = NULL;
+ }
+}
diff --git a/src/attr.h b/src/attr.h
new file mode 100644
index 000000000..518fb9d3b
--- /dev/null
+++ b/src/attr.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2009-2011 the libgit2 contributors
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+#ifndef INCLUDE_attr_h__
+#define INCLUDE_attr_h__
+
+#include "hashtable.h"
+#include "attr_file.h"
+
+/* EXPORT */
+typedef struct {
+ git_hashtable *files; /* hash path to git_attr_file */
+} git_attr_cache;
+
+extern void git_repository__attr_cache_free(git_attr_cache *attrs);
+
+#endif
+
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;
+}
diff --git a/src/attr_file.h b/src/attr_file.h
new file mode 100644
index 000000000..4774f148c
--- /dev/null
+++ b/src/attr_file.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2009-2011 the libgit2 contributors
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+#ifndef INCLUDE_attr_file_h__
+#define INCLUDE_attr_file_h__
+
+#include "git2/attr.h"
+#include "vector.h"
+
+typedef struct {
+ char *pattern;
+ size_t length;
+ int negative;
+ int directory;
+ int fullpath;
+} git_attr_fnmatch;
+
+typedef struct {
+ const char *name;
+ unsigned long name_hash;
+} git_attr_name;
+
+typedef struct {
+ char *name;
+ unsigned long name_hash;
+ size_t name_len;
+ const char *value;
+ int is_allocated;
+} git_attr_assignment;
+
+typedef struct {
+ git_attr_fnmatch match;
+ git_vector assigns; /* <git_attr_assignment*> */
+} git_attr_rule;
+
+typedef struct {
+ char *path;
+ git_vector rules; /* <git_attr_rule*> */
+} git_attr_file;
+
+typedef struct {
+ const char *path;
+ const char *basename;
+ int is_dir;
+} git_attr_path;
+
+/*
+ * git_attr_file API
+ */
+
+extern int git_attr_file__from_buffer(git_attr_file **out, const char *buf);
+extern int git_attr_file__from_file(git_attr_file **out, const char *path);
+
+extern void git_attr_file__free(git_attr_file *file);
+
+extern int git_attr_file__lookup_one(
+ git_attr_file *file,
+ const git_attr_path *path,
+ const char *attr,
+ const char **value);
+
+/* loop over rules in file from bottom to top */
+#define git_attr_file__foreach_matching_rule(file, path, iter, rule) \
+ git_vector_rforeach(&(file)->rules, (iter), (rule)) \
+ if (git_attr_rule__match_path((rule), (path)) == GIT_SUCCESS)
+
+extern unsigned long git_attr_file__name_hash(const char *name);
+
+
+/*
+ * other utilities
+ */
+
+extern int git_attr_rule__match_path(
+ git_attr_rule *rule,
+ const git_attr_path *path);
+
+extern git_attr_assignment *git_attr_rule__lookup_assignment(
+ git_attr_rule *rule, const char *name);
+
+extern int git_attr_path__init(
+ git_attr_path *info, const char *path);
+
+#endif
diff --git a/src/hashtable.c b/src/hashtable.c
index 15d173992..f836f166d 100644
--- a/src/hashtable.c
+++ b/src/hashtable.c
@@ -241,3 +241,17 @@ int git_hashtable_merge(git_hashtable *self, git_hashtable *other)
return insert_nodes(self, other->nodes, other->key_count);
}
+
+/**
+ * Standard string
+ */
+uint32_t git_hash__strhash_cb(const void *key, int hash_id)
+{
+ static uint32_t hash_seeds[GIT_HASHTABLE_HASHES] = {
+ 2147483647,
+ 0x5d20bb23,
+ 0x7daaab3c
+ };
+
+ return git__hash(key, strlen((const char *)key), hash_seeds[hash_id]);
+}
diff --git a/src/hashtable.h b/src/hashtable.h
index f0ca3ebd2..485b17aa6 100644
--- a/src/hashtable.h
+++ b/src/hashtable.h
@@ -76,5 +76,12 @@ GIT_INLINE(int) git_hashtable_insert(git_hashtable *h, const void *key, void *va
_node->key = NULL; _node->value = NULL; _self->key_count--;\
}
+/*
+ * If you want a hashtable with standard string keys, you can
+ * just pass git_hash__strcmp_cb and git_hash__strhash_cb to
+ * git_hashtable_alloc.
+ */
+#define git_hash__strcmp_cb git__strcmp_cb
+extern uint32_t git_hash__strhash_cb(const void *key, int hash_id);
#endif
diff --git a/src/refs.c b/src/refs.c
index 8c3f700ad..cf76b23be 100644
--- a/src/refs.c
+++ b/src/refs.c
@@ -31,17 +31,6 @@ struct packref {
static const int default_table_size = 32;
-static uint32_t reftable_hash(const void *key, int hash_id)
-{
- static uint32_t hash_seeds[GIT_HASHTABLE_HASHES] = {
- 2147483647,
- 0x5d20bb23,
- 0x7daaab3c
- };
-
- return git__hash(key, strlen((const char *)key), hash_seeds[hash_id]);
-}
-
static int reference_read(
git_fbuffer *file_content,
time_t *mtime,
@@ -443,9 +432,7 @@ static int packed_load(git_repository *repo)
/* First we make sure we have allocated the hash table */
if (ref_cache->packfile == NULL) {
ref_cache->packfile = git_hashtable_alloc(
- default_table_size,
- reftable_hash,
- (git_hash_keyeq_ptr)&git__strcmp_cb);
+ default_table_size, git_hash__strhash_cb, git_hash__strcmp_cb);
if (ref_cache->packfile == NULL) {
error = GIT_ENOMEM;
diff --git a/src/repository.c b/src/repository.c
index 67afa2ee2..e0d4c6387 100644
--- a/src/repository.c
+++ b/src/repository.c
@@ -59,6 +59,7 @@ void git_repository_free(git_repository *repo)
git_cache_free(&repo->objects);
git_repository__refcache_free(&repo->references);
+ git_repository__attr_cache_free(&repo->attrcache);
git__free(repo->path_repository);
git__free(repo->workdir);
diff --git a/src/repository.h b/src/repository.h
index c3a9a5c60..5274fc1d0 100644
--- a/src/repository.h
+++ b/src/repository.h
@@ -19,6 +19,7 @@
#include "refs.h"
#include "buffer.h"
#include "odb.h"
+#include "attr.h"
#define DOT_GIT ".git"
#define GIT_DIR DOT_GIT "/"
@@ -38,6 +39,7 @@ struct git_repository {
git_cache objects;
git_refcache references;
+ git_attr_cache attrcache;
char *path_repository;
char *workdir;
diff --git a/src/util.h b/src/util.h
index 4b1104b7b..be978a6a5 100644
--- a/src/util.h
+++ b/src/util.h
@@ -109,6 +109,7 @@ extern void **git__bsearch(const void *key, void **base, size_t nmemb,
int (*compar)(const void *, const void *));
extern int git__strcmp_cb(const void *a, const void *b);
+extern uint32_t git__strhash_cb(const void *key, int hash_id);
typedef struct {
short refcount;
diff --git a/src/vector.c b/src/vector.c
index 123aae8e6..e745d77dd 100644
--- a/src/vector.c
+++ b/src/vector.c
@@ -29,7 +29,12 @@ static int resize_vector(git_vector *v)
void git_vector_free(git_vector *v)
{
assert(v);
+
git__free(v->contents);
+ v->contents = NULL;
+
+ v->length = 0;
+ v->_alloc_size = 0;
}
int git_vector_init(git_vector *v, unsigned int initial_size, git_vector_cmp cmp)
diff --git a/src/vector.h b/src/vector.h
index 08f5a501c..4c053e6ae 100644
--- a/src/vector.h
+++ b/src/vector.h
@@ -19,6 +19,8 @@ typedef struct git_vector {
int sorted;
} git_vector;
+#define GIT_VECTOR_INIT {0}
+
int git_vector_init(git_vector *v, unsigned int initial_size, git_vector_cmp cmp);
void git_vector_free(git_vector *v);
void git_vector_clear(git_vector *v);
@@ -39,6 +41,9 @@ GIT_INLINE(void *) git_vector_get(git_vector *v, unsigned int position)
#define git_vector_foreach(v, iter, elem) \
for ((iter) = 0; (iter) < (v)->length && ((elem) = (v)->contents[(iter)], 1); (iter)++ )
+#define git_vector_rforeach(v, iter, elem) \
+ for ((iter) = (v)->length; (iter) > 0 && ((elem) = (v)->contents[(iter)-1], 1); (iter)-- )
+
int git_vector_insert(git_vector *v, void *element);
int git_vector_remove(git_vector *v, unsigned int idx);
void git_vector_uniq(git_vector *v);
diff --git a/tests-clay/attr/file.c b/tests-clay/attr/file.c
new file mode 100644
index 000000000..0a5bff59d
--- /dev/null
+++ b/tests-clay/attr/file.c
@@ -0,0 +1,236 @@
+#include "clay_libgit2.h"
+#include "attr_file.h"
+
+#define get_rule(X) ((git_attr_rule *)git_vector_get(&file->rules,(X)))
+#define get_assign(R,Y) ((git_attr_assignment *)git_vector_get(&(R)->assigns,(Y)))
+
+void test_attr_file__simple_read(void)
+{
+ git_attr_file *file = NULL;
+
+ cl_git_pass(git_attr_file__from_file(&file, cl_fixture("attr/attr0")));
+ cl_assert_strequal(cl_fixture("attr/attr0"), file->path);
+ cl_assert(file->rules.length == 1);
+
+ git_attr_rule *rule = get_rule(0);
+ cl_assert(rule != NULL);
+ cl_assert_strequal("*", rule->match.pattern);
+ cl_assert(rule->match.length == 1);
+ cl_assert(!rule->match.negative);
+ cl_assert(!rule->match.directory);
+ cl_assert(!rule->match.fullpath);
+
+ cl_assert(rule->assigns.length == 1);
+ git_attr_assignment *assign = get_assign(rule, 0);
+ cl_assert(assign != NULL);
+ cl_assert_strequal("binary", assign->name);
+ cl_assert(assign->name_len == 6);
+ cl_assert(assign->value == GIT_ATTR_TRUE);
+ cl_assert(!assign->is_allocated);
+
+ git_attr_file__free(file);
+}
+
+void test_attr_file__match_variants(void)
+{
+ git_attr_file *file = NULL;
+ git_attr_rule *rule;
+ git_attr_assignment *assign;
+
+ cl_git_pass(git_attr_file__from_file(&file, cl_fixture("attr/attr1")));
+ cl_assert_strequal(cl_fixture("attr/attr1"), file->path);
+ cl_assert(file->rules.length == 10);
+
+ /* let's do a thorough check of this rule, then just verify
+ * the things that are unique for the later rules
+ */
+ rule = get_rule(0);
+ cl_assert(rule);
+ cl_assert_strequal("pat0", rule->match.pattern);
+ cl_assert(rule->match.length == strlen("pat0"));
+ cl_assert(!rule->match.negative);
+ cl_assert(!rule->match.directory);
+ cl_assert(!rule->match.fullpath);
+ cl_assert(rule->assigns.length == 1);
+ assign = get_assign(rule,0);
+ cl_assert_strequal("attr0", assign->name);
+ cl_assert(assign->name_hash == git_attr_file__name_hash(assign->name));
+ cl_assert(assign->name_len == strlen("attr0"));
+ cl_assert(assign->value == GIT_ATTR_TRUE);
+ cl_assert(!assign->is_allocated);
+
+ rule = get_rule(1);
+ cl_assert_strequal("pat1", rule->match.pattern);
+ cl_assert(rule->match.length == strlen("pat1"));
+ cl_assert(rule->match.negative);
+
+ rule = get_rule(2);
+ cl_assert_strequal("pat2", rule->match.pattern);
+ cl_assert(rule->match.length == strlen("pat2"));
+ cl_assert(rule->match.directory);
+ cl_assert(!rule->match.fullpath);
+
+ rule = get_rule(3);
+ cl_assert_strequal("pat3dir/pat3file", rule->match.pattern);
+ cl_assert(!rule->match.directory);
+ cl_assert(rule->match.fullpath);
+
+ rule = get_rule(4);
+ cl_assert_strequal("pat4.*", rule->match.pattern);
+ cl_assert(!rule->match.negative);
+ cl_assert(!rule->match.directory);
+ cl_assert(!rule->match.fullpath);
+
+ rule = get_rule(5);
+ cl_assert_strequal("*.pat5", rule->match.pattern);
+
+ rule = get_rule(7);
+ cl_assert_strequal("pat7[a-e]??[xyz]", rule->match.pattern);
+ cl_assert(rule->assigns.length == 1);
+ assign = get_assign(rule,0);
+ cl_assert_strequal("attr7", assign->name);
+ cl_assert(assign->value == GIT_ATTR_TRUE);
+
+ rule = get_rule(8);
+ cl_assert_strequal("pat8 with spaces", rule->match.pattern);
+ cl_assert(rule->match.length == strlen("pat8 with spaces"));
+ cl_assert(!rule->match.negative);
+ cl_assert(!rule->match.directory);
+ cl_assert(!rule->match.fullpath);
+
+ rule = get_rule(9);
+ cl_assert_strequal("pat9", rule->match.pattern);
+
+ git_attr_file__free(file);
+}
+
+static void check_one_assign(
+ git_attr_file *file,
+ int rule_idx,
+ int assign_idx,
+ const char *pattern,
+ const char *name,
+ const char *value,
+ int is_allocated)
+{
+ git_attr_rule *rule = get_rule(rule_idx);
+ git_attr_assignment *assign = get_assign(rule, assign_idx);
+
+ cl_assert_strequal(pattern, rule->match.pattern);
+ cl_assert(rule->assigns.length == 1);
+ cl_assert_strequal(name, assign->name);
+ cl_assert(assign->name_hash == git_attr_file__name_hash(assign->name));
+ cl_assert(assign->name_len == strlen(name));
+ cl_assert(assign->is_allocated == is_allocated);
+ if (is_allocated)
+ cl_assert_strequal(value, assign->value);
+ else
+ cl_assert(assign->value == value);
+}
+
+void test_attr_file__assign_variants(void)
+{
+ git_attr_file *file = NULL;
+ git_attr_rule *rule;
+ git_attr_assignment *assign;
+
+ cl_git_pass(git_attr_file__from_file(&file, cl_fixture("attr/attr2")));
+ cl_assert_strequal(cl_fixture("attr/attr2"), file->path);
+ cl_assert(file->rules.length == 11);
+
+ check_one_assign(file, 0, 0, "pat0", "simple", GIT_ATTR_TRUE, 0);
+ check_one_assign(file, 1, 0, "pat1", "neg", GIT_ATTR_FALSE, 0);
+ check_one_assign(file, 2, 0, "*", "notundef", GIT_ATTR_TRUE, 0);
+ check_one_assign(file, 3, 0, "pat2", "notundef", NULL, 0);
+ check_one_assign(file, 4, 0, "pat3", "assigned", "test-value", 1);
+ check_one_assign(file, 5, 0, "pat4", "rule-with-more-chars", "value-with-more-chars", 1);
+ check_one_assign(file, 6, 0, "pat5", "empty", GIT_ATTR_TRUE, 0);
+ check_one_assign(file, 7, 0, "pat6", "negempty", GIT_ATTR_FALSE, 0);
+
+ rule = get_rule(8);
+ cl_assert_strequal("pat7", rule->match.pattern);
+ cl_assert(rule->assigns.length == 5);
+ /* assignments will be sorted by hash value, so we have to do
+ * lookups by search instead of by position
+ */
+ assign = git_attr_rule__lookup_assignment(rule, "multiple");
+ cl_assert(assign);
+ cl_assert_strequal("multiple", assign->name);
+ cl_assert(assign->value == GIT_ATTR_TRUE);
+ assign = git_attr_rule__lookup_assignment(rule, "single");
+ cl_assert(assign);
+ cl_assert_strequal("single", assign->name);
+ cl_assert(assign->value == GIT_ATTR_FALSE);
+ assign = git_attr_rule__lookup_assignment(rule, "values");
+ cl_assert(assign);
+ cl_assert_strequal("values", assign->name);
+ cl_assert_strequal("1", assign->value);
+ assign = git_attr_rule__lookup_assignment(rule, "also");
+ cl_assert(assign);
+ cl_assert_strequal("also", assign->name);
+ cl_assert_strequal("a-really-long-value/*", assign->value);
+ assign = git_attr_rule__lookup_assignment(rule, "happy");
+ cl_assert(assign);
+ cl_assert_strequal("happy", assign->name);
+ cl_assert_strequal("yes!", assign->value);
+ assign = git_attr_rule__lookup_assignment(rule, "other");
+ cl_assert(!assign);
+
+ rule = get_rule(9);
+ cl_assert_strequal("pat8", rule->match.pattern);
+ cl_assert(rule->assigns.length == 2);
+ assign = git_attr_rule__lookup_assignment(rule, "again");
+ cl_assert(assign);
+ cl_assert_strequal("again", assign->name);
+ cl_assert(assign->value == GIT_ATTR_TRUE);
+ assign = git_attr_rule__lookup_assignment(rule, "another");
+ cl_assert(assign);
+ cl_assert_strequal("another", assign->name);
+ cl_assert_strequal("12321", assign->value);
+
+ check_one_assign(file, 10, 0, "pat9", "at-eof", GIT_ATTR_FALSE, 0);
+
+ git_attr_file__free(file);
+}
+
+void test_attr_file__check_attr_examples(void)
+{
+ git_attr_file *file = NULL;
+ git_attr_rule *rule;
+ git_attr_assignment *assign;
+
+ cl_git_pass(git_attr_file__from_file(&file, cl_fixture("attr/attr3")));
+ cl_assert_strequal(cl_fixture("attr/attr3"), file->path);
+ cl_assert(file->rules.length == 3);
+
+ rule = get_rule(0);
+ cl_assert_strequal("*.java", rule->match.pattern);
+ cl_assert(rule->assigns.length == 3);
+ assign = git_attr_rule__lookup_assignment(rule, "diff");
+ cl_assert_strequal("diff", assign->name);
+ cl_assert_strequal("java", assign->value);
+ assign = git_attr_rule__lookup_assignment(rule, "crlf");
+ cl_assert_strequal("crlf", assign->name);
+ cl_assert(GIT_ATTR_FALSE == assign->value);
+ assign = git_attr_rule__lookup_assignment(rule, "myAttr");
+ cl_assert_strequal("myAttr", assign->name);
+ cl_assert(GIT_ATTR_TRUE == assign->value);
+ assign = git_attr_rule__lookup_assignment(rule, "missing");
+ cl_assert(assign == NULL);
+
+ rule = get_rule(1);
+ cl_assert_strequal("NoMyAttr.java", rule->match.pattern);
+ cl_assert(rule->assigns.length == 1);
+ assign = get_assign(rule, 0);
+ cl_assert_strequal("myAttr", assign->name);
+ cl_assert(assign->value == NULL);
+
+ rule = get_rule(2);
+ cl_assert_strequal("README", rule->match.pattern);
+ cl_assert(rule->assigns.length == 1);
+ assign = get_assign(rule, 0);
+ cl_assert_strequal("caveat", assign->name);
+ cl_assert_strequal("unspecified", assign->value);
+
+ git_attr_file__free(file);
+}
diff --git a/tests-clay/attr/lookup.c b/tests-clay/attr/lookup.c
new file mode 100644
index 000000000..870dcd343
--- /dev/null
+++ b/tests-clay/attr/lookup.c
@@ -0,0 +1,237 @@
+#include "clay_libgit2.h"
+#include "attr.h"
+
+void test_attr_lookup__simple(void)
+{
+ git_attr_file *file = NULL;
+ git_attr_path path;
+ const char *value = NULL;
+
+ cl_git_pass(git_attr_file__from_file(&file, cl_fixture("attr/attr0")));
+ cl_assert_strequal(cl_fixture("attr/attr0"), file->path);
+ cl_assert(file->rules.length == 1);
+
+ cl_git_pass(git_attr_path__init(&path, "test"));
+ cl_assert_strequal("test", path.path);
+ cl_assert_strequal("test", path.basename);
+ cl_assert(!path.is_dir);
+
+ cl_git_pass(git_attr_file__lookup_one(file,&path,"binary",&value));
+ cl_assert(value == GIT_ATTR_TRUE);
+
+ cl_git_pass(git_attr_file__lookup_one(file,&path,"missing",&value));
+ cl_assert(!value);
+
+ git_attr_file__free(file);
+}
+
+typedef struct {
+ const char *path;
+ const char *attr;
+ const char *expected;
+ int use_strcmp;
+ int force_dir;
+} test_case;
+
+static void run_test_cases(git_attr_file *file, test_case *cases)
+{
+ git_attr_path path;
+ const char *value = NULL;
+ test_case *c;
+ int error;
+
+ for (c = cases; c->path != NULL; c++) {
+ /* Put this in because I was surprised that all the tests passed */
+ /* fprintf(stderr, "checking '%s' attr %s == %s\n", */
+ /* c->path, c->attr, c->expected); */
+
+ cl_git_pass(git_attr_path__init(&path, c->path));
+
+ if (c->force_dir)
+ path.is_dir = 1;
+
+ error = git_attr_file__lookup_one(file,&path,c->attr,&value);
+ if (error != GIT_SUCCESS)
+ fprintf(stderr, "failure with %s %s %s\n", c->path, c->attr, c->expected);
+ cl_git_pass(error);
+
+ if (c->use_strcmp)
+ cl_assert_strequal(c->expected, value);
+ else
+ cl_assert(c->expected == value);
+ }
+}
+
+void test_attr_lookup__match_variants(void)
+{
+ git_attr_file *file = NULL;
+ git_attr_path path;
+ test_case cases[] = {
+ /* pat0 -> simple match */
+ { "pat0", "attr0", GIT_ATTR_TRUE, 0, 0 },
+ { "/testing/for/pat0", "attr0", GIT_ATTR_TRUE, 0, 0 },
+ { "relative/to/pat0", "attr0", GIT_ATTR_TRUE, 0, 0 },
+ { "this-contains-pat0-inside", "attr0", NULL, 0, 0 },
+ { "this-aint-right", "attr0", NULL, 0, 0 },
+ { "/this/pat0/dont/match", "attr0", NULL, 0, 0 },
+ /* negative match */
+ { "pat0", "attr1", GIT_ATTR_TRUE, 0, 0 },
+ { "pat1", "attr1", NULL, 0, 0 },
+ { "/testing/for/pat1", "attr1", NULL, 0, 0 },
+ { "/testing/for/pat0", "attr1", GIT_ATTR_TRUE, 0, 0 },
+ { "/testing/for/pat1/inside", "attr1", GIT_ATTR_TRUE, 0, 0 },
+ { "misc", "attr1", GIT_ATTR_TRUE, 0, 0 },
+ /* dir match */
+ { "pat2", "attr2", NULL, 0, 0 },
+ { "pat2", "attr2", GIT_ATTR_TRUE, 0, 1 },
+ { "/testing/for/pat2", "attr2", NULL, 0, 0 },
+ { "/testing/for/pat2", "attr2", GIT_ATTR_TRUE, 0, 1 },
+ { "/not/pat2/yousee", "attr2", NULL, 0, 0 },
+ { "/not/pat2/yousee", "attr2", NULL, 0, 1 },
+ /* path match */
+ { "pat3file", "attr3", NULL, 0, 0 },
+ { "/pat3dir/pat3file", "attr3", NULL, 0, 0 },
+ { "pat3dir/pat3file", "attr3", GIT_ATTR_TRUE, 0, 0 },
+ /* pattern* match */
+ { "pat4.txt", "attr4", GIT_ATTR_TRUE, 0, 0 },
+ { "/fun/fun/fun/pat4.c", "attr4", GIT_ATTR_TRUE, 0, 0 },
+ { "pat4.", "attr4", GIT_ATTR_TRUE, 0, 0 },
+ { "pat4", "attr4", NULL, 0, 0 },
+ { "/fun/fun/fun/pat4.dir", "attr4", GIT_ATTR_TRUE, 0, 1 },
+ /* *pattern match */
+ { "foo.pat5", "attr5", GIT_ATTR_TRUE, 0, 0 },
+ { "foo.pat5", "attr5", GIT_ATTR_TRUE, 0, 1 },
+ { "/this/is/ok.pat5", "attr5", GIT_ATTR_TRUE, 0, 0 },
+ { "/this/is/bad.pat5/yousee.txt", "attr5", NULL, 0, 0 },
+ { "foo.pat5", "attr100", NULL, 0, 0 },
+ /* glob match with slashes */
+ { "foo.pat6", "attr6", NULL, 0, 0 },
+ { "pat6/pat6/foobar.pat6", "attr6", GIT_ATTR_TRUE, 0, 0 },
+ { "pat6/pat6/.pat6", "attr6", GIT_ATTR_TRUE, 0, 0 },
+ { "pat6/pat6/extra/foobar.pat6", "attr6", NULL, 0, 0 },
+ { "/prefix/pat6/pat6/foobar.pat6", "attr6", NULL, 0, 0 },
+ { "/pat6/pat6/foobar.pat6", "attr6", NULL, 0, 0 },
+ /* complex pattern */
+ { "pat7a12z", "attr7", GIT_ATTR_TRUE, 0, 0 },
+ { "pat7e__x", "attr7", GIT_ATTR_TRUE, 0, 0 },
+ { "pat7b/1y", "attr7", NULL, 0, 0 }, /* ? does not match / */
+ { "pat7e_x", "attr7", NULL, 0, 0 },
+ { "pat7aaaa", "attr7", NULL, 0, 0 },
+ { "pat7zzzz", "attr7", NULL, 0, 0 },
+ { "/this/can/be/anything/pat7a12z", "attr7", GIT_ATTR_TRUE, 0, 0 },
+ { "but/it/still/must/match/pat7aaaa", "attr7", NULL, 0, 0 },
+ { "pat7aaay.fail", "attr7", NULL, 0, 0 },
+ /* pattern with spaces */
+ { "pat8 with spaces", "attr8", GIT_ATTR_TRUE, 0, 0 },
+ { "/gotta love/pat8 with spaces", "attr8", GIT_ATTR_TRUE, 0, 0 },
+ { "failing pat8 with spaces", "attr8", NULL, 0, 0 },
+ { "spaces", "attr8", NULL, 0, 0 },
+ /* pattern at eof */
+ { "pat9", "attr9", GIT_ATTR_TRUE, 0, 0 },
+ { "/eof/pat9", "attr9", GIT_ATTR_TRUE, 0, 0 },
+ { "pat", "attr9", NULL, 0, 0 },
+ { "at9", "attr9", NULL, 0, 0 },
+ { "pat9.fail", "attr9", NULL, 0, 0 },
+ /* sentinel at end */
+ { NULL, NULL, NULL, 0, 0 }
+ };
+
+ cl_git_pass(git_attr_file__from_file(&file, cl_fixture("attr/attr1")));
+ cl_assert_strequal(cl_fixture("attr/attr1"), file->path);
+ cl_assert(file->rules.length == 10);
+
+ cl_git_pass(git_attr_path__init(&path, "/testing/for/pat0"));
+ cl_assert_strequal("pat0", path.basename);
+
+ run_test_cases(file, cases);
+
+ git_attr_file__free(file);
+}
+
+void test_attr_lookup__assign_variants(void)
+{
+ git_attr_file *file = NULL;
+ test_case cases[] = {
+ /* pat0 -> simple assign */
+ { "pat0", "simple", GIT_ATTR_TRUE, 0, 0 },
+ { "/testing/pat0", "simple", GIT_ATTR_TRUE, 0, 0 },
+ { "pat0", "fail", NULL, 0, 0 },
+ { "/testing/pat0", "fail", NULL, 0, 0 },
+ /* negative assign */
+ { "pat1", "neg", GIT_ATTR_FALSE, 0, 0 },
+ { "/testing/pat1", "neg", GIT_ATTR_FALSE, 0, 0 },
+ { "pat1", "fail", NULL, 0, 0 },
+ { "/testing/pat1", "fail", NULL, 0, 0 },
+ /* forced undef */
+ { "pat1", "notundef", GIT_ATTR_TRUE, 0, 0 },
+ { "pat2", "notundef", NULL, 0, 0 },
+ { "/lead/in/pat1", "notundef", GIT_ATTR_TRUE, 0, 0 },
+ { "/lead/in/pat2", "notundef", NULL, 0, 0 },
+ /* assign value */
+ { "pat3", "assigned", "test-value", 1, 0 },
+ { "pat3", "notassigned", NULL, 0, 0 },
+ /* assign value */
+ { "pat4", "rule-with-more-chars", "value-with-more-chars", 1, 0 },
+ { "pat4", "notassigned-rule-with-more-chars", NULL, 0, 0 },
+ /* empty assignments */
+ { "pat5", "empty", GIT_ATTR_TRUE, 0, 0 },
+ { "pat6", "negempty", GIT_ATTR_FALSE, 0, 0 },
+ /* multiple assignment */
+ { "pat7", "multiple", GIT_ATTR_TRUE, 0, 0 },
+ { "pat7", "single", GIT_ATTR_FALSE, 0, 0 },
+ { "pat7", "values", "1", 1, 0 },
+ { "pat7", "also", "a-really-long-value/*", 1, 0 },
+ { "pat7", "happy", "yes!", 1, 0 },
+ { "pat8", "again", GIT_ATTR_TRUE, 0, 0 },
+ { "pat8", "another", "12321", 1, 0 },
+ /* bad assignment */
+ { "patbad0", "simple", NULL, 0, 0 },
+ { "patbad0", "notundef", GIT_ATTR_TRUE, 0, 0 },
+ { "patbad1", "simple", NULL, 0, 0 },
+ /* eof assignment */
+ { "pat9", "at-eof", GIT_ATTR_FALSE, 0, 0 },
+ /* sentinel at end */
+ { NULL, NULL, NULL, 0, 0 }
+ };
+
+ cl_git_pass(git_attr_file__from_file(&file, cl_fixture("attr/attr2")));
+ cl_assert(file->rules.length == 11);
+
+ run_test_cases(file, cases);
+
+ git_attr_file__free(file);
+}
+
+void test_attr_lookup__check_attr_examples(void)
+{
+ git_attr_file *file = NULL;
+ test_case cases[] = {
+ { "foo.java", "diff", "java", 1, 0 },
+ { "foo.java", "crlf", GIT_ATTR_FALSE, 0, 0 },
+ { "foo.java", "myAttr", GIT_ATTR_TRUE, 0, 0 },
+ { "foo.java", "other", NULL, 0, 0 },
+ { "/prefix/dir/foo.java", "diff", "java", 1, 0 },
+ { "/prefix/dir/foo.java", "crlf", GIT_ATTR_FALSE, 0, 0 },
+ { "/prefix/dir/foo.java", "myAttr", GIT_ATTR_TRUE, 0, 0 },
+ { "/prefix/dir/foo.java", "other", NULL, 0, 0 },
+ { "NoMyAttr.java", "crlf", GIT_ATTR_FALSE, 0, 0 },
+ { "NoMyAttr.java", "myAttr", NULL, 0, 0 },
+ { "NoMyAttr.java", "other", NULL, 0, 0 },
+ { "/prefix/dir/NoMyAttr.java", "crlf", GIT_ATTR_FALSE, 0, 0 },
+ { "/prefix/dir/NoMyAttr.java", "myAttr", NULL, 0, 0 },
+ { "/prefix/dir/NoMyAttr.java", "other", NULL, 0, 0 },
+ { "README", "caveat", "unspecified", 1, 0 },
+ { "/specific/path/README", "caveat", "unspecified", 1, 0 },
+ { "README", "missing", NULL, 0, 0 },
+ { "/specific/path/README", "missing", NULL, 0, 0 },
+ /* sentinel at end */
+ { NULL, NULL, NULL, 0, 0 }
+ };
+
+ cl_git_pass(git_attr_file__from_file(&file, cl_fixture("attr/attr3")));
+ cl_assert(file->rules.length == 3);
+
+ run_test_cases(file, cases);
+
+ git_attr_file__free(file);
+}
diff --git a/tests-clay/attr/repo.c b/tests-clay/attr/repo.c
new file mode 100644
index 000000000..b6815f3ba
--- /dev/null
+++ b/tests-clay/attr/repo.c
@@ -0,0 +1,140 @@
+#include "clay_libgit2.h"
+#include "fileops.h"
+#include "git2/attr.h"
+
+static git_repository *g_repo = NULL;
+
+void test_attr_repo__initialize(void)
+{
+ /* before each test, instantiate the attr repo from the fixtures and
+ * rename the .gitted to .git so it is a repo with a working dir.
+ */
+ cl_fixture_sandbox("attr");
+ cl_git_pass(p_rename("attr/.gitted", "attr/.git"));
+ cl_git_pass(git_repository_open(&g_repo, "attr/.git"));
+}
+
+void test_attr_repo__cleanup(void)
+{
+ git_repository_free(g_repo);
+ g_repo = NULL;
+ cl_fixture_cleanup("attr");
+}
+
+void test_attr_repo__get_one(void)
+{
+ const char *value;
+ struct {
+ const char *file;
+ const char *attr;
+ const char *expected;
+ } test_cases[] = {
+ { "root_test1", "repoattr", GIT_ATTR_TRUE },
+ { "root_test1", "rootattr", GIT_ATTR_TRUE },
+ { "root_test1", "missingattr", NULL },
+ { "root_test1", "subattr", NULL },
+ { "root_test1", "negattr", NULL },
+ { "root_test2", "repoattr", GIT_ATTR_TRUE },
+ { "root_test2", "rootattr", GIT_ATTR_FALSE },
+ { "root_test2", "missingattr", NULL },
+ { "root_test3", "repoattr", GIT_ATTR_TRUE },
+ { "root_test3", "rootattr", NULL },
+ { "subdir/subdir_test1", "repoattr", GIT_ATTR_TRUE },
+ { "subdir/subdir_test1", "rootattr", GIT_ATTR_TRUE },
+ { "subdir/subdir_test1", "missingattr", NULL },
+ { "subdir/subdir_test1", "subattr", "yes" },
+ { "subdir/subdir_test1", "negattr", GIT_ATTR_FALSE },
+ { "subdir/subdir_test1", "another", NULL },
+ { "subdir/subdir_test2.txt", "repoattr", GIT_ATTR_TRUE },
+ { "subdir/subdir_test2.txt", "rootattr", GIT_ATTR_TRUE },
+ { "subdir/subdir_test2.txt", "missingattr", NULL },
+ { "subdir/subdir_test2.txt", "subattr", "yes" },
+ { "subdir/subdir_test2.txt", "negattr", GIT_ATTR_FALSE },
+ { "subdir/subdir_test2.txt", "another", "one" },
+ { NULL, NULL, NULL }
+ }, *scan;
+
+ for (scan = test_cases; scan->file != NULL; scan++) {
+ git_buf b = GIT_BUF_INIT;
+
+ git_buf_printf(&b, "%s:%s == expect %s",
+ scan->file, scan->attr, scan->expected);
+
+ cl_must_pass_(
+ git_attr_get(g_repo, scan->file, scan->attr, &value) == GIT_SUCCESS,
+ b.ptr);
+
+ git_buf_printf(&b, ", got %s", value);
+
+ if (scan->expected == NULL ||
+ scan->expected == GIT_ATTR_TRUE ||
+ scan->expected == GIT_ATTR_FALSE)
+ {
+ cl_assert_(scan->expected == value, b.ptr);
+ } else {
+ cl_assert_strequal(scan->expected, value);
+ }
+
+ git_buf_free(&b);
+ }
+}
+
+void test_attr_repo__get_many(void)
+{
+ const char *names[4] = { "repoattr", "rootattr", "missingattr", "subattr" };
+ const char *values[4];
+
+ cl_git_pass(git_attr_get_many(g_repo, "root_test1", 4, names, values));
+
+ cl_assert(values[0] == GIT_ATTR_TRUE);
+ cl_assert(values[1] == GIT_ATTR_TRUE);
+ cl_assert(values[2] == NULL);
+ cl_assert(values[3] == NULL);
+
+ cl_git_pass(git_attr_get_many(g_repo, "root_test2", 4, names, values));
+
+ cl_assert(values[0] == GIT_ATTR_TRUE);
+ cl_assert(values[1] == GIT_ATTR_FALSE);
+ cl_assert(values[2] == NULL);
+ cl_assert(values[3] == NULL);
+
+ cl_git_pass(git_attr_get_many(g_repo, "subdir/subdir_test1", 4, names, values));
+
+ cl_assert(values[0] == GIT_ATTR_TRUE);
+ cl_assert(values[1] == GIT_ATTR_TRUE);
+ cl_assert(values[2] == NULL);
+ cl_assert_strequal("yes", values[3]);
+
+}
+
+static int count_attrs(
+ const char *GIT_UNUSED(name),
+ const char *GIT_UNUSED(value),
+ void *payload)
+{
+ GIT_UNUSED_ARG(name);
+ GIT_UNUSED_ARG(value);
+
+ *((int *)payload) += 1;
+
+ return GIT_SUCCESS;
+}
+
+void test_attr_repo__foreach(void)
+{
+ int count;
+
+ count = 0;
+ cl_git_pass(git_attr_foreach(g_repo, "root_test1", &count_attrs, &count));
+ cl_assert(count == 2);
+
+ count = 0;
+ cl_git_pass(git_attr_foreach(g_repo, "subdir/subdir_test1",
+ &count_attrs, &count));
+ cl_assert(count == 4); /* repoattr, rootattr, subattr, negattr */
+
+ count = 0;
+ cl_git_pass(git_attr_foreach(g_repo, "subdir/subdir_test2.txt",
+ &count_attrs, &count));
+ cl_assert(count == 5); /* repoattr, rootattr, subattr, negattr, another */
+}
diff --git a/tests-clay/clay.h b/tests-clay/clay.h
index 210273532..8237991c0 100644
--- a/tests-clay/clay.h
+++ b/tests-clay/clay.h
@@ -59,6 +59,19 @@ void cl_fixture_cleanup(const char *fixture_name);
*/
extern void clay_on_init(void);
extern void clay_on_shutdown(void);
+extern void test_attr_file__assign_variants(void);
+extern void test_attr_file__check_attr_examples(void);
+extern void test_attr_file__match_variants(void);
+extern void test_attr_file__simple_read(void);
+extern void test_attr_lookup__assign_variants(void);
+extern void test_attr_lookup__check_attr_examples(void);
+extern void test_attr_lookup__match_variants(void);
+extern void test_attr_lookup__simple(void);
+extern void test_attr_repo__cleanup(void);
+extern void test_attr_repo__foreach(void);
+extern void test_attr_repo__get_many(void);
+extern void test_attr_repo__get_one(void);
+extern void test_attr_repo__initialize(void);
extern void test_buf_basic__printf(void);
extern void test_buf_basic__resize(void);
extern void test_config_add__cleanup(void);
diff --git a/tests-clay/clay_main.c b/tests-clay/clay_main.c
index af9e08877..49a867698 100644
--- a/tests-clay/clay_main.c
+++ b/tests-clay/clay_main.c
@@ -108,6 +108,23 @@ static int clay_sandbox(void);
#define clay_on_suite() /* nop */
/* Autogenerated test data by clay */
+static const struct clay_func _clay_cb_attr_file[] = {
+ {"assign_variants", &test_attr_file__assign_variants},
+ {"check_attr_examples", &test_attr_file__check_attr_examples},
+ {"match_variants", &test_attr_file__match_variants},
+ {"simple_read", &test_attr_file__simple_read}
+};
+static const struct clay_func _clay_cb_attr_lookup[] = {
+ {"assign_variants", &test_attr_lookup__assign_variants},
+ {"check_attr_examples", &test_attr_lookup__check_attr_examples},
+ {"match_variants", &test_attr_lookup__match_variants},
+ {"simple", &test_attr_lookup__simple}
+};
+static const struct clay_func _clay_cb_attr_repo[] = {
+ {"foreach", &test_attr_repo__foreach},
+ {"get_many", &test_attr_repo__get_many},
+ {"get_one", &test_attr_repo__get_one}
+};
static const struct clay_func _clay_cb_buf_basic[] = {
{"printf", &test_buf_basic__printf},
{"resize", &test_buf_basic__resize}
@@ -303,6 +320,24 @@ static const struct clay_func _clay_cb_status_worktree[] = {
static const struct clay_suite _clay_suites[] = {
{
+ "attr::file",
+ {NULL, NULL},
+ {NULL, NULL},
+ _clay_cb_attr_file, 4
+ },
+ {
+ "attr::lookup",
+ {NULL, NULL},
+ {NULL, NULL},
+ _clay_cb_attr_lookup, 4
+ },
+ {
+ "attr::repo",
+ {"initialize", &test_attr_repo__initialize},
+ {"cleanup", &test_attr_repo__cleanup},
+ _clay_cb_attr_repo, 3
+ },
+ {
"buf::basic",
{NULL, NULL},
{NULL, NULL},
@@ -520,8 +555,8 @@ static const struct clay_suite _clay_suites[] = {
}
};
-static size_t _clay_suite_count = 36;
-static size_t _clay_callback_count = 120;
+static size_t _clay_suite_count = 39;
+static size_t _clay_callback_count = 131;
/* Core test functions */
static void
diff --git a/tests/resources/attr/.gitattributes b/tests/resources/attr/.gitattributes
new file mode 100644
index 000000000..f2c6d717c
--- /dev/null
+++ b/tests/resources/attr/.gitattributes
@@ -0,0 +1,4 @@
+* rootattr
+root_test2 -rootattr
+root_test3 !rootattr
+
diff --git a/tests/resources/attr/.gitted/HEAD b/tests/resources/attr/.gitted/HEAD
new file mode 100644
index 000000000..cb089cd89
--- /dev/null
+++ b/tests/resources/attr/.gitted/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/resources/attr/.gitted/config b/tests/resources/attr/.gitted/config
new file mode 100644
index 000000000..af107929f
--- /dev/null
+++ b/tests/resources/attr/.gitted/config
@@ -0,0 +1,6 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = false
+ logallrefupdates = true
+ ignorecase = true
diff --git a/tests/resources/attr/.gitted/description b/tests/resources/attr/.gitted/description
new file mode 100644
index 000000000..498b267a8
--- /dev/null
+++ b/tests/resources/attr/.gitted/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/resources/attr/.gitted/index b/tests/resources/attr/.gitted/index
new file mode 100644
index 000000000..6841fb2ec
--- /dev/null
+++ b/tests/resources/attr/.gitted/index
Binary files differ
diff --git a/tests/resources/attr/.gitted/info/attributes b/tests/resources/attr/.gitted/info/attributes
new file mode 100644
index 000000000..93efc0c34
--- /dev/null
+++ b/tests/resources/attr/.gitted/info/attributes
@@ -0,0 +1,2 @@
+* repoattr
+
diff --git a/tests/resources/attr/.gitted/info/exclude b/tests/resources/attr/.gitted/info/exclude
new file mode 100644
index 000000000..a5196d1be
--- /dev/null
+++ b/tests/resources/attr/.gitted/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/resources/attr/.gitted/logs/HEAD b/tests/resources/attr/.gitted/logs/HEAD
new file mode 100644
index 000000000..cfd1f9525
--- /dev/null
+++ b/tests/resources/attr/.gitted/logs/HEAD
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000 6bab5c79cd5140d0f800917f550eb2a3dc32b0da Russell Belfer <arrbee@arrbee.com> 1324416995 -0800 commit (initial): initial test data
diff --git a/tests/resources/attr/.gitted/logs/refs/heads/master b/tests/resources/attr/.gitted/logs/refs/heads/master
new file mode 100644
index 000000000..cfd1f9525
--- /dev/null
+++ b/tests/resources/attr/.gitted/logs/refs/heads/master
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000 6bab5c79cd5140d0f800917f550eb2a3dc32b0da Russell Belfer <arrbee@arrbee.com> 1324416995 -0800 commit (initial): initial test data
diff --git a/tests/resources/attr/.gitted/objects/29/29de282ce999e95183aedac6451d3384559c4b b/tests/resources/attr/.gitted/objects/29/29de282ce999e95183aedac6451d3384559c4b
new file mode 100644
index 000000000..ad84f0854
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/29/29de282ce999e95183aedac6451d3384559c4b
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/2c/66e14f77196ea763fb1e41612c1aa2bc2d8ed2 b/tests/resources/attr/.gitted/objects/2c/66e14f77196ea763fb1e41612c1aa2bc2d8ed2
new file mode 100644
index 000000000..4b75d50eb
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/2c/66e14f77196ea763fb1e41612c1aa2bc2d8ed2
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/2d/e7dfe3588f3c7e9ad59e7d50ba90e3329df9d9 b/tests/resources/attr/.gitted/objects/2d/e7dfe3588f3c7e9ad59e7d50ba90e3329df9d9
new file mode 100644
index 000000000..e0fd0468e
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/2d/e7dfe3588f3c7e9ad59e7d50ba90e3329df9d9
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/3b/74db7ab381105dc0d28f8295a77f6a82989292 b/tests/resources/attr/.gitted/objects/3b/74db7ab381105dc0d28f8295a77f6a82989292
new file mode 100644
index 000000000..e5cef35fa
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/3b/74db7ab381105dc0d28f8295a77f6a82989292
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/45/141a79a77842c59a63229403220a4e4be74e3d b/tests/resources/attr/.gitted/objects/45/141a79a77842c59a63229403220a4e4be74e3d
new file mode 100644
index 000000000..5b58ef024
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/45/141a79a77842c59a63229403220a4e4be74e3d
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/55/6f8c827b8e4a02ad5cab77dca2bcb3e226b0b3 b/tests/resources/attr/.gitted/objects/55/6f8c827b8e4a02ad5cab77dca2bcb3e226b0b3
new file mode 100644
index 000000000..4bcff1faa
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/55/6f8c827b8e4a02ad5cab77dca2bcb3e226b0b3
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/6b/ab5c79cd5140d0f800917f550eb2a3dc32b0da b/tests/resources/attr/.gitted/objects/6b/ab5c79cd5140d0f800917f550eb2a3dc32b0da
new file mode 100644
index 000000000..f51e11ccc
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/6b/ab5c79cd5140d0f800917f550eb2a3dc32b0da
@@ -0,0 +1,3 @@
+x 0 E)@d'~@(#tQiQn޷(Pm"Ř2hs L+d{"{Z`u
+O4Y[޷;@>MSOmʧh
+* <- \ No newline at end of file
diff --git a/tests/resources/attr/.gitted/objects/c0/091889c0c77142b87a1fa5123a6398a61d33e7 b/tests/resources/attr/.gitted/objects/c0/091889c0c77142b87a1fa5123a6398a61d33e7
new file mode 100644
index 000000000..11dc63c79
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/c0/091889c0c77142b87a1fa5123a6398a61d33e7
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/c4/85abe35abd4aa6fd83b076a78bbea9e2e7e06c b/tests/resources/attr/.gitted/objects/c4/85abe35abd4aa6fd83b076a78bbea9e2e7e06c
new file mode 100644
index 000000000..58569ca0e
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/c4/85abe35abd4aa6fd83b076a78bbea9e2e7e06c
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/c7/aadd770d5907a8475c29e9ee21a27b88bf675d b/tests/resources/attr/.gitted/objects/c7/aadd770d5907a8475c29e9ee21a27b88bf675d
new file mode 100644
index 000000000..39aedb7d9
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/c7/aadd770d5907a8475c29e9ee21a27b88bf675d
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/dc/cada462d3df8ac6de596fb8c896aba9344f941 b/tests/resources/attr/.gitted/objects/dc/cada462d3df8ac6de596fb8c896aba9344f941
new file mode 100644
index 000000000..ef62f8b9d
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/dc/cada462d3df8ac6de596fb8c896aba9344f941
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/e5/63cf4758f0d646f1b14b76016aa17fa9e549a4 b/tests/resources/attr/.gitted/objects/e5/63cf4758f0d646f1b14b76016aa17fa9e549a4
new file mode 100644
index 000000000..1bc1f0f0b
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/e5/63cf4758f0d646f1b14b76016aa17fa9e549a4
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/f2/c6d717cf4a5a3e6b02684155ab07b766982165 b/tests/resources/attr/.gitted/objects/f2/c6d717cf4a5a3e6b02684155ab07b766982165
new file mode 100644
index 000000000..27a25dc86
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/f2/c6d717cf4a5a3e6b02684155ab07b766982165
Binary files differ
diff --git a/tests/resources/attr/.gitted/objects/fb/5067b1aef3ac1ada4b379dbcb7d17255df7d78 b/tests/resources/attr/.gitted/objects/fb/5067b1aef3ac1ada4b379dbcb7d17255df7d78
new file mode 100644
index 000000000..6c8ff837e
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/fb/5067b1aef3ac1ada4b379dbcb7d17255df7d78
Binary files differ
diff --git a/tests/resources/attr/.gitted/refs/heads/master b/tests/resources/attr/.gitted/refs/heads/master
new file mode 100644
index 000000000..279272e5c
--- /dev/null
+++ b/tests/resources/attr/.gitted/refs/heads/master
@@ -0,0 +1 @@
+6bab5c79cd5140d0f800917f550eb2a3dc32b0da
diff --git a/tests/resources/attr/attr0 b/tests/resources/attr/attr0
new file mode 100644
index 000000000..556f8c827
--- /dev/null
+++ b/tests/resources/attr/attr0
@@ -0,0 +1 @@
+* binary
diff --git a/tests/resources/attr/attr1 b/tests/resources/attr/attr1
new file mode 100644
index 000000000..3b74db7ab
--- /dev/null
+++ b/tests/resources/attr/attr1
@@ -0,0 +1,29 @@
+# a comment followed by some blank lines
+
+
+
+ # another comment that is indented
+
+# variations on fnmatch
+
+pat0 attr0
+!pat1 attr1
+pat2/ attr2
+pat3dir/pat3file attr3
+pat4.* attr4
+ *.pat5 attr5
+pat6/pat6/*.pat6 attr6
+
+pat7[a-e]??[xyz] attr7 # with a comment on the line
+
+pat8\ with\ spaces attr8
+
+ invalid # attr with no assignments doesn't count
+
+also/invalid
+
+invalid.again/
+
+# next attr is at eof
+
+ pat9 attr9 \ No newline at end of file
diff --git a/tests/resources/attr/attr2 b/tests/resources/attr/attr2
new file mode 100644
index 000000000..2c66e14f7
--- /dev/null
+++ b/tests/resources/attr/attr2
@@ -0,0 +1,21 @@
+
+# variations on assignments
+
+pat0 simple
+pat1 -neg
+* notundef
+pat2 !notundef
+pat3 assigned=test-value
+pat4 rule-with-more-chars=value-with-more-chars
+pat5 empty=
+pat6 -negempty=
+pat7 multiple -single values=1 also=a-really-long-value/* happy=yes!
+# the next line has trailing spaces
+pat8 again= another=12321
+patbad0 # empty assignment does not count
+# next line will be another simple empty assign that should not count
+ patbad1
+
+# BTW I think there are 11 valid rules and two "invalid" empty ones
+
+pat9 -at-eof \ No newline at end of file
diff --git a/tests/resources/attr/attr3 b/tests/resources/attr/attr3
new file mode 100644
index 000000000..c485abe35
--- /dev/null
+++ b/tests/resources/attr/attr3
@@ -0,0 +1,4 @@
+# These are examples from the git-check-attr.1 man page
+*.java diff=java -crlf myAttr
+NoMyAttr.java !myAttr
+README caveat=unspecified
diff --git a/tests/resources/attr/root_test1 b/tests/resources/attr/root_test1
new file mode 100644
index 000000000..45141a79a
--- /dev/null
+++ b/tests/resources/attr/root_test1
@@ -0,0 +1 @@
+Hello from the root
diff --git a/tests/resources/attr/root_test2 b/tests/resources/attr/root_test2
new file mode 100644
index 000000000..45141a79a
--- /dev/null
+++ b/tests/resources/attr/root_test2
@@ -0,0 +1 @@
+Hello from the root
diff --git a/tests/resources/attr/root_test3 b/tests/resources/attr/root_test3
new file mode 100644
index 000000000..45141a79a
--- /dev/null
+++ b/tests/resources/attr/root_test3
@@ -0,0 +1 @@
+Hello from the root
diff --git a/tests/resources/attr/root_test4.txt b/tests/resources/attr/root_test4.txt
new file mode 100644
index 000000000..fb5067b1a
--- /dev/null
+++ b/tests/resources/attr/root_test4.txt
@@ -0,0 +1 @@
+Hello again
diff --git a/tests/resources/attr/subdir/.gitattributes b/tests/resources/attr/subdir/.gitattributes
new file mode 100644
index 000000000..210f3a8ba
--- /dev/null
+++ b/tests/resources/attr/subdir/.gitattributes
@@ -0,0 +1,3 @@
+* subattr=yes -negattr
+subdir/*.txt another=one
+
diff --git a/tests/resources/attr/subdir/subdir_test1 b/tests/resources/attr/subdir/subdir_test1
new file mode 100644
index 000000000..e563cf475
--- /dev/null
+++ b/tests/resources/attr/subdir/subdir_test1
@@ -0,0 +1,2 @@
+Hello from the subdir
+
diff --git a/tests/resources/attr/subdir/subdir_test2.txt b/tests/resources/attr/subdir/subdir_test2.txt
new file mode 100644
index 000000000..fb5067b1a
--- /dev/null
+++ b/tests/resources/attr/subdir/subdir_test2.txt
@@ -0,0 +1 @@
+Hello again
diff --git a/tests/resources/attr/subdir2/subdir2_test1 b/tests/resources/attr/subdir2/subdir2_test1
new file mode 100644
index 000000000..dccada462
--- /dev/null
+++ b/tests/resources/attr/subdir2/subdir2_test1
@@ -0,0 +1 @@
+Hello from subdir2