#!/usr/bin/perl -w
# svn-arch-mirror - one-way mirror from a Subversion archive to Arch
# 
# Copyright (C) 2004-2005 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; either version 2 of the License, or
# (at your option) any later version.
#
# 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, 51 Franklin Street, Fifth Floor, MA 02110-1301  USA.

our $AUTHOR = 'Eric Wong <eric@petta-tech.com>';
our $VERSION = '0.4.4';
our $SVN_VER = undef;
our $SVN_REV = undef;

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 rmtree);
use Cwd qw(abs_path getcwd);
use Getopt::Long qw(:config gnu_getopt no_ignore_case no_auto_abbrev);
use Fcntl qw(O_CREAT O_EXCL O_WRONLY LOCK_EX);
use Time::Local;
use POSIX qw(strftime);
use Carp qw(cluck);
use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$ENV{LC_ALL} = 'C';
$ENV{TMPDIR} = '/tmp' if (!defined $ENV{TMPDIR});

my ($extra_re, $ignore_ext); # new flags for svn 1.2.x

sub __func__ () { ((caller 1)[3]) || 'CORE::main' }
sub __line__ () { ((caller 1)[2]) }
sub w (@) { print STDERR 'W: ',@_,"\n" }
sub e (@) { print STDERR 'E: ',@_,"\n" }
sub p (@) { print @_,"\n" }
sub _die (@) { e @_; quit(1) }

sub d (@) {
	if ($ENV{SVN_ARCH_MIRROR_DEBUG}) {
		my ($func, $line) = (((caller 1)[3] || 'CORE::main'),
					(caller 0)[2]);
		print STDERR $func,':',$line,': ',@_,"\n"
	}
}

sub dd (@) {
	if ($ENV{SVN_ARCH_MIRROR_DEBUG}) {
		my ($func, $line) = (((caller 1)[3] || 'CORE::main'),
					(caller 0)[2]);
		print STDERR $func,':',$line,': ',Dumper(@_);
	}
}

# copy directories recursively: cp -pR from to
sub cp_d ($$) {
	my ($from,$to) = @_;
	$to = "$to/".basename($from) if (-d $to);
	unless (-d $from) { cluck "from: $from is not a directory\n"; quit(1) }
	sys('cp','-pR',$from,$to);
}

# precompile the regex for skipping junk and downloaded changesets
my $arch_ignore_re = qr/\/?(?:,,|\+\+|(?:new|old)-files-archive)\/?/s;
my $arch_exclude = qr/\/?(?:\{arch\}|\.arch-ids)\/?/s;
my $arch_inventory = qr/\/?(?:\.arch-inventory)\/?/s;
my $arch_id = qr/\.arch-ids\/.*\.id$/s;

my %modes = (
	'sync' => [ \&sync_one,
		"<run this inside an Arch tree-root/Subversion working copy>" ],
	'init' => [ \&init_arch_tree,
		"category--<branch-->version" ],
	'init-branch' => [ \&init_branch, 
		"FROM-category--<branch-->version TO-category--<branch-->version" ],
	'find-nested' => [ \&find_nested,
		"<list nested directories managed both by SVN and Arch>"],
	'sync-nested' => [ \&sync_nested,
		"<sync nested directories managed both by SVN and Arch>"],
	'get' => [ \&double_get, 'category--<branch-->version<--revision>' ],
);

my %alias = (
	'up' => 'sync',
	'update' => 'sync',
	'replay' => 'sync',
	'init-tree' => 'init',
	'init-tag' => 'init-branch',
	'sync-all' => 'sync-nested-',  # trailing '-' denotes deprecated
);

sub usage {
	my $exit = shift || 0;
	my $p = basename $0;
	
	p "$p ($VERSION) - one-way mirroring from Subversion to Arch\n",
		"Usage: $p [options] command [arguments]\n",
		"Available commands:\n";
	p " $_ $modes{$_}->[1]" foreach (keys %modes);
	p "\nOptions:\n",
	  "  -c, --arch-client    Specify an Arch client to use (baz or tla)\n",
	  "  -A, --archive        Override `my-default-archive'\n",
	  "  -d, --dir DIR        Switch to target directory before running\n",
	  "  -h, --help           Show this help message\n",
	  "  -r, --revision       init-branch: Tag from this Arch revision\n",
	  "                       init: Start tracking at this SVN revision\n",
	  "  -l, --revision-limit Stop tracking at this SVN revision\n",
	  "  -1, --one-repo       sync-nested optimizes for one SVN repository\n",
	  "  --ids-from DIR       Use arch-id files from target directory\n",
	  "  --no-datefudge       Don't use datefudge to preserve SVN dates\n",
	  "  --no-my-id-switch    Don't switch user-ids to preserve creator\n",
	  "  --no-tmp-home        Don't redefine \$HOME\n",
	  "  --no-lint            Don't lint, use this for broken symlinks\n",
	  "  --sign               Alias for --no-my-id-switch --no-tmp-home",
	  "\n",
	  "Report bugs to $AUTHOR\n";
	quit($exit);
}

# just like the *nix command
sub which ($) {
	foreach (split(/:/,$ENV{PATH})) {
		my $x = "$_/$_[0]";
		return $x if (-x $x && (-f _ || -l _));
	}
	return undef;
}

my %_o = (	revision_limit => 'HEAD',
		datefudge => 1,
		tmp_home => 1,
		lint => 1,
		my_id_switch => 1);

chomp($_o{arch_client} = which('tla') || which('baz'));
usage(1) unless (GetOptions(\%_o,
		'archive|A=s',
		'revision|r=s',
		'sign|signed|signed-archive',
		'revision_limit|revision-limit|l=s',
		'arch_client|arch-client|c=s',
		'dir|d=s',
		'ids_from|ids-from=s',
		'datefudge!',
		'my_id_switch|my-id-switch!',
		'tmp_home|tmp-home!',
		'config',
		'lint|tree-lint!',
		'import_only|import-only',
		'strict',
		'one_repo|one-repo|1',
		'help|h|H'));

