diff --git a/include/git2/remote.h b/include/git2/remote.h index 3633501e2..07cd2e7c6 100644 --- a/include/git2/remote.h +++ b/include/git2/remote.h @@ -610,6 +610,19 @@ GIT_EXTERN(void) git_remote_set_update_fetchhead(git_remote *remote, int value); */ GIT_EXTERN(int) git_remote_is_valid_name(const char *remote_name); +/** +* Delete an existing persisted remote. +* +* All remote-tracking branches and configuration settings +* for the remote will be removed. +* +* once deleted, the passed remote object will be freed and invalidated. +* +* @param remote A valid remote +* @return 0 on success, or an error code. +*/ +GIT_EXTERN(int) git_remote_delete(git_remote *remote); + /** @} */ GIT_END_DECL #endif diff --git a/src/remote.c b/src/remote.c index 31ee97795..bdc4791a9 100644 --- a/src/remote.c +++ b/src/remote.c @@ -1304,13 +1304,14 @@ static int rename_remote_config_section( if (git_buf_printf(&old_section_name, "remote.%s", old_name) < 0) goto cleanup; - if (git_buf_printf(&new_section_name, "remote.%s", new_name) < 0) - goto cleanup; + if (new_name && + (git_buf_printf(&new_section_name, "remote.%s", new_name) < 0)) + goto cleanup; error = git_config_rename_section( repo, git_buf_cstr(&old_section_name), - git_buf_cstr(&new_section_name)); + new_name ? git_buf_cstr(&new_section_name) : NULL); cleanup: git_buf_free(&old_section_name); @@ -1743,3 +1744,144 @@ int git_remote_init_callbacks(git_remote_callbacks *opts, unsigned int version) opts, version, git_remote_callbacks, GIT_REMOTE_CALLBACKS_INIT); return 0; } + +/* asserts a branch..remote format */ +static const char *name_offset(size_t *len_out, const char *name) +{ + size_t prefix_len; + const char *dot; + + prefix_len = strlen("remote."); + dot = strchr(name + prefix_len, '.'); + + assert(dot); + + *len_out = dot - name - prefix_len; + return name + prefix_len; +} + +static int remove_branch_config_related_entries( + git_repository *repo, + const char *remote_name) +{ + int error; + git_config *config; + git_config_entry *entry; + git_config_iterator *iter; + git_buf buf = GIT_BUF_INIT; + + if ((error = git_repository_config__weakptr(&config, repo)) < 0) + return error; + + if ((error = git_config_iterator_glob_new(&iter, config, "branch\\..+\\.remote")) < 0) + return error; + + /* find any branches with us as upstream and remove that config */ + while ((error = git_config_next(&entry, iter)) == 0) { + const char *branch; + size_t branch_len; + + if (strcmp(remote_name, entry->value)) + continue; + + branch = name_offset(&branch_len, entry->name); + + git_buf_clear(&buf); + if (git_buf_printf(&buf, "branch.%.*s.merge", (int)branch_len, branch) < 0) + break; + + if ((error = git_config_delete_entry(config, git_buf_cstr(&buf))) < 0) + break; + + git_buf_clear(&buf); + if (git_buf_printf(&buf, "branch.%.*s.remote", (int)branch_len, branch) < 0) + break; + + if ((error = git_config_delete_entry(config, git_buf_cstr(&buf))) < 0) + break; + } + + if (error == GIT_ITEROVER) + error = 0; + + git_buf_free(&buf); + git_config_iterator_free(iter); + return error; +} + +static int remove_refs(git_repository *repo, const char *glob) +{ + git_reference_iterator *iter; + const char *name; + int error; + + if ((error = git_reference_iterator_glob_new(&iter, repo, glob)) < 0) + return error; + + while ((error = git_reference_next_name(&name, iter)) == 0) { + if ((error = git_reference_remove(repo, name)) < 0) + break; + } + git_reference_iterator_free(iter); + + if (error == GIT_ITEROVER) + error = 0; + + return error; +} + +static int remove_remote_tracking(git_repository *repo, const char *remote_name) +{ + git_remote *remote; + int error; + size_t i, count; + + /* we want to use what's on the config, regardless of changes to the instance in memory */ + if ((error = git_remote_load(&remote, repo, remote_name)) < 0) + return error; + + count = git_remote_refspec_count(remote); + for (i = 0; i < count; i++) { + const git_refspec *refspec = git_remote_get_refspec(remote, i); + + /* shouldn't ever actually happen */ + if (refspec == NULL) + continue; + + if ((error = remove_refs(repo, git_refspec_dst(refspec))) < 0) + break; + } + + git_remote_free(remote); + return error; +} + +int git_remote_delete(git_remote *remote) +{ + int error; + git_repository *repo; + + assert(remote); + + if (!remote->name) { + giterr_set(GITERR_INVALID, "Can't delete an anonymous remote."); + return -1; + } + + repo = git_remote_owner(remote); + + if ((error = remove_branch_config_related_entries(repo, + git_remote_name(remote))) < 0) + return error; + + if ((error = remove_remote_tracking(repo, git_remote_name(remote))) < 0) + return error; + + if ((error = rename_remote_config_section( + repo, git_remote_name(remote), NULL)) < 0) + return error; + + git_remote_free(remote); + + return 0; +} diff --git a/tests/config/config_helpers.c b/tests/config/config_helpers.c index 53bd945a0..35da720e0 100644 --- a/tests/config/config_helpers.c +++ b/tests/config/config_helpers.c @@ -35,3 +35,31 @@ void assert_config_entry_value( cl_assert_equal_s(expected_value, out); } + +static int count_config_entries_cb( + const git_config_entry *entry, + void *payload) +{ + int *how_many = (int *)payload; + + GIT_UNUSED(entry); + + (*how_many)++; + + return 0; +} + +int count_config_entries_match(git_repository *repo, const char *pattern) +{ + git_config *config; + int how_many = 0; + + cl_git_pass(git_repository_config(&config, repo)); + + cl_assert_equal_i(0, git_config_foreach_match( + config, pattern, count_config_entries_cb, &how_many)); + + git_config_free(config); + + return how_many; +} diff --git a/tests/config/config_helpers.h b/tests/config/config_helpers.h index b887b3d38..440645730 100644 --- a/tests/config/config_helpers.h +++ b/tests/config/config_helpers.h @@ -7,3 +7,7 @@ extern void assert_config_entry_value( git_repository *repo, const char *name, const char *expected_value); + +extern int count_config_entries_match( + git_repository *repo, + const char *pattern); diff --git a/tests/network/remote/delete.c b/tests/network/remote/delete.c new file mode 100644 index 000000000..db55b0768 --- /dev/null +++ b/tests/network/remote/delete.c @@ -0,0 +1,61 @@ +#include "clar_libgit2.h" +#include "config/config_helpers.h" + +#include "repository.h" + +static git_remote *_remote; +static git_repository *_repo; + +void test_network_remote_delete__initialize(void) +{ + _repo = cl_git_sandbox_init("testrepo.git"); + + cl_git_pass(git_remote_load(&_remote, _repo, "test")); +} + +void test_network_remote_delete__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_network_remote_delete__cannot_delete_an_anonymous_remote(void) +{ + git_remote *remote; + + cl_git_pass(git_remote_create_anonymous(&remote, _repo, "git://github.com/libgit2/libgit2", NULL)); + + cl_git_fail(git_remote_delete(remote)); + + git_remote_free(remote); + git_remote_free(_remote); +} + +void test_network_remote_delete__remove_remote_tracking_branches(void) +{ + git_reference *ref; + + cl_git_pass(git_remote_delete(_remote)); + cl_git_fail_with(GIT_ENOTFOUND, git_reference_lookup(&ref, _repo, "refs/remotes/test/master")); +} + +void test_network_remote_delete__remove_remote_configuration_settings(void) +{ + cl_assert(count_config_entries_match(_repo, "remote\\.test\\.+") > 0); + + cl_git_pass(git_remote_delete(_remote)); + + cl_assert_equal_i(0, count_config_entries_match(_repo, "remote\\.test\\.+")); +} + +void test_network_remote_delete__remove_branch_upstream_configuration_settings(void) +{ + assert_config_entry_existence(_repo, "branch.mergeless.remote", true); + assert_config_entry_existence(_repo, "branch.master.remote", true); + + cl_git_pass(git_remote_delete(_remote)); + + assert_config_entry_existence(_repo, "branch.mergeless.remote", false); + assert_config_entry_existence(_repo, "branch.mergeless.merge", false); + assert_config_entry_existence(_repo, "branch.master.remote", false); + assert_config_entry_existence(_repo, "branch.master.merge", false); +}