diff options
author | nulltoken <emeric.fermas@gmail.com> | 2012-04-04 15:57:19 +0200 |
---|---|---|
committer | nulltoken <emeric.fermas@gmail.com> | 2012-04-10 21:39:03 +0200 |
commit | 731df57080704183cad128c17fd065e5e25fa886 (patch) | |
tree | 52dc19eeb977dd1459c0e770c9be7da08ad785b5 | |
parent | 79fd42301e80c1f787ee9e9b83dc5159ae12854a (diff) | |
download | libgit2-731df57080704183cad128c17fd065e5e25fa886.tar.gz |
Add basic branch management API: git_branch_create(), git_branch_delete(), git_branch_list()
-rw-r--r-- | include/git2/branch.h | 97 | ||||
-rw-r--r-- | include/git2/types.h | 5 | ||||
-rw-r--r-- | src/branch.c | 180 | ||||
-rw-r--r-- | src/branch.h | 17 | ||||
-rw-r--r-- | tests-clar/refs/branches/create.c | 114 | ||||
-rw-r--r-- | tests-clar/refs/branches/delete.c | 76 | ||||
-rw-r--r-- | tests-clar/refs/branches/listall.c | 49 |
7 files changed, 532 insertions, 6 deletions
diff --git a/include/git2/branch.h b/include/git2/branch.h index 75927e99a..fa1c6f3ec 100644 --- a/include/git2/branch.h +++ b/include/git2/branch.h @@ -4,12 +4,97 @@ * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ -#ifndef INCLUDE_branch_h__ -#define INCLUDE_branch_h__ +#ifndef INCLUDE_git_branch_h__ +#define INCLUDE_git_branch_h__ -struct git_branch { - char *remote; /* TODO: Make this a git_remote */ - char *merge; -}; +#include "common.h" +#include "types.h" +/** + * @file git2/branch.h + * @brief Git branch parsing routines + * @defgroup git_branch Git branch management + * @ingroup Git + * @{ + */ +GIT_BEGIN_DECL + +/** + * Create a new branch pointing at a target commit + * + * A new direct reference will be created pointing to + * this target commit. If `force` is true and a reference + * already exists with the given name, it'll be replaced. + * + * @param oid_out Pointer where to store the OID of the target commit. + * + * @param repo Repository where to store the branch. + * + * @param branch_name Name for the branch; this name is + * validated for consistency. It should also not conflict with + * an already existing branch name. + * + * @param target Object to which this branch should point. This object + * must belong to the given `repo` and can either be a git_commit or a + * git_tag. When a git_tag is being passed, it should be dereferencable + * to a git_commit which oid will be used as the target of the branch. + * + * @param force Overwrite existing branch. + * + * @return GIT_SUCCESS or an error code. + * A proper reference is written in the refs/heads namespace + * pointing to the provided target commit. + */ +GIT_EXTERN(int) git_branch_create( + git_oid *oid_out, + git_repository *repo, + const char *branch_name, + const git_object *target, + int force); + +/** + * Delete an existing branch reference. + * + * @param repo Repository where lives the branch. + * + * @param branch_name Name of the branch to be deleted; + * this name is validated for consistency. + * + * @param branch_type Type of the considered branch. This should + * be valued with either GIT_BRANCH_LOCAL or GIT_BRANCH_REMOTE. + * + * @return GIT_SUCCESS on success, GIT_ENOTFOUND if the branch + * doesn't exist or an error code. + */ +GIT_EXTERN(int) git_branch_delete( + git_repository *repo, + const char *branch_name, + enum git_branch_type branch_type); + +/** + * Fill a list with all the branches in the Repository + * + * The string array will be filled with the names of the + * matching branches; these values are owned by the user and + * should be free'd manually when no longer needed, using + * `git_strarray_free`. + * + * @param branch_names Pointer to a git_strarray structure + * where the branch names will be stored. + * + * @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. + * + * @return GIT_SUCCESS or an error code. + */ +GIT_EXTERN(int) git_branch_list( + git_strarray *branch_names, + git_repository *repo, + unsigned int list_flags); + +/** @} */ +GIT_END_DECL #endif diff --git a/include/git2/types.h b/include/git2/types.h index ffada630a..98eea5374 100644 --- a/include/git2/types.h +++ b/include/git2/types.h @@ -160,6 +160,11 @@ typedef enum { GIT_REF_LISTALL = GIT_REF_OID|GIT_REF_SYMBOLIC|GIT_REF_PACKED, } git_rtype; +/** Basic type of any Git branch. */ +typedef enum { + GIT_BRANCH_LOCAL = 1, + GIT_BRANCH_REMOTE = 2, +} git_branch_type; typedef struct git_refspec git_refspec; typedef struct git_remote git_remote; diff --git a/src/branch.c b/src/branch.c new file mode 100644 index 000000000..c4dbc354d --- /dev/null +++ b/src/branch.c @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2009-2012 the libgit2 contributors + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" +#include "commit.h" +#include "branch.h" +#include "tag.h" + +static int retrieve_branch_reference( + git_reference **branch_reference_out, + git_repository *repo, + const char *branch_name, + int is_remote) +{ + git_reference *branch; + int error = -1; + char *prefix; + git_buf ref_name = GIT_BUF_INIT; + + *branch_reference_out = NULL; + + prefix = is_remote ? GIT_REFS_REMOTES_DIR : GIT_REFS_HEADS_DIR; + + if (git_buf_joinpath(&ref_name, prefix, branch_name) < 0) + goto cleanup; + + if ((error = git_reference_lookup(&branch, repo, ref_name.ptr)) < 0) { + giterr_set(GITERR_REFERENCE, + "Cannot locate %s branch '%s'.", is_remote ? "remote-tracking" : "local", branch_name); + goto cleanup; + } + + *branch_reference_out = branch; + +cleanup: + git_buf_free(&ref_name); + return error; +} + +static int create_error_invalid(const char *msg) +{ + giterr_set(GITERR_INVALID, "Cannot create branch - %s", msg); + return -1; +} + +int git_branch_create( + git_oid *oid_out, + git_repository *repo, + const char *branch_name, + const git_object *target, + int force) +{ + git_otype target_type = GIT_OBJ_BAD; + git_object *commit = NULL; + git_reference *branch = NULL; + git_buf canonical_branch_name = GIT_BUF_INIT; + int error = -1; + + assert(repo && branch_name && target && oid_out); + + if (git_object_owner(target) != repo) + return create_error_invalid("The given target does not belong to this repository"); + + target_type = git_object_type(target); + + switch (target_type) + { + case GIT_OBJ_TAG: + if (git_tag_peel(&commit, (git_tag *)target) < 0) + goto cleanup; + + if (git_object_type(commit) != GIT_OBJ_COMMIT) { + create_error_invalid("The given target does not resolve to a commit"); + goto cleanup; + } + break; + + case GIT_OBJ_COMMIT: + commit = (git_object *)target; + break; + + default: + return create_error_invalid("Only git_tag and git_commit objects are valid targets."); + } + + if (git_buf_joinpath(&canonical_branch_name, GIT_REFS_HEADS_DIR, branch_name) < 0) + goto cleanup; + + if (git_reference_create_oid(&branch, repo, git_buf_cstr(&canonical_branch_name), git_object_id(commit), force) < 0) + goto cleanup; + + git_oid_cpy(oid_out, git_reference_oid(branch)); + error = 0; + +cleanup: + if (target_type == GIT_OBJ_TAG) + git_object_free(commit); + + git_reference_free(branch); + git_buf_free(&canonical_branch_name); + return error; +} + +int git_branch_delete(git_repository *repo, const char *branch_name, enum git_branch_type branch_type) +{ + git_reference *branch = NULL; + git_reference *head = NULL; + int error; + + assert((branch_type == GIT_BRANCH_LOCAL) || (branch_type == GIT_BRANCH_REMOTE)); + + if ((error = retrieve_branch_reference(&branch, repo, branch_name, branch_type == GIT_BRANCH_REMOTE)) < 0) + goto cleanup; + + if (git_reference_lookup(&head, repo, GIT_HEAD_FILE) < 0) { + giterr_set(GITERR_REFERENCE, "Cannot locate HEAD."); + error = -1; + goto cleanup; + } + + if ((git_reference_type(head) == GIT_REF_SYMBOLIC) + && (strcmp(git_reference_target(head), git_reference_name(branch)) == 0)) { + giterr_set(GITERR_REFERENCE, + "Cannot delete branch '%s' as it is the current HEAD of the repository.", branch_name); + error = -1; + goto cleanup; + } + + return git_reference_delete(branch); + +cleanup: + git_reference_free(head); + git_reference_free(branch); + return error; +} + +typedef struct { + git_vector *branchlist; + unsigned int branch_type; +} branch_filter_data; + +static int branch_list_cb(const char *branch_name, void *payload) +{ + branch_filter_data *filter = (branch_filter_data *)payload; + + if ((filter->branch_type & GIT_BRANCH_LOCAL && git__prefixcmp(branch_name, GIT_REFS_HEADS_DIR) == 0) + || (filter->branch_type & GIT_BRANCH_REMOTE && git__prefixcmp(branch_name, GIT_REFS_REMOTES_DIR) == 0)) + return git_vector_insert(filter->branchlist, git__strdup(branch_name)); + + return 0; +} + +int git_branch_list(git_strarray *branch_names, git_repository *repo, unsigned int list_flags) +{ + int error; + branch_filter_data filter; + git_vector branchlist; + + assert(branch_names && repo); + + if (git_vector_init(&branchlist, 8, NULL) < 0) + return -1; + + filter.branchlist = &branchlist; + filter.branch_type = list_flags; + + error = git_reference_foreach(repo, GIT_REF_OID|GIT_REF_PACKED, &branch_list_cb, (void *)&filter); + if (error < 0) { + git_vector_free(&branchlist); + return -1; + } + + branch_names->strings = (char **)branchlist.contents; + branch_names->count = branchlist.length; + return 0; +} diff --git a/src/branch.h b/src/branch.h new file mode 100644 index 000000000..d0e5abc8b --- /dev/null +++ b/src/branch.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2009-2012 the libgit2 contributors + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_branch_h__ +#define INCLUDE_branch_h__ + +#include "git2/branch.h" + +struct git_branch { + char *remote; /* TODO: Make this a git_remote */ + char *merge; +}; + +#endif diff --git a/tests-clar/refs/branches/create.c b/tests-clar/refs/branches/create.c new file mode 100644 index 000000000..dd66ec99e --- /dev/null +++ b/tests-clar/refs/branches/create.c @@ -0,0 +1,114 @@ +#include "clar_libgit2.h" +#include "refs.h" +#include "branch.h" + +static git_repository *repo; +static git_reference *fake_remote; +static git_oid branch_target_oid; +static git_object *target; + +void test_refs_branches_create__initialize(void) +{ + cl_fixture_sandbox("testrepo.git"); + cl_git_pass(git_repository_open(&repo, "testrepo.git")); +} + +void test_refs_branches_create__cleanup(void) +{ + git_object_free(target); + git_repository_free(repo); + + cl_fixture_cleanup("testrepo.git"); +} + +static void retrieve_target_from_oid(git_object **object_out, git_repository *repo, const char *sha) +{ + git_oid oid; + + cl_git_pass(git_oid_fromstr(&oid, sha)); + cl_git_pass(git_object_lookup(object_out, repo, &oid, GIT_OBJ_ANY)); +} + +static void retrieve_known_commit(git_object **object, git_repository *repo) +{ + retrieve_target_from_oid(object, repo, "e90810b8df3e80c413d903f631643c716887138d"); +} + +#define NEW_BRANCH_NAME "new-branch-on-the-block" + +void test_refs_branches_create__can_create_a_local_branch(void) +{ + retrieve_known_commit(&target, repo); + + cl_git_pass(git_branch_create(&branch_target_oid, repo, NEW_BRANCH_NAME, target, 0)); + cl_git_pass(git_oid_cmp(&branch_target_oid, git_object_id(target))); +} + +void test_refs_branches_create__creating_a_local_branch_triggers_the_creation_of_a_new_direct_reference(void) +{ + git_reference *branch; + + retrieve_known_commit(&target, repo); + + cl_git_fail(git_reference_lookup(&branch, repo, GIT_REFS_HEADS_DIR NEW_BRANCH_NAME)); + + cl_git_pass(git_branch_create(&branch_target_oid, repo, NEW_BRANCH_NAME, target, 0)); + + cl_git_pass(git_reference_lookup(&branch, repo, GIT_REFS_HEADS_DIR NEW_BRANCH_NAME)); + cl_assert(git_reference_type(branch) == GIT_REF_OID); + + git_reference_free(branch); +} + +void test_refs_branches_create__can_not_create_a_branch_if_its_name_collide_with_an_existing_one(void) +{ + retrieve_known_commit(&target, repo); + + cl_git_fail(git_branch_create(&branch_target_oid, repo, "br2", target, 0)); +} + +void test_refs_branches_create__can_force_create_over_an_existing_branch(void) +{ + retrieve_known_commit(&target, repo); + + cl_git_pass(git_branch_create(&branch_target_oid, repo, "br2", target, 1)); + cl_git_pass(git_oid_cmp(&branch_target_oid, git_object_id(target))); +} + +void test_refs_branches_create__can_not_create_a_branch_pointing_at_an_object_unknown_from_the_repository(void) +{ + git_repository *repo2; + + /* Open another instance of the same repository */ + cl_git_pass(git_repository_open(&repo2, cl_fixture("testrepo.git"))); + + /* Retrieve a commit object from this different repository */ + retrieve_known_commit(&target, repo2); + + cl_git_fail(git_branch_create(&branch_target_oid, repo, NEW_BRANCH_NAME, target, 0)); + + git_repository_free(repo2); +} + +void test_refs_branches_create__creating_a_branch_targeting_a_tag_dereferences_it_to_its_commit(void) +{ + /* b25fa35 is a tag, pointing to another tag which points to a commit */ + retrieve_target_from_oid(&target, repo, "b25fa35b38051e4ae45d4222e795f9df2e43f1d1"); + + cl_git_pass(git_branch_create(&branch_target_oid, repo, NEW_BRANCH_NAME, target, 0)); + cl_git_pass(git_oid_streq(&branch_target_oid, "e90810b8df3e80c413d903f631643c716887138d")); +} + +void test_refs_branches_create__can_not_create_a_branch_pointing_to_a_non_commit_object(void) +{ + /* 53fc32d is the tree of commit e90810b */ + retrieve_target_from_oid(&target, repo, "53fc32d17276939fc79ed05badaef2db09990016"); + + cl_git_fail(git_branch_create(&branch_target_oid, repo, NEW_BRANCH_NAME, target, 0)); + git_object_free(target); + + /* 521d87c is an annotated tag pointing to a blob */ + retrieve_target_from_oid(&target, repo, "521d87c1ec3aef9824daf6d96cc0ae3710766d91"); + + cl_git_fail(git_branch_create(&branch_target_oid, repo, NEW_BRANCH_NAME, target, 0)); +} diff --git a/tests-clar/refs/branches/delete.c b/tests-clar/refs/branches/delete.c new file mode 100644 index 000000000..095893020 --- /dev/null +++ b/tests-clar/refs/branches/delete.c @@ -0,0 +1,76 @@ +#include "clar_libgit2.h" +#include "refs.h" +#include "branch.h" + +static git_repository *repo; +static git_reference *fake_remote; + +void test_refs_branches_delete__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_delete__cleanup(void) +{ + git_reference_free(fake_remote); + git_repository_free(repo); + + cl_fixture_cleanup("testrepo.git"); +} + +void test_refs_branches_delete__can_not_delete_a_non_existing_branch(void) +{ + cl_git_fail(git_branch_delete(repo, "i-am-not-a-local-branch", GIT_BRANCH_LOCAL)); + cl_git_fail(git_branch_delete(repo, "neither/a-remote-one", GIT_BRANCH_REMOTE)); +} + +void test_refs_branches_delete__can_not_delete_a_branch_pointed_at_by_HEAD(void) +{ + git_reference *head; + + /* Ensure HEAD targets the local master branch */ + cl_git_pass(git_reference_lookup(&head, repo, GIT_HEAD_FILE)); + cl_assert(strcmp("refs/heads/master", git_reference_target(head)) == 0); + git_reference_free(head); + + cl_git_fail(git_branch_delete(repo, "master", GIT_BRANCH_LOCAL)); +} + +void test_refs_branches_delete__can_not_delete_a_branch_if_HEAD_is_missing(void) +{ + git_reference *head; + + cl_git_pass(git_reference_lookup(&head, repo, GIT_HEAD_FILE)); + git_reference_delete(head); + + cl_git_fail(git_branch_delete(repo, "br2", GIT_BRANCH_LOCAL)); +} + +void test_refs_branches_delete__can_delete_a_branch_pointed_at_by_detached_HEAD(void) +{ + git_reference *master, *head; + + /* Detach HEAD and make it target the commit that "master" points to */ + cl_git_pass(git_reference_lookup(&master, repo, "refs/heads/master")); + cl_git_pass(git_reference_create_oid(&head, repo, "HEAD", git_reference_oid(master), 1)); + git_reference_free(head); + git_reference_free(master); + + cl_git_pass(git_branch_delete(repo, "master", GIT_BRANCH_LOCAL)); +} + +void test_refs_branches_delete__can_delete_a_local_branch(void) +{ + cl_git_pass(git_branch_delete(repo, "br2", GIT_BRANCH_LOCAL)); +} + +void test_refs_branches_delete__can_delete_a_remote_branch(void) +{ + cl_git_pass(git_branch_delete(repo, "nulltoken/master", GIT_BRANCH_REMOTE)); +} diff --git a/tests-clar/refs/branches/listall.c b/tests-clar/refs/branches/listall.c new file mode 100644 index 000000000..391177368 --- /dev/null +++ b/tests-clar/refs/branches/listall.c @@ -0,0 +1,49 @@ +#include "clar_libgit2.h" +#include "refs.h" +#include "branch.h" + +static git_repository *repo; +static git_strarray branch_list; +static git_reference *fake_remote; + +void test_refs_branches_listall__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_listall__cleanup(void) +{ + git_strarray_free(&branch_list); + git_reference_free(fake_remote); + git_repository_free(repo); + + cl_fixture_cleanup("testrepo.git"); +} + +static void assert_retrieval(unsigned int flags, unsigned int expected_count) +{ + cl_git_pass(git_branch_list(&branch_list, repo, flags)); + + cl_assert(branch_list.count == expected_count); +} + +void test_refs_branches_listall__retrieve_all_branches(void) +{ + assert_retrieval(GIT_BRANCH_LOCAL | GIT_BRANCH_REMOTE, 6 + 1); +} + +void test_refs_branches_listall__retrieve_remote_branches(void) +{ + assert_retrieval(GIT_BRANCH_REMOTE, 1); +} + +void test_refs_branches_listall__retrieve_local_branches(void) +{ + assert_retrieval(GIT_BRANCH_LOCAL, 6); +} |