diff --git a/include/git2/push.h b/include/git2/push.h index 51f059ac4..6e07f368e 100644 --- a/include/git2/push.h +++ b/include/git2/push.h @@ -38,6 +38,15 @@ GIT_EXTERN(int) git_push_new(git_push **out, git_remote *remote); */ GIT_EXTERN(int) git_push_add_refspec(git_push *push, const char *refspec); +/** + * Update remote tips after a push + * + * @param push The push object + * + * @return 0 or an error code + */ +GIT_EXTERN(int) git_push_update_tips(git_push *push); + /** * Actually push all given refspecs * diff --git a/src/push.c b/src/push.c index 452ead405..ddfe5ec07 100644 --- a/src/push.c +++ b/src/push.c @@ -161,6 +161,60 @@ int git_push_add_refspec(git_push *push, const char *refspec) return 0; } +int git_push_update_tips(git_push *push) +{ + git_refspec *fetch_spec = &push->remote->fetch; + git_buf remote_ref_name = GIT_BUF_INIT; + size_t i, j; + push_spec *push_spec; + git_reference *remote_ref; + push_status *status; + int error = 0; + + git_vector_foreach(&push->status, i, status) { + /* If this ref update was successful (ok, not ng), it will have an empty message */ + if (status->msg) + continue; + + /* Find the corresponding remote ref */ + if (!git_refspec_src_matches(fetch_spec, status->ref)) + continue; + + if ((error = git_refspec_transform_r(&remote_ref_name, fetch_spec, status->ref)) < 0) + goto on_error; + + /* Find matching push ref spec */ + git_vector_foreach(&push->specs, j, push_spec) { + if (!strcmp(push_spec->rref, status->ref)) + break; + } + + /* Could not find the corresponding push ref spec for this push update */ + if (j == push->specs.length) + continue; + + /* Update the remote ref */ + if (git_oid_iszero(&push_spec->loid)) { + error = git_reference_lookup(&remote_ref, push->remote->repo, git_buf_cstr(&remote_ref_name)); + + if (!error) { + if ((error = git_reference_delete(remote_ref)) < 0) + goto on_error; + } else if (error == GIT_ENOTFOUND) + giterr_clear(); + else + goto on_error; + } else if ((error = git_reference_create(NULL, push->remote->repo, git_buf_cstr(&remote_ref_name), &push_spec->loid, 1)) < 0) + goto on_error; + } + + error = 0; + +on_error: + git_buf_free(&remote_ref_name); + return error; +} + static int revwalk(git_vector *commits, git_push *push) { git_remote_head *head; diff --git a/tests-clar/online/push.c b/tests-clar/online/push.c index 15351ae08..a065e4b78 100644 --- a/tests-clar/online/push.c +++ b/tests-clar/online/push.c @@ -4,6 +4,8 @@ #include "vector.h" #include "../submodule/submodule_helpers.h" #include "push_util.h" +#include "refspec.h" +#include "remote.h" static git_repository *_repo; @@ -127,6 +129,100 @@ static void verify_refs(git_remote *remote, expected_ref expected_refs[], size_t git_vector_free(&actual_refs); } +static int tracking_branch_list_cb(const char *branch_name, git_branch_t branch_type, void *payload) +{ + git_vector *tracking = (git_vector *)payload; + + if (branch_type == GIT_BRANCH_REMOTE) + git_vector_insert(tracking, git__strdup(branch_name)); + else + GIT_UNUSED(branch_name); + + return 0; +} + +/** + * Verifies that after git_push_update_tips(), remote tracking branches have the expected + * names and oids. + * + * @param remote remote to verify + * @param expected_refs expected remote refs after push + * @param expected_refs_len length of expected_refs + */ +static void verify_tracking_branches(git_remote *remote, expected_ref expected_refs[], size_t expected_refs_len) +{ + git_refspec *fetch_spec = &remote->fetch; + size_t i, j; + git_buf msg = GIT_BUF_INIT; + git_buf ref_name = GIT_BUF_INIT; + git_buf canonical_ref_name = GIT_BUF_INIT; + git_vector actual_refs = GIT_VECTOR_INIT; + char *actual_ref; + git_oid oid; + int failed = 0; + + /* Get current remote branches */ + cl_git_pass(git_branch_foreach(remote->repo, GIT_BRANCH_REMOTE, tracking_branch_list_cb, &actual_refs)); + + /* Loop through expected refs, make sure they exist */ + for (i = 0; i < expected_refs_len; i++) { + + /* Convert remote reference name into tracking branch name. + * If the spec is not under refs/heads/, then skip. + */ + if (!git_refspec_src_matches(fetch_spec, expected_refs[i].name)) + continue; + + cl_git_pass(git_refspec_transform_r(&ref_name, fetch_spec, expected_refs[i].name)); + + /* Find matching remote branch */ + git_vector_foreach(&actual_refs, j, actual_ref) { + + /* Construct canonical ref name from the actual_ref name */ + git_buf_clear(&canonical_ref_name); + cl_git_pass(git_buf_printf(&canonical_ref_name, "refs/remotes/%s", actual_ref)); + if (!strcmp(git_buf_cstr(&ref_name), git_buf_cstr(&canonical_ref_name))) + break; + } + + if (j == actual_refs.length) { + git_buf_printf(&msg, "Did not find expected tracking branch '%s'.", git_buf_cstr(&ref_name)); + failed = 1; + goto failed; + } + + /* Make sure tracking branch is at expected commit ID */ + cl_git_pass(git_reference_name_to_id(&oid, remote->repo, git_buf_cstr(&canonical_ref_name))); + + if (git_oid_cmp(expected_refs[i].oid, &oid) != 0) { + git_buf_puts(&msg, "Tracking branch commit does not match expected ID."); + failed = 1; + goto failed; + } + + cl_git_pass(git_vector_remove(&actual_refs, j)); + } + + /* Make sure there are no extra branches */ + if (actual_refs.length > 0) { + git_buf_puts(&msg, "Unexpected remote tracking branches exist."); + failed = 1; + goto failed; + } + +failed: + + if(failed) + cl_fail(git_buf_cstr(&msg)); + + git_vector_foreach(&actual_refs, i, actual_ref) + git__free(actual_ref); + + git_vector_free(&actual_refs); + git_buf_free(&msg); + return; +} + void test_online_push__initialize(void) { git_vector delete_specs = GIT_VECTOR_INIT; @@ -265,11 +361,12 @@ static void do_push(const char *refspecs[], size_t refspecs_len, cl_assert_equal_i(expected_ret, ret); - git_push_free(push); - verify_refs(_remote, expected_refs, expected_refs_len); - cl_git_pass(git_remote_update_tips(_remote)); + cl_git_pass(git_push_update_tips(push)); + verify_tracking_branches(_remote, expected_refs, expected_refs_len); + + git_push_free(push); git_remote_disconnect(_remote); }