Commit 14a513e05866721f5ceba18cf425568d2f6af143

Russell Belfer 2012-04-13T15:00:29

Add support for pathspec to diff and status This adds preliminary support for pathspecs to diff and status. The implementation is not very optimized (it still looks at every single file and evaluated the the pathspec match against them), but it works.

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,