]> git.proxmox.com Git - libgit2.git/blobdiff - src/ignore.c
New upstream version 1.1.0+dfsg.1
[libgit2.git] / src / ignore.c
index aedc1401e1a640ef038da37bafec550a0185519d..f5fb507e68f185e3d7a724ed804086fa1904af57 100644 (file)
@@ -1,48 +1,89 @@
+/*
+ * 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 "fnmatch.h"
+#include "wildmatch.h"
 
 #define GIT_IGNORE_INTERNAL            "[internal]exclude"
 
 #define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n"
 
 /**
- * A negative ignore pattern can match a positive one without
- * wildcards if its pattern equals the tail of the positive
- * pattern. Thus
+ * 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.
+ * 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) {
-               /*
-                * no chance of matching if rule is shorter than
-                * the negated one
-                */
-               if (rule->length < neg->length)
-                       return false;
+       if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0
+           || (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0)
+               return false;
 
-               /*
-                * shift pattern so its tail aligns with the
-                * negated pattern
-                */
-               p = rule->pattern + rule->length - neg->length;
-               if (strcmp(p, neg->pattern) == 0)
-                       return true;
+       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;
        }
 
-       return false;
+       /* 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;
 }
 
 /**
@@ -60,7 +101,7 @@ static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
  */
 static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match)
 {
-       int error = 0;
+       int error = 0, wildmatch_flags;
        size_t i;
        git_attr_fnmatch *rule;
        char *path;
@@ -68,6 +109,10 @@ static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match
 
        *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);
@@ -88,32 +133,16 @@ static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match
                                continue;
                }
 
-       /*
-        * When dealing with a directory, we add '/<star>' so
-        * p_fnmatch() honours FNM_PATHNAME. Checking for LEADINGDIR
-        * alone isn't enough as that's also set for nagations, so we
-        * need to check that NEGATIVE is off.
-        */
                git_buf_clear(&buf);
-               if (rule->containing_dir) {
+               if (rule->containing_dir)
                        git_buf_puts(&buf, rule->containing_dir);
-               }
-
-               error = git_buf_puts(&buf, rule->pattern);
+               git_buf_puts(&buf, rule->pattern);
 
-               if ((rule->flags & (GIT_ATTR_FNMATCH_LEADINGDIR | GIT_ATTR_FNMATCH_NEGATIVE)) == GIT_ATTR_FNMATCH_LEADINGDIR)
-                       error = git_buf_PUTS(&buf, "/*");
-
-               if (error < 0)
+               if (git_buf_oom(&buf))
                        goto out;
 
-               if ((error = p_fnmatch(git_buf_cstr(&buf), path, FNM_PATHNAME)) < 0) {
-                       giterr_set(GITERR_INVALID, "error matching pattern");
-                       goto out;
-               }
-
                /* if we found a match, we want to keep this rule */
-               if (error != FNM_NOMATCH) {
+               if ((wildmatch(git_buf_cstr(&buf), path, wildmatch_flags)) == WM_MATCH) {
                        *out = 1;
                        error = 0;
                        goto out;
@@ -124,20 +153,22 @@ static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match
 
 out:
        git__free(path);
-       git_buf_free(&buf);
+       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 &&
@@ -146,7 +177,7 @@ static int parse_ignore_file(
                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;
        }
 
@@ -158,7 +189,8 @@ static int parse_ignore_file(
                        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)))
@@ -170,8 +202,14 @@ static int parse_ignore_file(
 
                        scan = git__next_line(scan);
 
-                       /* if a negative match doesn't actually do anything, throw it away */
-                       if (match->flags & GIT_ATTR_FNMATCH_NEGATIVE)
+                       /*
+                        * 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)
@@ -203,9 +241,8 @@ static int push_ignore_file(
        int error = 0;
        git_attr_file *file = NULL;
 
-       error = git_attr_cache__get(
-               &file, ignores->repo, NULL, 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;
 
@@ -231,12 +268,12 @@ static int get_internal_ignores(git_attr_file **out, git_repository *repo)
        if ((error = git_attr_cache__init(repo)) < 0)
                return error;
 
-       error = git_attr_cache__get(
-               out, repo, NULL, 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;
 }
@@ -248,25 +285,34 @@ int git_ignore__for_path(
 {
        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;
 
@@ -285,12 +331,13 @@ int git_ignore__for_path(
                        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)
@@ -299,6 +346,7 @@ int git_ignore__for_path(
                        git_repository_attr_cache(repo)->cfg_excl_file);
 
 cleanup:
+       git_buf_dispose(&infopath);
        if (error < 0)
                git_ignore__free(ignores);
 
@@ -370,7 +418,7 @@ void git_ignore__free(git_ignores *ignores)
        }
        git_vector_free(&ignores->ign_global);
 
-       git_buf_free(&ignores->dir);
+       git_buf_dispose(&ignores->dir);
 }
 
 static bool ignore_lookup_in_rules(
@@ -380,6 +428,9 @@ 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;
@@ -393,7 +444,7 @@ static bool ignore_lookup_in_rules(
 int git_ignore__lookup(
        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;
 
@@ -407,8 +458,11 @@ int git_ignore__lookup(
        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;
        }
@@ -432,7 +486,7 @@ int git_ignore_add_rule(git_repository *repo, const char *rules)
        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;
@@ -448,7 +502,7 @@ int git_ignore_clear_internal_rules(git_repository *repo)
 
        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;
@@ -465,15 +519,21 @@ int git_ignore_path_is_ignored(
        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, GIT_DIR_FLAG_UNKNOWN)) < 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;
 
@@ -560,7 +620,7 @@ int git_ignore__check_pathspec_for_exact_ignores(
                        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;
@@ -568,7 +628,7 @@ int git_ignore__check_pathspec_for_exact_ignores(
        }
 
        git_index_free(idx);
-       git_buf_free(&path);
+       git_buf_dispose(&path);
 
        return error;
 }