Commit c030ada7ff7f9c93a2287ca2f57173d66fbff88a

nulltoken 2012-09-11T12:06:57

refs: make git_reference_normalize_name() accept refspec pattern

diff --git a/include/git2/refs.h b/include/git2/refs.h
index 73b32a9..acca692 100644
--- a/include/git2/refs.h
+++ b/include/git2/refs.h
@@ -414,8 +414,6 @@ enum {
  * Once normalized, if the reference name is valid, it will be
  * returned in the user allocated buffer.
  *
- * TODO: Implement handling of GIT_REF_FORMAT_REFSPEC_PATTERN
- *
  * @param buffer_out The user allocated buffer where the
  * normalized name will be stored.
  *
diff --git a/src/refs.c b/src/refs.c
index 74c40e8..ef8300a 100644
--- a/src/refs.c
+++ b/src/refs.c
@@ -1098,7 +1098,7 @@ int git_reference_lookup_resolved(
 	scan->name = git__calloc(GIT_REFNAME_MAX + 1, sizeof(char));
 	GITERR_CHECK_ALLOC(scan->name);
 
-	if ((result = git_reference__normalize_name(
+	if ((result = git_reference__normalize_name_lax(
 		scan->name,
 		GIT_REFNAME_MAX,
 		name)) < 0) {
@@ -1200,7 +1200,7 @@ int git_reference_create_symbolic(
 	char normalized[GIT_REFNAME_MAX];
 	git_reference *ref = NULL;
 
-	if (git_reference__normalize_name(
+	if (git_reference__normalize_name_lax(
 		normalized,
 		sizeof(normalized),
 		name) < 0)
@@ -1322,7 +1322,7 @@ int git_reference_set_target(git_reference *ref, const char *target)
 		return -1;
 	}
 
-	if (git_reference__normalize_name(
+	if (git_reference__normalize_name_lax(
 		normalized,
 		sizeof(normalized),
 		target))
@@ -1584,106 +1584,148 @@ static int is_valid_ref_char(char ch)
 	}
 }
 
-int git_reference_normalize_name(
-	char *buffer_out,
-	size_t buffer_size,
-	const char *name,
-	unsigned int flags)
+static int ensure_segment_validity(const char *name)
 {
-	const char *name_end, *buffer_out_start;
-	const char *current;
-	int contains_a_slash = 0;
+	const char *current = name;
+	char prev = '\0';
 
-	assert(name && buffer_out);
+	if (*current == '.')
+		return -1; /* Refname starts with "." */
 
-	if (flags & GIT_REF_FORMAT_REFSPEC_PATTERN) {
-		giterr_set(GITERR_INVALID, "Unimplemented");
-		return -1;
-	}
+	for (current = name; ; current++) {
+		if (*current == '\0' || *current == '/')
+			break;
 
-	buffer_out_start = buffer_out;
-	current = name;
-	name_end = name + strlen(name);
+		if (!is_valid_ref_char(*current))
+			return -1; /* Illegal character in refname */
 
-	/* Terminating null byte */
-	buffer_size--;
+		if (prev == '.' && *current == '.')
+			return -1; /* Refname contains ".." */
 
-	/* A refname can not be empty */
-	if (name_end == name)
-		goto invalid_name;
+		if (prev == '@' && *current == '{')
+			return -1; /* Refname contains "@{" */
 
-	/* A refname can not end with a dot or a slash */
-	if (*(name_end - 1) == '.' || *(name_end - 1) == '/')
-		goto invalid_name;
+		prev = *current;
+	}
 
-	while (current < name_end && buffer_size > 0) {
-		if (!is_valid_ref_char(*current))
-			goto invalid_name;
+	return current - name;
+}
+
+int git_reference__normalize_name(
+	git_buf *buf,
+	const char *name,
+	unsigned int flags)
+{
+	// Inspired from https://github.com/git/git/blob/f06d47e7e0d9db709ee204ed13a8a7486149f494/refs.c#L36-100
 
-		if (buffer_out > buffer_out_start) {
-			char prev = *(buffer_out - 1);
+	char *current;
+	int segment_len, segments_count = 0, error = -1;
+	
+	assert(name && buf);
+
+	current = (char *)name;
+
+	git_buf_clear(buf);
+
+	while (true) {
+		segment_len = ensure_segment_validity(current);
+		if (segment_len < 0) {
+			if ((flags & GIT_REF_FORMAT_REFSPEC_PATTERN) &&
+					current[0] == '*' &&
+					(current[1] == '\0' || current[1] == '/')) {
+				/* Accept one wildcard as a full refname component. */
+				flags &= ~GIT_REF_FORMAT_REFSPEC_PATTERN;
+				segment_len = 1;
+			} else
+				goto cleanup;
+		}
 
-			/* A refname can not start with a dot nor contain a double dot */
-			if (*current == '.' && ((prev == '.') || (prev == '/')))
-				goto invalid_name;
+		if (segment_len > 0) {
+			int cur_len = git_buf_len(buf);
 
-			/* '@{' is forbidden within a refname */
-			if (*current == '{' && prev == '@')
-				goto invalid_name;
+			git_buf_joinpath(buf, git_buf_cstr(buf), current);
+			git_buf_truncate(buf, 
+				cur_len + segment_len + (segments_count ? 1 : 0));
 
-			/* Prevent multiple slashes from being added to the output */
-			if (*current == '/' && prev == '/') {
-				current++;
-				continue;
-			}
-		}
+			segments_count++;
 
-		if (*current == '/') {
-			if (buffer_out > buffer_out_start)
-				contains_a_slash = 1;
-			else {
-				current++;
-				continue;
-			}
+			if (git_buf_oom(buf))
+				goto cleanup;
 		}
 
-		*buffer_out++ = *current++;
-		buffer_size--;
-	}
+		if (current[segment_len] == '\0')
+			break;
 
-	if (current < name_end) {
-		giterr_set(
-		GITERR_REFERENCE,
-		"The provided buffer is too short to hold the normalization of '%s'", name);
-		return GIT_EBUFS;
+		current += segment_len + 1;
 	}
 
+	/* A refname can not be empty */
+	if (git_buf_len(buf) == 0)
+		goto cleanup;
+
+	/* A refname can not end with "." */
+	if (current[segment_len - 1] == '.')
+		goto cleanup;
+
+	/* A refname can not end with "/" */
+	if (current[segment_len - 1] == '/')
+		goto cleanup;
+
+	/* A refname can not end with ".lock" */
+	if (!git__suffixcmp(name, GIT_FILELOCK_EXTENSION))
+		goto cleanup;
+
 	/* Object id refname have to contain at least one slash, except
 	 * for HEAD in a detached state or MERGE_HEAD if we're in the
 	 * middle of a merge */
-	if (!(flags & GIT_REF_FORMAT_ALLOW_ONELEVEL) &&
-		!contains_a_slash &&
+	if (!(flags & GIT_REF_FORMAT_ALLOW_ONELEVEL) && 
+		segments_count < 2 &&
 		strcmp(name, GIT_HEAD_FILE) != 0 &&
 		strcmp(name, GIT_MERGE_HEAD_FILE) != 0 &&
 		strcmp(name, GIT_FETCH_HEAD_FILE) != 0)
-		goto invalid_name;
+		return -1;
 
-	/* A refname can not end with ".lock" */
-	if (!git__suffixcmp(name, GIT_FILELOCK_EXTENSION))
-		goto invalid_name;
+	error = 0;
 
-	*buffer_out = '\0';
+cleanup:
+	if (error)
+		giterr_set(
+			GITERR_REFERENCE,
+			"The given reference name '%s' is not valid", name);
 
-	return 0;
+	return error;
+}
 
-invalid_name:
-	giterr_set(
+int git_reference_normalize_name(
+	char *buffer_out,
+	size_t buffer_size,
+	const char *name,
+	unsigned int flags)
+{
+	git_buf buf = GIT_BUF_INIT;
+	int error;
+
+	if ((error = git_reference__normalize_name(&buf, name, flags)) < 0)
+		goto cleanup;
+
+	if (git_buf_len(&buf) > buffer_size - 1) {
+		giterr_set(
 		GITERR_REFERENCE,
-		"The given reference name '%s' is not valid", name);
-	return -1;
+		"The provided buffer is too short to hold the normalization of '%s'", name);
+		error = GIT_EBUFS;
+		goto cleanup;
+	}
+
+	git_buf_copy_cstr(buffer_out, buffer_size, &buf);
+
+	error = 0;
+
+cleanup:
+	git_buf_free(&buf);
+	return error;
 }
 
-int git_reference__normalize_name(
+int git_reference__normalize_name_lax(
 	char *buffer_out,
 	size_t out_size,
 	const char *name)
diff --git a/src/refs.h b/src/refs.h
index 0823502..0674d87 100644
--- a/src/refs.h
+++ b/src/refs.h
@@ -11,6 +11,7 @@
 #include "git2/oid.h"
 #include "git2/refs.h"
 #include "strmap.h"
+#include "buffer.h"
 
 #define GIT_REFS_DIR "refs/"
 #define GIT_REFS_HEADS_DIR GIT_REFS_DIR "heads/"
@@ -52,8 +53,9 @@ typedef struct {
 
 void git_repository__refcache_free(git_refcache *refs);
 
-int git_reference__normalize_name(char *buffer_out, size_t out_size, const char *name);
+int git_reference__normalize_name_lax(char *buffer_out, size_t out_size, const char *name);
 int git_reference__normalize_name_oid(char *buffer_out, size_t out_size, const char *name);
+int git_reference__normalize_name(git_buf *buf, const char *name, unsigned int flags);
 int git_reference__update(git_repository *repo, const git_oid *oid, const char *ref_name);
 
 /**
diff --git a/tests-clar/refs/normalize.c b/tests-clar/refs/normalize.c
index 4e80e4b..db10964 100644
--- a/tests-clar/refs/normalize.c
+++ b/tests-clar/refs/normalize.c
@@ -5,9 +5,10 @@
 #include "reflog.h"
 
 // Helpers
-static void ensure_refname_normalized(unsigned int flags,
-                                      const char *input_refname,
-                                      const char *expected_refname)
+static void ensure_refname_normalized(
+	unsigned int flags,
+	const char *input_refname,
+	const char *expected_refname)
 {
 	char buffer_out[GIT_REFNAME_MAX];
 
@@ -115,7 +116,7 @@ void test_refs_normalize__symbolic(void)
  * See https://github.com/spearce/JGit/commit/e4bf8f6957bbb29362575d641d1e77a02d906739 */
 void test_refs_normalize__jgit_suite(void)
 {
-   // tests borrowed from JGit
+	// tests borrowed from JGit
 
 /* EmptyString */
 	ensure_refname_invalid(
@@ -314,3 +315,57 @@ void test_refs_normalize__buffer_has_to_be_big_enough_to_hold_the_normalized_ver
 	cl_git_fail(git_reference_normalize_name(
 		buffer_out, 20, "//refs//heads/long///name", GIT_REF_FORMAT_NORMAL));
 }
+
+#define ONE_LEVEL_AND_REFSPEC \
+	GIT_REF_FORMAT_ALLOW_ONELEVEL \
+	| GIT_REF_FORMAT_REFSPEC_PATTERN
+
+void test_refs_normalize__refspec_pattern(void)
+{
+	ensure_refname_invalid(
+		GIT_REF_FORMAT_REFSPEC_PATTERN, "heads/*foo/bar");
+	ensure_refname_invalid(
+		GIT_REF_FORMAT_REFSPEC_PATTERN, "heads/foo*/bar");
+	ensure_refname_invalid(
+		GIT_REF_FORMAT_REFSPEC_PATTERN, "heads/f*o/bar");
+
+	ensure_refname_invalid(
+		GIT_REF_FORMAT_REFSPEC_PATTERN, "foo");
+	ensure_refname_normalized(
+		ONE_LEVEL_AND_REFSPEC, "foo", "foo");
+
+	ensure_refname_normalized(
+		GIT_REF_FORMAT_REFSPEC_PATTERN, "foo/bar", "foo/bar");
+	ensure_refname_normalized(
+		ONE_LEVEL_AND_REFSPEC, "foo/bar", "foo/bar");
+
+	ensure_refname_normalized(
+		GIT_REF_FORMAT_REFSPEC_PATTERN, "*/foo", "*/foo");
+	ensure_refname_normalized(
+		ONE_LEVEL_AND_REFSPEC, "*/foo", "*/foo");
+
+	ensure_refname_normalized(
+		GIT_REF_FORMAT_REFSPEC_PATTERN, "foo/*/bar", "foo/*/bar");
+	ensure_refname_normalized(
+		ONE_LEVEL_AND_REFSPEC, "foo/*/bar", "foo/*/bar");
+
+	ensure_refname_invalid(
+		GIT_REF_FORMAT_REFSPEC_PATTERN, "*");
+	ensure_refname_normalized(
+		ONE_LEVEL_AND_REFSPEC, "*", "*");
+
+	ensure_refname_invalid(
+		GIT_REF_FORMAT_REFSPEC_PATTERN, "foo/*/*");
+	ensure_refname_invalid(
+		ONE_LEVEL_AND_REFSPEC, "foo/*/*");
+
+	ensure_refname_invalid(
+		GIT_REF_FORMAT_REFSPEC_PATTERN, "*/foo/*");
+	ensure_refname_invalid(
+		ONE_LEVEL_AND_REFSPEC, "*/foo/*");
+
+	ensure_refname_invalid(
+		GIT_REF_FORMAT_REFSPEC_PATTERN, "*/*/foo");
+	ensure_refname_invalid(
+		ONE_LEVEL_AND_REFSPEC, "*/*/foo");
+}