diff options
author | Vicent Marti <vicent@github.com> | 2014-05-22 02:28:42 +0200 |
---|---|---|
committer | Vicent Marti <vicent@github.com> | 2014-05-22 02:28:42 +0200 |
commit | a598264463442da0013d6e8b6a0c93d075e14d3a (patch) | |
tree | 0e92ceda6fb9168525181ce01ed2c70a20429e69 | |
parent | 7ee233a9203d09a08e269788ca4877778bc0c489 (diff) | |
parent | 2a597116583696486141261a8b459319b513facf (diff) | |
download | libgit2-a598264463442da0013d6e8b6a0c93d075e14d3a.tar.gz |
Merge pull request #2376 from libgit2/cmn/remote-symref
Add support for the symref extension
-rw-r--r-- | include/git2/net.h | 5 | ||||
-rw-r--r-- | include/git2/remote.h | 18 | ||||
-rw-r--r-- | src/clone.c | 113 | ||||
-rw-r--r-- | src/remote.c | 47 | ||||
-rw-r--r-- | src/transports/local.c | 22 | ||||
-rw-r--r-- | src/transports/smart.c | 45 | ||||
-rw-r--r-- | src/transports/smart.h | 5 | ||||
-rw-r--r-- | src/transports/smart_pkt.c | 1 | ||||
-rw-r--r-- | src/transports/smart_protocol.c | 58 | ||||
-rw-r--r-- | tests/network/remote/defaultbranch.c | 50 | ||||
-rw-r--r-- | tests/online/fetch.c | 18 |
11 files changed, 284 insertions, 98 deletions
diff --git a/include/git2/net.h b/include/git2/net.h index e70ba1f71..a727696a2 100644 --- a/include/git2/net.h +++ b/include/git2/net.h @@ -41,6 +41,11 @@ struct git_remote_head { git_oid oid; git_oid loid; char *name; + /** + * If the server send a symref mapping for this ref, this will + * point to the target. + */ + char *symref_target; }; /** diff --git a/include/git2/remote.h b/include/git2/remote.h index 07cd2e7c6..28771ac42 100644 --- a/include/git2/remote.h +++ b/include/git2/remote.h @@ -623,6 +623,24 @@ GIT_EXTERN(int) git_remote_is_valid_name(const char *remote_name); */ GIT_EXTERN(int) git_remote_delete(git_remote *remote); +/** + * Retrieve the name of the remote's default branch + * + * The default branch of a repository is the branch which HEAD points + * to. If the remote does not support reporting this information + * directly, it performs the guess as git does; that is, if there are + * multiple branches which point to the same commit, the first one is + * chosen. If the master branch is a candidate, it wins. + * + * This function must only be called after connecting. + * + * @param out the buffern in which to store the reference name + * @param remote the remote + * @return 0, GIT_ENOTFOUND if the remote does not have any references + * or none of them point to HEAD's commit, or an error message. + */ +GIT_EXTERN(int) git_remote_default_branch(git_buf *out, git_remote *remote); + /** @} */ GIT_END_DECL #endif diff --git a/src/clone.c b/src/clone.c index 9ac9eb2a1..f19771c1e 100644 --- a/src/clone.c +++ b/src/clone.c @@ -105,54 +105,6 @@ static int create_tracking_branch( git_reference_name(*branch)); } -struct head_info { - git_repository *repo; - git_oid remote_head_oid; - git_buf branchname; - const git_refspec *refspec; - bool found; -}; - -static int reference_matches_remote_head( - const char *reference_name, - void *payload) -{ - struct head_info *head_info = (struct head_info *)payload; - git_oid oid; - int error; - - /* TODO: Should we guard against references - * which name doesn't start with refs/heads/ ? - */ - - error = git_reference_name_to_id(&oid, head_info->repo, reference_name); - if (error == GIT_ENOTFOUND) { - /* If the reference doesn't exists, it obviously cannot match the - * expected oid. */ - giterr_clear(); - return 0; - } - - if (!error && !git_oid__cmp(&head_info->remote_head_oid, &oid)) { - /* Determine the local reference name from the remote tracking one */ - error = git_refspec_rtransform( - &head_info->branchname, head_info->refspec, reference_name); - - if (!error && - git_buf_len(&head_info->branchname) > 0 && - !(error = git_buf_sets( - &head_info->branchname, - git_buf_cstr(&head_info->branchname) + - strlen(GIT_REFS_HEADS_DIR)))) - { - head_info->found = true; - error = GIT_ITEROVER; - } - } - - return error; -} - static int update_head_to_new_branch( git_repository *repo, const git_oid *target, @@ -161,6 +113,10 @@ static int update_head_to_new_branch( const char *reflog_message) { git_reference *tracking_branch = NULL; + + if (!git__prefixcmp(name, GIT_REFS_HEADS_DIR)) + name += strlen(GIT_REFS_HEADS_DIR); + int error = create_tracking_branch(&tracking_branch, repo, target, name, signature, reflog_message); @@ -184,12 +140,13 @@ static int update_head_to_remote( const git_signature *signature, const char *reflog_message) { - int error = 0; + int error = 0, found_branch = 0; size_t refs_len; - git_refspec dummy_spec; + git_refspec dummy_spec, *refspec; const git_remote_head *remote_head, **refs; - struct head_info head_info; + const git_oid *remote_head_id; git_buf remote_master_name = GIT_BUF_INIT; + git_buf branch = GIT_BUF_INIT; if ((error = git_remote_ls(&refs, &refs_len, remote)) < 0) return error; @@ -199,63 +156,45 @@ static int update_head_to_remote( return setup_tracking_config( repo, "master", GIT_REMOTE_ORIGIN, GIT_REFS_HEADS_MASTER_FILE); + error = git_remote_default_branch(&branch, remote); + if (error == GIT_ENOTFOUND) { + git_buf_puts(&branch, GIT_REFS_HEADS_MASTER_FILE); + } else { + found_branch = 1; + } + /* Get the remote's HEAD. This is always the first ref in the list. */ remote_head = refs[0]; assert(remote_head); - memset(&head_info, 0, sizeof(head_info)); - git_oid_cpy(&head_info.remote_head_oid, &remote_head->oid); - head_info.repo = repo; - head_info.refspec = - git_remote__matching_refspec(remote, GIT_REFS_HEADS_MASTER_FILE); + remote_head_id = &remote_head->oid; + refspec = git_remote__matching_refspec(remote, git_buf_cstr(&branch)); - if (head_info.refspec == NULL) { + if (refspec == NULL) { memset(&dummy_spec, 0, sizeof(git_refspec)); - head_info.refspec = &dummy_spec; + refspec = &dummy_spec; } /* Determine the remote tracking reference name from the local master */ if ((error = git_refspec_transform( &remote_master_name, - head_info.refspec, - GIT_REFS_HEADS_MASTER_FILE)) < 0) + refspec, + git_buf_cstr(&branch))) < 0) return error; - /* Check to see if the remote HEAD points to the remote master */ - error = reference_matches_remote_head( - git_buf_cstr(&remote_master_name), &head_info); - if (error < 0 && error != GIT_ITEROVER) - goto cleanup; - - if (head_info.found) { + if (found_branch) { error = update_head_to_new_branch( repo, - &head_info.remote_head_oid, - git_buf_cstr(&head_info.branchname), - signature, reflog_message); - goto cleanup; - } - - /* Not master. Check all the other refs. */ - error = git_reference_foreach_name( - repo, reference_matches_remote_head, &head_info); - if (error < 0 && error != GIT_ITEROVER) - goto cleanup; - - if (head_info.found) { - error = update_head_to_new_branch( - repo, - &head_info.remote_head_oid, - git_buf_cstr(&head_info.branchname), + remote_head_id, + git_buf_cstr(&branch), signature, reflog_message); } else { error = git_repository_set_head_detached( - repo, &head_info.remote_head_oid, signature, reflog_message); + repo, remote_head_id, signature, reflog_message); } -cleanup: git_buf_free(&remote_master_name); - git_buf_free(&head_info.branchname); + git_buf_free(&branch); return error; } diff --git a/src/remote.c b/src/remote.c index bdc4791a9..f2e2e2f7a 100644 --- a/src/remote.c +++ b/src/remote.c @@ -1885,3 +1885,50 @@ int git_remote_delete(git_remote *remote) return 0; } + +int git_remote_default_branch(git_buf *out, git_remote *remote) +{ + const git_remote_head **heads; + const git_remote_head *guess = NULL; + const git_oid *head_id; + size_t heads_len, i; + int error; + + if ((error = git_remote_ls(&heads, &heads_len, remote)) < 0) + return error; + + if (heads_len == 0) + return GIT_ENOTFOUND; + + git_buf_sanitize(out); + /* the first one must be HEAD so if that has the symref info, we're done */ + if (heads[0]->symref_target) + return git_buf_puts(out, heads[0]->symref_target); + + /* + * If there's no symref information, we have to look over them + * and guess. We return the first match unless the master + * branch is a candidate. Then we return the master branch. + */ + head_id = &heads[0]->oid; + + for (i = 1; i < heads_len; i++) { + if (git_oid_cmp(head_id, &heads[i]->oid)) + continue; + + if (!guess) { + guess = heads[i]; + continue; + } + + if (!git__strcmp(GIT_REFS_HEADS_MASTER_FILE, heads[i]->name)) { + guess = heads[i]; + break; + } + } + + if (!guess) + return GIT_ENOTFOUND; + + return git_buf_puts(out, guess->name); +} diff --git a/src/transports/local.c b/src/transports/local.c index 2c17e6271..8d3619388 100644 --- a/src/transports/local.c +++ b/src/transports/local.c @@ -43,14 +43,19 @@ typedef struct { static int add_ref(transport_local *t, const char *name) { const char peeled[] = "^{}"; - git_oid head_oid; + git_reference *ref, *resolved; git_remote_head *head; + git_oid obj_id; git_object *obj = NULL, *target = NULL; git_buf buf = GIT_BUF_INIT; int error; - error = git_reference_name_to_id(&head_oid, t->repo, name); + if ((error = git_reference_lookup(&ref, t->repo, name)) < 0) + return error; + + error = git_reference_resolve(&resolved, ref); if (error < 0) { + git_reference_free(ref); if (!strcmp(name, GIT_HEAD_FILE) && error == GIT_ENOTFOUND) { /* This is actually okay. Empty repos often have a HEAD that * points to a nonexistent "refs/heads/master". */ @@ -60,13 +65,22 @@ static int add_ref(transport_local *t, const char *name) return error; } + git_oid_cpy(&obj_id, git_reference_target(resolved)); + git_reference_free(resolved); + head = git__calloc(1, sizeof(git_remote_head)); GITERR_CHECK_ALLOC(head); head->name = git__strdup(name); GITERR_CHECK_ALLOC(head->name); - git_oid_cpy(&head->oid, &head_oid); + git_oid_cpy(&head->oid, &obj_id); + + if (git_reference_type(ref) == GIT_REF_SYMBOLIC) { + head->symref_target = git__strdup(git_reference_symbolic_target(ref)); + GITERR_CHECK_ALLOC(head->symref_target); + } + git_reference_free(ref); if ((error = git_vector_insert(&t->refs, head)) < 0) { git__free(head->name); @@ -176,7 +190,7 @@ static int path_from_url_or_path(git_buf *local_path_out, const char *url_or_pat /* * Try to open the url as a git directory. The direction doesn't - * matter in this case because we're calulating the heads ourselves. + * matter in this case because we're calculating the heads ourselves. */ static int local_connect( git_transport *transport, diff --git a/src/transports/smart.c b/src/transports/smart.c index 7fb41f788..c9845bff9 100644 --- a/src/transports/smart.c +++ b/src/transports/smart.c @@ -7,6 +7,7 @@ #include "git2.h" #include "smart.h" #include "refs.h" +#include "refspec.h" static int git_smart__recv_cb(gitno_buffer *buf) { @@ -63,7 +64,7 @@ static int git_smart__set_callbacks( return 0; } -int git_smart__update_heads(transport_smart *t) +int git_smart__update_heads(transport_smart *t, git_vector *symrefs) { size_t i; git_pkt *pkt; @@ -74,6 +75,25 @@ int git_smart__update_heads(transport_smart *t) if (pkt->type != GIT_PKT_REF) continue; + if (symrefs) { + git_refspec *spec; + git_buf buf = GIT_BUF_INIT; + size_t j; + int error; + + git_vector_foreach(symrefs, j, spec) { + git_buf_clear(&buf); + if (git_refspec_src_matches(spec, ref->head.name) && + !(error = git_refspec_transform(&buf, spec, ref->head.name))) + ref->head.symref_target = git_buf_detach(&buf); + } + + git_buf_free(&buf); + + if (error < 0) + return error; + } + if (git_vector_insert(&t->heads, &ref->head) < 0) return -1; } @@ -81,6 +101,19 @@ int git_smart__update_heads(transport_smart *t) return 0; } +static void free_symrefs(git_vector *symrefs) +{ + git_refspec *spec; + size_t i; + + git_vector_foreach(symrefs, i, spec) { + git_refspec__free(spec); + git__free(spec); + } + + git_vector_free(symrefs); +} + static int git_smart__connect( git_transport *transport, const char *url, @@ -94,6 +127,7 @@ static int git_smart__connect( int error; git_pkt *pkt; git_pkt_ref *first; + git_vector symrefs; git_smart_service_t service; if (git_smart__reset_stream(t, true) < 0) @@ -147,8 +181,11 @@ static int git_smart__connect( first = (git_pkt_ref *)git_vector_get(&t->refs, 0); + if ((error = git_vector_init(&symrefs, 1, NULL)) < 0) + return error; + /* Detect capabilities */ - if (git_smart__detect_caps(first, &t->caps) < 0) + if (git_smart__detect_caps(first, &t->caps, &symrefs) < 0) return -1; /* If the only ref in the list is capabilities^{} with OID_ZERO, remove it */ @@ -159,7 +196,9 @@ static int git_smart__connect( } /* Keep a list of heads for _ls */ - git_smart__update_heads(t); + git_smart__update_heads(t, &symrefs); + + free_symrefs(&symrefs); if (t->rpc && git_smart__reset_stream(t, false) < 0) return -1; diff --git a/src/transports/smart.h b/src/transports/smart.h index a2b6b2a71..f1fc29520 100644 --- a/src/transports/smart.h +++ b/src/transports/smart.h @@ -23,6 +23,7 @@ #define GIT_CAP_DELETE_REFS "delete-refs" #define GIT_CAP_REPORT_STATUS "report-status" #define GIT_CAP_THIN_PACK "thin-pack" +#define GIT_CAP_SYMREF "symref" enum git_pkt_type { GIT_PKT_CMD, @@ -154,7 +155,7 @@ typedef struct { /* smart_protocol.c */ int git_smart__store_refs(transport_smart *t, int flushes); -int git_smart__detect_caps(git_pkt_ref *pkt, transport_smart_caps *caps); +int git_smart__detect_caps(git_pkt_ref *pkt, transport_smart_caps *caps, git_vector *symrefs); int git_smart__push(git_transport *transport, git_push *push); int git_smart__negotiate_fetch( @@ -174,7 +175,7 @@ int git_smart__download_pack( int git_smart__negotiation_step(git_transport *transport, void *data, size_t len); int git_smart__get_push_stream(transport_smart *t, git_smart_subtransport_stream **out); -int git_smart__update_heads(transport_smart *t); +int git_smart__update_heads(transport_smart *t, git_vector *symrefs); /* smart_pkt.c */ int git_pkt_parse_line(git_pkt **head, const char *line, const char **out, size_t len); diff --git a/src/transports/smart_pkt.c b/src/transports/smart_pkt.c index e9376ae6f..b5f9d6dbe 100644 --- a/src/transports/smart_pkt.c +++ b/src/transports/smart_pkt.c @@ -433,6 +433,7 @@ void git_pkt_free(git_pkt *pkt) if (pkt->type == GIT_PKT_REF) { git_pkt_ref *p = (git_pkt_ref *) pkt; git__free(p->head.name); + git__free(p->head.symref_target); } if (pkt->type == GIT_PKT_OK) { diff --git a/src/transports/smart_protocol.c b/src/transports/smart_protocol.c index 5dd6bab24..bab0cf113 100644 --- a/src/transports/smart_protocol.c +++ b/src/transports/smart_protocol.c @@ -78,7 +78,52 @@ int git_smart__store_refs(transport_smart *t, int flushes) return flush; } -int git_smart__detect_caps(git_pkt_ref *pkt, transport_smart_caps *caps) +static int append_symref(const char **out, git_vector *symrefs, const char *ptr) +{ + int error; + const char *end; + git_buf buf = GIT_BUF_INIT; + git_refspec *mapping; + + ptr += strlen(GIT_CAP_SYMREF); + if (*ptr != '=') + goto on_invalid; + + ptr++; + if (!(end = strchr(ptr, ' ')) && + !(end = strchr(ptr, '\0'))) + goto on_invalid; + + if ((error = git_buf_put(&buf, ptr, end - ptr)) < 0) + return error; + + /* symref mapping has refspec format */ + mapping = git__malloc(sizeof(git_refspec)); + GITERR_CHECK_ALLOC(mapping); + + error = git_refspec__parse(mapping, git_buf_cstr(&buf), true); + git_buf_free(&buf); + + /* if the error isn't OOM, then it's a parse error; let's use a nicer message */ + if (error < 0) { + if (giterr_last()->klass != GITERR_NOMEMORY) + goto on_invalid; + + return error; + } + + if ((error = git_vector_insert(symrefs, mapping)) < 0) + return error; + + *out = end; + return 0; + +on_invalid: + giterr_set(GITERR_NET, "remote sent invalid symref"); + return -1; +} + +int git_smart__detect_caps(git_pkt_ref *pkt, transport_smart_caps *caps, git_vector *symrefs) { const char *ptr; @@ -141,6 +186,15 @@ int git_smart__detect_caps(git_pkt_ref *pkt, transport_smart_caps *caps) continue; } + if (!git__prefixcmp(ptr, GIT_CAP_SYMREF)) { + int error; + + if ((error = append_symref(&ptr, symrefs, ptr)) < 0) + return error; + + continue; + } + /* We don't know this capability, so skip it */ ptr = strchr(ptr, ' '); } @@ -969,7 +1023,7 @@ int git_smart__push(git_transport *transport, git_push *push) if (error < 0) goto done; - error = git_smart__update_heads(t); + error = git_smart__update_heads(t, NULL); } done: diff --git a/tests/network/remote/defaultbranch.c b/tests/network/remote/defaultbranch.c new file mode 100644 index 000000000..fa3a329db --- /dev/null +++ b/tests/network/remote/defaultbranch.c @@ -0,0 +1,50 @@ +#include "clar_libgit2.h" +#include "buffer.h" +#include "refspec.h" +#include "remote.h" + +static git_remote *g_remote; +static git_repository *g_repo_a, *g_repo_b; + +void test_network_remote_defaultbranch__initialize(void) +{ + g_repo_a = cl_git_sandbox_init("testrepo.git"); + cl_git_pass(git_repository_init(&g_repo_b, "repo-b.git", true)); + cl_git_pass(git_remote_create(&g_remote, g_repo_b, "origin", git_repository_path(g_repo_a))); +} + +void test_network_remote_defaultbranch__cleanup(void) +{ + git_remote_free(g_remote); + git_repository_free(g_repo_b); + + cl_git_sandbox_cleanup(); + cl_fixture_cleanup("repo-b.git"); +} + +static void assert_default_branch(const char *should) +{ + git_buf name = GIT_BUF_INIT; + + cl_git_pass(git_remote_connect(g_remote, GIT_DIRECTION_FETCH)); + cl_git_pass(git_remote_default_branch(&name, g_remote)); + cl_assert_equal_s(should, name.ptr); + git_buf_free(&name); +} + +void test_network_remote_defaultbranch__master(void) +{ + assert_default_branch("refs/heads/master"); +} + +void test_network_remote_defaultbranch__master_does_not_win(void) +{ + cl_git_pass(git_repository_set_head(g_repo_a, "refs/heads/not-good", NULL, NULL)); + assert_default_branch("refs/heads/not-good"); +} + +void test_network_remote_defaultbranch__master_on_detached(void) +{ + cl_git_pass(git_repository_detach_head(g_repo_a, NULL, NULL)); + assert_default_branch("refs/heads/master"); +} diff --git a/tests/online/fetch.c b/tests/online/fetch.c index c54ec5673..f03a6faa6 100644 --- a/tests/online/fetch.c +++ b/tests/online/fetch.c @@ -184,3 +184,21 @@ void test_online_fetch__ls_disconnected(void) git_remote_free(remote); } + +void test_online_fetch__remote_symrefs(void) +{ + const git_remote_head **refs; + size_t refs_len; + git_remote *remote; + + cl_git_pass(git_remote_create(&remote, _repo, "test", + "http://github.com/libgit2/TestGitRepository.git")); + cl_git_pass(git_remote_connect(remote, GIT_DIRECTION_FETCH)); + git_remote_disconnect(remote); + cl_git_pass(git_remote_ls(&refs, &refs_len, remote)); + + cl_assert_equal_s("HEAD", refs[0]->name); + cl_assert_equal_s("refs/heads/master", refs[0]->symref_target); + + git_remote_free(remote); +} |