Accessing SVN through Ruby/DL

I was following the old thread regarding accessing SVN through Ruby,
using SWIG or DL. I cobbled together the following to see how hard it
would be to do this in DL. I implemented the svn_client_status2
function as that is one API not implemented in the current SWIG/Ruby
binding to SVN (and it was the one I needed).

I did both a command line parsing version and the Ruby/DL calling
/usr/local/lib/libsvn_client-1.so.

The latter was far easier to deal with even though the DL stuff is not
really well documented and examples are far and few between, I hope
this can be added to the samples, as it involves callbacks, structures
and pointer types. (I can send the command line parsing version if
anyone is interested).

I suspect this Ruby/DL version will work on win32 given the correct
path to the equivalent .DLL

Anyway there is a huge amount of effort required to get the Ruby/DL to
the same point that the Swig/Ruby bindings are currently at, but this
example may get someone started if they are so inclined.

The one thing I have tried to do with this approach is to make it more
ruby'ish, by hiding the SVN pool and context stuff in the class
SvnClient, and using a Proc Block for the callback.

I'm interested in any feedback on this approach as I suspect I will be
playing with Ruby/DL a lot more. Also any improvements and/or
suggestions on how to handle the large number of enums the c version
of SVN uses.

--- snip Svnrb.rb ----
require 'dl/import'
require 'dl/struct'

