diff --git a/ext/rugged/rugged.c b/ext/rugged/rugged.c index 16aa6832e..79008af58 100644 --- a/ext/rugged/rugged.c +++ b/ext/rugged/rugged.c @@ -299,6 +299,25 @@ void rugged_exception_raise(void) rb_exc_raise(err_obj); } +VALUE rugged__block_yield_splat(VALUE args) { + VALUE block = rb_ary_shift(args); + int n = RARRAY_LENINT(args); + + if (n == 0) { + return rb_funcall(block, rb_intern("call"), 0); + } else { + int i; + VALUE *argv; + argv = ALLOCA_N(VALUE, n); + + for (i=0; i < n; i++) { + argv[i] = rb_ary_entry(args, i); + } + + return rb_funcall2(block, rb_intern("call"), n, argv); + } +} + /* * call-seq: * Rugged.__cache_usage__ -> [current, max] diff --git a/ext/rugged/rugged.h b/ext/rugged/rugged.h index 9c28d05f4..26eb95814 100644 --- a/ext/rugged/rugged.h +++ b/ext/rugged/rugged.h @@ -93,6 +93,8 @@ VALUE rb_git_delta_file_fromC(const git_diff_file *file); void rugged_parse_diff_options(git_diff_options *opts, VALUE rb_options); void rugged_parse_merge_options(git_merge_options *opts, VALUE rb_options); +void rugged_cred_extract(git_cred **cred, int allowed_types, VALUE rb_credential); + VALUE rugged_otype_new(git_otype t); git_otype rugged_otype_get(VALUE rb_type); @@ -131,11 +133,29 @@ static inline int rugged_parse_bool(VALUE boolean) extern VALUE rb_cRuggedRepo; -struct rugged_cb_payload { +VALUE rugged__block_yield_splat(VALUE args); + +struct rugged_cb_payload +{ VALUE rb_data; int exception; }; +struct rugged_remote_cb_payload +{ + VALUE progress; + VALUE completion; + VALUE transfer_progress; + VALUE update_tips; + VALUE credentials; + int exception; +}; + +void rugged_remote_init_callbacks_and_payload_from_options( + VALUE rb_options, + git_remote_callbacks *callbacks, + struct rugged_remote_cb_payload *payload); + static inline void rugged_check_repo(VALUE rb_repo) { if (!rb_obj_is_kind_of(rb_repo, rb_cRuggedRepo)) diff --git a/ext/rugged/rugged_cred.c b/ext/rugged/rugged_cred.c index 39744e1f2..6812b7ce6 100644 --- a/ext/rugged/rugged_cred.c +++ b/ext/rugged/rugged_cred.c @@ -26,15 +26,100 @@ extern VALUE rb_mRugged; VALUE rb_mRuggedCred; -VALUE rb_cRuggedCredPlaintext; +VALUE rb_cRuggedCredUserPassword; VALUE rb_cRuggedCredSshKey; +VALUE rb_cRuggedCredSshKeyFromAgent; VALUE rb_cRuggedCredDefault; +static void rugged_cred_extract_userpass(git_cred **cred, VALUE rb_credential) +{ + VALUE rb_username = rb_iv_get(rb_credential, "@username"); + VALUE rb_password = rb_iv_get(rb_credential, "@password"); + + Check_Type(rb_username, T_STRING); + Check_Type(rb_password, T_STRING); + + rugged_exception_check( + git_cred_userpass_plaintext_new(cred, + StringValueCStr(rb_username), + StringValueCStr(rb_password) + ) + ); +} + +static void rugged_cred_extract_ssh_key(git_cred **cred, VALUE rb_credential) +{ + VALUE rb_username = rb_iv_get(rb_credential, "@username"); + VALUE rb_publickey = rb_iv_get(rb_credential, "@publickey"); + VALUE rb_privatekey = rb_iv_get(rb_credential, "@privatekey"); + VALUE rb_passphrase = rb_iv_get(rb_credential, "@passphrase"); + + Check_Type(rb_username, T_STRING); + Check_Type(rb_privatekey, T_STRING); + + if (!NIL_P(rb_publickey)) + Check_Type(rb_publickey, T_STRING); + if (!NIL_P(rb_passphrase)) + Check_Type(rb_passphrase, T_STRING); + + rugged_exception_check( + git_cred_ssh_key_new(cred, + StringValueCStr(rb_username), + NIL_P(rb_publickey) ? NULL : StringValueCStr(rb_publickey), + StringValueCStr(rb_privatekey), + NIL_P(rb_passphrase) ? NULL : StringValueCStr(rb_passphrase) + ) + ); +} + +static void rugged_credential_extract_ssh_key_from_agent(git_cred **cred, VALUE rb_credential) +{ + VALUE rb_username = rb_iv_get(rb_credential, "@username"); + + Check_Type(rb_username, T_STRING); + + rugged_exception_check( + git_cred_ssh_key_from_agent(cred, StringValueCStr(rb_username)) + ); +} + +static void rugged_cred_extract_default(git_cred **cred, VALUE rb_credential) +{ + rugged_exception_check(git_cred_default_new(cred)); +} + +void rugged_cred_extract(git_cred **cred, int allowed_types, VALUE rb_credential) +{ + if (rb_obj_is_kind_of(rb_credential, rb_cRuggedCredUserPassword)) { + if (!(allowed_types & GIT_CREDTYPE_USERPASS_PLAINTEXT)) + rb_raise(rb_eArgError, "Invalid credential type"); + + rugged_cred_extract_userpass(cred, rb_credential); + } else if (rb_obj_is_kind_of(rb_credential, rb_cRuggedCredSshKey)) { + if (!(allowed_types & GIT_CREDTYPE_SSH_KEY)) + rb_raise(rb_eArgError, "Invalid credential type"); + + rugged_cred_extract_ssh_key(cred, rb_credential); + } else if (rb_obj_is_kind_of(rb_credential, rb_cRuggedCredSshKeyFromAgent)) { + if (!(allowed_types & GIT_CREDTYPE_SSH_KEY)) + rb_raise(rb_eArgError, "Invalid credential type"); + + rugged_credential_extract_ssh_key_from_agent(cred, rb_credential); + } else if (rb_obj_is_kind_of(rb_credential, rb_cRuggedCredDefault)) { + if (!(allowed_types & GIT_CREDTYPE_DEFAULT)) + rb_raise(rb_eArgError, "Invalid credential type"); + + rugged_cred_extract_default(cred, rb_credential); + } +} + + void Init_rugged_cred(void) { - rb_mRuggedCred = rb_define_module_under(rb_mRugged, "Credentials"); + rb_mRuggedCred = rb_define_module_under(rb_mRugged, "Credentials"); - rb_cRuggedCredPlaintext = rb_define_class_under(rb_mRuggedCred, "Plaintext", rb_cObject); - rb_cRuggedCredSshKey = rb_define_class_under(rb_mRuggedCred, "SshKey", rb_cObject); - rb_cRuggedCredDefault = rb_define_class_under(rb_mRuggedCred, "Default", rb_cObject); + rb_cRuggedCredUserPassword = rb_define_class_under(rb_mRuggedCred, "UserPassword", rb_cObject); + rb_cRuggedCredSshKey = rb_define_class_under(rb_mRuggedCred, "SshKey", rb_cObject); + rb_cRuggedCredSshKeyFromAgent = rb_define_class_under(rb_mRuggedCred, "SshKeyFromAgent", rb_cObject); + rb_cRuggedCredDefault = rb_define_class_under(rb_mRuggedCred, "Default", rb_cObject); } diff --git a/ext/rugged/rugged_remote.c b/ext/rugged/rugged_remote.c index e44dddffd..8c292f247 100644 --- a/ext/rugged/rugged_remote.c +++ b/ext/rugged/rugged_remote.c @@ -26,8 +26,150 @@ 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, transfer_progress_cb, update_tips_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 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)); + + 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 rb_git_remote__free(git_remote *remote) { git_remote_free(remote); @@ -149,74 +291,6 @@ static VALUE rb_git_remote_lookup(VALUE klass, VALUE rb_repo, VALUE rb_name) return rugged_remote_new(klass, rb_repo, remote); } -/* - * call-seq: - * remote.disconnect() -> nil - * - * Disconnect from the remote, closes the connection to the remote - */ -static VALUE rb_git_remote_disconnect(VALUE self) -{ - git_remote *remote; - Data_Get_Struct(self, git_remote, remote); - - git_remote_disconnect(remote); - return Qnil; -} - -/* - * call-seq: - * remote.connect(direction) -> nil - * remote.connect(direction) { |connected_remote| block } - * - * Open a connection to a remote: - * - +direction+: +:fetch+ or +:push+ - * - * If a block is given it will be passed the connected remote as argument - * and the remote will be disconnected when the block terminates. - * - * The transport is selected based on the URL. - * - * remote.connect(:fetch) #=> nil - * - * remote.connect(:fetch) do |r| - * r.connected? #=> true - * end - * - */ -static VALUE rb_git_remote_connect(VALUE self, VALUE rb_direction) -{ - int error, direction = 0, exception = 0; - git_remote *remote; - ID id_direction; - - Data_Get_Struct(self, git_remote, remote); - - 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 remote direction. Expected `:fetch` or `:push`"); - - error = git_remote_connect(remote, direction); - rugged_exception_check(error); - - if (rb_block_given_p()) { - rb_protect(rb_yield, self, &exception); - git_remote_disconnect(remote); - - if (exception) - rb_jump_tag(exception); - } - - return Qnil; -} - static VALUE rugged_rhead_new(const git_remote_head *head) { VALUE rb_head = rb_hash_new(); @@ -232,8 +306,8 @@ static VALUE rugged_rhead_new(const git_remote_head *head) /* * call-seq: - * remote.ls() -> an_enumerator - * remote.ls() { |remote_head_hash| block } + * remote.ls(options = {}) -> an_enumerator + * remote.ls(options = {}) { |remote_head_hash| block } * * List references available in a connected +remote+ repository along * with the associated commit IDs. @@ -242,35 +316,58 @@ static VALUE rugged_rhead_new(const git_remote_head *head) * +Hash+. * If no block is given an Enumerator is returned. * - * remote.connect(:fetch) do |r| - * r.ls.to_a #=> [{:local?=>false, :oid=>"b3ee97a91b02e91c35394950bda6ea622044baad", :loid=> nil, :name=>"refs/heads/development"}] - * end + * r.ls.to_a #=> [{:local?=>false, :oid=>"b3ee97a91b02e91c35394950bda6ea622044baad", :loid=> nil, :name=>"refs/heads/development"}] * * remote head hash includes: * [:oid] oid of the remote head * [:name] name of the remote head * + * 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. */ -static VALUE rb_git_remote_ls(VALUE self) +static VALUE rb_git_remote_ls(int argc, VALUE *argv, VALUE self) { - int error, exception = 0; git_remote *remote; + git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT; const git_remote_head **heads; + + struct rugged_remote_cb_payload payload = { 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"), 1, CSTR2SYM("ls")); + return rb_funcall(self, rb_intern("to_enum"), 2, CSTR2SYM("ls"), rb_options); - error = git_remote_ls(&heads, &heads_len, remote); + if (!NIL_P(rb_options)) + rugged_remote_init_callbacks_and_payload_from_options(rb_options, &callbacks, &payload); - for (i = 0; i < heads_len && !exception; i++) { - rb_protect(rb_yield, rugged_rhead_new(heads[i]), &exception); - } + if ((error = git_remote_set_callbacks(remote, &callbacks)) || + (error = git_remote_connect(remote, GIT_DIRECTION_FETCH)) || + (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); + + if (payload.exception) + rb_jump_tag(payload.exception); - if (exception) - rb_jump_tag(exception); rugged_exception_check(error); return Qnil; @@ -472,111 +569,6 @@ static VALUE rb_git_remote_clear_refspecs(VALUE self) return Qnil; } -/* - * call-seq: - * remote.connected? -> true or false - * - * Returns if the remote is connected - * - * remote.connected? #=> false - * remote.connect(:fetch) - * remote.connected? #=> true - */ -static VALUE rb_git_remote_connected(VALUE self) -{ - git_remote *remote; - Data_Get_Struct(self, git_remote, remote); - - return git_remote_connected(remote) ? Qtrue : Qfalse; -} - -/* - * call-seq: - * remote.download() -> nil - * - * Download the packfile from a connected remote - * - * Negotiate what objects should be downloaded and download the - * packfile with those objects. - * - * remote.connect(:fetch) do |r| - * r.download - * end - */ -static VALUE rb_git_remote_download(VALUE self) -{ - int error; - git_remote *remote; - - Data_Get_Struct(self, git_remote, remote); - - error = git_remote_download(remote); - rugged_exception_check(error); - - return Qnil; -} - -static int cb_remote__update_tips(const char *refname, const git_oid *src, const git_oid *dest, void *payload) -{ - VALUE rb_args = rb_ary_new2(3); - int *exception = (int *)payload; - rb_ary_push(rb_args, rb_str_new_utf8(refname)); - rb_ary_push(rb_args, git_oid_iszero(src) ? Qnil : rugged_create_oid(src)); - rb_ary_push(rb_args, git_oid_iszero(dest) ? Qnil : rugged_create_oid(dest)); - - rb_protect(rb_yield_splat, rb_args, exception); - - return *exception ? GIT_ERROR : GIT_OK; -} - -/* - * call-seq: - * remote.update_tips! -> nil - * remote.update_tips! { |reference, source, destination| block } - * - * Update the tips to a new state from a connected remote. The target - * objects must be downloaded before the tips are updated. - * - * r.update_tips! do |ref, src, dst| - * puts "#{ref}: #{src}..#{dst}" - * end - */ -static VALUE rb_git_remote_update_tips(VALUE self) -{ - git_remote *remote; - - Data_Get_Struct(self, git_remote, remote); - - if (rb_block_given_p()) { - int exception = 0, error; - git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT; - - callbacks.payload = &exception; - callbacks.update_tips = &cb_remote__update_tips; - rugged_exception_check( - git_remote_set_callbacks(remote, &callbacks) - ); - - error = git_remote_update_tips(remote, NULL, NULL); - - callbacks.update_tips = NULL; - // Don't overwrite the first error we've seen - if (!error) error = git_remote_set_callbacks(remote, &callbacks); - else git_remote_set_callbacks(remote, &callbacks); - - if (exception) - rb_jump_tag(exception); - - rugged_exception_check(error); - } else { - rugged_exception_check( - git_remote_update_tips(remote, NULL, NULL) - ); - } - - return Qnil; -} - /* * call-seq: * Remote.names(repository) -> array @@ -707,6 +699,276 @@ static VALUE rb_git_remote_rename(VALUE self, VALUE rb_new_name) return RARRAY_LEN(rb_refspec_ary) == 0 ? Qnil : rb_refspec_ary; } +/* + * 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 an alternative list of +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. + * + * :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". + * + * :signature :: + * The signature to be used for updating the reflogs. + * + * 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, *tmp_remote = NULL; + git_repository *repo; + git_signature *signature = NULL; + git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT; + struct rugged_remote_cb_payload payload = { Qnil, Qnil, Qnil, Qnil, Qnil, 0 }; + + char *log_message = NULL; + int error, i; + + VALUE rb_options, rb_refspecs, rb_result = Qnil, rb_repo = rugged_owner(self); + + rb_scan_args(argc, argv, "01:", &rb_refspecs, &rb_options); + + if (!NIL_P(rb_refspecs)) { + Check_Type(rb_refspecs, T_ARRAY); + for (i = 0; i < RARRAY_LEN(rb_refspecs); ++i) { + VALUE rb_refspec = rb_ary_entry(rb_refspecs, i); + Check_Type(rb_refspec, T_STRING); + } + } + + Data_Get_Struct(self, git_remote, remote); + rugged_check_repo(rb_repo); + Data_Get_Struct(rb_repo, git_repository, repo); + + if (!NIL_P(rb_options)) { + VALUE rb_val = rb_hash_aref(rb_options, CSTR2SYM("signature")); + if (!NIL_P(rb_val)) + signature = rugged_signature_get(rb_val, repo); + + rb_val = rb_hash_aref(rb_options, CSTR2SYM("message")); + if (!NIL_P(rb_val)) + log_message = StringValueCStr(rb_val); + + rugged_remote_init_callbacks_and_payload_from_options(rb_options, &callbacks, &payload); + } + + if ((error = git_remote_dup(&tmp_remote, remote)) || + (error = git_remote_set_callbacks(tmp_remote, &callbacks))) + goto cleanup; + + if (!NIL_P(rb_refspecs)) { + git_remote_clear_refspecs(tmp_remote); + for (i = 0; !error && i < RARRAY_LEN(rb_refspecs); ++i) { + VALUE rb_refspec = rb_ary_entry(rb_refspecs, i); + + if ((error = git_remote_add_fetch(tmp_remote, StringValueCStr(rb_refspec)))) + goto cleanup; + } + } + + if ((error = git_remote_fetch(tmp_remote, signature, log_message)) == GIT_OK) { + const git_transfer_progress *stats = git_remote_stats(tmp_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)); + } + + cleanup: + + git_signature_free(signature); + git_remote_free(tmp_remote); + + if (payload.exception) + rb_jump_tag(payload.exception); + + rugged_exception_check(error); + + return rb_result; +} + +static int push_status_cb(const char *ref, const char *msg, void *payload) +{ + VALUE rb_result_hash = (VALUE)payload; + if (msg != NULL) + rb_hash_aset(rb_result_hash, rb_str_new_utf8(ref), rb_str_new_utf8(msg)); + + return GIT_OK; +} + +/* + * 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. + * + * 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. + * + * :message :: + * A single line log message to be appended to the reflog of each local remote-tracking + * branch that gets updated. Defaults to: "fetch". + * + * :signature :: + * The signature to be used for populating the reflog entries. + * + * 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, rb_val; + VALUE rb_repo = rugged_owner(self); + VALUE rb_exception = Qnil, rb_result = rb_hash_new(); + + git_repository *repo; + git_remote *remote, *tmp_remote = NULL; + git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT; + git_push *push = NULL; + git_signature *signature = NULL; + + int error = 0, i = 0; + char *log_message = NULL; + + struct rugged_remote_cb_payload payload = { Qnil, Qnil, Qnil, Qnil, 0 }; + + rb_scan_args(argc, argv, "01:", &rb_refspecs, &rb_options); + + if (!NIL_P(rb_refspecs)) { + Check_Type(rb_refspecs, T_ARRAY); + for (i = 0; i < RARRAY_LEN(rb_refspecs); ++i) { + VALUE rb_refspec = rb_ary_entry(rb_refspecs, i); + Check_Type(rb_refspec, T_STRING); + } + } + + rugged_check_repo(rb_repo); + Data_Get_Struct(rb_repo, git_repository, repo); + Data_Get_Struct(self, git_remote, remote); + + if (!NIL_P(rb_options)) { + rugged_remote_init_callbacks_and_payload_from_options(rb_options, &callbacks, &payload); + + rb_val = rb_hash_aref(rb_options, CSTR2SYM("message")); + if (!NIL_P(rb_val)) + log_message = StringValueCStr(rb_val); + + rb_val = rb_hash_aref(rb_options, CSTR2SYM("signature")); + if (!NIL_P(rb_val)) + signature = rugged_signature_get(rb_val, repo); + } + + // Create a temporary remote that we use for pushing + if ((error = git_remote_dup(&tmp_remote, remote)) || + (error = git_remote_set_callbacks(tmp_remote, &callbacks))) + goto cleanup; + + if (!NIL_P(rb_refspecs)) { + git_remote_clear_refspecs(tmp_remote); + + for (i = 0; !error && i < RARRAY_LEN(rb_refspecs); ++i) { + VALUE rb_refspec = rb_ary_entry(rb_refspecs, i); + + if ((error = git_remote_add_push(tmp_remote, StringValueCStr(rb_refspec)))) + goto cleanup; + } + } + + if ((error = git_push_new(&push, tmp_remote))) + goto cleanup; + + // TODO: Get rid of this once git_remote_push lands in libgit2. + { + git_strarray push_refspecs; + size_t i; + + if ((error = git_remote_get_push_refspecs(&push_refspecs, tmp_remote))) + goto cleanup; + + if (push_refspecs.count == 0) { + rb_exception = rb_exc_new2(rb_eRuggedError, "no pushspecs are configured for the given remote"); + goto cleanup; + } + + for (i = 0; !error && i < push_refspecs.count; ++i) { + error = git_push_add_refspec(push, push_refspecs.strings[i]); + } + + git_strarray_free(&push_refspecs); + if (error) goto cleanup; + } + + if ((error = git_push_finish(push))) + goto cleanup; + + if (!git_push_unpack_ok(push)) { + rb_exception = rb_exc_new2(rb_eRuggedError, "the remote side did not unpack successfully"); + goto cleanup; + } + + if ((error = git_push_status_foreach(push, &push_status_cb, (void *)rb_result)) || + (error = git_push_update_tips(push, signature, log_message))) + goto cleanup; + +cleanup: + git_push_free(push); + git_remote_free(tmp_remote); + git_signature_free(signature); + + if (!NIL_P(rb_exception)) + rb_exc_raise(rb_exception); + + rugged_exception_check(error); + + return rb_result; +} + void Init_rugged_remote(void) { rb_cRuggedRemote = rb_define_class_under(rb_mRugged, "Remote", rb_cObject); @@ -717,8 +979,6 @@ void Init_rugged_remote(void) rb_define_singleton_method(rb_cRuggedRemote, "names", rb_git_remote_names, 1); rb_define_singleton_method(rb_cRuggedRemote, "each", rb_git_remote_each, 1); - rb_define_method(rb_cRuggedRemote, "connect", rb_git_remote_connect, 1); - rb_define_method(rb_cRuggedRemote, "disconnect", rb_git_remote_disconnect, 0); 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, "url=", rb_git_remote_set_url, 1); @@ -728,10 +988,9 @@ void Init_rugged_remote(void) rb_define_method(rb_cRuggedRemote, "push_refspecs", rb_git_remote_push_refspecs, 0); rb_define_method(rb_cRuggedRemote, "add_fetch", rb_git_remote_add_fetch, 1); rb_define_method(rb_cRuggedRemote, "add_push", rb_git_remote_add_push, 1); - rb_define_method(rb_cRuggedRemote, "connected?", rb_git_remote_connected, 0); - rb_define_method(rb_cRuggedRemote, "ls", rb_git_remote_ls, 0); - rb_define_method(rb_cRuggedRemote, "download", rb_git_remote_download, 0); - rb_define_method(rb_cRuggedRemote, "update_tips!", rb_git_remote_update_tips, 0); + rb_define_method(rb_cRuggedRemote, "ls", rb_git_remote_ls, -1); + rb_define_method(rb_cRuggedRemote, "fetch", rb_git_remote_fetch, -1); + rb_define_method(rb_cRuggedRemote, "push", rb_git_remote_push, -1); rb_define_method(rb_cRuggedRemote, "clear_refspecs", rb_git_remote_clear_refspecs, 0); rb_define_method(rb_cRuggedRemote, "save", rb_git_remote_save, 0); rb_define_method(rb_cRuggedRemote, "rename!", rb_git_remote_rename, 1); diff --git a/ext/rugged/rugged_repo.c b/ext/rugged/rugged_repo.c index 7de29ddba..e28976d2a 100644 --- a/ext/rugged/rugged_repo.c +++ b/ext/rugged/rugged_repo.c @@ -289,217 +289,26 @@ static VALUE rb_git_repo_init_at(int argc, VALUE *argv, VALUE klass) return rugged_repo_new(klass, repo); } -static VALUE rugged__block_yield_splat(VALUE args) { - VALUE block = rb_ary_shift(args); - int n = RARRAY_LENINT(args); - - if (n == 0) { - return rb_funcall(block, rb_intern("call"), 0); - } else { - int i; - VALUE *argv; - argv = ALLOCA_N(VALUE, n); - - for (i=0; i < n; i++) { - argv[i] = rb_ary_entry(args, i); - } - - return rb_funcall2(block, rb_intern("call"), n, argv); - } -} - -struct rugged_remote_cb_payload -{ - VALUE progress; - VALUE completion; - VALUE transfer_progress; - VALUE update_tips; - VALUE credentials; - int exception; -}; - -static int rugged__remote_transfer_progress_cb(const git_transfer_progress *stats, void *payload) -{ - struct rugged_remote_cb_payload *remote_payload = payload; - VALUE args = rb_ary_new2(4); - rb_ary_push(args, remote_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, INT2FIX(stats->received_bytes)); - rb_protect(rugged__block_yield_splat, args, &remote_payload->exception); - - return remote_payload->exception ? GIT_ERROR : GIT_OK; -} - -struct extract_cred_payload -{ - VALUE rb_cred; - git_cred **cred; - unsigned int allowed_types; -}; - -static VALUE rugged__extract_cred(VALUE payload) { - struct extract_cred_payload *cred_payload = (struct extract_cred_payload*)payload; - git_cred **cred = cred_payload->cred; - VALUE rb_cred = cred_payload->rb_cred; - - if (rb_obj_is_kind_of(rb_cred, rb_cRuggedCredPlaintext)) { - if (!(cred_payload->allowed_types & GIT_CREDTYPE_USERPASS_PLAINTEXT)) { - rb_raise(rb_eArgError, "Invalid credential type"); - } else { - VALUE rb_username = rb_iv_get(rb_cred, "@username"); - VALUE rb_password = rb_iv_get(rb_cred, "@password"); - - Check_Type(rb_username, T_STRING); - Check_Type(rb_password, T_STRING); - - - rugged_exception_check( - git_cred_userpass_plaintext_new(cred, - StringValueCStr(rb_username), StringValueCStr(rb_password))); - } - } else if (rb_obj_is_kind_of(rb_cred, rb_cRuggedCredSshKey)) { - if (!(cred_payload->allowed_types & GIT_CREDTYPE_SSH_KEY)) { - rb_raise(rb_eArgError, "Invalid credential type"); - } else { - VALUE rb_username = rb_iv_get(rb_cred, "@username"); - VALUE rb_publickey = rb_iv_get(rb_cred, "@publickey"); - VALUE rb_privatekey = rb_iv_get(rb_cred, "@privatekey"); - VALUE rb_passphrase = rb_iv_get(rb_cred, "@passphrase"); - - Check_Type(rb_privatekey, T_STRING); - - if (!NIL_P(rb_username)) - Check_Type(rb_username, T_STRING); - if (!NIL_P(rb_publickey)) - Check_Type(rb_publickey, T_STRING); - if (!NIL_P(rb_passphrase)) - Check_Type(rb_passphrase, T_STRING); - - rugged_exception_check( - git_cred_ssh_key_new(cred, - NIL_P(rb_username) ? NULL : StringValueCStr(rb_username), - NIL_P(rb_publickey) ? NULL : StringValueCStr(rb_publickey), - StringValueCStr(rb_privatekey), - NIL_P(rb_passphrase) ? NULL : StringValueCStr(rb_passphrase))); - } - } else if (rb_obj_is_kind_of(rb_cred, rb_cRuggedCredDefault)) { - if (!(cred_payload->allowed_types & GIT_CREDTYPE_SSH_KEY)) { - rb_raise(rb_eArgError, "Invalid credential type"); - } else { - rugged_exception_check(git_cred_default_new(cred)); - } - } - - return Qnil; -} - -static int rugged__remote_credentials_cb( - git_cred **cred, - const char *url, - const char *username_from_url, - unsigned int allowed_types, - void *payload) -{ - struct rugged_remote_cb_payload *remote_payload = payload; - struct extract_cred_payload cred_payload; - VALUE args = rb_ary_new2(4), 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")); - - rb_ary_push(args, remote_payload->credentials); - rb_ary_push(args, url ? rb_str_new2(url) : Qnil); - rb_ary_push(args, username_from_url ? rb_str_new2(username_from_url) : Qnil); - rb_ary_push(args, rb_allowed_types); - - cred_payload.cred = cred; - cred_payload.rb_cred = rb_protect(rugged__block_yield_splat, args, &remote_payload->exception); - cred_payload.allowed_types = allowed_types; - - if (!remote_payload->exception) - rb_protect(rugged__extract_cred, (VALUE)&cred_payload, &remote_payload->exception); - - return remote_payload->exception ? GIT_ERROR : GIT_OK; -} - -static int rugged__default_remote_credentials_cb( - git_cred **cred, - const char *url, - const char *username_from_url, - unsigned int allowed_types, - void *payload) -{ - struct rugged_remote_cb_payload *remote_payload = payload; - struct extract_cred_payload cred_payload; - - cred_payload.cred = cred; - cred_payload.rb_cred = remote_payload->credentials; - cred_payload.allowed_types = allowed_types; - - rb_protect(rugged__extract_cred, (VALUE)&cred_payload, &remote_payload->exception); - - return remote_payload->exception ? GIT_ERROR : GIT_OK; -} - -static void parse_clone_options(git_clone_options *ret, VALUE rb_options_hash, struct rugged_remote_cb_payload *remote_payload) +static void parse_clone_options(git_clone_options *ret, VALUE rb_options, struct rugged_remote_cb_payload *remote_payload) { git_remote_callbacks remote_callbacks = GIT_REMOTE_CALLBACKS_INIT; VALUE val; - if (NIL_P(rb_options_hash)) + if (NIL_P(rb_options)) return; - val = rb_hash_aref(rb_options_hash, CSTR2SYM("bare")); + val = rb_hash_aref(rb_options, CSTR2SYM("bare")); if (RTEST(val)) ret->bare = 1; - val = rb_hash_aref(rb_options_hash, CSTR2SYM("checkout_branch")); + val = rb_hash_aref(rb_options, CSTR2SYM("checkout_branch")); if (!NIL_P(val)) { Check_Type(val, T_STRING); ret->checkout_branch = StringValueCStr(val); } - val = rb_hash_aref(rb_options_hash, CSTR2SYM("credentials")); - if (RTEST(val)) { - if (rb_obj_is_kind_of(val, rb_cRuggedCredPlaintext) || - rb_obj_is_kind_of(val, rb_cRuggedCredSshKey) || - rb_obj_is_kind_of(val, rb_cRuggedCredDefault)) - { - remote_callbacks.credentials = rugged__default_remote_credentials_cb; - remote_payload->credentials = val; - } else if (rb_respond_to(val, rb_intern("call"))) { - remote_callbacks.credentials = rugged__remote_credentials_cb; - remote_payload->credentials = val; - } else { - rb_raise(rb_eArgError, - "Expected a Rugged::Credentials type, a Proc or an object that responds to call (:credentials)."); - } - } - - val = rb_hash_aref(rb_options_hash, CSTR2SYM("callbacks")); - if (RTEST(val)) { - VALUE cb; + rugged_remote_init_callbacks_and_payload_from_options(rb_options, &remote_callbacks, remote_payload); - cb = rb_hash_aref(val, CSTR2SYM("transfer_progress")); - if (RTEST(cb)) { - if (!rb_respond_to(cb, rb_intern("call"))) { - rb_raise(rb_eArgError, "Expected a Proc or an object that responds to call (:transfer_progress)."); - } - - remote_payload->transfer_progress = cb; - remote_callbacks.transfer_progress = rugged__remote_transfer_progress_cb; - } - } - - remote_callbacks.payload = remote_payload; ret->remote_callbacks = remote_callbacks; } @@ -530,30 +339,24 @@ static void parse_clone_options(git_clone_options *ret, VALUE rb_options_hash, s * The proc will be called with the +url+, the +username+ from the url (if applicable) and * a list of applicable credential types. * - * :callbacks :: - * A Hash containing name-value pairs that define different callbacks to run during - * the clone operation. Possible callbacks are: - * - * :progress :: - * Not yet implemented. - * - * :completion :: - * Not yet implemented. + * :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+ and +received_bytes+. + * :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 :: - * Not yet implemented. + * :update_tips :: + * A callback that will be executed each time a reference was updated locally. It will be + * passed the +refname+, +old_oid+ and +new_oid+. * * Example: * * Repository.clone_at("https://github.com/libgit2/rugged.git", "./some/dir", { - * :callbacks => { - * :progress => lambda { |total_objects, indexed_objects, received_objects, received_bytes| - * # ... - * } + * progress: lambda { |total_objects, indexed_objects, received_objects, local_objects, total_deltas, indexed_deltas, received_bytes| + * # ... * } * }) */ @@ -1545,100 +1348,6 @@ static VALUE rb_git_repo_reset_path(int argc, VALUE *argv, VALUE self) return Qnil; } - -static int rugged__push_status_cb(const char *ref, const char *msg, void *payload) -{ - VALUE rb_result_hash = (VALUE)payload; - if (msg != NULL) - rb_hash_aset(rb_result_hash, rb_str_new_utf8(ref), rb_str_new_utf8(msg)); - - return GIT_OK; -} - -/* - * call-seq: - * repo.push(remote, refspecs) -> 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. - * - * Example: - * - * repo.push("origin", ["refs/heads/master", ":refs/heads/to_be_deleted"]) - */ -static VALUE rb_git_repo_push(VALUE self, VALUE rb_remote, VALUE rb_refspecs) -{ - VALUE rb_refspec, rb_exception = Qnil, rb_result = rb_hash_new(); - git_repository *repo; - git_remote *remote = NULL; - git_push *push = NULL; - - int error = 0, i = 0; - - Check_Type(rb_refspecs, T_ARRAY); - for (i = 0; i < RARRAY_LEN(rb_refspecs); ++i) { - rb_refspec = rb_ary_entry(rb_refspecs, i); - Check_Type(rb_refspec, T_STRING); - } - - Data_Get_Struct(self, git_repository, repo); - - if (rb_obj_is_kind_of(rb_remote, rb_cRuggedRemote)) { - Data_Get_Struct(rb_remote, git_remote, remote); - } else if (TYPE(rb_remote) == T_STRING) { - error = git_remote_load(&remote, repo, StringValueCStr(rb_remote)); - if (error) goto cleanup; - } else { - rb_raise(rb_eTypeError, "Expecting a String or Rugged::Remote instance"); - } - - error = git_push_new(&push, remote); - if (error) goto cleanup; - - for (i = 0; !error && i < RARRAY_LEN(rb_refspecs); ++i) { - rb_refspec = rb_ary_entry(rb_refspecs, i); - error = git_push_add_refspec(push, StringValueCStr(rb_refspec)); - } - if (error) goto cleanup; - - error = git_push_finish(push); - - if (error) { - if (error == GIT_ENONFASTFORWARD) { - rb_exception = rb_exc_new2(rb_eRuggedError, "non-fast-forward update rejected"); - } else if (error == -1) { - rb_exception = rb_exc_new2(rb_eRuggedError, "could not push to repo (check for non-bare repo)"); - } - - goto cleanup; - } - - if (!git_push_unpack_ok(push)) { - rb_exception = rb_exc_new2(rb_eRuggedError, "the remote side did not unpack successfully"); - goto cleanup; - } - - error = git_push_status_foreach(push, &rugged__push_status_cb, (void *)rb_result); - if (error) goto cleanup; - - error = git_push_update_tips(push, NULL, NULL); - -cleanup: - git_push_free(push); - - // We can only free the remote if we have loaded it ourselves. - if (!rb_obj_is_kind_of(rb_remote, rb_cRuggedRemote)) { - git_remote_free(remote); - } - - if (!NIL_P(rb_exception)) - rb_exc_raise(rb_exception); - - rugged_exception_check(error); - - return rb_result; -} - /* * call-seq: * repo.close -> nil @@ -2240,8 +1949,6 @@ void Init_rugged_repo(void) rb_define_method(rb_cRuggedRepo, "workdir=", rb_git_repo_set_workdir, 1); rb_define_method(rb_cRuggedRepo, "status", rb_git_repo_status, -1); - rb_define_method(rb_cRuggedRepo, "push", rb_git_repo_push, 2); - rb_define_method(rb_cRuggedRepo, "index", rb_git_repo_get_index, 0); rb_define_method(rb_cRuggedRepo, "index=", rb_git_repo_set_index, 1); rb_define_method(rb_cRuggedRepo, "config", rb_git_repo_get_config, 0); diff --git a/lib/rugged/credentials.rb b/lib/rugged/credentials.rb index 3707fad3d..a68f8683e 100644 --- a/lib/rugged/credentials.rb +++ b/lib/rugged/credentials.rb @@ -1,10 +1,14 @@ module Rugged module Credentials # A plain-text username and password credential object. - class Plaintext + class UserPassword def initialize(options) @username, @password = options[:username], options[:password] end + + def call(url, username_from_url, allowed_types) + self + end end # A ssh key credential object that can optionally be passphrase-protected @@ -12,11 +16,28 @@ class SshKey def initialize(options) @username, @publickey, @privatekey, @passphrase = options[:username], options[:publickey], options[:privatekey], options[:passphrase] end + + def call(url, username_from_url, allowed_types) + self + end + end + + class SshKeyFromAgent + def initialize(options) + @username = options[:username] + end + + def call(url, username_from_url, allowed_types) + self + end end # A "default" credential usable for Negotiate mechanisms like NTLM or # Kerberos authentication class Default + def call(url, username_from_url, allowed_types) + self + end end end end diff --git a/lib/rugged/remote.rb b/lib/rugged/remote.rb index 540149ca3..f9e2e09bd 100644 --- a/lib/rugged/remote.rb +++ b/lib/rugged/remote.rb @@ -1,13 +1,5 @@ module Rugged class Remote - # Push a list of refspecs to the given remote. - # - # refspecs - A list of refspecs that should be pushed to the remote. - # - # Returns a hash containing the pushed refspecs as keys and - # any error messages or +nil+ as values. - def push(refspecs) - @owner.push(self, refspecs) - end + end end diff --git a/lib/rugged/repository.rb b/lib/rugged/repository.rb index 11ff076b8..2ebd1ba63 100644 --- a/lib/rugged/repository.rb +++ b/lib/rugged/repository.rb @@ -194,5 +194,27 @@ def blob_at(revision, path) blob = Rugged::Blob.lookup(self, blob_data[:oid]) (blob.type == :blob) ? blob : nil end + + def fetch(remote_or_url, *args) + unless remote_or_url.kind_of? Remote + remote_or_url = Remote.lookup(self, remote_or_url) || Remote.new(self, remote_or_url) + end + + remote_or_url.fetch(*args) + end + + # Push a list of refspecs to the given remote. + # + # refspecs - A list of refspecs that should be pushed to the remote. + # + # Returns a hash containing the pushed refspecs as keys and + # any error messages or +nil+ as values. + def push(remote_or_url, *args) + unless remote_or_url.kind_of? Remote + remote_or_url = Remote.lookup(self, remote_or_url) || Remote.new(self, remote_or_url) + end + + remote_or_url.push(*args) + end end end diff --git a/script/travisbuild b/script/travisbuild index dca39e518..6b3693ed8 100755 --- a/script/travisbuild +++ b/script/travisbuild @@ -1,22 +1,26 @@ #!/bin/sh -# Create a test repo which we can use for the online::push tests +# Create a test repo which we can use for the online tests mkdir $HOME/_temp git init --bare $HOME/_temp/test.git git daemon --listen=localhost --export-all --enable=receive-pack --base-path=$HOME/_temp $HOME/_temp 2>/dev/null & sudo start ssh -ssh-keygen -t rsa -f ~/.ssh/id_rsa -N "" -q -cat ~/.ssh/id_rsa.pub >>~/.ssh/authorized_keys +ssh-keygen -t rsa -f ~/.ssh/rugged_test_rsa -N "" -q +cat ~/.ssh/rugged_test_rsa.pub >>~/.ssh/authorized_keys ssh-keyscan -t rsa localhost >>~/.ssh/known_hosts export GITTEST_REMOTE_GIT_URL="git://localhost/test.git" export GITTEST_REMOTE_SSH_URL="ssh://localhost/$HOME/_temp/test.git" export GITTEST_REMOTE_SSH_USER=$USER -export GITTEST_REMOTE_SSH_KEY="$HOME/.ssh/id_rsa" -export GITTEST_REMOTE_SSH_PUBKEY="$HOME/.ssh/id_rsa.pub" +export GITTEST_REMOTE_SSH_KEY="$HOME/.ssh/rugged_test_rsa" +export GITTEST_REMOTE_SSH_PUBKEY="$HOME/.ssh/rugged_test_rsa.pub" export GITTEST_REMOTE_SSH_PASSPHRASE="" +export GITTEST_REMOTE_REPO_PATH="$HOME/_temp/test.git" + +eval $(ssh-agent) +ssh-add $GITTEST_REMOTE_SSH_KEY # We need the config so the tests don't fail git config --global user.name 'The rugged tests are fragile' diff --git a/test/online/clone_test.rb b/test/online/clone_test.rb index dea34c266..9150cb53d 100644 --- a/test/online/clone_test.rb +++ b/test/online/clone_test.rb @@ -1,37 +1,18 @@ require 'test_helper' -class OnlineCloneTest < Rugged::TestCase - def ssh_creds? - %w{URL USER KEY PUBKEY PASSPHRASE}.all? { |key| ENV["GITTEST_REMOTE_SSH_#{key}"] } - end - - def git_creds? - ENV['GITTEST_REMOTE_GIT_URL'] - end - - def ssh_key_credential - Rugged::Credentials::SshKey.new({ - username: ENV["GITTEST_REMOTE_SSH_USER"], - publickey: ENV["GITTEST_REMOTE_SSH_PUBKEY"], - privatekey: ENV["GITTEST_REMOTE_SSH_KEY"], - passphrase: ENV["GITTEST_REMOTE_SSH_PASSPHRASE"], - }) - end - - def test_clone_over_git - skip unless git_creds? - - Dir.mktmpdir do |dir| - repo = Rugged::Repository.clone_at(ENV['GITTEST_REMOTE_GIT_URL'], dir) +class OnlineCloneTest < Rugged::OnlineTestCase + if git_creds? + def test_clone_over_git + Dir.mktmpdir do |dir| + repo = Rugged::Repository.clone_at(ENV['GITTEST_REMOTE_GIT_URL'], dir) - assert_instance_of Rugged::Repository, repo + assert_instance_of Rugged::Repository, repo + end end end - if Rugged.features.include? :ssh + if Rugged.features.include?(:ssh) && ssh_creds? def test_clone_over_ssh_with_credentials - skip unless ssh_creds? - Dir.mktmpdir do |dir| repo = Rugged::Repository.clone_at(ENV['GITTEST_REMOTE_SSH_URL'], dir, { credentials: ssh_key_credential @@ -41,9 +22,17 @@ def test_clone_over_ssh_with_credentials end end - def test_clone_over_ssh_with_credentials_callback - skip unless ssh_creds? + def test_clone_over_ssh_with_credentials_from_agent + Dir.mktmpdir do |dir| + repo = Rugged::Repository.clone_at(ENV['GITTEST_REMOTE_SSH_URL'], dir, { + credentials: ssh_key_credential_from_agent + }) + assert_instance_of Rugged::Repository, repo + end + end + + def test_clone_over_ssh_with_credentials_callback Dir.mktmpdir do |dir| repo = Rugged::Repository.clone_at(ENV['GITTEST_REMOTE_SSH_URL'], dir, { credentials: lambda { |url, username, allowed_types| diff --git a/test/online/fetch_test.rb b/test/online/fetch_test.rb new file mode 100644 index 000000000..34d14c1c8 --- /dev/null +++ b/test/online/fetch_test.rb @@ -0,0 +1,74 @@ +require 'test_helper' + +class OnlineFetchTest < Rugged::OnlineTestCase + def setup + super + @repo = Rugged::Repository.init_at(File.join(@_sandbox_path, "repo")) + end + + def teardown + @repo.close + super + end + + if git_creds? + def test_fetch_over_git + reset_remote_repo + + Rugged::Remote.add(@repo, "origin", ENV['GITTEST_REMOTE_GIT_URL']) + + @repo.fetch("origin") + end + end + + if Rugged.features.include?(:https) + def test_fetch_over_https + Rugged::Remote.add(@repo, "origin", "https://github.com/libgit2/TestGitRepository.git") + + @repo.fetch("origin") + + assert_equal 5, @repo.refs.count + assert_equal [ + "refs/remotes/origin/first-merge", + "refs/remotes/origin/master", + "refs/remotes/origin/no-parent", + "refs/tags/blob", + "refs/tags/commit_tree" + ], @repo.refs.map(&:name).sort + end + end + + if Rugged.features.include?(:ssh) && ssh_creds? + def test_fetch_over_ssh_with_credentials + reset_remote_repo + + Rugged::Remote.add(@repo, "origin", ENV['GITTEST_REMOTE_SSH_URL']) + + @repo.fetch("origin", { + credentials: ssh_key_credential + }) + end + + def test_fetch_over_ssh_with_credentials_from_agent + reset_remote_repo + + Rugged::Remote.add(@repo, "origin", ENV['GITTEST_REMOTE_SSH_URL']) + + @repo.fetch("origin", { + credentials: ssh_key_credential_from_agent + }) + end + + def test_fetch_over_ssh_with_credentials_callback + reset_remote_repo + + Rugged::Remote.add(@repo, "origin", ENV['GITTEST_REMOTE_SSH_URL']) + + @repo.fetch("origin", { + credentials: lambda { |url, username, allowed_types| + return ssh_key_credential + } + }) + end + end +end diff --git a/test/online/ls_test.rb b/test/online/ls_test.rb new file mode 100644 index 000000000..267629e13 --- /dev/null +++ b/test/online/ls_test.rb @@ -0,0 +1,59 @@ +require 'test_helper' + +class OnlineLsTest < Rugged::OnlineTestCase + def setup + super + + @repo = sandbox_init("push_src") + end + + def teardown + @repo.close + + super + end + + if Rugged.features.include?(:https) + def test_ls_over_https + remote = Rugged::Remote.add(@repo, "origin", "https://github.com/libgit2/TestGitRepository.git") + + assert_equal [ + { :local? => false, :oid => "49322bb17d3acc9146f98c97d078513228bbf3c0", :loid => nil, :name => "HEAD" }, + { :local? => false, :oid => "0966a434eb1a025db6b71485ab63a3bfbea520b6", :loid => nil, :name => "refs/heads/first-merge" }, + { :local? => false, :oid => "49322bb17d3acc9146f98c97d078513228bbf3c0", :loid => nil, :name => "refs/heads/master" }, + { :local? => false, :oid => "42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", :loid => nil, :name => "refs/heads/no-parent" }, + { :local? => false, :oid => "d96c4e80345534eccee5ac7b07fc7603b56124cb", :loid => nil, :name => "refs/tags/annotated_tag" }, + { :local? => false, :oid => "c070ad8c08840c8116da865b2d65593a6bb9cd2a", :loid => nil, :name => "refs/tags/annotated_tag^{}" }, + { :local? => false, :oid => "55a1a760df4b86a02094a904dfa511deb5655905", :loid => nil, :name => "refs/tags/blob" }, + { :local? => false, :oid => "8f50ba15d49353813cc6e20298002c0d17b0a9ee", :loid => nil, :name => "refs/tags/commit_tree" }, + { :local? => false, :oid => "6e0c7bdb9b4ed93212491ee778ca1c65047cab4e", :loid => nil, :name => "refs/tags/nearly-dangling"} + ], remote.ls.to_a + end + end + + if git_creds? + def test_ls_over_git + reset_remote_repo + + remote = Rugged::Remote.add(@repo, "origin", ENV['GITTEST_REMOTE_GIT_URL']) + remote.push(["refs/heads/b1:refs/heads/b1"]) + + assert_equal [ + { :local? => false, :oid => "a78705c3b2725f931d3ee05348d83cc26700f247", :loid => nil, :name => "refs/heads/b1" } + ], remote.ls.to_a + end + end + + if Rugged.features.include?(:ssh) && ssh_creds? + def test_ls_over_ssh_with_credentials + reset_remote_repo + + remote = Rugged::Remote.add(@repo, "origin", ENV['GITTEST_REMOTE_SSH_URL']) + remote.push(["refs/heads/b1:refs/heads/b1"], credentials: ssh_key_credential) + + assert_equal [ + { :local? => false, :oid => "a78705c3b2725f931d3ee05348d83cc26700f247", :loid => nil, :name => "refs/heads/b1" } + ], remote.ls(credentials: ssh_key_credential).to_a + end + end +end diff --git a/test/online/push_test.rb b/test/online/push_test.rb new file mode 100644 index 000000000..6f6508241 --- /dev/null +++ b/test/online/push_test.rb @@ -0,0 +1,73 @@ +require 'test_helper' + +class OnlineGitPushTest < Rugged::OnlineTestCase + def setup + super + + @repo = sandbox_init("push_src") + @remote = Rugged::Remote.add(@repo, "test", ENV['GITTEST_REMOTE_GIT_URL']) + @target_repo = Rugged::Repository.new(ENV['GITTEST_REMOTE_REPO_PATH']) + end + + def teardown + @repo.close + + super + end + + if git_creds? + def test_push_branches + @remote.push([ + "refs/heads/b1:refs/heads/b1", + "refs/heads/b2:refs/heads/b2", + "refs/heads/b3:refs/heads/b3", + "refs/heads/b4:refs/heads/b4", + "refs/heads/b5:refs/heads/b5" + ]) + + assert_equal @repo.references["refs/heads/b1"].target_id, @target_repo.references["refs/heads/b1"].target_id + assert_equal @repo.references["refs/heads/b2"].target_id, @target_repo.references["refs/heads/b2"].target_id + assert_equal @repo.references["refs/heads/b3"].target_id, @target_repo.references["refs/heads/b3"].target_id + assert_equal @repo.references["refs/heads/b4"].target_id, @target_repo.references["refs/heads/b4"].target_id + assert_equal @repo.references["refs/heads/b5"].target_id, @target_repo.references["refs/heads/b5"].target_id + end + end +end + +if Rugged.features.include?(:ssh) + class OnlineSshPushTest < Rugged::OnlineTestCase + def setup + super + + @repo = sandbox_init("push_src") + @remote = Rugged::Remote.add(@repo, "test", ENV['GITTEST_REMOTE_SSH_URL']) + @target_repo = Rugged::Repository.new(ENV['GITTEST_REMOTE_REPO_PATH']) + end + + def teardown + @repo.close + + super + end + + if ssh_creds? + def test_push_branches + @remote.push([ + "refs/heads/b1:refs/heads/b1", + "refs/heads/b2:refs/heads/b2", + "refs/heads/b3:refs/heads/b3", + "refs/heads/b4:refs/heads/b4", + "refs/heads/b5:refs/heads/b5" + ], { + credentials: ssh_key_credential + }) + + assert_equal @repo.references["refs/heads/b1"].target_id, @target_repo.references["refs/heads/b1"].target_id + assert_equal @repo.references["refs/heads/b2"].target_id, @target_repo.references["refs/heads/b2"].target_id + assert_equal @repo.references["refs/heads/b3"].target_id, @target_repo.references["refs/heads/b3"].target_id + assert_equal @repo.references["refs/heads/b4"].target_id, @target_repo.references["refs/heads/b4"].target_id + assert_equal @repo.references["refs/heads/b5"].target_id, @target_repo.references["refs/heads/b5"].target_id + end + end + end +end diff --git a/test/remote_test.rb b/test/remote_test.rb index b1cb14501..b17a6d7bb 100644 --- a/test/remote_test.rb +++ b/test/remote_test.rb @@ -12,12 +12,7 @@ def test_remote_network_connect end remote = Rugged::Remote.new(@repo, 'git://github.com/libgit2/libgit2.git') - - remote.connect(:fetch) do |r| - assert r.connected? - end - - assert !remote.connected? + assert remote.ls.any? end end @@ -181,16 +176,19 @@ def test_push_single_ref def test_push_to_non_bare_raise_error @remote_repo.config['core.bare'] = 'false' - assert_raises Rugged::InvalidError do + exception = assert_raises Rugged::InvalidError do @remote.push(["refs/heads/master"]) end + + assert_equal "Local push doesn't (yet) support pushing to non-bare repos.", exception.message end def test_push_non_forward_raise_error - assert_raises Rugged::Error do + exception = assert_raises Rugged::ReferenceError do @remote.push(["refs/heads/unit_test:refs/heads/master"]) end + assert_equal "Cannot push non-fastforwardable reference", exception.message assert_equal "a65fedf39aefe402d3bb6e24df4d4f5fe4547750", @remote_repo.ref("refs/heads/master").target_id end @@ -269,63 +267,78 @@ def teardown FileUtils.remove_entry_secure(@path) end - def test_remote_disconnect - @remote.connect(:fetch) - assert @remote.connected? - - @remote.disconnect - refute @remote.connected? - end - def test_remote_ls - @remote.connect(:fetch) do |r| - assert r.ls.kind_of? Enumerable - rheads = r.ls.to_a + assert @remote.ls.kind_of? Enumerable + rheads = @remote.ls.to_a - assert_equal 7, rheads.count + assert_equal 7, rheads.count - rhead = rheads.first - assert_equal false, rhead[:local?] - assert rhead[:oid] - assert_nil rhead[:loid] - end + rhead = rheads.first + assert_equal false, rhead[:local?] + assert rhead[:oid] + assert_nil rhead[:loid] end - def test_update_tips_callback - @remote.connect(:fetch) do |r| - r.download - r.update_tips! do |ref, source, destination| - assert @repo.references[ref] - assert_nil source - assert destination - end - end + def test_remote_fetch + assert_equal({ + total_objects: 19, + indexed_objects: 19, + received_objects: 19, + local_objects: 0, + total_deltas: 2, + indexed_deltas: 2, + received_bytes: 1563 + }, @remote.fetch) + + assert_equal '36060c58702ed4c2a40832c51758d5344201d89a', @repo.branches['origin/master'].target_id + assert_equal '41bc8c69075bbdb46c5c6f0566cc8cc5b46e8bd9', @repo.branches["origin/packed"].target_id + assert_equal '5b5b025afb0b4c913b4c338a42934a3863bf3644', @repo.tags["v0.9"].target_id + assert_equal '0c37a5391bbff43c37f0d0371823a5509eed5b1d', @repo.tags["v1.0"].target_id end - # this is not as useless as it seems - # LocalJumpError is raised in the second call to - # update_tips! if libgit2 callback is not cleared - # Also the exception is explicitly raised after the - # callback is cleared - def test_update_tips_cleanup_callbacks - @remote.connect(:fetch) do |r| - r.download - assert_raises TestException do - r.update_tips! do - raise TestException - end - end - r.update_tips! - end + def test_update_tips_callback + @remote.fetch update_tips: lambda { |ref, source, destination| + assert @repo.references[ref] + assert_nil source + assert destination + } + + assert_equal '36060c58702ed4c2a40832c51758d5344201d89a', @repo.branches['origin/master'].target_id + assert_equal '41bc8c69075bbdb46c5c6f0566cc8cc5b46e8bd9', @repo.branches["origin/packed"].target_id + assert_equal '5b5b025afb0b4c913b4c338a42934a3863bf3644', @repo.tags["v0.9"].target_id + assert_equal '0c37a5391bbff43c37f0d0371823a5509eed5b1d', @repo.tags["v1.0"].target_id end - def test_remote_fetch - @remote.connect(:fetch) do |r| - r.download - r.update_tips! + def test_update_tips_callback_error + assert_raises TestException do + @remote.fetch update_tips: lambda { |*args| + raise TestException + } end - assert_equal '36060c58702ed4c2a40832c51758d5344201d89a', @repo.rev_parse('origin/master').oid - assert @repo.lookup('36060c5') + # In case of an error inside the callback, all further tip updates get cancelled + assert_equal '36060c58702ed4c2a40832c51758d5344201d89a', @repo.branches["origin/master"].target_id + refute @repo.branches["origin/packed"] + refute @repo.tags["v0.9"] + refute @repo.tags["v1.0"] + end + + def test_transfer_progress_callback + total_objects = indexed_objects = received_objects = local_objects = total_deltas = indexed_deltas = received_bytes = nil + callsback = 0 + + @remote.fetch transfer_progress: lambda { |*args| + total_objects, indexed_objects, received_objects, local_objects, total_deltas, indexed_deltas, received_bytes = args + callsback += 1 + } + + assert_equal 22, callsback + assert_equal 19, total_objects + assert_equal 19, indexed_objects + assert_equal 19, received_objects + assert_equal 0, local_objects + assert_equal 2, total_deltas + assert_equal 2, indexed_deltas + assert_equal 1563, received_bytes end end diff --git a/test/repo_test.rb b/test/repo_test.rb index 866ccd29f..3bf7c92d7 100644 --- a/test/repo_test.rb +++ b/test/repo_test.rb @@ -431,15 +431,13 @@ def test_clone_bare end end - def test_clone_with_progress - total_objects = indexed_objects = received_objects = received_bytes = nil + def test_clone_with_transfer_progress_callback + total_objects = indexed_objects = received_objects = local_objects = total_deltas = indexed_deltas = received_bytes = nil callsback = 0 - repo = Rugged::Repository.clone_at(@source_path, @tmppath,{ - :callbacks => { - :transfer_progress => lambda { |*args| - total_objects, indexed_objects, received_objects, received_bytes = args - callsback += 1 - } + repo = Rugged::Repository.clone_at(@source_path, @tmppath, { + transfer_progress: lambda { |*args| + total_objects, indexed_objects, received_objects, local_objects, total_deltas, indexed_deltas, received_bytes = args + callsback += 1 } }) repo.close @@ -447,9 +445,34 @@ def test_clone_with_progress assert_equal 19, total_objects assert_equal 19, indexed_objects assert_equal 19, received_objects + assert_equal 0, local_objects + assert_equal 2, total_deltas + assert_equal 2, indexed_deltas assert_equal 1563, received_bytes end + + def test_clone_with_update_tips_callback + calls = 0 + updated_tips = {} + + repo = Rugged::Repository.clone_at(@source_path, @tmppath, { + update_tips: lambda { |refname, a, b| + calls += 1 + updated_tips[refname] = [a, b] + } + }) + repo.close + + assert_equal 4, calls + assert_equal({ + "refs/remotes/origin/master" => [nil, "36060c58702ed4c2a40832c51758d5344201d89a"], + "refs/remotes/origin/packed" => [nil, "41bc8c69075bbdb46c5c6f0566cc8cc5b46e8bd9"], + "refs/tags/v0.9" => [nil, "5b5b025afb0b4c913b4c338a42934a3863bf3644"], + "refs/tags/v1.0" => [nil, "0c37a5391bbff43c37f0d0371823a5509eed5b1d"], + }, updated_tips) + end + def test_clone_with_branch repo = Rugged::Repository.clone_at(@source_path, @tmppath, {checkout_branch: "packed"}) begin @@ -463,8 +486,8 @@ def test_clone_with_branch def test_clone_quits_on_error begin - Rugged::Repository.clone_at(@source_path, @tmppath, :callbacks => { - :transfer_progress => lambda { |*_| raise 'boom' } + Rugged::Repository.clone_at(@source_path, @tmppath, { + transfer_progress: lambda { |*_| raise 'boom' } }) rescue => e assert_equal 'boom', e.message @@ -474,8 +497,8 @@ def test_clone_quits_on_error def test_clone_with_bad_progress_callback assert_raises ArgumentError do - Rugged::Repository.clone_at(@source_path, @tmppath, :callbacks => { - :transfer_progress => Object.new + Rugged::Repository.clone_at(@source_path, @tmppath, { + transfer_progress: Object.new }) end assert_no_dotgit_dir(@tmppath) @@ -559,16 +582,19 @@ def test_push_to_remote_instance def test_push_to_non_bare_raise_error @remote_repo.config['core.bare'] = 'false' - assert_raises Rugged::InvalidError do + exception = assert_raises Rugged::InvalidError do @repo.push("origin", ["refs/heads/master"]) end + + assert_equal "Local push doesn't (yet) support pushing to non-bare repos.", exception.message end def test_push_non_forward_raise_error - assert_raises Rugged::Error do + exception = assert_raises Rugged::ReferenceError do @repo.push("origin", ["refs/heads/unit_test:refs/heads/master"]) end + assert_equal "Cannot push non-fastforwardable reference", exception.message assert_equal "a65fedf39aefe402d3bb6e24df4d4f5fe4547750", @remote_repo.ref("refs/heads/master").target_id end diff --git a/test/test_helper.rb b/test/test_helper.rb index 04f21f529..2fa53bb0c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -56,6 +56,39 @@ def sandbox_clone(repository, name) end end + class OnlineTestCase < SandboxedTestCase + def reset_remote_repo + remote_repo = Rugged::Repository.new(ENV['GITTEST_REMOTE_REPO_PATH']) + remote_repo.references.each do |ref| + remote_repo.references.delete(ref) + end + remote_repo.close + end + + def self.ssh_creds? + %w{URL USER KEY PUBKEY PASSPHRASE}.all? { |key| ENV["GITTEST_REMOTE_SSH_#{key}"] } + end + + def self.git_creds? + ENV['GITTEST_REMOTE_GIT_URL'] + end + + def ssh_key_credential + Rugged::Credentials::SshKey.new({ + username: ENV["GITTEST_REMOTE_SSH_USER"], + publickey: ENV["GITTEST_REMOTE_SSH_PUBKEY"], + privatekey: ENV["GITTEST_REMOTE_SSH_KEY"], + passphrase: ENV["GITTEST_REMOTE_SSH_PASSPHASE"], + }) + end + + def ssh_key_credential_from_agent + Rugged::Credentials::SshKeyFromAgent.new({ + username: ENV["GITTEST_REMOTE_SSH_USER"] + }) + end + end + module RepositoryAccess def setup @path = File.dirname(__FILE__) + '/fixtures/testrepo.git/'