diff --git a/include/git2/index.h b/include/git2/index.h index 51694aded..1fb77efa3 100644 --- a/include/git2/index.h +++ b/include/git2/index.h @@ -138,6 +138,14 @@ typedef enum { GIT_INDEX_ADD_CHECK_PATHSPEC = (1u << 2), } git_index_add_option_t; +/** + * Match any index stage. + * + * Some index APIs take a stage to match; pass this value to match + * any entry matching the path regardless of stage. + */ +#define GIT_INDEX_STAGE_ANY -1 + /** @name Index File Functions * * These functions work on the index file itself. diff --git a/include/git2/pathspec.h b/include/git2/pathspec.h new file mode 100644 index 000000000..8122d9927 --- /dev/null +++ b/include/git2/pathspec.h @@ -0,0 +1,202 @@ +/* + * 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. + */ +#ifndef INCLUDE_git_pathspec_h__ +#define INCLUDE_git_pathspec_h__ + +#include "common.h" +#include "types.h" +#include "strarray.h" + +/** + * Compiled pathspec + */ +typedef struct git_pathspec git_pathspec; + +/** + * List of filenames matching a pathspec + */ +typedef struct git_pathspec_match_list git_pathspec_match_list; + +/** + * Options controlling how pathspec match should be executed + * + * - GIT_PATHSPEC_IGNORE_CASE forces match to ignore case; otherwise + * match will use native case sensitivity of platform + * - GIT_PATHSPEC_USE_CASE forces case sensitive match; otherwise + * match will use native case sensitivity of platform + * - GIT_PATHSPEC_NO_GLOB disables glob patterns and just uses simple + * string comparison for matching + * - GIT_PATHSPEC_NO_MATCH_ERROR means the match function will return + * GIT_ENOTFOUND if no matches are found; otherwise it will return 0 + * for success and `git_pathspec_match_list_entrycount` will be 0. + * - GIT_PATHSPEC_FIND_FAILURES only applies to a git_pathspec_match_list; + * it means to check file names against all unmatched patterns so that + * at the end of a match we can identify patterns that did not match any + * files. + * - GIT_PATHSPEC_FAILURES_ONLY only applies to a git_pathspec_match_list; + * it means to only check for mismatches and not record matched paths. + */ +typedef enum { + GIT_PATHSPEC_DEFAULT = 0, + GIT_PATHSPEC_IGNORE_CASE = (1u << 0), + GIT_PATHSPEC_USE_CASE = (1u << 1), + GIT_PATHSPEC_NO_GLOB = (1u << 2), + GIT_PATHSPEC_NO_MATCH_ERROR = (1u << 3), + GIT_PATHSPEC_FIND_FAILURES = (1u << 4), + GIT_PATHSPEC_FAILURES_ONLY = (1u << 5), +} git_pathspec_flag_t; + +/** + * Compile a pathspec + * + * @param out Output of the compiled pathspec + * @param flags Combination of git_pathspec_flag_t values + * @param pathspec A git_strarray of the paths to match + * @return 0 on success, <0 on failure + */ +GIT_EXTERN(int) git_pathspec_new( + git_pathspec **out, const git_strarray *pathspec); + +/** + * Free a pathspec + * + * @param ps The compiled pathspec + */ +GIT_EXTERN(void) git_pathspec_free(git_pathspec *ps); + +/** + * Try to match a path against a pathspec + * + * Unlike most of the other pathspec matching functions, this will not + * fall back on the native case-sensitivity for your platform. You must + * explicitly pass flags to control case sensitivity or else this will + * fall back on being case sensitive. + * + * @param ps The compiled pathspec + * @param flags Match flags to influence matching behavior + * @param path The pathname to attempt to match + * @return 1 is path matches spec, 0 if it does not + */ +GIT_EXTERN(int) git_pathspec_matches_path( + const git_pathspec *ps, uint32_t flags, const char *path); + +/** + * Match a pathspec against the working directory of a repository. + * + * This returns a `git_patchspec_match` object that contains the list of + * all files matching the given pathspec in the working directory of the + * repository. This handles git ignores (i.e. ignored files will not be + * considered to match the `pathspec` unless the file is tracked in the + * index). + * + * @param out Object with list of matching items + * @param repo The repository in which to match; bare repo is an error + * @param flags Options to control matching behavior + * @param ps Pathspec to be matched + * @return 0 on success, -1 on error, GIT_ENOTFOUND if no matches and + * the GIT_PATHSPEC_NO_MATCH_ERROR flag is used + */ +GIT_EXTERN(int) git_pathspec_match_workdir( + git_pathspec_match_list **out, + git_repository *repo, + uint32_t flags, + git_pathspec *ps); + +/** + * Match a pathspec against entries in an index. + * + * This returns a `git_patchspec_match` object that contains the list of + * all files matching the given pathspec in the index. + * + * NOTE: At the moment, the case sensitivity of this match is controlled + * by the current case-sensitivity of the index object itself and the + * USE_CASE and IGNORE_CASE flags will have no effect. This behavior will + * be corrected in a future release. + * + * @param out Object with list of matching items + * @param inex The index in which to match + * @param flags Options to control matching behavior + * @param ps Pathspec to be matched + * @return 0 on success, -1 on error, GIT_ENOTFOUND if no matches and + * the GIT_PATHSPEC_NO_MATCH_ERROR flag is used + */ +GIT_EXTERN(int) git_pathspec_match_index( + git_pathspec_match_list **out, + git_index *index, + uint32_t flags, + git_pathspec *ps); + +/** + * Match a pathspec against files in a tree. + * + * This returns a `git_patchspec_match` object that contains the list of + * all files matching the given pathspec in the given tree. + * + * @param out Object with list of matching items + * @param inex The index in which to match + * @param flags Options to control matching behavior + * @param ps Pathspec to be matched + * @return 0 on success, -1 on error, GIT_ENOTFOUND if no matches and + * the GIT_PATHSPEC_NO_MATCH_ERROR flag is used + */ +GIT_EXTERN(int) git_pathspec_match_tree( + git_pathspec_match_list **out, + git_tree *tree, + uint32_t flags, + git_pathspec *ps); + +/** + * Free memory associates with a git_pathspec_match_list + * + * @param m The git_pathspec_match_list to be freed + */ +GIT_EXTERN(void) git_pathspec_match_list_free(git_pathspec_match_list *m); + +/** + * Get the number of items in a match list. + * + * @param m The git_pathspec_match_list object + * @return Number of items in match list + */ +GIT_EXTERN(size_t) git_pathspec_match_list_entrycount( + const git_pathspec_match_list *m); + +/** + * Get a matching filename by position. + * + * @param m The git_pathspec_match_list object + * @param pos The index into the list + * @return The filename of the match + */ +GIT_EXTERN(const char *) git_pathspec_match_list_entry( + const git_pathspec_match_list *m, size_t pos); + +/** + * Get the number of pathspec items that did not match. + * + * This will be zero unless you passed GIT_PATHSPEC_FIND_FAILURES when + * generating the git_pathspec_match_list. + * + * @param m The git_pathspec_match_list object + * @return Number of items in original pathspec that had no matches + */ +GIT_EXTERN(size_t) git_pathspec_match_list_failed_entrycount( + const git_pathspec_match_list *m); + +/** + * Get an original pathspec string that had no matches. + * + * This will be return NULL for positions out of range. + * + * @param m The git_pathspec_match_list object + * @param pos The index into the failed items + * @return The pathspec pattern that didn't match anything + */ +GIT_EXTERN(const char *) git_pathspec_match_list_failed_entry( + const git_pathspec_match_list *m, size_t pos); + +#endif diff --git a/src/checkout.c b/src/checkout.c index 8f9ec64e4..ec9da7e2e 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -246,10 +246,10 @@ static int checkout_action_wd_only( bool remove = false; git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE; - if (!git_pathspec_match_path( + if (!git_pathspec__match( pathspec, wd->path, (data->strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) != 0, - git_iterator_ignore_case(workdir), NULL)) + git_iterator_ignore_case(workdir), NULL, NULL)) return 0; /* check if item is tracked in the index but not in the checkout diff */ @@ -607,7 +607,7 @@ static int checkout_get_actions( uint32_t *actions = NULL; if (data->opts.paths.count > 0 && - git_pathspec_init(&pathspec, &data->opts.paths, &pathpool) < 0) + git_pathspec__vinit(&pathspec, &data->opts.paths, &pathpool) < 0) return -1; if ((error = git_iterator_current(&wditem, workdir)) < 0 && @@ -659,7 +659,7 @@ static int checkout_get_actions( goto fail; } - git_pathspec_free(&pathspec); + git_pathspec__vfree(&pathspec); git_pool_clear(&pathpool); return 0; @@ -670,7 +670,7 @@ fail: *actions_ptr = NULL; git__free(actions); - git_pathspec_free(&pathspec); + git_pathspec__vfree(&pathspec); git_pool_clear(&pathpool); return error; diff --git a/src/diff.c b/src/diff.c index 0980f412a..56232ebf4 100644 --- a/src/diff.c +++ b/src/diff.c @@ -81,11 +81,11 @@ static int diff_delta__from_one( DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) return 0; - if (!git_pathspec_match_path( + if (!git_pathspec__match( &diff->pathspec, entry->path, DIFF_FLAG_IS_SET(diff, GIT_DIFF_DISABLE_PATHSPEC_MATCH), DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE), - &matched_pathspec)) + &matched_pathspec, NULL)) return 0; delta = diff_delta__alloc(diff, status, entry->path); @@ -387,7 +387,7 @@ static int diff_list_apply_options( DIFF_FLAG_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE, icase); /* initialize pathspec from options */ - if (git_pathspec_init(&diff->pathspec, &opts->pathspec, pool) < 0) + if (git_pathspec__vinit(&diff->pathspec, &opts->pathspec, pool) < 0) return -1; } @@ -473,7 +473,7 @@ static void diff_list_free(git_diff_list *diff) } git_vector_free(&diff->deltas); - git_pathspec_free(&diff->pathspec); + git_pathspec__vfree(&diff->pathspec); git_pool_clear(&diff->pool); git__memzero(diff, sizeof(*diff)); @@ -634,11 +634,11 @@ static int maybe_modified( bool new_is_workdir = (info->new_iter->type == GIT_ITERATOR_TYPE_WORKDIR); const char *matched_pathspec; - if (!git_pathspec_match_path( + if (!git_pathspec__match( &diff->pathspec, oitem->path, DIFF_FLAG_IS_SET(diff, GIT_DIFF_DISABLE_PATHSPEC_MATCH), DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE), - &matched_pathspec)) + &matched_pathspec, NULL)) return 0; memset(&noid, 0, sizeof(noid)); diff --git a/src/index.c b/src/index.c index ffa84e53f..21efd2c63 100644 --- a/src/index.c +++ b/src/index.c @@ -101,8 +101,6 @@ static int parse_index(git_index *index, const char *buffer, size_t buffer_size) static bool is_index_extended(git_index *index); static int write_index(git_index *index, git_filebuf *file); -static int index_find(size_t *at_pos, git_index *index, const char *path, int stage); - static void index_entry_free(git_index_entry *entry); static void index_entry_reuc_free(git_index_reuc_entry *reuc); @@ -114,7 +112,7 @@ static int index_srch(const void *key, const void *array_member) ret = strcmp(srch_key->path, entry->path); - if (ret == 0) + if (ret == 0 && srch_key->stage != GIT_INDEX_STAGE_ANY) ret = srch_key->stage - GIT_IDXENTRY_STAGE(entry); return ret; @@ -128,7 +126,7 @@ static int index_isrch(const void *key, const void *array_member) ret = strcasecmp(srch_key->path, entry->path); - if (ret == 0) + if (ret == 0 && srch_key->stage != GIT_INDEX_STAGE_ANY) ret = srch_key->stage - GIT_IDXENTRY_STAGE(entry); return ret; @@ -562,7 +560,7 @@ const git_index_entry *git_index_get_bypath( git_vector_sort(&index->entries); - if (index_find(&pos, index, path, stage) < 0) { + if (git_index__find(&pos, index, path, stage) < 0) { giterr_set(GITERR_INDEX, "Index does not contain %s", path); return NULL; } @@ -719,7 +717,8 @@ static int index_insert(git_index *index, git_index_entry *entry, int replace) entry->flags |= GIT_IDXENTRY_NAMEMASK; /* look if an entry with this path already exists */ - if (!index_find(&position, index, entry->path, GIT_IDXENTRY_STAGE(entry))) { + if (!git_index__find( + &position, index, entry->path, GIT_IDXENTRY_STAGE(entry))) { existing = (git_index_entry **)&index->entries.contents[position]; /* update filemode to existing values if stat is not trusted */ @@ -831,7 +830,7 @@ int git_index_remove(git_index *index, const char *path, int stage) git_vector_sort(&index->entries); - if (index_find(&position, index, path, stage) < 0) { + if (git_index__find(&position, index, path, stage) < 0) { giterr_set(GITERR_INDEX, "Index does not contain %s at stage %d", path, stage); return GIT_ENOTFOUND; @@ -887,7 +886,8 @@ int git_index_remove_directory(git_index *index, const char *dir, int stage) return error; } -static int index_find(size_t *at_pos, git_index *index, const char *path, int stage) +int git_index__find( + size_t *at_pos, git_index *index, const char *path, int stage) { struct entry_srch_key srch_key; @@ -896,7 +896,8 @@ static int index_find(size_t *at_pos, git_index *index, const char *path, int st srch_key.path = path; srch_key.stage = stage; - return git_vector_bsearch2(at_pos, &index->entries, index->entries_search, &srch_key); + return git_vector_bsearch2( + at_pos, &index->entries, index->entries_search, &srch_key); } int git_index_find(size_t *at_pos, git_index *index, const char *path) @@ -2053,7 +2054,7 @@ int git_index_add_all( git_iterator *wditer = NULL; const git_index_entry *wd = NULL; git_index_entry *entry; - git_pathspec_context ps; + git_pathspec ps; const char *match; size_t existing; bool no_fnmatch = (flags & GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH) != 0; @@ -2074,7 +2075,7 @@ int git_index_add_all( if (git_repository__cvar(&ignorecase, repo, GIT_CVAR_IGNORECASE) < 0) return -1; - if ((error = git_pathspec_context_init(&ps, paths)) < 0) + if ((error = git_pathspec__init(&ps, paths)) < 0) return error; /* optionally check that pathspec doesn't mention any ignored files */ @@ -2091,14 +2092,14 @@ int git_index_add_all( while (!(error = git_iterator_advance(&wd, wditer))) { /* check if path actually matches */ - if (!git_pathspec_match_path( - &ps.pathspec, wd->path, no_fnmatch, ignorecase, &match)) + if (!git_pathspec__match( + &ps.pathspec, wd->path, no_fnmatch, ignorecase, &match, NULL)) continue; /* skip ignored items that are not already in the index */ if ((flags & GIT_INDEX_ADD_FORCE) == 0 && git_iterator_current_is_ignored(wditer) && - index_find(&existing, index, wd->path, 0) < 0) + git_index__find(&existing, index, wd->path, 0) < 0) continue; /* issue notification callback if requested */ @@ -2148,7 +2149,7 @@ int git_index_add_all( cleanup: git_iterator_free(wditer); - git_pathspec_context_free(&ps); + git_pathspec__clear(&ps); return error; } @@ -2168,13 +2169,13 @@ static int index_apply_to_all( { int error = 0; size_t i; - git_pathspec_context ps; + git_pathspec ps; const char *match; git_buf path = GIT_BUF_INIT; assert(index); - if ((error = git_pathspec_context_init(&ps, paths)) < 0) + if ((error = git_pathspec__init(&ps, paths)) < 0) return error; git_vector_sort(&index->entries); @@ -2183,8 +2184,9 @@ static int index_apply_to_all( git_index_entry *entry = git_vector_get(&index->entries, i); /* check if path actually matches */ - if (!git_pathspec_match_path( - &ps.pathspec, entry->path, false, index->ignore_case, &match)) + if (!git_pathspec__match( + &ps.pathspec, entry->path, false, index->ignore_case, + &match, NULL)) continue; /* issue notification callback if requested */ @@ -2231,7 +2233,7 @@ static int index_apply_to_all( } git_buf_free(&path); - git_pathspec_context_free(&ps); + git_pathspec__clear(&ps); return error; } diff --git a/src/index.h b/src/index.h index a59107a7b..40577e105 100644 --- a/src/index.h +++ b/src/index.h @@ -47,13 +47,17 @@ struct git_index_conflict_iterator { size_t cur; }; -extern void git_index_entry__init_from_stat(git_index_entry *entry, struct stat *st); +extern void git_index_entry__init_from_stat( + git_index_entry *entry, struct stat *st); extern size_t git_index__prefix_position(git_index *index, const char *path); extern int git_index_entry__cmp(const void *a, const void *b); extern int git_index_entry__cmp_icase(const void *a, const void *b); +extern int git_index__find( + size_t *at_pos, git_index *index, const char *path, int stage); + extern void git_index__set_ignore_case(git_index *index, bool ignore_case); #endif diff --git a/src/pathspec.c b/src/pathspec.c index f029836d0..35421dbef 100644 --- a/src/pathspec.c +++ b/src/pathspec.c @@ -5,9 +5,13 @@ * a Linking Exception. For full terms see the included COPYING file. */ +#include "git2/pathspec.h" #include "pathspec.h" #include "buf_text.h" #include "attr_file.h" +#include "iterator.h" +#include "repository.h" +#include "index.h" /* what is the common non-wildcard prefix for all items in the pathspec */ char *git_pathspec_prefix(const git_strarray *pathspec) @@ -56,7 +60,7 @@ bool git_pathspec_is_empty(const git_strarray *pathspec) } /* build a vector of fnmatch patterns to evaluate efficiently */ -int git_pathspec_init( +int git_pathspec__vinit( git_vector *vspec, const git_strarray *strspec, git_pool *strpool) { size_t i; @@ -93,7 +97,7 @@ int git_pathspec_init( } /* free data from the pathspec vector */ -void git_pathspec_free(git_vector *vspec) +void git_pathspec__vfree(git_vector *vspec) { git_attr_fnmatch *match; unsigned int i; @@ -106,60 +110,91 @@ void git_pathspec_free(git_vector *vspec) git_vector_free(vspec); } +struct pathspec_match_context { + int fnmatch_flags; + int (*strcomp)(const char *, const char *); + int (*strncomp)(const char *, const char *, size_t); +}; + +static void pathspec_match_context_init( + struct pathspec_match_context *ctxt, + bool disable_fnmatch, + bool casefold) +{ + if (disable_fnmatch) + ctxt->fnmatch_flags = -1; + else if (casefold) + ctxt->fnmatch_flags = FNM_CASEFOLD; + else + ctxt->fnmatch_flags = 0; + + if (casefold) { + ctxt->strcomp = git__strcasecmp; + ctxt->strncomp = git__strncasecmp; + } else { + ctxt->strcomp = git__strcmp; + ctxt->strncomp = git__strncmp; + } +} + +static int pathspec_match_one( + const git_attr_fnmatch *match, + struct pathspec_match_context *ctxt, + const char *path) +{ + int result = (match->flags & GIT_ATTR_FNMATCH_MATCH_ALL) ? 0 : FNM_NOMATCH; + + if (result == FNM_NOMATCH) + result = ctxt->strcomp(match->pattern, path) ? FNM_NOMATCH : 0; + + if (ctxt->fnmatch_flags >= 0 && result == FNM_NOMATCH) + result = p_fnmatch(match->pattern, path, ctxt->fnmatch_flags); + + /* if we didn't match, look for exact dirname prefix match */ + if (result == FNM_NOMATCH && + (match->flags & GIT_ATTR_FNMATCH_HASWILD) == 0 && + ctxt->strncomp(path, match->pattern, match->length) == 0 && + path[match->length] == '/') + result = 0; + + if (result == 0) + return (match->flags & GIT_ATTR_FNMATCH_NEGATIVE) ? 0 : 1; + return -1; +} + /* match a path against the vectorized pathspec */ -bool git_pathspec_match_path( - git_vector *vspec, +bool git_pathspec__match( + const git_vector *vspec, const char *path, bool disable_fnmatch, bool casefold, - const char **matched_pathspec) + const char **matched_pathspec, + size_t *matched_at) { size_t i; - git_attr_fnmatch *match; - int fnmatch_flags = 0; - int (*use_strcmp)(const char *, const char *); - int (*use_strncmp)(const char *, const char *, size_t); + const git_attr_fnmatch *match; + struct pathspec_match_context ctxt; if (matched_pathspec) *matched_pathspec = NULL; + if (matched_at) + *matched_at = GIT_PATHSPEC_NOMATCH; if (!vspec || !vspec->length) return true; - if (disable_fnmatch) - fnmatch_flags = -1; - else if (casefold) - fnmatch_flags = FNM_CASEFOLD; - - if (casefold) { - use_strcmp = git__strcasecmp; - use_strncmp = git__strncasecmp; - } else { - use_strcmp = git__strcmp; - use_strncmp = git__strncmp; - } + pathspec_match_context_init(&ctxt, disable_fnmatch, casefold); git_vector_foreach(vspec, i, match) { - int result = (match->flags & GIT_ATTR_FNMATCH_MATCH_ALL) ? 0 : FNM_NOMATCH; + int result = pathspec_match_one(match, &ctxt, path); - if (result == FNM_NOMATCH) - result = use_strcmp(match->pattern, path) ? FNM_NOMATCH : 0; - - if (fnmatch_flags >= 0 && result == FNM_NOMATCH) - result = p_fnmatch(match->pattern, path, fnmatch_flags); - - /* if we didn't match, look for exact dirname prefix match */ - if (result == FNM_NOMATCH && - (match->flags & GIT_ATTR_FNMATCH_HASWILD) == 0 && - use_strncmp(path, match->pattern, match->length) == 0 && - path[match->length] == '/') - result = 0; - - if (result == 0) { + if (result >= 0) { if (matched_pathspec) *matched_pathspec = match->pattern; + if (matched_at) + *matched_at = i; - return (match->flags & GIT_ATTR_FNMATCH_NEGATIVE) ? false : true; + return (result != 0); } } @@ -167,27 +202,344 @@ bool git_pathspec_match_path( } -int git_pathspec_context_init( - git_pathspec_context *ctxt, const git_strarray *paths) +int git_pathspec__init(git_pathspec *ps, const git_strarray *paths) { int error = 0; - memset(ctxt, 0, sizeof(*ctxt)); + memset(ps, 0, sizeof(*ps)); - ctxt->prefix = git_pathspec_prefix(paths); + ps->prefix = git_pathspec_prefix(paths); - if ((error = git_pool_init(&ctxt->pool, 1, 0)) < 0 || - (error = git_pathspec_init(&ctxt->pathspec, paths, &ctxt->pool)) < 0) - git_pathspec_context_free(ctxt); + if ((error = git_pool_init(&ps->pool, 1, 0)) < 0 || + (error = git_pathspec__vinit(&ps->pathspec, paths, &ps->pool)) < 0) + git_pathspec__clear(ps); return error; } -void git_pathspec_context_free( - git_pathspec_context *ctxt) +void git_pathspec__clear(git_pathspec *ps) { - git__free(ctxt->prefix); - git_pathspec_free(&ctxt->pathspec); - git_pool_clear(&ctxt->pool); - memset(ctxt, 0, sizeof(*ctxt)); + git__free(ps->prefix); + git_pathspec__vfree(&ps->pathspec); + git_pool_clear(&ps->pool); + memset(ps, 0, sizeof(*ps)); +} + +int git_pathspec_new(git_pathspec **out, const git_strarray *pathspec) +{ + int error = 0; + git_pathspec *ps = git__malloc(sizeof(git_pathspec)); + GITERR_CHECK_ALLOC(ps); + + if ((error = git_pathspec__init(ps, pathspec)) < 0) { + git__free(ps); + return error; + } + + GIT_REFCOUNT_INC(ps); + *out = ps; + return 0; +} + +static void pathspec_free(git_pathspec *ps) +{ + git_pathspec__clear(ps); + git__free(ps); +} + +void git_pathspec_free(git_pathspec *ps) +{ + if (!ps) + return; + GIT_REFCOUNT_DEC(ps, pathspec_free); +} + +int git_pathspec_matches_path( + const git_pathspec *ps, uint32_t flags, const char *path) +{ + bool no_fnmatch = (flags & GIT_PATHSPEC_NO_GLOB) != 0; + bool casefold = (flags & GIT_PATHSPEC_IGNORE_CASE) != 0; + + assert(ps && path); + + return (0 != git_pathspec__match( + &ps->pathspec, path, no_fnmatch, casefold, NULL, NULL)); +} + +static void pathspec_match_free(git_pathspec_match_list *m) +{ + git_pathspec_free(m->pathspec); + m->pathspec = NULL; + + git_array_clear(m->matches); + git_array_clear(m->failures); + git_pool_clear(&m->pool); + git__free(m); +} + +static git_pathspec_match_list *pathspec_match_alloc(git_pathspec *ps) +{ + git_pathspec_match_list *m = git__calloc(1, sizeof(git_pathspec_match_list)); + + if (m != NULL && git_pool_init(&m->pool, 1, 0) < 0) { + pathspec_match_free(m); + m = NULL; + } + + /* need to keep reference to pathspec and increment refcount because + * failures array stores pointers to the pattern strings of the + * pathspec that had no matches + */ + GIT_REFCOUNT_INC(ps); + m->pathspec = ps; + + return m; +} + +GIT_INLINE(void) pathspec_mark_pattern(uint8_t *used, size_t pos, size_t *ct) +{ + if (!used[pos]) { + used[pos] = 1; + (*ct)++; + } +} + +static int pathspec_match_from_iterator( + git_pathspec_match_list **out, + git_iterator *iter, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_pathspec_match_list *m; + const git_index_entry *entry = NULL; + struct pathspec_match_context ctxt; + git_vector *patterns = &ps->pathspec; + bool find_failures = (flags & GIT_PATHSPEC_FIND_FAILURES) != 0; + bool failures_only = (flags & GIT_PATHSPEC_FAILURES_ONLY) != 0; + size_t pos, used_ct = 0, found_files = 0; + git_index *index = NULL; + uint8_t *used_patterns = NULL; + char **file; + + *out = m = pathspec_match_alloc(ps); + GITERR_CHECK_ALLOC(m); + + if ((error = git_iterator_reset(iter, ps->prefix, ps->prefix)) < 0) + goto done; + + if (patterns->length > 0) { + used_patterns = git__calloc(patterns->length, sizeof(uint8_t)); + GITERR_CHECK_ALLOC(used_patterns); + } + + if (git_iterator_type(iter) == GIT_ITERATOR_TYPE_WORKDIR && + (error = git_repository_index__weakptr( + &index, git_iterator_owner(iter))) < 0) + goto done; + + pathspec_match_context_init(&ctxt, + (flags & GIT_PATHSPEC_NO_GLOB) != 0, git_iterator_ignore_case(iter)); + + while (!(error = git_iterator_advance(&entry, iter))) { + int result = -1; + + for (pos = 0; pos < patterns->length; ++pos) { + const git_attr_fnmatch *pat = git_vector_get(patterns, pos); + + result = pathspec_match_one(pat, &ctxt, entry->path); + if (result >= 0) + break; + } + + /* no matches for this path */ + if (result < 0) + continue; + + /* if result was a negative pattern match, then don't list file */ + if (!result) { + pathspec_mark_pattern(used_patterns, pos, &used_ct); + continue; + } + + /* check if path is untracked and ignored */ + if (index != NULL && + git_iterator_current_is_ignored(iter) && + git_index__find(NULL, index, entry->path, GIT_INDEX_STAGE_ANY) < 0) + continue; + + /* mark the matched pattern as used */ + pathspec_mark_pattern(used_patterns, pos, &used_ct); + ++found_files; + + /* if find_failures is on, check if any later patterns also match */ + if (find_failures && used_ct < patterns->length) { + for (++pos; pos < patterns->length; ++pos) { + const git_attr_fnmatch *pat = git_vector_get(patterns, pos); + if (used_patterns[pos]) + continue; + + if (pathspec_match_one(pat, &ctxt, entry->path) > 0) + pathspec_mark_pattern(used_patterns, pos, &used_ct); + } + } + + /* if only looking at failures, exit early or just continue */ + if (failures_only) { + if (used_ct == patterns->length) + break; + continue; + } + + /* insert matched path into matches array */ + if ((file = git_array_alloc(m->matches)) == NULL || + (*file = git_pool_strdup(&m->pool, entry->path)) == NULL) { + error = -1; + goto done; + } + } + + if (error < 0 && error != GIT_ITEROVER) + goto done; + error = 0; + + /* insert patterns that had no matches into failures array */ + if (find_failures && used_ct < patterns->length) { + for (pos = 0; pos < patterns->length; ++pos) { + const git_attr_fnmatch *pat = git_vector_get(patterns, pos); + if (used_patterns[pos]) + continue; + + if ((file = git_array_alloc(m->failures)) == NULL || + (*file = git_pool_strdup(&m->pool, pat->pattern)) == NULL) { + error = -1; + goto done; + } + } + } + + /* if every pattern failed to match, then we have failed */ + if ((flags & GIT_PATHSPEC_NO_MATCH_ERROR) != 0 && !found_files) { + giterr_set(GITERR_INVALID, "No matching files were found"); + error = GIT_ENOTFOUND; + } + +done: + git__free(used_patterns); + + if (error < 0) { + pathspec_match_free(m); + *out = NULL; + } + + return error; +} + +static git_iterator_flag_t pathspec_match_iter_flags(uint32_t flags) +{ + git_iterator_flag_t f = 0; + + if ((flags & GIT_PATHSPEC_IGNORE_CASE) != 0) + f |= GIT_ITERATOR_IGNORE_CASE; + else if ((flags & GIT_PATHSPEC_USE_CASE) != 0) + f |= GIT_ITERATOR_DONT_IGNORE_CASE; + + return f; +} + +int git_pathspec_match_workdir( + git_pathspec_match_list **out, + git_repository *repo, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_iterator *iter; + + assert(out && repo); + + if (!(error = git_iterator_for_workdir( + &iter, repo, pathspec_match_iter_flags(flags), NULL, NULL))) { + + error = pathspec_match_from_iterator(out, iter, flags, ps); + + git_iterator_free(iter); + } + + return error; +} + +int git_pathspec_match_index( + git_pathspec_match_list **out, + git_index *index, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_iterator *iter; + + assert(out && index); + + if (!(error = git_iterator_for_index( + &iter, index, pathspec_match_iter_flags(flags), NULL, NULL))) { + + error = pathspec_match_from_iterator(out, iter, flags, ps); + + git_iterator_free(iter); + } + + return error; +} + +int git_pathspec_match_tree( + git_pathspec_match_list **out, + git_tree *tree, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_iterator *iter; + + assert(out && tree); + + if (!(error = git_iterator_for_tree( + &iter, tree, pathspec_match_iter_flags(flags), NULL, NULL))) { + + error = pathspec_match_from_iterator(out, iter, flags, ps); + + git_iterator_free(iter); + } + + return error; +} + +void git_pathspec_match_list_free(git_pathspec_match_list *m) +{ + pathspec_match_free(m); +} + +size_t git_pathspec_match_list_entrycount( + const git_pathspec_match_list *m) +{ + return git_array_size(m->matches); +} + +const char *git_pathspec_match_list_entry( + const git_pathspec_match_list *m, size_t pos) +{ + char **entry = git_array_get(m->matches, pos); + return entry ? *entry : NULL; +} + +size_t git_pathspec_match_list_failed_entrycount( + const git_pathspec_match_list *m) +{ + return git_array_size(m->failures); +} + +const char * git_pathspec_match_list_failed_entry( + const git_pathspec_match_list *m, size_t pos) +{ + char **entry = git_array_get(m->failures, pos); + return entry ? *entry : NULL; } diff --git a/src/pathspec.h b/src/pathspec.h index f6509df4c..e7edfea38 100644 --- a/src/pathspec.h +++ b/src/pathspec.h @@ -8,9 +8,27 @@ #define INCLUDE_pathspec_h__ #include "common.h" +#include #include "buffer.h" #include "vector.h" #include "pool.h" +#include "array.h" + +/* public compiled pathspec */ +struct git_pathspec { + git_refcount rc; + char *prefix; + git_vector pathspec; + git_pool pool; +}; + +/* public interface to pathspec matching */ +struct git_pathspec_match_list { + git_pathspec *pathspec; + git_array_t(char *) matches; + git_array_t(char *) failures; + git_pool pool; +}; /* what is the common non-wildcard prefix for all items in the pathspec */ extern char *git_pathspec_prefix(const git_strarray *pathspec); @@ -19,36 +37,31 @@ extern char *git_pathspec_prefix(const git_strarray *pathspec); extern bool git_pathspec_is_empty(const git_strarray *pathspec); /* build a vector of fnmatch patterns to evaluate efficiently */ -extern int git_pathspec_init( +extern int git_pathspec__vinit( git_vector *vspec, const git_strarray *strspec, git_pool *strpool); /* free data from the pathspec vector */ -extern void git_pathspec_free(git_vector *vspec); +extern void git_pathspec__vfree(git_vector *vspec); + +#define GIT_PATHSPEC_NOMATCH ((size_t)-1) /* * Match a path against the vectorized pathspec. * The matched pathspec is passed back into the `matched_pathspec` parameter, * unless it is passed as NULL by the caller. */ -extern bool git_pathspec_match_path( - git_vector *vspec, +extern bool git_pathspec__match( + const git_vector *vspec, const char *path, bool disable_fnmatch, bool casefold, - const char **matched_pathspec); + const char **matched_pathspec, + size_t *matched_at); /* easy pathspec setup */ -typedef struct { - char *prefix; - git_vector pathspec; - git_pool pool; -} git_pathspec_context; +extern int git_pathspec__init(git_pathspec *ps, const git_strarray *paths); -extern int git_pathspec_context_init( - git_pathspec_context *ctxt, const git_strarray *paths); - -extern void git_pathspec_context_free( - git_pathspec_context *ctxt); +extern void git_pathspec__clear(git_pathspec *ps); #endif diff --git a/tests-clar/repo/pathspec.c b/tests-clar/repo/pathspec.c new file mode 100644 index 000000000..334066b67 --- /dev/null +++ b/tests-clar/repo/pathspec.c @@ -0,0 +1,385 @@ +#include "clar_libgit2.h" +#include "git2/pathspec.h" + +static git_repository *g_repo; + +void test_repo_pathspec__initialize(void) +{ + g_repo = cl_git_sandbox_init("status"); +} + +void test_repo_pathspec__cleanup(void) +{ + cl_git_sandbox_cleanup(); + g_repo = NULL; +} + +static char *str0[] = { "*_file", "new_file", "garbage" }; +static char *str1[] = { "*_FILE", "NEW_FILE", "GARBAGE" }; +static char *str2[] = { "staged_*" }; +static char *str3[] = { "!subdir", "*_file", "new_file" }; +static char *str4[] = { "*" }; +static char *str5[] = { "S*" }; + +void test_repo_pathspec__workdir0(void) +{ + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "*_file", "new_file", "garbage" } */ + s.strings = str0; s.count = ARRAY_SIZE(str0); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, 0, ps)); + cl_assert_equal_sz(10, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(10, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(1, git_pathspec_match_list_failed_entrycount(m)); + cl_assert_equal_s("garbage", git_pathspec_match_list_failed_entry(m, 0)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_FIND_FAILURES | GIT_PATHSPEC_FAILURES_ONLY, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(1, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__workdir1(void) +{ + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "*_FILE", "NEW_FILE", "GARBAGE" } */ + s.strings = str1; s.count = ARRAY_SIZE(str1); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_IGNORE_CASE, ps)); + cl_assert_equal_sz(10, git_pathspec_match_list_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_USE_CASE, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_fail(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_USE_CASE | GIT_PATHSPEC_NO_MATCH_ERROR, ps)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_IGNORE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(10, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(1, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_USE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(3, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__workdir2(void) +{ + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "staged_*" } */ + s.strings = str2; s.count = ARRAY_SIZE(str2); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, 0, ps)); + cl_assert_equal_sz(5, git_pathspec_match_list_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(5, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_fail(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_NO_GLOB | GIT_PATHSPEC_NO_MATCH_ERROR, ps)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_NO_GLOB | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(1, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__workdir3(void) +{ + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "!subdir", "*_file", "new_file" } */ + s.strings = str3; s.count = ARRAY_SIZE(str3); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, 0, ps)); + cl_assert_equal_sz(7, git_pathspec_match_list_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(7, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + + cl_assert_equal_s("current_file", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("modified_file", git_pathspec_match_list_entry(m, 1)); + cl_assert_equal_s("new_file", git_pathspec_match_list_entry(m, 2)); + cl_assert_equal_s("staged_changes_modified_file", git_pathspec_match_list_entry(m, 3)); + cl_assert_equal_s("staged_delete_modified_file", git_pathspec_match_list_entry(m, 4)); + cl_assert_equal_s("staged_new_file", git_pathspec_match_list_entry(m, 5)); + cl_assert_equal_s("staged_new_file_modified_file", git_pathspec_match_list_entry(m, 6)); + cl_assert_equal_s(NULL, git_pathspec_match_list_entry(m, 7)); + + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__workdir4(void) +{ + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "*" } */ + s.strings = str4; s.count = ARRAY_SIZE(str4); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, 0, ps)); + cl_assert_equal_sz(13, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_s("θΏ™", git_pathspec_match_list_entry(m, 12)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); +} + + +void test_repo_pathspec__index0(void) +{ + git_index *idx; + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + cl_git_pass(git_repository_index(&idx, g_repo)); + + /* { "*_file", "new_file", "garbage" } */ + s.strings = str0; s.count = ARRAY_SIZE(str0); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_index(&m, idx, 0, ps)); + cl_assert_equal_sz(9, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + cl_assert_equal_s("current_file", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("modified_file", git_pathspec_match_list_entry(m, 1)); + cl_assert_equal_s("staged_changes_modified_file", git_pathspec_match_list_entry(m, 2)); + cl_assert_equal_s("staged_new_file", git_pathspec_match_list_entry(m, 3)); + cl_assert_equal_s("staged_new_file_deleted_file", git_pathspec_match_list_entry(m, 4)); + cl_assert_equal_s("staged_new_file_modified_file", git_pathspec_match_list_entry(m, 5)); + cl_assert_equal_s("subdir/current_file", git_pathspec_match_list_entry(m, 6)); + cl_assert_equal_s("subdir/deleted_file", git_pathspec_match_list_entry(m, 7)); + cl_assert_equal_s("subdir/modified_file", git_pathspec_match_list_entry(m, 8)); + cl_assert_equal_s(NULL, git_pathspec_match_list_entry(m, 9)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_index(&m, idx, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(9, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(2, git_pathspec_match_list_failed_entrycount(m)); + cl_assert_equal_s("new_file", git_pathspec_match_list_failed_entry(m, 0)); + cl_assert_equal_s("garbage", git_pathspec_match_list_failed_entry(m, 1)); + cl_assert_equal_s(NULL, git_pathspec_match_list_failed_entry(m, 2)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); + git_index_free(idx); +} + +void test_repo_pathspec__index1(void) +{ + /* Currently the USE_CASE and IGNORE_CASE flags don't work on the + * index because the index sort order for the index iterator is + * set by the index itself. I think the correct fix is for the + * index not to embed a global sort order but to support traversal + * in either case sensitive or insensitive order in a stateless + * manner. + * + * Anyhow, as it is, there is no point in doing this test. + */ +#if 0 + git_index *idx; + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + cl_git_pass(git_repository_index(&idx, g_repo)); + + /* { "*_FILE", "NEW_FILE", "GARBAGE" } */ + s.strings = str1; s.count = ARRAY_SIZE(str1); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_index(&m, idx, + GIT_PATHSPEC_USE_CASE, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_index(&m, idx, + GIT_PATHSPEC_USE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(3, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_index(&m, idx, + GIT_PATHSPEC_IGNORE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(10, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(2, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); + git_index_free(idx); +#endif +} + +void test_repo_pathspec__tree0(void) +{ + git_object *tree; + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "*_file", "new_file", "garbage" } */ + s.strings = str0; s.count = ARRAY_SIZE(str0); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_revparse_single(&tree, g_repo, "HEAD~2^{tree}")); + + cl_git_pass(git_pathspec_match_tree(&m, (git_tree *)tree, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(4, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_s("current_file", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("modified_file", git_pathspec_match_list_entry(m, 1)); + cl_assert_equal_s("staged_changes_modified_file", git_pathspec_match_list_entry(m, 2)); + cl_assert_equal_s("staged_delete_modified_file", git_pathspec_match_list_entry(m, 3)); + cl_assert_equal_s(NULL, git_pathspec_match_list_entry(m, 4)); + cl_assert_equal_sz(2, git_pathspec_match_list_failed_entrycount(m)); + cl_assert_equal_s("new_file", git_pathspec_match_list_failed_entry(m, 0)); + cl_assert_equal_s("garbage", git_pathspec_match_list_failed_entry(m, 1)); + cl_assert_equal_s(NULL, git_pathspec_match_list_failed_entry(m, 2)); + git_pathspec_match_list_free(m); + + git_object_free(tree); + + cl_git_pass(git_revparse_single(&tree, g_repo, "HEAD^{tree}")); + + cl_git_pass(git_pathspec_match_tree(&m, (git_tree *)tree, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(7, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_s("current_file", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("modified_file", git_pathspec_match_list_entry(m, 1)); + cl_assert_equal_s("staged_changes_modified_file", git_pathspec_match_list_entry(m, 2)); + cl_assert_equal_s("staged_delete_modified_file", git_pathspec_match_list_entry(m, 3)); + cl_assert_equal_s("subdir/current_file", git_pathspec_match_list_entry(m, 4)); + cl_assert_equal_s("subdir/deleted_file", git_pathspec_match_list_entry(m, 5)); + cl_assert_equal_s("subdir/modified_file", git_pathspec_match_list_entry(m, 6)); + cl_assert_equal_s(NULL, git_pathspec_match_list_entry(m, 7)); + cl_assert_equal_sz(2, git_pathspec_match_list_failed_entrycount(m)); + cl_assert_equal_s("new_file", git_pathspec_match_list_failed_entry(m, 0)); + cl_assert_equal_s("garbage", git_pathspec_match_list_failed_entry(m, 1)); + cl_assert_equal_s(NULL, git_pathspec_match_list_failed_entry(m, 2)); + git_pathspec_match_list_free(m); + + git_object_free(tree); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__tree5(void) +{ + git_object *tree; + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "S*" } */ + s.strings = str5; s.count = ARRAY_SIZE(str5); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_revparse_single(&tree, g_repo, "HEAD~2^{tree}")); + + cl_git_pass(git_pathspec_match_tree(&m, (git_tree *)tree, + GIT_PATHSPEC_USE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(1, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_tree(&m, (git_tree *)tree, + GIT_PATHSPEC_IGNORE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(5, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_s("staged_changes", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("staged_delete_modified_file", git_pathspec_match_list_entry(m, 4)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_object_free(tree); + + cl_git_pass(git_revparse_single(&tree, g_repo, "HEAD^{tree}")); + + cl_git_pass(git_pathspec_match_tree(&m, (git_tree *)tree, + GIT_PATHSPEC_IGNORE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(9, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_s("staged_changes", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("subdir.txt", git_pathspec_match_list_entry(m, 5)); + cl_assert_equal_s("subdir/current_file", git_pathspec_match_list_entry(m, 6)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_object_free(tree); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__in_memory(void) +{ + static char *strings[] = { "one", "two*", "!three*", "*four" }; + git_strarray s = { strings, ARRAY_SIZE(strings) }; + git_pathspec *ps; + + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_assert(git_pathspec_matches_path(ps, 0, "one")); + cl_assert(!git_pathspec_matches_path(ps, 0, "ONE")); + cl_assert(git_pathspec_matches_path(ps, GIT_PATHSPEC_IGNORE_CASE, "ONE")); + cl_assert(git_pathspec_matches_path(ps, 0, "two")); + cl_assert(git_pathspec_matches_path(ps, 0, "two.txt")); + cl_assert(!git_pathspec_matches_path(ps, 0, "three.txt")); + cl_assert(git_pathspec_matches_path(ps, 0, "anything.four")); + cl_assert(!git_pathspec_matches_path(ps, 0, "three.four")); + cl_assert(!git_pathspec_matches_path(ps, 0, "nomatch")); + cl_assert(!git_pathspec_matches_path(ps, GIT_PATHSPEC_NO_GLOB, "two")); + cl_assert(git_pathspec_matches_path(ps, GIT_PATHSPEC_NO_GLOB, "two*")); + cl_assert(!git_pathspec_matches_path(ps, GIT_PATHSPEC_NO_GLOB, "anyfour")); + cl_assert(git_pathspec_matches_path(ps, GIT_PATHSPEC_NO_GLOB, "*four")); + + git_pathspec_free(ps); +}