Commit 7779437fd56882060eca6128e9680ba1705e0081

Russell Belfer 2012-07-24T11:21:32

Merge pull request #799 from yorah/fix/issue-787 Fix/issue 787

diff --git a/include/git2/diff.h b/include/git2/diff.h
index edec995..85727d9 100644
--- a/include/git2/diff.h
+++ b/include/git2/diff.h
@@ -46,6 +46,7 @@ enum {
 	GIT_DIFF_INCLUDE_UNTRACKED = (1 << 8),
 	GIT_DIFF_INCLUDE_UNMODIFIED = (1 << 9),
 	GIT_DIFF_RECURSE_UNTRACKED_DIRS = (1 << 10),
+	GIT_DIFF_DISABLE_PATHSPEC_MATCH = (1 << 11),
 };
 
 /**
diff --git a/include/git2/status.h b/include/git2/status.h
index 69b6e47..9e7b5de 100644
--- a/include/git2/status.h
+++ b/include/git2/status.h
@@ -96,6 +96,8 @@ typedef enum {
  *   the top-level directory will be included (with a trailing
  *   slash on the entry name).  Given this flag, the directory
  *   itself will not be included, but all the files in it will.
+ * - GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH indicates that the given
+ *   path will be treated as a literal path, and not as a pathspec.
  */
 
 enum {
@@ -104,6 +106,7 @@ enum {
 	GIT_STATUS_OPT_INCLUDE_UNMODIFIED = (1 << 2),
 	GIT_STATUS_OPT_EXCLUDE_SUBMODULES = (1 << 3),
 	GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS = (1 << 4),
+	GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH = (1 << 5),
 };
 
 /**
diff --git a/src/attr_file.c b/src/attr_file.c
index 0dad097..837c42d 100644
--- a/src/attr_file.c
+++ b/src/attr_file.c
@@ -426,17 +426,7 @@ int git_attr_fnmatch__parse(
 		return -1;
 	} else {
 		/* strip '\' that might have be used for internal whitespace */
-		char *to = spec->pattern;
-		for (scan = spec->pattern; *scan; to++, scan++) {
-			if (*scan == '\\')
-				scan++; /* skip '\' but include next char */
-			if (to != scan)
-				*to = *scan;
-		}
-		if (to != scan) {
-			*to = '\0';
-			spec->length = (to - spec->pattern);
-		}
+		spec->length = git__unescape(spec->pattern);
 	}
 
 	return 0;
diff --git a/src/buffer.c b/src/buffer.c
index 5d54ee1..b57998e 100644
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -496,3 +496,7 @@ bool git_buf_is_binary(const git_buf *buf)
 	return ((printable >> 7) < nonprintable);
 }
 
+void git_buf_unescape(git_buf *buf)
+{
+	buf->size = git__unescape(buf->ptr);
+}
diff --git a/src/buffer.h b/src/buffer.h
index 75f3b0e..17922e4 100644
--- a/src/buffer.h
+++ b/src/buffer.h
@@ -151,4 +151,7 @@ int git_buf_common_prefix(git_buf *buf, const git_strarray *strings);
 /* Check if buffer looks like it contains binary data */
 bool git_buf_is_binary(const git_buf *buf);
 
+/* Unescape all characters in a buffer */
+void git_buf_unescape(git_buf *buf);
+
 #endif
diff --git a/src/diff.c b/src/diff.c
index 09f319e..2b1529d 100644
--- a/src/diff.c
+++ b/src/diff.c
@@ -20,14 +20,21 @@ static char *diff_prefix_from_pathspec(const git_strarray *pathspec)
 		return NULL;
 
 	/* diff prefix will only be leading non-wildcards */
-	for (scan = prefix.ptr; *scan && !git__iswildcard(*scan); ++scan);
+	for (scan = prefix.ptr; *scan; ++scan) {
+		if (git__iswildcard(*scan) &&
+			(scan == prefix.ptr || (*(scan - 1) != '\\')))
+			break;
+	}
 	git_buf_truncate(&prefix, scan - prefix.ptr);
 
-	if (prefix.size > 0)
-		return git_buf_detach(&prefix);
+	if (prefix.size <= 0) {
+		git_buf_free(&prefix);
+		return NULL;
+	}
 
-	git_buf_free(&prefix);
-	return NULL;
+	git_buf_unescape(&prefix);
+
+	return git_buf_detach(&prefix);
 }
 
 static bool diff_pathspec_is_interesting(const git_strarray *pathspec)
