/*
 * 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"
#include <git2/sys/repository.h>
#include <git2/sys/odb_backend.h>
#include <git2/sys/refdb_backend.h>
#include <git2/refs.h>

extern VALUE rb_mRugged;
extern VALUE rb_eRuggedError;
extern VALUE rb_cRuggedIndex;
extern VALUE rb_cRuggedConfig;
extern VALUE rb_cRuggedBackend;
extern VALUE rb_cRuggedRemote;
extern VALUE rb_cRuggedCommit;
extern VALUE rb_cRuggedTag;
extern VALUE rb_cRuggedTree;
extern VALUE rb_cRuggedReference;
extern VALUE rb_cRuggedBackend;

extern VALUE rb_cRuggedCredPlaintext;
extern VALUE rb_cRuggedCredSshKey;
extern VALUE rb_cRuggedCredDefault;

VALUE rb_cRuggedRepo;
VALUE rb_cRuggedOdbObject;

static ID id_call;

/*
 *  call-seq:
 *    odb_obj.oid -> hex_oid
 *
 *  Return the Object ID (a 40 character SHA1 hash) for this raw
 *  object.
 *
 *    odb_obj.oid #=> "d8786bfc97485e8d7b19b21fb88c8ef1f199fc3f"
 */
static VALUE rb_git_odbobj_oid(VALUE self)
{
	git_odb_object *obj;
	Data_Get_Struct(self, git_odb_object, obj);
	return rugged_create_oid(git_odb_object_id(obj));
}

/*
 *  call-seq:
 *    odb_obj.data -> buffer
 *
 *  Return an ASCII buffer with the raw bytes that form the Git object.
 *
 *    odb_obj.data #=> "tree 87ebee8367f9cc5ac04858b3bd5610ca74f04df9\n"
 *                 #=> "parent 68d041ee999cb07c6496fbdd4f384095de6ca9e1\n"
 *                 #=> "author Vicent Martí <tanoku@gmail.com> 1326863045 -0800\n"
 *                 #=> ...
 */
static VALUE rb_git_odbobj_data(VALUE self)
{
	git_odb_object *obj;
	Data_Get_Struct(self, git_odb_object, obj);
	return rb_str_new(git_odb_object_data(obj), git_odb_object_size(obj));
}

/*
 *  call-seq:
 *    odb_obj.size -> size
 *
 *  Return the size in bytes of the Git object after decompression. This is
 *  also the size of the +obj.data+ buffer.
 *
 *    odb_obj.size #=> 231
 */
static VALUE rb_git_odbobj_size(VALUE self)
{
	git_odb_object *obj;
	Data_Get_Struct(self, git_odb_object, obj);
	return INT2FIX(git_odb_object_size(obj));
}

/*
 *  call-seq:
 *    odb_obj.type -> Symbol
 *
 *  Return a Ruby symbol representing the basic Git type of this object.
 *  Possible values are +:tree+, +:blob+, +:commit+ and +:tag+.
 *
 *    odb_obj.type #=> :tag
 */
static VALUE rb_git_odbobj_type(VALUE self)
{
	git_odb_object *obj;
	Data_Get_Struct(self, git_odb_object, obj);
	return rugged_otype_new(git_odb_object_type(obj));
}

void rb_git__odbobj_free(void *obj)
{
	git_odb_object_free((git_odb_object *)obj);
}

VALUE rugged_raw_read(git_repository *repo, const git_oid *oid)
{
	git_odb *odb;
	git_odb_object *obj;

	int error;

	error = git_repository_odb(&odb, repo);
	rugged_exception_check(error);

	error = git_odb_read(&obj, odb, oid);
	git_odb_free(odb);
	rugged_exception_check(error);

	return Data_Wrap_Struct(rb_cRuggedOdbObject, NULL, rb_git__odbobj_free, obj);
}

void rb_git_repo__free(git_repository *repo)
{
	git_repository_free(repo);
}

VALUE rugged_repo_new(VALUE klass, git_repository *repo)
{
	VALUE rb_repo = Data_Wrap_Struct(klass, NULL, &rb_git_repo__free, repo);

#ifdef HAVE_RUBY_ENCODING_H
	/* TODO: set this properly */
	rb_iv_set(rb_repo, "@encoding",
		rb_enc_from_encoding(rb_filesystem_encoding()));
#endif

	rb_iv_set(rb_repo, "@config", Qnil);
	rb_iv_set(rb_repo, "@index", Qnil);

	return rb_repo;
}

static void load_alternates(git_repository *repo, VALUE rb_alternates)
{
	git_odb *odb = NULL;
	int i, error;

	if (NIL_P(rb_alternates))
		return;

	Check_Type(rb_alternates, T_ARRAY);

	if (RARRAY_LEN(rb_alternates) == 0)
		return;

	for (i = 0; i < RARRAY_LEN(rb_alternates); ++i)
		Check_Type(rb_ary_entry(rb_alternates, i), T_STRING);

	error = git_repository_odb(&odb, repo);
	rugged_exception_check(error);

	for (i = 0; !error && i < RARRAY_LEN(rb_alternates); ++i) {
		VALUE alt = rb_ary_entry(rb_alternates, i);
		error = git_odb_add_disk_alternate(odb, StringValueCStr(alt));
	}

	git_odb_free(odb);
	rugged_exception_check(error);
}

static void rugged_repo_new_with_backend(git_repository **repo, VALUE rb_path, VALUE rb_backend)
{
	char *path;

	git_odb *odb = NULL;
	git_odb_backend *odb_backend = NULL;
	git_refdb *refdb = NULL;
	git_refdb_backend *refdb_backend = NULL;
	git_reference *head = NULL;
	rugged_backend *backend;

	int error = 0;

	Check_Type(rb_path, T_STRING);
	path = StringValueCStr(rb_path);

	if (rb_obj_is_kind_of(rb_backend, rb_cRuggedBackend) == Qfalse) {
		rb_raise(rb_eRuggedError, "Backend must be an instance of Rugged::Backend");
	}

	Data_Get_Struct(rb_backend, rugged_backend, backend);

	error = git_odb_new(&odb);
	if (error) goto cleanup;

	error = backend->odb_backend(&odb_backend, backend, path);
	if (error) goto cleanup;

	error = git_odb_add_backend(odb, odb_backend, 1);
	if (error) {
		assert(odb_backend->free);
		odb_backend->free(odb_backend);
		goto cleanup;
	}

	error = git_repository_wrap_odb(repo, odb);
	if (error) goto cleanup;

	error = git_refdb_new(&refdb, *repo);
	if (error) goto cleanup;

	error = backend->refdb_backend(&refdb_backend, backend, path);
	if (error) {
		assert(refdb_backend->free);
		refdb_backend->free(refdb_backend);
		goto cleanup;
	}

	error = git_refdb_set_backend(refdb, refdb_backend);
	if (error) goto cleanup;

	git_repository_set_refdb(*repo, refdb);

	error = git_reference_lookup(&head, *repo, "HEAD");

	if (error == GIT_ENOTFOUND) {
		giterr_clear();
		error = git_reference_symbolic_create(&head, *repo, "HEAD", "refs/heads/master", 0, NULL);
	}

	if (!error) {
		git_reference_free(head);
		return;
	}

cleanup:
	git_repository_free(*repo);
	git_odb_free(odb);
	git_refdb_free(refdb);

	rugged_exception_check(error);
}

/*
 *  call-seq:
 *    Repository.bare(path[, alternates]) -> repository OR
 *    Repository.bare(path[, options]) -> repository
 *
 *  Open a bare Git repository at +path+ and return a +Repository+
 *  object representing it.
 *
 *  This is faster than Rugged::Repository.new, as it won't attempt to perform
 *  any +.git+ directory discovery, won't try to load the config options to
 *  determine whether the repository is bare and won't try to load the workdir.
 *
 *  Optionally, you can pass a list of alternate object folders or an options Hash.
 *
 *    Rugged::Repository.bare(path, ['./other/repo/.git/objects'])
 *    Rugged::Repository.bare(path, opts)
 *
 *  The following options can be passed in the +options+ Hash:
 *
 *  :backend ::
 *    A Rugged::Backend instance
 *  :alternates ::
 *    A list of alternate object folders.
 *    Rugged::Repository.bare(path, :alternates => ['./other/repo/.git/objects'])
 */
static VALUE rb_git_repo_open_bare(int argc, VALUE *argv, VALUE klass)
{
	git_repository *repo = NULL;
	int error = 0;
	VALUE rb_path, rb_options, rb_alternates = 0;

	rb_scan_args(argc, argv, "11", &rb_path, &rb_options);

	if (!NIL_P(rb_options) && TYPE(rb_options) == T_ARRAY)
		rb_alternates = rb_options;

	if (!NIL_P(rb_options) && TYPE(rb_options) == T_HASH) {
		/* Check for `:backend` */
		VALUE rb_backend = rb_hash_aref(rb_options, CSTR2SYM("backend"));

		if (!NIL_P(rb_backend)) {
			rugged_repo_new_with_backend(&repo, rb_path, rb_backend);
		}

		/* Check for `:alternates` */
		rb_alternates = rb_hash_aref(rb_options, CSTR2SYM("alternates"));
	}

	if (!repo) {
		Check_Type(rb_path, T_STRING);

		error = git_repository_open_bare(&repo, StringValueCStr(rb_path));
		rugged_exception_check(error);
	}

	if (rb_alternates) {
		load_alternates(repo, rb_alternates);
	}

	return rugged_repo_new(klass, repo);
}

/*
 *  call-seq:
 *    Repository.new(path, options = {}) -> repository
 *
 *  Open a Git repository in the given +path+ and return a +Repository+ object
 *  representing it. An exception will be thrown if +path+ doesn't point to a
 *  valid repository. If you need to create a repository from scratch, use
 *  Rugged::Repository.init_at instead.
 *
 *  The +path+ must point to either the actual folder (+.git+) of a Git repository,
 *  or to the directorly that contains the +.git+ folder.
 *
 *  See also Rugged::Repository.discover and Rugged::Repository.bare.
 *
 *  The following options can be passed in the +options+ Hash:
 *
 *  :alternates ::
 *    A list of alternate object folders.
 *
 *  Examples:
 *
 *    Rugged::Repository.new('test/.git') #=> #<Rugged::Repository:0x108849488>
 *    Rugged::Repository.new(path, :alternates => ['./other/repo/.git/objects'])
 */
static VALUE rb_git_repo_new(int argc, VALUE *argv, VALUE klass)
{
	git_repository *repo;
	int error = 0;
	VALUE rb_path, rb_options;

	rb_scan_args(argc, argv, "10:", &rb_path, &rb_options);
	Check_Type(rb_path, T_STRING);

	error = git_repository_open(&repo, StringValueCStr(rb_path));
	rugged_exception_check(error);

	if (!NIL_P(rb_options)) {
		/* Check for `:alternates` */
		load_alternates(repo, rb_hash_aref(rb_options, CSTR2SYM("alternates")));
	}

	return rugged_repo_new(klass, repo);
}

