contrib/git-svn: allow rebuild to work on non-linear remote heads
[git.git] / contrib / git-svn / git-svn.perl
index 69b6be3..f3fc3ec 100755 (executable)
@@ -4,19 +4,12 @@
 use warnings;
 use strict;
 use vars qw/   $AUTHOR $VERSION
-               $SVN_URL $SVN_INFO $SVN_WC
+               $SVN_URL $SVN_INFO $SVN_WC $SVN_UUID
                $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";
-$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";
-
 # make sure the svn binary gives consistent output between locales and TZs:
 $ENV{TZ} = 'UTC';
 $ENV{LC_ALL} = 'C';
@@ -37,6 +30,7 @@ my $sha1_short = qr/[a-f\d]{4,40}/;
 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,
@@ -67,23 +61,24 @@ for (my $i = 0; $i < @ARGV; $i++) {
        }
 };
 
-# we may be called as git-svn-(command), or git-svn(command).
-foreach (keys %cmd) {
-       if (/git\-svn\-?($_)(?:\.\w+)?$/) {
-               $cmd = $1;
-               last;
-       }
-}
+my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
 
-my %opts;
-%opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
+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";
 
-GetOptions(%opts, 'help|H|h' => \$_help, 'version|V' => \$_version ) or exit 1;
 usage(0) if $_help;
 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;
 
@@ -94,15 +89,25 @@ sub usage {
        print $fd <<"";
 git-svn - bidirectional operations between a single Subversion tree and git
 Usage: $0 <command> [options] [arguments]\n
-Available commands:
+
+       print $fd "Available commands:\n" unless $cmd;
 
        foreach (sort keys %cmd) {
+               next if $cmd && $cmd ne $_;
                print $fd '  ',pack('A13',$_),$cmd{$_}->[1],"\n";
+               foreach (keys %{$cmd{$_}->[2]}) {
+                       # prints out arguments as they should be passed:
+                       my $x = s#=s$## ? '<arg>' : s#=i$## ? '<num>' : '';
+                       print $fd ' ' x 17, join(', ', map { length $_ > 1 ?
+                                                       "--$_" : "-$_" }
+                                               split /\|/,$_)," $x\n";
+               }
        }
        print $fd <<"";
-\nGIT_SVN_ID may be set in the environment to an arbitrary identifier if
-you're tracking multiple SVN branches/repositories in one git repository
-and want to keep them separate.  See git-svn(1) for more information.
+\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an
+arbitrary identifier if you're tracking multiple SVN branches/repositories in
+one git repository and want to keep them separate.  See git-svn(1) for more
+information.
 
        exit $exit;
 }
@@ -114,7 +119,6 @@ sub version {
 
 sub rebuild {
        $SVN_URL = shift or undef;
-       my $repo_uuid;
        my $newest_rev = 0;
        if ($_upgrade) {
                sys('git-update-ref',"refs/remotes/$GIT_SVN","$GIT_SVN-HEAD");
@@ -150,8 +154,8 @@ sub rebuild {
 
                # if we merged or otherwise started elsewhere, this is
                # how we break out of it
-               next if (defined $repo_uuid && ($uuid ne $repo_uuid));
-               next if (defined $SVN_URL && ($url ne $SVN_URL));
+               next if (defined $SVN_UUID && ($uuid ne $SVN_UUID));
+               next if (defined $SVN_URL && defined $url && ($url ne $SVN_URL));
 
                print "r$rev = $c\n";
                unless (defined $latest) {
@@ -159,7 +163,8 @@ sub rebuild {
                                croak "SVN repository location required: $url\n";
                        }
                        $SVN_URL ||= $url;
-                       $repo_uuid ||= setup_git_svn();
+                       $SVN_UUID ||= $uuid;
+                       setup_git_svn();
                        $latest = $rev;
                }
                assert_revision_eq_or_unknown($rev, $c);
@@ -168,9 +173,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 $!;
        }
 
@@ -219,14 +222,14 @@ sub fetch {
        my $base = shift @$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);
@@ -252,7 +255,7 @@ sub commit {
                print "Reading from stdin...\n";
                @commits = ();
                while (<STDIN>) {
-                       if (/\b($sha1_short)\b/) {
+                       if (/\b($sha1_short)\b/o) {
                                unshift @commits, $1;
                        }
                }
@@ -272,7 +275,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) {
@@ -311,6 +316,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) {
@@ -320,24 +333,27 @@ sub setup_git_svn {
        mkpath(["$GIT_DIR/$GIT_SVN/info"]);
        mkpath([$REV_DIR]);
        s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url");
-       my $uuid = svn_info($SVN_URL)->{'Repository UUID'} or
-                                       croak "Repository UUID unreadable\n";
-       s_to_file($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 $uuid;
 }
 
 sub assert_svn_wc_clean {
        my ($svn_rev, $treeish) = @_;
        croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
        croak "$treeish is not a sha1!\n" unless ($treeish =~ /^$sha1$/o);
-       my $svn_info = svn_info('.');
-       if ($svn_rev != $svn_info->{'Last Changed Rev'}) {
-               croak "Expected r$svn_rev, got r",
-                               $svn_info->{'Last Changed Rev'},"\n";
+       my $lcr = svn_info('.')->{'Last Changed Rev'};
+       if ($svn_rev != $lcr) {
+               print STDERR "Checking for copy-tree ... ";
+               # use
+               my @diff = grep(/^Index: /,(safe_qx(qw(svn diff),
+                                               "-r$lcr:$svn_rev")));
+               if (@diff) {
+                       croak "Nope!  Expected r$svn_rev, got r$lcr\n";
+               } else {
+                       print STDERR "OK!\n";
+               }
        }
        my @status = grep(!/^Performing status on external/,(`svn status`));
        @status = grep(!/^\s*$/,@status);
@@ -834,11 +850,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";
                }
@@ -850,9 +878,6 @@ sub git_commit {
        my ($log_msg, @parents) = @_;
        assert_revision_unknown($log_msg->{revision});
        my $out_fh = IO::File->new_tmpfile or croak $!;
-       my $info = svn_info('.');
-       my $uuid = $info->{'Repository UUID'};
-       defined $uuid or croak "Unable to get Repository UUID\n";
 
        map_tree_joins() if (@_branch_from && !%tree_map);
 
@@ -891,11 +916,11 @@ sub git_commit {
                my $msg_fh = IO::File->new_tmpfile or croak $!;
                print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ",
                                        "$SVN_URL\@$log_msg->{revision}",
-                                       " $uuid\n" or croak $!;
+                                       " $SVN_UUID\n" or croak $!;
                $msg_fh->flush == 0 or croak $!;
                seek $msg_fh, 0, 0 or croak $!;
 
-               set_commit_env($log_msg, $uuid);
+               set_commit_env($log_msg);
 
                my @exec = ('git-commit-tree',$tree);
                push @exec, '-p', $_  foreach @exec_parents;
@@ -914,7 +939,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);
@@ -923,10 +957,10 @@ sub git_commit {
 }
 
 sub set_commit_env {
-       my ($log_msg, $uuid) = @_;
+       my ($log_msg) = @_;
        my $author = $log_msg->{author};
        my ($name,$email) = defined $users{$author} ?  @{$users{$author}}
-                               : ($author,"$author\@$uuid");
+                               : ($author,"$author\@$SVN_UUID");
        $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name;
        $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email;
        $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date};
@@ -987,13 +1021,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 {