Commit 2e3e8c889b5fac03ca0f3b1f1303bbdabb15f1a5

Jameson Miller 2013-02-08T11:05:47

Teach remote branch to return its remote

diff --git a/include/git2/branch.h b/include/git2/branch.h
index 54a1ab1..3c7fb44 100644
--- a/include/git2/branch.h
+++ b/include/git2/branch.h
@@ -210,6 +210,31 @@ GIT_EXTERN(int) git_branch_tracking_name(
 GIT_EXTERN(int) git_branch_is_head(
 		git_reference *branch);
 
+/**
+ * Return the name of remote that the remote tracking branch belongs to.
+ *
+ * @param remote_name_out The user-allocated buffer which will be
+ *     filled with the name of the remote. Pass NULL if you just want to
+ *     get the needed size of the name of the remote as the output value.
+ *
+ * @param buffer_size Size of the `out` buffer in bytes.
+ *
+ * @param repo The repository where the branch lives.
+ *
+ * @param branch The reference to the remote tracking branch.
+ *
+ * @return Number of characters in the reference name
+ *     including the trailing NUL byte; GIT_ENOTFOUND
+ *     when no remote matching remote was gound,
+ *     GIT_EAMBIGUOUS when the branch maps to several remotes,
+ *     otherwise an error code.
+ */
+GIT_EXTERN(int) git_branch_remote_name(
+	char *remote_name_out,
+	size_t buffer_size,
+	git_repository *repo,
+	git_reference *branch);
+
 /** @} */
 GIT_END_DECL
 #endif
diff --git a/include/git2/refspec.h b/include/git2/refspec.h
index ee06f8e..3e83e41 100644
--- a/include/git2/refspec.h
+++ b/include/git2/refspec.h
@@ -53,6 +53,15 @@ GIT_EXTERN(int) git_refspec_force(const git_refspec *refspec);
 GIT_EXTERN(int) git_refspec_src_matches(const git_refspec *refspec, const char *refname);
 
 /**
+ * Check if a refspec's destination descriptor matches a reference 
+ *
+ * @param refspec the refspec
+ * @param refname the name of the reference to check
+ * @return 1 if the refspec matches, 0 otherwise
+ */
+GIT_EXTERN(int) git_refspec_dst_matches(const git_refspec *refspec, const char *refname);
+
+/**
  * Transform a reference to its target following the refspec's rules
  *
  * @param out where to store the target name
diff --git a/src/branch.c b/src/branch.c
index 3959409..936947a 100644
--- a/src/branch.c
+++ b/src/branch.c
@@ -319,6 +319,87 @@ cleanup:
 	return error;
 }
 
+int git_branch_remote_name(
+	char *remote_name_out,
+	size_t buffer_size,
+	git_repository *repo,
+	git_reference *branch)
+{
+	git_strarray remote_list = {0};
+	size_t i, remote_name_size;
+	git_remote *remote;
+	const git_refspec *fetchspec;
+	int error = 0;
+	char *remote_name = NULL;
+
+	assert(branch);
+
+	if (remote_name_out && buffer_size)
+		*remote_name_out = '\0';
+
+	/* Verify that this is a remote branch */
+	if (!git_reference_is_remote(branch)) {
+		giterr_set(GITERR_INVALID,
+				   "Reference '%s' is not a remote branch.", branch->name);
+		error = GIT_ERROR;
+		goto cleanup;
+	}
+
+	/* Get the remotes */
+	if ((error = git_remote_list(&remote_list, repo)) < 0)
+		goto cleanup;
+
+	/* Find matching remotes */
+	for (i = 0; i < remote_list.count; i++) {
+		if ((error = git_remote_load(&remote, repo, remote_list.strings[i])) < 0)
+			goto cleanup;
+
+		fetchspec = git_remote_fetchspec(remote);
+
+		/* Defensivly check that we have a fetchspec */
+		if (fetchspec &&
+			git_refspec_dst_matches(fetchspec, branch->name)) {
+			/* If we have not already set out yet, then set
+			 * it to the matching remote name. Otherwise
+			 * multiple remotes match this reference, and it
+			 * is ambiguous. */
+			if (!remote_name) {
+				remote_name = remote_list.strings[i];
+			} else {
+				git_remote_free(remote);
+				error = GIT_EAMBIGUOUS;
+				goto cleanup;
+			}
+		}
+
+		git_remote_free(remote);
+	}
+
+	if (remote_name) {
+		remote_name_size = strlen(remote_name) + 1;
+		error = (int) remote_name_size;
+
+		if (remote_name_out) {
+			if(remote_name_size > buffer_size) {
+				giterr_set(
+					GITERR_INVALID,
+					"Buffer too short to hold the remote name.");
+				error = GIT_ERROR;
+				goto cleanup;
+			}
+
+			memcpy(remote_name_out, remote_name, remote_name_size);
+		}
+	} else {
+		error = GIT_ENOTFOUND;
+		goto cleanup;
+	}
+
+cleanup:
+	git_strarray_free(&remote_list);
+	return error;
+}
+
 int git_branch_tracking_name(
 	char *tracking_branch_name_out,
 	size_t buffer_size,
diff --git a/src/refspec.c b/src/refspec.c
index bd69f58..4f324d3 100644
--- a/src/refspec.c
+++ b/src/refspec.c
@@ -159,6 +159,14 @@ int git_refspec_src_matches(const git_refspec *refspec, const char *refname)
 	return (p_fnmatch(refspec->src, refname, 0) == 0);
 }
 