if ($_o{sign}) {
	$_o{tmp_home} = 0;
	$_o{my_id_switch} = 0;
}

# --ids-from is forced to an absolute path
if ($_o{ids_from}) {
	unless (-d $_o{ids_from}) {
		_die '--ids-from directory: ',$_o{ids_from}," not found\n";
	}
	$_o{ids_from} = abs_path($_o{ids_from});
}

# allow svn-like parsing of -r
if ($_o{revision} && $_o{revision} =~ /:/) {
	if ($_o{revision_limit} ne 'HEAD') {
		w 'specified --revision-limit will be overridden by ',
				(split(/:/,$_o{revision}))[1]
	}
	($_o{revision},$_o{revision_limit}) = split(/:/,$_o{revision},2);
}

my $mode = shift || usage(1);
if (!$_o{help}) {
	if (my $mode_alias = $alias{$mode}) {
		if ($mode_alias =~ s/\-$//) {
			w "`$mode' is deprecated, use `$mode_alias' instead.",
			  " Continuing...";
		}
		$mode = $mode_alias;
	}
} else {
	usage(0);
}

# make things easier to type...
my $tla = $_o{arch_client};
my $baz = (index($tla,'baz') >= 0) ? 1 : 0;
my $lintcmd = $baz ? 'lint' : 'tree-lint';
my $tempdir = tempdir("$ENV{TMPDIR}/svn-arch-mirror.$$.XXXX",
				CLEANUP => $ENV{SVN_ARCH_MIRROR_DEBUG} ? 0 : 1);

if ($_o{datefudge} && !which('datefudge')) {
	_die "E: datefudge not available in \$PATH.\n",
		"specify --no-datefudge to continue regardless, but be warned",
		" this will cause commit dates to be lost";
}

$SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} = \&quit;

chomp($_o{-orig_my_id} = `$tla my-id`) if $_o{my_id_switch};
if ($_o{archive}) {
	chomp($_o{-orig_archive} = `$tla my-default-archive`);
	sys($tla, 'my-default-archive', $_o{archive});
}

if ($_o{dir}) { cd($_o{dir}); $ENV{PWD} = getcwd }

$modes{$mode}->[0]->(@ARGV);
quit(0);

## subroutines begin here:

# copy stuff to a tempdir so we can mess with my-id/my-default-archive
# while running concurrent processes (and not clobber tla)
sub set_tmp_home () {
	if (($_o{tmp_home} && $_o{my_id_switch})) {
		cp_d("$ENV{HOME}/.arch-params", $tempdir);
		symlink "$ENV{HOME}/.subversion", "$tempdir/.subversion";
		$ENV{HOME} = $tempdir;
	}
}

sub quit {
	$SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} = 'IGNORE';
	sys($tla,'my-id',$_o{-orig_my_id}) if $_o{-orig_my_id};
	sys($tla,'my-default-archive',$_o{-orig_archive}) if $_o{-orig_archive};
	exit(shift || 0);
}

# compatiblity function because baz and tla differ here

sub arch_tag_branch ($$$$) {
	my ($date,$log,$cur,$to) = @_;
	my @arg = $baz ? ('branch','-l') : ('tag','-S','-l');
	my $pid = open(my $pipe,'-|');
	if ($pid) {
		while (<$pipe>) {
			if (/(\S+\/$to)\s*$/) {
				$to = $1;
			}
		}
		close $pipe;
	} else {
		exec('datefudge',$date,$tla,@arg,$log,$cur,$to) || _die "branch"
	}
	return $to;
}

sub arch_set_tree_version ($) {
	sys($tla, $baz ? 'tree-version' : 'set-tree-version', $_[0]);
}

sub arch_tree_lint () { sys($tla, $lintcmd, '--strict') if ($_o{lint}) }

sub arch_import ($$) {
	my ($date,$log) = @_;
	my @arg = $baz ? ('import') : ('import','-S');
	sys('datefudge',$date,$tla,@arg,'-l',$log);
}

sub arch_my_id ($) { sys($tla,'my-id',$_[0]) if ($_o{my_id_switch}) }	

sub svn_tree_lint () {
	my @err;
	svn_read_version();
	open my $svn, "svn status $ignore_ext |" || _die "$!: svn status\n";
	while (<$svn>) {
		chomp;
		next if /$arch_exclude/;
		next if /$arch_inventory/;
		push @err, $_;
	}
	close $svn || _die "svn status failed";
	if (@err) {
		e "svn status revealed unmanaged files:";
		e $_ foreach (@err);
		e "Exiting";
		quit(1);
	}
}

sub svn_read_version () {
	return ($SVN_VER, $SVN_REV) if ($SVN_VER && $SVN_REV);
	open my $svn, 'svn --version |' or _die "$!: svn --version\n";
	while (<$svn>) {
		chomp;
		if (/svn, version ([a-zA-Z\d\.]+) \(r(\d+)\)/) {
			$SVN_VER = $1;
			$SVN_REV = $2;
		}
	}
	if ($SVN_VER =~ /^[1-9]\.[2-9]\.\d+/) {
		$extra_re = '[ADUCG ]   ';
		$ignore_ext = '--ignore-externals'
	} else {
		$extra_re = '  ';
		$ignore_ext = '';
	}

}

sub svn_info () {
	my $info;
	open my $svn, 'svn info |' || _die "$!: svn info\n";
	# only single-lines seem to exist...:
	while (<$svn>) {
		if (m#^([^:]+)\s*:\s*(\S*)$#) {
			$info->{$1} = $2;
			push @{$info->{-order}}, $1;
		}
	}
	close $svn;
	return $info;
}

# 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"
	}
	return $df;
}

