#!/usr/bin/perl -w
# svn-arch-mirror - one-way mirror from a Subversion archive to Arch
# 
# Copyright (C) 2004 Eric Wong <eric@petta-tech.com>
#                    (normalperson in #arch, Freenode)
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; version 2.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this file; see the file COPYING.  If not, write to the Free
# Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.

my $AUTHOR = 'Eric Wong <eric@petta-tech.com>';
my $VERSION = '0.2.6';

use strict;
use File::Basename qw(basename dirname);
use File::Temp qw(tempdir tempfile);
use File::Find qw(find);
use File::Copy qw(copy);
use File::Path qw(mkpath);
use Getopt::Long qw(:config gnu_getopt no_ignore_case no_auto_abbrev);
use XML::Simple;
use Time::Local;
use POSIX qw(strftime);

use Carp qw(confess);
#local $SIG{__WARN__} = \&Carp::cluck;

my %modes = (
	'sync' => [ \&update_tree,
		"<run this inside a tla tree-root>" ],
	'init' => [ \&init_arch_tree,
		"category--branch--version" ],
	'sync-all' => [ \&sync_all,
		"<run this inside to update an entire svn repo>" ],
	'init-branch' => [ \&init_branch, 
		"FROM-category--branch TO-category--branch--version" ],
);

my %alias = (
	"up" => "sync",
	"update" => "sync",
	"replay" => "sync",
	"init-tree" => "init"
);

sub usage {
	my $exit = shift || 0;
	my $p = basename $0;
	
	print "$p ($VERSION) - one-way mirroring from Subversion to Arch\n",
		"Usage: $p [options] command [arguments]\n\n",
		"Available commands:\n";
	print " $_ $modes{$_}->[1]\n" foreach (keys %modes);
	print "\nOptions:\n",
		"  -A, --archive        Override `tla my-default-archive\n",
		"  -C, --cacherev-mod   Automated cacherev interval\n",
		"  -d, --dir            Switch to target directory before executing\n",
		"  -h, --help           Show this help message\n",
		"  -r, --revision       Tag from this Arch revision <init-branch only>\n",
		"\n\n",
		"Report bugs to $AUTHOR\n\n";
	exit $exit;
}

chomp (my $A = $ENV{"TLA_DEFAULT_ARCHIVE"} || `tla my-default-archive`);
my ($CR,$R,$dir);
GetOptions( "archive|A=s" => \$A ,
	"cacherev-mod|C=i" => \$CR, "revision|r=s" => \$R, "dir|d=s" => \$dir );

my $mode = shift || usage();
if (my $m = $alias{$mode}) {
	$mode = $m;
}
usage(1) unless (defined $modes{$mode});

# copy stuff to a tempdir so we can mess with my-id/my-default-archive
# while running concurrent processes
my $tempdir = tempdir("/tmp/svn-arch-mirror.$$.XXXX", CLEANUP => 1);
cp_d("$ENV{HOME}/.arch-params", $tempdir);
symlink "$ENV{HOME}/.subversion", $tempdir;
$ENV{HOME} = $tempdir;
sys("tla my-default-archive $A");

if ($dir) {
	cd($dir);
	use Cwd;
	$ENV{PWD} = cwd;
}
if (system("which datefudge >/dev/null")){
	die "E: datefudge not available in \$PATH.\n";
}
$modes{$mode}->[0]->(@ARGV);
exit 0;

# fudgedate: convert the date format used by svn --xml into localtime
sub fudgedate {
	my $df;
	# subversion stores all times in UTC (Z)
	# ref: http://www.contactor.se/~dast/svn/archive-2003-05/1861.shtml
	if ($_[0] =~/^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)\.\d+Z$/) {
		$df = timegm($6,$5,$4,$3,$2-1,$1);
		$df = strftime("%Y-%m-%d %H:%M:%S",localtime($df));
	} else {
		die "Bad date format: $_[0]\n"
	}
	#print "$df\n";
	return $df;
}