/*
 *  call-seq:
 *    Repository.init_at(path, is_bare = false, opts = {}) -> repository
 *
 *  Initialize a Git repository in +path+. This implies creating all the
 *  necessary files on the FS, or re-initializing an already existing
 *  repository if the files have already been created.
 *
 *  The +is_bare+ (optional, defaults to false) attribute specifies whether
 *  the Repository should be created on disk as bare or not.
 *  Bare repositories have no working directory and are created in the root
 *  of +path+. Non-bare repositories are created in a +.git+ folder and
 *  use +path+ as working directory.
 *
 *  The following options can be passed in the +options+ Hash:
 *
 *  :backend ::
 *    A Rugged::Backend instance
 *
 *
 *    Rugged::Repository.init_at('repository', :bare) #=> #<Rugged::Repository:0x108849488>
 */
static VALUE rb_git_repo_init_at(int argc, VALUE *argv, VALUE klass)
{
	git_repository *repo = NULL;
	VALUE rb_path, rb_is_bare, rb_options;
	int error;

	rb_scan_args(argc, argv, "11:", &rb_path, &rb_is_bare, &rb_options);
	Check_Type(rb_path, T_STRING);

	if (!NIL_P(rb_options)) {
		/* Check for `:backend` */
		VALUE rb_backend = rb_hash_aref(rb_options, CSTR2SYM("backend"));

		if (rb_backend && !NIL_P(rb_backend)) {
			rugged_repo_new_with_backend(&repo, rb_path, rb_backend);
		}
	}

	if(!repo) {
		error =	git_repository_init(&repo, StringValueCStr(rb_path), RTEST(rb_is_bare));
		rugged_exception_check(error);
	}

	return rugged_repo_new(klass, repo);
}

static void parse_clone_options(git_clone_options *ret, VALUE rb_options, struct rugged_remote_cb_payload *remote_payload)
{
	VALUE val;

	if (NIL_P(rb_options))
		return;

	val = rb_hash_aref(rb_options, CSTR2SYM("bare"));
	if (RTEST(val))
		ret->bare = 1;

	val = rb_hash_aref(rb_options, CSTR2SYM("checkout_branch"));
	if (!NIL_P(val)) {
		Check_Type(val, T_STRING);
		ret->checkout_branch = StringValueCStr(val);
	}

	rugged_remote_init_callbacks_and_payload_from_options(rb_options, &ret->fetch_opts.callbacks, remote_payload);
}

/*
 *  call-seq:
 *    Repository.clone_at(url, local_path[, options]) -> repository
 *
 *  Clone a repository from +url+ to +local_path+.
 *
 *  The following options can be passed in the +options+ Hash:
 *
 *  :bare ::
 *    If +true+, the clone will be created as a bare repository.
 *    Defaults to +false+.
 *
 *  :checkout_branch ::
 *    The name of a branch to checkout. Defaults to the remote's +HEAD+.
 *
 *  :remote ::
 *    The name to give to the "origin" remote. Defaults to <tt>"origin"</tt>.
 *
 *  :ignore_cert_errors ::
 *    If set to +true+, errors while validating the remote's host certificate will be ignored.
 *
 *  :credentials ::
 *    The credentials to use for the clone 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 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", {
 *      transfer_progress: lambda { |total_objects, indexed_objects, received_objects, local_objects, total_deltas, indexed_deltas, received_bytes|
 *        # ...
 *      }
 *    })
 */
static VALUE rb_git_repo_clone_at(int argc, VALUE *argv, VALUE klass)
{
	VALUE url, local_path, rb_options_hash;
	git_clone_options options = GIT_CLONE_OPTIONS_INIT;
	struct rugged_remote_cb_payload remote_payload = { Qnil, Qnil, Qnil, Qnil, 0 };
	git_repository *repo;
	int error;

	rb_scan_args(argc, argv, "21", &url, &local_path, &rb_options_hash);
	Check_Type(url, T_STRING);
	Check_Type(local_path, T_STRING);

	parse_clone_options(&options, rb_options_hash, &remote_payload);

	error = git_clone(&repo, StringValueCStr(url), StringValueCStr(local_path), &options);

	if (RTEST(remote_payload.exception))
		rb_jump_tag(remote_payload.exception);
	rugged_exception_check(error);

	return rugged_repo_new(klass, repo);
}