# find_given_ancestor: find the ancestor of a given branch
sub find_given_ancestor {
	_die "No ancestor (Arch revision) specified!\n" unless ($_o{revision});
	my ($r) = (`$tla cat-archive-log $_o{revision}` =~
	             /(?:^|\n)Keywords:\s+r(\d+)/s);
	p "r: $r\nR: $_o{revision}";
	_die "$! cat-archive-log: $_o{revision}\n" unless $r;
	my $info = svn_info();
	my $url = $info->{URL};
	_die "$! no Subversion URL!\n" unless $url;
	my $rel_path = svn_info_rel_path($info);
	$url =~ s/$rel_path$//;
	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);
	
	svn_read_version();
	
	# 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);
	
	# 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 {
			p "First branched revision: $r->{revision} ~= $last";
			unless ($last) {
				w "no ancestor patchlevel!";
				next;
			}
			if (system("svn up $ignore_ext -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}};
	
	p "FROM: $first->{revision} LAST: $last";
	return ($last,$first);
}


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

# explicitly tags all untagged files
sub add_all_untagged ($$$$$) {
	my ($my_id,$rel_path,$df,$r,$quiet) = @_;
	my @added;
	while (chomp(my @new = `$tla $lintcmd -t`)) {
		foreach my $x (@new) {
			push @added, $x;
			# escape backslashes since `` expands
			$x =~ s#\\#\\\\#g;
			# yes, $x should be double quoted:
			add_id($my_id,$df,$rel_path,
					`$tla escape --unescaped "$x"`,$r);
		}
		d '++A ', join("\n++A ",@new) if (!$quiet);
	}
	return \@added;
}

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

	# pop out of the working dir, and get
	cd("..");
	my $tmpdir = ",,".time.".$$.$from";
	$tmpdir =~ s/\//%/g;
	sys($tla,'get',$last,$tmpdir);

	# go into the new directory and grab ids
	cd($tmpdir);
	
	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("$tmpdir/$d", "$svndir/$t");
	}

	# remove files that don't like being copied around:
	rmtree([',,inode-sigs',"$tmpdir/\{arch\}/++pristine-trees/"]);
	cp_d("$tmpdir/\{arch\}",$svndir);
	rmtree([$tmpdir]);

	cd($svndir);
	
	my $cur = current_arch_rev();
	my $info = svn_info();
	my $email = svn_info_uid_email($info);
	my $rel_path = svn_info_rel_path($info);
	my $my_id = "$first->{author} <$email>";
	arch_my_id($my_id);
	my $df = fudgedate($first->{date});

	# fall back calls:
	add_all_untagged($my_id,$rel_path,$df,$first->{revision},0);
	rm_dangling_ids();
	
	arch_tree_lint();
	svn_tree_lint();
	
	my $log = do_log($first,$info);
	p "r$first->{revision}: $log->{summary}";
	$to = arch_tag_branch($df,$log->{file},$cur,$to);
	
	sys($tla,'sync-tree',$to);
	arch_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 $lockfile = '{arch}/,,svn-arch-mirror.lock';
	sysopen(LOCKF, $lockfile, (O_EXCL|O_WRONLY|O_CREAT))
			|| _die "$!: failed lock directory, ",
				"lockfile: $lockfile\n";
	flock(LOCKF,LOCK_EX) || _die "$! failed to lock directory, ",
					"lockfile: $lockfile\n";
	truncate(LOCKF, 0) || _die "$! failed to truncate after lock, ",
					"lockfile: $lockfile\n";
	my @revs = open_log("-rBASE:$_o{revision_limit}");
	system("$tla replay >/dev/null");
	replay_up(@revs);
	close(LOCKF);
	unlink $lockfile;
	cd($ENV{PWD}); # chdir doesn't update this unless you make it
}

sub open_log_hr {
	my (@args) = ('svn','log','-v','--xml');
	push @args, $_ foreach (@_);
	my $log; 

	my ($fh, $filename) = tempfile("$ENV{TMPDIR}/svn-arch-mirror.XXXX");
	my $pid = open(my $pipe,'-|');
	if ($pid) {
		print $fh $_ while (<$pipe>);
		close $pipe;
	} else {
		exec(@args) || _die "$!: svn log";
	}
	close $fh || _die(join(' ',@args));
	
	eval {
		require XML::Simple;
		import XML::Simple;
	};
	_die "unable to load XML::Simple!\n" if ($@);
	
	$log = XMLin( $filename,
		ForceArray => ['path','revision','logentry'],
		KeepRoot => 0,
		KeyAttr => { logentry => "+revision", paths => "+path" }
		);
	unlink $filename;

	return $log->{logentry};
}

sub open_log {
	my @revs;
	my $log = open_log_hr(@_);
	
	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;
}


# arch-id methods:

sub file_to_id ($) { dirname($_[0]).'/.arch-ids/'.basename($_[0]).'.id' }
sub dir_to_id ($) { $_[0].'/.arch-ids/=id' }
sub any_to_id ($) {(!-l $_[0] && -d _) ? dir_to_id($_[0]) : file_to_id($_[0])}
sub arch_rm_id ($) { unlink(any_to_id(shift)) }

sub id_to_filename {
	my $new = $_[0];
	unless ($new =~	s/(\/?)\.arch-ids\/(.*)\.id$/$1$2/s) {
		$new =~ s/\/?\.arch-ids\/=id$//s
	}
	d "before: $_[0] => after: $new";
	return $new;
}

sub file_to_s ($) {
	my $ret = undef;
	if (open my $fd, $_[0]) { local $/; $ret = (<$fd>); close $fd }
	return $ret;
}

