Commit e77e53edb37b7dd603da7758b243ef8e91d9e394

Vicent Martí 2012-04-13T20:06:49

Merge pull request #627 from arrbee/diff-with-pathspec Diff with pathspec

diff --git a/include/git2/common.h b/include/git2/common.h
index 170ef34..9186fe5 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 6568313..b2edce9 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 53e479a..294033d 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 fa841f7..c6a0088 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 b4a3755..9da07c2 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 95e4588..0732d4a 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 d0ad474..81ad106 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 132b906..6aeaa51 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 9fefdbb..2a93039 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,