Merge branch 'se/rebase' into next
[gitweb.git] / contrib / git-svn / git-svn.perl
index 3c860e458c2894c0addf008e4f011abb68e8b562..de13a96b8a66c97fdc2e5822986ea48285fa9b6d 100755 (executable)
@@ -8,8 +8,12 @@
                $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';
@@ -30,6 +34,7 @@
 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,
@@ -37,7 +42,8 @@
 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;
 
@@ -154,7 +178,7 @@ sub rebuild {
                # 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) {
@@ -162,7 +186,8 @@ sub rebuild {
                                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);
@@ -171,9 +196,7 @@ sub rebuild {
        }
        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 $!;
        }
 
@@ -198,7 +221,8 @@ sub rebuild {
 }
 
 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');
        }
@@ -217,35 +241,38 @@ sub fetch {
        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 {
@@ -275,7 +302,9 @@ 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) {
@@ -314,6 +343,14 @@ sub show_ignore {
 
 ########################### 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) {
@@ -323,14 +360,10 @@ sub setup_git_svn {
        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 {
@@ -702,49 +735,61 @@ sub svn_commit_tree {
        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+)/);
@@ -752,36 +797,34 @@ sub svn_log_raw {
                                        /(\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 {
@@ -844,11 +887,23 @@ sub assert_revision_unknown {
        }
 }
 
+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";
                }
@@ -860,7 +915,6 @@ sub git_commit {
        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);
 
@@ -922,7 +976,16 @@ sub git_commit {
        }
        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);
@@ -995,13 +1058,37 @@ sub safe_qx {
        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 {
@@ -1064,9 +1151,13 @@ sub load_authors {
 
 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