text:
export isbare, isempty, workdir, path, init_repo, head, exists,
       set_head!, tags, tag!, commits, references, lookup,
       lookup_tree, lookup_commit, commit, ref_names,
       revparse_single, create_ref, create_sym_ref, lookup_ref,
       repo_odb, config,  GitTreeBuilder, set_workdir!,
       insert!, write!, close, lookup, revparse, revparse_oid, remotes,
       ahead_behind, is_descendant_of, merge_base, merge_commits,
       blob_at, isshallow, hash_data,
       default_signature, repo_discover, isempty, namespace, set_namespace!,
       notes, create_note!, remove_note!, note_default_ref,
       branch_names, lookup_branch, create_branch, lookup_remote,
       remote_names, remote_add!, checkout_tree!, checkout_head!, checkout!,
       is_head_detached, GitCredential, CredDefault, CredPlainText, CredSSHKey,
       repo_clone, foreach, reset!, currentstate, remove_untracked!, branches

typealias MaybeOid Union(Nothing, Oid)
typealias MaybeString Union(Nothing, String)
typealias MaybeSignature Union(Nothing, Signature)

GitRepo(path::String; alternates={}) = begin
    repo_ptr = Ptr{Void}[0]
    err = ccall((:git_repository_open, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Uint8}), repo_ptr, path)
    if err != GitErrorConst.GIT_OK
        if repo_ptr[1] != C_NULL
            ccall((:git_repository_free, libgit2), Void, (Ptr{Void},), repo_ptr[1])
        end
        throw(LibGitError(err))
    end
    repo = GitRepo(repo_ptr[1])
    if !isempty(alternates)
        odb = Odb(repo)
        for pth in alternates
            if !isdir(pth)
                throw(ArgumentError("alternate $path is not a valid directory"))
            end
            @check ccall((:git_odb_add_disk_alternate, libgit2), Cint,
                         (Ptr{Void}, Ptr{Uint8}), odb, pth)
        end
    end
    return repo
end

Base.show(io::IO, r::GitRepo) = begin
    write(io, "GitRepo(\"$(workdir(r))\")")
end

Base.close(r::GitRepo) = begin
    if r.ptr != C_NULL
        ccall((:git_repository__cleanup, libgit2), Void, (Ptr{Void},), r.ptr)
    end
end

Base.in(id::Oid, r::GitRepo) = exists(Odb(r), id)

function cb_iter_oids(idptr::Ptr{Uint8}, o::Ptr{Void})
    try
        produce(Oid(idptr))
        return GitErrorConst.GIT_OK
    catch err
        return GitErrorConst.ERROR
    end
end

const c_cb_iter_oids = cfunction(cb_iter_oids, Cint, (Ptr{Uint8}, Ptr{Void}))

#TODO: better error handling
Base.start(r::GitRepo) = begin
    odb = Odb(r)
    t = @task ccall((:git_odb_foreach, libgit2), Cint,
                    (Ptr{Void}, Ptr{Void}, Ptr{Cint}), odb, c_cb_iter_oids, C_NULL)
    return (consume(t), t)
end

Base.done(r::GitRepo, state) = istaskdone(state[2])

Base.next(r::GitRepo, state) = begin
    v = consume(state[2])
    return (state[1], (v, state[2]))
end

Base.read(r::GitRepo, id::Oid) = begin
    odb = Odb(r)
    obj_ptr = Ptr{Void}[0]
    @check ccall((:git_odb_read, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Oid}), obj_ptr, odb, &id)
    return OdbObject(obj_ptr[1])
end

Base.delete!(r::GitRepo, ref::GitReference) = begin
    @check ccall((:git_reference_delete, libgit2), Cint, (Ptr{Void}, Ptr{Void}), r, ref)
    return r
end

Base.delete!(r::GitRepo, t::GitTag) = begin
    @check ccall((:git_tag_delete, libgit2), Cint, (Ptr{Void}, Ptr{Uint8}), r, name(t))
    return r
end

Base.getindex(r::GitRepo, o) = lookup(r, o)

read_header(r::GitRepo, id::Oid) = read_header(Odb(r), id)

exists(r::GitRepo, ref::String) = begin
    ref_ptr = Ptr{Void}[0]
    err = ccall((:git_reference_lookup, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}), ref_ptr, r, ref)
    ccall((:git_reference_free, libgit2), Void, (Ptr{Void},), ref_ptr[1])
    if err == GitErrorConst.ENOTFOUND
        return false
    elseif err != GitErrorConst.GIT_OK
        throw(LibGitError(err))
    end
    return true
end
exists(r::GitRepo, id::Oid) = id in r

function currentstate(r::GitRepo)
    res = ccall((:git_repository_state, libgit2), Cint, (Ptr{Void},), r)
    res == 0 && return :none
    res == 1 && return :merge
    res == 2 && return :revert
    res == 3 && return :cherry_pick
    res == 4 && return :bisect
    res == 5 && return :rebase
    res == 6 && return :rebase_interactive
    res == 7 && return :rebase_merge
    res == 8 && return :apply_mailbox
    res == 9 && return :apply_mailbox_or_rebase
    error("unknown state constant $res")
end

function isbare(r::GitRepo)
    return bool(ccall((:git_repository_is_bare, libgit2), Cint, (Ptr{Void},), r))
end

Base.isempty(r::GitRepo) = begin
    return bool(ccall((:git_repository_is_empty, libgit2), Cint, (Ptr{Void},), r))
end

function isshallow(r::GitRepo)
    return bool(ccall((:git_repository_is_shallow, libgit2), Cint, (Ptr{Void},), r))
end

function workdir(r::GitRepo)
    dir = ccall((:git_repository_workdir, libgit2), Ptr{Uint8}, (Ptr{Void},), r)
    dir == C_NULL && return nothing
    # remove trailing slash
    return bytestring(dir)[1:end-1]
end

function path(r::GitRepo)
    cpath = ccall((:git_repository_path, libgit2), Ptr{Uint8}, (Ptr{Void},), r)
    cpath == C_NULL && return nothing
    # remove trailing slash
    return bytestring(cpath)[1:end-1]
end

function hash_data{T<:GitObject}(::Type{T}, content::String)
    id_ptr = [Oid()]
    @check ccall((:git_odb_hash, libgit2), Cint,
                 (Ptr{Oid}, Ptr{Uint8}, Csize_t, Cint),
                 id_ptr, content, length(content), git_otype(T))
    return id_ptr[1]
end

function default_signature(r::GitRepo)
    sig_ptr = Ptr{SignatureStruct}[0]
    @check ccall((:git_signature_default, libgit2), Cint,
                 (Ptr{Ptr{SignatureStruct}}, Ptr{Void}), sig_ptr, r)
    sig = Signature(sig_ptr[1])
    ccall((:git_signature_free, libgit2), Void, (Ptr{SignatureStruct},), sig_ptr[1])
    return sig
end

function is_head_detached(r::GitRepo)
    return bool(ccall((:git_repository_head_detached, libgit2), Cint, (Ptr{Void},), r))
end

function init_repo(path::String; bare::Bool=false)
    repo_ptr = Ptr{Void}[0]
    err = ccall((:git_repository_init, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Uint8}, Cint), repo_ptr, path, bare)
    if err != GitErrorConst.GIT_OK
        if repo_ptr[1] != C_NULL
            ccall((:git_repository_free, libgit2), Void, (Ptr{Void},), repo_ptr[1])
        end
        throw(LibGitError(err))
    end
    return GitRepo(repo_ptr[1])
end

function set_namespace!(r::GitRepo, ns)
    if ns == nothing || isempty(ns)
        @check ccall((:git_repository_set_namespace, libgit2), Cint,
                     (Ptr{Void}, Ptr{Uint8}), r, C_NULL)
    else
        @check ccall((:git_repository_set_namespace, libgit2), Cint,
                     (Ptr{Void}, Ptr{Uint8}), r, ns)
    end
    return r
end

function namespace(r::GitRepo)
    ns_ptr = ccall((:git_repository_get_namespace, libgit2), Ptr{Uint8}, (Ptr{Void},), r)
    ns_ptr == C_NULL && return nothing
    return bytestring(ns_ptr)
end

function head(r::GitRepo)
    head_ptr = Ptr{Void}[0]
    err = ccall((:git_repository_head, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Void}), head_ptr, r)
    if err == GitErrorConst.ENOTFOUND || err == GitErrorConst.EUNBORNBRANCH
        return nothing
    elseif err != GitErrorConst.GIT_OK
        throw(LibGitError(err))
    end
    return GitReference(head_ptr[1])
end

Oid(r::GitRepo, val::GitObject) = Oid(val)
Oid(r::GitRepo, val::Oid) = val

function Oid(r::GitRepo, val::String)
    if length(val) == OID_HEXSZ
        try
            return Oid(val)
        end
    end
    return revparse_oid(r, val)
end

function set_head!(r::GitRepo, ref::String;
                   sig::MaybeSignature=nothing,
                   logmsg::MaybeString=nothing)
    @check ccall((:git_repository_set_head, libgit2), Cint,
                 (Ptr{Void}, Ptr{Uint8}, Ptr{SignatureStruct}, Ptr{Uint8}),
                 r, ref, sig != nothing ? sig : C_NULL, logmsg != nothing ? logmsg : C_NULL)
    return r
end

commits(repo::GitRepo) = begin
    cs = GitCommit[]
    for id in repo
        obj = lookup(repo, id)
        isa(obj, GitCommit) && push!(cs, obj)
    end
    return cs
end

function remotes(r::GitRepo)
    sa_ptr = [StrArrayStruct()]
    @check ccall((:git_remote_list, libgit2), Cint,
                 (Ptr{StrArrayStruct}, Ptr{Void}), sa_ptr, r)
    sa = sa_ptr[1]
    sa.count == 0 && return nothing
    remote_ptr = Ptr{Void}[0]
    out = Array(GitRemote, sa.count)
    for i in 1:sa.count
        rstr_ptr = unsafe_load(sa.strings, i)
        @check ccall((:git_remote_load, libgit2), Cint,
                     (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}), remote_ptr, r, rstr_ptr)
        out[i] = GitRemote(remote_ptr[1])
    end
    free!(sa)
    return out
end

function remote_names(r::GitRepo)
    sa_ptr = [StrArrayStruct()]
    @check ccall((:git_remote_list, libgit2), Cint,
                  (Ptr{StrArrayStruct}, Ptr{Void}), sa_ptr, r)
    sa = sa_ptr[1]
    ns = Array(UTF8String, sa.count)
    for i in 1:sa.count
        ns[i] = utf8(bytestring(unsafe_load(sa.strings, i)))
    end
    free!(sa)
    return ns
end

function remote_add!(r::GitRepo, name::String, url::String)
    check_valid_url(url)
    remote_ptr = Ptr{Void}[0]
    @check ccall((:git_remote_create, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}, Ptr{Uint8}), remote_ptr, r, name, url)
    return GitRemote(remote_ptr[1])
end

function cb_push_status(ref_ptr::Ptr{Uint8}, msg_ptr::Ptr{Uint8}, payload::Ptr{Void})
    if msg_ptr != C_NULL
        result = unsafe_pointer_to_objref(payload)::Dict{UTF8String,UTF8String}
        result[utf8(bytestring(ref_ptr))] = utf8(bytestring(msg_ptr))
    end
    return GitErrorConst.GIT_OK
end

const c_cb_push_status = cfunction(cb_push_status, Cint,
                                   (Ptr{Uint8}, Ptr{Uint8}, Ptr{Void}))

#TODO: better error messages
Base.push!{T<:String}(r::GitRepo, remote::GitRemote, refs::Vector{T};
                      sig::MaybeSignature=nothing,
                      logmsg::MaybeString=nothing) = begin
    push_ptr = Ptr{Void}[0]
    err = ccall((:git_push_new, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Void}), push_ptr, remote)
    p = push_ptr[1]
    if err != GitErrorConst.GIT_OK
        if p != C_NULL
            ccall((:git_push_free, libgit2), Void, (Ptr{Void},), p)
        end
        throw(LibGitError(err))
    end
    @assert p != C_NULL
    for ref in refs
        err = ccall((:git_push_add_refspec, libgit2), Cint, (Ptr{Void}, Ptr{Uint8}), p, ref)
    end
    if err != GitErrorConst.GIT_OK
        ccall((:git_push_free, libgit2), Void, (Ptr{Void},), p)
        throw(LibGitError(err))
    end
    err = ccall((:git_push_finish, libgit2), Cint, (Ptr{Void},), p)
    if err != GitErrorConst.GIT_OK
        ccall((:git_push_free, libgit2), Void, (Ptr{Void},), p)
        if err == GitErrorConst.ENONFASTFORWARD
            error("non-fast-forward update rejected")
        elseif err == GitErrorConst.EBAREREPO
            error("could not push to repo (check for non-bare repo)")
        end
    end
    err = ccall((:git_push_unpack_ok, libgit2), Cint, (Ptr{Void},), p)
    if err == GitErrorConst.GIT_OK
        ccall((:git_push_free, libgit2), Void, (Ptr{Void},), p)
        error("remote side did not unpack successfully")
    end
    result = Dict{UTF8String, UTF8String}()
    err = ccall((:git_push_status_foreach, libgit2), Cint,
                (Ptr{Void}, Ptr{Void}, Any), p, c_cb_push_status, &result)
    if err != GitErrorConst.GIT_OK
        ccall((:git_push_free, libgit2), Void, (Ptr{Void},), p)
        throw(LibGitError(err))
    end
    err = ccall((:git_push_update_tips, libgit2), Cint,
                (Ptr{Void}, Ptr{SignatureStruct}, Ptr{Uint8}), p,
                sig != nothing ? sig : C_NULL,
                logmsg != nothing ? logmsg : C_NULL)
    if err != GitErrorConst.GIT_OK
        ccall((:git_push_free, libgit2), Void, (Ptr{Void},), p)
        throw(LibGitError(err))
    end
    return result