@@ -54,7 +61,11 @@ static bool diff_path_matches_pathspec(git_diff_list *diff, const char *path)
 		return true;
 
 	git_vector_foreach(&diff->pathspec, i, match) {
-		int result = p_fnmatch(match->pattern, path, 0);
+		int result = strcmp(match->pattern, path) ? FNM_NOMATCH : 0;
+		
+		if (((diff->opts.flags & GIT_DIFF_DISABLE_PATHSPEC_MATCH) == 0) && 
+			result == FNM_NOMATCH)
+			result = p_fnmatch(match->pattern, path, 0);
 
 		/* if we didn't match, look for exact dirname prefix match */
 		if (result == FNM_NOMATCH &&
@@ -826,4 +837,3 @@ int git_diff_merge(
 
 	return error;
 }
-
diff --git a/src/status.c b/src/status.c
index e9ad3cf..d782376 100644
--- a/src/status.c
+++ b/src/status.c
@@ -99,6 +99,8 @@ int git_status_foreach_ext(
 		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNMODIFIED;
 	if ((opts->flags & GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS) != 0)
 		diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_UNTRACKED_DIRS;
+	if ((opts->flags & GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH) != 0)
+		diffopt.flags = diffopt.flags | GIT_DIFF_DISABLE_PATHSPEC_MATCH;
 	/* TODO: support EXCLUDE_SUBMODULES flag */
 
 	if (show != GIT_STATUS_SHOW_WORKDIR_ONLY &&
@@ -176,10 +178,12 @@ static int get_one_status(const char *path, unsigned int status, void *data)
 	sfi->count++;
 	sfi->status = status;
 
-	if (sfi->count > 1 || strcmp(sfi->expected, path) != 0) {
+	if (sfi->count > 1 || 
+		(strcmp(sfi->expected, path) != 0 &&
+		p_fnmatch(sfi->expected, path, 0) != 0)) {
 		giterr_set(GITERR_INVALID,
 			"Ambiguous path '%s' given to git_status_file", sfi->expected);
-		return -1;
+		return GIT_EAMBIGUOUS;
 	}
 
 	return 0;
diff --git a/src/util.c b/src/util.c
index 3093cd7..90bb3d0 100644
--- a/src/util.c
+++ b/src/util.c
@@ -435,3 +435,21 @@ int git__parse_bool(int *out, const char *value)
 
 	return -1;
 }
+
+size_t git__unescape(char *str)
+{
+	char *scan, *pos = str;
+
+	for (scan = str; *scan; pos++, scan++) {
+		if (*scan == '\\' && *(scan + 1) != '\0')
+			scan++; /* skip '\' but include next char */
+		if (pos != scan)
+			*pos = *scan;
+	}
+
+	if (pos != scan) {
+		*pos = '\0';
+	}
+
+	return (pos - str);
+}
diff --git a/src/util.h b/src/util.h
index a84dcab..905fc92 100644
--- a/src/util.h
+++ b/src/util.h
@@ -238,4 +238,13 @@ extern int git__parse_bool(int *out, const char *value);
  */
 int git__date_parse(git_time_t *out, const char *date);
 
+/*
+ * Unescapes a string in-place.
+ * 
+ * Edge cases behavior:
+ * - "jackie\" -> "jacky\"
+ * - "chan\\" -> "chan\"
+ */
+extern size_t git__unescape(char *str);
+
 #endif /* INCLUDE_util_h__ */
diff --git a/tests-clar/core/buffer.c b/tests-clar/core/buffer.c
index 21aaaed..b6274b0 100644
--- a/tests-clar/core/buffer.c
+++ b/tests-clar/core/buffer.c
@@ -658,3 +658,23 @@ void test_core_buffer__puts_escaped(void)
 
 	git_buf_free(&a);
 }
+
+static void assert_unescape(char *expected, char *to_unescape) {
+	git_buf buf = GIT_BUF_INIT;
+
+	cl_git_pass(git_buf_sets(&buf, to_unescape));
+	git_buf_unescape(&buf);
+	cl_assert_equal_s(expected, buf.ptr);
+	cl_assert_equal_i(strlen(expected), buf.size);
+
+	git_buf_free(&buf);
+}
+
+void test_core_buffer__unescape(void)
+{
+	assert_unescape("Escaped\\", "Es\\ca\\ped\\");
+	assert_unescape("Es\\caped\\", "Es\\\\ca\\ped\\\\");
+	assert_unescape("\\", "\\");
+	assert_unescape("\\", "\\\\");
+	assert_unescape("", "");
+}
diff --git a/tests-clar/status/worktree.c b/tests-clar/status/worktree.c
index fed81e5..bd57cf2 100644
--- a/tests-clar/status/worktree.c
+++ b/tests-clar/status/worktree.c
@@ -517,6 +517,85 @@ void test_status_worktree__status_file_with_clean_index_and_empty_workdir(void)
 	cl_git_pass(p_unlink("my-index"));
 }
 
+void test_status_worktree__bracket_in_filename(void)
+{
+	git_repository *repo;
+	git_index *index;
+	status_entry_single result;
+	unsigned int status_flags;
+	int error;
+
+	#define FILE_WITH_BRACKET "LICENSE[1].md"
+	#define FILE_WITHOUT_BRACKET "LICENSE1.md"
+
+	cl_git_pass(git_repository_init(&repo, "with_bracket", 0));
+	cl_git_mkfile("with_bracket/" FILE_WITH_BRACKET, "I have a bracket in my name\n");
+	
+	/* file is new to working directory */
+
+	memset(&result, 0, sizeof(result));
+	cl_git_pass(git_status_foreach(repo, cb_status__single, &result));
+	cl_assert_equal_i(1, result.count);
+	cl_assert(result.status == GIT_STATUS_WT_NEW);
+
+	cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_BRACKET));
+	cl_assert(status_flags == GIT_STATUS_WT_NEW);
+
+	/* ignore the file */
+
+	cl_git_rewritefile("with_bracket/.gitignore", "*.md\n.gitignore\n");
+
+	memset(&result, 0, sizeof(result));
+	cl_git_pass(git_status_foreach(repo, cb_status__single, &result));
+	cl_assert_equal_i(2, result.count);
+	cl_assert(result.status == GIT_STATUS_IGNORED);
+
+	cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_BRACKET));
+	cl_assert(status_flags == GIT_STATUS_IGNORED);
+
+	/* don't ignore the file */
+
+	cl_git_rewritefile("with_bracket/.gitignore", ".gitignore\n");
+
+	memset(&result, 0, sizeof(result));
+	cl_git_pass(git_status_foreach(repo, cb_status__single, &result));
+	cl_assert_equal_i(2, result.count);
+	cl_assert(result.status == GIT_STATUS_WT_NEW);
+
+	cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_BRACKET));
+	cl_assert(status_flags == GIT_STATUS_WT_NEW);
+
+	/* add the file to the index */
+
+	cl_git_pass(git_repository_index(&index, repo));
+	cl_git_pass(git_index_add(index, FILE_WITH_BRACKET, 0));
+	cl_git_pass(git_index_write(index));
+
+	memset(&result, 0, sizeof(result));
+	cl_git_pass(git_status_foreach(repo, cb_status__single, &result));
+	cl_assert_equal_i(2, result.count);
+	cl_assert(result.status == GIT_STATUS_INDEX_NEW);
+
+	cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_BRACKET));
+	cl_assert(status_flags == GIT_STATUS_INDEX_NEW);
+	
+	/* Create file without bracket */
+
+	cl_git_mkfile("with_bracket/" FILE_WITHOUT_BRACKET, "I have no bracket in my name!\n");
+
+	cl_git_pass(git_status_file(&status_flags, repo, FILE_WITHOUT_BRACKET));
+	cl_assert(status_flags == GIT_STATUS_WT_NEW);
+
+	cl_git_pass(git_status_file(&status_flags, repo, "LICENSE\\[1\\].md"));
+	cl_assert(status_flags == GIT_STATUS_INDEX_NEW);
+
+	error = git_status_file(&status_flags, repo, FILE_WITH_BRACKET);
+	cl_git_fail(error);
+	cl_assert(error == GIT_EAMBIGUOUS);
+
+	git_index_free(index);
+	git_repository_free(repo);
+}
 
 void test_status_worktree__space_in_filename(void)
 {
@@ -647,3 +726,49 @@ void test_status_worktree__filemode_changes(void)
 
 	git_config_free(cfg);
 }
