[svn.haxx.se] · SVN Dev · SVN Users · SVN Org · TSVN Dev · TSVN Users · Subclipse Dev · Subclipse Users · this month's index

Shadow/Mirror hook script

From: Nathaniel Case <ncase_at_xes-inc.com>
Date: 2005-06-01 21:45:46 CEST

I noticed that a few people have asked about a feature that would
automatically mirror the latest version of a repository to a 'shadow' copy.

I also needed this, so I wrote a post-commit hook script to do it
(attached). It works by using svnlook to determine what has changed,
and then implements those changes (modify, add, delete) to the target
shadow directory.

The first time the script is ran, it will do a full export to the target
directory. Subsequent runs should do the incremental changes. It's
smart enough to track the last version it sync'd with, so it will sync
with all versions it hasn't sync'd with yet in the history (though this
shouldn't be necessary assuming it runs successfully on each commit).

This should be nicer than other alternatives for many use cases, since
you don't have to do a full export each time, and you also don't
unnecessarily have the .svn directories in the shadow directory.

It hasn't been heavily tested however, so I would appreciate any
feedback. See the comments at the top of the script for example usage.

- Nate Case <ncase@xes-inc.com>

# Mirror a copy of the latest version of a Subversion repository to a
# specified directory. This is intended to be called by a post-commit
# hook.
# Example usage in post-commit hook:
# /path/to/svn-shadow.pl $1 svn:// \
# /mirrors/Foo >> /tmp/Foo-shadow.log
# Author: Nate Case <ncase@xes-inc.com>

use strict;
use strict 'refs';
use Data::Dumper;

if ($#ARGV < 2) {
    printf (STDERR "Usage: $0 <local repo> <URL to mirror> <destination>\n");

my $LOCAL_REPO = $ARGV[0];
my $URL = $ARGV[1];
my $DEST_DIR = $ARGV[2];
my $cfg;

$URL =~ s/\/$//; # Strip any trailing slash
$DEST_DIR =~ s/\/$//; # Strip any trailing slash
$LOCAL_REPO =~ s/\/$//; # Strip any trailing slash
my $CFGNAME = $LOCAL_REPO."/hooks/svn-shadows.dat";

my $ROOT;
if ($URL =~ /:\/\/.[^\/]+\/[^\/]+\/(.*)$/) {
    $ROOT = $1;
} else {
    printf (STDERR "Error parsing URL '$URL'\n");

sub svn_copy_file {
    my ($rev, $filepath, $dest) = @_;
    system("/usr/bin/svnlook -r $rev cat \"$LOCAL_REPO\" \"$filepath\" > \"$dest\"");

sub get_youngest {
    my $youngest = `/usr/bin/svnlook youngest $LOCAL_REPO`;
    chop $youngest;
    return $youngest;

sub cfg_write {
    open(fout, ">$CFGNAME");
    print(fout Data::Dumper::Dumper($cfg));

# Apply changes that occured in a specified revision
sub sync_rev {
    my ($revnum) = @_;

    # Find out what changed
    my $changed = `/usr/bin/svnlook -r $revnum changed \"$LOCAL_REPO\"`;
    my @lines = split/\n+/, $changed;

    foreach my $line (@lines) {
        if ($line =~ /^([A-Z])\s+(.*)$/) { # Change Type = $1, File = $2
            my $ctype = $1;
            my $path = $2;
            my $isdir = substr($path, -1, 1) eq '/';
            my $dest = substr($path, length($ROOT));
            if (($ctype eq 'U' || $ctype eq 'A') && !$isdir) {
                printf("Copying $path\n");
                svn_copy_file($revnum, $path, $DEST_DIR.$dest);
# chmod 0440, $DEST_DIR.$dest;
            elsif (($ctype eq 'A') && $isdir) {
                printf("Creating directory $DEST_DIR$dest\n");
            elsif (($ctype eq 'D') && (!$isdir)) {
                printf("Deleting file $DEST_DIR$dest\n");
            elsif (($ctype eq 'D') && ($isdir)) {
                printf("Deleting directory $DEST_DIR$dest\n");
            } else {
                printf("Warning: Unknown modification '$ctype' to '$path'\n");
    $cfg->{$DEST_DIR} = $revnum;

# Read config with the latest sync'd revision numbers
if (-f $CFGNAME) {
    ($cfg) = do "$CFGNAME";
} else {
    my %hash = ();
    $cfg = \%hash;

# Check if script has never sync'd this directory before
# In this case, export a full copy to start
if (!($cfg->{$DEST_DIR} > 0)) {
    printf("No shadow history found, exporting a full copy ..\n");
    system("svn --force export $URL $DEST_DIR");
    $cfg->{$DEST_DIR} = get_youngest();

my $youngest = get_youngest();
my $rev = $cfg->{$DEST_DIR};
while ($rev < $youngest) {
    printf("Applying changes for rev ".($rev+1)."\n");
    sync_rev($rev + 1);

# Remember what version we last sync'd with

To unsubscribe, e-mail: users-unsubscribe@subversion.tigris.org
For additional commands, e-mail: users-help@subversion.tigris.org
Received on Wed Jun 1 21:56:10 2005

This is an archived mail posted to the Subversion Users mailing list.

This site is subject to the Apache Privacy Policy and the Apache Public Forum Archive Policy.