end

Base.push!{T<:String}(r::GitRepo, remote::String, refs::Vector{T}) = begin
    remote_ptr = Ptr{Void}[0]
    @check ccall((:git_remote_load, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}), remote_ptr, r, remote)
    return push!(r, GitRemote(remote_ptr[1]), refs)
end

function lookup(::Type{GitRemote}, r::GitRepo, remote_name::String)
    remote_ptr = Ptr{Void}[0]
    err = ccall((:git_remote_load, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}), remote_ptr, r, remote_name)
    if err == GitErrorConst.ENOTFOUND
        return nothing
    elseif err != GitErrorConst.GIT_OK
        if remote_ptr[1] != C_NULL
            ccall((:git_remote_free, libgit2), Void, (Ptr{Void},), remote_ptr[1])
        end
        throw(LibGitError(err))
    end
    return GitRemote(remote_ptr[1])
end

lookup_remote(r::GitRepo, remote_name::String) = lookup(GitRemote, r, remote_name)
lookup_tag(r::GitRepo, id::Oid) = lookup(GitTag, r, id)

function tags(r::GitRepo, glob::String="")
    sa_ptr = [StrArrayStruct()]
    @check ccall((:git_tag_list_match, libgit2), Cint,
                 (Ptr{StrArrayStruct}, Ptr{Uint8}, Ptr{Void}), sa_ptr, glob, r)
    sa = sa_ptr[1]
    sa.count == 0 && return GitTag[]
    out = Array(UTF8String, sa.count)
    for i in 1:sa.count
        out[i] = utf8(bytestring(unsafe_load(sa.strings, i)))
    end
    free!(sa)
    return out
end

function tag!(r::GitRepo;
              name::String="",
              message::String="",
              target::MaybeOid=nothing,
              tagger::MaybeSignature=nothing,
              force::Bool=false)
   id_ptr = [Oid()]
   target != nothing && (obj = lookup(r, target))
   if !isempty(message)
       sig = tagger != nothing ? tagger : default_signature(r)
       @check ccall((:git_tag_create, libgit2), Cint,
                    (Ptr{Oid}, Ptr{Void}, Ptr{Uint8},
                     Ptr{Void}, Ptr{SignatureStruct}, Ptr{Uint8}, Cint),
                    id_ptr, r, name, obj, sig, message, force)
   else
       @check ccall((:git_tag_create_lightweight, libgit2), Cint,
                    (Ptr{Oid}, Ptr{Void}, Ptr{Uint8}, Ptr{Void}, Cint),
                    id_ptr, r, name, obj, force)
   end
   return id_ptr[1]
end

function create_note!(
               obj::GitObject, msg::String;
               committer::MaybeSignature=nothing,
               author::MaybeSignature=nothing,
               ref::MaybeString=nothing,
               force::Bool=false)
    repo_ptr  = ccall((:git_object_owner, libgit2), Ptr{Void}, (Ptr{Void},), obj)
    bref = ref != nothing ? bytestring(ref) : convert(Ptr{Uint8}, C_NULL)
    #TODO: we can avoid having to lookup the default signature twice
    local committer_ptr::Ptr{SignatureStruct}
    if committer != nothing
        committer_ptr = convert(Ptr{SignatureStruct}, committer)
    else
        sig_ptr = Ptr{SignatureStruct}[0]
        @check ccall((:git_signature_default, libgit2), Cint,
                     (Ptr{Ptr{SignatureStruct}}, Ptr{Void}), sig_ptr, repo_ptr)
        committer_ptr = sig_ptr[1]

    end
    local author_ptr::Ptr{SignatureStruct}
    if author != nothing
        author_ptr = convert(Ptr{SignatureStruct}, author)
    else
        sig_ptr = Ptr{SignatureStruct}[0]
        @check ccall((:git_signature_default, libgit2), Cint,
                     (Ptr{Ptr{SignatureStruct}}, Ptr{Void}), sig_ptr, repo_ptr)
        author_ptr = sig_ptr[1]
    end
    tid = Oid(obj)
    nid_ptr = [Oid()]
    @check ccall((:git_note_create, libgit2), Cint,
                 (Ptr{Oid}, Ptr{Void}, Ptr{SignatureStruct},
                  Ptr{SignatureStruct}, Ptr{Uint8}, Ptr{Oid}, Ptr{Uint8}, Cint),
                  nid_ptr, repo_ptr, committer_ptr, author_ptr, bref, &tid, msg, force)
    return nid_ptr[1]
end

function remove_note!(obj::GitObject;
                      committer::MaybeSignature=nothing,
                      author::MaybeSignature=nothing,
                      ref::MaybeString=nothing)
    repo_ptr  = ccall((:git_object_owner, libgit2), Ptr{Void}, (Ptr{Void},), obj)
    notes_ref = ref != nothing ? bytestring(ref) : bytestring("refs/notes/commits")
    local committer_ptr::Ptr{SignatureStruct}
    if committer != nothing
        committer_ptr = convert(Ptr{SignatureStruct}, committer)
    else
        sig_ptr = Ptr{SignatureStruct}[0]
        @check ccall((:git_signature_default, libgit2), Cint,
                     (Ptr{Ptr{SignatureStruct}}, Ptr{Void}), sig_ptr, repo_ptr)
        committer_ptr = sig_ptr[1]
    end
    local author_ptr::Ptr{SignatureStruct}
    if author != nothing
        author_ptr = convert(Ptr{SignatureStruct}, committer)
    else
        sig_ptr = Ptr{SignatureStruct}[0]
        @check ccall((:git_signature_default, libgit2), Cint,
                     (Ptr{Ptr{SignatureStruct}}, Ptr{Void}), sig_ptr, repo_ptr)
        author_ptr = sig_ptr[1]
    end
    tid = Oid(obj)
    err = ccall((:git_note_remove, libgit2), Cint,
                (Ptr{Void}, Ptr{Uint8},
                 Ptr{SignatureStruct}, Ptr{SignatureStruct}, Ptr{Oid}),
                 repo_ptr, notes_ref, author_ptr, committer_ptr, &tid)
    if err == GitErrorConst.ENOTFOUND
        return false
    elseif err != GitErrorConst.GIT_OK
        throw(LibGitError(err))
    end
    return true
end

function note_default_ref(r::GitRepo)
    refname_ptr = Ptr{Uint8}[0]
    @check ccall((:git_note_default_ref, libgit2), Cint,
                 (Ptr{Ptr{Uint8}}, Ptr{Void}), refname_ptr, r)
    return utf8(bytestring(refname_ptr[1]))
end

type NoteIterator
    repo::GitRepo
    ptr::Ptr{Void}
    notes_ref::MaybeString

    function NoteIterator(r::GitRepo, ptr::Ptr{Void}, notes_ref::MaybeString=nothing)
        @assert ptr != C_NULL
        it = new(r, ptr, notes_ref)
        finalizer(it, free!)
        return it
    end
end

free!(it::NoteIterator) = begin
    if it.ptr != C_NULL
        ccall((:git_note_iterator_free, libgit2), Void, (Ptr{Void},), it.ptr)
        it.ptr = C_NULL
    end
end

Base.convert(::Type{Ptr{Void}}, it::NoteIterator) = it.ptr

function foreach(::Type{GitNote}, r::GitRepo, notes_ref::MaybeString=nothing)
    iter_ptr = Ptr{Void}[0]
    @check ccall((:git_note_iterator_new, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}), iter_ptr, r, notes_ref)
    return NoteIterator(r, iter_ptr[1], notes_ref)
end

Base.start(it::NoteIterator) = begin
    note_id_ptr, ann_id_ptr = [Oid()], [Oid()]
    err = ccall((:git_note_next, libgit2), Cint,
                (Ptr{Oid}, Ptr{Oid}, Ptr{Void}), note_id_ptr, ann_id_ptr, it)
    if err == GitErrorConst.ITEROVER
        return nothing
    elseif err != GitErrorConst.GIT_OK
        throw(LibGitError(err))
    end
    note_ptr = Ptr{Void}[0]
    err = ccall((:git_note_read, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}, Ptr{Oid}),
                 note_ptr, it.repo, it.notes_ref, ann_id_ptr)
    if err != GitErrorConst.GIT_OK
        if note_ptr[1] != C_NULL
            ccall((:git_note_free, libgit2), Void, (Ptr{Void},), note_ptr[1])
        end
        throw(LibGitError(err))
    end
    n = GitNote(note_ptr[1])
    ccall((:git_note_free, libgit2), Void, (Ptr{Void},), note_ptr[1])
    return (n, ann_id_ptr[1])
end

Base.done(it::NoteIterator, state) = is(state, nothing)

Base.next(it::NoteIterator, state) = begin
    note_id_ptr, ann_id_ptr = [Oid()], [Oid()]
    err = ccall((:git_note_next, libgit2), Cint,
                (Ptr{Oid}, Ptr{Oid}, Ptr{Void}), note_id_ptr, ann_id_ptr, it)
    if err == GitErrorConst.ITEROVER
        return (state, nothing)
    elseif err != GitErrorConst.GIT_OK
        throw(LibGitError(err))
    end
    note_ptr = Ptr{Void}[0]
    err = ccall((:git_note_read, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}, Ptr{Oid}),
                 note_ptr, it.repo, C_NULL, note_id_ptr)
    if err != GitErrorConst.GIT_OK
        if note_ptr[1] != C_NULL
            ccall((:git_note_free, libgit2), Void, (Ptr{Void},), note_ptr[1])
        end
        throw(LibGitError(err))
    end
    n = GitNote(note_ptr[1])
    ccall((:git_note_free, libgit2), Void, (Ptr{Void},), note_ptr[1])
    return (state, (n, ann_id_ptr[1]))
end

function ahead_behind(r::GitRepo, lcommit::GitCommit, ucommit::GitCommit)
    return ahead_behind(r, Oid(lcommit), Oid(ucommit))
end

function ahead_behind(r::GitRepo, lid::Oid, uid::Oid)
    ahead, behind = Csize_t[0], Csize_t[0]
    @check ccall((:git_graph_ahead_behind, libgit2), Cint,
                 (Ptr{Csize_t}, Ptr{Csize_t}, Ptr{Void}, Ptr{Oid}, Ptr{Oid}),
                 ahead, behind, r, &lid, &uid)
    return (int(ahead[1]), int(behind[1]))
end

is_descendant_of(r::GitRepo, prev::GitCommit, ances::GitCommit) =
    is_descendant_of(r, Oid(prev), Oid(ances))

function is_descendant_of(r::GitRepo, prev::Oid, ances::Oid)
    if !isa(lookup(GitCommit, repo, prev), GitCommit)
        throw(ArgumentError("prev $prev is not a valid commit id"))
    end
    if !isa(lookup(GitCommit, repo, ances), GitCommit)
        throw(ArgumentError("ances $ances is not a valid commit id"))
    end
    return bool(ccall((:git_graph_descendant_of, libgit2), Cint,
                      (Ptr{Void}, Ptr{Oid}, Ptr{Oid}), r, &prev, &ances))
end

function blob_at(r::GitRepo, rev::Oid, pth::String)
    tree = GitTree(lookup_commit(r, rev))
    local blob_entry::GitTreeEntry
    try
        blob_entry = entry_bypath(tree, pth)
        @assert isa(blob_entry, GitTreeEntry{GitBlob})
    catch
        return nothing
    end
    blob = lookup_blob(r, Oid(blob_entry))
    return blob
end

#TODO: consolidate with odb
function write!{T<:GitObject}(::Type{T}, r::GitRepo, buf::ByteString)
    odb = Odb(r)
    stream_ptr = Ptr{Void}[0]
    @check ccall((:git_odb_open_wstream, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Csize_t, Cint),
                 stream_ptr, odb, length(buf), git_otype(T))
    @assert stream_ptr[1] != C_NULL
    err = ccall((:git_odb_stream_write, libgit2), Cint,
                (Ptr{Void}, Ptr{Uint8}, Csize_t), stream_ptr[1], buf, length(buf))
    id_ptr = [Oid()]
    if err == GitErrorConst.GIT_OK
        err = ccall((:git_odb_stream_finalize_write, libgit2), Cint,
                    (Ptr{Oid}, Ptr{Void}), id_ptr, stream_ptr[1])
    end
    ccall((:git_odb_stream_free, libgit2), Void, (Ptr{Void},), stream_ptr[1])
    if err != GitErrorConst.GIT_OK
        throw(LibGitError(err))
    end
    return id_ptr[1]
