diff --git a/include/git2/branch.h b/include/git2/branch.h index 54a1ab118..3c7fb443c 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 ee06f8eca..ec7830b7c 100644 --- a/include/git2/refspec.h +++ b/include/git2/refspec.h @@ -52,6 +52,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 * @@ -63,6 +72,17 @@ GIT_EXTERN(int) git_refspec_src_matches(const git_refspec *refspec, const char * */ GIT_EXTERN(int) git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, const char *name); +/** + * Transform a target reference to its source reference following the refspec's rules + * + * @param out where to store the source reference name + * @param outlen the size of the `out` buffer + * @param spec the refspec + * @param name the name of the reference to transform + * @return 0, GIT_EBUFS or another error + */ +GIT_EXTERN(int) git_refspec_rtransform(char *out, size_t outlen, const git_refspec *spec, const char *name); + GIT_END_DECL #endif diff --git a/src/branch.c b/src/branch.c index 3959409c5..936947a73 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 bd69f58ae..a51b0cfab 100644 --- a/src/refspec.c +++ b/src/refspec.c @@ -159,11 +159,19 @@ int git_refspec_src_matches(const git_refspec *refspec, const char *refname) return (p_fnmatch(refspec->src, refname, 0) == 0); } -int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, const char *name) +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); +} + +static int refspec_transform_internal(char *out, size_t outlen, const char *from, const char *to, const char *name) { size_t baselen, namelen; - baselen = strlen(spec->dst); + baselen = strlen(to); if (outlen <= baselen) { giterr_set(GITERR_INVALID, "Reference name too long"); return GIT_EBUFS; @@ -173,8 +181,8 @@ int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, con * No '*' at the end means that it's mapped to one specific local * branch, so no actual transformation is needed. */ - if (spec->dst[baselen - 1] != '*') { - memcpy(out, spec->dst, baselen + 1); /* include '\0' */ + if (to[baselen - 1] != '*') { + memcpy(out, to, baselen + 1); /* include '\0' */ return 0; } @@ -182,7 +190,7 @@ int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, con baselen--; /* skip the prefix, -1 is for the '*' */ - name += strlen(spec->src) - 1; + name += strlen(from) - 1; namelen = strlen(name); @@ -191,12 +199,22 @@ int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, con return GIT_EBUFS; } - memcpy(out, spec->dst, baselen); + memcpy(out, to, baselen); memcpy(out + baselen, name, namelen + 1); return 0; } +int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, const char *name) +{ + return refspec_transform_internal(out, outlen, spec->src, spec->dst, name); +} + +int git_refspec_rtransform(char *out, size_t outlen, const git_refspec *spec, const char *name) +{ + return refspec_transform_internal(out, outlen, spec->dst, spec->src, name); +} + static int refspec_transform(git_buf *out, const char *from, const char *to, const char *name) { if (git_buf_sets(out, to) < 0) diff --git a/tests-clar/network/remotes.c b/tests-clar/network/remotes.c index b138d8c10..51d6c946f 100644 --- a/tests-clar/network/remotes.c +++ b/tests-clar/network/remotes.c @@ -173,13 +173,20 @@ void test_network_remotes__fnmatch(void) void test_network_remotes__transform(void) { - char ref[1024]; + char ref[1024] = {0}; - memset(ref, 0x0, sizeof(ref)); cl_git_pass(git_refspec_transform(ref, sizeof(ref), _refspec, "refs/heads/master")); cl_assert_equal_s(ref, "refs/remotes/test/master"); } +void test_network_remotes__transform_destination_to_source(void) +{ + char ref[1024] = {0}; + + cl_git_pass(git_refspec_rtransform(ref, sizeof(ref), _refspec, "refs/remotes/test/master")); + cl_assert_equal_s(ref, "refs/heads/master"); +} + void test_network_remotes__transform_r(void) { git_buf buf = GIT_BUF_INIT; diff --git a/tests-clar/refs/branches/remote.c b/tests-clar/refs/branches/remote.c new file mode 100644 index 000000000..be355af46 --- /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); +}