$GIT_SVN_INDEX $GIT_SVN
$GIT_DIR $REV_DIR/;
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
-$VERSION = '0.10.0';
-$GIT_DIR = $ENV{GIT_DIR} || "$ENV{PWD}/.git";
+$VERSION = '1.0.0';
+
+use Cwd qw/abs_path/;
+$GIT_DIR = abs_path($ENV{GIT_DIR} || '.git');
+$ENV{GIT_DIR} = $GIT_DIR;
+
# make sure the svn binary gives consistent output between locales and TZs:
$ENV{TZ} = 'UTC';
$ENV{LC_ALL} = 'C';
my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
$_find_copies_harder, $_l, $_version, $_upgrade, $_authors);
my (@_branch_from, %tree_map, %users);
+my $_svn_co_url_revs;
my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext,
'branch|b=s' => \@_branch_from,
my %cmd = (
fetch => [ \&fetch, "Download new revisions from SVN",
{ 'revision|r=s' => \$_revision, %fc_opts } ],
- init => [ \&init, "Initialize and fetch (import)", { } ],
+ init => [ \&init, "Initialize a repo for tracking" .
+ " (requires URL argument)", { } ],
commit => [ \&commit, "Commit git revisions to SVN",
{ 'stdin|' => \$_stdin,
'edit|e' => \$_edit,
my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
+# convert GetOpt::Long specs for use by git-repo-config
+foreach my $o (keys %opts) {
+ my $v = $opts{$o};
+ my ($key) = ($o =~ /^([a-z\-]+)/);
+ $key =~ s/-//g;
+ my $arg = 'git-repo-config';
+ $arg .= ' --int' if ($o =~ /=i$/);
+ $arg .= ' --bool' if ($o !~ /=[sfi]$/);
+ if (ref $v eq 'ARRAY') {
+ chomp(my @tmp = `$arg --get-all svn.$key`);
+ @$v = @tmp if @tmp;
+ } else {
+ chomp(my $tmp = `$arg --get svn.$key`);
+ if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) {
+ $$v = $tmp;
+ }
+ }
+}
+
GetOptions(%opts, 'help|H|h' => \$_help,
'version|V' => \$_version,
'id|i=s' => \$GIT_SVN) or exit 1;
$GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index";
-$ENV{GIT_DIR} ||= $GIT_DIR;
$SVN_URL = undef;
$REV_DIR = "$GIT_DIR/$GIT_SVN/revs";
$SVN_WC = "$GIT_DIR/$GIT_SVN/tree";
version() if $_version;
usage(1) unless defined $cmd;
load_authors() if $_authors;
-svn_check_ignore_externals();
+svn_compat_check();
$cmd{$cmd}->[0]->(@ARGV);
exit 0;
# if we merged or otherwise started elsewhere, this is
# how we break out of it
next if (defined $SVN_UUID && ($uuid ne $SVN_UUID));
- next if (defined $SVN_URL && ($url ne $SVN_URL));
+ next if (defined $SVN_URL && defined $url && ($url ne $SVN_URL));
print "r$rev = $c\n";
unless (defined $latest) {
croak "SVN repository location required: $url\n";
}
$SVN_URL ||= $url;
- $SVN_UUID ||= setup_git_svn();
+ $SVN_UUID ||= $uuid;
+ setup_git_svn();
$latest = $rev;
}
assert_revision_eq_or_unknown($rev, $c);
}
close $rev_list or croak $?;
if (!chdir $SVN_WC) {
- my @svn_co = ('svn','co',"-r$latest");
- push @svn_co, '--ignore-externals' unless $_no_ignore_ext;
- sys(@svn_co, $SVN_URL, $SVN_WC);
+ svn_cmd_checkout($SVN_URL, $latest, $SVN_WC);
chdir $SVN_WC or croak $!;
}
}
sub init {
- $SVN_URL = shift or croak "SVN repository location required\n";
+ $SVN_URL = shift or die "SVN repository location required " .
+ "as a command-line argument\n";
unless (-d $GIT_DIR) {
sys('git-init-db');
}
push @log_args, '--stop-on-copy' unless $_no_stop_copy;
my $svn_log = svn_log_raw(@log_args);
- @$svn_log = sort { $a->{revision} <=> $b->{revision} } @$svn_log;
- my $base = shift @$svn_log or croak "No base revision!\n";
+ my $base = next_log_entry($svn_log) or croak "No base revision!\n";
my $last_commit = undef;
unless (-d $SVN_WC) {
- my @svn_co = ('svn','co',"-r$base->{revision}");
- push @svn_co,'--ignore-externals' unless $_no_ignore_ext;
- sys(@svn_co, $SVN_URL, $SVN_WC);
+ svn_cmd_checkout($SVN_URL,$base->{revision},$SVN_WC);
chdir $SVN_WC or croak $!;
+ read_uuid();
$last_commit = git_commit($base, @parents);
assert_svn_wc_clean($base->{revision}, $last_commit);
} else {
chdir $SVN_WC or croak $!;
+ read_uuid();
$last_commit = file_to_s("$REV_DIR/$base->{revision}");
}
my @svn_up = qw(svn up);
push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
- my $last_rev = $base->{revision};
- foreach my $log_msg (@$svn_log) {
- assert_svn_wc_clean($last_rev, $last_commit);
- $last_rev = $log_msg->{revision};
- sys(@svn_up,"-r$last_rev");
+ my $last = $base;
+ while (my $log_msg = next_log_entry($svn_log)) {
+ assert_svn_wc_clean($last->{revision}, $last_commit);
+ if ($last->{revision} >= $log_msg->{revision}) {
+ croak "Out of order: last >= current: ",
+ "$last->{revision} >= $log_msg->{revision}\n";
+ }
+ sys(@svn_up,"-r$log_msg->{revision}");
$last_commit = git_commit($log_msg, $last_commit, @parents);
+ $last = $log_msg;
}
- assert_svn_wc_clean($last_rev, $last_commit);
+ assert_svn_wc_clean($last->{revision}, $last_commit);
unless (-e "$GIT_DIR/refs/heads/master") {
sys(qw(git-update-ref refs/heads/master),$last_commit);
}
- return pop @$svn_log;
+ return $last;
}
sub commit {
fetch();
chdir $SVN_WC or croak $!;
- my $svn_current_rev = svn_info('.')->{'Last Changed Rev'};
+ my $info = svn_info('.');
+ read_uuid($info);
+ my $svn_current_rev = $info->{'Last Changed Rev'};
foreach my $c (@revs) {
my $mods = svn_checkout_tree($svn_current_rev, $c);
if (scalar @$mods == 0) {
########################### utility functions #########################
+sub read_uuid {
+ return if $SVN_UUID;
+ my $info = shift || svn_info('.');
+ $SVN_UUID = $info->{'Repository UUID'} or
+ croak "Repository UUID unreadable\n";
+ s_to_file($SVN_UUID,"$GIT_DIR/$GIT_SVN/info/uuid");
+}
+
sub setup_git_svn {
defined $SVN_URL or croak "SVN repository location required\n";
unless (-d $GIT_DIR) {
mkpath(["$GIT_DIR/$GIT_SVN/info"]);
mkpath([$REV_DIR]);
s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url");
- $SVN_UUID = svn_info($SVN_URL)->{'Repository UUID'} or
- croak "Repository UUID unreadable\n";
- s_to_file($SVN_UUID,"$GIT_DIR/$GIT_SVN/info/uuid");
open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!;
print $fd '.svn',"\n";
close $fd or croak $!;
- return $SVN_UUID;
}
sub assert_svn_wc_clean {
return fetch("$rev_committed=$commit")->{revision};
}
+# read the entire log into a temporary file (which is removed ASAP)
+# and store the file handle + parser state
sub svn_log_raw {
my (@log_args) = @_;
- my $pid = open my $log_fh,'-|';
+ my $log_fh = IO::File->new_tmpfile or croak $!;
+ my $pid = fork;
defined $pid or croak $!;
-
- if ($pid == 0) {
+ if (!$pid) {
+ open STDOUT, '>&', $log_fh or croak $!;
exec (qw(svn log), @log_args) or croak $!
}
+ waitpid $pid, 0;
+ croak if $?;
+ seek $log_fh, 0, 0 or croak $!;
+ return { state => 'sep', fh => $log_fh };
+}
+
+sub next_log_entry {
+ my $log = shift; # retval of svn_log_raw()
+ my $ret = undef;
+ my $fh = $log->{fh};
- my @svn_log;
- my $state = 'sep';
- while (<$log_fh>) {
+ while (<$fh>) {
chomp;
if (/^\-{72}$/) {
- if ($state eq 'msg') {
- if ($svn_log[$#svn_log]->{lines}) {
- $svn_log[$#svn_log]->{msg} .= $_."\n";
- unless(--$svn_log[$#svn_log]->{lines}) {
- $state = 'sep';
+ if ($log->{state} eq 'msg') {
+ if ($ret->{lines}) {
+ $ret->{msg} .= $_."\n";
+ unless(--$ret->{lines}) {
+ $log->{state} = 'sep';
}
} else {
croak "Log parse error at: $_\n",
- $svn_log[$#svn_log]->{revision},
+ $ret->{revision},
"\n";
}
next;
}
- if ($state ne 'sep') {
+ if ($log->{state} ne 'sep') {
croak "Log parse error at: $_\n",
- "state: $state\n",
- $svn_log[$#svn_log]->{revision},
+ "state: $log->{state}\n",
+ $ret->{revision},
"\n";
}
- $state = 'rev';
+ $log->{state} = 'rev';
# if we have an empty log message, put something there:
- if (@svn_log) {
- $svn_log[$#svn_log]->{msg} ||= "\n";
- delete $svn_log[$#svn_log]->{lines};
+ if ($ret) {
+ $ret->{msg} ||= "\n";
+ delete $ret->{lines};
+ return $ret;
}
next;
}
- if ($state eq 'rev' && s/^r(\d+)\s*\|\s*//) {
+ if ($log->{state} eq 'rev' && s/^r(\d+)\s*\|\s*//) {
my $rev = $1;
my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3);
($lines) = ($lines =~ /(\d+)/);
/(\d{4})\-(\d\d)\-(\d\d)\s
(\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
or croak "Failed to parse date: $date\n";
- my %log_msg = ( revision => $rev,
+ $ret = { revision => $rev,
date => "$tz $Y-$m-$d $H:$M:$S",
author => $author,
lines => $lines,
- msg => '' );
+ msg => '' };
if (defined $_authors && ! defined $users{$author}) {
die "Author: $author not defined in ",
"$_authors file\n";
}
- push @svn_log, \%log_msg;
- $state = 'msg_start';
+ $log->{state} = 'msg_start';
next;
}
# skip the first blank line of the message:
- if ($state eq 'msg_start' && /^$/) {
- $state = 'msg';
- } elsif ($state eq 'msg') {
- if ($svn_log[$#svn_log]->{lines}) {
- $svn_log[$#svn_log]->{msg} .= $_."\n";
- unless (--$svn_log[$#svn_log]->{lines}) {
- $state = 'sep';
+ if ($log->{state} eq 'msg_start' && /^$/) {
+ $log->{state} = 'msg';
+ } elsif ($log->{state} eq 'msg') {
+ if ($ret->{lines}) {
+ $ret->{msg} .= $_."\n";
+ unless (--$ret->{lines}) {
+ $log->{state} = 'sep';
}
} else {
croak "Log parse error at: $_\n",
- $svn_log[$#svn_log]->{revision},"\n";
+ $ret->{revision},"\n";
}
}
}
- close $log_fh or croak $?;
- return \@svn_log;
+ return $ret;
}
sub svn_info {
}
}
+sub trees_eq {
+ my ($x, $y) = @_;
+ my @x = safe_qx('git-cat-file','commit',$x);
+ my @y = safe_qx('git-cat-file','commit',$y);
+ if (($y[0] ne $x[0]) || $x[0] !~ /^tree $sha1\n$/
+ || $y[0] !~ /^tree $sha1\n$/) {
+ print STDERR "Trees not equal: $y[0] != $x[0]\n";
+ return 0
+ }
+ return 1;
+}
+
sub assert_revision_eq_or_unknown {
my ($revno, $commit) = @_;
if (-f "$REV_DIR/$revno") {
my $current = file_to_s("$REV_DIR/$revno");
- if ($commit ne $current) {
+ if (($commit ne $current) && !trees_eq($commit, $current)) {
croak "$REV_DIR/$revno already exists!\n",
"current: $current\nexpected: $commit\n";
}
my ($log_msg, @parents) = @_;
assert_revision_unknown($log_msg->{revision});
my $out_fh = IO::File->new_tmpfile or croak $!;
- $SVN_UUID ||= svn_info('.')->{'Repository UUID'};
map_tree_joins() if (@_branch_from && !%tree_map);
}
my @update_ref = ('git-update-ref',"refs/remotes/$GIT_SVN",$commit);
if (my $primary_parent = shift @exec_parents) {
- push @update_ref, $primary_parent;
+ $pid = fork;
+ defined $pid or croak $!;
+ if (!$pid) {
+ close STDERR;
+ close STDOUT;
+ exec 'git-rev-parse','--verify',
+ "refs/remotes/$GIT_SVN^0";
+ }
+ waitpid $pid, 0;
+ push @update_ref, $primary_parent unless $?;
}
sys(@update_ref);
sys('git-update-ref',"$GIT_SVN/revs/$log_msg->{revision}",$commit);
return wantarray ? @ret : join('',@ret);
}
-sub svn_check_ignore_externals {
- return if $_no_ignore_ext;
- unless (grep /ignore-externals/,(safe_qx(qw(svn co -h)))) {
+sub svn_compat_check {
+ my @co_help = safe_qx(qw(svn co -h));
+ unless (grep /ignore-externals/,@co_help) {
print STDERR "W: Installed svn version does not support ",
"--ignore-externals\n";
$_no_ignore_ext = 1;
}
+ if (grep /usage: checkout URL\[\@REV\]/,@co_help) {
+ $_svn_co_url_revs = 1;
+ }
+
+ # I really, really hope nobody hits this...
+ unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) {
+ print STDERR <<'';
+W: The installed svn version does not support the --stop-on-copy flag in
+ the log command.
+ Lets hope the directory you're tracking is not a branch or tag
+ and was never moved within the repository...
+
+ $_no_stop_copy = 1;
+ }
+}
+
+# *sigh*, new versions of svn won't honor -r<rev> without URL@<rev>,
+# (and they won't honor URL@<rev> without -r<rev>, too!)
+sub svn_cmd_checkout {
+ my ($url, $rev, $dir) = @_;
+ my @cmd = ('svn','co', "-r$rev");
+ push @cmd, '--ignore-externals' unless $_no_ignore_ext;
+ $url .= "\@$rev" if $_svn_co_url_revs;
+ sys(@cmd, $url, $dir);
}
sub check_upgrade_needed {
Data structures:
-@svn_log = array of log_msg hashes
+$svn_log hashref (as returned by svn_log_raw)
+{
+ fh => file handle of the log file,
+ state => state of the log file parser (sep/msg/rev/msg_start...)
+}
-$log_msg hash
+$log_msg hashref as returned by next_log_entry($svn_log)
{
msg => 'whitespace-formatted log entry
', # trailing newline is preserved