--- trunk/svn2cvs.pl 2004/03/09 22:16:19 6 +++ trunk/svn2cvs.pl 2005/03/06 12:09:42 18 @@ -8,31 +8,25 @@ # http://raw.no/personal/blog # # 2004-03-09 Dobrica Pavlinusic +# +# documentation is after __END__ use strict; use File::Temp qw/ tempdir /; use Data::Dumper; use XML::Simple; -# get current user home directory -my $HOME = $ENV{'HOME'} || die "can't get home directory!"; +if (@ARGV < 2) { + print "usage: $0 SVN_URL CVSROOT CVSREPOSITORY\n"; + exit 1; +} + +my ($SVNROOT,$CVSROOT, $CVSREP) = @ARGV; -# cvsroot directory -#my $CVSROOT="$HOME/x/cvsroot"; -my $CVSROOT=':pserver:dpavlin@cvs.tigris.org:/cvs'; -# name of cvs repository to commit to -my $CVSREP="svn2cvs/src"; - -# svnroot directory -my $SVNROOT="file://$HOME/private/svn/svn2cvs"; -# name of respository -my $SVNREP="trunk"; - -# webpac example -#$CVSROOT="$HOME/x/cvsroot"; -#$CVSREP="webpac"; -#$SVNROOT="file://$HOME/private/svn/webpac/"; -#$SVNREP="trunk"; +if ($SVNROOT !~ m,^[\w+]+:///*\w+,) { + print "ERROR: invalid svn root $SVNROOT\n"; + exit 1; +} my $TMPDIR=tempdir( "/tmp/checkoutXXXXX", CLEANUP => 1 ); @@ -41,14 +35,13 @@ # cvs command with root my $cvs="cvs -d $CVSROOT"; - # # sub to do logging and system calls # sub log_system($$) { my ($cmd,$errmsg) = @_; print STDERR "## $cmd\n"; - system $cmd || die "$errmsg: $!"; + system($cmd) == 0 || die "$errmsg: $!"; } # @@ -60,35 +53,77 @@ die "commit_svnrev needs revision" if (! defined($rev)); - open(SVNREV,"> $CVSREP/.svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!"; + open(SVNREV,"> .svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!"; print SVNREV $rev; close(SVNREV); my $path=".svnrev"; if ($add_new) { - system "$cvs add $CVSREP/$path" || die "cvs add of $path failed: $!"; + system "$cvs add $path" || die "cvs add of $path failed: $!"; } else { my $msg="subversion revision $rev commited to CVS"; print "$msg\n"; - system "$cvs commit -m \"$msg\" $CVSREP/$path" || die "cvs commit of $path failed: $!"; + system "$cvs commit -m '$msg' $path" || die "cvs commit of $path failed: $!"; } } +# current revision in CVS +my $rev; # ok, now do the checkout +eval { + log_system("$cvs -q checkout $CVSREP", "cvs checkout failed"); +}; -log_system("$cvs -q checkout $CVSREP","cvs checkout failed"); +if ($@) { + print <<_NEW_REP_; -my $rev; +There is no CVS repository '$CVSREP' in your CVS. I will assume that +this is import of new module in your CVS and start from revision 0. + +Press enter to continue importing new CVS repository or CTRL+C to abort. + +_NEW_REP_ + + print "start import of new module [yes]: "; + my $in = ; + mkdir($CVSREP) || die "can't create $CVSREP: $!"; + + chdir($CVSREP) || die "can't cd to $TMPDIR/$CVSREP: $!"; + + open(SVNREV,"> .svnrev") || die "can't open $CVSREP/.svnrev: $!"; + print SVNREV "0"; + close(SVNREV); + + $rev = 0; + + # create new module + log_system("$cvs import -m 'new CVS module' $CVSREP svn2cvs r0", "can't import new module into $CVSREP"); + + unlink ".svnrev" || die "can't remove .svnrev: $!"; + chdir($TMPDIR) || die "can't cd to $TMPDIR: $!"; + rmdir $CVSREP || die "can't remove $CVSREP: $!"; -# check if svnrev exists -if (! -e "$CVSREP/.svnrev") { - print <<_USAGE_; + # and checkout it + log_system("$cvs -q checkout $CVSREP", "cvs checkout failed"); + + chdir($CVSREP) || die "can't cd to $TMPDIR/$CVSREP: $!"; + +} else { + + # import into existing module directory in CVS + + chdir($CVSREP) || die "can't cd to $TMPDIR/$CVSREP: $!"; + + + # check if svnrev exists + if (! -e ".svnrev") { + print <<_USAGE_; Your CVS repository doesn't have .svnrev file! -This file is used to keep CVS repository and SubVersion in sync, so +This file is used to keep CVS repository and Subversion in sync, so that only newer changes will be commited. It's quote possible that this is first svn2cvs run for this repository. @@ -97,30 +132,31 @@ been checkouted. If you migrated your cvs repository to svn using cvs2svn, this will be -last SubVersion revision. If this is initial run of conversion of -SubVersion repository to CVS, correct revision is 0. +last Subversion revision. If this is initial run of conversion of +Subversion repository to CVS, correct revision is 0. _USAGE_ - print "svn revision corresponding to CVS [abort]: "; - my $in = ; - chomp($in); - if ($in !~ /^\d+$/) { - print "Aborting: revision not a number\n"; - exit 1; + print "svn revision corresponding to CVS [abort]: "; + my $in = ; + chomp($in); + if ($in !~ /^\d+$/) { + print "Aborting: revision not a number\n"; + exit 1; + } else { + $rev = $in; + commit_svnrev($rev,1); # create new + } } else { - $rev = $in; - commit_svnrev($rev,1); # create new + open(SVNREV,".svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!"; + $rev = ; + chomp($rev); + close(SVNREV); } -} else { - open(SVNREV,"$CVSREP/.svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!"; - $rev = ; - chomp($rev); - close(SVNREV); -} -print "Starting after revision $rev\n"; -$rev++; + print "Starting after revision $rev\n"; + $rev++; +} # @@ -130,7 +166,7 @@ # case much about accuracy and completnes of logs there, this might # be good. YMMV # -open(LOG, "svn log -r $rev:HEAD -v --xml $SVNROOT/$SVNREP |") || die "svn log for repository $SVNROOT/$SVNREP failed: $!"; +open(LOG, "svn log -r $rev:HEAD -v --xml $SVNROOT |") || die "svn log for repository $SVNROOT failed: $!"; my $log; while() { $log .= $_; @@ -140,28 +176,64 @@ my $xml = XMLin($log, ForceArray => [ 'logentry', 'path' ]); -=begin log_example - ------------------------------------------------------------------------- -r256 | dpavlin | 2004-03-09 13:18:17 +0100 (Tue, 09 Mar 2004) | 2 lines - -ported r254 from hidra branch - -=cut +#=begin log_example +# +#------------------------------------------------------------------------ +#r256 | dpavlin | 2004-03-09 13:18:17 +0100 (Tue, 09 Mar 2004) | 2 lines +# +#ported r254 from hidra branch +# +#=cut my $fmt = "\n" . "-" x 79 . "\nr%5s| %8s | %s\n\n%s\n"; if (! $xml->{'logentry'}) { - print "no newer log entries in SubVersion repostory. CVS is current\n"; + print "no newer log entries in Subversion repostory. CVS is current\n"; exit 0; } +# check if file exists in CVS/Entries +sub in_entries($) { + my $path = shift; + if ($path !~ m,^(.*?/*)([^/]+)$,) { + die "can't split '$path' to dir and file!"; + } else { + my ($d,$f) = ($1,$2); + if ($d !~ m,/$, && $d ne "") { + $d .= "/"; + } + open(E, $d."CVS/Entries") || die "can't open ${d}CVS/Entries: $!"; + while() { + return(1) if (m,^/$f/,); + } + close(E); + return 0; + } +} + foreach my $e (@{$xml->{'logentry'}}) { die "BUG: revision from .svnrev ($rev) greater than from subversion (".$e->{'revision'}.")" if ($rev > $e->{'revision'}); $rev = $e->{'revision'}; - log_system("svn export --force -q -r $rev $SVNROOT/$SVNREP $CVSREP", "svn export of revision $rev failed"); + log_system("svn export --force -q -r $rev $SVNROOT $TMPDIR/$CVSREP", "svn export of revision $rev failed"); + + # deduce name of svn directory + my $SVNREP = ""; + my $tmpsvn = $SVNROOT || die "BUG: SVNROOT empty!"; + my $tmppath = $e->{'paths'}->{'path'}->[0]->{'content'} || die "BUG: tmppath empty!"; + do { + if ($tmpsvn =~ s,(/\w+)/*$,,) { + $SVNREP .= $1; + } else { + print "NOTICE: can't deduce svn dir from $SVNROOT - skipping\n"; + next; + } + } until ($tmppath =~ m/^$SVNREP/); + + print "NOTICE: using $SVNREP as directory for svn\n"; printf($fmt, $e->{'revision'}, $e->{'author'}, $e->{'date'}, $e->{'msg'}); + my @commit; + foreach my $p (@{$e->{'paths'}->{'path'}}) { my ($action,$path) = ($p->{'action'},$p->{'content'}); @@ -169,44 +241,181 @@ # prepare path and message my $file = $path; - $path =~ s,^/$SVNREP/*,, || die "BUG: can't strip SVNREP from path"; + $path =~ s,^$SVNREP/*,, || die "BUG: can't strip SVNREP '$SVNREP' from path"; + + if (! $path) { + print "NOTICE: skipped this operation. Probably trunk creation\n"; + next; + } + my $msg = $e->{'msg'}; - $msg =~ s/"/\\"/g; # quote " + $msg =~ s/'/'\\''/g; # quote " if ($action =~ /M/) { print "svn2cvs: modify $path -- nop\n"; } elsif ($action =~ /A/) { - log_system("$cvs add -m \"$msg\" $CVSREP/$path", "cvs add of $path failed"); + if (-d $path) { + chdir($path) || die "can't cd into dir $path for import: $!"; + log_system("$cvs import -d -m '$msg' $CVSREP/$path svn r$rev", "cvs import of $path failed"); + if (-d "$CVSREP/$path") { + rmdir "$CVSREP/$path" || die "can't remove $CVSREP/$path: $!"; + } else { + unlink "$CVSREP/$path" || die "can't remove $CVSREP/$path: $!"; + } + chdir("$TMPDIR") || die "can't cd to $TMPDIR/$CVSREP: $!"; + log_system("$cvs checkout $CVSREP/$path", "cvs checkout of imported dir $path failed"); + chdir("$TMPDIR/$CVSREP") || die "can't cd back to $TMPDIR/$CVSREP: $!"; + } elsif ($path =~ m,^(.+)/[^/]+$, && ! -e "$1/CVS/Root") { + my $dir = $1; + in_entries($dir) || log_system("$cvs add $dir", "cvs add of dir $dir failed"); + in_entries($path) || log_system("$cvs add $path", "cvs add of $path failed"); + } else { + in_entries($path) || log_system("$cvs add $path", "cvs add of $path failed"); + } } elsif ($action =~ /D/) { - log_system("$cvs delete -m \"$msg\" $CVSREP/$path", "cvs delete of $path failed"); + unlink $path || die "can't delete $path: $!"; + log_system("$cvs delete $path", "cvs delete of $path failed"); } else { print "WARNING: action $action not implemented on $path. Bug or missing feature of $0\n"; } - # now commit changes - log_system("$cvs commit -m \"$msg\" $CVSREP/$path", "cvs commit of $path failed"); + # save commits for later + push @commit, $path; } + my $msg = $e->{'msg'}; + $msg =~ s/'/'\\''/g; # quote " + + # now commit changes + log_system("$cvs commit -m '$msg' ".join(" ",@commit), "cvs commit of ".join(",",@commit)." failed"); + commit_svnrev($rev); } __END__ -svn export --force "$SVNROOT/$SVNREP" "$CVSREP" - -cd dotfiles - -for file in $(find -type f -not -path \*CVS\*); do - FILE=$(basename $file) - DIR=$(dirname $file) - if ! grep -q "^/$FILE/" $DIR/CVS/Entries ; then - cvs add $file - fi -done - -#cvs commit -m "Automatic commit from SVN" - -#rm -rf $TMPDIR +=pod + +=head1 NAME + +svn2cvs - save subversion commits to (read-only) cvs repository + +=head1 SYNOPSIS + + ./svn2cvs.pl SVN_URL CVSROOT CVSREPOSITORY + +Usage example (used to self-host this script): + + ./svn2cvs.pl file:///home/dpavlin/private/svn/svn2cvs/trunk/ \ + :pserver:dpavlin@cvs.tigris.org:/cvs svn2cvs/src + +=head1 DESCRIPTION + +This script will allows you to commit changes made to Subversion repository to +(read-only) CVS repository manually or from Subversion's C hook. + +It's using F<.svnrev> file (which will be created on first run) in +B to store last Subversion revision which was +committed into CVS. + +One run will do following things: + +=over 4 + +=item * +checkout B from B to temporary directory + +=item * +check if F<.svnrev> file exists and create it if it doesn't + +=item * +loop through all revisions from current in B (using +F<.svnrev>) up to B (current one) + +=over 5 + +=item * +checkout next Subversion revision from B over CVS checkout +temporary directory + +=item * +make modification (add and/or delete) done in that revision + +=item * +commit modification (added, deleted or modified files/dirs) while +preserving original message from CVS + +=item * +update F<.svnrev> to match current revision + +=back + +=item * +cleanup temporary directory + +=back + +If checkout fails for some reason (e.g. flaky ssh connection), you will +still have valid CVS repository, so all you have to do is run B +again. + +=head1 WARNINGS + +"Cheap" copy operations in Subversion are not at all cheap in CVS. They will +create multiple copies of files in CVS repository! + +This script assume that you want to sync your C (or any other +directory for that matter) directory with CVS, not root of your subversion. +This might be considered bug, but since common practise is to have +directories C and C in svn and source code in them, it's +not serious limitation. + +=head1 RELATED PROJECTS + +B L version control system that is a +compelling replacement for CVS in the open source community. + +B L converts a CVS repository to a +Subversion repository. It is designed for one-time conversions, not for +repeated synchronizations between CVS and Subversion. + +=head1 CHANGES + +Versions of this utility are actually Subversion repository revisions, +so they might not be in sequence. + +=over 3 + +=item r10 + +First release available to public + +=item r15 + +Addition of comprehensive documentation, fixes for quoting in commit +messages, and support for skipping changes which are not under current +Subversion checkout root (e.g. branches). + +=item r16 + +Support for importing your svn into empty CVS repository (it will first +create module and than dump all revisions). +Group commit operations to save round-trips to CVS server. +Documentation improvements and other small fixes. + + +=back + +=head1 AUTHOR + +Dobrica Pavlinusic + +L + +=head1 LICENSE + +This product is licensed under GNU Public License (GPL) v2 or later. + +=cut -echo "cvs left in $TMPDIR"