sub init_branch {
	my ($from,$to) = @_;
	
	if ($from !~ /^(?:.+)--(?:.+)(?:--(?:[\d\.]+)?)?$/) {
		print STDERR "Bad FROM revision: $from\n";
		usage(1); 
	}
	if (!$to) {
		chomp($to = `tla tree-version`)
	}
	if ($to !~ /^(?:.+)--(?:.+)--(?:[\d\.]+)$/) {
		print STDERR "Bad TO revision: $to\n";
		usage(1);
	}
	
	print "tagging FROM: $from  TO: $to\n";

	# last: the fully-qualified Arch revision
	# first: the Subversion revision we start from
	my ($last,$first);

	if ($R) {
		$last = $R;
		$first = find_given_ancestor();
	} else {
		($last,$first) = auto_find_ancestor($from,$to)
	}
	
	tag_branch($last,$first,$from,$to);
}

# find_given_ancestor: find the ancestor of a given branch
sub find_given_ancestor {
	die "No ancestor (Arch revision) specified!\n" unless ($R);
	my ($r) = (`tla cat-archive-log $R` =~ /(?:^|\n)Keywords:\s+r(\S+)\n/);
	print "r: $r\nR: $R\n";
	die "$! cat-archive-log: $R\n" unless $r;
	my ($url) = (`svn info` =~ /(?:^|\n)URL:\s+(\S+)\n/);
	die "$! no Subversion URL!\n" unless $url;
	my $z = get_rel_path();
	$url =~ s/$z$//;
	my $ret = open_log_hr("-r $r $url");
	die "open_log_hr failed to get r$r from $url!\n" unless ($ret->{$r});
	return $ret->{$r};
}