end

function repo_discover(pth::String="", acrossfs::Bool=true)
    isempty(pth) && (pth = pwd())
    brepo = zeros(Uint8, GitConst.GIT_PATH_MAX)
    buf_ptr = [BufferStruct()]
    @check ccall((:git_repository_discover, libgit2), Cint,
                 (Ptr{BufferStruct}, Ptr{Uint8}, Cint, Ptr{Uint8}),
                 buf_ptr, pth, acrossfs, C_NULL)
    bstr = bytestring(buf_ptr[1])
    ccall((:git_buf_free, libgit2), Void, (Ptr{BufferStruct},), buf_ptr)
    return GitRepo(bstr)
end

function revparse(r::GitRepo, rev::String)
    obj_ptr = Ptr{Void}[0]
    @check ccall((:git_revparse_single, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}), obj_ptr, r, rev)
    return gitobj_from_ptr(obj_ptr[1])
end
revparse(r::GitRepo, rev::Oid) = revparse(r, string(rev))

function merge_base(r::GitRepo, args...)
    if length(args) < 2
        throw(ArgumentError("merge_base needs 2+ commits"))
    end
    arg_oids = vcat([raw(Oid(r, a)) for a in args]...)
    @assert length(arg_oids) == length(args) * OID_RAWSZ
    len = convert(Csize_t, length(args))
    id_ptr = [Oid()]
    err = ccall((:git_merge_base_many, libgit2), Cint,
                (Ptr{Oid}, Ptr{Void}, Csize_t, Ptr{Uint8}), id_ptr, r, len, arg_oids)
    if err == GitErrorConst.ENOTFOUND
        return nothing
    elseif err != GitErrorConst.GIT_OK
        throw(LibGitError(err))
    end
    return id_ptr[1]
end

revparse_oid(r::GitRepo, rev::Oid) = Oid(revparse(r, string(rev)))
revparse_oid(r::GitRepo, rev::String) = Oid(revparse(r, rev))

Odb(r::GitRepo) = begin
    odb_ptr = Ptr{Void}[0]
    @check ccall((:git_repository_odb, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}), odb_ptr, r)
    return Odb(odb_ptr[1])
end

function lookup{T<:GitObject}(::Type{T}, r::GitRepo, id::Oid)
    obj_ptr = Ptr{Void}[0]
    @check ccall((:git_object_lookup, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Oid}, Cint),
                 obj_ptr, r, &id, git_otype(T))
    return T(obj_ptr[1])
end

function lookup{T<:GitObject}(::Type{T}, r::GitRepo, oid::String)
    len = length(oid)
    id_ptr  = [Oid()]
    obj_ptr = Ptr{Void}[0]
    @check ccall((:git_oid_fromstrn, libgit2), Cint,
                 (Ptr{Oid}, Ptr{Uint8}, Csize_t), id_ptr, oid, len)
    if len < OID_HEXSZ
        @check ccall((:git_object_lookup_prefix, libgit2), Cint,
                     (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Oid}, Csize_t, Cint),
                     obj_ptr, r, id_ptr, len, git_otype(T))
    else
        @check ccall((:git_object_lookup, libgit2), Cint,
                     (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Oid}, Cint),
                     obj_ptr, r, id_ptr, git_otype(T))
    end
    return T(obj_ptr[1])
end
lookup(r::GitRepo, id::Oid)    = lookup(GitAnyObject, r, id)
lookup(r::GitRepo, id::String) = lookup(GitAnyObject, r, id)

lookup_blob(r::GitRepo, id)   = lookup(GitBlob, r, id)
lookup_tree(r::GitRepo, id)   = lookup(GitTree, r, id)
lookup_commit(r::GitRepo, id) = lookup(GitCommit, r, id)

lookup(r::GitRepo, o::GitReference) = lookup(GitReference, r, o)

references(r::GitRepo, glob::String="") = collect(GitReference, foreach(GitReference, r, glob))

function lookup(::Type{GitReference}, r::GitRepo, name::String)
    ref_ptr = Ptr{Void}[0]
    err = ccall((:git_reference_lookup, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}), ref_ptr, r, name)
    if err == GitErrorConst.ENOTFOUND
        return nothing
    elseif err != GitErrorConst.GIT_OK
        return LibGitError(err)
    end
    return GitReference(ref_ptr[1])
end
lookup(::Type{GitReference}, r::GitRepo, ref::GitReference{Oid}) = lookup(r, target(ref))

lookup_ref(r::GitRepo, name::String) = lookup(GitReference, r, name)
lookup_ref(r::GitRepo, ref::GitReference) = lookup(GitReference, r, ref)

function create_ref(r::GitRepo, refname::String, id::Oid;
                    force::Bool=false,
                    sig::Union(Nothing,Signature)=nothing,
                    logmsg::Union(Nothing,String)=nothing)
    ref_ptr = Ptr{Void}[0]
    @check ccall((:git_reference_create, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}, Ptr{Oid},
                  Cint, Ptr{SignatureStruct}, Ptr{Uint8}),
                 ref_ptr, r, refname, &id, force,
                 sig != nothing ? sig : C_NULL,
                 logmsg != nothing ? logmsg : C_NULL)
    return GitReference(ref_ptr[1])
end

function create_ref(r::GitRepo, refname::String, target::String;
                    force::Bool=false, sig=nothing, logmsg=nothing)
    create_sym_ref(r, refname, target; force=force, sig=sig, logmsg=logmsg)
end

function create_sym_ref(r::GitRepo, refname::String, target::String;
                        force::Bool=false, sig=sig, logmsg=logmsg)
    ref_ptr = Ptr{Void}[0]
    @check ccall((:git_reference_symbolic_create, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}, Ptr{Uint8},
                 Cint, Ptr{SignatureStruct}, Ptr{Uint8}),
                 ref_ptr, r, refname, target, force,
                 sig != nothing ? sig : C_NULL,
                 logmsg != nothing ? logmsg : C_NULL)
    return GitReference(ref_ptr[1])
end

function commit(r::GitRepo,
                refname::String,
                msg::String,
                author::Signature,
                committer::Signature,
                tree::GitTree,
                parents::GitCommit...)
    id_ptr = [Oid()]
    nparents = length(parents)
    parentptrs = Ptr{Void}[c.ptr for c in parents]
    @check ccall((:git_commit_create, libgit2), Cint,
                 (Ptr{Oid}, Ptr{Void}, Ptr{Uint8},
                  Ptr{SignatureStruct}, Ptr{SignatureStruct},
                  Ptr{Uint8}, Ptr{Uint8}, Ptr{Void},
                  Csize_t, Ptr{Ptr{Void}}),
                 id_ptr, r, refname, author, committer, C_NULL, msg, tree,
                 nparents, nparents > 0 ? parentptrs : C_NULL)
    return id_ptr[1]
end

function commit(r::GitRepo,
                msg::String,
                author::Signature,
                committer::Signature)
    idx = GitIndex(r)
    id_ptr = [Oid()]
    @check ccall((:git_index_write_tree, libgit2), Cint,
                 (Ptr{Oid}, Ptr{Void}), id_ptr, idx)
    tree = lookup(GitTree, r, id_ptr[1])
    parent = lookup(GitCommit, r, target(head(r)))
    return commit(r, "HEAD", msg, author, committer, tree, parent)
end

function reset!(r::GitRepo, obj::Union(GitCommit,GitTag), typ::Symbol;
                sig::MaybeSignature=nothing,
                logmsg::MaybeString=nothing)
    git_reset = typ === :soft ? 1 :
                typ === :mixed ? 2 :
                typ === :hard ? 3 :
                throw(ArgumentError("unknown reset type :$type"))
    @check ccall((:git_reset, libgit2), Cint,
                 (Ptr{Void}, Ptr{Void}, Cint, Ptr{Void}, Ptr{Uint8}),
                 r, obj, git_reset,
                 sig != nothing ? sig : C_NULL,
                 logmsg != nothing ? logmsg : C_NULL)
    return
end

function reset!(r::GitRepo, ref::String, typ::Symbol;
       sig::MaybeSignature=nothing,
       logmsg::MaybeString=nothing)
    reset!(r, revparse(r, ref), typ, sig=sig, logmsg=logmsg)
end

function set_workdir!(r::GitRepo, dir::String, update::Bool)
    @check ccall((:git_repository_set_workdir, libgit2), Cint,
                 (Ptr{Void}, Ptr{Uint8}, Cint), r, dir, update)
    return r
end
# branch type can be :local or :remote
function lookup(::Type{GitBranch}, r::GitRepo, branch_name::String, branch_type::Symbol=:local)
    git_branch_type = branch_type == :local  ? GitConst.BRANCH_LOCAL :
                      branch_type == :remote ? GitConst.BRANCH_REMOTE :
                      throw(ArgumentError("branch_type can be :local or :remote"))
    branch_ptr = Ptr{Void}[0]
    err = ccall((:git_branch_lookup, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}, Cint),
                branch_ptr, r, branch_name, git_branch_type)
    if err == GitErrorConst.GIT_OK
        return GitBranch(branch_ptr[1])
    elseif err == GitErrorConst.ENOTFOUND
        return nothing
    else
        throw(LibGitError(err))
    end
end

lookup_branch(r::GitRepo, branch_name::String, branch_type=:local) =
        lookup(GitBranch, r, branch_name, branch_type)

lookup_branch(r::GitRepo, branch_id::Oid, branch_type=:local) =
        lookup(GitBranch, r, string(branch_id), branch_type)

#TODO: give intelligent error msg when target does not exist
function create_branch(r::GitRepo, bname::String, target::Oid;
                       force::Bool=false,
                       sig::Union(Nothing,Signature)=nothing,
                       logmsg::Union(Nothing,String)=nothing)
    commit = lookup_commit(r, target)
    branch_ptr = Ptr{Void}[0]
    @check ccall((:git_branch_create, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}, Ptr{Void},
                 Cint, Ptr{SignatureStruct}, Ptr{Uint8}),
                 branch_ptr, r, bname, commit, force,
                 sig != nothing ? sig : C_NULL,
                 logmsg != nothing ? logmsg : C_NULL)
    return GitBranch(branch_ptr[1])
end

function create_branch(r::GitRepo, n::String, target::String="HEAD";
                       force::Bool=false, sig=nothing, logmsg=nothing)
    id = revparse_oid(r, target)
    return create_branch(r, n, id, force=force, sig=sig, logmsg=logmsg)
end

function create_branch(r::GitRepo, n::String, target::GitCommit;
                       force::Bool=false, sig=nothing, logmsg=nothing)
    id = Oid(target)
    return create_branch(r, n, id, force=force, sig=sig, logmsg=logmsg)
end

type BranchIterator
    repo::GitRepo
    ptr::Ptr{Void}

    function BranchIterator(r::GitRepo, ptr::Ptr{Void})
        @assert ptr != C_NULL
        it = new(r, ptr)
        finalizer(it, free!)
        return it
    end
end

free!(it::BranchIterator) = begin
    if it.ptr != C_NULL
        ccall((:git_branch_iterator_free, libgit2), Void, (Ptr{Void},), it.ptr)
        it.ptr = C_NULL
    end
end

Base.convert(::Type{Ptr{Void}}, b::BranchIterator) = b.ptr

function foreach(::Type{GitBranch}, r::GitRepo, filter::Symbol=:all)
    git_filter = filter === :all ? (GitConst.BRANCH_LOCAL | GitConst.BRANCH_REMOTE) :
                 filter == :local ? GitConst.BRANCH_LOCAL :
                 filter == :remote ? GitConst.BRANCH_REMOTE :
                 throw(ArgumentError("filter can be :all, :local, or :remote"))
    iter_ptr = Ptr{Void}[0]
    @check ccall((:git_branch_iterator_new, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Cint), iter_ptr, r, git_filter)
    return BranchIterator(r, iter_ptr[1])
end

Base.start(b::BranchIterator) = begin
    branch_ptr, branch_type = Ptr{Void}[0], Cint[0]
    err = ccall((:git_branch_next, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Cint}, Ptr{Void}), branch_ptr, branch_type, b)
    if err == GitErrorConst.ITEROVER
        return nothing
    elseif err != GitErrorConst.GIT_OK
        if branch_ptr[1] != C_NULL
            ccall((:git_reference_free, libgit2), Void, (Ptr{Void},), branch_ptr[1])
        end
        throw(LibGitError(err))
    end
    return GitBranch(branch_ptr[1])
end

Base.done(it::BranchIterator, state) = is(state, nothing)

