diff --git a/include/git2/branch.h b/include/git2/branch.h index e2432bcfc..f2239a207 100644 --- a/include/git2/branch.h +++ b/include/git2/branch.h @@ -95,6 +95,31 @@ GIT_EXTERN(int) git_branch_list( git_repository *repo, unsigned int list_flags); +/** + * Loop over all the branches and issue a callback for each one. + * + * @param repo Repository where to find the branches. + * + * @param list_flags Filtering flags for the branch + * listing. Valid values are GIT_BRANCH_LOCAL, GIT_BRANCH_REMOTE + * or a combination of the two. + * + * @param branch_cb Callback to invoke per found branch. + * + * @param payload Extra parameter to callback function. + * + * @return 0 or an error code. + */ +GIT_EXTERN(int) git_branch_foreach( + git_repository *repo, + unsigned int list_flags, + int (*branch_cb)( + const char *branch_name, + git_branch_t branch_type, + void *payload), + void *payload +); + /** * Move/rename an existing branch reference. * diff --git a/src/branch.c b/src/branch.c index 8b97a8206..94d9a955e 100644 --- a/src/branch.c +++ b/src/branch.c @@ -183,6 +183,49 @@ int git_branch_list(git_strarray *branch_names, git_repository *repo, unsigned i return 0; } +typedef struct { + int (*branch_cb)( + const char *branch_name, + git_branch_t branch_type, + void *payload); + void *callback_payload; + unsigned int branch_type; +} branch_foreach_filter; + +static int branch_foreach_cb(const char *branch_name, void *payload) +{ + branch_foreach_filter *filter = (branch_foreach_filter *)payload; + + if (filter->branch_type & GIT_BRANCH_LOCAL && + git__prefixcmp(branch_name, GIT_REFS_HEADS_DIR) == 0) + return filter->branch_cb(branch_name + strlen(GIT_REFS_HEADS_DIR), GIT_BRANCH_LOCAL, filter->callback_payload); + + if (filter->branch_type & GIT_BRANCH_REMOTE && + git__prefixcmp(branch_name, GIT_REFS_REMOTES_DIR) == 0) + return filter->branch_cb(branch_name + strlen(GIT_REFS_REMOTES_DIR), GIT_BRANCH_REMOTE, filter->callback_payload); + + return 0; +} + +int git_branch_foreach( + git_repository *repo, + unsigned int list_flags, + int (*branch_cb)( + const char *branch_name, + git_branch_t branch_type, + void *payload), + void *payload +) +{ + branch_foreach_filter filter; + + filter.branch_cb = branch_cb; + filter.branch_type = list_flags; + filter.callback_payload = payload; + + return git_reference_foreach(repo, GIT_REF_LISTALL, &branch_foreach_cb, (void *)&filter); +} + int git_branch_move(git_repository *repo, const char *old_branch_name, const char *new_branch_name, int force) { git_reference *reference = NULL; diff --git a/tests-clar/refs/branches/foreach.c b/tests-clar/refs/branches/foreach.c new file mode 100644 index 000000000..60bf50bd5 --- /dev/null +++ b/tests-clar/refs/branches/foreach.c @@ -0,0 +1,121 @@ +#include "clar_libgit2.h" +#include "refs.h" +#include "branch.h" + +static git_repository *repo; +static git_reference *fake_remote; + +void test_refs_branches_foreach__initialize(void) +{ + git_oid id; + + cl_fixture_sandbox("testrepo.git"); + cl_git_pass(git_repository_open(&repo, "testrepo.git")); + + cl_git_pass(git_oid_fromstr(&id, "be3563ae3f795b2b4353bcce3a527ad0a4f7f644")); + cl_git_pass(git_reference_create_oid(&fake_remote, repo, "refs/remotes/nulltoken/master", &id, 0)); +} + +void test_refs_branches_foreach__cleanup(void) +{ + git_reference_free(fake_remote); + git_repository_free(repo); + + cl_fixture_cleanup("testrepo.git"); +} + +static int count_branch_list_cb(const char *branch_name, git_branch_t branch_type, void *payload) +{ + int *count = (int *)payload; + + (*count)++; + + return 0; +} + +static void assert_retrieval(unsigned int flags, unsigned int expected_count) +{ + int count = 0; + + cl_git_pass(git_branch_foreach(repo, flags, count_branch_list_cb, &count)); + + cl_assert_equal_i(expected_count, count); +} + +void test_refs_branches_foreach__retrieve_all_branches(void) +{ + assert_retrieval(GIT_BRANCH_LOCAL | GIT_BRANCH_REMOTE, 9); +} + +void test_refs_branches_foreach__retrieve_remote_branches(void) +{ + assert_retrieval(GIT_BRANCH_REMOTE, 2); +} + +void test_refs_branches_foreach__retrieve_local_branches(void) +{ + assert_retrieval(GIT_BRANCH_LOCAL, 7); +} + +struct expectations { + const char *branch_name; + int encounters; +}; + +static void assert_branch_has_been_found(struct expectations *findings, const char* expected_branch_name) +{ + int pos = 0; + + while (findings[pos].branch_name) + { + if (strcmp(expected_branch_name, findings[pos].branch_name) == 0) { + cl_assert_equal_i(1, findings[pos].encounters); + return; + } + + pos++; + } + + cl_fail("expected branch not found in list."); +} + +static int contains_branch_list_cb(const char *branch_name, git_branch_t branch_type, void *payload) +{ + int pos = 0; + + struct expectations *exp = (struct expectations *)payload; + + while (exp[pos].branch_name) + { + if (strcmp(branch_name, exp[pos].branch_name) == 0) + exp[pos].encounters++; + + pos++; + } + + return 0; +} + +/* + * $ git branch -r + * nulltoken/HEAD -> nulltoken/master + * nulltoken/master + */ +void test_refs_branches_foreach__retrieve_remote_symbolic_HEAD_when_present(void) +{ + struct expectations exp[] = { + { "nulltoken/HEAD", 0 }, + { "nulltoken/master", 0 }, + { NULL, 0 } + }; + + git_reference_free(fake_remote); + cl_git_pass(git_reference_create_symbolic(&fake_remote, repo, "refs/remotes/nulltoken/HEAD", "refs/remotes/nulltoken/master", 0)); + + assert_retrieval(GIT_BRANCH_REMOTE, 3); + + cl_git_pass(git_branch_foreach(repo, GIT_BRANCH_REMOTE, contains_branch_list_cb, &exp)); + + assert_branch_has_been_found(exp, "nulltoken/HEAD"); + assert_branch_has_been_found(exp, "nulltoken/HEAD"); +}