#! /usr/bin/perl -w # # $Id: svn-merge-repos.pl 446 2006-09-23 11:39:41Z coelho $ # use strict; use Pod::Usage; =head1 NAME svn-merge-repos - Merge SVN repositories into one, in date order =head1 SYNOPSIS svn-merge-repos --target repos_path[/subdir] repos_path[/ssubdir]:tsubdir ... =head1 OPTIONS =over 4 =item B<--help> This help. =item B<--man> More help. =item B<--verbose> Be verbose. =item B<--svn=/path/to/svn> Use this 'svn' command. =item B<--svnadmin=/path/to/svnadmin> Use this 'svnadmin' command. =item B<--svndumpfilter=/path/to/svndumpfilter> Use this 'svndumpfilter' command. =item B<--target=repos_path/subdir> Path to destination repository, possibly within the specified sub directory. =item B<--source> Add a 'merge:source' revision property to tell the revision source. Revision property changes must be allowed on the target repos in order to do so. =back =head1 ARGUMENTS Arguments are of the form repos_path/ssubdir:tsubdir. The source sub-directory 'ssubdir' of the source repository is merged into the specified target sub directory 'subdir/tsubdir' in the target repository. =head1 DESIGN NOTE Revisions are exported and imported thanks to the 'svnadmin' command. This means that an actual filesystem path to the repository is necessary. An url-based subversion repository path will not work. It is a bad idea to use the repository while the merge is in progress. =head1 ADVICES =over 4 =item * Do not merge big and pretty independent repositories. Consider 'svn:external' prior to merging repositories. =item * To unmerge a repository, 'svnadmin' + 'svndumpfilter include' are your friends. =back =head1 BUGS I'm unsure about what happens with svn copies. Maybe just merge your trunk? Dates are in some timezone. What happens around daylight time changes? The sorting breaks around one million revisions. =head1 AUTHOR (c) Fabien COELHO 2006 =head1 COPYRIGHT This is free software: do whatever you want with it. However use it at your own risk, and do not blame me. Do not trust hardware. Do not trust software either. Always backup your data. =cut # svn revision number my $rev = '$Rev: 446 $'; $rev =~ tr/0-9//cd; ####################################################################### OPTIONS my $svn = 'svn'; my $svnadmin = 'svnadmin'; my $svndumpfilter = 'svndumpfilter'; my $target = undef; my $verb = 0; my $source = 0; use Getopt::Long; GetOptions("target|t=s" => \$target, "svndumpfilter|dumpfilter|d=s" => \$svndumpfilter, "svnadmin|admin|a=s" => \$svnadmin, "svn|s=s" => \$svn, "verbose|v+" => \$verb, "source" => \$source, "help|h" => sub { pod2usage(-verbose => 1); }, "man|m" => sub { pod2usage(-verbose => 2); }, "version" => sub { print "$0 version $rev\n"; }) or die "invalid option ($!)"; die 'expecting target option' unless $target; ##################################################################### FUNCTIONS # safe execute system sub sys($) { my ($cmd) = @_; print STDERR "sys($cmd)\n" if $verb; system($cmd) and die "cmd=$cmd\n$!"; } # simple quote string for shell sub quote($) { my ($str) = @_; $str =~ s/([\'\\])/\\$1/; return "'$str'"; } # is argument an svn repository? sub is_repository($) { my ($path) = @_; return -d $path and -f "$path/README.txt" and -f "$path/format" and -d "$path/db" and -d "$path/hooks" and -d "$path/locks" and -d "$path/conf" and -d "$path/dav"; } # return separated repository path and sub directory from path sub repository_path($) { my ($path) = @_; my ($dir, $subdir) = ($path, ''); while ($dir and not is_repository($dir) and $dir =~ m,(.*)/([^/]*)$,) { ($dir, $subdir) = ($1, $subdir? "$2/$subdir": $2); } die "path '$path' not a repository" unless is_repository($dir); return ($dir, $subdir); } ################################################################## DO SOMETHING my ($target_repos, $target_subdir) = repository_path($target); die "no such repository: $target_repos" unless -d $target_repos; # full source path -> subdir map in target repository my %sources = (); # subdir map in target repository -> full source path my %dirs = (); # full source path -> source repository path my %srcrepos = (); # full source path -> include source directory my %srcinclude = (); # process sources specifications repos-path:sub/dir for my $src (@ARGV) { die "invalid source specification: $src" unless $src =~ /(.*):(.*)/; my ($path,$dir) = ($1,$2); $dir = "$target_subdir/$dir" if $target_subdir; my ($path_repos, $path_include) = repository_path($path); die "repository $path is already taken" if exists $sources{$path}; # prefixes should also be forbidden die "directory $dir is already taken" if exists $dirs{$dir}; die "must used relative path for $2" if $dir =~ /^\//; $sources{$path} = $dir; $dirs{$dir} = $path; $srcrepos{$path} = $path_repos; $srcinclude{$path} = $path_include? "/$path_include": ''; } # get all log history my @all = (); my %dates = (); for my $path (sort keys %sources) { my $dir = $sources{$path}; open LOG, "$svn log --quiet " . quote("file://$path") . " |" or die $!; while () { chomp; next if /^-+$/; # hmmm... the date is in some timezone. my ($rev, $date) = (split ' \| ')[0,2]; $rev =~ s/^r//; $date =~ tr/0-9//cd; $date =~ s/^(\d{14}).*/$1/; push @all, "$dir:$rev"; # it must have less than 1,000,000 commits $dates{"$dir:$rev"} = sprintf("$date%06d", $rev); } close LOG or die $!; } # create directories within target repository for my $dirpath (sort keys %dirs) { my $path = ''; for my $dir (split '/', $dirpath) { $path .= "/$dir"; sys("$svn ls " . quote("file://$target_repos$path") . " || " . "$svn mkdir --message " . quote("mkdir $path") . " " . quote("file://$target_repos$path")); } } # get them sorted by date # the sorting will break around year 10000. for (sort { $dates{$a} cmp $dates{$b} } @all) { my ($dir,$rev) = split /:/; print STDERR "considering dir=$dir rev=$rev\n" if $verb; my $path = $dirs{$dir}; my $repospath = $srcrepos{$path}; sys(# dump wanted revision from source repository "$svnadmin dump --revision $rev --incremental " . quote($repospath) . # pass through optionnal filter ($srcinclude{$path}? " | $svndumpfilter include " . quote($srcinclude{$path}): '') . # load into target repository " | $svnadmin load --parent-dir " . quote($dir) . " " . quote($target_repos)); # store source as a rev prop sys("$svn pset merge:source " . quote("file://$repospath$srcinclude{$path}\@$rev") . " --revprop --revision HEAD " . quote("file://$target_repos")) if $source; }