#!/usr/bin/perl -w

# This script will transfer changes from Subversion repository
# to CVS repository (e.g. SourceForge) while preserving commit
# logs.
#
# Based on original shell version by Tollef Fog Heen available at
# http://raw.no/personal/blog
#
# 2004-03-09 Dobrica Pavlinusic <dpavlin@rot13.org>

use strict;
use File::Temp qw/ tempdir /;
use Data::Dumper;
use XML::Simple;

if (@ARGV < 2) {
	print "usage: $0 SVN_URL CVSROOT CVSREPOSITORY\n";
	exit 1;
}

my ($SVNROOT,$CVSROOT, $CVSREP) = @ARGV;

if ($SVNROOT !~ m,^[\w+]+:///*\w+,) {
	print "ERROR: invalid svn root $SVNROOT\n";
	exit 1;
}

my $TMPDIR=tempdir( "/tmp/checkoutXXXXX", CLEANUP => 1 );

chdir($TMPDIR) || die "can't cd to $TMPDIR: $!";

# 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) == 0 || die "$errmsg: $!";
}

#
# sub to commit .svn rev file later
#
sub commit_svnrev {
	my $rev = shift @_;
	my $add_new = shift @_;

	die "commit_svnrev needs revision" if (! defined($rev));

	open(SVNREV,"> .svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
	print SVNREV $rev;
	close(SVNREV);

	my $path=".svnrev";

	if ($add_new) {
		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\" $path" || die "cvs commit of $path failed: $!";
	}
}

# ok, now do the checkout

log_system("$cvs -q checkout $CVSREP", "cvs checkout failed");

chdir($CVSREP) || die "can't cd to $TMPDIR/$CVSREP: $!";


my $rev;

# 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
that only newer changes will be commited.

It's quote possible that this is first svn2cvs run for this repository.
If so, you will have to identify correct svn revision which
corresponds to current version of CVS repository that has just
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.

_USAGE_

	print "svn revision corresponding to CVS [abort]: ";
	my $in = <STDIN>;
	chomp($in);
	if ($in !~ /^\d+$/) {
		print "Aborting: revision not a number\n";
		exit 1;
	} else {
		$rev = $in;
		commit_svnrev($rev,1);	# create new
	}
} else {
	open(SVNREV,".svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
	$rev = <SVNREV>;
	chomp($rev);
	close(SVNREV);
}

print "Starting after revision $rev\n";
$rev++;


#
# FIXME!! HEAD should really be next verison and loop because this way we
# loose multiple edits of same file and corresponding messages. On the
# other hand, if you want to compress your traffic to CVS server and don't
# case much about accuracy and completnes of logs there, this might
# be good. YMMV
#
open(LOG, "svn log -r $rev:HEAD -v --xml $SVNROOT |") || die "svn log for repository $SVNROOT failed: $!";
my $log;
while(<LOG>) {
	$log .= $_;
}
close(LOG);

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

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";
	exit 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 $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 {
print "## tmppath: $tmppath tmpsvn: $tmpsvn SVNREP: $SVNREP\n";
		if ($tmpsvn =~ s,(/\w+/*)$,,) {
			$SVNREP .= $1;
		} else {
			die "ERROR: can't deduce svn dir from $SVNROOT.\nUsing root of snv repository for current version instead of /trunk/ is not supported.\n";
		}
	} until ($tmppath =~ m/^$SVNREP/);

	print "NOTICE: using $SVNREP as directory for svn\n";

	printf($fmt, $e->{'revision'}, $e->{'author'}, $e->{'date'}, $e->{'msg'});
	foreach my $p (@{$e->{'paths'}->{'path'}}) {
		my ($action,$path) = ($p->{'action'},$p->{'content'});

		print "svn2cvs: $action $path\n";

		# prepare path and message
		my $file = $path;
		$path =~ s,^$SVNREP/*,, || die "BUG: can't strip SVNREP from path";

		if (! $path) {
			print "NOTICE: skipped this operation. Probably trunk creation\n";
			next;
		}

		my $msg = $e->{'msg'};
		$msg =~ s/"/\\"/g;	# quote "

		if ($action =~ /M/) {
			print "svn2cvs: modify $path -- nop\n";
		} elsif ($action =~ /A/) {
			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");
				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: $!";
			} else {
				log_system("$cvs add -m \"$msg\" $path", "cvs add of $path failed");
			}
		} elsif ($action =~ /D/) {
			log_system("$cvs delete -m \"$msg\" $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\" $path", "cvs commit of $path failed");

	}

	commit_svnrev($rev);
}