+
+int cb_status__expected_path(const char *p, unsigned int s, void *payload)
+{
+	const char *expected_path = (const char *)payload;
+
+	GIT_UNUSED(s);
+
+	if (payload == NULL)
+		cl_fail("Unexpected path");
+
+	cl_assert_equal_s(expected_path, p);
+
+	return 0;
+}
+
+void test_status_worktree__disable_pathspec_match(void)
+{
+	git_repository *repo;
+	git_status_options opts;
+	char *file_with_bracket = "LICENSE[1].md", 
+		*imaginary_file_with_bracket = "LICENSE[1-2].md";
+
+	cl_git_pass(git_repository_init(&repo, "pathspec", 0));
+	cl_git_mkfile("pathspec/LICENSE[1].md", "screaming bracket\n");
+	cl_git_mkfile("pathspec/LICENSE1.md", "no bracket\n");
+
+	memset(&opts, 0, sizeof(opts));
+	opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | 
+		GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH;
+	opts.pathspec.count = 1;
+	opts.pathspec.strings = &file_with_bracket;
+
+	cl_git_pass(
+		git_status_foreach_ext(repo, &opts, cb_status__expected_path, 
+		file_with_bracket)
+	);
+
+	/* Test passing a pathspec matching files in the workdir. */
+	/* Must not match because pathspecs are disabled. */ 
+	opts.pathspec.strings = &imaginary_file_with_bracket;
+	cl_git_pass(
+		git_status_foreach_ext(repo, &opts, cb_status__expected_path, NULL)
+	);
+	
+	git_repository_free(repo);
+}