Base.next(it::BranchIterator, state) = begin
    branch_ptr, branch_type = Ptr{Void}[0], Cint[0]
    err = ccall((:git_branch_next, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Cint}, Ptr{Void}), branch_ptr, branch_type, it)
    if err == GitErrorConst.ITEROVER
        return (state, nothing)
    elseif err != GitErrorConst.GIT_OK
        if branch_ptr[1] != C_NULL
            ccall((:git_reference_free, libgit2), Void, (Ptr{Void},), branch_ptr[1])
        end
        throw(LibGitError(err))
    end
    return (state, GitBranch(branch_ptr[1]))
end

branches(r::GitRepo, filter::Symbol=:all) = collect(GitBranch, foreach(GitBranch, r, filter))
branch_names(r::GitRepo, filter::Symbol=:all) = map(name, branches(r, filter))

#------- Tree merge ---------
parse_merge_options(opts::Nothing) = MergeTreeOptsStruct()

function parse_merge_options(opts::Dict)
    if isempty(opts)
        return MergeTreeOptsStruct()
    end
    local rename_threshold = zero(Cuint)
    if haskey(opts, :rename_threshold)
        rename_threshold = convert(Cuint, opts[:rename_threshold])
    end
    local target_limit = zero(Cuint)
    if haskey(opts, :target_limit)
        target_limit = convert(Cuint, opts[:target_limit])
    end
    local automerge_flags = zero(Cint)
    if haskey(opts, :automerge)
        a = opts[:automerge]
        if a === :normal
            automerge_flags = GitConst.MERGE_AUTOMERGE_NORMAL
        elseif a === :favor_ours
            automerge_flags = GitConst.MERGE_AUTOMERGE_FAVOR_OURS
        elseif a === :favor_theirs
            automerge_flags = GitConst.MERGE_AUTOMERGE_FAVOR_THEIRS
        elseif a === :union
            automerge_flags = GitConst.MERGE_AUTOMERGE_FAVOR_UNION
        else
            throw(ArgumentError("Unknown automerge option :$a"))
        end
    end
    local flags = zero(Cint)
    if get(opts, :renames, false)
        flags |= MERGE_TREE_FIND_RENAMES
    end
    return MergeTreeOptsStruct(one(Cint),
                               flags,
                               rename_threshold,
                               target_limit,
                               C_NULL,
                               automerge_flags)
end

immutable MergeResult
    status::Symbol
    commit::Oid
end

#=
merge_base(r::GitRepo, id1::Oid, id2::Oid) = begin
    out_id = [Oid()]
    @check ccall((:git_merge_base, libgit2), Cint,
                 (Ptr{Oid}, Ptr{Void}, Ptr{Oid}, Ptr{Oid}),
                 id_ptr, r, id1, id2)
    return out_id[1]
end

merge_base(r::GitRepo, ids::Oid...) = begin
    in_ids = [ids...]
    out_id = [Oid()]
    @check ccall((:git_merge_base_many, libgit2), Cint,
                 (Ptr{Oid}, Ptr{Void}, Csize_t, Ptr{Oid}),
                 out_id, r, length(in_ids), in_ids)
    return out_id[1]
end
=#

merge_analysis(r::GitRepo, their_commit::String) = begin
    commit = revparse(r, revparse)
    if !isa(commit, GitCommit)
        throw(ArgumentError("reference string must point to a valid git commit"))
    end
    return merge_analysis(r, commit)
end

merge_analysis(r::GitRepo, their_commit::GitReference) = begin
    commit = lookup(r, their_commit)
    if !isa(commit, GitCommit)
        throw(ArgumentError("GitReference must point to a valid git commit"))
    end
    return merge_analysis(r, commit)
end

merge_analysis(r::GitRepo, their_commit::Oid) = begin
    commit = lookup(r, their_commit)
    if !isa(commit, GitCommit)
        throw(ArgumentError("Oid must be a point to a valid git commit"))
    end
    return merge_analysis(r, commit)
end

merge_analysis(r::GitRepo, their_commit::GitCommit) = begin
    id = Oid(their_commit)
    mhead_ptr = Ptr{Void}[0]
    @check ccall((:git_merge_head_from_id, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Oid}),
                 mhead_ptr, r, &id)
    analysis_ptr, pref_ptr = Cint[0], Cint[0]
    @check ccall((:git_merge_analysis, libgit2), Cint,
                 (Ptr{Cint}, Ptr{Cint}, Ptr{Void},  Ptr{Ptr{Void}}, Csize_t),
                 analysis_ptr, pref_ptr, r, mhead_ptr, 1)
    ccall((:git_merge_head_free, libgit2), Void, (Ptr{Void},), mhead_ptr[1])
    res = Symbol[]
    analysis = analysis_ptr[1]
    if bool(analysis & GitConst.GIT_MERGE_ANALYSIS_NORMAL)
        push!(res, :normal)
    end
    if bool(analysis & GitConst.GIT_MERGE_ANALYSIS_UP_TO_DATE)
        push!(res, :uptodate)
    end
    if bool(analysis & GitConst.GIT_MERGE_ANALYSIS_FASTFORWARD)
        push!(res, :fastforward)
    end
    if bool(analysis & GitConst.GIT_MERGE_ANALYSIS_UNBORN)
        push!(res, :unborn)
    end
    pref = pref_ptr[1] == GitConst.GIT_MERGE_PREFERENCE_NONE ? :none :
           pref_ptr[1] == GitConst.GIT_MERGE_PREFERENCE_NO_FASTFORWARD ? :nofastforward :
           pref_ptr[1] == GitConst.GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY ? :fastforwardonly :
           error("unknown merge analysis preference const $(pref_ptr[1])")
    return res, pref
end

merge_analysis(repo::GitRepo, mheads::Vector{Ptr{Void}}) = begin
    analysis_ptr = Cint[0]
    preference_ptr = Cint[0]
    @check ccall((:git_merge_analysis, libgit2), Cint,
                 (Ptr{Cint}, Ptr{Cint}, Ptr{Void}, Ptr{Ptr{Void}}, Csize_t),
                 analysis_ptr, preference_ptr, repo, mheads, length(mheads))
    return analysis_ptr[1], preference_ptr[1]
end

#TODO: tree's should reference owning repository
Base.merge!(r::GitRepo, t1::GitTree, t2::GitTree, opts=nothing) = begin
    gopts = parse_merge_options(opts)
    idx_ptr = Ptr{Void}[0]
    @check ccall((:git_merge_trees, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Void},
                  Ptr{Void}, Ptr{Void}, Ptr{MergeTreeOptsStruct}),
                 idx_ptr, r, C_NULL, t1, t2, &gopts)
    return GitIndex(idx_ptr[1])
end

Base.merge!(r::GitRepo,
            t1::GitTree,
            t2::GitTree,
            ancestor::GitTree,
            opts::MaybeDict=nothing) = begin
    gopts = parse_merge_options(opts)
    idx_ptr = Ptr{Void}[0]
    @check ccall((:git_merge_trees, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Void},
                 Ptr{Void}, Ptr{Void}, Ptr{MergeTreeOptsStruct}),
                 idx_ptr, r, ancestor.ptr, t1, t2, &gopts)
    return GitIndex(idx_ptr[1])
end

Base.merge!(r::GitRepo, b::String, opts=Dict()) = merge!(r, revparse(r, b), opts)
Base.merge!(r::GitRepo, id::Oid, opts=Dict()) = merge!(r, lookup(r, id), opts)

Base.merge!(r::GitRepo, c::GitCommit, opts=Dict()) = begin
    id = Oid(c)
    mhead_ptr = Ptr{Void}[0]
    @check ccall((:git_merge_head_from_id, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Oid}),
                 mhead_ptr, r, &id)
    try
        return _do_merge(r, mhead_ptr, #=sig,=# opts)
    finally
        if mhead_ptr[1] != C_NULL
            ccall((:git_merge_head_free, libgit2), Void, (Ptr{Void},), mhead_ptr[1])
        end
    end
end

Base.merge!(r::GitRepo, b::GitBranch, opts=Dict()) = begin
    ref = resolve(b)
    mhead_ptr = Ptr{Void}[0]
    @check ccall((:git_merge_head_from_ref, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Void}), mhead_ptr, r, ref)
    try
        _do_merge(r, mhead_ptr, opts)
    finally
        if mhead_ptr[1] != C_NULL
            ccall((:git_merge_head_free, libgit2), Void, (Ptr{Void},), mhead_ptr[1])
        end
    end
end