sub s_to_file ($$) {
	my ($str,$file) = @_;
	d "opening $file for str: $str";
	my $dn = dirname($file);
	mkpath($dn) unless (-d $dn);
	open(IDFD, '>', $file) || _die "$@ $! ".__func__.": $str, $file";
	print IDFD $str || _die "$@ $! ".__func__.": $str, $file";
	close IDFD || _die "$@ $! ".__func__.": $str, $file";
}

sub read_id ($) { file_to_s(any_to_id(shift)) }
sub read_id_rm ($) {
	my $id = any_to_id(shift);
	my $ret = file_to_s($id);
	unlink $id if (defined $ret);
	return $ret;
}

sub read_id_tree ($) {
	my $ret;
	find({	no_chdir => 1,
		wanted => sub {
				if (my $id = read_id($_)) {
					$ret->{$_} = $id
				}
			}
		},shift);
	return $ret;
}

sub read_id_tree_rm ($) {
	d "read_id_tree_rm($_[0])";
	my $ret = read_id_tree($_[0]);
	my @rm;
	find( sub {
			push @rm, $File::Find::name if (-d && /^\.arch-ids\z/)
		}, $_[0]);
	rmtree(\@rm);
	return $ret;
}

# XXX this will break when a file has been split into multiple parts
sub find_revision_added {
	my ($map,$path,$full) = @_;
	my $x;
	# yes, I mean '=' (assignment) here:
	if (($x = $map->{A}->{$path}) || ($x = $map->{R}->{$path})) {
		if (exists $x->{'copyfrom-path'}) {
			if ($path eq $$full) {
				$$full = $x->{'copyfrom-path'};
			} else {
				$$full =~ s/^$path/$x->{'copyfrom-path'}/;
			}
		} else {
			return 1;
		}
	}
	return 0;
}

# traverse history of a file and find the UUID we would've generated
# if it had been a full mirror
sub historical_uuid ($$$) {
	my ($email, $rel_path, $file) = @_;
	my @revs = open_log($file);
	my $full = "$rel_path/$file";
	foreach my $r (@revs) {
		my $map = build_cset_map(undef,@{$r->{paths}->{path}});
		if ($map) {
			my $path = $full;
			while ($path ne '/') {
				if (find_revision_added($map,$path,\$full)) {
					$full =~ s#^\/+##g;
					my $uuid = $r->{author}." $email ".
							fudgedate($r->{date}).
							' /'.$full.' @r'.
							$r->{revision}."\n";
					d "uuid($full): $uuid";
					return $uuid;
				}
				$path = dirname($path);
			}
		}
	}
	_die "unable to determine uuid for `$full' based on history!\n";
}

# like tla add-id, but generates a uuid on our terms suitable for merging
# between independently created mirrors
sub add_id ($$$$$) {
	my ($my_id,$df,$rel_path,$new,$r) = @_;
	my ($func, $line) = ((caller 1)[3], (caller 0)[2]);
	d $func,':',$line,': ',"add_id($rel_path, $new) called";

	$new =~ s#/+#/#go; # remove extra slashes
	$new =~ s#^/##go;  # remove prefix
	my $id = (!-l $new && -d _) ? dir_to_id($new) : file_to_id($new);
	unless (-e $id) {
		d "add_id: '$id'";
		my $dir_id = dirname($id);
		mkpath $dir_id unless (-d $dir_id);
		if ($_o{ids_from} && (-f "$_o{ids_from}/$id")) {
			copy "$_o{ids_from}/$id", $id;
		} else {
			$rel_path =~ s#/+#/#go;
			$rel_path =~ s#^/##go;
			my $uuid;
			if (0 && $_o{revision} && ($_o{revision} == $r) &&
					($mode =~ /^init/)) {
				my ($email) = ($my_id =~ m/(<[^>]+>)/);
				$uuid = historical_uuid($email,$rel_path,$new);	
			} else {
				$uuid = "$my_id $df /$rel_path/$new \@r$r\n";
 			}
			open my $fd, '>', $id
					|| _die "$@ $! add_id: $new, $id\n";
			print $fd $uuid;
			close $fd;
		}
	}
}

sub parse_svn_up ($$$$$$) {
	my ($my_id,$rel_path,$r,$tmethod,$map,$df) = @_;
	my $die_on_error = 1;
	my $mod_only = 1;
	my %excl_id;
	my (@mod, @to_add, @moved_ids);
	
	svn_read_version();
	
	open my $svn,"svn up $ignore_ext -r $r->{revision} |"
			|| _die "$! svn up $ignore_ext -r $r->{revision}";
	while (<$svn>) {
		next if ($tmethod eq 'names');
		chomp;
		d "r",$r->{revision},": ", $_;
		if (/^A$extra_re(.*)$/o) {
			my $new = $1;
			$mod_only = 0;
			if (defined $map->{$new}) {
				d "moved_ids: $new";
				push @moved_ids, $new;
			} elsif ($new =~ /$arch_exclude/) {
				d "arch_exclude checked in: $new";
				if ($new =~ /$arch_id/) {
					$excl_id{file_to_s($new)} = $new;
					dd \%excl_id;
				}
			} else {
				d "to_add: $new";
				push @to_add, $new;
			}
		} elsif (/^D$extra_re(.*)$/o) {
			my $del = $1;
			$mod_only = 0;
			if (/\.arch-ids\/\S+/) {
				my $f = id_to_filename($del);
				d "archid: $del ($f)";
				if (defined $map->{$f}) {
					push @moved_ids, $f
				}
			} else {
				d "normal delete: $del";
			}
		} elsif (/^U$extra_re(.*)$/) {
			push @mod, $1 if $mod_only;
		} elsif (/^(G|C)$extra_re/) {
			e $_;
			_die "Conflict or merge not expected!";
		} elsif ((my ($res) = (/^Restored '(.*)'$/))
						&& ($1 =~ /$arch_id/)) {
			d "restored $res";
			$excl_id{file_to_s($res)} = $res;
			dd \%excl_id;
		}
	}
	close $svn || _die "$@ $! svn up $ignore_ext -r $r->{revision} died!\n";
	
	# TODO figure out why this is needed:
	sys("svn up $ignore_ext -r $r->{revision}");

	dd $map;
	foreach my $file (keys %$map) {
		next unless (-e $file);
		if (my $id = $map->{$file}) {
			if (my $dangling_id = $excl_id{$id}) {
				w "somebody intentionally put a dangling_id: ",
						$dangling_id,
						" in the svn tree!";
				$_o{strict_ignore} = $r->{revision};
				push @to_add, $file;
			} else {
				d "$file =gets= $id";
				s_to_file($id, any_to_id($file));
			}
		}
	}
	foreach (@to_add) {
		add_id($my_id,$df,$rel_path,$_,$r->{revision}) if (-e $_);
	}
	
	@mod = () unless $mod_only;
	return \@mod;
}

