diff options
Diffstat (limited to 'src/libgit2/transports/local.c')
-rw-r--r-- | src/libgit2/transports/local.c | 754 |
1 files changed, 754 insertions, 0 deletions
diff --git a/src/libgit2/transports/local.c b/src/libgit2/transports/local.c new file mode 100644 index 000000000..6c754a034 --- /dev/null +++ b/src/libgit2/transports/local.c @@ -0,0 +1,754 @@ +/* + * 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. + */ + +#include "common.h" + +#include "pack-objects.h" +#include "refs.h" +#include "posix.h" +#include "fs_path.h" +#include "repository.h" +#include "odb.h" +#include "push.h" +#include "remote.h" +#include "proxy.h" + +#include "git2/types.h" +#include "git2/net.h" +#include "git2/repository.h" +#include "git2/object.h" +#include "git2/tag.h" +#include "git2/transport.h" +#include "git2/revwalk.h" +#include "git2/odb_backend.h" +#include "git2/pack.h" +#include "git2/commit.h" +#include "git2/revparse.h" +#include "git2/sys/remote.h" + +typedef struct { + git_transport parent; + git_remote *owner; + char *url; + int direction; + git_atomic32 cancelled; + git_repository *repo; + git_remote_connect_options connect_opts; + git_vector refs; + unsigned connected : 1, + have_refs : 1; +} transport_local; + +static void free_head(git_remote_head *head) +{ + git__free(head->name); + git__free(head->symref_target); + git__free(head); +} + +static void free_heads(git_vector *heads) +{ + git_remote_head *head; + size_t i; + + git_vector_foreach(heads, i, head) + free_head(head); + + git_vector_free(heads); +} + +static int add_ref(transport_local *t, const char *name) +{ + const char peeled[] = "^{}"; + git_reference *ref, *resolved; + git_remote_head *head; + git_oid obj_id; + git_object *obj = NULL, *target = NULL; + git_str buf = GIT_STR_INIT; + int error; + + if ((error = git_reference_lookup(&ref, t->repo, name)) < 0) + return error; + + error = git_reference_resolve(&resolved, ref); + if (error < 0) { + git_reference_free(ref); + if (!strcmp(name, GIT_HEAD_FILE) && error == GIT_ENOTFOUND) { + /* This is actually okay. Empty repos often have a HEAD that + * points to a nonexistent "refs/heads/master". */ + git_error_clear(); + return 0; + } + return error; + } + + git_oid_cpy(&obj_id, git_reference_target(resolved)); + git_reference_free(resolved); + + head = git__calloc(1, sizeof(git_remote_head)); + GIT_ERROR_CHECK_ALLOC(head); + + head->name = git__strdup(name); + GIT_ERROR_CHECK_ALLOC(head->name); + + git_oid_cpy(&head->oid, &obj_id); + + if (git_reference_type(ref) == GIT_REFERENCE_SYMBOLIC) { + head->symref_target = git__strdup(git_reference_symbolic_target(ref)); + GIT_ERROR_CHECK_ALLOC(head->symref_target); + } + git_reference_free(ref); + + if ((error = git_vector_insert(&t->refs, head)) < 0) { + free_head(head); + return error; + } + + /* If it's not a tag, we don't need to try to peel it */ + if (git__prefixcmp(name, GIT_REFS_TAGS_DIR)) + return 0; + + if ((error = git_object_lookup(&obj, t->repo, &head->oid, GIT_OBJECT_ANY)) < 0) + return error; + + head = NULL; + + /* If it's not an annotated tag, or if we're mocking + * git-receive-pack, just get out */ + if (git_object_type(obj) != GIT_OBJECT_TAG || + t->direction != GIT_DIRECTION_FETCH) { + git_object_free(obj); + return 0; + } + + /* And if it's a tag, peel it, and add it to the list */ + head = git__calloc(1, sizeof(git_remote_head)); + GIT_ERROR_CHECK_ALLOC(head); + + if (git_str_join(&buf, 0, name, peeled) < 0) { + free_head(head); + return -1; + } + head->name = git_str_detach(&buf); + + if (!(error = git_tag_peel(&target, (git_tag *)obj))) { + git_oid_cpy(&head->oid, git_object_id(target)); + + if ((error = git_vector_insert(&t->refs, head)) < 0) { + free_head(head); + } + } + + git_object_free(obj); + git_object_free(target); + + return error; +} + +static int store_refs(transport_local *t) +{ + size_t i; + git_remote_head *head; + git_strarray ref_names = {0}; + + GIT_ASSERT_ARG(t); + + if (git_reference_list(&ref_names, t->repo) < 0) + goto on_error; + + /* Clear all heads we might have fetched in a previous connect */ + git_vector_foreach(&t->refs, i, head) { + git__free(head->name); + git__free(head); + } + + /* Clear the vector so we can reuse it */ + git_vector_clear(&t->refs); + + /* Sort the references first */ + git__tsort((void **)ref_names.strings, ref_names.count, &git__strcmp_cb); + + /* Add HEAD iff direction is fetch */ + if (t->direction == GIT_DIRECTION_FETCH && add_ref(t, GIT_HEAD_FILE) < 0) + goto on_error; + + for (i = 0; i < ref_names.count; ++i) { + if (add_ref(t, ref_names.strings[i]) < 0) + goto on_error; + } + + t->have_refs = 1; + git_strarray_dispose(&ref_names); + return 0; + +on_error: + git_vector_free(&t->refs); + git_strarray_dispose(&ref_names); + return -1; +} + +/* + * Try to open the url as a git directory. The direction doesn't + * matter in this case because we're calculating the heads ourselves. + */ +static int local_connect( + git_transport *transport, + const char *url, + int direction, + const git_remote_connect_options *connect_opts) +{ + git_repository *repo; + int error; + transport_local *t = (transport_local *)transport; + const char *path; + git_str buf = GIT_STR_INIT; + + if (t->connected) + return 0; + + if (git_remote_connect_options_normalize(&t->connect_opts, t->owner->repo, connect_opts) < 0) + return -1; + + free_heads(&t->refs); + + t->url = git__strdup(url); + GIT_ERROR_CHECK_ALLOC(t->url); + t->direction = direction; + + /* 'url' may be a url or path; convert to a path */ + if ((error = git_fs_path_from_url_or_path(&buf, url)) < 0) { + git_str_dispose(&buf); + return error; + } + path = git_str_cstr(&buf); + + error = git_repository_open(&repo, path); + + git_str_dispose(&buf); + + if (error < 0) + return -1; + + t->repo = repo; + + if (store_refs(t) < 0) + return -1; + + t->connected = 1; + + return 0; +} + +static int local_set_connect_opts( + git_transport *transport, + const git_remote_connect_options *connect_opts) +{ + transport_local *t = (transport_local *)transport; + + if (!t->connected) { + git_error_set(GIT_ERROR_NET, "cannot reconfigure a transport that is not connected"); + return -1; + } + + return git_remote_connect_options_normalize(&t->connect_opts, t->owner->repo, connect_opts); +} + +static int local_capabilities(unsigned int *capabilities, git_transport *transport) +{ + GIT_UNUSED(transport); + + *capabilities = GIT_REMOTE_CAPABILITY_TIP_OID | + GIT_REMOTE_CAPABILITY_REACHABLE_OID; + return 0; +} + +static int local_ls(const git_remote_head ***out, size_t *size, git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + if (!t->have_refs) { + git_error_set(GIT_ERROR_NET, "the transport has not yet loaded the refs"); + return -1; + } + + *out = (const git_remote_head **)t->refs.contents; + *size = t->refs.length; + + return 0; +} + +static int local_negotiate_fetch( + git_transport *transport, + git_repository *repo, + const git_remote_head * const *refs, + size_t count) +{ + transport_local *t = (transport_local*)transport; + git_remote_head *rhead; + unsigned int i; + + GIT_UNUSED(refs); + GIT_UNUSED(count); + + /* Fill in the loids */ + git_vector_foreach(&t->refs, i, rhead) { + git_object *obj; + + int error = git_revparse_single(&obj, repo, rhead->name); + if (!error) + git_oid_cpy(&rhead->loid, git_object_id(obj)); + else if (error != GIT_ENOTFOUND) + return error; + else + git_error_clear(); + git_object_free(obj); + } + + return 0; +} + +static int local_push_update_remote_ref( + git_repository *remote_repo, + const char *lref, + const char *rref, + git_oid *loid, + git_oid *roid) +{ + int error; + git_reference *remote_ref = NULL; + + /* check for lhs, if it's empty it means to delete */ + if (lref[0] != '\0') { + /* Create or update a ref */ + error = git_reference_create(NULL, remote_repo, rref, loid, + !git_oid_is_zero(roid), NULL); + } else { + /* Delete a ref */ + if ((error = git_reference_lookup(&remote_ref, remote_repo, rref)) < 0) { + if (error == GIT_ENOTFOUND) + error = 0; + return error; + } + + error = git_reference_delete(remote_ref); + git_reference_free(remote_ref); + } + + return error; +} + +static int transfer_to_push_transfer(const git_indexer_progress *stats, void *payload) +{ + const git_remote_callbacks *cbs = payload; + + if (!cbs || !cbs->push_transfer_progress) + return 0; + + return cbs->push_transfer_progress(stats->received_objects, stats->total_objects, stats->received_bytes, + cbs->payload); +} + +static int local_push( + git_transport *transport, + git_push *push) +{ + transport_local *t = (transport_local *)transport; + git_remote_callbacks *cbs = &t->connect_opts.callbacks; + git_repository *remote_repo = NULL; + push_spec *spec; + char *url = NULL; + const char *path; + git_str buf = GIT_STR_INIT, odb_path = GIT_STR_INIT; + int error; + size_t j; + + /* 'push->remote->url' may be a url or path; convert to a path */ + if ((error = git_fs_path_from_url_or_path(&buf, push->remote->url)) < 0) { + git_str_dispose(&buf); + return error; + } + path = git_str_cstr(&buf); + + error = git_repository_open(&remote_repo, path); + + git_str_dispose(&buf); + + if (error < 0) + return error; + + /* We don't currently support pushing locally to non-bare repos. Proper + non-bare repo push support would require checking configs to see if + we should override the default 'don't let this happen' behavior. + + Note that this is only an issue when pushing to the current branch, + but we forbid all pushes just in case */ + if (!remote_repo->is_bare) { + error = GIT_EBAREREPO; + git_error_set(GIT_ERROR_INVALID, "local push doesn't (yet) support pushing to non-bare repos."); + goto on_error; + } + + if ((error = git_repository__item_path(&odb_path, remote_repo, GIT_REPOSITORY_ITEM_OBJECTS)) < 0 + || (error = git_str_joinpath(&odb_path, odb_path.ptr, "pack")) < 0) + goto on_error; + + error = git_packbuilder_write(push->pb, odb_path.ptr, 0, transfer_to_push_transfer, (void *) cbs); + git_str_dispose(&odb_path); + + if (error < 0) + goto on_error; + + push->unpack_ok = 1; + + git_vector_foreach(&push->specs, j, spec) { + push_status *status; + const git_error *last; + char *ref = spec->refspec.dst; + + status = git__calloc(1, sizeof(push_status)); + if (!status) + goto on_error; + + status->ref = git__strdup(ref); + if (!status->ref) { + git_push_status_free(status); + goto on_error; + } + + error = local_push_update_remote_ref(remote_repo, spec->refspec.src, spec->refspec.dst, + &spec->loid, &spec->roid); + + switch (error) { + case GIT_OK: + break; + case GIT_EINVALIDSPEC: + status->msg = git__strdup("funny refname"); + break; + case GIT_ENOTFOUND: + status->msg = git__strdup("Remote branch not found to delete"); + break; + default: + last = git_error_last(); + + if (last && last->message) + status->msg = git__strdup(last->message); + else + status->msg = git__strdup("Unspecified error encountered"); + break; + } + + /* failed to allocate memory for a status message */ + if (error < 0 && !status->msg) { + git_push_status_free(status); + goto on_error; + } + + /* failed to insert the ref update status */ + if ((error = git_vector_insert(&push->status, status)) < 0) { + git_push_status_free(status); + goto on_error; + } + } + + if (push->specs.length) { + url = git__strdup(t->url); + + if (!url || t->parent.close(&t->parent) < 0 || + t->parent.connect(&t->parent, url, + GIT_DIRECTION_PUSH, NULL)) + goto on_error; + } + + error = 0; + +on_error: + git_repository_free(remote_repo); + git__free(url); + + return error; +} + +typedef struct foreach_data { + git_indexer_progress *stats; + git_indexer_progress_cb progress_cb; + void *progress_payload; + git_odb_writepack *writepack; +} foreach_data; + +static int foreach_cb(void *buf, size_t len, void *payload) +{ + foreach_data *data = (foreach_data*)payload; + + data->stats->received_bytes += len; + return data->writepack->append(data->writepack, buf, len, data->stats); +} + +static const char *counting_objects_fmt = "Counting objects %d\r"; +static const char *compressing_objects_fmt = "Compressing objects: %.0f%% (%d/%d)"; + +static int local_counting(int stage, unsigned int current, unsigned int total, void *payload) +{ + git_str progress_info = GIT_STR_INIT; + transport_local *t = payload; + int error; + + if (!t->connect_opts.callbacks.sideband_progress) + return 0; + + if (stage == GIT_PACKBUILDER_ADDING_OBJECTS) { + git_str_printf(&progress_info, counting_objects_fmt, current); + } else if (stage == GIT_PACKBUILDER_DELTAFICATION) { + float perc = (((float) current) / total) * 100; + git_str_printf(&progress_info, compressing_objects_fmt, perc, current, total); + if (current == total) + git_str_printf(&progress_info, ", done\n"); + else + git_str_putc(&progress_info, '\r'); + + } + + if (git_str_oom(&progress_info)) + return -1; + + if (progress_info.size > INT_MAX) { + git_error_set(GIT_ERROR_NET, "remote sent overly large progress data"); + git_str_dispose(&progress_info); + return -1; + } + + + error = t->connect_opts.callbacks.sideband_progress( + progress_info.ptr, + (int)progress_info.size, + t->connect_opts.callbacks.payload); + + git_str_dispose(&progress_info); + return error; +} + +static int foreach_reference_cb(git_reference *reference, void *payload) +{ + git_revwalk *walk = (git_revwalk *)payload; + int error; + + if (git_reference_type(reference) != GIT_REFERENCE_DIRECT) { + git_reference_free(reference); + return 0; + } + + error = git_revwalk_hide(walk, git_reference_target(reference)); + /* The reference is in the local repository, so the target may not + * exist on the remote. It also may not be a commit. */ + if (error == GIT_ENOTFOUND || error == GIT_ERROR_INVALID) { + git_error_clear(); + error = 0; + } + + git_reference_free(reference); + + return error; +} + +static int local_download_pack( + git_transport *transport, + git_repository *repo, + git_indexer_progress *stats) +{ + transport_local *t = (transport_local*)transport; + git_revwalk *walk = NULL; + git_remote_head *rhead; + unsigned int i; + int error = -1; + git_packbuilder *pack = NULL; + git_odb_writepack *writepack = NULL; + git_odb *odb = NULL; + git_str progress_info = GIT_STR_INIT; + foreach_data data = {0}; + + if ((error = git_revwalk_new(&walk, t->repo)) < 0) + goto cleanup; + + git_revwalk_sorting(walk, GIT_SORT_TIME); + + if ((error = git_packbuilder_new(&pack, t->repo)) < 0) + goto cleanup; + + git_packbuilder_set_callbacks(pack, local_counting, t); + + stats->total_objects = 0; + stats->indexed_objects = 0; + stats->received_objects = 0; + stats->received_bytes = 0; + + git_vector_foreach(&t->refs, i, rhead) { + git_object *obj; + if ((error = git_object_lookup(&obj, t->repo, &rhead->oid, GIT_OBJECT_ANY)) < 0) + goto cleanup; + + if (git_object_type(obj) == GIT_OBJECT_COMMIT) { + /* Revwalker includes only wanted commits */ + error = git_revwalk_push(walk, &rhead->oid); + } else { + /* Tag or some other wanted object. Add it on its own */ + error = git_packbuilder_insert_recur(pack, &rhead->oid, rhead->name); + } + git_object_free(obj); + if (error < 0) + goto cleanup; + } + + if ((error = git_reference_foreach(repo, foreach_reference_cb, walk))) + goto cleanup; + + if ((error = git_packbuilder_insert_walk(pack, walk))) + goto cleanup; + + if (t->connect_opts.callbacks.sideband_progress) { + if ((error = git_str_printf( + &progress_info, + counting_objects_fmt, + git_packbuilder_object_count(pack))) < 0 || + (error = t->connect_opts.callbacks.sideband_progress( + progress_info.ptr, + (int)progress_info.size, + t->connect_opts.callbacks.payload)) < 0) + goto cleanup; + } + + /* Walk the objects, building a packfile */ + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0) + goto cleanup; + + /* One last one with the newline */ + if (t->connect_opts.callbacks.sideband_progress) { + git_str_clear(&progress_info); + + if ((error = git_str_printf( + &progress_info, + counting_objects_fmt, + git_packbuilder_object_count(pack))) < 0 || + (error = git_str_putc(&progress_info, '\n')) < 0 || + (error = t->connect_opts.callbacks.sideband_progress( + progress_info.ptr, + (int)progress_info.size, + t->connect_opts.callbacks.payload)) < 0) + goto cleanup; + } + + if ((error = git_odb_write_pack( + &writepack, + odb, + t->connect_opts.callbacks.transfer_progress, + t->connect_opts.callbacks.payload)) < 0) + goto cleanup; + + /* Write the data to the ODB */ + data.stats = stats; + data.progress_cb = t->connect_opts.callbacks.transfer_progress; + data.progress_payload = t->connect_opts.callbacks.payload; + data.writepack = writepack; + + /* autodetect */ + git_packbuilder_set_threads(pack, 0); + + if ((error = git_packbuilder_foreach(pack, foreach_cb, &data)) != 0) + goto cleanup; + + error = writepack->commit(writepack, stats); + +cleanup: + if (writepack) writepack->free(writepack); + git_str_dispose(&progress_info); + git_packbuilder_free(pack); + git_revwalk_free(walk); + return error; +} + +static int local_is_connected(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + return t->connected; +} + +static void local_cancel(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + git_atomic32_set(&t->cancelled, 1); +} + +static int local_close(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + t->connected = 0; + + if (t->repo) { + git_repository_free(t->repo); + t->repo = NULL; + } + + if (t->url) { + git__free(t->url); + t->url = NULL; + } + + return 0; +} + +static void local_free(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + free_heads(&t->refs); + + /* Close the transport, if it's still open. */ + local_close(transport); + + /* Free the transport */ + git__free(t); +} + +/************** + * Public API * + **************/ + +int git_transport_local(git_transport **out, git_remote *owner, void *param) +{ + int error; + transport_local *t; + + GIT_UNUSED(param); + + t = git__calloc(1, sizeof(transport_local)); + GIT_ERROR_CHECK_ALLOC(t); + + t->parent.version = GIT_TRANSPORT_VERSION; + t->parent.connect = local_connect; + t->parent.set_connect_opts = local_set_connect_opts; + t->parent.capabilities = local_capabilities; + t->parent.negotiate_fetch = local_negotiate_fetch; + t->parent.download_pack = local_download_pack; + t->parent.push = local_push; + t->parent.close = local_close; + t->parent.free = local_free; + t->parent.ls = local_ls; + t->parent.is_connected = local_is_connected; + t->parent.cancel = local_cancel; + + if ((error = git_vector_init(&t->refs, 0, NULL)) < 0) { + git__free(t); + return error; + } + + t->owner = owner; + + *out = (git_transport *) t; + + return 0; +} |