function _do_merge(r::GitRepo, mhead_ptr::Vector{Ptr{Void}}, #=sig,=# opts=Dict())
    analysis_ptr, pref_ptr = Cint[0], Cint[0]
    @check ccall((:git_merge_analysis, libgit2), Cint,
                 (Ptr{Cint}, Ptr{Cint}, Ptr{Void},  Ptr{Ptr{Void}}, Csize_t),
                 analysis_ptr, pref_ptr, r, mhead_ptr, length(mhead_ptr))
    analysis, pref = analysis_ptr[1], pref_ptr[1]
    if isequal(analysis & GitConst.GIT_MERGE_ANALYSIS_UP_TO_DATE,
               GitConst.GIT_MERGE_ANALYSIS_UP_TO_DATE)
        return :uptodate, nothing
    end
    local ffstrategy::Symbol
    if haskey(opts, :ffstrategy) && opts[:ffstrategy] != :default
        ffstrategy = opts[:ffstrategy]
    else
        ffstrategy = pref == GitConst.GIT_MERGE_PREFERENCE_NONE ? :default :
                     pref == GitConst.GIT_MERGE_PREFERENCE_NO_FASTFORWARD ? :nofastforward :
                     pref == GitConst.GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY ? :fastforwardonly :
                     error("unknown merge analysis preference const $(pref)")
    end
    if ffstrategy === :default
        if bool(analysis & GitConst.GIT_MERGE_ANALYSIS_FASTFORWARD)
            merge_result = _fast_forward_merge(r, mhead_ptr[1], #=sig,=# opts)
        elseif bool(analysis & GitConst.GIT_MERGE_ANALYSIS_NORMAL)
            merge_result = _normal_merge(r, mhead_ptr, #=sig,=# opts)
        else
            error("unknown merge analysis strategy: $analysis")
        end
    elseif ffstrategy === :fastforwardonly
        if bool(analysis & GitConst.GIT_MERGE_ANALYSIS_FASTFORWARD)
            merge_result = _fast_forward_merge(r, mhead_ptr[1], #=sig,=# opts)
        else
            error("cannot perform fast forward on a non fast forward merge")
        end
    elseif ffstrategy === :nofastforward
        if bool(analysis & GitConst.GIT_MERGE_ANALYSIS_NORMAL)
            merge_result = _normal_merge(r, mhead_ptr, #=sig,=# opts)
        else
            error("unknown merge analysis const $analysis")
        end
    else
        error("unknown fast forward strategy :$ffstrategy")
    end
    return merge_result
end

# perform a normal merge
_normal_merge(r::GitRepo, mhead_ptr::Vector{Ptr{Void}}, opts=Dict()) = begin
    mopts = parse_merge_options(opts)
    copts = parse_checkout_options(opts)
    @check ccall((:git_merge, libgit2), Cint,
                 (Ptr{Void}, Ptr{Ptr{Void}}, Csize_t,
                  Ptr{MergeTreeOptsStruct}, Ptr{CheckoutOptionsStruct}),
                  r, mhead_ptr, length(mhead_ptr), &mopts, &copts)
    free!(copts.paths)
    idx = GitIndex(r)
    if is_fully_merged(idx)
        if haskey(opts, :commit_on_success)
            msg = opts[:commit_on_success]
            if !isa(msg, String)
                throw(ArgumentError(":commit_on_success option value must be a string"))
            end
            # todo: Signature
            sig = default_signature(r)
            # commit the merge
            merge_commit = commit(r, msg, sig, sig)
            return (:nonfastforward, merge_commit)
        else
            return (:nonfastforward, nothing)
        end
    else
        return (:conflicts, nothing)
    end
end

_fast_forward_merge(r::GitRepo, mhead::Ptr{Void}, opts=Dict()) = begin
    id = Oid(ccall((:git_merge_head_id, libgit2), Ptr{Oid}, (Ptr{Void},), mhead))
    ffcommit = lookup(r, id)
    @assert ffcommit != nothing
    opts[:strategy] = :safe

    checkout_tree!(r, GitTree(ffcommit), opts)
    ref = resolve(head(r))
    if ref === nothing
        # reference does not exist, create it :TODO sig
        create_ref(r, "HEAD", id, force=true)
    else
        # update target reference
        set_target(head(r), id)
    end
    return (:fastforward, ffcommit)
end

#------- Merge Commits -----
function merge_commits(r::GitRepo,
                       ours::Union(GitCommit, Oid),
                       theirs::Union(GitCommit, Oid),
                       opts::MaybeDict=nothing)
    gopts = parse_merge_options(opts)
    isa(ours,   Oid) && (ours   = lookup_commit(r, ours))
    isa(theirs, Oid) && (theirs = lookup_commit(r, theirs))
    index_ptr = Ptr{Void}[0]
    @check ccall((:git_merge_commits, libgit2), Cint,
                 (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Void},
                  Ptr{Void}, Ptr{MergeTreeOptsStruct}),
                 index_ptr, r, ours, theirs, &gopts)
    return GitIndex(index_ptr[1])
end

#------- Repo Checkout -------
function cb_checkout_progress(path_ptr::Ptr{Uint8},
                              completed_steps::Csize_t,
                              total_steps::Csize_t,
                              payload::Ptr{Void})
    callback = unsafe_pointer_to_objref(payload)::Function
    path = path_ptr != C_NULL ? bytestring(path_ptr) : nothing
    callback(path, completed_steps, total_steps)
    return
end

const c_cb_checkout_progress = cfunction(cb_checkout_progress, Void,
                                         (Ptr{Uint8}, Csize_t, Csize_t, Ptr{Void}))

function cb_checkout_notify(why::Cint,
                            path_ptr::Ptr{Uint8},
                            baseline::Ptr{DiffFileStruct},
                            target::Ptr{DiffFileStruct},
                            workdir::Ptr{DiffFileStruct},
                            payload::Ptr{Void})
    callback = unsafe_pointer_to_objref(payload)::Function
    path = path_ptr != C_NULL ? bytestring(path_ptr) : nothing
    local reason::Symbol
    if why == GitConst.CHECKOUT_NOTIFY_CONFLICT
        reason = :conflict
    elseif why == GitConst.CHECKOUT_NOTIFY_DIRTY
        reason = :dirty
    elseif why == GitConst.CHECKOUT_NOTIFY_UPDATED
        reason = :updated
    elseif why == GitConst.CHECKOUT_NOTIFY_UNTRACKED
        reason = :untracked
    elseif why == GitConst.CHECKOUT_NOTIFY_IGNORED
        reason = :ignored
    else
        reason = :unknown
    end
    try
        callback(why, path, DiffFile(baseline), DiffFile(target), DiffFile(workdir))
        return GitErrorConst.GIT_OK
    catch
        return GitErrorConst.ERROR
    end
end

const c_cb_checkout_notify = cfunction(cb_checkout_notify, Cint,
                                       (Cint,
                                        Ptr{Uint8},
                                        Ptr{DiffFileStruct},
                                        Ptr{DiffFileStruct},
                                        Ptr{DiffFileStruct},
                                        Ptr{Void}))

parse_checkout_options(opts::Nothing) = CheckoutOptionsStruct()
parse_checkout_options(opts::Dict) = begin
    isempty(opts) && return CheckoutOptionsStruct()
    progress_cb = convert(Ptr{Void}, C_NULL)
    progress_payload = convert(Ptr{Void}, C_NULL)
    if haskey(opts, :progress)
        if !isa(opts[:progress], Function)
            throw(ArgumentError("checkout options :progress flag must be a Function"))
        end
        progress_cb = c_cb_checkout_progress
        progress_payload = pointer_from_objref(opts[:progress])
    end
    notify_cb = convert(Ptr{Void}, C_NULL)
    notify_payload = convert(Ptr{Void}, C_NULL)
    if haskey(opts, :notify)
        if !isa(opts[:notify], Function)
            throw(ArgumentError("checkout options :notify flag must be a Function"))
        end
        notify_cb = c_cb_checkout_notify
        notify_payload = pointer_from_objref(opts[:notify])
    end
    checkout_strategy = zero(Cuint)
    if haskey(opts, :strategy)
        local strategy::Vector{Symbol}
        if isa(opts[:strategy], Symbol)
            strategy = [opts[:strategy]]
        else
            strategy = opts[:strategy]
        end
        for s in strategy
            if s == :safe
                checkout_strategy |= GitConst.CHECKOUT_SAFE
            elseif s == :safe_create
                checkout_strategy |= GitConst.CHECKOUT_SAFE_CREATE
            elseif s == :force
                checkout_strategy |= GitConst.CHECKOUT_FORCE
            elseif s == :allow_conflicts
                checkout_strategy |= GitConst.CHECKOUT_ALLOW_CONFLICTS
            elseif s == :remove_untracked
                checkout_strategy |= GitConst.CHECKOUT_REMOVE_UNTRACKED
            elseif s == :remove_ignored
                checkout_strategy |= GitConst.CHECKOUT_REMOVE_IGNORED
            elseif s == :update_only
                checkout_strategy |= GitConst.CHECKOUT_UPDATE_ONLY
            elseif s == :dont_update_index
                checkout_strategy |= GitConst.CHECKOUT_DONT_UPDATE_INDEX
            elseif s == :no_refresh
                checkout_strategy |= GitConst.CHECKOUT_NO_REFRESH
            elseif s == :disable_pathspec_match
                checkout_strategy |= GitConst.CHECKOUT_DISABLE_PATHSPEC_MATCH
            elseif s == :skip_locked_directories
                checkout_strategy |= GitConst.CHECKOUT_SKIP_LOCKED_DIRECTORIES
            elseif s == :skip_unmerged
                checkout_strategy |= GitConst.CHECKOUT_SKIP_UNMERGED
            elseif s == :use_ours
                checkout_strategy |= GitConst.CHECKOUT_USE_OURS
            elseif s == :use_theirs
                checkout_strategy |= GitConst.CHECKOUT_USE_THEIRS
            elseif s == :update_submodules
                checkout_strategy |= GitConst.CHECKOUT_UPDATE_SUBMODULES
            elseif s == :update_submodules_if_changed
                checkout_strategy |= GitConst.CHECKOUT_UPDATE_SUBMODULES_IF_CHANGED
            else
                throw(ArgumentError("unknown checkout options :strategy flag :$s"))
            end
        end
    end
    notify_flags = zero(Cuint)
    if haskey(opts, :notify_flags)
        local flags::Vector{Symbol}
        if isa(opts[:notify_flags], Symbol)
            flags = [opts[:notify_flags]]
        else
            flags = opts[:notify_flags]
        end
        for f in flags
            if f == :conflict
                notify_flags |= GitConst.CHECKOUT_NOTIFY_CONFLICT
            elseif f == :dirty
                notify_flags |= GitConst.CHECKOUT_NOTIFY_DIRTY
            elseif f == :updated
                notify_flags |= GitConst.CHECKOUT_NOTIFY_UPDATED
            elseif f == :untracked
                notify_flags |= GitConst.CHECKOUT_NOTIFY_UNTRACKED
            elseif f == :ignored
                notify_flags |= GitConst.CHECKOUT_NOTIFY_IGNORED
            elseif f == :all
                notify_flags |= GitConst.CHECKOUT_NOTIFY_ALL
            else
                throw(ArgumentError("unknown checkout options :notify flag :$f"))
            end
        end
    end

    disable_filters = convert(Cint, bool(get(opts, :disable_filters, false)))

    dir_mode = zero(Cuint)
    if haskey(opts, :dir_mode)
        dir_mode = convert(Cuint, opts[:dir_mode])
    end
    file_mode = zero(Cuint)
    if haskey(opts, :file_mode)
        file_mode = convert(Cuint, opts[:file_mode])
    end
    file_open_flags = zero(Cint)
    if haskey(opts, :file_open_flags)
        file_open_flags = convert(Cint, opts[:file_open_flags])
    end
    target_directory = zero(Ptr{Uint8})
    if haskey(opts, :target_directory)
        if !isa(opts[:target_directory], String)
            throw(ArgumentError("checkout options :target_target must be a String"))
        end
        target_directory = convert(Ptr{Uint8}, bytestring(opts[:target_directory]))
    end
    baseline = zero(Ptr{Void})
    if haskey(opts, :baseline)
        if isa(opts[:baseline], GitTree)
            baseline = convert(Ptr{Void}, opts[:baseline])
        else
            throw(ArgumentError("checkout options :baseline must be a GitTree"))
        end
    end
    paths = StrArrayStruct()
    if haskey(opts, :paths)
        paths = StrArrayStruct(opts[:paths])
    end
    ancestor_label = zero(Ptr{Uint8})
    if haskey(opts, :ancestor_label)
        if !isa(opts[:ancestor_label], String)
            throw(ArgumentError("checkout options :ancestor_label must be a String"))
        end
        ancestor_label = convert(Ptr{Uint8}, opts[:ancestor_label])
    end
    our_label = zero(Ptr{Uint8})
    if haskey(opts, :our_label)
        if !isa(opts[:ancestor_label], String)
            throw(ArgumentError("checkout options :our_label must be a String"))
        end
        our_label = convert(Ptr{Uint8}, opts[:our_label])
    end
    their_label = zero(Ptr{Uint8})
    if haskey(opts, :their_label)
        if !isa(opts[:ancestor_label], String)
            throw(ArgumentError("checkout options :their_label must be a String"))
        end
        their_label = convert(Ptr{Uint8}, opts[:their_label])
    end
    return CheckoutOptionsStruct(
                          one(Cuint),
                          checkout_strategy,
                          disable_filters,
                          dir_mode,
                          file_mode,
                          file_open_flags,
                          notify_flags,
                          notify_cb,
                          notify_payload,
                          progress_cb,
                          progress_payload,
                          paths,
                          baseline,
                          target_directory,
                          ancestor_label,
                          our_label,
                          their_label)
end

typealias Treeish Union(GitCommit, GitTag, GitTree)

function remove_untracked!(r::GitRepo)
    opts = {:strategy => [:remove_untracked, :allow_conflicts]}
    gopts = parse_checkout_options(opts)
    err = ccall((:git_checkout_index, libgit2), Cint,
                (Ptr{Void}, Ptr{Void}, Ptr{CheckoutOptionsStruct}),
                r, C_NULL, &gopts)
    free!(gopts.paths)
    if err != GitErrorConst.GIT_OK
        throw(LibGitError(err))
    end
    return r
end

checkout_tree!(r::GitRepo, tree::String, opts::MaybeDict=nothing) =
    return checkout_tree!(r, revparse(r, tree), opts)

function checkout_tree!(r::GitRepo, tree::Treeish, opts::MaybeDict=nothing)
    gopts = parse_checkout_options(opts)
    err = ccall((:git_checkout_tree, libgit2), Cint,
                (Ptr{Void}, Ptr{Void}, Ptr{CheckoutOptionsStruct}),
                r, tree, &gopts)
    free!(gopts.paths)
    if err != GitErrorConst.GIT_OK
        throw(LibGitError(err))
    end
    return r
end

function checkout_head!(r::GitRepo, opts::MaybeDict=nothing)
    gopts = parse_checkout_options(opts)
    err = ccall((:git_checkout_head, libgit2), Cint,
                (Ptr{Void}, Ptr{CheckoutOptionsStruct}),r, &gopts)
    free!(gopts.paths)
    if err != GitErrorConst.GIT_OK
        throw(GitError(err))
    end
    return r
end

function checkout!(r::GitRepo, target::GitCommit, opts::MaybeDict=nothing)
    prev_head = is_head_detached(r) ? tip(head(r)) : name(head(r))
    create_ref(r, "HEAD", Oid(target), force=true)
    checkout_tree!(r, target, opts)
end

checkout!(target::GitBranch, opts=Dict()) = checkout!(owner(target), target, opts)

function checkout!(r::GitRepo, target, opts=Dict())
    delete!(opts, :paths)
    !haskey(opts, :strategy) && (opts[:strategy] = :safe)
    if target == "HEAD"
        return checkout_head!(r, opts)
    end
    local branch
    if isa(target, GitBranch)
        branch = target
    else
        branch = lookup_branch(r, string(target), :local)
        if branch == nothing
            branch = lookup_branch(r, string(target), :remote)
        end
    end
    if branch != nothing
        checkout_tree!(r, tip(branch), opts)
        isremote(branch) ? create_ref(r, "HEAD", Oid(tip(branch)), force=true) :
                           create_ref(r, "HEAD", canonical_name(branch), force=true)
    else
        commit = lookup_commit(r, revparse_oid(r, target))
        create_ref(r, "HEAD", Oid(commit), force=true)
        checkout_tree!(r, commit, opts)
    end
end

#------- Repo Clone -------
function cb_remote_transfer(stats_ptr::Ptr{TransferProgressStruct},
                            payload_ptr::Ptr{Void})
    stats = unsafe_load(stats_ptr)::TransferProgressStruct
    payload  = unsafe_pointer_to_objref(payload_ptr)::Dict
    callback = payload[:callbacks][:transfer_progress]
    try
        callback(stats.total_objects,
                 stats.indexed_objects,
                 stats.received_objects,
                 stats.received_bytes)
        return GitErrorConst.GIT_OK
    catch ex
        payload[:exception] = ex
        return GitErrorConst.ERROR
    end
end

const c_cb_remote_transfer = cfunction(cb_remote_transfer, Cint,
                                       (Ptr{TransferProgressStruct}, Ptr{Void}))

function extract_cred!(cred::GitCredential, cred_ptr::Ptr{Ptr{Void}}, allowed_types::Cuint)
    if isa(cred, PlainTextCred)
        if !bool(allowed_types & GitConst.CREDTYPE_USERPASS_PLAINTEXT)
            error("invalid credential type")
        end
        @check ccall((:git_cred_userpass_plaintext_new, libgit2), Cint,
                      (Ptr{Ptr{Void}}, Ptr{Uint8}, Ptr{Uint8}),
                       cred_ptr, cred.username, cred.password)
    elseif isa(cred, SSHKeyCred)
        if !bool(allowed_types & GitConst.CREDTYPE_SSH_KEY)
            error("invalid credential type")
        end
        @check ccall((:git_cred_ssh_key_new, libgit2), Cint,
                     (Ptr{Ptr{Void}}, Ptr{Uint8}, Ptr{Uint8}, Ptr{Uint8}, Ptr{Uint8}),
                     cred_ptr,
                     cred.username   != nothing ? cred.username   : C_NULL,
                     cred.publickey  != nothing ? cred.publickey  : C_NULL,
                     cred.privatekey != nothing ? cred.privatekey : C_NULL,
                     cred.passphrase != nothing ? cred.passphrase : C_NULL)
    elseif (cred, DefaultCred)
        if !bool(allowed_types & GitConst.CREDTYPE_SSH_KEY)
            error("invalid credential type")
        end
        @check ccall((:git_cred_default_new, libgit2), Cint, (Ptr{Ptr{Void}},), cred_ptr)
    else
        error("invalid credential type")
    end
    return
end

function cb_default_remote_credentials(cred_ptr::Ptr{Ptr{Void}},
                                       url::Ptr{Uint8},
                                       username_from_url::Ptr{Uint8},
                                       allowed_types::Cuint,
                                       payload_ptr::Ptr{Void})
    payload = unsafe_pointer_to_objref(payload_ptr)::Dict
    cred = payload[:credentials]
    try
        extract_cred!(cred, cred_ptr, allowed_types)
        return GitErrorConst.GIT_OK
    catch ex
        payload[:exception] = ex
        return GitErrorConst.ERROR
    end
end

const c_cb_default_remote_credentials =
    cfunction(cb_default_remote_credentials, Cint,
             (Ptr{Ptr{Void}}, Ptr{Uint8}, Ptr{Uint8}, Cuint, Ptr{Void}))

function cb_remote_credentials(cred_ptr::Ptr{Ptr{Void}},
                               url::Ptr{Uint8},
                               username::Ptr{Uint8},
                               allowed_types::Cuint,
                               payload_ptr::Ptr{Void})
    payload = unsafe_pointer_to_objref(payload_ptr)::Dict
    cred_func = payload[:credentials]::Function
    types = Symbol[]
    if bool(allowed_types & GitConst.CREDTYPE_USERPASS_PLAINTEXT)
        push!(types, :plaintext)
    end
    if bool(allowed_types & GitConst.CREDTYPE_SSH_KEY)
        push!(types, :sshkey)
    end
    if bool(allowed_types & GitConst.CREDTYPE_DEFAULT)
        push!(types, :default)
    end
    try
        cred = cred_func(url != C_NULL ? bytestring(url) : nothing,
                         username != C_NULL ? bytestring(username) : nothing,
                         types)
        @assert cred <: GitCredential
        extract_cred!(cred, cred_ptr, allowed_types)
        return GitErrorConst.GIT_OK
    catch err
        payload[:exception] = err
        return GitErrorConst.ERROR
    end
end

const c_cb_remote_credential = cfunction(cb_remote_credentials, Cint,
                                         (Ptr{Ptr{Void}}, Ptr{Uint8}, Ptr{Uint8}, Cuint, Ptr{Void}))

parse_clone_options(opts::Nothing, payload::Dict) = CloneOptionsStruct()
parse_clone_options(opts, payload::Dict) = begin
    isempty(opts) && return CloneOptionsStruct()
    local bare = zero(Cint)
    if haskey(opts, :bare)
        bare = convert(Cint, bool(opts[:bare]))
    end
    local remote_credentials_cb = zero(Ptr{Void})
    if haskey(opts, :credentials)
        cred = opts[:credentials]
        if isa(cred, GitCredential)
            payload[:credentials] = cred
            remote_credentials_cb = convert(Ptr{Void}, c_cb_default_remote_credentials)
        elseif isa(cred, Function)
            payload[:credentials] = cred
            remote_credentials_cb = convert(Ptr{Void}, c_cb_remote_credential)
        else
            throw(ArgumentError("clone option :credentials must be a GitCredential or Function type"))
        end
    end
    local remote_transfer_progress_cb = zero(Ptr{Void})
    if haskey(opts, :callbacks)
        callbacks = opts[:callbacks]
        if haskey(callbacks, :transfer_progress)
            if !isa(callbacks[:transfer_progress], Function)
                throw(ArgumentError("clone callback :transfer_progress must be a Function"))
            end
            payload[:callbacks] = callbacks
            remote_transfer_progress_cb = convert(Ptr{Void}, c_cb_remote_transfer)
        end
    end
    remote_payload  = convert(Ptr{Void}, pointer_from_objref(payload))
    checkout_stuct  = parse_checkout_options(opts)
    remotecb_struct = RemoteCallbacksStruct(one(Cuint),
                                            zero(Ptr{Void}),
                                            zero(Ptr{Void}),
                                            remote_credentials_cb,
                                            remote_transfer_progress_cb,
                                            zero(Ptr{Void}),
                                            remote_payload)
    return CloneOptionsStruct(one(Cuint),
                              checkout_stuct,
                              remotecb_struct,
                              bare,
                              zero(Cint),
                              zero(Cint),
                              zero(Ptr{Uint8}),
                              zero(Ptr{Uint8}),
                              zero(Ptr{SignatureStruct}))
end

function cb_remote_transfer(stats_ptr::Ptr{TransferProgressStruct}, payload_ptr::Ptr{Void})
    stats = unsafe_load(stats_ptr)::TransferProgressStruct
    payload  = unsafe_pointer_to_objref(payload_ptr)::Dict
    callback = payload[:callbacks][:transfer_progress]
    try
        callback(stats.total_objects,
                 stats.indexed_objects,
                 stats.received_objects,
                 stats.local_objects,
                 stats.total_deltas,
                 stats.indexed_deltas,
                 stats.received_bytes)
        return GitErrorConst.GIT_OK
    catch err
        payload[:exception] = err
        return GitErrorConst.ERROR
    end
end

function repo_clone(url::String, localpath::String, opts::MaybeDict=nothing)
    check_valid_url(url)
    payload = Dict()
    opts_struct = parse_clone_options(opts, payload)::CloneOptionsStruct
    repo_ptr = Ptr{Void}[0]
    err = ccall((:git_clone, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Uint8}, Ptr{Uint8}, Ptr{CloneOptionsStruct}),
                repo_ptr, url, localpath, &opts_struct)
    if haskey(payload, :exception)
        #TODO: clean up clone options struct
        throw(payload[:exception])
    end
    if err != GitErrorConst.GIT_OK
        #TODO: clean up clone options struct
        throw(LibGitError(err))
    end
    return GitRepo(repo_ptr[1])
end

#-------- Reference Iterator --------
#TODO: handle error's when iterating (see branch)
type ReferenceIterator
    repo::GitRepo
    ptr::Ptr{Void}

    function ReferenceIterator(r::GitRepo, ptr::Ptr{Void})
        @assert ptr != C_NULL
        it = new(r, ptr)
        finalizer(it, free!)
        return it
    end
end

free!(it::ReferenceIterator) = begin
    if it.ptr != C_NULL
        ccall((:git_reference_iterator_free, libgit2), Void, (Ptr{Void},), it.ptr)
        it.ptr = C_NULL
    end
end

Base.convert(::Type{Ptr{Void}}, it::ReferenceIterator) = it.ptr

function foreach(::Type{GitReference}, r::GitRepo, glob::String="")
    iter_ptr = Ptr{Void}[0]
    if isempty(glob)
        @check ccall((:git_reference_iterator_new, libgit2), Cint,
                     (Ptr{Ptr{Void}}, Ptr{Void}), iter_ptr, r)
    else
        @check ccall((:git_reference_iterator_glob_new, libgit2), Cint,
                     (Ptr{Ptr{Void}}, Ptr{Void}, Ptr{Uint8}), iter_ptr, r, glob)
    end
    return ReferenceIterator(r, iter_ptr[1])
end

Base.start(it::ReferenceIterator) = begin
    ref_ptr = Ptr{Void}[0]
    err = ccall((:git_reference_next, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Void}), ref_ptr, it)
    if err == GitErrorConst.ITEROVER
        return nothing
    elseif err != GitErrorConst.GIT_OK
        if ref_ptr[1] != C_NULL
            ccall((:git_reference_free, libgit2), Void, (Ptr{Void},), ref_ptr[1])
        end
        throw(LibGitError(err))
    end
    return GitReference(ref_ptr[1])
end

Base.done(it::ReferenceIterator, state) = is(state, nothing)

Base.next(it::ReferenceIterator, state) = begin
    ref_ptr = Ptr{Void}[0]
    err = ccall((:git_reference_next, libgit2), Cint,
                (Ptr{Ptr{Void}}, Ptr{Void}), ref_ptr, it)
    if err == GitErrorConst.ITEROVER
        return (state, nothing)
    elseif err != GitErrorConst.GIT_OK
        if ref_ptr[1] != C_NULL
            ccall((:git_reference_free, libgit2), Void, (Ptr{Void},), ref_ptr[1])
        end
        throw(LibGitError(err))
    end
    return (state, GitReference(ref_ptr[1]))
end
ref_names(r::GitRepo, glob::String="") = map(name, foreach(GitReference, r, glob))


text:
module Rayuela

# For PQ/OPQ training and encoding
using Clustering, Distances

# For LSQR
using IterativeSolvers

# For nworkers()
using Distributed

# For sampling
using Distributions

# For LSQ encoding in the GPU
using CUDAdrv
using CuArrays

using HDF5
using LinearAlgebra

# For default keyword arguments in CQ parameters
using Parameters

using Printf

# For randperm
using Random

# For parallel CPU
using SharedArrays

# For std
using Statistics

# For codebook update baselines
using SparseArrays

### Load and initialize the linscan binaries ###
const depsfile = joinpath(dirname(@__DIR__), "deps", "deps.jl")
if isfile(depsfile)
  include(depsfile)
else
  error("Rayuela is not properly Installed.
  Please run Pkg.build(\"Rayuela\") and make sure you have nvcc and g++ available from the command line.")
end
cudautilsptx = cudautils[1:end-2] * "ptx"

# === Functions to read data ===
include("xvecs_read.jl")
include("xvecs_write.jl")
include("read_datasets.jl")

# === Utility functions mostly ===
include("utils.jl")
include("qerrors.jl")

include("Linscan.jl")
include("codebook_update.jl")

# === Orthogonal quantizers ===
include("PQ.jl")  # Product Quantizer
include("OPQ.jl") # Optimized Product Quantizer

# === Non-orthogonal quantizers ===
include("ChainQ.jl") # Chain (Tree) Quantization
include("CQ.jl")     # Functions to interact with C++ CQ release
include("RVQ.jl")    # RVQ
include("ERVQ.jl")   # ERVQ / Stacked quantizers
include("CompetitiveQ.jl") # Slow version

# === LSQ Quantizer ===
include("LSQ.jl") # Local search quantization
include("SR_perturbations.jl") # Utils for SR

# === CUDA ports ===
include("CudaUtilsModule.jl")
include("SR.jl")  # Stochastic relaxations
include("LSQ_GPU.jl") # Local search quantization

end # module

ext: jl


text:
using AdalmPluto
using FFTW

function normalizePower!(buffer)
    maxI = (buffer |> real .|> abs |> maximum) * 1.05 # Max scale + backoff of I path
    maxQ = (buffer |> imag .|> abs |> maximum) * 1.05 # Max scale + backoff of Q path
    buffer .= real(buffer) ./ maxI + 1im*imag(buffer) / maxQ
end

function tx()

    # ----------------------------------------------------
    # --- Radio
    # ----------------------------------------------------
    carrierFreq		= 700e6
    samplingRate    = 4e6;

    # ----------------------------------------------------
    # --- Opening radio
    # ----------------------------------------------------
    # global pluto = openPluto(Int(carrierFreq), Int(samplingRate), Int(40),bufferSize=UInt(2048));
    global pluto = openPluto(Int(carrierFreq), Int(samplingRate), Int(40));

    # ----------------------------------------------------
    # --- Create buffer
    # ----------------------------------------------------
    f_c     = 1e6
    nbSamples = 2048
    buffer  = 0.9.*[exp.(2im * π * f_c / samplingRate * n)  for n ∈ (0:nbSamples-1)];
    buffer  = convert.(Complex{Cfloat},buffer);

    # --- Emulate a simple OFDM signal
    nS = 1024
    bData = ComplexF32.( (rand([0 1],nS) +1im*(rand([0 1],nS)))/sqrt(2))
    buffer = zeros(ComplexF32,nbSamples)
    buffer[1:nS÷2] = bData[1:nS÷2]
    buffer[end-nS÷2+1:end] = bData[1+nS÷2:end]
    buffer = ifft(buffer)
    normalizePower!(buffer)

    # ----------------------------------------------------
    # --- Send buffer
    # ----------------------------------------------------
    cnt = 0
    try
        while (true)
            n = send(pluto,buffer)
            cnt += n
            yield()
        end
    catch exception
        @info "Get interruption, close radio after $cnt elements "
        close(pluto)
        rethrow(exception)
    end
    close(radio)
    return cnt
end


text:
module PlotlyBase

using Base.Iterators
using JSON
using DocStringExtensions
using Requires
using UUIDs
using Dates
using Logging
using ColorSchemes
using Parameters

import Base: ==

using Statistics: mean
using DelimitedFiles: readdlm

# import LaTeXStrings and export the handy macros
using LaTeXStrings
export @L_mstr, @L_str
using Pkg.Artifacts

# export some names from JSON
export json

_symbol_dict(x) = x
_symbol_dict(d::AbstractDict) =
    Dict{Symbol,Any}([(Symbol(k), _symbol_dict(v)) for (k, v) in d])

const _Maybe{T} = Union{Missing,T}

abstract type AbstractPlotlyAttribute end

mutable struct PlotlyAttribute{T <: AbstractDict{Symbol,Any}} <: AbstractPlotlyAttribute
    fields::T
end

const _ATTR = PlotlyAttribute{Dict{Symbol,Any}}

struct Cycler
    vals::Vector
end

Base.isempty(c::Cycler) = isempty(c.vals)
Base.length(c::Cycler) = length(c.vals)
Cycler(t::Tuple) = Cycler(collect(t))
Cycler(x::Union{String,Number,Date,Symbol}) = Cycler([x])

function Base.getindex(c::Cycler, ix::Integer)
    n = length(c.vals)
    @inbounds v = c.vals[mod1(ix, n)]
    v
end

function Base.getindex(c::Cycler, ixs::AbstractVector{<:Integer})
    [c[i] for i in ixs]
end

Base.iterate(c::Cycler, s::Int=1) = c[s], s + 1
Base.IteratorSize(::Cycler) = Base.IsInfinite()

# include these here because they are used below
include("plot_config.jl")
include("subplot_utils.jl")
include("traces_layouts.jl")

const PLOTSCHEMA = attr();

function get_plotschema()
    if _isempty(PLOTSCHEMA)
        out = JSON.parsefile(joinpath(artifact"plotly-base-artifacts", "plot-schema.json"))
        PLOTSCHEMA.fields = _symbol_dict(out)
    end
    return PLOTSCHEMA
end

# core plot object
mutable struct Plot{TT<:AbstractVector{<:AbstractTrace},TL<:AbstractLayout,TF<:AbstractVector{<:PlotlyFrame}}
    data::TT
    layout::TL
    frames::TF
    divid::UUID
    config::PlotConfig
end

# Default `convert` fallback constructor
Plot(p::Plot) = p

# include the rest of the core parts of the package
include("util.jl")
include("json.jl")
include("subplots.jl")
include("api.jl")
include("convenience_api.jl")
include("recession_bands.jl")
include("output.jl")
include("templates.jl")
include("colors.jl")

# Set some defaults for constructing `Plot`s
function Plot(;config::PlotConfig=PlotConfig())
    Plot(GenericTrace{Dict{Symbol,Any}}[], Layout(), PlotlyFrame[], uuid4(), config)
end

function Plot(data::AbstractVector{<:AbstractTrace}, layout=Layout(), frames::AbstractVector{<:PlotlyFrame}=PlotlyFrame[];
              config::PlotConfig=PlotConfig())
    Plot(data, layout, frames, uuid4(), config)
end

function Plot(data::AbstractTrace, layout=Layout(), frames::AbstractVector{<:PlotlyFrame}=PlotlyFrame[];
              config::PlotConfig=PlotConfig())
    Plot([data], layout, frames; config=config)
end

# empty plot
function Plot(layout::Layout, frames::AbstractVector{<:PlotlyFrame}=PlotlyFrame[];
    config::PlotConfig=PlotConfig())
    Plot(GenericTrace[], layout, frames; config=config)
end

# NOTE: we export trace constructing types from inside api.jl
# NOTE: we export names of shapes from traces_layouts.jl
export

    # core types
    Plot, GenericTrace, PlotlyFrame, Layout, Shape, AbstractTrace, AbstractLayout,
    PlotConfig, Spec, Subplots, Inset,

    # plotly.js api methods
    restyle!, relayout!, update!, addtraces!, deletetraces!, movetraces!,
    redraw!, extendtraces!, prependtraces!, purge!, to_image, download_image,
    react!,

    # non-!-versions (forks, then applies, then returns fork)
    restyle, relayout, update, addtraces, deletetraces, movetraces, redraw,
    extendtraces, prependtraces, react,

    # helper methods
    plot, fork, vline, hline, attr, frame, add_trace!,

    # templates
    templates, Template,

    # new trace types
    stem,

    # convenience stuff
    add_recession_bands!, Cycler,

    # other
    savejson, colors

function __init__()
    @require IJulia="7073ff75-c697-5162-941a-fcdaad2a7d2a" begin

        function IJulia.display_dict(p::Plot)
            Dict(
                "application/vnd.plotly.v1+json" => JSON.lower(p),
                "text/plain" => sprint(show, "text/plain", p),
                "text/html" => let
                    buf = IOBuffer()
                    show(buf, MIME("text/html"), p, include_plotlyjs="require")
                    String(resize!(buf.data, buf.size))
                end
            )
        end
    end
    @require DataFrames="a93c6f00-e57d-5684-b7b6-d8193f3e46c0" include("dataframes_api.jl")
    @require Distributions="31c24e10-a181-5473-b8eb-7969acd0382f" include("distributions.jl")
    @require Colors="5ae59095-9a9b-59fe-a467-6f913c188581" begin
        _json_lower(a::Colors.Colorant) = string("#", Colors.hex(a))
    end
    @require JSON2="2535ab7d-5cd8-5a07-80ac-9b1792aadce3" JSON2.write(io::IO, p::Plot) = begin
        data = JSON.lower(p)
        pop!(data, :config, nothing)
        JSON.print(io, data)
    end
    @require JSON3="0f8b85d8-7281-11e9-16c2-39a750bddbf1" include("json3.jl")
end

end # module


text:
# This file is a part of Julia. License is MIT: https://julialang.org/license

module Unicode

export graphemes, isequal_normalized

"""
    Unicode.normalize(s::AbstractString; keywords...)
    Unicode.normalize(s::AbstractString, normalform::Symbol)

Normalize the string `s`. By default, canonical composition (`compose=true`) is performed without ensuring
Unicode versioning stability (`compat=false`), which produces the shortest possible equivalent string
but may introduce composition characters not present in earlier Unicode versions.

Alternatively, one of the four "normal forms" of the Unicode standard can be specified:
`normalform` can be `:NFC`, `:NFD`, `:NFKC`, or `:NFKD`.  Normal forms C
(canonical composition) and D (canonical decomposition) convert different visually identical
representations of the same abstract string into a single canonical form, with form C being
more compact.  Normal forms KC and KD additionally canonicalize "compatibility equivalents":
they convert characters that are abstractly similar but visually distinct into a single
canonical choice (e.g. they expand ligatures into the individual characters), with form KC
being more compact.

Alternatively, finer control and additional transformations may be obtained by calling
`Unicode.normalize(s; keywords...)`, where any number of the following boolean keywords
options (which all default to `false` except for `compose`) are specified:

* `compose=false`: do not perform canonical composition
* `decompose=true`: do canonical decomposition instead of canonical composition
  (`compose=true` is ignored if present)
* `compat=true`: compatibility equivalents are canonicalized
* `casefold=true`: perform Unicode case folding, e.g. for case-insensitive string comparison
* `newline2lf=true`, `newline2ls=true`, or `newline2ps=true`: convert various newline
  sequences (LF, CRLF, CR, NEL) into a linefeed (LF), line-separation (LS), or
  paragraph-separation (PS) character, respectively
* `stripmark=true`: strip diacritical marks (e.g. accents)
* `stripignore=true`: strip Unicode's "default ignorable" characters (e.g. the soft hyphen
  or the left-to-right marker)
* `stripcc=true`: strip control characters; horizontal tabs and form feeds are converted to
  spaces; newlines are also converted to spaces unless a newline-conversion flag was
  specified
* `rejectna=true`: throw an error if unassigned code points are found
* `stable=true`: enforce Unicode versioning stability (never introduce characters missing from earlier Unicode versions)

For example, NFKC corresponds to the options `compose=true, compat=true, stable=true`.

# Examples
```jldoctest
julia> "é" == Unicode.normalize("é") #LHS: Unicode U+00e9, RHS: U+0065 & U+0301
true

julia> "μ" == Unicode.normalize("µ", compat=true) #LHS: Unicode U+03bc, RHS: Unicode U+00b5
true

julia> Unicode.normalize("JuLiA", casefold=true)
"julia"

julia> Unicode.normalize("JúLiA", stripmark=true)
"JuLiA"
```
"""
function normalize end
normalize(s::AbstractString, nf::Symbol) = Base.Unicode.normalize(s, nf)
normalize(s::AbstractString; kwargs...) = Base.Unicode.normalize(s; kwargs...)

"""
    Unicode.isassigned(c) -> Bool

Returns `true` if the given char or integer is an assigned Unicode code point.

# Examples
```jldoctest
julia> Unicode.isassigned(101)
true

julia> Unicode.isassigned('\\x01')
true
```
"""
isassigned(c) = Base.Unicode.isassigned(c)

"""
    graphemes(s::AbstractString) -> GraphemeIterator

Returns an iterator over substrings of `s` that correspond to the extended graphemes in the
string, as defined by Unicode UAX #29. (Roughly, these are what users would perceive as
single characters, even though they may contain more than one codepoint; for example a
letter combined with an accent mark is a single grapheme.)
"""
graphemes(s::AbstractString) = Base.Unicode.GraphemeIterator{typeof(s)}(s)

using Base.Unicode: utf8proc_error, UTF8PROC_DECOMPOSE, UTF8PROC_CASEFOLD, UTF8PROC_STRIPMARK

function _decompose_char!(codepoint::Union{Integer,Char}, dest::Vector{UInt32}, options::Integer)
    ret = @ccall utf8proc_decompose_char(codepoint::UInt32, dest::Ptr{UInt32}, length(dest)::Int, options::Cint, C_NULL::Ptr{Cint})::Int
    ret < 0 && utf8proc_error(ret)
    return ret
end

"""
    isequal_normalized(s1::AbstractString, s2::AbstractString; casefold=false, stripmark=false)

Return whether `s1` and `s2` are canonically equivalent Unicode strings.   If `casefold=true`,
ignores case (performs Unicode case-folding); if `stripmark=true`, strips diacritical marks
and other combining characters.

# Examples

For example, the string `"noël"` can be constructed in two canonically equivalent ways
in Unicode, depending on whether `"ë"` is formed from a single codepoint U+00EB or
from the ASCII character `'o'` followed by the U+0308 combining-diaeresis character.

```jldoctest
julia> s1 = "no\u00EBl"
"noël"

julia> s2 = "noe\u0308l"
"noël"

julia> s1 == s2
false

julia> isequal_normalized(s1, s2)
true

julia> isequal_normalized(s1, "noel", stripmark=true)
true

julia> isequal_normalized(s1, "NOËL", casefold=true)
true
```
"""
function isequal_normalized(s1::AbstractString, s2::AbstractString; casefold::Bool=false, stripmark::Bool=false)
    function decompose_next_char!(c, state, d, options, s)
        n = _decompose_char!(c, d, options)
        if n > length(d) # may be possible in future Unicode versions?
            n = _decompose_char!(c, resize!(d, n), options)
        end
        return 1, n, iterate(s, state)
    end
    options = UTF8PROC_DECOMPOSE
    casefold && (options |= UTF8PROC_CASEFOLD)
    stripmark && (options |= UTF8PROC_STRIPMARK)
    i1,i2 = iterate(s1),iterate(s2)
    d1,d2 = Vector{UInt32}(undef, 4), Vector{UInt32}(undef, 4) # codepoint buffers
    n1 = n2 = 0 # lengths of codepoint buffers
    j1 = j2 = 1 # indices in d1, d2
    while true
        if j1 > n1
            i1 === nothing && return i2 === nothing && j2 > n2
            j1, n1, i1 = decompose_next_char!(UInt32(i1[1]), i1[2], d1, options, s1)
        end
        if j2 > n2
            i2 === nothing && return false
            j2, n2, i2 = decompose_next_char!(UInt32(i2[1]), i2[2], d2, options, s2)
        end
        d1[j1] == d2[j2] || return false
        j1 += 1; j2 += 1
    end
end

end

max_stars_repo_name: bastiencaillot/julia

max_issues_repo_name: bastiencaillot/julia

max_forks_repo_name: bastiencaillot/julia


text:
println("... basic tests: only events  ...")
reset!(𝐶)
@test τ() == 0

ex1 = :(1+1)
ex2 = :(1+2)
f(a) = a+3
g(a) = a+4

conv = Simulate.sconvert
@test isa(conv(ex1), Array{SimExpr,1})
@test isa(conv([ex1, ex2]), Array{SimExpr,1})
@test isa(conv(SF(f,1)), Array{SimExpr,1})
@test isa(conv([SF(f,1),SF(g,1)]), Array{SimExpr,1})
@test isa(conv([ex1,SF(f,1),SF(g,1),ex2]), Array{SimExpr,1})
@test isa(conv((ex1,SF(f,1),SF(g,1),ex2)), Array{SimExpr,1})

# one expression
ev = Simulate.SimEvent(conv(:(1+1)), Main, 10, 0)
@test Simulate.simExec(ev.ex) == (2,)
@test ev.t == 10

# two expressions
ev = Simulate.SimEvent(conv([:(1+1), :(1+2)]), Main, 15, 0)
@test Simulate.simExec(ev.ex) == (2, 3)
@test ev.t == 15

# one SimFunction
ev = Simulate.SimEvent(conv(SF(f, 1)), Main, 10, 0)
@test Simulate.simExec(ev.ex) == (4,)

# two SimFunctions
ev = Simulate.SimEvent(conv([SF(f, 1), SF(g, 1)]), Main, 10, 0)
@test Simulate.simExec(ev.ex) == (4, 5)

# expressions and SimFunctions mixed in an array
ev = Simulate.SimEvent(conv([:(1+1), SF(g,2), :(1+2), SF(f, 1)]), Main, 10, 0)
@test sum([ex.func(ex.arg...; ex.kw...) for ex in ev.ex if isa(ex, SimFunction)]) == 10
@test sum([eval(ex) for ex in ev.ex if isa(ex, Expr)]) == 5
@test Simulate.simExec(ev.ex) == (2, 6, 3, 4)

# expressions and SimFunctions mixed in a tuple
ev = Simulate.SimEvent(conv((:(1+1), SF(g,2), :(1+2), SF(f, 1))), Main, 10, 0)
@test sum([ex.func(ex.arg...; ex.kw...) for ex in ev.ex if isa(ex, SimFunction)]) == 10
@test sum([eval(ex) for ex in ev.ex if isa(ex, Expr)]) == 5
@test Simulate.simExec(ev.ex) == (2, 6, 3, 4)

sim = Clock()  # set up clock without sampling
@test_warn "undefined transition" Simulate.step!(sim, sim.state, Simulate.Resume())
init!(sim)
@test sim.state == Simulate.Idle()
@test τ(sim) == 0
sim = Clock(t0=100)
@test τ(sim) == 100
@test_warn "nothing to evaluate" incr!(sim)

a = 0

for i ∈ 1:4
    t = i + τ(sim)
    @test event!(sim, :(a += 1), t) == t
end

for i ∈ 5:7
    t = i + tau(sim)
    @test event!(sim, :(a += 1), at, t) == t
end

for i ∈ 8:10
    @test event!(sim, :(a += 1), after, i) == 100+i
end

@test event!(sim, :(a += 1), every, 1) == 100

# conditional events
@test sim.Δt == 0
@test event!(sim, :(a +=1), (:(τ(sim)>110), :(a>20))) == 100
@test length(sim.cevents) == 1
@test Simulate.simExec(sim.cevents[1].cond) == (false, false)
@test sim.Δt == 0.01

@test length(sim.events) == 11
@test Simulate.nextevent(sim).t == 100

incr!(sim)
@test τ(sim) == 100
@test a == 1
@test Simulate.nextevent(sim).t == 101

run!(sim, 5)
@test τ(sim) == 105
@test a == 11
@test length(sim.events) == 6
stop = event!(sim, :(stop!(sim)), 108)
run!(sim, 6)
@test a == 16
@test sim.state == Simulate.Halted()
@test length(sim.cevents) == 1
@test τ(sim) == stop
resume!(sim)
@test τ(sim) == 111
@test length(sim.cevents) == 0
@test a == 23
@test length(sim.events) == 1

t = 121.0
for i ∈ 1:10
    _t = event!(sim, :(a += 1), 10 + tau(sim))
    @test t == _t
    global t = nextfloat(_t)
end
run!(sim,14)
@test τ(sim) == 125
@test a == 47
@test length(sim.events) == 1
reset!(sim)
@test tau(sim) == 0

println("... basic tests: sampling ...")
sim = Clock(1)  # clock with sample rate 1
@test sim.time == 0
@test sim.tsa == 1
@test sim.Δt == 1

b = 0
sample!(sim, :(b += 1))
@test length(sim.sexpr) == 1
incr!(sim)
@test sim.time == 1
@test b == 1
run!(sim, 9)
@test sim.time == 10
@test b == 10
sample_time!(sim, 0.5)
run!(sim, 10)
@test sim.time == 20
@test b == 30
reset!(sim, hard=false)
@test sim.time == 0

println("... basic tests: events and sampling ...")
function foo()
    global a += 1
    event!(sim, :(foo()), after, rand())
end
function bar()
    global b += 1
end
a = 0
b = 0
sim = Clock(0.5)
event!(sim, :(foo()), at, 0.5)
event!(sim, :(foo()), at, 1)
sample!(sim, :(bar()))
run!(sim, 10000)
@test a == sim.evcount
@test b == 20000

sync!(sim, 𝐶)
@test sim.time == 𝐶.time

println("... basic tests with SimFunction, now with 𝐶 ...")
D = Dict(:a=>0, :b=>0, :c=>0)
function f!(D, i)
    D[:a] += 1
    D[:b] = D[:a]^2
    D[:c] = D[:a]^3
end
event!(𝐶, SimFunction(f!, D, 1), every, 1)
run!(𝐶, 20)
@test τ() == 20
@test D[:a] == 21
@test D[:b] == 21^2
@test D[:c] == 21^3
reset!(𝐶)
@test tau() == 0

println("... unit tests ...")
c = Clock(unit = hr)
@test c.unit == hr
c = Clock(1s, t0=1hr, unit=minute)
@test c.time == 60
@test c.unit == minute
@test c.Δt == 1/60
c = Clock(1s)
@test c.unit == s
@test c.Δt == 1
c = Clock(t0=60s)
@test c.unit == s
@test c.time == 60
c = Clock(1s, t0=1hr)
@test c.unit == s
@test c.time == 3600
@test c.Δt ==1
init!(c)
println(c)
@test repr(c) == "Clock: state=Simulate.Idle(), time=3600.0, unit=s, events: 0, cevents: 0, processes: 0, sampling: 0, sample rate Δt=1.0"

reset!(𝐶)
@test 𝐶.unit == NoUnits
setUnit!(𝐶, s)
@test 𝐶.unit == s
@test setUnit!(𝐶, s) == 0s
c = Clock(1s, t0=1hr)
setUnit!(c, hr)
@test c.unit == hr
@test c.time == 1
@test c.Δt == 1/3600
setUnit!(c, Unitful.m)
@test c.unit == NoUnits

setUnit!(c, s)
run!(𝐶, 1)
sync!(c)
@test c.time == 1
reset!(𝐶)
sync!(c)
c = Clock(t0=1minute)
reset!(𝐶, t0=100s)
sync!(c)
@test c.time == 100
@test c.unit == s

reset!(𝐶, unit=s)
@test 𝐶.unit == s
@test isa(1𝐶.unit, Time)
reset!(𝐶, 1s, t0=1minute)
@test 𝐶.unit == s
@test 𝐶.time == 60
reset!(𝐶, t0=1minute)
@test 𝐶.unit == minute
@test 𝐶.time == 1

myfunc(a, b) = a+b
reset!(𝐶)
@test_warn "clock has no time unit" event!(𝐶, SimFunction(myfunc, 1, 2), 1s)

reset!(𝐶, unit=s)
@test event!(𝐶, SimFunction(myfunc, 4, 5), 1minute, cycle=1minute) == 60
@test event!(𝐶, SimFunction(myfunc, 5, 6), after, 1hr) == 3600
@test sample_time!(𝐶, 30s) == 30
sample!(𝐶, SimFunction(myfunc, 1, 2))
run!(𝐶, 1hr)
@test 𝐶.evcount == 61


text:
include(joinpath(@__DIR__, "../../src/FLS.jl"))

# Example: categories
catschema = @acset LabeledGraph_{Symbol} begin
  V = 7
  vlabel=[:O, :A, :cmp, :asc_l, :asc_r,
          :leftid, :rightid]
  E = 17
  elabel=[:src,:tgt, :refl,
      :c1, :c2, :c3,
      :l1, :l2, :r1, :r2, :asc,
      :lidv, :lida, :lidc,
      :ridv, :rida, :ridc,
      ]

  src = [2,2,1, 3,3,3, 4,4,5,5,4, 6,6,6, 7,7,7]
  tgt = [1,1,2, 2,2,2, 3,3,3,3,5, 1,2,3, 1,2,3]
end

"""Pair of arrows that match head to tail"""
cmpconed = @acset LabeledGraph_{Symbol} begin
  V = 3
  vlabel = [:A,:A,:O]
  E = 2
  elabel = [:src, :tgt]
  src = [1,2]
  tgt = [3,3]
end
cmpcone = Cone(cmpconed, :cmp, [1=>:c1, 2=>:c2])

"""(m₁⋅m₂)⋅m₃"""
asc_l_coned =  @acset LabeledGraph_{Symbol} begin
  V = 3
  E = 2
  vlabel = [:cmp, :cmp, :A]
  elabel = [:c3, :c1]
  src = [1,2]
  tgt = [3,3]
end

asc_l_cone = Cone(asc_l_coned,   :asc_l,  [1=>:l1, 2=>:l2])

"""m₁⋅(m₂⋅m₃)"""
asc_r_coned =  @acset LabeledGraph_{Symbol} begin
  V = 3
  E = 2
  vlabel = [:cmp, :cmp, :A]
  elabel = [:c2, :c3]
  src = [1,2]
  tgt = [3,3]
end
asc_r_cone = Cone(asc_r_coned,   :asc_r,   [1=>:r1, 2=>:r2])

"""(id(x)) ⋅ _ """
leftid_coned = @acset LabeledGraph_{Symbol} begin
  V = 3
  E = 2
  vlabel = [:O, :A, :cmp]
  elabel = [:c1, :refl]
  src = [3,1]
  tgt = [2,2]
end
leftid_cone = Cone(leftid_coned,  :leftid,  [1=>:lidv, 2=>:lida, 3=>:lidc])

rightid_coned = @acset LabeledGraph_{Symbol} begin
  V = 3
  E = 2
  vlabel = [:O, :A, :cmp]
  elabel = [:c2, :refl]
  src = [3,1]
  tgt = [2,2]
end
rightid_cone = Cone(rightid_coned, :rightid, [1=>:ridv, 2=>:rida, 3=>:ridc])

catcones = [cmpcone, asc_l_cone, asc_r_cone, leftid_cone, rightid_cone]

cateqs = [
  # reflexivity
  (:refl_src, [:refl, :src], Symbol[]),
  (:refl_tgt, [:refl, :tgt], Symbol[]),
  # Unitality
  (:unit_c2, [:lidc, :c2], [:lidc, :c3]),
  (:unit_c1, [:ridc, :c1], [:ridc, :c3]),
  # associativity
  (:l1c1, [:l1,:c1], [:asc, :r1, :c1]),
  (:l1c2, [:l1,:c2], [:asc, :r2, :c1]),
  (:l2c2, [:l2,:c2], [:asc, :r2, :c2]),
  (:l2c3, [:l2,:c3], [:asc, :r1, :c3])
]

catfls = FLS(:catt, catschema, catcones, cateqs);

cata2o1 = catfls.cset()
add_parts!(cata2o1, :O, 1)
add_parts!(cata2o1, :A, 2, src=[1,1], tgt=[1,1])
set_subpart!(cata2o1, 1, :refl, 1)
add_parts!(cata2o1, :cmp, 4, c1=[1,1,2,2], c2=[1,2,1,2], c3=[1,2,2,1])
add_parts!(cata2o1, :asc_r, 8, r1= [1,2,2,1,3,4,4,3],
                              # r1: 1st arg is l1.c1, last arg l1.c2⋅l2.c2
                            # l1c2:  1 1 2 2 1 1 2 2
                            # l2c2:  1 2 1 2 1 2 1 2
                            # l1c1:  1 1 1 1 2 2 2 2   <--
                     # l1.c2⋅l2.c2:  1 2 2 1 1 2 2 1   <--
                     r2= [1,2,3,4,1,2,3,4])
                     # r2: first arg is l1.c2, second arg is l2.c2

add_parts!(cata2o1, :asc_l, 8, l1= [1,1,2,2,3,3,4,4],
                              #l1c3: 1 1 2 2 3 3 4 4
                              #l1c3: 1 1 2 2 2 2 1 1 (must be 1st arg of l2)
                                l2= [1,2,3,4,3,4,1,2],
                                asc=[1,2,3,4,5,6,7,8]) # needs to be fixed.
add_parts!(cata2o1, :leftid, 2, lidv= [1,1], lida=[1,1], lidc=[1,2])
add_parts!(cata2o1, :rightid, 2, ridv= [1,1], rida=[1,1], ridc=[1,3]);
a2o1 = initrel(catfls, cata2o1);