+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include "ignore.h"
+
#include "git2/ignore.h"
#include "common.h"
-#include "ignore.h"
#include "attrcache.h"
#include "path.h"
#include "config.h"
+#include "wildmatch.h"
#define GIT_IGNORE_INTERNAL "[internal]exclude"
#define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n"
+/**
+ * A negative ignore pattern can negate a positive one without
+ * wildcards if it is a basename only and equals the basename of
+ * the positive pattern. Thus
+ *
+ * foo/bar
+ * !bar
+ *
+ * would result in foo/bar being unignored again while
+ *
+ * moo/foo/bar
+ * !foo/bar
+ *
+ * would do nothing. The reverse also holds true: a positive
+ * basename pattern can be negated by unignoring the basename in
+ * subdirectories. Thus
+ *
+ * bar
+ * !foo/bar
+ *
+ * would result in foo/bar being unignored again. As with the
+ * first case,
+ *
+ * foo/bar
+ * !moo/foo/bar
+ *
+ * would do nothing, again.
+ */
+static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
+{
+ int (*cmp)(const char *, const char *, size_t);
+ git_attr_fnmatch *longer, *shorter;
+ char *p;
+
+ if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0
+ || (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0)
+ return false;
+
+ if (neg->flags & GIT_ATTR_FNMATCH_ICASE)
+ cmp = git__strncasecmp;
+ else
+ cmp = git__strncmp;
+
+ /* If lengths match we need to have an exact match */
+ if (rule->length == neg->length) {
+ return cmp(rule->pattern, neg->pattern, rule->length) == 0;
+ } else if (rule->length < neg->length) {
+ shorter = rule;
+ longer = neg;
+ } else {
+ shorter = neg;
+ longer = rule;
+ }
+
+ /* Otherwise, we need to check if the shorter
+ * rule is a basename only (that is, it contains
+ * no path separator) and, if so, if it
+ * matches the tail of the longer rule */
+ p = longer->pattern + longer->length - shorter->length;
+
+ if (p[-1] != '/')
+ return false;
+ if (memchr(shorter->pattern, '/', shorter->length) != NULL)
+ return false;
+
+ return cmp(p, shorter->pattern, shorter->length) == 0;
+}
+
+/**
+ * A negative ignore can only unignore a file which is given explicitly before, thus
+ *
+ * foo
+ * !foo/bar
+ *
+ * does not unignore 'foo/bar' as it's not in the list. However
+ *
+ * foo/<star>
+ * !foo/bar
+ *
+ * does unignore 'foo/bar', as it is contained within the 'foo/<star>' rule.
+ */
+static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match)
+{
+ int error = 0, wildmatch_flags;
+ size_t i;
+ git_attr_fnmatch *rule;
+ char *path;
+ git_buf buf = GIT_BUF_INIT;
+
+ *out = 0;
+
+ wildmatch_flags = WM_PATHNAME;
+ if (match->flags & GIT_ATTR_FNMATCH_ICASE)
+ wildmatch_flags |= WM_CASEFOLD;
+
+ /* path of the file relative to the workdir, so we match the rules in subdirs */
+ if (match->containing_dir) {
+ git_buf_puts(&buf, match->containing_dir);
+ }
+ if (git_buf_puts(&buf, match->pattern) < 0)
+ return -1;
+
+ path = git_buf_detach(&buf);
+
+ git_vector_foreach(rules, i, rule) {
+ if (!(rule->flags & GIT_ATTR_FNMATCH_HASWILD)) {
+ if (does_negate_pattern(rule, match)) {
+ error = 0;
+ *out = 1;
+ goto out;
+ }
+ else
+ continue;
+ }
+
+ git_buf_clear(&buf);
+ if (rule->containing_dir)
+ git_buf_puts(&buf, rule->containing_dir);
+ git_buf_puts(&buf, rule->pattern);
+
+ if (git_buf_oom(&buf))
+ goto out;
+
+ /* if we found a match, we want to keep this rule */
+ if ((wildmatch(git_buf_cstr(&buf), path, wildmatch_flags)) == WM_MATCH) {
+ *out = 1;
+ error = 0;
+ goto out;
+ }
+ }
+
+ error = 0;
+
+out:
+ git__free(path);
+ git_buf_dispose(&buf);
+ return error;
+}
+
static int parse_ignore_file(
- git_repository *repo, git_attr_file *attrs, const char *data)
+ git_repository *repo, git_attr_file *attrs, const char *data, bool allow_macros)
{
int error = 0;
int ignore_case = false;
const char *scan = data, *context = NULL;
git_attr_fnmatch *match = NULL;
- if (git_repository__cvar(&ignore_case, repo, GIT_CVAR_IGNORECASE) < 0)
- giterr_clear();
+ GIT_UNUSED(allow_macros);
+
+ if (git_repository__configmap_lookup(&ignore_case, repo, GIT_CONFIGMAP_IGNORECASE) < 0)
+ git_error_clear();
/* if subdir file path, convert context for file paths */
if (attrs->entry &&
context = attrs->entry->path;
if (git_mutex_lock(&attrs->lock) < 0) {
- giterr_set(GITERR_OS, "Failed to lock ignore file");
+ git_error_set(GIT_ERROR_OS, "failed to lock ignore file");
return -1;
}
while (!error && *scan) {
+ int valid_rule = 1;
+
if (!match && !(match = git__calloc(1, sizeof(*match)))) {
error = -1;
break;
}
- match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
+ match->flags =
+ GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
if (!(error = git_attr_fnmatch__parse(
match, &attrs->pool, context, &scan)))
match->flags |= GIT_ATTR_FNMATCH_ICASE;
scan = git__next_line(scan);
- error = git_vector_insert(&attrs->rules, match);
+
+ /*
+ * If a negative match doesn't actually do anything,
+ * throw it away. As we cannot always verify whether a
+ * rule containing wildcards negates another rule, we
+ * do not optimize away these rules, though.
+ * */
+ if (match->flags & GIT_ATTR_FNMATCH_NEGATIVE
+ && !(match->flags & GIT_ATTR_FNMATCH_HASWILD))
+ error = does_negate_rule(&valid_rule, &attrs->rules, match);
+
+ if (!error && valid_rule)
+ error = git_vector_insert(&attrs->rules, match);
}
- if (error != 0) {
- git__free(match->pattern);
+ if (error != 0 || !valid_rule) {
match->pattern = NULL;
if (error == GIT_ENOTFOUND)
int error = 0;
git_attr_file *file = NULL;
- error = git_attr_cache__get(
- &file, ignores->repo, GIT_ATTR_FILE__FROM_FILE,
- base, filename, parse_ignore_file);
+ error = git_attr_cache__get(&file, ignores->repo, NULL, GIT_ATTR_FILE__FROM_FILE,
+ base, filename, parse_ignore_file, false);
if (error < 0)
return error;
if ((error = git_attr_cache__init(repo)) < 0)
return error;
- error = git_attr_cache__get(
- out, repo, GIT_ATTR_FILE__IN_MEMORY, NULL, GIT_IGNORE_INTERNAL, NULL);
+ error = git_attr_cache__get(out, repo, NULL, GIT_ATTR_FILE__IN_MEMORY, NULL,
+ GIT_IGNORE_INTERNAL, NULL, false);
/* if internal rules list is empty, insert default rules */
if (!error && !(*out)->rules.length)
- error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES);
+ error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES, false);
return error;
}
{
int error = 0;
const char *workdir = git_repository_workdir(repo);
+ git_buf infopath = GIT_BUF_INIT;
- assert(ignores && path);
+ assert(repo && ignores && path);
memset(ignores, 0, sizeof(*ignores));
ignores->repo = repo;
/* Read the ignore_case flag */
- if ((error = git_repository__cvar(
- &ignores->ignore_case, repo, GIT_CVAR_IGNORECASE)) < 0)
+ if ((error = git_repository__configmap_lookup(
+ &ignores->ignore_case, repo, GIT_CONFIGMAP_IGNORECASE)) < 0)
goto cleanup;
if ((error = git_attr_cache__init(repo)) < 0)
goto cleanup;
/* given a unrooted path in a non-bare repo, resolve it */
- if (workdir && git_path_root(path) < 0)
- error = git_path_find_dir(&ignores->dir, path, workdir);
- else
+ if (workdir && git_path_root(path) < 0) {
+ git_buf local = GIT_BUF_INIT;
+
+ if ((error = git_path_dirname_r(&local, path)) < 0 ||
+ (error = git_path_resolve_relative(&local, 0)) < 0 ||
+ (error = git_path_to_dir(&local)) < 0 ||
+ (error = git_buf_joinpath(&ignores->dir, workdir, local.ptr)) < 0)
+ {;} /* Nothing, we just want to stop on the first error */
+ git_buf_dispose(&local);
+ } else {
error = git_buf_joinpath(&ignores->dir, path, "");
+ }
if (error < 0)
goto cleanup;
goto cleanup;
}
- /* load .git/info/exclude */
- error = push_ignore_file(
- ignores, &ignores->ign_global,
- git_repository_path(repo), GIT_IGNORE_FILE_INREPO);
- if (error < 0)
- goto cleanup;
+ /* load .git/info/exclude if possible */
+ if ((error = git_repository_item_path(&infopath, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 ||
+ (error = push_ignore_file(ignores, &ignores->ign_global, infopath.ptr, GIT_IGNORE_FILE_INREPO)) < 0) {
+ if (error != GIT_ENOTFOUND)
+ goto cleanup;
+ error = 0;
+ }
/* load core.excludesfile */
if (git_repository_attr_cache(repo)->cfg_excl_file != NULL)
git_repository_attr_cache(repo)->cfg_excl_file);
cleanup:
+ git_buf_dispose(&infopath);
if (error < 0)
git_ignore__free(ignores);
}
git_vector_free(&ignores->ign_global);
- git_buf_free(&ignores->dir);
+ git_buf_dispose(&ignores->dir);
}
static bool ignore_lookup_in_rules(
git_attr_fnmatch *match;
git_vector_rforeach(&file->rules, j, match) {
+ if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY &&
+ path->is_dir == GIT_DIR_FLAG_FALSE)
+ continue;
if (git_attr_fnmatch__match(match, path)) {
*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;
}
int git_ignore__lookup(
- int *out, git_ignores *ignores, const char *pathname)
+ int *out, git_ignores *ignores, const char *pathname, git_dir_flag dir_flag)
{
- unsigned int i;
+ size_t i;
git_attr_file *file;
git_attr_path path;
*out = GIT_IGNORE_NOTFOUND;
if (git_attr_path__init(
- &path, pathname, git_repository_workdir(ignores->repo)) < 0)
+ &path, pathname, git_repository_workdir(ignores->repo), dir_flag) < 0)
return -1;
/* first process builtins - success means path was found */
if (ignore_lookup_in_rules(out, ignores->ign_internal, &path))
goto cleanup;
- /* next process files in the path */
- git_vector_foreach(&ignores->ign_path, i, file) {
+ /* next process files in the path.
+ * this process has to process ignores in reverse order
+ * to ensure correct prioritization of rules
+ */
+ git_vector_rforeach(&ignores->ign_path, i, file) {
if (ignore_lookup_in_rules(out, file, &path))
goto cleanup;
}
if ((error = get_internal_ignores(&ign_internal, repo)) < 0)
return error;
- error = parse_ignore_file(repo, ign_internal, rules);
+ error = parse_ignore_file(repo, ign_internal, rules, false);
git_attr_file__free(ign_internal);
return error;
if (!(error = git_attr_file__clear_rules(ign_internal, true)))
error = parse_ignore_file(
- repo, ign_internal, GIT_IGNORE_DEFAULT_RULES);
+ repo, ign_internal, GIT_IGNORE_DEFAULT_RULES, false);
git_attr_file__free(ign_internal);
return error;
git_ignores ignores;
unsigned int i;
git_attr_file *file;
+ git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
- assert(ignored && pathname);
+ assert(repo && ignored && pathname);
- workdir = repo ? git_repository_workdir(repo) : NULL;
+ workdir = git_repository_workdir(repo);
memset(&path, 0, sizeof(path));
memset(&ignores, 0, sizeof(ignores));
- if ((error = git_attr_path__init(&path, pathname, workdir)) < 0 ||
+ if (!git__suffixcmp(pathname, "/"))
+ dir_flag = GIT_DIR_FLAG_TRUE;
+ else if (git_repository_is_bare(repo))
+ dir_flag = GIT_DIR_FLAG_FALSE;
+
+ if ((error = git_attr_path__init(&path, pathname, workdir, dir_flag)) < 0 ||
(error = git_ignore__for_path(repo, path.path, &ignores)) < 0)
goto cleanup;
break;
if (ignored) {
- giterr_set(GITERR_INVALID, "pathspec contains ignored file '%s'",
+ git_error_set(GIT_ERROR_INVALID, "pathspec contains ignored file '%s'",
filename);
error = GIT_EINVALIDSPEC;
break;
}
git_index_free(idx);
- git_buf_free(&path);
+ git_buf_dispose(&path);
return error;
}