# a somewhat reasonable course of action, or not....
sub handle_svn_externals ($$$$) {
 	my ($my_id,$rel_path,$df,$r) = @_;
	# add all externals 
	my $added = add_all_untagged($my_id,$rel_path,$df,$r,1);
	my $deleted = rm_dangling_ids();
	return ($added,$deleted);
}

sub merge_ids_to($$) {
	my ($dest,$tmp) = @_;
	my $dest_id = "$dest/.arch-ids";
	mkdir ($dest_id) unless (-d $dest_id);
	foreach my $id (<$tmp/*>) {
		my $bn = basename($id);
		d "$id => $dest_id/$bn";
		copy $id, "$dest_id/$bn" || _die $!
	}
}

sub _assert_replay_up_misses ($$$) {
	my ($add,$rm,$mods) = @_;
	if (@$add || @$rm) {
		if ($_o{strict}) {
			e "missed rm:\nD  ",join("\nD  ",@$rm) if (@$rm);
			e "missed add:\nA  ", join("\nA  ",@$add) if (@$add);
			if (my $r = $_o{strict_ignore}) {
				w "--strict ignored because revision r$r",
					" added dangling ids";
			} else {
				_die;
			}
		}
		@$mods = ();
	} else {
		# svn user cleaned up their tree
		delete($_o{strict_ignore}) if (exists $_o{strict_ignore});
	}
	foreach (@$mods) {
		if (-B $_) {
			@$mods = ();
			last;
		}
	}
}

# this is the meaty function where stuff happens
# (ignored unless we're doing sync-nested),
# rest of the args (@_) is an array of hashrefs from svn log -v --xml
sub replay_up {
	my $email = svn_info_uid_email();
	my $tmethod = get_tagging_method();
	while (my $r = pop @_) {
		my $info = svn_info();
		my $rel_path = svn_info_rel_path($info);
		$rel_path =~ s/^[\.\/]+//g;
		dd $r;
		dd $info;
		my $cur = current_svn_rev($info);
		next if ($cur == $r->{revision});
		
		my $df = fudgedate($r->{date});
		my $my_id = "$r->{author} <$email>";
		arch_my_id($my_id);
	
		# create the file movement map, this is the hardest
		# and trickiest part:
		my $map = map_moved_ids($rel_path,@{$r->{paths}->{path}});
		my $mods = parse_svn_up($my_id,$rel_path,$r,$tmethod,$map,$df);

		my ($add,$rm) = handle_svn_externals($my_id,$rel_path,
							$df,$r->{revision});
		_assert_replay_up_misses($add,$rm,$mods);
	
		arch_tree_lint();
		svn_tree_lint();
		
		my $log = do_log($r,$info);

		p "r$r->{revision}: $log->{summary}";
		my @commit = ('datefudge',$df,$tla,'commit','-l',$log->{file});
		push(@commit, '--', @$mods) if (@$mods);
		sys(@commit);
	}
}

sub cd {
	unless (chdir $_[0]) {
		w "$!: Failed to chdir $_[0]";
		system('svn','info');
		cluck;
		_die "really _die on a bad chdir\n";
	}
	return 1;
}

my %rel_path_cache;

# svn_info_rel_path: get the path of the current working copy
# relative to the svn root
sub svn_info_rel_path {
	my $info = shift || svn_info();
	my $rev = $info->{'Last Changed Rev'};
	my $url = $info->{URL};
	$url =~ s#^([a-z\+]+://)##o;
	my $method = $1 || _die "unable to extract method:// from $url!\n";
	my $rel_path = $url;
	my ($key, @keys);
	while (1) {
		if ($url =~ m#/#o) {
			my $tmp = dirname($url);
			$key = $method.$tmp;
			if (my $cached_rel_path = $rel_path_cache{$key}) {
				d "cached_rel_path: ",$cached_rel_path;
				return $cached_rel_path;
			}
			my $x = "svn ls --non-interactive -r$rev $key";
			d "system: $x";
			if (0 == system("$x >/dev/null 2>&1")) {
				$url = $tmp;
				push (@keys, $key);
			} else {
				last;
			}
		} else {
			last;
		}
	}
	$rel_path =~ s/^\Q$url\E//;
	d "rel_path: ",$rel_path;
	$rel_path_cache{$keys[0]} = $rel_path if (@keys);
 	return $rel_path;
}

# filters out a rel_path (relative path from the current tree to
# the root of the repository, also strips extraneous slashes
sub filter {
	my ($filter,$path) = @_;
	$path =~ s/$filter//;
	$path =~ s#/+#/#go;
	$path =~ s#^/+##o;
	return $path;
}

# build a simple hashref map of the changeset we're about to apply
sub build_cset_map ($@) {
	my $rel_path = shift;
	my $map;
	dd \@_; # @{$r->{paths}->{path}}
	foreach my $p (@_) {
		my $action = $p->{action};
		my $content = defined $rel_path ?
				filter($rel_path,$p->{content}) : $p->{content};
		next if (($action ne 'D') && ($content =~ /$arch_exclude/));
		if (($action eq 'M') && (-d $content)) {
			w "Modification on directory: $content. ",
					'svn:externals are not supported, ',
					'other metadata changes are ignored';
			$map->{$action}->{$content} = $p;
		}
		next unless ($action =~ /^A|D|R$/);
		if ($map->{$action}->{$content}) {
			# this should never happen:
			w "PANIC! duplicate data: ",
				"$action: $content\n",
				__func__,': ',Dumper(@_),"\ncurrent: ",
				Dumper($map->{$action}->{$content}),
				"\nThis should never happen";
			quit(1);
		}
		$map->{$action}->{$content} = $p;
	}
	return $map;
}

sub merge_hashes ($$) {
	my ($from,$to) = @_;
	foreach my $k (keys %$from) {
		if (defined($from->{$k})) {
			my $v = $from->{$k};
			if (defined($to->{$k}) && ($to->{$k} ne $v)) {
				w "$k => $to->{$k} clobbered by $v!"
			}
			$to->{$k} = $v;
		}
	}
}

# handle the case where the following happens in the same changeset:
#   doo/AUTHORS => AUTHORS
#   doo/ => doc/
sub chase_nested_mv ($$$$) {
	my ($from,$to,$map,$ret) = @_;
	my ($from_dn, $from_bn) = (dirname($from), basename($from));
	d "from_dn: $from_dn";
	my ($last, $break) = ('.', 0); 
	while (!$break && ($from_dn ne $last)) {
		my $new_from = "$from_dn/$from_bn";
		d "new_from: $new_from";
		if (exists $map->{D}->{$from_dn}) {
			d "exists \$map->{D}->{$from_dn}";
			if (-e $new_from) {
				d "-e $new_from";
				if (defined $ret->{$new_from}) {
					$ret->{$to} = $ret->{$new_from};
					delete($ret->{$new_from});
				} elsif (my $read_id = read_id($new_from)) {
					$ret->{$to} = $read_id
				}
				if (defined $ret->{$to}) {
					arch_rm_id($new_from);
					d "\$ret->{$to} = $ret->{$to}"; 
				}
				$break = 1;
			}
		} else {
			$last = $from_dn;
			$from_dn = dirname($from_dn);
		}
	}
	w "copy $from => $to history will be lost" unless ($break);
}

# action => 'A' or 'R'
sub map_added_replaced ($$$$) {
	my ($rel_path,$action,$map,$ret) = @_;
	foreach my $file (sort keys %{$map->{$action}}) {
		if (($action eq 'R') && -d $file) {
			d "replace move -d $file";
			merge_hashes(read_id_tree_rm($file),$ret);
			d "\$ret->{$file} = $ret->{$file}"; 
		}
		next unless (my $copy_from =
				$map->{$action}->{$file}->{'copyfrom-path'});
		# moved file: move-id, too
		my $to = $file; # this one is already filtered)
		my $from = filter($rel_path,$copy_from);
		if (!-l $from && -d _) {
			d 'dir: before: '; dd $ret;
			my $tmp = read_id_tree_rm($from);
			foreach (keys %$tmp) {
				my $new = $_;
				$new =~ s/$from/$to/;
				$tmp->{$new} = $tmp->{$_};
				delete($tmp->{$_});
			}
			merge_hashes($tmp,$ret);
			d ' after: '; dd $ret;
		} elsif (-e $from) {
			d "-e $from";
			if (exists $map->{D}->{$from}) {
				d "rename D ($from => $to) $map->{D}->{$from}";
				$ret->{$to} = read_id_rm($from) || _die;      
				d "\$ret->{$to} = $ret->{$to}"; 
			} else {
				chase_nested_mv($from,$to,$map,$ret);
			}
		} else {
			w "out-of-tree $action: $from => $to";
		}
	}
}

# basically, we pre-read the svn log info and already know what svn is going
# to do to our working tree on update; but we still have to let svn update
# run while keeping our arch directories intact
sub map_moved_ids {
	my $rel_path = shift;
	my $map = build_cset_map($rel_path,@_); # @_ = @{$r->{paths}->{path}}
	dd $map;
	my $ret = {};
	# deal with added files first
	map_added_replaced($rel_path,'A',$map,$ret);
	map_added_replaced($rel_path,'R',$map,$ret);
	foreach my $c (keys %{$map->{D}}) {
		d "D: $c";
		if (-d $c && -d "$c/.arch-ids") {
			d "merge hashes <$c> (normal)";
			merge_hashes(read_id_tree_rm($c),$ret);
		} elsif ($c =~ /\.arch-ids$/) {
			my $ax = $c;
			$ax =~ s/\.arch-ids$//;
			d "merge hashes <$c> (.arch-id) .. not fun $ax";
			# not really deleted, somebody committed an .arch-ids
			# and didn't mean to delete, preserve the ids
			dd $ret;
			merge_hashes(read_id_tree($ax),$ret);
			dd $ret;
		} else {
			unlink any_to_id($c);
		}
	}
	return $ret;
}

# turns the uuid info of the repo into pseudo-email address
sub svn_info_uid_email {
	my $info = shift || svn_info();
	my $domain = $info->{'Repository UUID'};
	_die "No UUID info!" unless ($domain);
	return "svn-arch-mirror\@$domain";
}

# do_log: writes a changelog in the current directory based on the rev
# info it's given
sub do_log {
	my $r = shift;
	my $info = shift || svn_info();
	my $log = '++log.svn-arch-mirror.'.$r->{revision};

	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; # put a space in for header-formatting
	}
	my $ret = {
		summary => $sum,
		file => $log,
	};
	print $msg "Summary: r$r->{revision}: $sum\n",
		"Keywords: r$r->{revision}\n\n";

	print $msg "Message:\n$r->{msg}\n" if (ref($r->{msg}) eq '');
	foreach (@{$info->{-order}}) { print $msg "$_: $info->{$_}\n" }
	close $msg;
	return $ret;
}

# abrowse_revs: abrowse and find an Arch revision that matches a
# corresponding SVN rev
sub abrowse_revs {
	my $from = shift;
	if (my $filter = shift) {
		$from =~ s/^([^\-]+)--.*/$1/;
	}
	my $ret;
	open my $t, "$tla abrowse -s -f $from |" 
			|| _die "$!: $tla abrowse $from";
	my $prev;
	while (<$t>) {
		# dirty secret: branch is optional in both baz and 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 $t;
	_die "No revisions in $from" unless $ret;
	return $ret;
}

# removes all leftover ids as a fallback
sub rm_dangling_ids {
	my @deleted;
	while (chomp(my @rm = `$tla $lintcmd -m`)) {
		foreach my $x (@rm) {
			push @deleted, $x;
			# escape backslashes since `` expands
			$x =~ s#\\#\\\\#g;
			# yes, $x should be double quoted:
			unlink `$tla escape --unescaped "$x"`;
		}
		d '++D ', join("\n++D ",@rm);
	}
	return \@deleted;
}

# current_svn_rev: grab the current svn revision info
sub current_svn_rev {
	my $info = shift || svn_info();
	my $ret = $info->{Revision};
	_die "No revision!" unless ($ret=~/^\d+$/);
	return $ret;
}

# current_arch_rev: grab the current arch revision info
sub current_arch_rev () {
	open (my $lr, "$tla logs -rf |") || _die "$!: $tla logs -rf";
	my $cur;
	while (<$lr>) {
		if (my $cur = $_) {
			chomp $cur;
			close $lr;
			return $cur;
		}
	}
	_die "Failed to get Arch revision info!\n";
}

# open_revision: get log entry info for any given revision
sub open_revision {
	my $rev = shift;
	my @revs;
	
	# fall down to the next lowest revision:
	while (($rev =~ /^\d+$/) && ($rev > 0)
			&& !(@revs = open_log("-r$rev"))) {
		$rev--;
	}
	my $r = pop @revs;
	unless ($r->{revision} =~ /^\d+$/) {
		dd $r,\@revs;
		_die "Bad revision: $r->{revision}\n"
	}
	return $r;
}

# 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 {
	# remove datefudge <date> calls if we don't have datefudge
	if (!$_o{datefudge} && ($_[0] eq 'datefudge')) {
		shift; shift
	}
	if ($ENV{SVN_ARCH_MIRROR_DEBUG}) {
		my ($func, $line) = (   ((caller 1)[3] || 'CORE::main'),
					(caller 0)[2]);
		p 'sys: ',$func,':',$line,": '",join("' '",@_),"'";
	}
	unless (system(@_) == 0) {
		e "$!: '".join("' '",@_)."'\nPWD: ",getcwd,"\n";
		system('svn','info');
		cluck;
		quit(1);
	}
}