module Svnrb
    extend DL::Importable
    # NOTE you may have to change this path depending on where your svn is installed
    # On win32 it will need to point to the relevant .DLL
    dlload "/usr/local/lib/libsvn_client-1.so" #,"/usr/local/lib/libsvn_subr-1.so"

    typealias("apr_pool_t*", "void*")
    typealias("apr_status_t", "int")

    # define some convenient structures used by SVN
    Svn_opt_revision_t= struct [
        "int kind",
        "int revision"
    ]

    Svn_error_t= struct [
        "int apr_err",
        "char *message",
        "void *child",
        "apr_pool_t *pool",
        "char *file",
        "long line"
    ]

    # used where we pass a pointer to a long which gets modified in the call
    # and where we need to actually read the modified value within Ruby
    LongArg= struct [
        "long val"
    ]

    Svn_status_t = struct [
        "void *entry",
        "int text_status",
        "int prop_status",
        "int locked",
        "int copied",
        "int switched",
        "int repos_text_status",
        "int repos_prop_status",
        "void *repos_lock"
    ]

    # an experimental way to match standard SVN status enum with the value
    # Could also use Constants here
    Svn_wc_status_kind= {
        # does not exist
        "svn_wc_status_none" => 1,
        # is not a versioned thing in this wc
        "svn_wc_status_unversioned" => 2,
        # exists, but uninteresting.
        "svn_wc_status_normal" => 3,
        # is scheduled for addition
        "svn_wc_status_added" => 4,
        # under v.c., but is missing
        "svn_wc_status_missing" => 5,
        # scheduled for deletion
        "svn_wc_status_deleted" => 6,
        # was deleted and then re-added
        "svn_wc_status_replaced" => 7,
        # text or props have been modified
        "svn_wc_status_modified" => 8,
        # local mods received repos mods
        "svn_wc_status_merged" => 9,
        # local mods received conflicting repos mods
        "svn_wc_status_conflicted" => 10,
        # resource marked as ignored
        "svn_wc_status_ignored" => 11,
        # an unversioned resource is in the way of the versioned resource
        "svn_wc_status_obstructed" => 12,
        # an unversioned path populated by an svn:external property
        "svn_wc_status_external" => 13,
        # directory doesn't contain a complete entries list
        "svn_wc_status_incomplete" => 14
    }

    # the functions in various svn libraries we call in this example
    extern "int svn_cmdline_init(char *, void*)"
    extern "apr_pool_t *svn_pool_create_ex(apr_pool_t *, void *)"
    extern "void *svn_config_ensure(char *, apr_pool_t *)"
    extern "void *svn_client_create_context(void **, apr_pool_t *)"
    extern "void *svn_stream_for_stdout(void **, apr_pool_t *)"
    extern "void *svn_client_cat(void *, char *, Svn_opt_revision_t *, void *, apr_pool_t *)"
    extern "void *svn_client_status2(int *, char *, Svn_opt_revision_t *, void *, void *, int, int, int, int, int, void *, apr_pool_t *)"

    # a wrapper around access to the SVN Client library, to make it more "ruby-like" to the user
    class SvnClient
        @ctx= nil # active context
        @pool= nil # active pool

        # this initializes the svn library and gets the contect and pool for use in other calls
        def initialize(name)
            err= Svnrb::svn_cmdline_init(name, nil)
            raise "svn_cmdline_init failed" if err != 0
            @pool= Svnrb::svn_pool_create_ex(nil, nil)
            raise "svn_pool_create_ex" if @pool == nil
            err= Svnrb::svn_config_ensure("", @pool)
            raise "svn_config_ensure failed" if err != nil;
            # effectively void *, passed in as void **, will get the pointer to the context
            # Ruby does not need to every read this value, it just passes it through to subsequent calls
            # ditto for the pool
            tctx= DL.malloc(DL.sizeof('P'))
            err = Svnrb::svn_client_create_context(tctx, @pool);
            raise "svn_client_create_context failed" if err != nil
            @ctx= tctx.ptr # this becomes the contect to use for other calls
        end

        # gets the stdout stream for use in cat
        def getStdoutStream
            sto= DL.malloc(DL.sizeof('P')) # long *
            err= Svnrb::svn_stream_for_stdout(sto, @pool)
            sto.ptr
        end

        # implements the client cat call
        # rev is -1 to get HEAD, and a rev number for any other revision
        def cat(stream, path, rev)
            revision= Svn_opt_revision_t.malloc
            if rev >= 0
                revision.kind= 1 # revision #
                revision.revision= rev
            else
                revision.kind= 7 # HEAD
            end

            err= Svnrb::svn_client_cat(stream, path, revision, @ctx, @pool)
            if err != nil
                terr= Svn_error_t.new(err)
                raise "svn_client_cat failed: (#{terr.apr_err}) #{terr.message}"
            end
        end

        # call sthe Client Status2 function
        # url is the WC path
        # rev is -1 to get HEAD, and a rev number for any other revision
        # proc is the proc method callback for each resource which will get two parameters: path and status
        # status is a structure of type Svn_status_t
        # returns the current Youngest revision in the Repository
        def status(url, rev, &proc)
            resrev= LongArg.malloc
            revision= Svn_opt_revision_t.malloc
            if rev >= 0
                revision.kind= 1 # revision #
                revision.revision= rev
            else
                revision.kind= 7 # HEAD
            end

            # process callback and call the supplied Proc
            mycb= DL.callback('0PSP'){ |baton,path,pstatus|
                if pstatus
                    status= Svn_status_t.new(pstatus)
                    proc.call(path, status)
                end
            }
            err= Svnrb::svn_client_status2(resrev, url, revision, mycb, nil, 1, 1, 1, 0, 1, @ctx, @pool)
            resrev.val
        end
    end

end

if $0 == __FILE__
        # test it, NOTE these test are specific to paths in my WC, yours may vary

        svn= Svnrb::SvnClient.new("testsvn")

        #outStream= svn.getStdoutStream
        #svn.cat(outStream, "/home/morris/work/perl/vcvs.pl", -1)
        #svn.cat(outStream, "/home/morris/work/perl/.dddd", 7)

        # get the status of the specified WC path, gets the HEAD revision, and only prints out the status of versioned resources
        rev= svn.status("/home/morris/work/perl", -1) {
            >path, status|
            print path, "-> ", status.text_status, " - ", status.prop_status, "\n" if status.text_status != Svnrb::Svn_wc_status_kind["svn_wc_status_unversioned"]
        }

        print "rev= ", rev, "\n"
end
--- end snip ----

Jim Morris wrote:

The latter was far easier to deal with even though the DL stuff is not
really well documented and examples are far and few between, I hope
this can be added to the samples, as it involves callbacks, structures
and pointer types. (I can send the command line parsing version if
anyone is interested).

Thank you for your effort,
I will put the sample program on the following site.

   http://rubyforge.org/projects/dlcookbook

Thanks,

···

--
Takaaki Tateishi <ttate@ttsky.net>