Commit 731df570 by nulltoken

Add basic branch management API: git_branch_create(), git_branch_delete(), git_branch_list()

parent 79fd4230
...@@ -4,12 +4,97 @@ ...@@ -4,12 +4,97 @@
* This file is part of libgit2, distributed under the GNU GPL v2 with * This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file. * a Linking Exception. For full terms see the included COPYING file.
*/ */
#ifndef INCLUDE_branch_h__ #ifndef INCLUDE_git_branch_h__
#define INCLUDE_branch_h__ #define INCLUDE_git_branch_h__
struct git_branch { #include "common.h"
char *remote; /* TODO: Make this a git_remote */ #include "types.h"
char *merge;
};
/**
* @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 #endif
...@@ -160,6 +160,11 @@ typedef enum { ...@@ -160,6 +160,11 @@ typedef enum {
GIT_REF_LISTALL = GIT_REF_OID|GIT_REF_SYMBOLIC|GIT_REF_PACKED, GIT_REF_LISTALL = GIT_REF_OID|GIT_REF_SYMBOLIC|GIT_REF_PACKED,
} git_rtype; } 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_refspec git_refspec;
typedef struct git_remote git_remote; typedef struct git_remote git_remote;
......
/*
* 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;
}
/*
* 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
#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));
}
#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));
}
#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);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment