/* * The MIT License * * Copyright (c) 2014 GitHub, Inc * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "rugged.h" extern VALUE rb_mRugged; extern VALUE rb_cRuggedRepo; extern VALUE rb_eRuggedError; VALUE rb_cRuggedRemote; #define RUGGED_REMOTE_CALLBACKS_INIT {1, progress_cb, NULL, credentials_cb, NULL, transfer_progress_cb, update_tips_cb, NULL, NULL, push_update_reference_cb, NULL} static int progress_cb(const char *str, int len, void *data) { struct rugged_remote_cb_payload *payload = data; VALUE args = rb_ary_new2(2); if (NIL_P(payload->progress)) return 0; rb_ary_push(args, payload->progress); rb_ary_push(args, rb_str_new(str, len)); rb_protect(rugged__block_yield_splat, args, &payload->exception); return payload->exception ? GIT_ERROR : GIT_OK; } static int transfer_progress_cb(const git_transfer_progress *stats, void *data) { struct rugged_remote_cb_payload *payload = data; VALUE args = rb_ary_new2(5); if (NIL_P(payload->transfer_progress)) return 0; rb_ary_push(args, payload->transfer_progress); rb_ary_push(args, UINT2NUM(stats->total_objects)); rb_ary_push(args, UINT2NUM(stats->indexed_objects)); rb_ary_push(args, UINT2NUM(stats->received_objects)); rb_ary_push(args, UINT2NUM(stats->local_objects)); rb_ary_push(args, UINT2NUM(stats->total_deltas)); rb_ary_push(args, UINT2NUM(stats->indexed_deltas)); rb_ary_push(args, INT2FIX(stats->received_bytes)); rb_protect(rugged__block_yield_splat, args, &payload->exception); return payload->exception ? GIT_ERROR : GIT_OK; } static int push_update_reference_cb(const char *refname, const char *status, void *data) { struct rugged_remote_cb_payload *payload = data; if (status != NULL) rb_hash_aset(payload->result, rb_str_new_utf8(refname), rb_str_new_utf8(status)); return GIT_OK; } static int update_tips_cb(const char *refname, const git_oid *src, const git_oid *dest, void *data) { struct rugged_remote_cb_payload *payload = data; VALUE args = rb_ary_new2(4); if (NIL_P(payload->update_tips)) return 0; rb_ary_push(args, payload->update_tips); rb_ary_push(args, rb_str_new_utf8(refname)); rb_ary_push(args, git_oid_iszero(src) ? Qnil : rugged_create_oid(src)); rb_ary_push(args, git_oid_iszero(dest) ? Qnil : rugged_create_oid(dest)); rb_protect(rugged__block_yield_splat, args, &payload->exception); return payload->exception ? GIT_ERROR : GIT_OK; } struct extract_cred_args { VALUE rb_callback; git_cred **cred; const char *url; const char *username_from_url; unsigned int allowed_types; }; static VALUE allowed_types_to_rb_ary(int allowed_types) { VALUE rb_allowed_types = rb_ary_new(); if (allowed_types & GIT_CREDTYPE_USERPASS_PLAINTEXT) rb_ary_push(rb_allowed_types, CSTR2SYM("plaintext")); if (allowed_types & GIT_CREDTYPE_SSH_KEY) rb_ary_push(rb_allowed_types, CSTR2SYM("ssh_key")); if (allowed_types & GIT_CREDTYPE_DEFAULT) rb_ary_push(rb_allowed_types, CSTR2SYM("default")); return rb_allowed_types; } static VALUE extract_cred(VALUE data) { struct extract_cred_args *args = (struct extract_cred_args*)data; VALUE rb_url, rb_username_from_url, rb_cred; rb_url = args->url ? rb_str_new2(args->url) : Qnil; rb_username_from_url = args->username_from_url ? rb_str_new2(args->username_from_url) : Qnil; rb_cred = rb_funcall(args->rb_callback, rb_intern("call"), 3, rb_url, rb_username_from_url, allowed_types_to_rb_ary(args->allowed_types)); rugged_cred_extract(args->cred, args->allowed_types, rb_cred); return Qnil; } static int credentials_cb( git_cred **cred, const char *url, const char *username_from_url, unsigned int allowed_types, void *data) { struct rugged_remote_cb_payload *payload = data; struct extract_cred_args args = { payload->credentials, cred, url, username_from_url, allowed_types }; if (NIL_P(payload->credentials)) return GIT_PASSTHROUGH; rb_protect(extract_cred, (VALUE)&args, &payload->exception); return payload->exception ? GIT_ERROR : GIT_OK; } #define CALLABLE_OR_RAISE(ret, rb_options, name) \ do { \ ret = rb_hash_aref(rb_options, CSTR2SYM(name)); \ \ if (!NIL_P(ret) && !rb_respond_to(ret, rb_intern("call"))) \ rb_raise(rb_eArgError, "Expected a Proc or an object that responds to #call (:" name " )."); \ } while (0); void rugged_remote_init_callbacks_and_payload_from_options( VALUE rb_options, git_remote_callbacks *callbacks, struct rugged_remote_cb_payload *payload) { git_remote_callbacks prefilled = RUGGED_REMOTE_CALLBACKS_INIT; prefilled.payload = payload; memcpy(callbacks, &prefilled, sizeof(git_remote_callbacks)); if (!NIL_P(rb_options)) { CALLABLE_OR_RAISE(payload->update_tips, rb_options, "update_tips"); CALLABLE_OR_RAISE(payload->progress, rb_options, "progress"); CALLABLE_OR_RAISE(payload->transfer_progress, rb_options, "transfer_progress"); CALLABLE_OR_RAISE(payload->credentials, rb_options, "credentials"); } } static void init_custom_headers(VALUE rb_options, git_strarray *custom_headers) { if (!NIL_P(rb_options)) { VALUE rb_headers = rb_hash_aref(rb_options, CSTR2SYM("headers")); rugged_rb_ary_to_strarray(rb_headers, custom_headers); } } static int parse_prune_type(VALUE rb_prune_type) { if (rb_prune_type == Qtrue) { return GIT_FETCH_PRUNE; } else if (rb_prune_type == Qfalse) { return GIT_FETCH_NO_PRUNE; } else if (rb_prune_type == Qnil) { return GIT_FETCH_PRUNE_UNSPECIFIED; } else { rb_raise(rb_eTypeError, "wrong argument type for :prune (expected true, false or nil)"); } } static void rb_git_remote__free(git_remote *remote) { git_remote_free(remote); } VALUE rugged_remote_new(VALUE owner, git_remote *remote) { VALUE rb_remote; rb_remote = Data_Wrap_Struct(rb_cRuggedRemote, NULL, &rb_git_remote__free, remote); rugged_set_owner(rb_remote, owner); return rb_remote; } static VALUE rugged_rhead_new(const git_remote_head *head) { VALUE rb_head = rb_hash_new(); rb_hash_aset(rb_head, CSTR2SYM("local?"), head->local ? Qtrue : Qfalse); rb_hash_aset(rb_head, CSTR2SYM("oid"), rugged_create_oid(&head->oid)); rb_hash_aset(rb_head, CSTR2SYM("loid"), git_oid_iszero(&head->loid) ? Qnil : rugged_create_oid(&head->loid)); rb_hash_aset(rb_head, CSTR2SYM("name"), rb_str_new_utf8(head->name)); return rb_head; } /* * call-seq: * remote.ls(options = {}) -> an_enumerator * remote.ls(options = {}) { |remote_head_hash| block } * * Connects +remote+ to list all references available along with their * associated commit ids. * * The given block is called once for each remote head with a Hash containing the * following keys: * * :local? :: * +true+ if the remote head is available locally, +false+ otherwise. * * :oid :: * The id of the object the remote head is currently pointing to. * * :loid :: * The id of the object the local copy of the remote head is currently * pointing to. Set to +nil+ if there is no local copy of the remote head. * * :name :: * The fully qualified reference name of the remote head. * * If no block is given, an enumerator will be returned. * * The following options can be passed in the +options+ Hash: * * :credentials :: * The credentials to use for the ls operation. Can be either an instance of one * of the Rugged::Credentials types, or a proc returning one of the former. * The proc will be called with the +url+, the +username+ from the url (if applicable) and * a list of applicable credential types. * * :headers :: * Extra HTTP headers to include with the request (only applies to http:// or https:// remotes) */ static VALUE rb_git_remote_ls(int argc, VALUE *argv, VALUE self) { git_remote *remote; git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT; git_strarray custom_headers = {0}; const git_remote_head **heads; struct rugged_remote_cb_payload payload = { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil, 0 }; VALUE rb_options; int error; size_t heads_len, i; Data_Get_Struct(self, git_remote, remote); rb_scan_args(argc, argv, ":", &rb_options); if (!rb_block_given_p()) return rb_funcall(self, rb_intern("to_enum"), 2, CSTR2SYM("ls"), rb_options); rugged_remote_init_callbacks_and_payload_from_options(rb_options, &callbacks, &payload); init_custom_headers(rb_options, &custom_headers); if ((error = git_remote_connect(remote, GIT_DIRECTION_FETCH, &callbacks, &custom_headers)) || (error = git_remote_ls(&heads, &heads_len, remote))) goto cleanup; for (i = 0; i < heads_len && !payload.exception; i++) rb_protect(rb_yield, rugged_rhead_new(heads[i]), &payload.exception); cleanup: git_remote_disconnect(remote); git_strarray_free(&custom_headers); if (payload.exception) rb_jump_tag(payload.exception); rugged_exception_check(error); return Qnil; } /* * call-seq: * remote.name() -> string * * Returns the remote's name. * * remote.name #=> "origin" */ static VALUE rb_git_remote_name(VALUE self) { git_remote *remote; const char * name; Data_Get_Struct(self, git_remote, remote); name = git_remote_name(remote); return name ? rb_str_new_utf8(name) : Qnil; } /* * call-seq: * remote.url() -> string * * Returns the remote's url * * remote.url #=> "git://github.com/libgit2/rugged.git" */ static VALUE rb_git_remote_url(VALUE self) { git_remote *remote; Data_Get_Struct(self, git_remote, remote); return rb_str_new_utf8(git_remote_url(remote)); } /* * call-seq: * remote.push_url() -> string or nil * * Returns the remote's url for pushing or nil if no special url for * pushing is set. * * remote.push_url #=> "git://github.com/libgit2/rugged.git" */ static VALUE rb_git_remote_push_url(VALUE self) { git_remote *remote; const char * push_url; Data_Get_Struct(self, git_remote, remote); push_url = git_remote_pushurl(remote); return push_url ? rb_str_new_utf8(push_url) : Qnil; } /* * call-seq: * remote.push_url = url -> url * * Sets the remote's url for pushing without persisting it in the config. * Existing connections will not be updated. * * remote.push_url = 'git@github.com/libgit2/rugged.git' #=> "git@github.com/libgit2/rugged.git" */ static VALUE rb_git_remote_set_push_url(VALUE self, VALUE rb_url) { VALUE rb_repo = rugged_owner(self); git_remote *remote; git_repository *repo; rugged_check_repo(rb_repo); Data_Get_Struct(rb_repo, git_repository, repo); Check_Type(rb_url, T_STRING); Data_Get_Struct(self, git_remote, remote); rugged_exception_check( git_remote_set_pushurl(repo, git_remote_name(remote), StringValueCStr(rb_url)) ); return rb_url; } static VALUE rb_git_remote_refspecs(VALUE self, git_direction direction) { git_remote *remote; int error = 0; git_strarray refspecs; VALUE rb_refspec_array; Data_Get_Struct(self, git_remote, remote); if (direction == GIT_DIRECTION_FETCH) error = git_remote_get_fetch_refspecs(&refspecs, remote); else error = git_remote_get_push_refspecs(&refspecs, remote); rugged_exception_check(error); rb_refspec_array = rugged_strarray_to_rb_ary(&refspecs); git_strarray_free(&refspecs); return rb_refspec_array; } /* * call-seq: * remote.fetch_refspecs -> array * * Get the remote's list of fetch refspecs as +array+. */ static VALUE rb_git_remote_fetch_refspecs(VALUE self) { return rb_git_remote_refspecs(self, GIT_DIRECTION_FETCH); } /* * call-seq: * remote.push_refspecs -> array * * Get the remote's list of push refspecs as +array+. */ static VALUE rb_git_remote_push_refspecs(VALUE self) { return rb_git_remote_refspecs(self, GIT_DIRECTION_PUSH); } /* * call-seq: * remote.check_connection(direction, options = {}) -> boolean * * Try to connect to the +remote+. Useful to simulate * git fetch --dry-run and git push --dry-run. * * Returns +true+ if connection is successful, +false+ otherwise. * * +direction+ must be either +:fetch+ or +:push+. * * The following options can be passed in the +options+ Hash: * * +credentials+ :: * The credentials to use for the connection. Can be either an instance of * one of the Rugged::Credentials types, or a proc returning one of the * former. * The proc will be called with the +url+, the +username+ from the url (if * applicable) and a list of applicable credential types. * * :headers :: * Extra HTTP headers to include with the request (only applies to http:// or https:// remotes) * * Example: * * remote = repo.remotes["origin"] * success = remote.check_connection(:fetch) * raise Error("Unable to pull without credentials") unless success */ static VALUE rb_git_remote_check_connection(int argc, VALUE *argv, VALUE self) { git_remote *remote; git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT; git_strarray custom_headers = {0}; struct rugged_remote_cb_payload payload = { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil, 0 }; VALUE rb_direction, rb_options; ID id_direction; int error, direction; Data_Get_Struct(self, git_remote, remote); rb_scan_args(argc, argv, "01:", &rb_direction, &rb_options); Check_Type(rb_direction, T_SYMBOL); id_direction = SYM2ID(rb_direction); if (id_direction == rb_intern("fetch")) direction = GIT_DIRECTION_FETCH; else if (id_direction == rb_intern("push")) direction = GIT_DIRECTION_PUSH; else rb_raise(rb_eTypeError, "Invalid direction. Expected :fetch or :push"); rugged_remote_init_callbacks_and_payload_from_options(rb_options, &callbacks, &payload); init_custom_headers(rb_options, &custom_headers); error = git_remote_connect(remote, direction, &callbacks, &custom_headers); git_remote_disconnect(remote); git_strarray_free(&custom_headers); if (payload.exception) rb_jump_tag(payload.exception); return error ? Qfalse : Qtrue; } /* * call-seq: * remote.fetch(refspecs = nil, options = {}) -> hash * * Downloads new data from the remote for the given +refspecs+ and updates tips. * * You can optionally pass in a single or multiple alternative +refspecs+ to use instead of the fetch * refspecs already configured for +remote+. * * Returns a hash containing statistics for the fetch operation. * * The following options can be passed in the +options+ Hash: * * :credentials :: * The credentials to use for the fetch operation. Can be either an instance of one * of the Rugged::Credentials types, or a proc returning one of the former. * The proc will be called with the +url+, the +username+ from the url (if applicable) and * a list of applicable credential types. * * :headers :: * Extra HTTP headers to include with the request (only applies to http:// or https:// remotes) * * :progress :: * A callback that will be executed with the textual progress received from the remote. * This is the text send over the progress side-band (ie. the "counting objects" output). * * :transfer_progress :: * A callback that will be executed to report clone progress information. It will be passed * the amount of +total_objects+, +indexed_objects+, +received_objects+, +local_objects+, * +total_deltas+, +indexed_deltas+ and +received_bytes+. * * :update_tips :: * A callback that will be executed each time a reference is updated locally. It will be * passed the +refname+, +old_oid+ and +new_oid+. * * :message :: * The message to insert into the reflogs. Defaults to "fetch". * * :prune :: * Specifies the prune mode for the fetch. +true+ remove any remote-tracking references that * no longer exist, +false+ do not prune, +nil+ use configured settings Defaults to "nil". * * Example: * * remote = Rugged::Remote.lookup(@repo, 'origin') * remote.fetch({ * transfer_progress: lambda { |total_objects, indexed_objects, received_objects, local_objects, total_deltas, indexed_deltas, received_bytes| * # ... * } * }) */ static VALUE rb_git_remote_fetch(int argc, VALUE *argv, VALUE self) { git_remote *remote; git_strarray refspecs; git_fetch_options opts = GIT_FETCH_OPTIONS_INIT; const git_transfer_progress *stats; struct rugged_remote_cb_payload payload = { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil, 0 }; char *log_message = NULL; int error; VALUE rb_options, rb_refspecs, rb_result = Qnil; rb_scan_args(argc, argv, "01:", &rb_refspecs, &rb_options); rugged_rb_ary_to_strarray(rb_refspecs, &refspecs); Data_Get_Struct(self, git_remote, remote); rugged_remote_init_callbacks_and_payload_from_options(rb_options, &opts.callbacks, &payload); init_custom_headers(rb_options, &opts.custom_headers); if (!NIL_P(rb_options)) { VALUE rb_prune_type; VALUE rb_val = rb_hash_aref(rb_options, CSTR2SYM("message")); if (!NIL_P(rb_val)) log_message = StringValueCStr(rb_val); rb_prune_type = rb_hash_aref(rb_options, CSTR2SYM("prune")); opts.prune = parse_prune_type(rb_prune_type); } error = git_remote_fetch(remote, &refspecs, &opts, log_message); xfree(refspecs.strings); git_strarray_free(&opts.custom_headers); if (payload.exception) rb_jump_tag(payload.exception); rugged_exception_check(error); stats = git_remote_stats(remote); rb_result = rb_hash_new(); rb_hash_aset(rb_result, CSTR2SYM("total_objects"), UINT2NUM(stats->total_objects)); rb_hash_aset(rb_result, CSTR2SYM("indexed_objects"), UINT2NUM(stats->indexed_objects)); rb_hash_aset(rb_result, CSTR2SYM("received_objects"), UINT2NUM(stats->received_objects)); rb_hash_aset(rb_result, CSTR2SYM("local_objects"), UINT2NUM(stats->local_objects)); rb_hash_aset(rb_result, CSTR2SYM("total_deltas"), UINT2NUM(stats->total_deltas)); rb_hash_aset(rb_result, CSTR2SYM("indexed_deltas"), UINT2NUM(stats->indexed_deltas)); rb_hash_aset(rb_result, CSTR2SYM("received_bytes"), INT2FIX(stats->received_bytes)); return rb_result; } /* * call-seq: * remote.push(refspecs = nil, options = {}) -> hash * * Pushes the given +refspecs+ to the given +remote+. Returns a hash that contains * key-value pairs that reflect pushed refs and error messages, if applicable. * * You can optionally pass in an alternative list of +refspecs+ to use instead of the push * refspecs already configured for +remote+. * * The following options can be passed in the +options+ Hash: * * :credentials :: * The credentials to use for the push operation. Can be either an instance of one * of the Rugged::Credentials types, or a proc returning one of the former. * The proc will be called with the +url+, the +username+ from the url (if applicable) and * a list of applicable credential types. * * :update_tips :: * A callback that will be executed each time a reference is updated remotely. It will be * passed the +refname+, +old_oid+ and +new_oid+. * * :headers :: * Extra HTTP headers to include with the push (only applies to http:// or https:// remotes) * * Example: * * remote = Rugged::Remote.lookup(@repo, 'origin') * remote.push(["refs/heads/master", ":refs/heads/to_be_deleted"]) */ static VALUE rb_git_remote_push(int argc, VALUE *argv, VALUE self) { VALUE rb_refspecs, rb_options; git_remote *remote; git_strarray refspecs; git_push_options opts = GIT_PUSH_OPTIONS_INIT; int error = 0; struct rugged_remote_cb_payload payload = { Qnil, Qnil, Qnil, Qnil, Qnil, rb_hash_new(), 0 }; rb_scan_args(argc, argv, "01:", &rb_refspecs, &rb_options); rugged_rb_ary_to_strarray(rb_refspecs, &refspecs); Data_Get_Struct(self, git_remote, remote); rugged_remote_init_callbacks_and_payload_from_options(rb_options, &opts.callbacks, &payload); init_custom_headers(rb_options, &opts.custom_headers); error = git_remote_push(remote, &refspecs, &opts); xfree(refspecs.strings); git_strarray_free(&opts.custom_headers); if (payload.exception) rb_jump_tag(payload.exception); rugged_exception_check(error); return payload.result; } void Init_rugged_remote(void) { rb_cRuggedRemote = rb_define_class_under(rb_mRugged, "Remote", rb_cObject); rb_define_method(rb_cRuggedRemote, "name", rb_git_remote_name, 0); rb_define_method(rb_cRuggedRemote, "url", rb_git_remote_url, 0); rb_define_method(rb_cRuggedRemote, "push_url", rb_git_remote_push_url, 0); rb_define_method(rb_cRuggedRemote, "push_url=", rb_git_remote_set_push_url, 1); rb_define_method(rb_cRuggedRemote, "fetch_refspecs", rb_git_remote_fetch_refspecs, 0); rb_define_method(rb_cRuggedRemote, "push_refspecs", rb_git_remote_push_refspecs, 0); rb_define_method(rb_cRuggedRemote, "ls", rb_git_remote_ls, -1); rb_define_method(rb_cRuggedRemote, "check_connection", rb_git_remote_check_connection, -1); rb_define_method(rb_cRuggedRemote, "fetch", rb_git_remote_fetch, -1); rb_define_method(rb_cRuggedRemote, "push", rb_git_remote_push, -1); }