#define RB_GIT_REPO_OWNED_GET(_klass, _object) \
	VALUE rb_data = rb_iv_get(self, "@" #_object); \
	if (NIL_P(rb_data)) { \
		git_repository *repo; \
		git_##_object *data; \
		int error; \
		Data_Get_Struct(self, git_repository, repo); \
		error = git_repository_##_object(&data, repo); \
		rugged_exception_check(error); \
		rb_data = rugged_##_object##_new(_klass, self, data); \
		rb_iv_set(self, "@" #_object, rb_data); \
	} \
	return rb_data; \

#define RB_GIT_REPO_OWNED_SET(_klass, _object) \
	VALUE rb_old_data; \
	git_repository *repo; \
	git_##_object *data; \
	if (!rb_obj_is_kind_of(rb_data, _klass))\
		rb_raise(rb_eTypeError, \
			"The given object is not a Rugged::" #_object); \
	if (!NIL_P(rugged_owner(rb_data))) \
		rb_raise(rb_eRuntimeError, \
			"The given object is already owned by another repository"); \
	Data_Get_Struct(self, git_repository, repo); \
	Data_Get_Struct(rb_data, git_##_object, data); \
	git_repository_set_##_object(repo, data); \
	rb_old_data = rb_iv_get(self, "@" #_object); \
	if (!NIL_P(rb_old_data)) rugged_set_owner(rb_old_data, Qnil); \
	rugged_set_owner(rb_data, self); \
	rb_iv_set(self, "@" #_object, rb_data); \
	return Qnil; \


/*
 *  call-seq:
 *    repo.index = idx
 *
 *  Set the index for this +Repository+. +idx+ must be a instance of
 *  Rugged::Index. This index will be used internally by all
 *  operations that use the Git index on +repo+.
 *
 *  Note that it's not necessary to set the +index+ for any repository;
 *  by default repositories are loaded with the index file that can be
 *  located on the +.git+ folder in the filesystem.
 */
static VALUE rb_git_repo_set_index(VALUE self, VALUE rb_data)
{
	RB_GIT_REPO_OWNED_SET(rb_cRuggedIndex, index);
}

/*
 *  call-seq:
 *    repo.index -> idx
 *
 *  Return the default index for this repository.
 */
static VALUE rb_git_repo_get_index(VALUE self)
{
	RB_GIT_REPO_OWNED_GET(rb_cRuggedIndex, index);
}

/*
 *  call-seq:
 *    repo.config = cfg
 *
 *  Set the configuration file for this +Repository+. +cfg+ must be a instance of
 *  Rugged::Config. This config file will be used internally by all
 *  operations that need to lookup configuration settings on +repo+.
 *
 *  Note that it's not necessary to set the +config+ for any repository;
 *  by default repositories are loaded with their relevant config files
 *  on the filesystem, and the corresponding global and system files if
 *  they can be found.
 */
static VALUE rb_git_repo_set_config(VALUE self, VALUE rb_data)
{
	RB_GIT_REPO_OWNED_SET(rb_cRuggedConfig, config);
}

/*
 *  call-seq:
 *    repo.config -> cfg
 *
 *  Return a Rugged::Config object representing this repository's config.
 */
static VALUE rb_git_repo_get_config(VALUE self)
{
	RB_GIT_REPO_OWNED_GET(rb_cRuggedConfig, config);
}

/*
 *  call-seq:
 *    repo.ident = ident
 *
 *  Set the identity to be used for writing reflogs.
 *
 *  +ident+ can be either +nil+ or a Hash containing +name+ and/or +email+ entries.
 */
static VALUE rb_git_repo_set_ident(VALUE self, VALUE rb_ident) {
	VALUE rb_val;

	git_repository *repo;
	const char *name = NULL, *email = NULL;

	Data_Get_Struct(self, git_repository, repo);

	if (!NIL_P(rb_ident)) {
		Check_Type(rb_ident, T_HASH);

		if (!NIL_P(rb_val = rb_hash_aref(rb_ident, CSTR2SYM("name")))) {
			Check_Type(rb_val, T_STRING);
			name = StringValueCStr(rb_val);
		}

		if (!NIL_P(rb_val = rb_hash_aref(rb_ident, CSTR2SYM("email")))) {
			Check_Type(rb_val, T_STRING);
			email = StringValueCStr(rb_val);
		}
	}

	rugged_exception_check(
		git_repository_set_ident(repo, name, email)
	);

	return Qnil;
}

/*
 *  call-seq:
 *    repo.ident -> ident
 *
 *  Return a Hash containing the identity that is used to write reflogs.
 *
 *  +ident+ is a Hash containing +name+ and/or +email+ entries, or `nil`.
 */
static VALUE rb_git_repo_get_ident(VALUE self)
{
	VALUE rb_ident = rb_hash_new();

	git_repository *repo;
	const char *name = NULL, *email = NULL;

	Data_Get_Struct(self, git_repository, repo);

	rugged_exception_check(
		git_repository_ident(&name, &email, repo)
	);

	if (name) {
		rb_hash_aset(rb_ident, CSTR2SYM("name"), rb_str_new_utf8(name));
	}

	if (email) {
		rb_hash_aset(rb_ident, CSTR2SYM("email"), rb_str_new_utf8(email));
	}

	return rb_ident;
}

/*
 *  call-seq:
 *    repo.merge_base(oid1, oid2, ...)
 *    repo.merge_base(ref1, ref2, ...)
 *    repo.merge_base(commit1, commit2, ...)
 *
 *  Find a merge base, given two or more commits or oids.
 *  Returns nil if a merge base is not found.
 */
static VALUE rb_git_repo_merge_base(VALUE self, VALUE rb_args)
{
	int error = GIT_OK, i;
	git_repository *repo;
	git_oid base, *input_array = xmalloc(sizeof(git_oid) * RARRAY_LEN(rb_args));
	int len = (int)RARRAY_LEN(rb_args);

	if (len < 2)
		rb_raise(rb_eArgError, "wrong number of arguments (%d for 2+)", len);

	Data_Get_Struct(self, git_repository, repo);

	for (i = 0; !error && i < len; ++i) {
		error = rugged_oid_get(&input_array[i], repo, rb_ary_entry(rb_args, i));
	}

	if (error) {
		xfree(input_array);
		rugged_exception_check(error);
	}

	error = git_merge_base_many(&base, repo, len, input_array);
	xfree(input_array);

	if (error == GIT_ENOTFOUND)
		return Qnil;

	rugged_exception_check(error);

	return rugged_create_oid(&base);
}

/*
 *  call-seq:
 *    repo.merge_bases(oid1, oid2, ...) -> Array
 *    repo.merge_bases(ref1, ref2, ...) -> Array
 *    repo.merge_bases(commit1, commit2, ...) -> Array
 *
 *  Find all merge bases, given two or more commits or oids.
 *  Returns an empty array if no merge bases are found.
 */
static VALUE rb_git_repo_merge_bases(VALUE self, VALUE rb_args)
{
	int error = GIT_OK;
	size_t i, len = (size_t)RARRAY_LEN(rb_args);
	git_repository *repo;
	git_oidarray bases = {NULL, 0};
	git_oid *input_array;

	VALUE rb_bases;

	if (len < 2)
		rb_raise(rb_eArgError, "wrong number of arguments (%ld for 2+)", RARRAY_LEN(rb_args));

	Data_Get_Struct(self, git_repository, repo);

	input_array = xmalloc(sizeof(git_oid) * len);

	for (i = 0; !error && i < len; ++i) {
		error = rugged_oid_get(&input_array[i], repo, rb_ary_entry(rb_args, i));
	}

	if (error) {
		xfree(input_array);
		rugged_exception_check(error);
	}

	error = git_merge_bases_many(&bases, repo, len, input_array);
	xfree(input_array);

	if (error != GIT_ENOTFOUND)
		rugged_exception_check(error);

	rb_bases = rb_ary_new2(bases.count);

	for (i = 0; i < bases.count; ++i) {
		rb_ary_push(rb_bases, rugged_create_oid(&bases.ids[i]));
	}

	git_oidarray_free(&bases);

	return rb_bases;
}

/*
 *  call-seq:
 *    repo.merge_analysis(their_commit) -> Array
 *
 *  Analyzes the given commit and determines the opportunities for merging
 *  it into the repository's HEAD. Returns an Array containing a combination
 *  of the following symbols:
 *
 *  :normal ::
 *    A "normal" merge is possible, both HEAD and the given commit have
 *    diverged from their common ancestor. The divergent commits must be
 *    merged.
 *
 *  :up_to_date ::
 *    The given commit is reachable from HEAD, meaning HEAD is up-to-date
 *    and no merge needs to be performed.
 *
 *  :fastforward ::
 *    The given commit is a fast-forward from HEAD and no merge needs to be
 *    performed. HEAD can simply be set to the given commit.
 *
 *  :unborn ::
 *    The HEAD of the current repository is "unborn" and does not point to
 *    a valid commit. No merge can be performed, but the caller may wish
 *    to simply set HEAD to the given commit.
 */
static VALUE rb_git_repo_merge_analysis(int argc, VALUE *argv, VALUE self)
{
	int error;
	git_repository *repo;
	git_commit *their_commit;
	git_annotated_commit *annotated_commit;
	git_merge_analysis_t analysis;
	git_merge_preference_t preference;
	VALUE rb_their_commit, result;

	rb_scan_args(argc, argv, "10", &rb_their_commit);

	Data_Get_Struct(self, git_repository, repo);

	if (TYPE(rb_their_commit) == T_STRING) {
		rb_their_commit = rugged_object_rev_parse(self, rb_their_commit, 1);
	}

	if (!rb_obj_is_kind_of(rb_their_commit, rb_cRuggedCommit)) {
		rb_raise(rb_eArgError, "Expected a Rugged::Commit.");
	}

	Data_Get_Struct(rb_their_commit, git_commit, their_commit);

	error = git_annotated_commit_lookup(&annotated_commit, repo, git_commit_id(their_commit));
	rugged_exception_check(error);

	error = git_merge_analysis(&analysis, &preference, repo,
				   /* hack as we currently only do one commit */
				   (const git_annotated_commit **) &annotated_commit, 1);
	git_annotated_commit_free(annotated_commit);
	rugged_exception_check(error);

	result = rb_ary_new();
	if (analysis & GIT_MERGE_ANALYSIS_NORMAL)
		rb_ary_push(result, CSTR2SYM("normal"));
	if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE)
		rb_ary_push(result, CSTR2SYM("up_to_date"));
	if (analysis & GIT_MERGE_ANALYSIS_FASTFORWARD)
		rb_ary_push(result, CSTR2SYM("fastforward"));
	if (analysis & GIT_MERGE_ANALYSIS_UNBORN)
		rb_ary_push(result, CSTR2SYM("unborn"));

	return result;
}

/*
 *  call-seq:
 *    repo.revert_commit(revert_commit, our_commit, options = {}) -> index
 *    
 *	Reverts the given commit against the given "our" commit, producing an
 *	index that reflects the result of the revert.
 */
static VALUE rb_git_repo_revert_commit(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_revert_commit, rb_our_commit, rb_options;
	git_commit *revert_commit, *our_commit;
	git_index *index;
	git_repository *repo;
	git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
	unsigned int mainline = 0;
	int error;

	rb_scan_args(argc, argv, "20:", &rb_revert_commit, &rb_our_commit, &rb_options);

	if (TYPE(rb_revert_commit) == T_STRING)
		rb_revert_commit = rugged_object_rev_parse(self, rb_revert_commit, 1);

	if (TYPE(rb_our_commit) == T_STRING)
		rb_our_commit = rugged_object_rev_parse(self, rb_our_commit, 1);

	if (!rb_obj_is_kind_of(rb_revert_commit, rb_cRuggedCommit) ||
		!rb_obj_is_kind_of(rb_our_commit, rb_cRuggedCommit)) {
		rb_raise(rb_eArgError, "Expected a Rugged::Commit.");
	}

	if (!NIL_P(rb_options)) {
		VALUE rb_mainline;

		Check_Type(rb_options, T_HASH);
		rugged_parse_merge_options(&opts, rb_options);

		rb_mainline = rb_hash_aref(rb_options, CSTR2SYM("mainline"));
		if (!NIL_P(rb_mainline)) {
			Check_Type(rb_mainline, T_FIXNUM);
			mainline = FIX2UINT(rb_mainline);
		}
	}

	Data_Get_Struct(self, git_repository, repo);
	Data_Get_Struct(rb_revert_commit, git_commit, revert_commit);
	Data_Get_Struct(rb_our_commit, git_commit, our_commit);

	error = git_revert_commit(&index, repo, revert_commit, our_commit, mainline, &opts);
	if (error == GIT_EMERGECONFLICT)
		return Qnil;

	rugged_exception_check(error);

	return rugged_index_new(rb_cRuggedIndex, self, index);
}

/*
 *  call-seq:
 *    repo.merge_commits(our_commit, their_commit, options = {}) -> index
 *
 *  Merges the two given commits, returning a Rugged::Index that reflects
 *  the result of the merge.
 *
 *  +our_commit+ and +their_commit+ can either be Rugged::Commit objects,
 *  or OIDs resolving to the former.
 */
static VALUE rb_git_repo_merge_commits(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_our_commit, rb_their_commit, rb_options;
	git_commit *our_commit, *their_commit;
	git_index *index;
	git_repository *repo;
	git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
	int error;

	rb_scan_args(argc, argv, "20:", &rb_our_commit, &rb_their_commit, &rb_options);

	if (TYPE(rb_our_commit) == T_STRING) {
		rb_our_commit = rugged_object_rev_parse(self, rb_our_commit, 1);
	}

	if (!rb_obj_is_kind_of(rb_our_commit, rb_cRuggedCommit)) {
		rb_raise(rb_eArgError, "Expected a Rugged::Commit.");
	}

	if (TYPE(rb_their_commit) == T_STRING) {
		rb_their_commit = rugged_object_rev_parse(self, rb_their_commit, 1);
	}

	if (!rb_obj_is_kind_of(rb_their_commit, rb_cRuggedCommit)) {
		rb_raise(rb_eArgError, "Expected a Rugged::Commit.");
	}

	if (!NIL_P(rb_options)) {
		Check_Type(rb_options, T_HASH);
		rugged_parse_merge_options(&opts, rb_options);
	}

	Data_Get_Struct(self, git_repository, repo);
	Data_Get_Struct(rb_our_commit, git_commit, our_commit);
	Data_Get_Struct(rb_their_commit, git_commit, their_commit);

	error = git_merge_commits(&index, repo, our_commit, their_commit, &opts);
	if (error == GIT_EMERGECONFLICT)
		return Qnil;

	rugged_exception_check(error);

	return rugged_index_new(rb_cRuggedIndex, self, index);
}

/*
 *  call-seq:
 *    repo.include?(oid) -> true or false
 *    repo.exists?(oid) -> true or false
 *
 *  Return whether an object with the given SHA1 OID (represented as
 *  a hex string of at least 7 characters) exists in the repository.
 *
 *    repo.include?("d8786bfc97485e8d7b19b21fb88c8ef1f199fc3f") #=> true
 *    repo.include?("d8786bfc") #=> true
 */
static VALUE rb_git_repo_exists(VALUE self, VALUE hex)
{
	git_repository *repo;
	git_odb *odb;
	git_oid oid;
	int error;

	Data_Get_Struct(self, git_repository, repo);
	Check_Type(hex, T_STRING);

	error = git_oid_fromstrn(&oid, RSTRING_PTR(hex), RSTRING_LEN(hex));
	rugged_exception_check(error);

	error = git_repository_odb(&odb, repo);
	rugged_exception_check(error);

	error = git_odb_exists_prefix(NULL, odb, &oid, RSTRING_LEN(hex));
	git_odb_free(odb);

	if (error == 0 || error == GIT_EAMBIGUOUS)
		return Qtrue;

	return Qfalse;
}

/*
 *  call-seq:
 *    repo.read(oid) -> str
 *
 *  Read and return the raw data of the object identified by the given +oid+.
 */
static VALUE rb_git_repo_read(VALUE self, VALUE hex)
{
	git_repository *repo;
	git_oid oid;
	int error;

	Data_Get_Struct(self, git_repository, repo);
	Check_Type(hex, T_STRING);

	error = git_oid_fromstr(&oid, StringValueCStr(hex));
	rugged_exception_check(error);

	return rugged_raw_read(repo, &oid);
}

/*
 *  call-seq:
 *    repo.read_header(oid) -> hash
 *
 *  Read and return the header information in +repo+'s ODB
 *  for the object identified by the given +oid+.
 *
 *  Returns a Hash object with the following key/value pairs:
 *
 *  :type ::
 *    A Symbol denoting the object's type. Possible values are:
 *    +:tree+, +:blob+, +:commit+ or +:tag+.
 *  :len ::
 *    A Number representing the object's length, in bytes.
 */
static VALUE rb_git_repo_read_header(VALUE self, VALUE hex)
{
	git_repository *repo;
	git_oid oid;
	git_odb *odb;
	git_otype type;
	size_t len;
	VALUE rb_hash;
	int error;

	Data_Get_Struct(self, git_repository, repo);
	Check_Type(hex, T_STRING);

	error = git_oid_fromstr(&oid, StringValueCStr(hex));
	rugged_exception_check(error);

	error = git_repository_odb(&odb, repo);
	rugged_exception_check(error);

	error = git_odb_read_header(&len, &type, odb, &oid);
	git_odb_free(odb);
	rugged_exception_check(error);

	rb_hash = rb_hash_new();
	rb_hash_aset(rb_hash, CSTR2SYM("type"), CSTR2SYM(git_object_type2string(type)));
	rb_hash_aset(rb_hash, CSTR2SYM("len"), INT2FIX(len));

	return rb_hash;
}

/**
 *  call-seq:
 *    repo.expand_oids([oid..], object_type = :any) -> hash
 *
 *  Expand a list of short oids to their full value, assuming they exist
 *  in the repository. If `object_type` is passed, OIDs are expected to be
 *  of the given type.
 *
 *  Returns a hash of `{ short_oid => full_oid }` for the short OIDs which
 *  exist in the repository and match the expected object type. Missing OIDs
 *  will not appear in the resulting hash.
 */
static VALUE rb_git_repo_expand_oids(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_result, rb_oids, rb_expected_type;

	git_otype expected_type = GIT_OBJ_ANY;

	git_repository *repo;
	git_oid oid;
	git_odb *odb;
	int i, error;

	Data_Get_Struct(self, git_repository, repo);

	rb_scan_args(argc, argv, "11", &rb_oids, &rb_expected_type);

	Check_Type(rb_oids, T_ARRAY);
	expected_type = rugged_otype_get(rb_expected_type);

	error = git_repository_odb(&odb, repo);
	rugged_exception_check(error);

	rb_result = rb_hash_new();

	for (i = 0; i < RARRAY_LEN(rb_oids); ++i) {
		VALUE hex_oid = rb_ary_entry(rb_oids, i);
		git_oid found_oid;

		if (TYPE(hex_oid) != T_STRING) {
			git_odb_free(odb);
			rb_raise(rb_eTypeError, "Expected a SHA1 OID");
		}

		error = git_oid_fromstrn(&oid, RSTRING_PTR(hex_oid), RSTRING_LEN(hex_oid));
		if (error < 0) {
			git_odb_free(odb);
			rugged_exception_check(error);
		}

		error = git_odb_exists_prefix(&found_oid, odb, &oid, RSTRING_LEN(hex_oid));

		if (!error) {
			if (expected_type != GIT_OBJ_ANY) {
				size_t found_size;
				git_otype found_type;

				if (git_odb_read_header(&found_size, &found_type, odb, &found_oid) < 0)
					continue;

				if (found_type != expected_type)
					continue;
			}

			rb_hash_aset(rb_result, hex_oid, rugged_create_oid(&found_oid));
		}
	}

	git_odb_free(odb);
	return rb_result;
}

/*
 *  call-seq:
 *    repo.descendant_of?(commit, ancestor) -> true or false
 *
 *  +commit+ and +ancestor+ must be String commit OIDs or instances of Rugged::Commit.
 *
 *  Returns true if +commit+ is a descendant of +ancestor+, or false if not.
 */
static VALUE rb_git_repo_descendant_of(VALUE self, VALUE rb_commit, VALUE rb_ancestor)
{
	int result;
	int error;
	git_repository *repo;
	git_oid commit, ancestor;

	Data_Get_Struct(self, git_repository, repo);

	error = rugged_oid_get(&commit, repo, rb_commit);
	rugged_exception_check(error);

	error = rugged_oid_get(&ancestor, repo, rb_ancestor);
	rugged_exception_check(error);

	result = git_graph_descendant_of(repo, &commit, &ancestor);
	rugged_exception_check(result);

	return result ? Qtrue : Qfalse;
}

/*
 *  call-seq:
 *    Repository.hash_data(str, type) -> oid
 *
 *  Hash the contents of +str+ as raw bytes (ignoring any encoding
 *  information) and adding the relevant header corresponding to +type+,
 *  and return a hex string representing the result from the hash.
 *
 *    Repository.hash_data('hello world', :commit) #=> "de5ba987198bcf2518885f0fc1350e5172cded78"
 *
 *    Repository.hash_data('hello_world', :tag) #=> "9d09060c850defbc7711d08b57def0d14e742f4e"
 */
static VALUE rb_git_repo_hash(VALUE self, VALUE rb_buffer, VALUE rb_type)
{
	int error;
	git_oid oid;

	Check_Type(rb_buffer, T_STRING);

	error = git_odb_hash(&oid,
		RSTRING_PTR(rb_buffer),
		RSTRING_LEN(rb_buffer),
		rugged_otype_get(rb_type)
	);
	rugged_exception_check(error);

	return rugged_create_oid(&oid);
}

/*
 *  call-seq:
 *    Repository.hash_file(path, type) -> oid
 *
 *  Hash the contents of the file pointed at by +path+, assuming
 *  that it'd be stored in the ODB with the given +type+, and return
 *  a hex string representing the SHA1 OID resulting from the hash.
 *
 *    Repository.hash_file('foo.txt', :commit) #=> "de5ba987198bcf2518885f0fc1350e5172cded78"
 *
 *    Repository.hash_file('foo.txt', :tag) #=> "9d09060c850defbc7711d08b57def0d14e742f4e"
 */
static VALUE rb_git_repo_hashfile(VALUE self, VALUE rb_path, VALUE rb_type)
{
	int error;
	git_oid oid;

	Check_Type(rb_path, T_STRING);

	error = git_odb_hashfile(&oid,
		StringValueCStr(rb_path),
		rugged_otype_get(rb_type)
	);
	rugged_exception_check(error);

	return rugged_create_oid(&oid);
}

/*
 *  call-seq:
 *    repo.write(buffer, type) -> oid
 *
 *  Write the data contained in the +buffer+ string as a raw object of the
 *  given +type+ into the repository's object database.
 *
 *  +type+ can be either +:tag+, +:commit+, +:tree+ or +:blob+.
 *
 *  Returns the newly created object's oid.
 */
static VALUE rb_git_repo_write(VALUE self, VALUE rb_buffer, VALUE rub_type)
{
	git_repository *repo;
	git_odb_stream *stream;

	git_odb *odb;
	git_oid oid;
	int error;

	git_otype type;

	Data_Get_Struct(self, git_repository, repo);
	Check_Type(rb_buffer, T_STRING);

	error = git_repository_odb(&odb, repo);
	rugged_exception_check(error);

	type = rugged_otype_get(rub_type);

	error = git_odb_open_wstream(&stream, odb, RSTRING_LEN(rb_buffer), type);
	git_odb_free(odb);
	rugged_exception_check(error);

	error = git_odb_stream_write(stream, RSTRING_PTR(rb_buffer), RSTRING_LEN(rb_buffer));
	if (!error)
		error = git_odb_stream_finalize_write(&oid, stream);

	git_odb_stream_free(stream);
	rugged_exception_check(error);

	return rugged_create_oid(&oid);
}

#define RB_GIT_REPO_GETTER(method) \
	git_repository *repo; \
	int error; \
	Data_Get_Struct(self, git_repository, repo); \
	error = git_repository_##method(repo); \
	rugged_exception_check(error); \
	return error ? Qtrue : Qfalse; \

/*
 *  call-seq:
 *    repo.bare? -> true or false
 *
 *  Return whether a repository is bare or not. A bare repository has no
 *  working directory.
 */
static VALUE rb_git_repo_is_bare(VALUE self)
{
	RB_GIT_REPO_GETTER(is_bare);
}

/*
 *  call-seq:
 *    repo.shallow? -> true or false
 *
 *  Return whether a repository is a shallow clone or not. A shallow clone has
 *  a truncated history and can not be cloned or fetched from, nor can be
 *  pushed from nor into it.
 */
static VALUE rb_git_repo_is_shallow(VALUE self)
{
	RB_GIT_REPO_GETTER(is_shallow);
}

/*
 *  call-seq:
 *    repo.empty? -> true or false
 *
 *  Return whether a repository is empty or not. An empty repository has just
 *  been initialized and has no commits yet.
 */
static VALUE rb_git_repo_is_empty(VALUE self)
{
	RB_GIT_REPO_GETTER(is_empty);
}

/*
 *  call-seq:
 *    repo.head_detached? -> true or false
 *
 *  Return whether the +HEAD+ of a repository is detached or not.
 */
static VALUE rb_git_repo_head_detached(VALUE self)
{
	RB_GIT_REPO_GETTER(head_detached);
}

/*
 *  call-seq:
 *    repo.head_unborn? -> true or false
 *
 *  Return whether the current branch is unborn (+HEAD+ points to a
 *  non-existent branch).
 */
static VALUE rb_git_repo_head_unborn(VALUE self)
{
	RB_GIT_REPO_GETTER(head_unborn);
}

/*
 *  call-seq:
 *    repo.head = str
 *
 *  Make the repository's +HEAD+ point to the specified reference.
 */
static VALUE rb_git_repo_set_head(VALUE self, VALUE rb_head)
{
	git_repository *repo;
	int error;

	Data_Get_Struct(self, git_repository, repo);

	Check_Type(rb_head, T_STRING);
	error = git_repository_set_head(repo, StringValueCStr(rb_head));
	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    repo.head -> ref
 *
 *  Retrieve and resolve the reference pointed at by the repository's +HEAD+.
 *
 *  Returns +nil+ if +HEAD+ is missing.
 */
static VALUE rb_git_repo_get_head(VALUE self)
{
	git_repository *repo;
	git_reference *head;
	int error;

	Data_Get_Struct(self, git_repository, repo);

	error = git_repository_head(&head, repo);
	if (error == GIT_ENOTFOUND)
		return Qnil;
	else
		rugged_exception_check(error);

	return rugged_ref_new(rb_cRuggedReference, self, head);
}

/*
 *  call-seq:
 *    repo.path -> path
 *
 *  Return the full, normalized path to this repository. For non-bare repositories,
 *  this is the path of the actual +.git+ folder, not the working directory.
 *
 *    repo.path #=> "/home/foo/workthing/.git"
 */
static VALUE rb_git_repo_path(VALUE self)
{
	git_repository *repo;
	const char *path;

	Data_Get_Struct(self, git_repository, repo);
	path = git_repository_path(repo);

	return path ? rb_str_new_utf8(path) : Qnil;
}

/*
 *  call-seq:
 *    repo.workdir -> path or nil
 *
 *  Return the working directory for this repository, or +nil+ if
 *  the repository is bare.
 *
 *    repo1.bare? #=> false
 *    repo1.workdir #=> "/home/foo/workthing/"
 *
 *    repo2.bare? #=> true
 *    repo2.workdir #=> nil
 */
static VALUE rb_git_repo_workdir(VALUE self)
{
	git_repository *repo;
	const char *workdir;

	Data_Get_Struct(self, git_repository, repo);
	workdir = git_repository_workdir(repo);

	return workdir ? rb_str_new_utf8(workdir) : Qnil;
}

/*
 *  call-seq:
 *    repo.workdir = path
 *
 *  Sets the working directory of +repo+ to +path+. All internal
 *  operations on +repo+ that affect the working directory will
 *  instead use +path+.
 *
 *  The +workdir+ can be set on bare repositories to temporarily
 *  turn them into normal repositories.
 *
 *    repo.bare? #=> true
 *    repo.workdir = "/tmp/workdir"
 *    repo.bare? #=> false
 *    repo.checkout
 */
static VALUE rb_git_repo_set_workdir(VALUE self, VALUE rb_workdir)
{
	git_repository *repo;

	Data_Get_Struct(self, git_repository, repo);
	Check_Type(rb_workdir, T_STRING);

	rugged_exception_check(
		git_repository_set_workdir(repo, StringValueCStr(rb_workdir), 0)
	);

	return Qnil;
}

/*
 *  call-seq:
 *    Repository.discover(path = nil, across_fs = true) -> repository
 *
 *  Traverse +path+ upwards until a Git working directory with a +.git+
 *  folder has been found, open it and return it as a +Repository+
 *  object.
 *
 *  If +path+ is +nil+, the current working directory will be used as
 *  a starting point.
 *
 *  If +across_fs+ is +true+, the traversal won't stop when reaching
 *  a different device than the one that contained +path+ (only applies
 *  to UNIX-based OSses).
 */
static VALUE rb_git_repo_discover(int argc, VALUE *argv, VALUE klass)
{
	git_repository *repo;
	VALUE rb_path, rb_across_fs;
	git_buf repository_path = { NULL };
	int error, across_fs = 0;

	rb_scan_args(argc, argv, "02", &rb_path, &rb_across_fs);

	if (NIL_P(rb_path)) {
		VALUE rb_dir = rb_const_get(rb_cObject, rb_intern("Dir"));
		rb_path = rb_funcall(rb_dir, rb_intern("pwd"), 0);
	}

	if (!NIL_P(rb_across_fs)) {
		across_fs = rugged_parse_bool(rb_across_fs);
	}

	Check_Type(rb_path, T_STRING);

	error = git_repository_discover(
		&repository_path,
		StringValueCStr(rb_path),
		across_fs,
		NULL
	);

	rugged_exception_check(error);

	error = git_repository_open(&repo, repository_path.ptr);
	git_buf_free(&repository_path);

	rugged_exception_check(error);

	return rugged_repo_new(klass, repo);
}

static VALUE flags_to_rb(unsigned int flags)
{
	VALUE rb_flags = rb_ary_new();

	if (flags & GIT_STATUS_INDEX_NEW)
		rb_ary_push(rb_flags, CSTR2SYM("index_new"));

	if (flags & GIT_STATUS_INDEX_MODIFIED)
		rb_ary_push(rb_flags, CSTR2SYM("index_modified"));

	if (flags & GIT_STATUS_INDEX_DELETED)
		rb_ary_push(rb_flags, CSTR2SYM("index_deleted"));

	if (flags & GIT_STATUS_WT_NEW)
		rb_ary_push(rb_flags, CSTR2SYM("worktree_new"));

	if (flags & GIT_STATUS_WT_MODIFIED)
		rb_ary_push(rb_flags, CSTR2SYM("worktree_modified"));

	if (flags & GIT_STATUS_WT_DELETED)
		rb_ary_push(rb_flags, CSTR2SYM("worktree_deleted"));

	if (flags & GIT_STATUS_IGNORED)
		rb_ary_push(rb_flags, CSTR2SYM("ignored"));

	return rb_flags;
}

static int rugged__status_cb(const char *path, unsigned int flags, void *payload)
{
	rb_funcall((VALUE)payload, rb_intern("call"), 2,
		rb_str_new_utf8(path), flags_to_rb(flags)
	);

	return GIT_OK;
}

/*
 *  call-seq:
 *    repo.status { |file, status_data| block }
 *    repo.status(path) -> status_data
 *
 *  Returns the status for one or more files in the working directory
 *  of the repository. This is equivalent to the +git status+ command.
 *
 *  The returned +status_data+ is always an array containing one or more
 *  status flags as Ruby symbols. Possible flags are:
 *
 *  - +:index_new+: the file is new in the index
 *  - +:index_modified+: the file has been modified in the index
 *  - +:index_deleted+: the file has been deleted from the index
 *  - +:worktree_new+: the file is new in the working directory
 *  - +:worktree_modified+: the file has been modified in the working directory
 *  - +:worktree_deleted+: the file has been deleted from the working directory
 *
 *  If a +block+ is given, status information will be gathered for every
 *  single file on the working dir. The +block+ will be called with the
 *  status data for each file.
 *
 *    repo.status { |file, status_data| puts "#{file} has status: #{status_data.inspect}" }
 *
 *  results in, for example:
 *
 *    src/diff.c has status: [:index_new, :worktree_new]
 *    README has status: [:worktree_modified]
 *
 *  If a +path+ is given instead, the function will return the +status_data+ for
 *  the file pointed to by path, or raise an exception if the path doesn't exist.
 *
 *  +path+ must be relative to the repository's working directory.
 *
 *    repo.status('src/diff.c') #=> [:index_new, :worktree_new]
 */
static VALUE rb_git_repo_status(int argc, VALUE *argv, VALUE self)
{
	int error;
	VALUE rb_path;
	git_repository *repo;

	Data_Get_Struct(self, git_repository, repo);

	if (rb_scan_args(argc, argv, "01", &rb_path) == 1) {
		unsigned int flags;
		Check_Type(rb_path, T_STRING);
		error = git_status_file(&flags, repo, StringValueCStr(rb_path));
		rugged_exception_check(error);

		return flags_to_rb(flags);
	}

	if (!rb_block_given_p())
		rb_raise(rb_eRuntimeError,
			"A block was expected for iterating through "
			"the repository contents.");

	error = git_status_foreach(
		repo,
		&rugged__status_cb,
		(void *)rb_block_proc()
	);

	rugged_exception_check(error);
	return Qnil;
}

static int rugged__each_id_cb(const git_oid *id, void *payload)
{
	int *exception = (int *)payload;
	rb_protect(rb_yield, rugged_create_oid(id), exception);
	return *exception ? GIT_ERROR : GIT_OK;
}

/*
 *  call-seq:
 *    repo.each_id { |id| block }
 *    repo.each_id -> Iterator
 *
 *  Call the given +block+ once with every object ID found in +repo+
 *  and all its alternates. Object IDs are passed as 40-character
 *  strings.
 */
static VALUE rb_git_repo_each_id(VALUE self)
{
	git_repository *repo;
	git_odb *odb;
	int error, exception = 0;

	if (!rb_block_given_p())
		return rb_funcall(self, rb_intern("to_enum"), 1, CSTR2SYM("each_id"));

	Data_Get_Struct(self, git_repository, repo);

	error = git_repository_odb(&odb, repo);
	rugged_exception_check(error);

	error = git_odb_foreach(odb, &rugged__each_id_cb, &exception);
	git_odb_free(odb);

	if (exception)
		rb_jump_tag(exception);
	rugged_exception_check(error);

	return Qnil;
}

static int parse_reset_type(VALUE rb_reset_type)
{
	ID id_reset_type;

	Check_Type(rb_reset_type, T_SYMBOL);
	id_reset_type = SYM2ID(rb_reset_type);

	if (id_reset_type == rb_intern("soft")) {
		return GIT_RESET_SOFT;
	} else if (id_reset_type == rb_intern("mixed")) {
		return GIT_RESET_MIXED;
	} else if (id_reset_type == rb_intern("hard")) {
		return GIT_RESET_HARD;
	} else {
		rb_raise(rb_eArgError,
			"Invalid reset type. Expected `:soft`, `:mixed` or `:hard`");
	}
}

/*
 *  call-seq:
 *    repo.reset(target, reset_type) -> nil
 *
 *  Sets the current head to the specified commit oid and optionally
 *  resets the index and working tree to match.
 *  - +target+: Rugged::Commit, Rugged::Tag or rev that resolves to a commit or tag object
 *  - +reset_type+: +:soft+, +:mixed+ or +:hard+
 *
 *  [:soft]
 *    the head will be moved to the commit.
 *  [:mixed]
 *    will trigger a +:soft+ reset, plus the index will be replaced
 *    with the content of the commit tree.
 *  [:hard]
 *    will trigger a +:mixed+ reset and the working directory will be
 *    replaced with the content of the index. (Untracked and ignored files
 *    will be left alone)
 *
 *  Examples:
 *
 *    repo.reset('origin/master', :hard) #=> nil
 */
static VALUE rb_git_repo_reset(VALUE self, VALUE rb_target, VALUE rb_reset_type)
{
	git_repository *repo;
	int reset_type;
	git_object *target = NULL;
	int error;

	Data_Get_Struct(self, git_repository, repo);

	reset_type = parse_reset_type(rb_reset_type);
	target = rugged_object_get(repo, rb_target, GIT_OBJ_ANY);

	error = git_reset(repo, target, reset_type, NULL);

	git_object_free(target);

	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    repo.reset_path(pathspecs, target=nil) -> nil
 *
 *  Updates entries in the index from the +target+ commit tree, matching
 *  the given +pathspecs+.
 *
 *  Passing a nil +target+ will result in removing
 *  entries in the index matching the provided pathspecs.
 *
 *  - +pathspecs+: list of pathspecs to operate on (+String+ or +Array+ of +String+ objects)
 *  - +target+(optional): Rugged::Commit, Rugged::Tag or rev that resolves to a commit or tag object.
 *
 *  Examples:
 *    reset_path(File.join('subdir','file.txt'), '441034f860c1d5d90e4188d11ae0d325176869a8') #=> nil
 */
static VALUE rb_git_repo_reset_path(int argc, VALUE *argv, VALUE self)
{
	git_repository *repo;
	git_object *target = NULL;
	git_strarray pathspecs;
	VALUE rb_target, rb_paths;
	int error = 0;

	pathspecs.strings = NULL;
	pathspecs.count = 0;

	Data_Get_Struct(self, git_repository, repo);

	rb_scan_args(argc, argv, "11", &rb_paths, &rb_target);

	rugged_rb_ary_to_strarray(rb_paths, &pathspecs);

	if (!NIL_P(rb_target))
		target = rugged_object_get(repo, rb_target, GIT_OBJ_ANY);

	error = git_reset_default(repo, target, &pathspecs);

	xfree(pathspecs.strings);
	git_object_free(target);

	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    repo.close -> nil
 *
 *  Frees all the resources used by this repository immediately. The repository can
 *  still be used after this call. Resources will be opened as necessary.
 *
 *  It is not required to call this method explicitly. Repositories are closed
 *  automatically before garbage collection
 */
static VALUE rb_git_repo_close(VALUE self)
{
	git_repository *repo;
	Data_Get_Struct(self, git_repository, repo);

	git_repository__cleanup(repo);

	return Qnil;
}

/*
 *  call-seq:
 *    repo.namespace = new_namespace
 *
 *  Sets the active namespace for the repository.
 *  If set to nil, no namespace will be active.
 */
static VALUE rb_git_repo_set_namespace(VALUE self, VALUE rb_namespace)
{
	git_repository *repo;
	int error;

	Data_Get_Struct(self, git_repository, repo);

	if (!NIL_P(rb_namespace)) {
		Check_Type(rb_namespace, T_STRING);
		error = git_repository_set_namespace(repo, StringValueCStr(rb_namespace));
	} else {
		error = git_repository_set_namespace(repo, NULL);
	}
	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    repo.namespace -> str
 *
 *  Returns the active namespace for the repository.
 */
static VALUE rb_git_repo_get_namespace(VALUE self)
{
	git_repository *repo;
	const char *namespace;

	Data_Get_Struct(self, git_repository, repo);

	namespace = git_repository_get_namespace(repo);
	return namespace ? rb_str_new_utf8(namespace) : Qnil;
}

/*
 *  call-seq:
 *    repo.ahead_behind(local, upstream) -> Array
 *
 *  Returns a 2 element Array containing the number of commits
 *  that the upstream object is ahead and behind the local object.
 *
 *  +local+ and +upstream+ can either be strings containing SHA1 OIDs or
 *  Rugged::Object instances.
 */
static VALUE rb_git_repo_ahead_behind(VALUE self, VALUE rb_local, VALUE rb_upstream) {
	git_repository *repo;
	int error;
	git_oid local, upstream;
	size_t ahead, behind;
	VALUE rb_result;

	Data_Get_Struct(self, git_repository, repo);

	error = rugged_oid_get(&local, repo, rb_local);
	rugged_exception_check(error);

	error = rugged_oid_get(&upstream, repo, rb_upstream);
	rugged_exception_check(error);

	error = git_graph_ahead_behind(&ahead, &behind, repo, &local, &upstream);
	rugged_exception_check(error);

	rb_result = rb_ary_new2(2);
	rb_ary_push(rb_result, INT2FIX((int) ahead));
	rb_ary_push(rb_result, INT2FIX((int) behind));
	return rb_result;
}

/*
 *  call-seq:
 *    repo.default_signature -> signature or nil
 *
 *  Returns a +Hash+ with the default user +signature+ or +nil+.
 *
 *  Looks up the +user.name+ and +user.email+ from the configuration and
 *  uses the current time as the timestamp, and creates a new signature
 *  based on that information.  It will return +nil+ if either the
 *  +user.name+ or +user.email+ are not set.
 *
 *  Returns a +Hash+:
 *  - +:name+: the +user.name+ config value
 *  - +:email+: the +user.email+ config value
 *  - +:time+: the current time as a +Time+ instance
 */
static VALUE rb_git_repo_default_signature(VALUE self) {
	int error;
	git_repository *repo;
	git_signature *signature;
	VALUE rb_signature;

	Data_Get_Struct(self, git_repository, repo);

	error = git_signature_default(&signature, repo);

	if (error == GIT_ENOTFOUND)
		return Qnil;

	rugged_exception_check(error);

	rb_signature = rugged_signature_new(signature, NULL);
	git_signature_free(signature);
	return rb_signature;
}

void rugged__checkout_progress_cb(
	const char *path,
	size_t completed_steps,
	size_t total_steps,
	void *data
) {
	struct rugged_cb_payload *payload = data;
	VALUE args = rb_ary_new2(4);
	rb_ary_push(args, payload->rb_data);
	rb_ary_push(args, path == NULL ? Qnil : rb_str_new2(path));
	rb_ary_push(args, INT2FIX(completed_steps));
	rb_ary_push(args, INT2FIX(total_steps));

	rb_protect(rugged__block_yield_splat, args, &payload->exception);
}

static int rugged__checkout_notify_cb(
	git_checkout_notify_t why,
	const char *path,
	const git_diff_file *baseline,
	const git_diff_file *target,
	const git_diff_file *workdir,
	void *data
) {
	struct rugged_cb_payload *payload = data;
	VALUE args = rb_ary_new2(5);
	rb_ary_push(args, payload->rb_data);

	switch (why) {
		case GIT_CHECKOUT_NOTIFY_CONFLICT:
			rb_ary_push(args, CSTR2SYM("conflict"));
		break;

		case GIT_CHECKOUT_NOTIFY_DIRTY:
			rb_ary_push(args, CSTR2SYM("dirty"));
		break;

		case GIT_CHECKOUT_NOTIFY_UPDATED:
			rb_ary_push(args, CSTR2SYM("updated"));
		break;

		case GIT_CHECKOUT_NOTIFY_UNTRACKED:
			rb_ary_push(args, CSTR2SYM("untracked"));
		break;

		case GIT_CHECKOUT_NOTIFY_IGNORED:
			rb_ary_push(args, CSTR2SYM("ignored"));
		break;

		default:
			rb_ary_push(args, CSTR2SYM("unknown"));
	}

	rb_ary_push(args, rb_git_delta_file_fromC(baseline));
	rb_ary_push(args, rb_git_delta_file_fromC(target));
	rb_ary_push(args, rb_git_delta_file_fromC(workdir));

	rb_protect(rugged__block_yield_splat, args, &payload->exception);

	return payload->exception ? GIT_ERROR : GIT_OK;
}

/**
 * The caller has to free the returned git_checkout_options paths strings array.
 */
void rugged_parse_checkout_options(git_checkout_options *opts, VALUE rb_options)
{
	VALUE rb_value;

	if (NIL_P(rb_options))
		return;

	Check_Type(rb_options, T_HASH);

	rb_value = rb_hash_aref(rb_options, CSTR2SYM("progress"));
	if (!NIL_P(rb_value)) {
		struct rugged_cb_payload *payload = malloc(sizeof(struct rugged_cb_payload));
		payload->rb_data = rb_value;
		payload->exception = 0;

		opts->progress_payload = payload;
		opts->progress_cb = &rugged__checkout_progress_cb;
	}

	rb_value = rb_hash_aref(rb_options, CSTR2SYM("notify"));
	if (!NIL_P(rb_value)) {
		struct rugged_cb_payload *payload = malloc(sizeof(struct rugged_cb_payload));
		payload->rb_data = rb_value;
		payload->exception = 0;

		opts->notify_payload = payload;
		opts->notify_cb = &rugged__checkout_notify_cb;
	}

	if (!NIL_P(rb_value = rb_hash_aref(rb_options, CSTR2SYM("strategy")))) {
		int i;

		rb_value = rb_ary_to_ary(rb_value);
		for (i = 0; i < RARRAY_LEN(rb_value); ++i) {
			VALUE rb_strategy = rb_ary_entry(rb_value, i);

			if (rb_strategy == CSTR2SYM("safe")) {
				opts->checkout_strategy |= GIT_CHECKOUT_SAFE;
			} else if (rb_strategy == CSTR2SYM("force")) {
				opts->checkout_strategy |= GIT_CHECKOUT_FORCE;
			} else if (rb_strategy == CSTR2SYM("recreate_missing")) {
				opts->checkout_strategy |= GIT_CHECKOUT_RECREATE_MISSING;
			} else if (rb_strategy == CSTR2SYM("allow_conflicts")) {
				opts->checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS;
			} else if (rb_strategy == CSTR2SYM("remove_untracked")) {
				opts->checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED;
			} else if (rb_strategy == CSTR2SYM("remove_ignored")) {
				opts->checkout_strategy |= GIT_CHECKOUT_REMOVE_IGNORED;
			} else if (rb_strategy == CSTR2SYM("update_only")) {
				opts->checkout_strategy |= GIT_CHECKOUT_UPDATE_ONLY;
			} else if (rb_strategy == CSTR2SYM("dont_update_index")) {
				opts->checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX;
			} else if (rb_strategy == CSTR2SYM("no_refresh")) {
				opts->checkout_strategy |= GIT_CHECKOUT_NO_REFRESH;
			} else if (rb_strategy == CSTR2SYM("disable_pathspec_match")) {
				opts->checkout_strategy |= GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH;
			} else if (rb_strategy == CSTR2SYM("skip_locked_directories")) {
				opts->checkout_strategy |= GIT_CHECKOUT_SKIP_LOCKED_DIRECTORIES;
			} else if (rb_strategy == CSTR2SYM("skip_unmerged")) {
				opts->checkout_strategy |= GIT_CHECKOUT_SKIP_UNMERGED;
			} else if (rb_strategy == CSTR2SYM("use_ours")) {
				opts->checkout_strategy |= GIT_CHECKOUT_USE_OURS;
			} else if (rb_strategy == CSTR2SYM("use_theirs")) {
				opts->checkout_strategy |= GIT_CHECKOUT_USE_THEIRS;
			} else if (rb_strategy == CSTR2SYM("update_submodules")) {
				opts->checkout_strategy |= GIT_CHECKOUT_UPDATE_SUBMODULES;
			} else if (rb_strategy == CSTR2SYM("update_submodules_if_changed")) {
				opts->checkout_strategy |= GIT_CHECKOUT_UPDATE_SUBMODULES_IF_CHANGED;
			} else if (rb_strategy != CSTR2SYM("none")) {
				rb_raise(rb_eArgError, "Unknown checkout strategy");
			}
		}
	}

	if (!NIL_P(rb_value = rb_hash_aref(rb_options, CSTR2SYM("notify_flags")))) {
		int i;

		rb_value = rb_ary_to_ary(rb_value);
		for (i = 0; i < RARRAY_LEN(rb_value); ++i) {
			VALUE rb_notify_flag = rb_ary_entry(rb_value, i);

			if (rb_notify_flag == CSTR2SYM("conflict")) {
				opts->notify_flags |= GIT_CHECKOUT_NOTIFY_CONFLICT;
			} else if (rb_notify_flag == CSTR2SYM("dirty")) {
				opts->notify_flags |= GIT_CHECKOUT_NOTIFY_DIRTY;
			} else if (rb_notify_flag == CSTR2SYM("updated")) {
				opts->notify_flags |= GIT_CHECKOUT_NOTIFY_UPDATED;
			} else if (rb_notify_flag == CSTR2SYM("untracked")) {
				opts->notify_flags |= GIT_CHECKOUT_NOTIFY_UNTRACKED;
			} else if (rb_notify_flag == CSTR2SYM("ignored")) {
				opts->notify_flags |= GIT_CHECKOUT_NOTIFY_IGNORED;
			} else if (rb_notify_flag == CSTR2SYM("all")) {
				opts->notify_flags |= GIT_CHECKOUT_NOTIFY_ALL;
			} else if (rb_notify_flag != CSTR2SYM("none")) {
				rb_raise(rb_eArgError, "Unknown checkout notify flag");
			}
		}
	}

	opts->disable_filters = RTEST(rb_hash_aref(rb_options, CSTR2SYM("disable_filters")));

	rb_value = rb_hash_aref(rb_options, CSTR2SYM("dir_mode"));
	if (!NIL_P(rb_value)) {
		opts->dir_mode = FIX2UINT(rb_value);
	}

	rb_value = rb_hash_aref(rb_options, CSTR2SYM("file_mode"));
	if (!NIL_P(rb_value)) {
		opts->file_mode = FIX2UINT(rb_value);
	}

	rb_value = rb_hash_aref(rb_options, CSTR2SYM("file_open_flags"));
	if (!NIL_P(rb_value)) {
		opts->file_mode = FIX2INT(rb_value);
	}

	rb_value = rb_hash_aref(rb_options, CSTR2SYM("target_directory"));
	if (!NIL_P(rb_value)) {
		opts->target_directory = StringValueCStr(rb_value);
	}

	rb_value = rb_hash_aref(rb_options, CSTR2SYM("baseline"));
	if (!NIL_P(rb_value)) {
		if (rb_obj_is_kind_of(rb_value, rb_cRuggedTree)) {
			Data_Get_Struct(rb_value, git_tree, opts->baseline);
		} else {
			rb_raise(rb_eTypeError, "Expected a Rugged::Tree.");
		}
	}

	rb_value = rb_hash_aref(rb_options, CSTR2SYM("paths"));
	rugged_rb_ary_to_strarray(rb_value, &opts->paths);
}

/**
 *  call-seq:
 *    repo.checkout_tree(treeish[, options])
 *
 *  Updates files in the index and working tree to match the content of the
 *  tree pointed at by the +treeish+.
 *
 *  The following options can be passed in the +options+ Hash:
 *
 *  :progress ::
 *    A callback that will be executed for checkout progress notifications.
 *    Up to 3 parameters are passed on each execution:
 *
 *    - The path to the last updated file (or +nil+ on the very first invocation).
 *    - The number of completed checkout steps.
 *    - The number of total checkout steps to be performed.
 *
 *  :notify ::
 *    A callback that will be executed for each checkout notification types specified
 *    with +:notify_flags+. Up to 5 parameters are passed on each execution:
 *
 *    - An array containing the +:notify_flags+ that caused the callback execution.
 *    - The path of the current file.
 *    - A hash describing the baseline blob (or +nil+ if it does not exist).
 *    - A hash describing the target blob (or +nil+ if it does not exist).
 *    - A hash describing the workdir blob (or +nil+ if it does not exist).
 *
 *  :strategy ::
 *    A single symbol or an array of symbols representing the strategies to use when
 *    performing the checkout. Possible values are:
 *
 *    :none ::
 *      Perform a dry run (default).
 *
 *    :safe ::
 *      Allow safe updates that cannot overwrite uncommitted data.
 *
 *    :recreate_missing ::
 *      Allow checkout to recreate missing files.
 *
 *    :force ::
 *      Allow all updates to force working directory to look like index.
 *
 *    :allow_conflicts ::
 *      Allow checkout to make safe updates even if conflicts are found.
 *
 *    :remove_untracked ::
 *      Remove untracked files not in index (that are not ignored).
 *
 *    :remove_ignored ::
 *      Remove ignored files not in index.
 *
 *    :update_only ::
 *      Only update existing files, don't create new ones.
 *
 *    :dont_update_index ::
 *      Normally checkout updates index entries as it goes; this stops that.
 *
 *    :no_refresh ::
 *      Don't refresh index/config/etc before doing checkout.
 *
 *    :disable_pathspec_match ::
 *      Treat pathspec as simple list of exact match file paths.
 *
 *    :skip_locked_directories ::
 *      Ignore directories in use, they will be left empty.
 *
 *    :skip_unmerged ::
 *      Allow checkout to skip unmerged files (NOT IMPLEMENTED).
 *
 *    :use_ours ::
 *      For unmerged files, checkout stage 2 from index (NOT IMPLEMENTED).
 *
 *    :use_theirs ::
 *      For unmerged files, checkout stage 3 from index (NOT IMPLEMENTED).
 *
 *    :update_submodules ::
 *      Recursively checkout submodules with same options (NOT IMPLEMENTED).
 *
 *    :update_submodules_if_changed ::
 *      Recursively checkout submodules if HEAD moved in super repo (NOT IMPLEMENTED).
 *
 *  :disable_filters ::
 *    If +true+, filters like CRLF line conversion will be disabled.
 *
 *  :dir_mode ::
 *    Mode for newly created directories. Default: +0755+.
 *
 *  :file_mode ::
 *    Mode for newly created files. Default: +0755+ or +0644+.
 *
 *  :file_open_flags ::
 *    Mode for opening files. Default: <code>IO::CREAT | IO::TRUNC | IO::WRONLY</code>.
 *
 *  :notify_flags ::
 *    A single symbol or an array of symbols representing the cases in which the +:notify+
 *    callback should be invoked. Possible values are:
 *
 *    :none ::
 *      Do not invoke the +:notify+ callback (default).
 *
 *    :conflict ::
 *      Invoke the callback for conflicting paths.
 *
 *    :dirty ::
 *      Invoke the callback for "dirty" files, i.e. those that do not need an update but
 *      no longer match the baseline.
 *
 *    :updated ::
 *      Invoke the callback for any file that was changed.
 *
 *    :untracked ::
 *      Invoke the callback for untracked files.
 *
 *    :ignored ::
 *      Invoke the callback for ignored files.
 *
 *    :all ::
 *      Invoke the callback for all these cases.
 *
 *  :paths ::
 *    A glob string or an array of glob strings specifying which paths should be taken
 *    into account for the checkout operation. +nil+ will match all files.
 *    Default: +nil+.
 *
 *  :baseline ::
 *    A Rugged::Tree that represents the current, expected contents of the workdir.
 *    Default: +HEAD+.
 *
 *  :target_directory ::
 *    A path to an alternative workdir directory in which the checkout should be performed.
 */
static VALUE rb_git_checkout_tree(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_treeish, rb_options;
	git_repository *repo;
	git_object *treeish;
	git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
	struct rugged_cb_payload *payload;
	int error, exception = 0;

	rb_scan_args(argc, argv, "10:", &rb_treeish, &rb_options);

	if (TYPE(rb_treeish) == T_STRING) {
		rb_treeish = rugged_object_rev_parse(self, rb_treeish, 1);
	}

	if (!rb_obj_is_kind_of(rb_treeish, rb_cRuggedCommit) &&
			!rb_obj_is_kind_of(rb_treeish, rb_cRuggedTag) &&
			!rb_obj_is_kind_of(rb_treeish, rb_cRuggedTree)) {
		rb_raise(rb_eTypeError, "Expected Rugged::Commit, Rugged::Tag or Rugged::Tree");
	}

	Data_Get_Struct(self, git_repository, repo);
	Data_Get_Struct(rb_treeish, git_object, treeish);

	rugged_parse_checkout_options(&opts, rb_options);

	error = git_checkout_tree(repo, treeish, &opts);
	xfree(opts.paths.strings);

	if ((payload = opts.notify_payload) != NULL) {
		exception = payload->exception;
		xfree(opts.notify_payload);
	}

	if ((payload = opts.progress_payload) != NULL) {
		exception = payload->exception;
		xfree(opts.progress_payload);
	}

	if (exception)
		rb_jump_tag(exception);

	rugged_exception_check(error);

	return Qnil;
}

/**
 *  call-seq: repo.checkout_index(index[,options]) -> nil
 *
 *  Updates files in the index and the working tree to match the content of the
 *  commit pointed at by +index+.
 *
 *  See Repository#checkout_tree for a list of supported +options+.
 */
static VALUE rb_git_checkout_index(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_index, rb_options;
	git_repository *repo;
	git_index *index;
	git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
	struct rugged_cb_payload *payload;
	int error, exception = 0;

	rb_scan_args(argc, argv, "10:", &rb_index, &rb_options);

	if (!rb_obj_is_kind_of(rb_index, rb_cRuggedIndex))
		rb_raise(rb_eTypeError, "Expected Rugged::Index");

	Data_Get_Struct(self, git_repository, repo);
	Data_Get_Struct(rb_index, git_index, index);

	rugged_parse_checkout_options(&opts, rb_options);

	error = git_checkout_index(repo, index, &opts);
	xfree(opts.paths.strings);

	if ((payload = opts.notify_payload) != NULL) {
		exception = payload->exception;
		xfree(opts.notify_payload);
	}

	if ((payload = opts.progress_payload) != NULL) {
		exception = payload->exception;
		xfree(opts.progress_payload);
	}

	if (exception)
		rb_jump_tag(exception);

	rugged_exception_check(error);

	return Qnil;
}

/**
 *  call-seq: repo.checkout_head([options]) -> nil
 *
 *  Updates files in the index and the working tree to match the content of the
 *  commit pointed at by +HEAD+.
 *
 *  See Repository#checkout_tree for a list of supported +options+.
 */
static VALUE rb_git_checkout_head(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_options;
	git_repository *repo;
	git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
	struct rugged_cb_payload *payload;
	int error, exception = 0;

	rb_scan_args(argc, argv, "00:", &rb_options);

	Data_Get_Struct(self, git_repository, repo);

	rugged_parse_checkout_options(&opts, rb_options);

	error = git_checkout_head(repo, &opts);
	xfree(opts.paths.strings);

	if ((payload = opts.notify_payload) != NULL) {
		exception = payload->exception;
		xfree(opts.notify_payload);
	}

	if ((payload = opts.progress_payload) != NULL) {
		exception = payload->exception;
		xfree(opts.progress_payload);
	}

	if (exception)
		rb_jump_tag(exception);

	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    repo.path_ignored?(path) -> true or false
 *
 *  Return whether a path is ignored or not.
 */
static VALUE rb_git_repo_is_path_ignored(VALUE self, VALUE rb_path) {
	git_repository *repo;
	const char *path;
	int error;
	int ignored;

	Data_Get_Struct(self, git_repository, repo);
	path = StringValueCStr(rb_path);
	error = git_ignore_path_is_ignored(&ignored, repo, path);
	rugged_exception_check(error);
	return ignored ? Qtrue : Qfalse;
}

static void rugged_parse_cherrypick_options(git_cherrypick_options *opts, VALUE rb_options)
{
	VALUE rb_value;

	if (NIL_P(rb_options))
		return;

	Check_Type(rb_options, T_HASH);

	rb_value = rb_hash_aref(rb_options, CSTR2SYM("mainline"));
	if (!NIL_P(rb_value)) {
		opts->mainline = FIX2UINT(rb_value);
	}
}

static VALUE rugged_create_attr(const char *attr)
{
	switch (git_attr_value(attr)) {
	case GIT_ATTR_TRUE_T:
		return Qtrue;

	case GIT_ATTR_FALSE_T:
		return Qfalse;

	case GIT_ATTR_VALUE_T:
		return rb_str_new2(attr);

	case GIT_ATTR_UNSPECIFIED_T:
	default:
		return Qnil;
	}
}

static int foreach_attr_hash(const char *name, const char *value, void *payload)
{
	VALUE rb_hash = (VALUE)payload;
	rb_hash_aset(rb_hash, rb_str_new2(name), rugged_create_attr(value));
	return 0;
}

static VALUE rb_git_repo_attributes(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_path, rb_names, rb_options;

	git_repository *repo;
	int error, options = 0;

	rb_scan_args(argc, argv, "12", &rb_path, &rb_names, &rb_options);

	Data_Get_Struct(self, git_repository, repo);
	Check_Type(rb_path, T_STRING);

	if (!NIL_P(rb_options)) {
		Check_Type(rb_options, T_FIXNUM);
		options = FIX2INT(rb_options);
	}

	switch (TYPE(rb_names)) {
	case T_ARRAY:
	{
		VALUE rb_result;
		const char **values;
		const char **names;
		long i, num_attr = RARRAY_LEN(rb_names);

		if (num_attr > 32)
			rb_raise(rb_eRuntimeError, "Too many attributes requested");

		values = alloca(num_attr * sizeof(const char *));
		names = alloca(num_attr * sizeof(const char *));

		for (i = 0; i < num_attr; ++i) {
			VALUE attr = rb_ary_entry(rb_names, i);
			Check_Type(attr, T_STRING);
			names[i] = StringValueCStr(attr);
		}

		error = git_attr_get_many(
			values, repo, options,
			StringValueCStr(rb_path),
			(size_t)num_attr, names);

		rugged_exception_check(error);

		rb_result = rb_hash_new();
		for (i = 0; i < num_attr; ++i) {
			VALUE attr = rb_ary_entry(rb_names, i);
			rb_hash_aset(rb_result, attr, rugged_create_attr(values[i]));
		}
		return rb_result;
	}

	case T_STRING:
	{
		const char *value;

		error = git_attr_get(
			&value, repo, options,
			StringValueCStr(rb_path),
			StringValueCStr(rb_names));

		rugged_exception_check(error);

		return rugged_create_attr(value);
	}

	case T_NIL:
	{
		VALUE rb_result = rb_hash_new();

		error = git_attr_foreach(
			repo, options,
			StringValueCStr(rb_path),
			&foreach_attr_hash,
			(void *)rb_result);

		rugged_exception_check(error);
		return rb_result;
	}

	default:
		rb_raise(rb_eTypeError,
			"Invalid attribute name (expected String or Array)");
	}
}

/*
 *  call-seq:
 *    repo.cherrypick(commit[, options]) -> nil
 *
 *  Cherry-pick the given commit and update the index and working
 *  directory accordingly.
 *
 *  `commit` can be either a string containing a commit id or a
 *  `Rugged::Commit` object.
 *
 *  The following options can be passed in the +options+ Hash:
 *
 *  :mainline ::
 *    When cherry-picking a merge, you need to specify the parent number
 *    (starting from 1) which should be considered the mainline.
 */
static VALUE rb_git_repo_cherrypick(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_options, rb_commit;

	git_repository *repo;
	git_commit *commit;
	git_cherrypick_options opts = GIT_CHERRYPICK_OPTIONS_INIT;

	int error;

	rb_scan_args(argc, argv, "10:", &rb_commit, &rb_options);

	if (TYPE(rb_commit) == T_STRING) {
		rb_commit = rugged_object_rev_parse(self, rb_commit, 1);
	}

	if (!rb_obj_is_kind_of(rb_commit, rb_cRuggedCommit)) {
		rb_raise(rb_eArgError, "Expected a Rugged::Commit.");
	}

	Data_Get_Struct(self, git_repository, repo);
	Data_Get_Struct(rb_commit, git_commit, commit);

	rugged_parse_cherrypick_options(&opts, rb_options);

	error = git_cherrypick(repo, commit, &opts);
	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    repo.cherrypick_commit(commit, our_commit, [mainline, options]) -> nil
 *
 *  Cherry-pick the given commit on the given base in-memory and
 *  return an index with the result.
 *
 *  `commit` can be either a string containing a commit id or a
 *  `Rugged::Commit` object.
 *
 *  `our_commit` is the base commit, can be either a string containing
 *  a commit id or a `Rugged::Commit` object.
 *
 *  `mainline` when cherry-picking a merge, this is the parent number
 *   (starting from 1) which should be considered the mainline.
 */
static VALUE rb_git_repo_cherrypick_commit(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_options, rb_commit, rb_our_commit, rb_mainline;

	git_repository *repo;
	git_commit *commit, *our_commit;
	git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
	git_index *index;
	int error, mainline;

	rb_scan_args(argc, argv, "21:", &rb_commit, &rb_our_commit, &rb_mainline, &rb_options);

	if (TYPE(rb_commit) == T_STRING) {
		rb_commit = rugged_object_rev_parse(self, rb_commit, 1);
	}
	if (TYPE(rb_our_commit) == T_STRING) {
		rb_our_commit = rugged_object_rev_parse(self, rb_our_commit, 1);
	}

	if (!rb_obj_is_kind_of(rb_commit, rb_cRuggedCommit)) {
		rb_raise(rb_eArgError, "Expected a Rugged::Commit.");
	}
	if (!rb_obj_is_kind_of(rb_our_commit, rb_cRuggedCommit)) {
		rb_raise(rb_eArgError, "Expected a Rugged::Commit.");
	}

	Data_Get_Struct(self, git_repository, repo);
	Data_Get_Struct(rb_commit, git_commit, commit);
	Data_Get_Struct(rb_our_commit, git_commit, our_commit);

	rugged_parse_merge_options(&opts, rb_options);

	mainline = NIL_P(rb_mainline) ? 0 : FIX2UINT(rb_mainline);
	error = git_cherrypick_commit(&index, repo, commit, our_commit, mainline, &opts);
	rugged_exception_check(error);

	return rugged_index_new(rb_cRuggedIndex, self, index);
}

void Init_rugged_repo(void)
{
	id_call = rb_intern("call");

	rb_cRuggedRepo = rb_define_class_under(rb_mRugged, "Repository", rb_cObject);

	rb_define_singleton_method(rb_cRuggedRepo, "new", rb_git_repo_new, -1);
	rb_define_singleton_method(rb_cRuggedRepo, "bare", rb_git_repo_open_bare, -1);
	rb_define_singleton_method(rb_cRuggedRepo, "hash_data", rb_git_repo_hash,  2);
	rb_define_singleton_method(rb_cRuggedRepo, "hash_file", rb_git_repo_hashfile,  2);
	rb_define_singleton_method(rb_cRuggedRepo, "init_at", rb_git_repo_init_at, -1);
	rb_define_singleton_method(rb_cRuggedRepo, "discover", rb_git_repo_discover, -1);
	rb_define_singleton_method(rb_cRuggedRepo, "clone_at", rb_git_repo_clone_at, -1);

	rb_define_method(rb_cRuggedRepo, "close", rb_git_repo_close, 0);

	rb_define_method(rb_cRuggedRepo, "exists?", rb_git_repo_exists, 1);
	rb_define_method(rb_cRuggedRepo, "include?", rb_git_repo_exists, 1);
	rb_define_method(rb_cRuggedRepo, "expand_oids", rb_git_repo_expand_oids, -1);
	rb_define_method(rb_cRuggedRepo, "descendant_of?", rb_git_repo_descendant_of, 2);

	rb_define_method(rb_cRuggedRepo, "read",   rb_git_repo_read,   1);
	rb_define_method(rb_cRuggedRepo, "read_header",   rb_git_repo_read_header,   1);
	rb_define_method(rb_cRuggedRepo, "write",  rb_git_repo_write,  2);
	rb_define_method(rb_cRuggedRepo, "each_id",  rb_git_repo_each_id,  0);

	rb_define_method(rb_cRuggedRepo, "path",  rb_git_repo_path, 0);
	rb_define_method(rb_cRuggedRepo, "workdir",  rb_git_repo_workdir, 0);
	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, "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);
	rb_define_method(rb_cRuggedRepo, "config=",  rb_git_repo_set_config,  1);

	rb_define_method(rb_cRuggedRepo, "ident", rb_git_repo_get_ident, 0);
	rb_define_method(rb_cRuggedRepo, "ident=", rb_git_repo_set_ident, 1);

	rb_define_method(rb_cRuggedRepo, "bare?",  rb_git_repo_is_bare,  0);
	rb_define_method(rb_cRuggedRepo, "shallow?",  rb_git_repo_is_shallow,  0);
	rb_define_method(rb_cRuggedRepo, "empty?",  rb_git_repo_is_empty,  0);

	rb_define_method(rb_cRuggedRepo, "head_detached?",  rb_git_repo_head_detached,  0);
	rb_define_method(rb_cRuggedRepo, "head_unborn?",  rb_git_repo_head_unborn,  0);
	rb_define_method(rb_cRuggedRepo, "head=", rb_git_repo_set_head, 1);
	rb_define_method(rb_cRuggedRepo, "head", rb_git_repo_get_head, 0);

	rb_define_method(rb_cRuggedRepo, "merge_base", rb_git_repo_merge_base, -2);
	rb_define_method(rb_cRuggedRepo, "merge_bases", rb_git_repo_merge_bases, -2);

	rb_define_method(rb_cRuggedRepo, "merge_analysis", rb_git_repo_merge_analysis, -1);
	rb_define_method(rb_cRuggedRepo, "merge_commits", rb_git_repo_merge_commits, -1);

	rb_define_method(rb_cRuggedRepo, "revert_commit", rb_git_repo_revert_commit, -1);

	rb_define_method(rb_cRuggedRepo, "path_ignored?", rb_git_repo_is_path_ignored, 1);

	rb_define_method(rb_cRuggedRepo, "reset", rb_git_repo_reset, 2);
	rb_define_method(rb_cRuggedRepo, "reset_path", rb_git_repo_reset_path, -1);

	rb_define_method(rb_cRuggedRepo, "namespace=", rb_git_repo_set_namespace, 1);
	rb_define_method(rb_cRuggedRepo, "namespace", rb_git_repo_get_namespace, 0);

	rb_define_method(rb_cRuggedRepo, "ahead_behind", rb_git_repo_ahead_behind, 2);

	rb_define_method(rb_cRuggedRepo, "default_signature", rb_git_repo_default_signature, 0);

	rb_define_method(rb_cRuggedRepo, "checkout_tree", rb_git_checkout_tree, -1);
	rb_define_method(rb_cRuggedRepo, "checkout_index", rb_git_checkout_index, -1);
	rb_define_method(rb_cRuggedRepo, "checkout_head", rb_git_checkout_head, -1);

	rb_define_method(rb_cRuggedRepo, "cherrypick", rb_git_repo_cherrypick, -1);
	rb_define_method(rb_cRuggedRepo, "cherrypick_commit", rb_git_repo_cherrypick_commit, -1);
	rb_define_method(rb_cRuggedRepo, "fetch_attributes", rb_git_repo_attributes, -1);

	rb_cRuggedOdbObject = rb_define_class_under(rb_mRugged, "OdbObject", rb_cObject);
	rb_define_method(rb_cRuggedOdbObject, "data",  rb_git_odbobj_data,  0);
	rb_define_method(rb_cRuggedOdbObject, "len",  rb_git_odbobj_size,  0);
	rb_define_method(rb_cRuggedOdbObject, "type",  rb_git_odbobj_type,  0);
	rb_define_method(rb_cRuggedOdbObject, "oid",  rb_git_odbobj_oid,  0);
}
