J
Jim Morris
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 ----
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 ----