+int git_refspec_dst_matches(const git_refspec *refspec, const char *refname)
+{
+	if (refspec == NULL || refspec->dst == NULL)
+		return false;
+    
+	return (p_fnmatch(refspec->dst, refname, 0) == 0);
+}
+
 int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, const char *name)
 {
 	size_t baselen, namelen;
diff --git a/tests-clar/refs/branches/remote.c b/tests-clar/refs/branches/remote.c
new file mode 100644
index 0000000..be355af
--- /dev/null
+++ b/tests-clar/refs/branches/remote.c
@@ -0,0 +1,119 @@
+#include "clar_libgit2.h"
+#include "branch.h"
+#include "remote.h"
+
+static git_repository *g_repo;
+
+static const char *current_master_tip = "099fabac3a9ea935598528c27f866e34089c2eff";
+
+void test_refs_branches_remote__initialize(void)
+{
+	git_oid id;
+
+	g_repo = cl_git_sandbox_init("testrepo");
+	git_oid_fromstr(&id, current_master_tip);
+
+	/* Create test/master */
+	git_reference_create(NULL, g_repo, "refs/remotes/test/master", &id, 1);
+}
+
+void test_refs_branches_remote__cleanup(void)
+{
+	cl_git_sandbox_cleanup();
+}
+
+void test_refs_branches_remote__can_get_remote_for_branch(void)
+{
+	git_reference *ref;
+	const char *name;
+	char *expectedRemoteName = "test";
+	int expectedRemoteNameLength = strlen(expectedRemoteName) + 1;
+	char remotename[1024] = {0};
+
+	cl_git_pass(git_branch_lookup(&ref, g_repo, "test/master", GIT_BRANCH_REMOTE));
+	cl_git_pass(git_branch_name(&name, ref));
+	cl_assert_equal_s("test/master", name);
+
+	cl_assert_equal_i(expectedRemoteNameLength,
+		git_branch_remote_name(NULL, 0, g_repo, ref));
+	cl_assert_equal_i(expectedRemoteNameLength,
+		git_branch_remote_name(remotename, expectedRemoteNameLength, g_repo, ref));
+	cl_assert_equal_s("test", remotename);
+
+	git_reference_free(ref);
+}
+
+void test_refs_branches_remote__insufficient_buffer_returns_error(void)
+{
+	git_reference *ref;
+	const char *name;
+	char *expectedRemoteName = "test";
+	int expectedRemoteNameLength = strlen(expectedRemoteName) + 1;
+	char remotename[1024] = {0};
+
+	cl_git_pass(git_branch_lookup(&ref, g_repo, "test/master", GIT_BRANCH_REMOTE));
+	cl_git_pass(git_branch_name(&name, ref));
+	cl_assert_equal_s("test/master", name);
+
+	cl_assert_equal_i(expectedRemoteNameLength,
+		git_branch_remote_name(NULL, 0, g_repo, ref));
+	cl_git_fail_with(GIT_ERROR,
+		git_branch_remote_name(remotename, expectedRemoteNameLength - 1, g_repo, ref));
+
+	git_reference_free(ref);
+}
+
+void test_refs_branches_remote__no_matching_remote_returns_error(void)
+{
+	git_reference *ref;
+	const char *name;
+	git_oid id;
+
+	git_oid_fromstr(&id, current_master_tip);
+
+	/* Create nonexistent/master */
+	git_reference_create(NULL, g_repo, "refs/remotes/nonexistent/master", &id, 1);
+
+	cl_git_pass(git_branch_lookup(&ref, g_repo,"nonexistent/master", GIT_BRANCH_REMOTE));
+	cl_git_pass(git_branch_name(&name, ref));
+	cl_assert_equal_s("nonexistent/master", name);
+
+	cl_git_fail_with(git_branch_remote_name(NULL, 0, g_repo, ref), GIT_ENOTFOUND);
+	git_reference_free(ref);
+}
+
+void test_refs_branches_remote__local_remote_returns_error(void)
+{
+	git_reference *ref;
+	const char *name;
+
+	cl_git_pass(git_branch_lookup(&ref,g_repo, "master", GIT_BRANCH_LOCAL));
+	cl_git_pass(git_branch_name(&name, ref));
+	cl_assert_equal_s("master",name);
+
+	cl_git_fail_with(git_branch_remote_name(NULL, 0, g_repo, ref), GIT_ERROR);
+	git_reference_free(ref);
+}
+
+void test_refs_branches_remote__ambiguous_remote_returns_error(void)
+{
+	git_reference *ref;
+	const char *name;
+	git_remote *remote;
+
+	/* Create the remote */
+	cl_git_pass(git_remote_create(&remote, g_repo, "addtest", "http://github.com/libgit2/libgit2"));
+
+	/* Update the remote fetch spec */
+	cl_git_pass(git_remote_set_fetchspec(remote, "refs/heads/*:refs/remotes/test/*"));
+	cl_git_pass(git_remote_save(remote));
+
+	git_remote_free(remote);
+
+	cl_git_pass(git_branch_lookup(&ref,g_repo, "test/master", GIT_BRANCH_REMOTE));
+	cl_git_pass(git_branch_name(&name, ref));
+	cl_assert_equal_s("test/master", name);
+
+	cl_git_fail_with(git_branch_remote_name(NULL, 0, g_repo, ref), GIT_EAMBIGUOUS);
+	git_reference_free(ref);
+}