sub auto_find_ancestor {
	my ($from,$to) = @_;
	my ($last,$first);
	
	# start inside an svn working dir
	# grab the revisions in this branch
	my @revs = open_log();
	die "No revisions for this tree!\n" unless (@revs);

	# and revisions in the trunk:
	my $x = abrowse_revs($from);
	
	#foreach (sort { $a <=> $b} keys %$x) {
	#	print "$_: $x->{$_}\n";
	#}
	#foreach my $r (reverse @revs) { print "R: $r->{revision}\n"; }
	
	# search for the first revision that appears in this branch
	# that's not in the trunk

	foreach my $r (reverse @revs) {
		if ($x->{$r->{revision}}) {
			$last = $x->{$r->{revision}};
		} else {
			print "First branched revision: ",
				"$r->{revision} ~= $last\n";
			unless ($last) {
				print "no ancestor patchlevel!\n";
				next;
			}
			if (system("svn up -r $r->{revision}") == 0) {
				$first = $r;
				last;
			} # else: SVN can't get to that revision (svn bug?)
			undef $first;
		}
	}
	
	$first ||= $revs[$#revs];
	$last ||= $x->{$first->{revision}};
	
	print "FR: $first->{revision} LAST: $last\n";
	#print "sleeping\n"; sleep 400;
	return ($last,$first);
}


sub get_arch_id_dirs {
	my @ids;
	#`find ./ -type d -name .arch-ids | grep -v '{arch}/++pristine-trees/'`;
	find(sub {
		-d && /^\.arch-ids\z/s
			&& $File::Find::dir !~ m#/?(?:,,|\+\+|new-files-archive|old-files-archive)/?#s
			&& push @ids, $File::Find::name;
		}, @_);
	return @ids;
}

sub tag_branch {
	my ($last,$first,$from,$to) = @_;
	my $svndir = basename($ENV{PWD});

	# pop out of the working dir, and get
	cd("..");
	sys("tla get $last $from");

	# go into the new directory and grab ids
	cd($from);
	
	#`find ./ -type d -name .arch-ids | grep -v '{arch}/++pristine-trees/'`;
	my @ids = get_arch_id_dirs("./");

	# back out
	cd("..");
	
	# remember, we keep $ENV{PWD} static :)
	# move .arch-ids dirs to the svn working copy
	foreach my $d (@ids) {
		my $t = dirname $d;
		cp_d("$from/$d", "$svndir/$t");
	}

	system("rm","-rf",",,inode-sigs","$from/\{arch\}/++pristine-trees/");
	cp_d("$from/\{arch\}",$svndir);
	sys("rm","-rf",$from);

	cd($svndir);
	
	chomp(my $cur = `tla logs -r -f | head -n 1`);
	
	system("tla tree-lint -t | xargs -r -n1 tla add");
	system("tla tree-lint -m | xargs -r -n1 tla rm");
	sys("tla tree-lint --strict");
	
	my $log = do_log($first);
	my $df = fudgedate($first->{date});
	
	my $email = url_to_email();
	sys("tla","my-id","$first->{author} <$email>");
	sys("datefudge",$df,"tla","tag","-S","-l",$log,$cur,$to);
	
	sys("tla sync-tree $to");
	sys("tla set-tree-version $to");
	update_tree(".");
}

sub update_tree {
	my $dir = shift || ".";
	cd($dir);
	die "Dir: $dir is not an Arch tree!" unless (-d '{arch}');
	die "Dir: $dir is not a Subversion working copy !" unless (-d '.svn');
	my @revs = open_log("-r BASE:HEAD");
	system("tla replay >/dev/null");
	#use Data::Dumper;print Dumper(@revs);
	replay_up($dir,@revs);
	cd($ENV{PWD}); # chdir doesn't update this unless you make it
}

sub open_log_hr {
	my $args = shift || '' ;
	my $log; 
	
	my ($fh, $filename) = tempfile( "/tmp/svn-arch-mirror.XXXX", CLEANUP => 1);
	open (my $svn, "svn log -v --xml $args |") || die "$!: svn log";
	while (<$svn>) { print $fh $_ }
	close $svn;
	close $fh;
	
	$log = XMLin( $filename,
		ForceArray => ['path','revision','logentry'],
		KeepRoot => 0,
		KeyAttr => { logentry => "+revision", paths => "+path" }
		);

	unlink $filename;

	#use Data::Dumper; print Dumper($log); print "fn: $filename\n"; exit 1;

	return $log->{logentry};
}

sub open_log {
	my $args = shift || '' ;
	my @revs;
	my $log = open_log_hr($args);
	
#	use Data::Dumper;
#	print "open_log: ",Dumper($log);
	
	foreach (sort {$b <=> $a} keys %$log) { push @revs, $log->{$_} }
	return @revs;
}

sub get_tagging_method {
	open my $tm, '{arch}/=tagging-method'
			|| die "Unable to open {arch}/=tagging-method\n";
	my $ret;
	while (<$tm>) {
		chomp;
		if (/^(names|implicit|tagline|explicit)\s*$/) {
			$ret = $1;
			last;
		}
	}
	close $tm;
	die "Unable to get tagging-method!\n" unless ($ret);
	return $ret;
}


sub replay_up {
	my $count = 0;
	my $email = url_to_email();
	my $z = shift;
	if ($mode ne 'sync-all') {
		$z = get_rel_path();
	}
	$z =~ s/^[\.\/]+//g;
	my $tmethod = get_tagging_method();
	while (my $r = pop @_) {
		my $cur = current();
		next if ($cur == $r->{revision});
		
		my $map = map_moved_ids($z,@{$r->{paths}->{path}});
		#use Data::Dumper;print "premove: ",Dumper($map->{'pre-moved'});

		open my $svn,"svn up -r $r->{revision} |"
				|| die "$! svn up -r $r->{revision}";
		while (<$svn>) {
			next if ($tmethod eq 'names');
			chomp;
			#print "SVN $_\n";
			if (/^A  (.*)$/) {
				my $new = $1;
				if (my $from = $map->{"move-id"}->{$new}) {
					sys("tla","move-id",$from,$new);
				} elsif (my $f = $map->{"pre-moved"}->{$new}) {
					print "W: pre-moved $f => $new\n";

					# this deals with copied files,
					# it's suboptimal, but its all we
					# can do:
					system("tla","add-id",$f) if (-e $f);
				} elsif (my $dm = $map->{"dmoved"}->{$new}) {
					my $cwd = cwd;
					cd($dm);
					my @ids = get_arch_id_dirs("./");
					cd($cwd);
					foreach my $d (@ids) {
						my $t = dirname $d;
						cp_d("$dm/$d","$new/$t");
						sys("rm","-rf","$dm/$d");
					}
				} else {
					system("tla","add-id",$new);
				}
			} elsif (/^D  (.*)$/) {
				my $d = $1;
				if (my $t = $map->{"moved"}->{$d}) {
					unless (-e $t) {
						die "Not Moved? $d => $t\n"
					}
				} else {
					system("tla","delete-id",$d);
				}
				sys("rm","-rf",$d) if (-d $d);
			}
		}
		close $svn;

		system("tla tree-lint -t | xargs -r -n1 tla add");
		system("tla tree-lint -m | xargs -r -n1 tla rm");

		sys("tla tree-lint --strict");
		my $log = do_log($r);
		my $df = fudgedate($r->{date});
		
		sys("tla","my-id","$r->{author} <$email>");
		sys("datefudge",$df,"tla","commit","-l",$log);
		
		if ($CR) {
			sys("tla cacherev") if (($count % $CR) == 0);
			++$count;
		}
	}
}

sub cd {
	unless (chdir $_[0]) {
		print STDERR "$!: Failed to chdir $_[0]\n";
		confess;
		die "really DIE on a bad chdir\n";
	}
}

# get_rel_path: get the path of the current working copy
# relative to the svn root
# WARNING: do not use in nested directories that were created by copying
# a parent
sub get_rel_path {
	#die "get_rel_path: run in a tla tree-root\n" unless (-d '{arch}');
	my @revs = open_log();
#	print STDERR "get_rel_path\n";
	my ($d,$p) = svn_path_info();
	my ($z,$bak);
	foreach my $r (@revs) {
		foreach my $x (@{$r->{paths}->{path}}) {
			next unless ($x->{action} =~/^[AR]$/);
			die "E: get_rel_path\n" unless (my $c = $x->{content});
			if ( (!$z || (length $c < length $z))
					&& $p =~ /$c$/) {
				$z = $c;
			}
			if (!defined $bak || (length $c < length $bak)) {
				$bak = $c;
			}
		}
	}
	if (!defined $z && defined $bak) {
		$z = $bak
	}

	if (!defined $z) {
		confess("get_rel_path failed!\n");
	}

	return $z;
}

sub filter {
	my ($filter,$path) = @_;
	$path =~ s/$filter//;
	$path =~ s#/+#/#g;
	$path =~ s#^[/\.]+##;
	return $path;
}

sub map_moved_ids {
	my $map;
	my $z = shift;
	my $ret;
	$ret->{'pre-moved'} = {};
	$ret->{'moved'} = {};
	$ret->{'move-id'} = {};
	$ret->{'dmoved'} = {};
	foreach my $p (@_) {
		next unless ($p->{action} eq 'A' || $p->{action} eq 'D');
		if ($map->{$p->{action}}->{$p->{content}}) {
			use Data::Dumper;
			print STDERR "PANIC! duplicate data: ",
				"$p->{action}: $p->{content}\n",
				"map_moved_ids(): ",Dumper(@_),"\n","current: ",
				Dumper($map->{$p->{action}}->{$p->{content}}),
				"\n";
			exit 1;
		}
		$map->{$p->{action}}->{$p->{content}} = $p;
	}
	# deal with added files first
	foreach my $c (sort keys %{$map->{A}}) {
		my $to = filter($z,$c);
		next unless (my $f = $map->{A}->{$c}->{'copyfrom-path'});
		# moved file: move-id, too
		my $from = filter($z,$f);
		if (-d $from) {
			# oh no! this is going to get tricky:
			if (exists $map->{D}->{$f}) {
				my $cwd = cwd;
				cd($from);
				my @ids = get_arch_id_dirs("./");
				cd($cwd);
				my $cpt = "$tempdir/".int(rand(time))."/$from";
				foreach my $d (@ids) {
					my $t = dirname $d;
					cp_d("$from/$d","$cpt/$t");
					sys("rm","-rf","$from/$d");
				}
				$ret->{"dmoved"}->{$to} = $cpt;
			} else {
				print STDERR "W: cp $from => $to, undefined\n";
			}
		} else {
			if (exists $map->{D}->{$f}) {
				$ret->{"move-id"}->{$to} = $from;
				$ret->{"moved"}->{$from} = $to;
			} elsif (-e $from) {
				print "W: early move-id: $from => $to\n";
				$ret->{"pre-moved"}->{$to} = $from;
				sys("tla","move-id",$from,$to);
				$ret->{"moved"}->{$from} = $to;
			} else {
				print "W: out-of-tree move: $from => $to\n";
			}
		}
	}
	foreach my $c (keys %{$map->{D}}) {
		$c = filter($z,$c);
		print "D: $c\n";
		unless ($ret->{"moved"}->{$c}) {
			if (-d $c && -d "$c/.arch-ids") {
				#sys("rm","-rf","$c/.arch-ids")
				#sys("find '$c' -name .arch-ids -type d -print0 | xargs -0 rm -rf");
				find(sub{
					-d && /^\.arch-ids\z/s && sys("rm","-rf",$_);
					}, $c);
			}
		}
	}
	return $ret;
}

sub svn_path_info {
	my $domain;
	my $path = "";
	open my $svn, "svn info |" || die "$!: svn info";
	while (<$svn>) { 
		if (m#^URL:\s*(?:[a-z\+]+):\/+([^\/]+)\/+(\S*)#i) {
			($domain,$path) = ($1,$2);
		}
	}
	close $svn;
	die "No URL info!" unless ($domain);
	return ($domain,$path);
}

# turns the URL info of the repo into pseudo-email address
# https://foo.org/path/to/stuff => path+to+stuff@foo.org
sub url_to_email {
	my ($d,$u) = svn_path_info();
	if ($u) {
		$u = join('+',split(/\//,$u))
	} else {
		$u = "root";
	}
	return "$u\@$d";
}

# do_log: writes a changelog in the current directory based on the rev
# info it's given
sub do_log {
	my $r = shift;
	chomp (my $log = `tla make-log`);

	open my $msg, "> $log" || die "$! log: $log\n";

	my $sum = '';
	if (ref($r->{msg}) eq '') {
		$sum = $r->{msg};
		$sum =~ s/^\s+//;
		$sum =~ s/\s+$//;
		$sum =~ s/\n/\n /g;
	}
	print $msg "Summary: r$r->{revision}: $sum\n",
		"Keywords: r$r->{revision}\n\n";

	open (my $svn,"svn info |") || die "$! svn info\n";
	print $msg "Message:\n$r->{msg}\n" if (ref($r->{msg}) eq '');
	while (<$svn>) { print $msg $_ }
	close $svn;

	close $msg;
	return $log;
}

# abrowse_revs: abrowse and find an Arch revision that matches a corresponding SVN rev
sub abrowse_revs {
	my $from = shift;
	$from =~ s/^([^\-]+)--.*/$1/;
	my $ret;
#	print "FROM: $from\n";
	open my $tla, "tla abrowse -s -f $from |" 
			|| die "$!: tla abrowse $from";
	my $prev;
	while (<$tla>) {
		if (/^\s{8}(\S+@\S+\/\S+--\S+--\S+--\S+)/) {
			$prev = $1;
		} elsif (/^\s{10}r(\d+):/ && $prev) {
			$ret->{$1} = $prev;
		} else {
			undef $prev;
		}
	}
	close $tla;
	die "No revisions in $from" unless $ret;
	return $ret;
}

sub init_arch_tree {
	my $treeversion;
	
	if (-d '{arch}') {
		chomp($treeversion = `tla tree-version`) 
	} else { 
		$treeversion = shift
	}

	unless (defined $treeversion
			&& $treeversion =~ /(?:.+)--(?:.+)--(?:[\d\.]+)$/) {
		die "No treeversion supplied!\n";
	}

	my $first = first();
	sys("svn up -r $first->{revision}");
	
	# we can allow the user to set up their own tagging method
	unless (-d '{arch}') { 
		sys("tla init-tree --nested $treeversion");
		# far too many people have dotfiles in svn repos,
		# so make this the default behaviour
		open(my $tm,">> {arch}/=tagging-method")
				|| die "$!: unable to open =tagging-method\n";
		print $tm "\n",'source ^\..*$',"\n"; 
		close $tm;

	}
	my $tmethod = get_tagging_method();
	if ($tmethod eq "explicit") {
		#sys("find | grep -v '/\\.svn' | grep -v '{arch}' | grep -v '\.arch-ids' | xargs -r tla add");
		find({ wanted => sub { 
			unless (/(?:^|\/)(?:\.svn|\{arch\}|\.arch-ids)(?:\/|$)/s) {
				system("tla","add",$_)
			}
		}, no_chdir => 1 }, "./");
	}

	sys("tla tree-lint --strict");
	my $log = do_log($first);
	my $df = fudgedate($first->{date});
	
	my $email = url_to_email();
	sys("tla","my-id","$first->{author} <$email>");
	sys("tla","archive-setup",$treeversion);
	sys("datefudge",$df,"tla","import","-l",$log);
	
	update_tree(".");
}

# current: grab the current svn revision info
sub current {
	my $ret;
	open (my $svn, "svn info |") || die "$!: svn info";
	while (<$svn>) { if (/^Revision:\s*(\d+)/) { $ret=$1 } }	
	close $svn;
	die "No revision!" unless ($ret=~/^\d+$/);
	return $ret;
}

# first: find the first svn revision
sub first {
	my @revs = open_log();
	my $r = pop @revs;
	unless ($r->{revision} =~ /^\d+$/) {
		die "Bad first revision: $r->{revision}\n"
	}
	return $r;
}

# sys: system() wrapper since perl can't do set -e
sub sys {
	#print "sys: ",join(' ',@_),"\n";
	unless (system(@_) == 0) {
		print STDERR "E: $!: '".join("' '",@_)."'\nPWD: ",`pwd`;
		confess;
	}
}

sub sync_all {
	foreach (`find -mindepth 2 -name {arch} -type d`) {
		chomp $_;
		next if (m#/,,#);
		next if (m#/\+\+#);
		next if (m#/(?:new|old)-files-archive#);
		my $d = dirname ($_);
		next unless (-d "$d/.svn");
		update_tree($d);
	}
	#sys("svn up") if (-d ".svn");
}

# copy directories recursively: cp -ax from to
sub cp_d {
	my ($from,$to) = @_;
	$from =~ s#/+$##g;
	$to =~ s#/+$##g;
#	print "cp_d: $from => $to\n";
	if (-d $to) {
		$to = "$to/".basename $from;
	}
	unless (-d $to) {
		mkpath $to || die $!;
	}
	confess "  to: $to is not a directory\n" unless (-d $to);
	confess "from: $from is not a directory\n" unless (-d $from);
	find( { no_chdir => 1,
		wanted => sub {
				my $new = $_;
				$new =~ s#$from#$to/#;
#				print "old: $_ new: $new\nPWD: "; system('pwd');
				die if ($new eq $_);
				if (-l $_) {
					my $l = readlink $_;
#					print "ln -s $_ => $new\n";
					symlink $l, $new || die $!;
				} elsif (-d $_) {
					mkpath $new unless -d $new;
				} else {
					my $d = dirname $new;
					mkpath $d unless -d $d;
#					print "cp $_ => $new\n";
					copy $_, $new || die $!;
				}
			}
		} ,$from);
}

__END__

=head1 NAME
 
svn-arch-mirror - one-way mirror from a Subversion archive to Arch

=head1 SYNOPSIS

svn-arch-mirror [options] <command> [arguments]

=head1 DESCRIPTION

svn-arch-mirror makes it possible to track upstream Subversion
repositories and replicate full project history from Subversion to
Arch.  This was designed for Arch users who want to track active
projects which use Subversion, and for repository maintainers who
wish to migrate from Subversion to Arch.

=head1 COMMON OPTIONS

The following options are common to all commands:

=over 4

=item -h, --help

Show this help message

=item -A, --archive

Override `tla my-default-archive'

=item -C, --cacherev-mod

Cacherev every <cachrev-mod> revisions (default: 0 / off)

=item -d, --dir

Switch to target directory before executing

=back

=head1 COMMON USAGE

=over 4

=item B<init> (category--branch--version)

Run from inside a designated tree-root, it will create and import a new
(category--branch--version) from the beginning, preserving Subversion
changes as Arch changesets.

You may manually do a `tla init-tree' and change your tagging method
before running this, and not specify a (category--branch--revision).

=item B<sync>

Run from inside a double-initialized svn/tla tree.  It will run `svn up'
on all new revisions and `tla commit' for each one that hasn't been
commited.

=back

=head1 ADVANCED USAGE

(useful for tracking large or multiple sub-projects):

=over 4

=item B<init-branch> (FROM-category--branch) (TO-category--branch--version)
    
Run from inside a designated tree-root, it will create and
import a new (TO-category--branch--version) assuming
(FROM-category--branch) is tracked using this tool.  Since
Subversion lacks advanced merge-tracking, svn-arch-mirror is
unable to track merges (merges are still recorded, but they're
not managed in the history-sensitive manner tracked by Arch).

=over 8

=item B<-r>, B<--revision>

Override auto-detection of branch ancestor and tag from this Arch
revision instead

=back

=item B<sync-all>
    
Same as sync, but it will recursively seek out trees, making it
ideal for tracking an entire Subversion repository as opposed
to one sub-project.

=back

=cut

=head1 EXAMPLES

To start tracking a new repo:
  
  svn co <URL> directory
  cd directory
  svn-arch-mirror init <category>--<branch>--<version>
  
Then, to keep a tree up-to-date, run this inside a tree-root
(you could make this a cronjob):
  
  svn-arch-mirror sync  

=head1 SEE ALSO

B<svn help>, B<tla help>

=head1 BUGS AND LIMITATIONS

Autodetection of branch handling is imperfect due to fundamental
differences in the repository models.

Subversion repositories that deviate from the structure recommended
by the Subversion authors may be difficult to track.

Tracking of merges and merge history is limited because Subversion has
limited support for this.

Files and directories copied within a project tree don't get history
tracked.  This is because Arch treats copied files as new files, whereas
Subversion has no distinction between branching and copying.

Signed Arch archives not yet supported.  Workaround: create a signed
mirror of the Arch archive, and mirror to that.

Deleted Subversion branches are not yet tracked.

=head1 AUTHOR

Copyright (C) 2004-2005 Eric Wong <eric@petta-tech.com>

This is free software; see the GNU General Public Licence version 2
for copying conditions. There is NO warranty.

=cut

# arch-tag: 8b331e91-36c8-4285-9121-df035275c4b9

