diff options
Diffstat (limited to 'src/branch.c')
-rw-r--r-- | src/branch.c | 569 |
1 files changed, 472 insertions, 97 deletions
diff --git a/src/branch.c b/src/branch.c index 5d5a24038..e7088790e 100644 --- a/src/branch.c +++ b/src/branch.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,8 +7,12 @@ #include "common.h" #include "commit.h" -#include "branch.h" #include "tag.h" +#include "config.h" +#include "refspec.h" +#include "refs.h" + +#include "git2/branch.h" static int retrieve_branch_reference( git_reference **branch_reference_out, @@ -41,168 +45,539 @@ cleanup: return error; } -static int create_error_invalid(const char *msg) +static int not_a_local_branch(const char *reference_name) { - giterr_set(GITERR_INVALID, "Cannot create branch - %s", msg); + giterr_set( + GITERR_INVALID, + "Reference '%s' is not a local branch.", reference_name); 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 **ref_out, + git_repository *repository, + const char *branch_name, + const git_commit *commit, + int force) +{ 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."); - } + assert(branch_name && commit && ref_out); + assert(git_object_owner((const git_object *)commit) == repository); 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; + error = git_reference_create(&branch, repository, + git_buf_cstr(&canonical_branch_name), git_commit_id(commit), force); - git_oid_cpy(oid_out, git_reference_oid(branch)); - error = 0; + if (!error) + *ref_out = branch; 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, git_branch_t branch_type) +int git_branch_delete(git_reference *branch) { - git_reference *branch = NULL; - git_reference *head = NULL; - int error; + int is_head; + git_buf config_section = GIT_BUF_INIT; + int error = -1; + + assert(branch); - assert((branch_type == GIT_BRANCH_LOCAL) || (branch_type == GIT_BRANCH_REMOTE)); + if (!git_reference_is_branch(branch) && + !git_reference_is_remote(branch)) { + giterr_set(GITERR_INVALID, "Reference '%s' is not a valid branch.", git_reference_name(branch)); + return -1; + } - if ((error = retrieve_branch_reference(&branch, repo, branch_name, branch_type == GIT_BRANCH_REMOTE)) < 0) - return error; + if ((is_head = git_branch_is_head(branch)) < 0) + return is_head; - if (git_reference_lookup(&head, repo, GIT_HEAD_FILE) < 0) { - giterr_set(GITERR_REFERENCE, "Cannot locate HEAD."); - goto on_error; + if (is_head) { + giterr_set(GITERR_REFERENCE, + "Cannot delete branch '%s' as it is the current HEAD of the repository.", git_reference_name(branch)); + return -1; } - 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); + if (git_buf_printf(&config_section, "branch.%s", git_reference_name(branch) + strlen(GIT_REFS_HEADS_DIR)) < 0) + goto on_error; + + if (git_config_rename_section( + git_reference_owner(branch), + git_buf_cstr(&config_section), + NULL) < 0) goto on_error; - } if (git_reference_delete(branch) < 0) goto on_error; - git_reference_free(head); - return 0; + error = 0; on_error: - git_reference_free(head); - git_reference_free(branch); - return -1; + git_buf_free(&config_section); + return error; } typedef struct { - git_vector *branchlist; + git_branch_foreach_cb branch_cb; + void *callback_payload; unsigned int branch_type; -} branch_filter_data; +} branch_foreach_filter; -static int branch_list_cb(const char *branch_name, void *payload) +static int branch_foreach_cb(const char *branch_name, void *payload) { - branch_filter_data *filter = (branch_filter_data *)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_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)); + 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_list(git_strarray *branch_names, git_repository *repo, unsigned int list_flags) +int git_branch_foreach( + git_repository *repo, + unsigned int list_flags, + git_branch_foreach_cb branch_cb, + 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_reference **out, + git_reference *branch, + const char *new_branch_name, + int force) +{ + git_buf new_reference_name = GIT_BUF_INIT, + old_config_section = GIT_BUF_INIT, + new_config_section = GIT_BUF_INIT; int error; - branch_filter_data filter; - git_vector branchlist; - assert(branch_names && repo); + assert(branch && new_branch_name); + + if (!git_reference_is_branch(branch)) + return not_a_local_branch(git_reference_name(branch)); + + if ((error = git_buf_joinpath(&new_reference_name, GIT_REFS_HEADS_DIR, new_branch_name)) < 0 || + (error = git_buf_printf(&old_config_section, "branch.%s", git_reference_name(branch) + strlen(GIT_REFS_HEADS_DIR))) < 0 || + (error = git_buf_printf(&new_config_section, "branch.%s", new_branch_name)) < 0) + goto done; + + if ((error = git_config_rename_section(git_reference_owner(branch), + git_buf_cstr(&old_config_section), + git_buf_cstr(&new_config_section))) < 0) + goto done; + + if ((error = git_reference_rename(out, branch, git_buf_cstr(&new_reference_name), force)) < 0) + goto done; + +done: + git_buf_free(&new_reference_name); + git_buf_free(&old_config_section); + git_buf_free(&new_config_section); - if (git_vector_init(&branchlist, 8, NULL) < 0) + return error; +} + +int git_branch_lookup( + git_reference **ref_out, + git_repository *repo, + const char *branch_name, + git_branch_t branch_type) +{ + assert(ref_out && repo && branch_name); + + return retrieve_branch_reference(ref_out, repo, branch_name, branch_type == GIT_BRANCH_REMOTE); +} + +int git_branch_name(const char **out, git_reference *ref) +{ + const char *branch_name; + + assert(out && ref); + + branch_name = ref->name; + + if (git_reference_is_branch(ref)) { + branch_name += strlen(GIT_REFS_HEADS_DIR); + } else if (git_reference_is_remote(ref)) { + branch_name += strlen(GIT_REFS_REMOTES_DIR); + } else { + giterr_set(GITERR_INVALID, + "Reference '%s' is neither a local nor a remote branch.", ref->name); return -1; + } + *out = branch_name; + return 0; +} - filter.branchlist = &branchlist; - filter.branch_type = list_flags; +static int retrieve_upstream_configuration( + const char **out, + git_repository *repo, + const char *canonical_branch_name, + const char *format) +{ + git_config *config; + git_buf buf = GIT_BUF_INIT; + int error; - error = git_reference_foreach(repo, GIT_REF_LISTALL, &branch_list_cb, (void *)&filter); - if (error < 0) { - git_vector_free(&branchlist); + if (git_repository_config__weakptr(&config, repo) < 0) return -1; + + if (git_buf_printf(&buf, format, + canonical_branch_name + strlen(GIT_REFS_HEADS_DIR)) < 0) + return -1; + + error = git_config_get_string(out, config, git_buf_cstr(&buf)); + git_buf_free(&buf); + return error; +} + +int git_branch_upstream__name( + git_buf *tracking_name, + git_repository *repo, + const char *canonical_branch_name) +{ + const char *remote_name, *merge_name; + git_buf buf = GIT_BUF_INIT; + int error = -1; + git_remote *remote = NULL; + const git_refspec *refspec; + + assert(tracking_name && canonical_branch_name); + + if (!git_reference__is_branch(canonical_branch_name)) + return not_a_local_branch(canonical_branch_name); + + if ((error = retrieve_upstream_configuration( + &remote_name, repo, canonical_branch_name, "branch.%s.remote")) < 0) + goto cleanup; + + if ((error = retrieve_upstream_configuration( + &merge_name, repo, canonical_branch_name, "branch.%s.merge")) < 0) + goto cleanup; + + if (!*remote_name || !*merge_name) { + error = GIT_ENOTFOUND; + goto cleanup; } - branch_names->strings = (char **)branchlist.contents; - branch_names->count = branchlist.length; - return 0; + if (strcmp(".", remote_name) != 0) { + if ((error = git_remote_load(&remote, repo, remote_name)) < 0) + goto cleanup; + + refspec = git_remote_fetchspec(remote); + if (refspec == NULL + || refspec->src == NULL + || refspec->dst == NULL) { + error = GIT_ENOTFOUND; + goto cleanup; + } + + if (git_refspec_transform_r(&buf, refspec, merge_name) < 0) + goto cleanup; + } else + if (git_buf_sets(&buf, merge_name) < 0) + goto cleanup; + + error = git_buf_set(tracking_name, git_buf_cstr(&buf), git_buf_len(&buf)); + +cleanup: + git_remote_free(remote); + git_buf_free(&buf); + return error; } -int git_branch_move(git_repository *repo, const char *old_branch_name, const char *new_branch_name, int force) +static int remote_name(git_buf *buf, git_repository *repo, const char *canonical_branch_name) { - git_reference *reference = NULL; - git_buf old_reference_name = GIT_BUF_INIT, new_reference_name = GIT_BUF_INIT; + git_strarray remote_list = {0}; + size_t i; + git_remote *remote; + const git_refspec *fetchspec; int error = 0; + char *remote_name = NULL; + + assert(buf && repo && canonical_branch_name); - if ((error = git_buf_joinpath(&old_reference_name, GIT_REFS_HEADS_DIR, old_branch_name)) < 0) + /* Verify that this is a remote branch */ + if (!git_reference__is_remote(canonical_branch_name)) { + giterr_set(GITERR_INVALID, "Reference '%s' is not a remote branch.", + canonical_branch_name); + error = GIT_ERROR; goto cleanup; + } - /* We need to be able to return GIT_ENOTFOUND */ - if ((error = git_reference_lookup(&reference, repo, git_buf_cstr(&old_reference_name))) < 0) + /* Get the remotes */ + if ((error = git_remote_list(&remote_list, repo)) < 0) goto cleanup; - if ((error = git_buf_joinpath(&new_reference_name, GIT_REFS_HEADS_DIR, new_branch_name)) < 0) + /* Find matching remotes */ + for (i = 0; i < remote_list.count; i++) { + if ((error = git_remote_load(&remote, repo, remote_list.strings[i])) < 0) + continue; + + fetchspec = git_remote_fetchspec(remote); + + /* Defensivly check that we have a fetchspec */ + if (fetchspec && + git_refspec_dst_matches(fetchspec, canonical_branch_name)) { + /* If we have not already set out yet, then set + * it to the matching remote name. Otherwise + * multiple remotes match this reference, and it + * is ambiguous. */ + if (!remote_name) { + remote_name = remote_list.strings[i]; + } else { + git_remote_free(remote); + error = GIT_EAMBIGUOUS; + goto cleanup; + } + } + + git_remote_free(remote); + } + + if (remote_name) { + git_buf_clear(buf); + error = git_buf_puts(buf, remote_name); + } else { + error = GIT_ENOTFOUND; + } + +cleanup: + git_strarray_free(&remote_list); + return error; +} + +int git_branch_remote_name(char *buffer, size_t buffer_len, git_repository *repo, const char *refname) +{ + int ret; + git_buf buf = GIT_BUF_INIT; + + if ((ret = remote_name(&buf, repo, refname)) < 0) + return ret; + + if (buffer) + git_buf_copy_cstr(buffer, buffer_len, &buf); + + ret = git_buf_len(&buf) + 1; + git_buf_free(&buf); + + return ret; +} + +int git_branch_upstream_name( + char *tracking_branch_name_out, + size_t buffer_size, + git_repository *repo, + const char *canonical_branch_name) +{ + git_buf buf = GIT_BUF_INIT; + int error; + + assert(canonical_branch_name); + + if (tracking_branch_name_out && buffer_size) + *tracking_branch_name_out = '\0'; + + if ((error = git_branch_upstream__name( + &buf, repo, canonical_branch_name)) < 0) + goto cleanup; + + if (tracking_branch_name_out && buf.size + 1 > buffer_size) { /* +1 for NUL byte */ + giterr_set( + GITERR_INVALID, + "Buffer too short to hold the tracked reference name."); + error = -1; goto cleanup; + } + + if (tracking_branch_name_out) + git_buf_copy_cstr(tracking_branch_name_out, buffer_size, &buf); - error = git_reference_rename(reference, git_buf_cstr(&new_reference_name), force); + error = (int)buf.size + 1; cleanup: - git_reference_free(reference); - git_buf_free(&old_reference_name); - git_buf_free(&new_reference_name); + git_buf_free(&buf); + return (int)error; +} + +int git_branch_upstream( + git_reference **tracking_out, + git_reference *branch) +{ + int error; + git_buf tracking_name = GIT_BUF_INIT; + + if ((error = git_branch_upstream__name(&tracking_name, + git_reference_owner(branch), git_reference_name(branch))) < 0) + return error; + error = git_reference_lookup( + tracking_out, + git_reference_owner(branch), + git_buf_cstr(&tracking_name)); + + git_buf_free(&tracking_name); return error; } + +static int unset_upstream(git_config *config, const char *shortname) +{ + git_buf buf = GIT_BUF_INIT; + + if (git_buf_printf(&buf, "branch.%s.remote", shortname) < 0) + return -1; + + if (git_config_delete_entry(config, git_buf_cstr(&buf)) < 0) + goto on_error; + + git_buf_clear(&buf); + if (git_buf_printf(&buf, "branch.%s.merge", shortname) < 0) + goto on_error; + + if (git_config_delete_entry(config, git_buf_cstr(&buf)) < 0) + goto on_error; + + git_buf_free(&buf); + return 0; + +on_error: + git_buf_free(&buf); + return -1; +} + +int git_branch_set_upstream(git_reference *branch, const char *upstream_name) +{ + git_buf key = GIT_BUF_INIT, value = GIT_BUF_INIT; + git_reference *upstream; + git_repository *repo; + git_remote *remote = NULL; + git_config *config; + const char *name, *shortname; + int local; + const git_refspec *fetchspec; + + name = git_reference_name(branch); + if (!git_reference__is_branch(name)) + return not_a_local_branch(name); + + if (git_repository_config__weakptr(&config, git_reference_owner(branch)) < 0) + return -1; + + shortname = name + strlen(GIT_REFS_HEADS_DIR); + + if (upstream_name == NULL) + return unset_upstream(config, shortname); + + repo = git_reference_owner(branch); + + /* First we need to figure out whether it's a branch or remote-tracking */ + if (git_branch_lookup(&upstream, repo, upstream_name, GIT_BRANCH_LOCAL) == 0) + local = 1; + else if (git_branch_lookup(&upstream, repo, upstream_name, GIT_BRANCH_REMOTE) == 0) + local = 0; + else + return GIT_ENOTFOUND; + + /* + * If it's local, the remote is "." and the branch name is + * simply the refname. Otherwise we need to figure out what + * the remote-tracking branch's name on the remote is and use + * that. + */ + if (local) + git_buf_puts(&value, "."); + else + remote_name(&value, repo, git_reference_name(upstream)); + + if (git_buf_printf(&key, "branch.%s.remote", shortname) < 0) + goto on_error; + + if (git_config_set_string(config, git_buf_cstr(&key), git_buf_cstr(&value)) < 0) + goto on_error; + + if (local) { + if (git_buf_puts(&value, git_reference_name(branch)) < 0) + goto on_error; + } else { + /* Get the remoe-tracking branch's refname in its repo */ + if (git_remote_load(&remote, repo, git_buf_cstr(&value)) < 0) + goto on_error; + + fetchspec = git_remote_fetchspec(remote); + git_buf_clear(&value); + if (git_refspec_transform_l(&value, fetchspec, git_reference_name(upstream)) < 0) + goto on_error; + + git_remote_free(remote); + remote = NULL; + } + + git_buf_clear(&key); + if (git_buf_printf(&key, "branch.%s.merge", shortname) < 0) + goto on_error; + + if (git_config_set_string(config, git_buf_cstr(&key), git_buf_cstr(&value)) < 0) + goto on_error; + + git_reference_free(upstream); + git_buf_free(&key); + git_buf_free(&value); + + return 0; + +on_error: + git_reference_free(upstream); + git_buf_free(&key); + git_buf_free(&value); + git_remote_free(remote); + + return -1; +} + +int git_branch_is_head( + git_reference *branch) +{ + git_reference *head; + bool is_same = false; + int error; + + assert(branch); + + if (!git_reference_is_branch(branch)) + return false; + + error = git_repository_head(&head, git_reference_owner(branch)); + + if (error == GIT_EORPHANEDHEAD || error == GIT_ENOTFOUND) + return false; + + if (error < 0) + return -1; + + is_same = strcmp( + git_reference_name(branch), + git_reference_name(head)) == 0; + + git_reference_free(head); + + return is_same; +} |