diff options
-rw-r--r-- | include/git2/diff.h | 8 | ||||
-rw-r--r-- | include/git2/pathspec.h | 41 | ||||
-rw-r--r-- | src/array.h | 2 | ||||
-rw-r--r-- | src/diff.c | 10 | ||||
-rw-r--r-- | src/diff.h | 2 | ||||
-rw-r--r-- | src/pathspec.c | 314 | ||||
-rw-r--r-- | src/pathspec.h | 12 | ||||
-rw-r--r-- | tests-clar/diff/pathspec.c | 92 |
8 files changed, 404 insertions, 77 deletions
diff --git a/include/git2/diff.h b/include/git2/diff.h index 43029c49c..121c9df5c 100644 --- a/include/git2/diff.h +++ b/include/git2/diff.h @@ -798,6 +798,14 @@ GIT_EXTERN(size_t) git_diff_num_deltas_of_type( git_delta_t type); /** + * Check if deltas are sorted case sensitively or insensitively. + * + * @param diff Diff list to check + * @return 0 if case sensitive, 1 if case is ignored + */ +GIT_EXTERN(int) git_diff_is_sorted_icase(const git_diff_list *diff); + +/** * Return the diff delta and patch for an entry in the diff list. * * The `git_diff_patch` is a newly created object contains the text diffs diff --git a/include/git2/pathspec.h b/include/git2/pathspec.h index 6d97bb326..a835f8e52 100644 --- a/include/git2/pathspec.h +++ b/include/git2/pathspec.h @@ -10,6 +10,7 @@ #include "common.h" #include "types.h" #include "strarray.h" +#include "diff.h" /** * Compiled pathspec @@ -167,6 +168,30 @@ GIT_EXTERN(int) git_pathspec_match_tree( git_pathspec *ps); /** + * Match a pathspec against files in a diff list. + * + * This matches the pathspec against the files in the given diff list. + * + * If `out` is not NULL, this returns a `git_patchspec_match_list`. That + * contains the list of all matched filenames (unless you pass the + * `GIT_PATHSPEC_FAILURES_ONLY` flag) and may also contain the list of + * pathspecs with no match (if you used the `GIT_PATHSPEC_FIND_FAILURES` + * flag). You must call `git_pathspec_match_list_free()` on this object. + * + * @param out Output list of matches; pass NULL to just get return value + * @param diff A generated diff list + * @param flags Combination of git_pathspec_flag_t options to control match + * @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_diff( + git_pathspec_match_list **out, + git_diff_list *diff, + 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 @@ -185,6 +210,9 @@ GIT_EXTERN(size_t) git_pathspec_match_list_entrycount( /** * Get a matching filename by position. * + * This routine cannot be used if the match list was generated by + * `git_pathspec_match_diff`. If so, it will always return NULL. + * * @param m The git_pathspec_match_list object * @param pos The index into the list * @return The filename of the match @@ -193,6 +221,19 @@ GIT_EXTERN(const char *) git_pathspec_match_list_entry( const git_pathspec_match_list *m, size_t pos); /** + * Get a matching diff delta by position. + * + * This routine can only be used if the match list was generated by + * `git_pathspec_match_diff`. Otherwise it will always return NULL. + * + * @param m The git_pathspec_match_list object + * @param pos The index into the list + * @return The filename of the match + */ +GIT_EXTERN(const git_diff_delta *) git_pathspec_match_list_diff_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 diff --git a/src/array.h b/src/array.h index 707570624..248010425 100644 --- a/src/array.h +++ b/src/array.h @@ -66,4 +66,6 @@ GIT_INLINE(void *) git_array_grow(git_array_generic_t *a, size_t item_size) #define git_array_size(a) (a).size +#define git_array_valid_index(a, i) ((i) < (a).size) + #endif diff --git a/src/diff.c b/src/diff.c index 56232ebf4..cc7be451f 100644 --- a/src/diff.c +++ b/src/diff.c @@ -247,6 +247,11 @@ GIT_INLINE(const char *) diff_delta__path(const git_diff_delta *delta) return str; } +const char *git_diff_delta__path(const git_diff_delta *delta) +{ + return diff_delta__path(delta); +} + int git_diff_delta__cmp(const void *a, const void *b) { const git_diff_delta *da = a, *db = b; @@ -1235,6 +1240,11 @@ size_t git_diff_num_deltas_of_type(git_diff_list *diff, git_delta_t type) return count; } +int git_diff_is_sorted_icase(const git_diff_list *diff) +{ + return (diff->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0; +} + int git_diff__paired_foreach( git_diff_list *head2idx, git_diff_list *idx2wd, diff --git a/src/diff.h b/src/diff.h index 6ef03ee7c..d09a130bc 100644 --- a/src/diff.h +++ b/src/diff.h @@ -76,6 +76,8 @@ extern void git_diff_list_addref(git_diff_list *diff); extern int git_diff_delta__cmp(const void *a, const void *b); extern int git_diff_delta__casecmp(const void *a, const void *b); +extern const char *git_diff_delta__path(const git_diff_delta *delta); + extern bool git_diff_delta__should_skip( const git_diff_options *opts, const git_diff_delta *delta); diff --git a/src/pathspec.c b/src/pathspec.c index 021f38f1c..625726e0b 100644 --- a/src/pathspec.c +++ b/src/pathspec.c @@ -6,12 +6,15 @@ */ #include "git2/pathspec.h" +#include "git2/diff.h" #include "pathspec.h" #include "buf_text.h" #include "attr_file.h" #include "iterator.h" #include "repository.h" #include "index.h" +#include "bitvec.h" +#include "diff.h" /* what is the common non-wildcard prefix for all items in the pathspec */ char *git_pathspec_prefix(const git_strarray *pathspec) @@ -162,6 +165,28 @@ static int pathspec_match_one( return -1; } +static int git_pathspec__match_at( + size_t *matched_at, + const git_vector *vspec, + struct pathspec_match_context *ctxt, + const char *path0, + const char *path1) +{ + int result = GIT_ENOTFOUND; + size_t i = 0; + const git_attr_fnmatch *match; + + git_vector_foreach(vspec, i, match) { + if (path0 && (result = pathspec_match_one(match, ctxt, path0)) >= 0) + break; + if (path1 && (result = pathspec_match_one(match, ctxt, path1)) >= 0) + break; + } + + *matched_at = i; + return result; +} + /* match a path against the vectorized pathspec */ bool git_pathspec__match( const git_vector *vspec, @@ -171,8 +196,8 @@ bool git_pathspec__match( const char **matched_pathspec, size_t *matched_at) { - size_t i; - const git_attr_fnmatch *match; + int result; + size_t pos; struct pathspec_match_context ctxt; if (matched_pathspec) @@ -185,20 +210,18 @@ bool git_pathspec__match( pathspec_match_context_init(&ctxt, disable_fnmatch, casefold); - git_vector_foreach(vspec, i, match) { - int result = pathspec_match_one(match, &ctxt, path); - - if (result >= 0) { - if (matched_pathspec) - *matched_pathspec = match->pattern; - if (matched_at) - *matched_at = i; - - return (result != 0); + result = git_pathspec__match_at(&pos, vspec, &ctxt, path, NULL); + if (result >= 0) { + if (matched_pathspec) { + const git_attr_fnmatch *match = git_vector_get(vspec, pos); + *matched_pathspec = match->pattern; } + + if (matched_at) + *matched_at = pos; } - return false; + return (result > 0); } @@ -277,7 +300,8 @@ static void pathspec_match_free(git_pathspec_match_list *m) git__free(m); } -static git_pathspec_match_list *pathspec_match_alloc(git_pathspec *ps) +static git_pathspec_match_list *pathspec_match_alloc( + git_pathspec *ps, int datatype) { git_pathspec_match_list *m = git__calloc(1, sizeof(git_pathspec_match_list)); @@ -292,16 +316,73 @@ static git_pathspec_match_list *pathspec_match_alloc(git_pathspec *ps) */ GIT_REFCOUNT_INC(ps); m->pathspec = ps; + m->datatype = datatype; return m; } -GIT_INLINE(void) pathspec_mark_pattern(uint8_t *used, size_t pos, size_t *ct) +GIT_INLINE(size_t) pathspec_mark_pattern(git_bitvec *used, size_t pos) +{ + if (!git_bitvec_get(used, pos)) { + git_bitvec_set(used, pos, true); + return 1; + } + + return 0; +} + +static size_t pathspec_mark_remaining( + git_bitvec *used, + git_vector *patterns, + struct pathspec_match_context *ctxt, + size_t start, + const char *path0, + const char *path1) +{ + size_t count = 0; + + if (path1 == path0) + path1 = NULL; + + for (; start < patterns->length; ++start) { + const git_attr_fnmatch *pat = git_vector_get(patterns, start); + + if (git_bitvec_get(used, start)) + continue; + + if (path0 && pathspec_match_one(pat, ctxt, path0) > 0) + count += pathspec_mark_pattern(used, start); + else if (path1 && pathspec_match_one(pat, ctxt, path1) > 0) + count += pathspec_mark_pattern(used, start); + } + + return count; +} + +static int pathspec_build_failure_array( + git_pathspec_string_array_t *failures, + git_vector *patterns, + git_bitvec *used, + git_pool *pool) { - if (!used[pos]) { - used[pos] = 1; - (*ct)++; + size_t pos; + char **failed; + const git_attr_fnmatch *pat; + + for (pos = 0; pos < patterns->length; ++pos) { + if (git_bitvec_get(used, pos)) + continue; + + if ((failed = git_array_alloc(*failures)) == NULL) + return -1; + + pat = git_vector_get(patterns, pos); + + if ((*failed = git_pool_strdup(pool, pat->pattern)) == NULL) + return -1; } + + return 0; } static int pathspec_match_from_iterator( @@ -315,47 +396,37 @@ static int pathspec_match_from_iterator( 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; + bool find_failures = out && (flags & GIT_PATHSPEC_FIND_FAILURES) != 0; + bool failures_only = !out || (flags & GIT_PATHSPEC_FAILURES_ONLY) != 0; size_t pos, used_ct = 0, found_files = 0; git_index *index = NULL; - uint8_t *used_patterns = NULL; + git_bitvec used_patterns; char **file; + if (git_bitvec_init(&used_patterns, patterns->length) < 0) + return -1; + if (out) { - *out = m = pathspec_match_alloc(ps); + *out = m = pathspec_match_alloc(ps, PATHSPEC_DATATYPE_STRINGS); GITERR_CHECK_ALLOC(m); - } else { - failures_only = true; - find_failures = false; } 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)); + 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; - } + /* search for match with entry->path */ + int result = git_pathspec__match_at( + &pos, patterns, &ctxt, entry->path, NULL); /* no matches for this path */ if (result < 0) @@ -363,31 +434,24 @@ static int pathspec_match_from_iterator( /* if result was a negative pattern match, then don't list file */ if (!result) { - pathspec_mark_pattern(used_patterns, pos, &used_ct); + used_ct += pathspec_mark_pattern(&used_patterns, pos); continue; } - /* check if path is untracked and ignored */ + /* check if path is ignored and untracked */ 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); + used_ct += pathspec_mark_pattern(&used_patterns, pos); ++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 (find_failures && used_ct < patterns->length) + used_ct += pathspec_mark_remaining( + &used_patterns, patterns, &ctxt, pos + 1, entry->path, NULL); /* if only looking at failures, exit early or just continue */ if (failures_only || !out) { @@ -397,7 +461,7 @@ static int pathspec_match_from_iterator( } /* insert matched path into matches array */ - if ((file = git_array_alloc(m->matches)) == NULL || + if ((file = (char **)git_array_alloc(m->matches)) == NULL || (*file = git_pool_strdup(&m->pool, entry->path)) == NULL) { error = -1; goto done; @@ -409,19 +473,10 @@ static int pathspec_match_from_iterator( 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 (find_failures && used_ct < patterns->length && + (error = pathspec_build_failure_array( + &m->failures, patterns, &used_patterns, &m->pool)) < 0) + goto done; /* if every pattern failed to match, then we have failed */ if ((flags & GIT_PATHSPEC_NO_MATCH_ERROR) != 0 && !found_files) { @@ -430,7 +485,7 @@ static int pathspec_match_from_iterator( } done: - git__free(used_patterns); + git_bitvec_free(&used_patterns); if (error < 0) { pathspec_match_free(m); @@ -518,33 +573,142 @@ int git_pathspec_match_tree( return error; } +int git_pathspec_match_diff( + git_pathspec_match_list **out, + git_diff_list *diff, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_pathspec_match_list *m = NULL; + struct pathspec_match_context ctxt; + git_vector *patterns = &ps->pathspec; + bool find_failures = out && (flags & GIT_PATHSPEC_FIND_FAILURES) != 0; + bool failures_only = !out || (flags & GIT_PATHSPEC_FAILURES_ONLY) != 0; + size_t i, pos, used_ct = 0, found_deltas = 0; + const git_diff_delta *delta, **match; + git_bitvec used_patterns; + + assert(diff); + + if (git_bitvec_init(&used_patterns, patterns->length) < 0) + return -1; + + if (out) { + *out = m = pathspec_match_alloc(ps, PATHSPEC_DATATYPE_DIFF); + GITERR_CHECK_ALLOC(m); + } + + pathspec_match_context_init( + &ctxt, (flags & GIT_PATHSPEC_NO_GLOB) != 0, + git_diff_is_sorted_icase(diff)); + + git_vector_foreach(&diff->deltas, i, delta) { + /* search for match with delta */ + int result = git_pathspec__match_at( + &pos, patterns, &ctxt, delta->old_file.path, delta->new_file.path); + + /* no matches for this path */ + if (result < 0) + continue; + + /* mark the matched pattern as used */ + used_ct += pathspec_mark_pattern(&used_patterns, pos); + + /* if result was a negative pattern match, then don't list file */ + if (!result) + continue; + + ++found_deltas; + + /* if find_failures is on, check if any later patterns also match */ + if (find_failures && used_ct < patterns->length) + used_ct += pathspec_mark_remaining( + &used_patterns, patterns, &ctxt, pos + 1, + delta->old_file.path, delta->new_file.path); + + /* if only looking at failures, exit early or just continue */ + if (failures_only || !out) { + if (used_ct == patterns->length) + break; + continue; + } + + /* insert matched delta into matches array */ + if (!(match = (const git_diff_delta **)git_array_alloc(m->matches))) { + error = -1; + goto done; + } else { + *match = delta; + } + } + + /* insert patterns that had no matches into failures array */ + if (find_failures && used_ct < patterns->length && + (error = pathspec_build_failure_array( + &m->failures, patterns, &used_patterns, &m->pool)) < 0) + goto done; + + /* if every pattern failed to match, then we have failed */ + if ((flags & GIT_PATHSPEC_NO_MATCH_ERROR) != 0 && !found_deltas) { + giterr_set(GITERR_INVALID, "No matching deltas were found"); + error = GIT_ENOTFOUND; + } + +done: + git_bitvec_free(&used_patterns); + + if (error < 0) { + pathspec_match_free(m); + if (out) *out = NULL; + } + + return error; +} + void git_pathspec_match_list_free(git_pathspec_match_list *m) { - pathspec_match_free(m); + if (m) + pathspec_match_free(m); } size_t git_pathspec_match_list_entrycount( const git_pathspec_match_list *m) { - return git_array_size(m->matches); + return m ? git_array_size(m->matches) : 0; } 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; + if (!m || m->datatype != PATHSPEC_DATATYPE_STRINGS || + !git_array_valid_index(m->matches, pos)) + return NULL; + + return *((const char **)git_array_get(m->matches, pos)); +} + +const git_diff_delta *git_pathspec_match_list_diff_entry( + const git_pathspec_match_list *m, size_t pos) +{ + if (!m || m->datatype != PATHSPEC_DATATYPE_DIFF || + !git_array_valid_index(m->matches, pos)) + return NULL; + + return *((const git_diff_delta **)git_array_get(m->matches, pos)); } size_t git_pathspec_match_list_failed_entrycount( const git_pathspec_match_list *m) { - return git_array_size(m->failures); + return m ? git_array_size(m->failures) : 0; } 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); + char **entry = m ? git_array_get(m->failures, pos) : NULL; + return entry ? *entry : NULL; } + diff --git a/src/pathspec.h b/src/pathspec.h index e7edfea38..40cd21c3f 100644 --- a/src/pathspec.h +++ b/src/pathspec.h @@ -22,12 +22,20 @@ struct git_pathspec { git_pool pool; }; +enum { + PATHSPEC_DATATYPE_STRINGS = 0, + PATHSPEC_DATATYPE_DIFF = 1, +}; + +typedef git_array_t(char *) git_pathspec_string_array_t; + /* public interface to pathspec matching */ struct git_pathspec_match_list { git_pathspec *pathspec; - git_array_t(char *) matches; - git_array_t(char *) failures; + git_array_t(void *) matches; + git_pathspec_string_array_t failures; git_pool pool; + int datatype; }; /* what is the common non-wildcard prefix for all items in the pathspec */ diff --git a/tests-clar/diff/pathspec.c b/tests-clar/diff/pathspec.c new file mode 100644 index 000000000..332b513b3 --- /dev/null +++ b/tests-clar/diff/pathspec.c @@ -0,0 +1,92 @@ +#include "clar_libgit2.h" +#include "diff_helpers.h" + +static git_repository *g_repo = NULL; + +void test_diff_pathspec__initialize(void) +{ + g_repo = cl_git_sandbox_init("status"); +} + +void test_diff_pathspec__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_diff_pathspec__0(void) +{ + const char *a_commit = "26a125ee"; /* the current HEAD */ + const char *b_commit = "0017bd4a"; /* the start */ + git_tree *a = resolve_commit_oid_to_tree(g_repo, a_commit); + git_tree *b = resolve_commit_oid_to_tree(g_repo, b_commit); + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + git_diff_list *diff = NULL; + git_strarray paths = { NULL, 1 }; + char *path; + git_pathspec *ps; + git_pathspec_match_list *matches; + + cl_assert(a); + cl_assert(b); + + path = "*_file"; + paths.strings = &path; + cl_git_pass(git_pathspec_new(&ps, &paths)); + + cl_git_pass(git_pathspec_match_tree(&matches, a, GIT_PATHSPEC_DEFAULT, ps)); + cl_assert_equal_i(7, git_pathspec_match_list_entrycount(matches)); + cl_assert_equal_s("current_file", git_pathspec_match_list_entry(matches,0)); + cl_assert(git_pathspec_match_list_diff_entry(matches,0) == NULL); + git_pathspec_match_list_free(matches); + + cl_git_pass(git_diff_tree_to_tree(&diff, g_repo, NULL, a, &opts)); + + cl_git_pass(git_pathspec_match_diff( + &matches, diff, GIT_PATHSPEC_DEFAULT, ps)); + cl_assert_equal_i(7, git_pathspec_match_list_entrycount(matches)); + cl_assert(git_pathspec_match_list_diff_entry(matches, 0) != NULL); + cl_assert(git_pathspec_match_list_entry(matches, 0) == NULL); + cl_assert_equal_s("current_file", + git_pathspec_match_list_diff_entry(matches,0)->new_file.path); + cl_assert_equal_i(GIT_DELTA_ADDED, + git_pathspec_match_list_diff_entry(matches,0)->status); + git_pathspec_match_list_free(matches); + + git_diff_list_free(diff); + diff = NULL; + + cl_git_pass(git_diff_tree_to_tree(&diff, g_repo, a, b, &opts)); + + cl_git_pass(git_pathspec_match_diff( + &matches, diff, GIT_PATHSPEC_DEFAULT, ps)); + cl_assert_equal_i(3, git_pathspec_match_list_entrycount(matches)); + cl_assert(git_pathspec_match_list_diff_entry(matches, 0) != NULL); + cl_assert(git_pathspec_match_list_entry(matches, 0) == NULL); + cl_assert_equal_s("subdir/current_file", + git_pathspec_match_list_diff_entry(matches,0)->new_file.path); + cl_assert_equal_i(GIT_DELTA_DELETED, + git_pathspec_match_list_diff_entry(matches,0)->status); + git_pathspec_match_list_free(matches); + + git_diff_list_free(diff); + diff = NULL; + + cl_git_pass(git_diff_tree_to_workdir(&diff, g_repo, a, &opts)); + + cl_git_pass(git_pathspec_match_diff( + &matches, diff, GIT_PATHSPEC_DEFAULT, ps)); + cl_assert_equal_i(4, git_pathspec_match_list_entrycount(matches)); + cl_assert(git_pathspec_match_list_diff_entry(matches, 0) != NULL); + cl_assert(git_pathspec_match_list_entry(matches, 0) == NULL); + cl_assert_equal_s("modified_file", + git_pathspec_match_list_diff_entry(matches,0)->new_file.path); + cl_assert_equal_i(GIT_DELTA_MODIFIED, + git_pathspec_match_list_diff_entry(matches,0)->status); + git_pathspec_match_list_free(matches); + + git_diff_list_free(diff); + diff = NULL; + + git_tree_free(a); + git_tree_free(b); +} |