From 3f46f313cbfcf57e5cbf3d7dc55b747568a21bef Mon Sep 17 00:00:00 2001 From: nulltoken Date: Fri, 6 Apr 2012 14:34:26 +0200 Subject: [PATCH 1/5] tag: Add git_tag_peel() which recursively peel a tag until a non tag git_object is met --- include/git2/tag.h | 15 +++++ src/tag.c | 20 +++++++ tests-clar/network/remotelocal.c | 4 +- tests-clar/object/tag/peel.c | 56 ++++++++++++++++++ .../52/1d87c1ec3aef9824daf6d96cc0ae3710766d91 | Bin 0 -> 152 bytes .../refs/tags/annotated_tag_to_blob | Bin 0 -> 41 bytes tests/t08-tag.c | 8 +-- tests/t10-refs.c | 4 +- 8 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 tests-clar/object/tag/peel.c create mode 100644 tests/resources/testrepo.git/objects/52/1d87c1ec3aef9824daf6d96cc0ae3710766d91 create mode 100644 tests/resources/testrepo.git/refs/tags/annotated_tag_to_blob diff --git a/include/git2/tag.h b/include/git2/tag.h index f7fce3a70..9ab4b7b9e 100644 --- a/include/git2/tag.h +++ b/include/git2/tag.h @@ -274,6 +274,21 @@ GIT_EXTERN(int) git_tag_list_match( const char *pattern, git_repository *repo); +/** + * Recursively peel a tag until a non tag git_object + * is met + * + * The retrieved `tag_target` object is owned by the repository + * and should be closed with the `git_object_free` method. + * + * @param tag_target Pointer to the peeled git_object + * @param tag The tag to be processed + * @return GIT_SUCCESS or an error code + */ +GIT_EXTERN(int) git_tag_peel( + git_object **tag_target, + git_tag *tag); + /** @} */ GIT_END_DECL #endif diff --git a/src/tag.c b/src/tag.c index a5089e71c..cfd2c7081 100644 --- a/src/tag.c +++ b/src/tag.c @@ -451,3 +451,23 @@ int git_tag_list(git_strarray *tag_names, git_repository *repo) { return git_tag_list_match(tag_names, "", repo); } + +int git_tag_peel(git_object **tag_target, git_tag *tag) +{ + int error; + git_object *target; + + assert(tag_target && tag); + + if (git_tag_target(&target, tag) < 0) + return -1; + + if (git_object_type(target) == GIT_OBJ_TAG) { + error = git_tag_peel(tag_target, (git_tag *)target); + git_object_free(target); + return error; + } + + *tag_target = target; + return 0; +} diff --git a/tests-clar/network/remotelocal.c b/tests-clar/network/remotelocal.c index 81af77756..74a0b57aa 100644 --- a/tests-clar/network/remotelocal.c +++ b/tests-clar/network/remotelocal.c @@ -88,7 +88,7 @@ void test_network_remotelocal__retrieve_advertised_references(void) cl_git_pass(git_remote_ls(remote, &count_ref__cb, &how_many_refs)); - cl_assert(how_many_refs == 12); /* 1 HEAD + 9 refs + 2 peeled tags */ + cl_assert(how_many_refs == 14); /* 1 HEAD + 6 heads + 1 lightweight tag + 3 annotated tags + 3 peeled target */ } void test_network_remotelocal__retrieve_advertised_references_from_spaced_repository(void) @@ -102,7 +102,7 @@ void test_network_remotelocal__retrieve_advertised_references_from_spaced_reposi cl_git_pass(git_remote_ls(remote, &count_ref__cb, &how_many_refs)); - cl_assert(how_many_refs == 12); /* 1 HEAD */ + cl_assert(how_many_refs == 14); /* 1 HEAD + 6 heads + 1 lightweight tag + 3 annotated tags + 3 peeled target */ cl_fixture_cleanup("spaced testrepo.git"); } diff --git a/tests-clar/object/tag/peel.c b/tests-clar/object/tag/peel.c new file mode 100644 index 000000000..97c5a7dd3 --- /dev/null +++ b/tests-clar/object/tag/peel.c @@ -0,0 +1,56 @@ +#include "clar_libgit2.h" +#include "tag.h" + +static git_repository *repo; +static git_tag *tag; +static git_object *target; + +void test_object_tag_peel__initialize(void) +{ + cl_fixture_sandbox("testrepo.git"); + cl_git_pass(git_repository_open(&repo, "testrepo.git")); +} + +void test_object_tag_peel__cleanup(void) +{ + git_tag_free(tag); + git_object_free(target); + git_repository_free(repo); + + cl_fixture_cleanup("testrepo.git"); +} + +static void retrieve_tag_from_oid(git_tag **tag_out, git_repository *repo, const char *sha) +{ + git_oid oid; + + cl_git_pass(git_oid_fromstr(&oid, sha)); + cl_git_pass(git_tag_lookup(tag_out, repo, &oid)); +} + +void test_object_tag_peel__can_peel_to_a_commit(void) +{ + retrieve_tag_from_oid(&tag, repo, "7b4384978d2493e851f9cca7858815fac9b10980"); + + cl_git_pass(git_tag_peel(&target, tag)); + cl_assert(git_object_type(target) == GIT_OBJ_COMMIT); + cl_git_pass(git_oid_streq(git_object_id(target), "e90810b8df3e80c413d903f631643c716887138d")); +} + +void test_object_tag_peel__can_peel_several_nested_tags_to_a_commit(void) +{ + retrieve_tag_from_oid(&tag, repo, "b25fa35b38051e4ae45d4222e795f9df2e43f1d1"); + + cl_git_pass(git_tag_peel(&target, tag)); + cl_assert(git_object_type(target) == GIT_OBJ_COMMIT); + cl_git_pass(git_oid_streq(git_object_id(target), "e90810b8df3e80c413d903f631643c716887138d")); +} + +void test_object_tag_peel__can_peel_to_a_non_commit(void) +{ + retrieve_tag_from_oid(&tag, repo, "521d87c1ec3aef9824daf6d96cc0ae3710766d91"); + + cl_git_pass(git_tag_peel(&target, tag)); + cl_assert(git_object_type(target) == GIT_OBJ_BLOB); + cl_git_pass(git_oid_streq(git_object_id(target), "1385f264afb75a56a5bec74243be9b367ba4ca08")); +} diff --git a/tests/resources/testrepo.git/objects/52/1d87c1ec3aef9824daf6d96cc0ae3710766d91 b/tests/resources/testrepo.git/objects/52/1d87c1ec3aef9824daf6d96cc0ae3710766d91 new file mode 100644 index 0000000000000000000000000000000000000000..351cff823065f92f8b4ef218194c1dfb06e86fd0 GIT binary patch literal 152 zcmV;J0B8Sr0X2<53c@fD06pgw+p|#8CTS{&2wv4Mlug{0+9WG=J@|Wj@i+|32u{#+ zZpYzCQJ^us8{5v}7`#K*p$infZLJA(2&VG^ZA9HG`MwB3;-F+JU@0sp^cXf8gonSG zXod1gNqC_GN6NI$u^ws7_&!e==Tt||r|oNu*WNk@d);cS)RlRG8&+^gH-U~DU literal 0 HcmV?d00001 diff --git a/tests/t08-tag.c b/tests/t08-tag.c index 4cbd48379..1586be1fa 100644 --- a/tests/t08-tag.c +++ b/tests/t08-tag.c @@ -73,7 +73,7 @@ BEGIN_TEST(read1, "list all tag names from the repository") must_pass(git_repository_open(&repo, REPOSITORY_FOLDER)); must_pass(git_tag_list(&tag_list, repo)); - must_be_true(tag_list.count == 3); + must_be_true(tag_list.count == 4); git_strarray_free(&tag_list); git_repository_free(repo); @@ -98,10 +98,10 @@ exit: BEGIN_TEST(read2, "list all tag names from the repository matching a specified pattern") git_repository *repo; must_pass(git_repository_open(&repo, REPOSITORY_FOLDER)); - must_pass(ensure_tag_pattern_match(repo, "", 3)); - must_pass(ensure_tag_pattern_match(repo, "*", 3)); + must_pass(ensure_tag_pattern_match(repo, "", 4)); + must_pass(ensure_tag_pattern_match(repo, "*", 4)); must_pass(ensure_tag_pattern_match(repo, "t*", 1)); - must_pass(ensure_tag_pattern_match(repo, "*b", 2)); + must_pass(ensure_tag_pattern_match(repo, "*b", 3)); must_pass(ensure_tag_pattern_match(repo, "e", 0)); must_pass(ensure_tag_pattern_match(repo, "e90810b", 1)); must_pass(ensure_tag_pattern_match(repo, "e90810[ab]", 1)); diff --git a/tests/t10-refs.c b/tests/t10-refs.c index ad881726e..7229ded9c 100644 --- a/tests/t10-refs.c +++ b/tests/t10-refs.c @@ -1164,10 +1164,10 @@ BEGIN_TEST(list0, "try to list all the references in our test repo") printf("# %s\n", ref_list.strings[i]); }*/ - /* We have exactly 9 refs in total if we include the packed ones: + /* We have exactly 10 refs in total if we include the packed ones: * there is a reference that exists both in the packfile and as * loose, but we only list it once */ - must_be_true(ref_list.count == 9); + must_be_true(ref_list.count == 10); git_strarray_free(&ref_list); git_repository_free(repo); From 79fd42301e80c1f787ee9e9b83dc5159ae12854a Mon Sep 17 00:00:00 2001 From: nulltoken Date: Fri, 6 Apr 2012 15:23:18 +0200 Subject: [PATCH 2/5] transport/local: Fix peeling of nested tags --- src/transports/local.c | 9 +++++++-- tests-clar/network/remotelocal.c | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/transports/local.c b/src/transports/local.c index eb24db0fd..6cf8ed9d6 100644 --- a/src/transports/local.c +++ b/src/transports/local.c @@ -27,7 +27,7 @@ static int add_ref(transport_local *t, const char *name) const char peeled[] = "^{}"; git_remote_head *head; git_reference *ref, *resolved_ref; - git_object *obj = NULL; + git_object *obj = NULL, *peeled_tag_target = NULL; int error = GIT_SUCCESS, peel_len, ret; head = git__malloc(sizeof(git_remote_head)); @@ -78,7 +78,11 @@ static int add_ref(transport_local *t, const char *name) assert(ret < peel_len + 1); (void)ret; - git_oid_cpy(&head->oid, git_tag_target_oid((git_tag *) obj)); + error = git_tag_peel(&peeled_tag_target, (git_tag *) obj); + if (error < 0) + goto out; + + git_oid_cpy(&head->oid, git_object_id(peeled_tag_target)); error = git_vector_insert(&t->refs, head); if (error < GIT_SUCCESS) @@ -89,6 +93,7 @@ static int add_ref(transport_local *t, const char *name) git_reference_free(resolved_ref); git_object_free(obj); + git_object_free(peeled_tag_target); if (head && error < GIT_SUCCESS) { git__free(head->name); git__free(head); diff --git a/tests-clar/network/remotelocal.c b/tests-clar/network/remotelocal.c index 74a0b57aa..e154226d9 100644 --- a/tests-clar/network/remotelocal.c +++ b/tests-clar/network/remotelocal.c @@ -71,6 +71,16 @@ static int count_ref__cb(git_remote_head *head, void *payload) return GIT_SUCCESS; } +static int ensure_peeled__cb(git_remote_head *head, void *payload) +{ + GIT_UNUSED(payload); + + if(strcmp(head->name, "refs/tags/test^{}") != 0) + return 0; + + return git_oid_streq(&head->oid, "e90810b8df3e80c413d903f631643c716887138d"); +} + static void connect_to_local_repository(const char *local_repository) { build_local_file_url(&file_path_buf, local_repository); @@ -104,5 +114,15 @@ void test_network_remotelocal__retrieve_advertised_references_from_spaced_reposi cl_assert(how_many_refs == 14); /* 1 HEAD + 6 heads + 1 lightweight tag + 3 annotated tags + 3 peeled target */ + git_remote_free(remote); /* Disconnect from the "spaced repo" before the cleanup */ + remote = NULL; + cl_fixture_cleanup("spaced testrepo.git"); } + +void test_network_remotelocal__nested_tags_are_completely_peeled(void) +{ + connect_to_local_repository(cl_fixture("testrepo.git")); + + cl_git_pass(git_remote_ls(remote, &ensure_peeled__cb, NULL)); +} From 731df57080704183cad128c17fd065e5e25fa886 Mon Sep 17 00:00:00 2001 From: nulltoken Date: Wed, 4 Apr 2012 15:57:19 +0200 Subject: [PATCH 3/5] Add basic branch management API: git_branch_create(), git_branch_delete(), git_branch_list() --- include/git2/branch.h | 97 +++++++++++++++- include/git2/types.h | 5 + src/branch.c | 180 +++++++++++++++++++++++++++++ src/branch.h | 17 +++ tests-clar/refs/branches/create.c | 114 ++++++++++++++++++ tests-clar/refs/branches/delete.c | 76 ++++++++++++ tests-clar/refs/branches/listall.c | 49 ++++++++ 7 files changed, 532 insertions(+), 6 deletions(-) create mode 100644 src/branch.c create mode 100644 src/branch.h create mode 100644 tests-clar/refs/branches/create.c create mode 100644 tests-clar/refs/branches/delete.c create mode 100644 tests-clar/refs/branches/listall.c 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); +} From 555aa453baefec98dbd026592b68214048bedac3 Mon Sep 17 00:00:00 2001 From: nulltoken Date: Mon, 9 Apr 2012 02:28:31 +0200 Subject: [PATCH 4/5] fileops: Make git_futils_mkdir_r() able to skip non-empty directories --- src/fileops.c | 23 +++++++++++++++++------ src/fileops.h | 17 ++++++++++++++++- tests-clar/core/rmdir.c | 28 +++++++++++++++++++++------- tests-clar/status/worktree.c | 6 +++--- 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/fileops.c b/src/fileops.c index b3bb3890e..9da1bf789 100644 --- a/src/fileops.c +++ b/src/fileops.c @@ -298,13 +298,20 @@ int git_futils_mkdir_r(const char *path, const char *base, const mode_t mode) static int _rmdir_recurs_foreach(void *opaque, git_buf *path) { - int force = *(int *)opaque; + enum git_directory_removal_type removal_type = *(enum git_directory_removal_type *)opaque; + + assert(removal_type == GIT_DIRREMOVAL_EMPTY_HIERARCHY + || removal_type == GIT_DIRREMOVAL_FILES_AND_DIRS + || removal_type == GIT_DIRREMOVAL_ONLY_EMPTY_DIRS); if (git_path_isdir(path->ptr) == true) { if (git_path_direach(path, _rmdir_recurs_foreach, opaque) < 0) return -1; if (p_rmdir(path->ptr) < 0) { + if (removal_type == GIT_DIRREMOVAL_ONLY_EMPTY_DIRS && errno == ENOTEMPTY) + return 0; + giterr_set(GITERR_OS, "Could not remove directory '%s'", path->ptr); return -1; } @@ -312,7 +319,7 @@ static int _rmdir_recurs_foreach(void *opaque, git_buf *path) return 0; } - if (force) { + if (removal_type == GIT_DIRREMOVAL_FILES_AND_DIRS) { if (p_unlink(path->ptr) < 0) { giterr_set(GITERR_OS, "Could not remove directory. File '%s' cannot be removed", path->ptr); return -1; @@ -321,18 +328,22 @@ static int _rmdir_recurs_foreach(void *opaque, git_buf *path) return 0; } - giterr_set(GITERR_OS, "Could not remove directory. File '%s' still present", path->ptr); - return -1; + if (removal_type == GIT_DIRREMOVAL_EMPTY_HIERARCHY) { + giterr_set(GITERR_OS, "Could not remove directory. File '%s' still present", path->ptr); + return -1; + } + + return 0; } -int git_futils_rmdir_r(const char *path, int force) +int git_futils_rmdir_r(const char *path, enum git_directory_removal_type removal_type) { int error; git_buf p = GIT_BUF_INIT; error = git_buf_sets(&p, path); if (!error) - error = _rmdir_recurs_foreach(&force, &p); + error = _rmdir_recurs_foreach(&removal_type, &p); git_buf_free(&p); return error; } diff --git a/src/fileops.h b/src/fileops.h index 865b3c9b0..9cc2d1699 100644 --- a/src/fileops.h +++ b/src/fileops.h @@ -58,10 +58,25 @@ extern int git_futils_mkdir_r(const char *path, const char *base, const mode_t m */ extern int git_futils_mkpath2file(const char *path, const mode_t mode); +typedef enum { + GIT_DIRREMOVAL_EMPTY_HIERARCHY = 0, + GIT_DIRREMOVAL_FILES_AND_DIRS = 1, + GIT_DIRREMOVAL_ONLY_EMPTY_DIRS = 2, +} git_directory_removal_type; + /** * Remove path and any files and directories beneath it. + * + * @param path Path to to top level directory to process. + * + * @param removal_type GIT_DIRREMOVAL_EMPTY_HIERARCHY to remove a hierarchy + * of empty directories (will fail if any file is found), GIT_DIRREMOVAL_FILES_AND_DIRS + * to remove a hierarchy of files and folders, GIT_DIRREMOVAL_ONLY_EMPTY_DIRS to only remove + * empty directories (no failure on file encounter). + * + * @return 0 on success; -1 on error. */ -extern int git_futils_rmdir_r(const char *path, int force); +extern int git_futils_rmdir_r(const char *path, enum git_directory_removal_type removal_type); /** * Create and open a temporary file with a `_git2_` suffix. diff --git a/tests-clar/core/rmdir.c b/tests-clar/core/rmdir.c index 66b647587..530f1f908 100644 --- a/tests-clar/core/rmdir.c +++ b/tests-clar/core/rmdir.c @@ -30,25 +30,39 @@ void test_core_rmdir__initialize(void) /* make sure empty dir can be deleted recusively */ void test_core_rmdir__delete_recursive(void) { - cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, 0)); + cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, GIT_DIRREMOVAL_EMPTY_HIERARCHY)); } /* make sure non-empty dir cannot be deleted recusively */ void test_core_rmdir__fail_to_delete_non_empty_dir(void) { git_buf file = GIT_BUF_INIT; - int fd; cl_git_pass(git_buf_joinpath(&file, empty_tmp_dir, "/two/file.txt")); - fd = p_creat(file.ptr, 0666); - cl_assert(fd >= 0); + cl_git_mkfile(git_buf_cstr(&file), "dummy"); - cl_must_pass(p_close(fd)); - cl_git_fail(git_futils_rmdir_r(empty_tmp_dir, 0)); + cl_git_fail(git_futils_rmdir_r(empty_tmp_dir, GIT_DIRREMOVAL_EMPTY_HIERARCHY)); cl_must_pass(p_unlink(file.ptr)); - cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, 0)); + cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, GIT_DIRREMOVAL_EMPTY_HIERARCHY)); + + git_buf_free(&file); +} + +void test_core_rmdir__can_skip__non_empty_dir(void) +{ + git_buf file = GIT_BUF_INIT; + + cl_git_pass(git_buf_joinpath(&file, empty_tmp_dir, "/two/file.txt")); + + cl_git_mkfile(git_buf_cstr(&file), "dummy"); + + cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, GIT_DIRREMOVAL_ONLY_EMPTY_DIRS)); + cl_assert(git_path_exists(git_buf_cstr(&file)) == true); + + cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, GIT_DIRREMOVAL_FILES_AND_DIRS)); + cl_assert(git_path_exists(empty_tmp_dir) == false); git_buf_free(&file); } diff --git a/tests-clar/status/worktree.c b/tests-clar/status/worktree.c index 7a0494ec9..708df2cee 100644 --- a/tests-clar/status/worktree.c +++ b/tests-clar/status/worktree.c @@ -110,7 +110,7 @@ static int remove_file_cb(void *data, git_buf *file) return 0; if (git_path_isdir(filename)) - cl_git_pass(git_futils_rmdir_r(filename, 1)); + cl_git_pass(git_futils_rmdir_r(filename, GIT_DIRREMOVAL_FILES_AND_DIRS)); else cl_git_pass(p_unlink(git_buf_cstr(file))); @@ -346,7 +346,7 @@ void test_status_worktree__issue_592_3(void) repo = cl_git_sandbox_init("issue_592"); cl_git_pass(git_buf_joinpath(&path, git_repository_workdir(repo), "c")); - cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), 1)); + cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), GIT_DIRREMOVAL_FILES_AND_DIRS)); cl_git_pass(git_status_foreach(repo, cb_status__check_592, "c/a.txt")); @@ -376,7 +376,7 @@ void test_status_worktree__issue_592_5(void) repo = cl_git_sandbox_init("issue_592"); cl_git_pass(git_buf_joinpath(&path, git_repository_workdir(repo), "t")); - cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), 1)); + cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), GIT_DIRREMOVAL_FILES_AND_DIRS)); cl_git_pass(p_mkdir(git_buf_cstr(&path), 0777)); cl_git_pass(git_status_foreach(repo, cb_status__check_592, NULL)); From 4615f0f71ba849adef08f7a677842af3e0ee3d53 Mon Sep 17 00:00:00 2001 From: nulltoken Date: Mon, 9 Apr 2012 03:22:14 +0200 Subject: [PATCH 5/5] branch: add git_branch_move() --- include/git2/branch.h | 22 ++++++++++++ src/branch.c | 18 ++++++++++ src/refs.c | 9 +++++ tests-clar/refs/branches/move.c | 62 +++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 tests-clar/refs/branches/move.c diff --git a/include/git2/branch.h b/include/git2/branch.h index fa1c6f3ec..7f4945d1d 100644 --- a/include/git2/branch.h +++ b/include/git2/branch.h @@ -95,6 +95,28 @@ GIT_EXTERN(int) git_branch_list( git_repository *repo, unsigned int list_flags); +/** + * Move/rename an existing branch reference. + * + * @param repo Repository where lives the branch. + * + * @param old_branch_name Current name of the branch to be moved; + * this name is validated for consistency. + * + * @param new_branch_name Target name of the branch once the move + * is performed; this name is validated for consistency. + * + * @param force Overwrite existing branch. + * + * @return GIT_SUCCESS on success, GIT_ENOTFOUND if the branch + * doesn't exist or an error code. + */ +GIT_EXTERN(int) git_branch_move( + git_repository *repo, + const char *old_branch_name, + const char *new_branch_name, + int force); + /** @} */ GIT_END_DECL #endif diff --git a/src/branch.c b/src/branch.c index c4dbc354d..5efb05b92 100644 --- a/src/branch.c +++ b/src/branch.c @@ -178,3 +178,21 @@ int git_branch_list(git_strarray *branch_names, git_repository *repo, unsigned i branch_names->count = branchlist.length; return 0; } + +int git_branch_move(git_repository *repo, const char *old_branch_name, const char *new_branch_name, int force) +{ + git_reference *reference; + git_buf old_reference_name = GIT_BUF_INIT, new_reference_name = GIT_BUF_INIT; + int error; + + if (git_buf_joinpath(&old_reference_name, GIT_REFS_HEADS_DIR, old_branch_name) < 0) + return -1; + + if (git_buf_joinpath(&new_reference_name, GIT_REFS_HEADS_DIR, new_branch_name) < 0) + return -1; + + if ((error = git_reference_lookup(&reference, repo, git_buf_cstr(&old_reference_name))) < 0) + return error; + + return git_reference_rename(reference, git_buf_cstr(&new_reference_name), force); +} diff --git a/src/refs.c b/src/refs.c index ed364cf90..fb23a0ef8 100644 --- a/src/refs.c +++ b/src/refs.c @@ -287,6 +287,15 @@ static int loose_write(git_reference *ref) if (git_buf_joinpath(&ref_path, ref->owner->path_repository, ref->name) < 0) return -1; + /* Remove a possibly existing empty directory hierarchy + * which name would collide with the reference name + */ + if (git_path_isdir(git_buf_cstr(&ref_path)) && + (git_futils_rmdir_r(git_buf_cstr(&ref_path), GIT_DIRREMOVAL_ONLY_EMPTY_DIRS) < 0)) { + git_buf_free(&ref_path); + return -1; + } + if (git_filebuf_open(&file, ref_path.ptr, GIT_FILEBUF_FORCE) < 0) { git_buf_free(&ref_path); return -1; diff --git a/tests-clar/refs/branches/move.c b/tests-clar/refs/branches/move.c new file mode 100644 index 000000000..208bb460e --- /dev/null +++ b/tests-clar/refs/branches/move.c @@ -0,0 +1,62 @@ +#include "clar_libgit2.h" +#include "branch.h" + +static git_repository *repo; + +void test_refs_branches_move__initialize(void) +{ + cl_fixture_sandbox("testrepo.git"); + cl_git_pass(git_repository_open(&repo, "testrepo.git")); +} + +void test_refs_branches_move__cleanup(void) +{ + git_repository_free(repo); + + cl_fixture_cleanup("testrepo.git"); +} + +#define NEW_BRANCH_NAME "new-branch-on-the-block" + +void test_refs_branches_move__can_move_a_local_branch(void) +{ + cl_git_pass(git_branch_move(repo, "br2", NEW_BRANCH_NAME, 0)); +} + +void test_refs_branches_move__can_move_a_local_branch_to_a_different_namespace(void) +{ + /* Downward */ + cl_git_pass(git_branch_move(repo, "br2", "somewhere/" NEW_BRANCH_NAME, 0)); + + /* Upward */ + cl_git_pass(git_branch_move(repo, "somewhere/" NEW_BRANCH_NAME, "br2", 0)); +} + +void test_refs_branches_move__can_move_a_local_branch_to_a_partially_colliding_namespace(void) +{ + /* Downward */ + cl_git_pass(git_branch_move(repo, "br2", "br2/" NEW_BRANCH_NAME, 0)); + + /* Upward */ + cl_git_pass(git_branch_move(repo, "br2/" NEW_BRANCH_NAME, "br2", 0)); +} + +void test_refs_branches_move__can_not_move_a_branch_if_its_destination_name_collide_with_an_existing_one(void) +{ + cl_git_fail(git_branch_move(repo, "br2", "master", 0)); +} + +void test_refs_branches_move__can_not_move_a_non_existing_branch(void) +{ + cl_git_fail(git_branch_move(repo, "i-am-no-branch", NEW_BRANCH_NAME, 0)); +} + +void test_refs_branches_move__can_force_move_over_an_existing_branch(void) +{ + cl_git_pass(git_branch_move(repo, "br2", "master", 1)); +} + +void test_refs_branches_move__can_not_move_a_branch_through_its_canonical_name(void) +{ + cl_git_fail(git_branch_move(repo, "refs/heads/br2", NEW_BRANCH_NAME, 1)); +}