diff --git a/include/git2/common.h b/include/git2/common.h index 170ef340d..9186fe54e 100644 --- a/include/git2/common.h +++ b/include/git2/common.h @@ -87,6 +87,7 @@ typedef struct { } git_strarray; GIT_EXTERN(void) git_strarray_free(git_strarray *array); +GIT_EXTERN(int) git_strarray_copy(git_strarray *tgt, const git_strarray *src); /** * Return the version of the libgit2 library diff --git a/src/attr_file.c b/src/attr_file.c index 6568313e5..b2edce90e 100644 --- a/src/attr_file.c +++ b/src/attr_file.c @@ -334,6 +334,10 @@ int git_attr_fnmatch__parse( spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH; slash_count++; } + /* remember if we see an unescaped wildcard in pattern */ + else if ((*scan == '*' || *scan == '.' || *scan == '[') && + (scan == pattern || (*(scan - 1) != '\\'))) + spec->flags = spec->flags | GIT_ATTR_FNMATCH_HASWILD; } *base = scan; diff --git a/src/attr_file.h b/src/attr_file.h index 53e479ad9..294033d5e 100644 --- a/src/attr_file.h +++ b/src/attr_file.h @@ -20,6 +20,7 @@ #define GIT_ATTR_FNMATCH_FULLPATH (1U << 2) #define GIT_ATTR_FNMATCH_MACRO (1U << 3) #define GIT_ATTR_FNMATCH_IGNORE (1U << 4) +#define GIT_ATTR_FNMATCH_HASWILD (1U << 5) typedef struct { char *pattern; diff --git a/src/diff.c b/src/diff.c index fa841f717..c6a0088ec 100644 --- a/src/diff.c +++ b/src/diff.c @@ -9,6 +9,50 @@ #include "diff.h" #include "fileops.h" #include "config.h" +#include "attr_file.h" + +static bool diff_pathspec_is_interesting(const git_strarray *pathspec) +{ + const char *str; + + if (pathspec == NULL || pathspec->count == 0) + return false; + if (pathspec->count > 1) + return true; + + str = pathspec->strings[0]; + if (!str || !str[0] || (!str[1] && (str[0] == '*' || str[0] == '.'))) + return false; + return true; +} + +static bool diff_path_matches_pathspec(git_diff_list *diff, const char *path) +{ + unsigned int i; + git_attr_fnmatch *match; + + if (!diff->pathspec.length) + return true; + + git_vector_foreach(&diff->pathspec, i, match) { + int result = git__fnmatch(match->pattern, path, 0); + + /* if we didn't match, look for exact dirname prefix match */ + if (result == GIT_ENOMATCH && + (match->flags & GIT_ATTR_FNMATCH_HASWILD) == 0 && + strncmp(path, match->pattern, match->length) == 0 && + path[match->length] == '/') + result = 0; + + if (result == 0) + return (match->flags & GIT_ATTR_FNMATCH_NEGATIVE) ? false : true; + + if (result != GIT_ENOMATCH) + giterr_clear(); + } + + return false; +} static void diff_delta__free(git_diff_delta *delta) { @@ -143,6 +187,9 @@ static int diff_delta__from_one( (diff->opts.flags & GIT_DIFF_INCLUDE_UNTRACKED) == 0) return 0; + if (!diff_path_matches_pathspec(diff, entry->path)) + return 0; + delta = diff_delta__alloc(diff, status, entry->path); GITERR_CHECK_ALLOC(delta); @@ -246,6 +293,7 @@ static git_diff_list *git_diff_list_alloc( git_repository *repo, const git_diff_options *opts) { git_config *cfg; + size_t i; git_diff_list *diff = git__calloc(1, sizeof(git_diff_list)); if (diff == NULL) return NULL; @@ -269,6 +317,7 @@ static git_diff_list *git_diff_list_alloc( return diff; memcpy(&diff->opts, opts, sizeof(git_diff_options)); + memset(&diff->opts.pathspec, 0, sizeof(diff->opts.pathspec)); diff->opts.src_prefix = diff_strdup_prefix( opts->src_prefix ? opts->src_prefix : DIFF_SRC_PREFIX_DEFAULT); @@ -287,21 +336,45 @@ static git_diff_list *git_diff_list_alloc( if (git_vector_init(&diff->deltas, 0, diff_delta__cmp) < 0) goto fail; - /* TODO: do something safe with the pathspec strarray */ + /* only copy pathspec if it is "interesting" so we can test + * diff->pathspec.length > 0 to know if it is worth calling + * fnmatch as we iterate. + */ + if (!diff_pathspec_is_interesting(&opts->pathspec)) + return diff; + + if (git_vector_init(&diff->pathspec, opts->pathspec.count, NULL) < 0) + goto fail; + + for (i = 0; i < opts->pathspec.count; ++i) { + int ret; + const char *pattern = opts->pathspec.strings[i]; + git_attr_fnmatch *match = + git__calloc(1, sizeof(git_attr_fnmatch)); + if (!match) + goto fail; + ret = git_attr_fnmatch__parse(match, NULL, &pattern); + if (ret == GIT_ENOTFOUND) { + git__free(match); + continue; + } else if (ret < 0) + goto fail; + + if (git_vector_insert(&diff->pathspec, match) < 0) + goto fail; + } return diff; fail: - git_vector_free(&diff->deltas); - git__free(diff->opts.src_prefix); - git__free(diff->opts.dst_prefix); - git__free(diff); + git_diff_list_free(diff); return NULL; } void git_diff_list_free(git_diff_list *diff) { git_diff_delta *delta; + git_attr_fnmatch *match; unsigned int i; if (!diff) @@ -312,6 +385,17 @@ void git_diff_list_free(git_diff_list *diff) diff->deltas.contents[i] = NULL; } git_vector_free(&diff->deltas); + + git_vector_foreach(&diff->pathspec, i, match) { + if (match != NULL) { + git__free(match->pattern); + match->pattern = NULL; + git__free(match); + diff->pathspec.contents[i] = NULL; + } + } + git_vector_free(&diff->pathspec); + git__free(diff->opts.src_prefix); git__free(diff->opts.dst_prefix); git__free(diff); @@ -366,6 +450,9 @@ static int maybe_modified( GIT_UNUSED(old); + if (!diff_path_matches_pathspec(diff, oitem->path)) + return 0; + /* on platforms with no symlinks, promote plain files to symlinks */ if (S_ISLNK(omode) && S_ISREG(nmode) && !(diff->diffcaps & GIT_DIFFCAPS_HAS_SYMLINKS)) diff --git a/src/diff.h b/src/diff.h index b4a375586..9da07c295 100644 --- a/src/diff.h +++ b/src/diff.h @@ -24,6 +24,7 @@ enum { struct git_diff_list { git_repository *repo; git_diff_options opts; + git_vector pathspec; git_vector deltas; /* vector of git_diff_file_delta */ git_iterator_type_t old_src; git_iterator_type_t new_src; diff --git a/src/status.c b/src/status.c index 95e4588b7..0732d4a9f 100644 --- a/src/status.c +++ b/src/status.c @@ -205,6 +205,7 @@ int git_status_foreach( { git_status_options opts; + memset(&opts, 0, sizeof(opts)); opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED | GIT_STATUS_OPT_INCLUDE_UNTRACKED | diff --git a/src/util.c b/src/util.c index d0ad47490..81ad10609 100644 --- a/src/util.c +++ b/src/util.c @@ -31,6 +31,35 @@ void git_strarray_free(git_strarray *array) git__free(array->strings); } +int git_strarray_copy(git_strarray *tgt, const git_strarray *src) +{ + size_t i; + + assert(tgt && src); + + memset(tgt, 0, sizeof(*tgt)); + + if (!src->count) + return 0; + + tgt->strings = git__calloc(src->count, sizeof(char *)); + GITERR_CHECK_ALLOC(tgt->strings); + + for (i = 0; i < src->count; ++i) { + tgt->strings[tgt->count] = git__strdup(src->strings[i]); + + if (!tgt->strings[tgt->count]) { + git_strarray_free(tgt); + memset(tgt, 0, sizeof(*tgt)); + return -1; + } + + tgt->count++; + } + + return 0; +} + int git__fnmatch(const char *pattern, const char *name, int flags) { int ret; diff --git a/tests-clar/attr/file.c b/tests-clar/attr/file.c index 132b906cd..6aeaa5135 100644 --- a/tests-clar/attr/file.c +++ b/tests-clar/attr/file.c @@ -20,7 +20,7 @@ void test_attr_file__simple_read(void) cl_assert(rule != NULL); cl_assert_strequal("*", rule->match.pattern); cl_assert(rule->match.length == 1); - cl_assert(rule->match.flags == 0); + cl_assert((rule->match.flags & GIT_ATTR_FNMATCH_HASWILD) != 0); cl_assert(rule->assigns.length == 1); assign = get_assign(rule, 0); @@ -74,14 +74,16 @@ void test_attr_file__match_variants(void) rule = get_rule(4); cl_assert_strequal("pat4.*", rule->match.pattern); - cl_assert(rule->match.flags == 0); + cl_assert((rule->match.flags & GIT_ATTR_FNMATCH_HASWILD) != 0); rule = get_rule(5); cl_assert_strequal("*.pat5", rule->match.pattern); + cl_assert((rule->match.flags & GIT_ATTR_FNMATCH_HASWILD) != 0); rule = get_rule(7); cl_assert_strequal("pat7[a-e]??[xyz]", rule->match.pattern); cl_assert(rule->assigns.length == 1); + cl_assert((rule->match.flags & GIT_ATTR_FNMATCH_HASWILD) != 0); assign = get_assign(rule,0); cl_assert_strequal("attr7", assign->name); cl_assert(GIT_ATTR_TRUE(assign->value)); diff --git a/tests-clar/diff/workdir.c b/tests-clar/diff/workdir.c index 9fefdbb03..2a93039f1 100644 --- a/tests-clar/diff/workdir.c +++ b/tests-clar/diff/workdir.c @@ -164,6 +164,79 @@ void test_diff_workdir__to_tree(void) git_tree_free(b); } +void test_diff_workdir__to_index_with_pathspec(void) +{ + git_diff_options opts = {0}; + git_diff_list *diff = NULL; + diff_expects exp; + char *pathspec = NULL; + + opts.context_lines = 3; + opts.interhunk_lines = 1; + opts.flags |= GIT_DIFF_INCLUDE_IGNORED | GIT_DIFF_INCLUDE_UNTRACKED; + opts.pathspec.strings = &pathspec; + opts.pathspec.count = 1; + + memset(&exp, 0, sizeof(exp)); + + cl_git_pass(git_diff_workdir_to_index(g_repo, &opts, &diff)); + cl_git_pass(git_diff_foreach(diff, &exp, diff_file_fn, NULL, NULL)); + + cl_assert_equal_i(12, exp.files); + cl_assert_equal_i(0, exp.file_adds); + cl_assert_equal_i(4, exp.file_dels); + cl_assert_equal_i(4, exp.file_mods); + cl_assert_equal_i(1, exp.file_ignored); + cl_assert_equal_i(3, exp.file_untracked); + + git_diff_list_free(diff); + + memset(&exp, 0, sizeof(exp)); + pathspec = "modified_file"; + + cl_git_pass(git_diff_workdir_to_index(g_repo, &opts, &diff)); + cl_git_pass(git_diff_foreach(diff, &exp, diff_file_fn, NULL, NULL)); + + cl_assert_equal_i(1, exp.files); + cl_assert_equal_i(0, exp.file_adds); + cl_assert_equal_i(0, exp.file_dels); + cl_assert_equal_i(1, exp.file_mods); + cl_assert_equal_i(0, exp.file_ignored); + cl_assert_equal_i(0, exp.file_untracked); + + git_diff_list_free(diff); + + memset(&exp, 0, sizeof(exp)); + pathspec = "subdir"; + + cl_git_pass(git_diff_workdir_to_index(g_repo, &opts, &diff)); + cl_git_pass(git_diff_foreach(diff, &exp, diff_file_fn, NULL, NULL)); + + cl_assert_equal_i(3, exp.files); + cl_assert_equal_i(0, exp.file_adds); + cl_assert_equal_i(1, exp.file_dels); + cl_assert_equal_i(1, exp.file_mods); + cl_assert_equal_i(0, exp.file_ignored); + cl_assert_equal_i(1, exp.file_untracked); + + git_diff_list_free(diff); + + memset(&exp, 0, sizeof(exp)); + pathspec = "*_deleted"; + + cl_git_pass(git_diff_workdir_to_index(g_repo, &opts, &diff)); + cl_git_pass(git_diff_foreach(diff, &exp, diff_file_fn, NULL, NULL)); + + cl_assert_equal_i(2, exp.files); + cl_assert_equal_i(0, exp.file_adds); + cl_assert_equal_i(2, exp.file_dels); + cl_assert_equal_i(0, exp.file_mods); + cl_assert_equal_i(0, exp.file_ignored); + cl_assert_equal_i(0, exp.file_untracked); + + git_diff_list_free(diff); +} + /* PREPARATION OF TEST DATA * * Since there is no command line equivalent of git_diff_workdir_to_tree,