Commit 51214b85dedc39b734cce9d09b9311221a31de4b

Patrick Steinhardt 2019-04-26T10:15:49

refs: loosen restriction on wildcard "*" refspecs In commit cd377f45c9 (refs: loosen restriction on wildcard "*" refspecs, 2015-07-22) in git.git, the restrictions on wildcard "*" refspecs has been loosened. While wildcards were previously only allowed if the component is a single "*", this was changed to also accept other patterns as part of the component. We never adapted to that change and still reject any wildcard patterns that aren't a single "*" only. Update our tests to reflect the upstream change and adjust our own code accordingly.

diff --git a/src/refs.c b/src/refs.c
index a031842..4604d4c 100644
--- a/src/refs.c
+++ b/src/refs.c
@@ -897,14 +897,13 @@ static int is_valid_ref_char(char ch)
 	case '\\':
 	case '?':
 	case '[':
-	case '*':
 		return 0;
 	default:
 		return 1;
 	}
 }
 
-static int ensure_segment_validity(const char *name)
+static int ensure_segment_validity(const char *name, char may_contain_glob)
 {
 	const char *current = name;
 	char prev = '\0';
@@ -927,6 +926,12 @@ static int ensure_segment_validity(const char *name)
 		if (prev == '@' && *current == '{')
 			return -1; /* Refname contains "@{" */
 
+		if (*current == '*') {
+			if (!may_contain_glob)
+				return -1;
+			may_contain_glob = 0;
+		}
+
 		prev = *current;
 	}
 
@@ -1005,19 +1010,20 @@ int git_reference__normalize_name(
 	}
 
 	while (true) {
-		segment_len = ensure_segment_validity(current);
-		if (segment_len < 0) {
-			if ((process_flags & GIT_REFERENCE_FORMAT_REFSPEC_PATTERN) &&
-					current[0] == '*' &&
-					(current[1] == '\0' || current[1] == '/')) {
-				/* Accept one wildcard as a full refname component. */
-				process_flags &= ~GIT_REFERENCE_FORMAT_REFSPEC_PATTERN;
-				segment_len = 1;
-			} else
-				goto cleanup;
-		}
+		char may_contain_glob = process_flags & GIT_REFERENCE_FORMAT_REFSPEC_PATTERN;
+
+		segment_len = ensure_segment_validity(current, may_contain_glob);
+		if (segment_len < 0)
+			goto cleanup;
 
 		if (segment_len > 0) {
+			/*
+			 * There may only be one glob in a pattern, thus we reset
+			 * the pattern-flag in case the current segment has one.
+			 */
+			if (memchr(current, '*', segment_len))
+				process_flags &= ~GIT_REFERENCE_FORMAT_REFSPEC_PATTERN;
+
 			if (normalize) {
 				size_t cur_len = git_buf_len(buf);
 
diff --git a/tests/network/refspecs.c b/tests/network/refspecs.c
index b16aac9..9df0a76 100644
--- a/tests/network/refspecs.c
+++ b/tests/network/refspecs.c
@@ -70,15 +70,18 @@ void test_network_refspecs__parsing(void)
 	assert_refspec(GIT_DIRECTION_PUSH, ":refs/remotes/frotz/delete me", false);
 	assert_refspec(GIT_DIRECTION_FETCH, ":refs/remotes/frotz/HEAD to me", false);
 
-	assert_refspec(GIT_DIRECTION_FETCH, "refs/heads/*/for-linus:refs/remotes/mine/*-blah", false);
-	assert_refspec(GIT_DIRECTION_PUSH, "refs/heads/*/for-linus:refs/remotes/mine/*-blah", false);
+	assert_refspec(GIT_DIRECTION_FETCH, "refs/heads/*/for-linus:refs/remotes/mine/*-blah", true);
+	assert_refspec(GIT_DIRECTION_PUSH, "refs/heads/*/for-linus:refs/remotes/mine/*-blah", true);
 
-	assert_refspec(GIT_DIRECTION_FETCH, "refs/heads*/for-linus:refs/remotes/mine/*", false);
-	assert_refspec(GIT_DIRECTION_PUSH, "refs/heads*/for-linus:refs/remotes/mine/*", false);
+	assert_refspec(GIT_DIRECTION_FETCH, "refs/heads*/for-linus:refs/remotes/mine/*", true);
+	assert_refspec(GIT_DIRECTION_PUSH, "refs/heads*/for-linus:refs/remotes/mine/*", true);
 
 	assert_refspec(GIT_DIRECTION_FETCH, "refs/heads/*/*/for-linus:refs/remotes/mine/*", false);
 	assert_refspec(GIT_DIRECTION_PUSH, "refs/heads/*/*/for-linus:refs/remotes/mine/*", false);
 
+	assert_refspec(GIT_DIRECTION_FETCH, "refs/heads/*g*/for-linus:refs/remotes/mine/*", false);
+	assert_refspec(GIT_DIRECTION_PUSH, "refs/heads/*g*/for-linus:refs/remotes/mine/*", false);
+
 	assert_refspec(GIT_DIRECTION_FETCH, "refs/heads/*/for-linus:refs/remotes/mine/*", true);
 	assert_refspec(GIT_DIRECTION_PUSH, "refs/heads/*/for-linus:refs/remotes/mine/*", true);
 
@@ -111,6 +114,12 @@ void test_network_refspecs__transform_mid_star(void)
 	assert_valid_transform("refs/*:refs/*", "refs/heads/master", "refs/heads/master");
 }
 
+void test_network_refspecs__transform_loosened_star(void)
+{
+	assert_valid_transform("refs/heads/branch-*:refs/remotes/origin/branch-*", "refs/heads/branch-a", "refs/remotes/origin/branch-a");
+	assert_valid_transform("refs/heads/branch-*/head:refs/remotes/origin/branch-*/head", "refs/heads/branch-a/head", "refs/remotes/origin/branch-a/head");
+}
+
 void test_network_refspecs__no_dst(void)
 {
 	assert_valid_transform("refs/heads/master:", "refs/heads/master", "");
diff --git a/tests/refs/normalize.c b/tests/refs/normalize.c
index 6da005d..ff81500 100644
--- a/tests/refs/normalize.c
+++ b/tests/refs/normalize.c
@@ -352,12 +352,12 @@ void test_refs_normalize__buffer_has_to_be_big_enough_to_hold_the_normalized_ver
 
 void test_refs_normalize__refspec_pattern(void)
 {
-	ensure_refname_invalid(
-		GIT_REFERENCE_FORMAT_REFSPEC_PATTERN, "heads/*foo/bar");
-	ensure_refname_invalid(
-		GIT_REFERENCE_FORMAT_REFSPEC_PATTERN, "heads/foo*/bar");
-	ensure_refname_invalid(
-		GIT_REFERENCE_FORMAT_REFSPEC_PATTERN, "heads/f*o/bar");
+	ensure_refname_normalized(
+		GIT_REFERENCE_FORMAT_REFSPEC_PATTERN, "heads/*foo/bar", "heads/*foo/bar");
+	ensure_refname_normalized(
+		GIT_REFERENCE_FORMAT_REFSPEC_PATTERN, "heads/foo*/bar", "heads/foo*/bar");
+	ensure_refname_normalized(
+		GIT_REFERENCE_FORMAT_REFSPEC_PATTERN, "heads/f*o/bar", "heads/f*o/bar");
 
 	ensure_refname_invalid(
 		GIT_REFERENCE_FORMAT_REFSPEC_PATTERN, "foo");