I'm in the process of writing a web application that's
using subversion, and as part of that project have been
calling some of the svn client routines from within Perl.
Since the documentation on how to do so felt a bit spotty,
and since I had to do some tweaking to get it to work at all,
others trying to do the same might find the attached Perl
module helpful. It only does a few basic tasks, but it
does include some installation notes and comments on
encountered problems.
So, for what they're worth, attached is
SVN_Client_Simple.pm
test.pl
Regards,
Jim Mahoney
(mahoney@marlboro.edu)
---- installation of Perl, SWIG, and Subversion under linux ----
$ tar zxf perl-5.8.7.tar.gz
$ cd perl-5.8.7
$ ./configure.gnu; make && make test && make install
$ cd ..
$ tar zxf swig-1.3.27.tar.gz
$ cd swig-1.3.27
$ ./configure --with-perl5=/usr/bin/perl; make && make install
$ cd ..
$ tar zxf subversion-1.1.4.tar.gz
$ cd subversion-1.1.4
$ ./configure PERL=/usr/bin/perl
$ make && make install
# This puts PATH_MAX and APR_PATH_MAX into subversion's SWIG bindings
# manually; "make swig-pl" crashed otherwise.
# I found my system's values in /user/include/linux/limits.h.
$ perl -pi -e 's{(%include apr.h)}{#define PATH_MAX 4096\n#define
APR_PATH_MAX PATH_MAX\n$1}' subversion/bindings/swig/apr.i
make swig-pl;
# make check-swig-pl (1 of 144 tests failed.)
make install-swig-pl
$ cd ..
---- use of SVN_Client_Simple.pm from within perl code -----
use SVN_Client_Simple qw( svn_log svn_cat svn_commit);
my $filename = '/path/to/working/copy/of/file.txt';
my $revision = 123; # or 'HEAD' or '{2005-03-12}';
# Fetch a file's version history from the repository.
# Alternate form : $history=svn_log($filename,$from,$to);
my $history = svn_log($filename);
print "Revision history for file '$filename':\n";
foreach my $record (@$history){
for my $key qw(revision date author message){
print " $key: '" . $record->{$key} . "'\n";
}
}
# Fetch an old revision of a file's text from the repository.
# If an error occurs (eg that file doesn't exist in that revision)
# then the empty string is returned.
my $text = svn_cat($filename, $revision);
# Put changes to a (possibly new) file into the repository.
# Equivalent to
# $ svn add $filename
# $ svn commit $filename -m $message
# If $message is omitted, it defaults to 'from SVN_Client_Simple'.
my $error_msg = svn_commit($filename, $message);
if ($error_msg){
die("Error during subversion commit : '$error_msg'.\n");
}
------------------------------------------------------------
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More qw(no_plan);
use Data::Dumper;
my $DEBUG = 0;
# test.pl : a few tests of SVN_Client_Simple.
#
# Assumes that $filename exists and is already in a subversion working copy.
#
# If not, you can accomplish that with something like
# $ svn checkout <some_working_copy>
# $ cd <somewhere_in_working_copy>
# $ touch test_file_for_svn.txt # or whatever $filename is.
# $ svn commit test_file_for_svn.txt -m "testing SVN_Client_Simple"
# and then run these tests with something like
# $ mv <wherever_it_is>/SVN_Client_Simple.pm .
# $ mv <wherever_it_is>/test.pl .
# $ ./test.pl
BEGIN{
use_ok('SVN_Client_Simple');
use Wikiacademia::Subversion qw( svn_log svn_cat svn_commit );
}
my $filename = 'test_file_for_svn.txt';
my $history = svn_log($filename);
print "svn_log($filename) = '" . Dumper($history) . "'\n" if $DEBUG;
ok (ref($history) eq 'ARRAY', 'svn_log returns array ref.');
my $n_entries = scalar(@$history);
open FILE, $filename or die "couldn't open '$filename' for reading";
my $old_text = join('', <FILE>);
close FILE;
open FILE, '>', $filename or die "couldn't modifify '$filename'";
my $date = scalar(localtime());
my $random = rand();
print FILE "
-- svn test file --
last modified : $date
random number : $random
";
close FILE;
my $error = svn_commit($filename, "committed by subversion.pl at $date");
ok((not $error), 'svn_commit on modified file appears successful.');
print "svn_commit error = '$error'\n" if $DEBUG;
my $text = svn_cat($filename, 'PREV');
print "svn_cat($filename) = '$text'\n" if $DEBUG;
ok($text eq $old_text, "svn_cat(file,'PREV') finds previous text.");
print "
--- text from file before change --- length=" . length($old_text) . "
$old_text
--- text from revision 'PREV' ------ length=" . length($text) . "
$text
----------------
" if $DEBUG;
$history = svn_log($filename);
ok($n_entries+1==scalar(@$history), 'svn_log finds another rev after commit.');
# my $tmp_dir = 'tmp' . rand();
# my $tmp_file = $tmp_dir . '/foo.txt';
# mkdir $tmp_dir;
# open FILE, '>', $tmp_file or die "couldn't open '$tmp_file'";
# print FILE "
# -- temporary file --
# ";
# close FILE;
# $error = svn_commit($filename, "tmp subversion.pl commit at $date");
# print "tmp svn_commit error = '$error'\n" if $DEBUG;
# unlink $tmp_file;
# rmdir $tmp_dir;
package SVN_Client_Simple;
######################################################
# Interface to some of subversion client using its SWIG API.
# Not very pretty, but perhaps better than the `svn ...` shell approach.
# See the POD at the end of this file for usage, caveats, author, etc.
######################################################
use strict;
use warnings;
use SVN::Client;
my $DEBUG = 0; # Set this to 1 for diagnostic ouput.
our @ISA = qw(Exporter);
our @EXPORT_OK = qw( svn_log
svn_cat
svn_commit
);
# Object interface to SVN::Client; pretty much cut'n'paste from docs.
my $svn = SVN::Client->new( auth => [
SVN::Client::get_simple_provider(),
SVN::Client::get_simple_prompt_provider(\&simple_prompt,1),
SVN::Client::get_username_provider()
]);
# Authentication routine required by the API.
sub simple_prompt {
# my ($credentials, $realm, $default_user, $may_save) = @_;
## Docs suggest getting $username and $password from user interaction, then
# $credentials->username($username);
# $credentials->password($password);
## But I'm using a file:/// subversion repository that is writable
## by this process user/group, so all this certificate stuff authentication
## is irrelevant ... so just ignoring all this apparently works.
return 1;
}
# Usage: $history = svn_log($filename, $from_revision, $to_revision);
# Last two arguments are optional.
# Return is a reference to an array of hash references :
# $history = [{ message=>$msg, revision=>$rev, date=>$date author=>$who},..];
sub svn_log {
my ($filename, $from, $to) = @_;
$from ||= 1; # default starting revision
$to ||= 'BASE'; # default end revision
my @logs;
my $log_reciever = sub {
my ($changed_path, $rev, $author, $date, $msg) = @_;
push @logs, {message=>$msg, revision=>$rev, date=>$date, author=>$author};
};
$svn->log($filename, $from, $to, 0, 0, $log_reciever);
return \@logs;
}
# Usage: $text = svn_cat($filename, $revision)
# If revision isn't given this fetches the most recent in the repository.
# This may be more recent than the local copy file if other submissions to
# the repository are allowed.
# The normal subversion scheme for revision labeling applies,
# i.e. an integer, a keyword, or (in curly braces) a date; see below.
sub svn_cat {
my ($filename, $revision) = @_;
$revision ||= 'HEAD';
my $buffer;
open(BUFFER, '>', \$buffer) or die q{perl open(BUFFER,'>',\$buffer) failed.};
eval {$svn->cat( \*BUFFER, $filename, $revision)};
if ($@){
# If the given file wasn't in the repository at the specified revision,
# the error includes something like
# "Unable to find repository location for 'filname' in revision 1"
$buffer = ''; # An error occured - return the empty string.
if ($DEBUG){
print "in svn_cat: error='$@'\n";
}
}
return $buffer || '';
}
# Usage: $error = svn_commit($filename, $log_message);
# Archive $filename in the svn repository with the given message.
# Returns false if svn complains with an error.
# Returns true if new revision commited with given log message.
# Also returns true if $filename is unchanged since last commit;
# in that case, $log_message is quietly ignored and repository isn't changed.
# If the log message is omitted it defaults to 'from SVN_Client_Simple'.
sub svn_commit {
my ($filename, $log_message) = @_;
$log_message ||= 'from ' . __PACKAGE__;
# the svn API has a fairly awkward way to supply a log message,
# namely be setting a global message handler function.
my $log_msg_handler = sub {
my $stringptr = shift;
$$stringptr = $log_message;
return 1;
};
$svn->log_msg($log_msg_handler);
my $info;
if ($DEBUG){
print "in svn_commit : trying to commit file '$filename'\n";
}
# ---- error handling -------------------------------------------
# The next line crashed regularly on errors.
#
# The docs say that this is the way to not to croak on errors ...
# but it crashes when dealing iwth a file not in repository with
# a "Undefined subroutine &main::0 ... in SVN/Client.pm" message.
# $SVN::Error::handler = SVN::Error::ignore_error;
#
# And this is how the API says errors should be caught ...
# but that gave seg faults.
# $SVN::Error::handler = \&svn_error_handler;
# my $svn_error;
# sub svn_error_handler {
# my ($error) = @_; # an svn_error_t object.
# if (ref($error)){
# $svn_err = $error->apr_err;
# # print " SVN ERROR: apr=$svn_err,msg='".$error->strerror."'\n";
# $error->clear(); # docs say "call this or you'll leak memory"
# }
# }
#
# So instead of handling errors by one of these "correct" methods,
# I'm just using "eval" to catch the "croak" that they're sending,
# and then looking at the english error message.
#
# An unfortunate side effect of this approach is that I'm dependent
# on the english text version of the eror, ie 'not under version control'.
# But it isn't any worse that using `svn ...`.
# ---------------------------------------------------
eval { $info = $svn->commit($filename, 1) }; # 1 => nonrecursive
if ($@ =~ /not under version control/){
if ($DEBUG){
print "in svn_commit : not in repository; adding it and trying again.\n";
}
$svn->add($filename, 0); # 0 => not recursive (!?)
eval { $info = $svn->commit($filename, 1) }; # 1 => nonrecursive
}
if ($DEBUG){
print "in svn_commit : ";
if ($@){
print " ... failed, error='$@'.";
}
elsif (not $info) {
print " ... tried, but it hasn't changed since last commit.";
# Or that file isn't under version control.
}
else {
print " ... success, new revision is '" . $info->revision . "'.";
}
print "\n";
}
return $@;
}
1;
__END__
=head1 NAME
SVN_Client_Simple - a simple interface to a bit of the SVN::Client API.
=head1 SYNOPSIS
use SVN_Client_Simple qw( svn_log svn_cat svn_commit);
my $filename = '/path/to/working/copy/of/file.txt';
my $revision = 123; # or 'HEAD' or '{2005-03-12}';
# Fetch a file's version history from the repository.
# With just a filename, gets all logs from version 1 to 'BASE'.
# Or you can set a range of revisions: $history=svn_log($filename,$from,$to);
my $history = svn_log($filename);
print "Revision history for file '$filename':\n";
foreach my $record (@$history){
for my $key qw(revision date author message){
print " $key: '" . $record->{$key} . "'\n";
}
}
# Fetch a previous version of a file's text from the repository.
# If an error occurs (eg that file doesn't exist in that revision)
# then the empty string is returned.
my $text = svn_cat($filename, $revision);
# Put changes to a (possibly new) file into the repository. Equivalent to
# $ svn add $filename
# $ svn commit $filename -m $message
# If $message is omitted, it defaults to 'from Wikiacademia::Subversion'.
my $error_msg = svn_commit($filename, $message);
if ($error_msg){
die("Error during subversion commit : '$error_msg'.\n");
}
=head1 DESCRIPTION
An interface to some of subversion client commands from perl.
Not very pretty, but probably better than the `svn ...` shell approach.
The Subversion-Perl connection via the SWIG API is still under
development, and I needed to do some tweaking on my system (Fedora
linux) to get it to compile at all. See below for the details.
This module assumes a particularly simple scenario in which the files
that these commands refer to are already in a checked out working copy
which contains .svn/ folders pointing to the repository. Moreover,
the repository is assumed to not need authentication - I've only
tested this for file:/// repository URL's, in which the user/group
permissions are consistent with those for this process.
Testing was done with Subversion 1.1.4, SWIG 1.3.27, and Perl 5.8.7.
The installation of these pieces looked something like
this on a unix system.
$ tar zxf perl-5.8.7.tar.gz
$ cd perl-5.8.7
$ ./configure.gnu; make && make test && make install
$ cd ..
$ tar zxf swig-1.3.27.tar.gz
$ cd swig-1.3.27
$ ./configure --with-perl5=/usr/bin/perl; make && make install
$ cd ..
$ tar zxf subversion-1.1.4.tar.gz
$ cd subversion-1.1.4
$ ./configure PERL=/usr/bin/perl
$ make && make install
# This puts PATH_MAX and APR_PATH_MAX into subversion's SWIG bindings
# manually; "make swig-pl" crashed otherwise.
# I found my system's values in /user/include/linux/limits.h.
$ perl -pi -e 's{(%include apr.h)}{#define PATH_MAX 4096\n#define APR_PATH_MAX PATH_MAX\n$1}' subversion/bindings/swig/apr.i
make swig-pl;
# make check-swig-pl (1 of 144 tests failed.)
make install-swig-pl
$ cd ..
=head1 AUTHOR
Jim Mahoney, E<lt>mahoney@marlboro.edu<gt>
=head1 COPYRIGHT AND LICENSE
Copyright (C) Nov 2005.
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
---------------------------------------------------------------------
To unsubscribe, e-mail: users-unsubscribe@subversion.tigris.org
For additional commands, e-mail: users-help@subversion.tigris.org
Received on Mon Nov 28 04:07:14 2005