sub init_arch_tree {
	set_tmp_home();	
	chomp(my $treeversion = (-d '{arch}') ? '' : shift);
	my $first = $_o{revision} ? open_revision($_o{revision})
				  : first();

	svn_read_version();
	# since open_revision() fudges it
	$_o{revision} = $first->{revision};
	sys("svn up $ignore_ext -r $first->{revision}");
	
	# we can allow the user to set up their own tagging method
	unless (-d '{arch}') {
		_die "No treeversion supplied!\n" unless ($treeversion);

		# check for tree-version using the arch client itself, since
		# tla and baz differ
		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# Added by svn-arch-mirror ",
				"(too many people have .cvsignore in svn):\n",
				'source ^\..*$',"\n"; 
		close $tm;

	}
	my $tmethod = get_tagging_method();
	my $info = svn_info();
	my $email = svn_info_uid_email($info);
	my $my_id = exists($first->{author}) ? "$first->{author} <$email>"
					: "(no author) <$email>";
	my $rel_path = svn_info_rel_path($info);
	arch_my_id($my_id);
	my $df = fudgedate($first->{date});
	
	if ($tmethod eq "explicit") {
		add_all_untagged($my_id,$rel_path,$df,$first->{revision},1);
	}

	arch_tree_lint();
	svn_tree_lint();
	
	my $log = do_log($first,$info);
	
	p "r$first->{revision}: $log->{summary}";
	arch_import($df,$log->{file});
	
	update_tree('.') unless ($_o{import_only});
}

sub find_nested_trees () {
	my @ret;
	find(sub { if (-d && /\{arch\}/ &&
				($File::Find::name !~ /$arch_ignore_re/)) {
			return unless (-d '.svn');
			my $dir = dirname($File::Find::name);
			push @ret, $dir unless ($dir eq '.');
		   }
	         }, '.');
	dd \@ret;
	return @ret;
}

sub find_nested {
	foreach my $d (find_nested_trees()) {
		if ($_o{config}) {
			print $d,'        ',`$tla tree-version $d`
		} else {
			p $d
		}
	}
}

sub sync_nested {
	if ($_o{one_repo}) {
		if (-d '.svn') {
			# shortcut out if we're handling an entire archive
			# and there are no updates
			if (my @revs = open_log('-rBASE:HEAD')) {
				my $r = shift @revs;   # top revision

				my $i = svn_info();
				# no updates!
				return if ($r->{revision} == $i->{Revision});
			} else {
				return;
			}
		} else {
			w "\.svn directory not found for --one-repo! ",
					"Continuing..."
		}
	}
	set_tmp_home();
	foreach my $d (find_nested_trees()) {
		update_tree($d);
	}
}

sub sync_one {
	set_tmp_home();
	update_tree(@_);
}

sub init_branch {
	my ($from,$to) = @_;
	
	# these checks aren't very strict, we'll let the arch client do
	# final checking
	if (!$from || $from !~ /^(?:.+)--(?:.+)/) {
		e "Bad FROM revision: $from";
		usage(1); 
	}
	if (!$to || $to !~ /^(?:.+)--(?:.+)/) {
		e "Bad TO revision: $to";
		usage(1);
	}
	
	p "tagging FROM: $from  TO: $to";

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

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

sub double_get {
	my ($arch_rev,$svn_rev,$dest);
	$arch_rev = shift || _die "Need Arch package name!\n";
	$dest = shift || undef;
	$svn_rev = $_o{revision} || undef;
	
	if ($arch_rev !~ /\//) {
		$arch_rev = $_o{archive}."/$arch_rev"
	}

	my $x = abrowse_revs($arch_rev,0);
	_die "$arch_rev is not a valid Arch package name!\n" if (!$x);

	if (!$svn_rev) {
		if ($arch_rev !~ /--(?:patch|version(?:fix)?|base)-\d+$/) {
			# make sure it's the latest rev unless specified:
			$arch_rev = $x->{(sort {$b <=> $a} (keys(%$x)))[0]};
		}
		my %rmap = map { $x->{$_} => $_ } keys %$x;
		$svn_rev = $rmap{$arch_rev} if (exists $rmap{$arch_rev});
	} else {
		if (defined $x->{$svn_rev}) {
			$arch_rev = $x->{$svn_rev};
		} else {
			_die "invalid svn revision: $!\n";
		}
	}
	
	_die "No svn revision specified!\n" unless ($svn_rev);
	if (!defined $dest) {
		my @tmp = split(/\//,$arch_rev);
		$dest = "$tmp[$#tmp]+svn.r$svn_rev";
	}
	sys($tla,'get',$arch_rev,$dest);
	my ($svn_url) = (`$tla cat-archive-log $arch_rev`
	                      =~ /(?:^|\n)URL:\s+(\S+)\n/s);
	my $svn_tmp = ',,tmp.'.time.".$$.$dest";
	$svn_tmp =~ s/\//--/g;
	$svn_tmp =~ s/\.+/./g;
	sys('svn','co','-r',$svn_rev,$svn_url,$svn_tmp);
	my @tmp;
	find(sub{-d && /^\.svn\z/s && push @tmp, $File::Find::name},$svn_tmp);
	foreach my $f (@tmp) {
		my $t = $f;
		$t =~ s/\Q$svn_tmp\E/$dest/;
		my $dn = dirname($t);
		mkpath($dn) unless (-d $dn);
		cp_d($f,$t);
	}
	rmtree([$svn_tmp]);
	p "$arch_rev stored to at $dest"
}

__END__

=head1 NAME
 
svn-arch-mirror - one-way mirror from a Subversion tree 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 -c, --arch-client

Specify an Arch client, either 'tla' (default) or 'baz'

=item -A, --archive

Override `tla my-default-archive'

=item -d, --dir

Switch to target directory before executing

=item -l, --revision-limit

Stop tracking at this SVN revision number.  This can also be specified
with the -r option as -r<revision>:<revision-limit>

=item --no-datefudge

Don't use the datefudge command to mirror and preserve changeset dates.  Use
this if you don't have datefudge installed on your machine.

=item --no-my-id-switch
	
Disable switching the user id.  The Creator: field of a commit log will use
the normal output of my-id, and not reflect the original svn committer's 
name.

=item --no-tmp-home

Disable using a temporary $HOME directory.  A temporary $HOME directory is
used by svn-arch-mirror to prevent the user's main tla/baz my-id from being
clobbered.  Use this to enable svn-arch-mirror to work with signed archives.

=item --no-lint

Don't run tla tree-lint or baz lint between commits.  This is needed in cases
where the tree you are tracking has symlinks pointing to non-existent files.

=item --sign

An alias for --no-my-id-switch --no-tmp-home

=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).

=over 8

=item -r, --revision

Start tracking a Subversion tree at this revision.  Takes a Subversion
revision number.

=item --import-only

Don't automatically run 'sync' after the initial import when a tree is
initialized

=back

=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.

=item B<get> (category--branch(--version(--revision)))

Constructs a project tree for a given category--branch(--version(--revision))

=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 -r, --revision

Override auto-detection of branch ancestor and tag from this Arch
revision instead.  Takes an Arch category--branch--version.

=back

=item B<find-nested>

Like sync-nested, but only shows you the location of the nested-trees
and does not update them.

=over 8

=item --config

Print an Arch multi-tree config of the current directory structure
to stdout.

=back

=item B<sync-nested>
    
Same as sync, but it will recursively seek out nested trees, making it
ideal for tracking an entire Subversion repository as opposed to one
sub-project.  This will not update the current tree if you are in one.

=over 8

=item -1, --one-repo

If sync-nested is being run in a directory that contains working trees from
only one repository, and itself is a Subversion working copy (usually the
top-level root of the repository), then avoid extra network traffic if
no changes are detected.

=back

=cut

=head1 EXAMPLES

To start tracking a new working directory inside a 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

To mirror a directory that has been renamed (copied and deleted in SVN)
at revision 400:

  svn co <OLD_URL> directory1 -r399
  cd directory1
  svn-arch-mirror init -l 399 <cat>--<b>--<version1>
  cd ..
  svn co <NEW_URL> directory2 -r400
  cd directory2
  svn-arch-mirror init -r 400 --ids-from ../directory1 <cat>--<b>--<version2>

* In the future, I hope to automate this process

=head1 SEE ALSO

B<svn help>, B<tla help>, B<baz 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.

The design is tended towards keeping individual working trees
(trunks/tags/branches) mirrored in Arch, keeping an entire repository
in Arch is possible (what sync-nested is for, but the individual
working trees must be manually initialized.)
 
=head1 AUTHOR

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

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

=cut

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

