git-svn: support manually placed initial trees from fetch
[git.git] / contrib / git-svn / git-svn.perl
index e003501..54f3d63 100755 (executable)
@@ -8,12 +8,13 @@ use vars qw/  $AUTHOR $VERSION
                $GIT_SVN_INDEX $GIT_SVN
                $GIT_DIR $REV_DIR/;
 $AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
-$VERSION = '0.11.0';
+$VERSION = '1.1.0-pre';
 
 use Cwd qw/abs_path/;
 $GIT_DIR = abs_path($ENV{GIT_DIR} || '.git');
 $ENV{GIT_DIR} = $GIT_DIR;
 
+my $LC_ALL = $ENV{LC_ALL};
 # make sure the svn binary gives consistent output between locales and TZs:
 $ENV{TZ} = 'UTC';
 $ENV{LC_ALL} = 'C';
@@ -32,13 +33,19 @@ use POSIX qw/strftime/;
 my $sha1 = qr/[a-f\d]{40}/;
 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);
+       $_find_copies_harder, $_l, $_cp_similarity,
+       $_version, $_upgrade, $_authors, $_branch_all_refs);
 my (@_branch_from, %tree_map, %users);
-my $_svn_co_url_revs;
+my ($_svn_co_url_revs, $_svn_pg_peg_revs);
 
 my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext,
                'branch|b=s' => \@_branch_from,
+               'branch-all-refs|B' => \$_branch_all_refs,
                'authors-file|A=s' => \$_authors );
+
+# yes, 'native' sets "\n".  Patches to fix this for non-*nix systems welcome:
+my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" );
+
 my %cmd = (
        fetch => [ \&fetch, "Download new revisions from SVN",
                        { 'revision|r=s' => \$_revision, %fc_opts } ],
@@ -50,6 +57,7 @@ my %cmd = (
                                'rmdir' => \$_rmdir,
                                'find-copies-harder' => \$_find_copies_harder,
                                'l=i' => \$_l,
+                               'copy-similarity|C=i'=> \$_cp_similarity,
                                %fc_opts,
                        } ],
        'show-ignore' => [ \&show_ignore, "Show svn:ignore listings", { } ],
@@ -101,6 +109,7 @@ usage(0) if $_help;
 version() if $_version;
 usage(1) unless defined $cmd;
 load_authors() if $_authors;
+load_all_refs() if $_branch_all_refs;
 svn_compat_check();
 $cmd{$cmd}->[0]->(@ARGV);
 exit 0;
@@ -207,7 +216,7 @@ sub rebuild {
                push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
                sys(@svn_up,"-r$newest_rev");
                $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
-               git_addremove();
+               index_changes();
                exec('git-write-tree');
        }
        waitpid $pid, 0;
@@ -249,26 +258,37 @@ sub fetch {
                chdir $SVN_WC or croak $!;
                read_uuid();
                $last_commit = git_commit($base, @parents);
-               assert_svn_wc_clean($base->{revision}, $last_commit);
+               assert_tree($last_commit);
        } else {
                chdir $SVN_WC or croak $!;
                read_uuid();
-               $last_commit = file_to_s("$REV_DIR/$base->{revision}");
+               eval { $last_commit = file_to_s("$REV_DIR/$base->{revision}") };
+               # looks like a user manually cp'd and svn switch'ed
+               unless ($last_commit) {
+                       sys(qw/svn revert -R ./);
+                       assert_svn_wc_clean($base->{revision});
+                       $last_commit = git_commit($base, @parents);
+                       assert_tree($last_commit);
+               }
        }
        my @svn_up = qw(svn up);
        push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
        my $last = $base;
        while (my $log_msg = next_log_entry($svn_log)) {
-               assert_svn_wc_clean($last->{revision}, $last_commit);
+               assert_tree($last_commit);
                if ($last->{revision} >= $log_msg->{revision}) {
                        croak "Out of order: last >= current: ",
                                "$last->{revision} >= $log_msg->{revision}\n";
                }
+               # Revert is needed for cases like:
+               # https://svn.musicpd.org/Jamming/trunk (r166:167), but
+               # I can't seem to reproduce something like that on a test...
+               sys(qw/svn revert -R ./);
+               assert_svn_wc_clean($last->{revision});
                sys(@svn_up,"-r$log_msg->{revision}");
                $last_commit = git_commit($log_msg, $last_commit, @parents);
                $last = $log_msg;
        }
-       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);
        }
@@ -300,9 +320,16 @@ sub commit {
        }
        chomp @revs;
 
-       fetch();
-       chdir $SVN_WC or croak $!;
+       chdir $SVN_WC or croak "Unable to chdir $SVN_WC: $!\n";
        my $info = svn_info('.');
+       my $fetched = fetch();
+       if ($info->{Revision} != $fetched->{revision}) {
+               print STDERR "There are new revisions that were fetched ",
+                               "and need to be merged (or acknowledged) ",
+                               "before committing.\n";
+               exit 1;
+       }
+       $info = svn_info('.');
        read_uuid($info);
        my $svn_current_rev =  $info->{'Last Changed Rev'};
        foreach my $c (@revs) {
@@ -314,7 +341,6 @@ sub commit {
                $svn_current_rev = svn_commit_tree($svn_current_rev, $c);
        }
        print "Done committing ",scalar @revs," revisions to SVN\n";
-
 }
 
 sub show_ignore {
@@ -329,7 +355,7 @@ sub show_ignore {
        my %ign;
        File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){
                s#^\./##;
-               @{$ign{$_}} = safe_qx(qw(svn propget svn:ignore),$_);
+               @{$ign{$_}} = svn_propget_base('svn:ignore', $_);
                }}, no_chdir=>1},'.');
 
        print "\n# /\n";
@@ -367,13 +393,11 @@ sub setup_git_svn {
 }
 
 sub assert_svn_wc_clean {
-       my ($svn_rev, $treeish) = @_;
+       my ($svn_rev) = @_;
        croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
-       croak "$treeish is not a sha1!\n" unless ($treeish =~ /^$sha1$/o);
        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) {
@@ -389,7 +413,6 @@ sub assert_svn_wc_clean {
                print STDERR $_ foreach @status;
                croak;
        }
-       assert_tree($treeish);
 }
 
 sub assert_tree {
@@ -416,7 +439,7 @@ sub assert_tree {
                unlink $tmpindex or croak $!;
        }
        $ENV{GIT_INDEX_FILE} = $tmpindex;
-       git_addremove();
+       index_changes(1);
        chomp(my $tree = `git-write-tree`);
        if ($old_index) {
                $ENV{GIT_INDEX_FILE} = $old_index;
@@ -426,6 +449,7 @@ sub assert_tree {
        if ($tree ne $expected) {
                croak "Tree mismatch, Got: $tree, Expected: $expected\n";
        }
+       unlink $tmpindex;
 }
 
 sub parse_diff_tree {
@@ -562,12 +586,17 @@ sub precommit_check {
 sub svn_checkout_tree {
        my ($svn_rev, $treeish) = @_;
        my $from = file_to_s("$REV_DIR/$svn_rev");
-       assert_svn_wc_clean($svn_rev,$from);
+       assert_tree($from);
        print "diff-tree $from $treeish\n";
        my $pid = open my $diff_fh, '-|';
        defined $pid or croak $!;
        if ($pid == 0) {
-               my @diff_tree = qw(git-diff-tree -z -r -C);
+               my @diff_tree = qw(git-diff-tree -z -r);
+               if ($_cp_similarity) {
+                       push @diff_tree, "-C$_cp_similarity";
+               } else {
+                       push @diff_tree, '-C';
+               }
                push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
                push @diff_tree, "-l$_l" if defined $_l;
                exec(@diff_tree, $from, $treeish) or croak $!;
@@ -699,23 +728,34 @@ sub svn_commit_tree {
        my ($oneline) = ($log_msg{msg} =~ /([^\n\r]+)/);
        print "Committing $commit: $oneline\n";
 
+       if (defined $LC_ALL) {
+               $ENV{LC_ALL} = $LC_ALL;
+       } else {
+               delete $ENV{LC_ALL};
+       }
        my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
-       my ($committed) = grep(/^Committed revision \d+\./,@ci_output);
+       $ENV{LC_ALL} = 'C';
        unlink $commit_msg;
-       defined $committed or croak
+       my ($committed) = ($ci_output[$#ci_output] =~ /(\d+)/);
+       if (!defined $committed) {
+               my $out = join("\n",@ci_output);
+               print STDERR "W: Trouble parsing \`svn commit' output:\n\n",
+                               $out, "\n\nAssuming English locale...";
+               ($committed) = ($out =~ /^Committed revision \d+\./sm);
+               defined $committed or die " FAILED!\n",
                        "Commit output failed to parse committed revision!\n",
-                       join("\n",@ci_output),"\n";
-       my ($rev_committed) = ($committed =~ /^Committed revision (\d+)\./);
+               print STDERR " OK\n";
+       }
 
        my @svn_up = qw(svn up);
        push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
-       if ($rev_committed == ($svn_rev + 1)) {
-               push @svn_up, "-r$rev_committed";
+       if ($committed == ($svn_rev + 1)) {
+               push @svn_up, "-r$committed";
                sys(@svn_up);
                my $info = svn_info('.');
                my $date = $info->{'Last Changed Date'} or die "Missing date\n";
-               if ($info->{'Last Changed Rev'} != $rev_committed) {
-                       croak "$info->{'Last Changed Rev'} != $rev_committed\n"
+               if ($info->{'Last Changed Rev'} != $committed) {
+                       croak "$info->{'Last Changed Rev'} != $committed\n"
                }
                my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
                                        /(\d{4})\-(\d\d)\-(\d\d)\s
@@ -723,16 +763,16 @@ sub svn_commit_tree {
                                         or croak "Failed to parse date: $date\n";
                $log_msg{date} = "$tz $Y-$m-$d $H:$M:$S";
                $log_msg{author} = $info->{'Last Changed Author'};
-               $log_msg{revision} = $rev_committed;
+               $log_msg{revision} = $committed;
                $log_msg{msg} .= "\n";
                my $parent = file_to_s("$REV_DIR/$svn_rev");
                git_commit(\%log_msg, $parent, $commit);
-               return $rev_committed;
+               return $committed;
        }
        # resync immediately
        push @svn_up, "-r$svn_rev";
        sys(@svn_up);
-       return fetch("$rev_committed=$commit")->{revision};
+       return fetch("$committed=$commit")->{revision};
 }
 
 # read the entire log into a temporary file (which is removed ASAP)
@@ -852,13 +892,82 @@ sub svn_info {
 
 sub sys { system(@_) == 0 or croak $? }
 
-sub git_addremove {
-       system( "git-diff-files --name-only -z ".
-                               " | git-update-index --remove -z --stdin && ".
-               "git-ls-files -z --others ".
-                       "'--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude'".
-                               " | git-update-index --add -z --stdin"
-               ) == 0 or croak $?
+sub eol_cp {
+       my ($from, $to) = @_;
+       my $es = svn_propget_base('svn:eol-style', $to);
+       open my $rfd, '<', $from or croak $!;
+       binmode $rfd or croak $!;
+       open my $wfd, '>', $to or croak $!;
+       binmode $wfd or croak $!;
+
+       my $eol = $EOL{$es} or undef;
+       my $buf;
+       use bytes;
+       while (1) {
+               my ($r, $w, $t);
+               defined($r = sysread($rfd, $buf, 4096)) or croak $!;
+               return unless $r;
+               if ($eol) {
+                       if ($buf =~ /\015$/) {
+                               my $c;
+                               defined($r = sysread($rfd,$c,1)) or croak $!;
+                               $buf .= $c if $r > 0;
+                       }
+                       $buf =~ s/(?:\015\012|\015|\012)/$eol/gs;
+                       $r = length($buf);
+               }
+               for ($w = 0; $w < $r; $w += $t) {
+                       $t = syswrite($wfd, $buf, $r - $w, $w) or croak $!;
+               }
+       }
+       no bytes;
+}
+
+sub do_update_index {
+       my ($z_cmd, $cmd, $no_text_base) = @_;
+
+       my $z = open my $p, '-|';
+       defined $z or croak $!;
+       unless ($z) { exec @$z_cmd or croak $! }
+
+       my $pid = open my $ui, '|-';
+       defined $pid or croak $!;
+       unless ($pid) {
+               exec('git-update-index',"--$cmd",'-z','--stdin') or croak $!;
+       }
+       local $/ = "\0";
+       while (my $x = <$p>) {
+               chomp $x;
+               if (!$no_text_base && lstat $x && ! -l _ &&
+                               svn_propget_base('svn:keywords', $x)) {
+                       my $mode = -x _ ? 0755 : 0644;
+                       my ($v,$d,$f) = File::Spec->splitpath($x);
+                       my $tb = File::Spec->catfile($d, '.svn', 'tmp',
+                                               'text-base',"$f.svn-base");
+                       $tb =~ s#^/##;
+                       unless (-f $tb) {
+                               $tb = File::Spec->catfile($d, '.svn',
+                                               'text-base',"$f.svn-base");
+                               $tb =~ s#^/##;
+                       }
+                       unlink $x or croak $!;
+                       eol_cp($tb, $x);
+                       chmod(($mode &~ umask), $x) or croak $!;
+               }
+               print $ui $x,"\0";
+       }
+       close $ui or croak $!;
+}
+
+sub index_changes {
+       my $no_text_base = shift;
+       do_update_index([qw/git-diff-files --name-only -z/],
+                       'remove',
+                       $no_text_base);
+       do_update_index([qw/git-ls-files -z --others/,
+                             "--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude"],
+                       'add',
+                       $no_text_base);
 }
 
 sub s_to_file {
@@ -936,7 +1045,7 @@ sub git_commit {
        defined $pid or croak $!;
        if ($pid == 0) {
                $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
-               git_addremove();
+               index_changes();
                chomp(my $tree = `git-write-tree`);
                croak if $?;
                if (exists $tree_map{$tree}) {
@@ -1068,6 +1177,9 @@ sub svn_compat_check {
        if (grep /usage: checkout URL\[\@REV\]/,@co_help) {
                $_svn_co_url_revs = 1;
        }
+       if (grep /\[TARGET\[\@REV\]\.\.\.\]/, `svn propget -h`) {
+               $_svn_pg_peg_revs = 1;
+       }
 
        # I really, really hope nobody hits this...
        unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) {
@@ -1115,26 +1227,44 @@ sub check_upgrade_needed {
 # fills %tree_map with a reverse mapping of trees to commits.  Useful
 # for finding parents to commit on.
 sub map_tree_joins {
+       my %seen;
        foreach my $br (@_branch_from) {
                my $pid = open my $pipe, '-|';
                defined $pid or croak $!;
                if ($pid == 0) {
-                       exec(qw(git-rev-list --pretty=raw), $br) or croak $?;
+                       exec(qw(git-rev-list --topo-order --pretty=raw), $br)
+                                                               or croak $?;
                }
                while (<$pipe>) {
                        if (/^commit ($sha1)$/o) {
                                my $commit = $1;
+
+                               # if we've seen a commit,
+                               # we've seen its parents
+                               last if $seen{$commit};
                                my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o);
                                unless (defined $tree) {
                                        die "Failed to parse commit $commit\n";
                                }
                                push @{$tree_map{$tree}}, $commit;
+                               $seen{$commit} = 1;
                        }
                }
-               close $pipe or croak $?;
+               close $pipe; # we could be breaking the pipe early
        }
 }
 
+sub load_all_refs {
+       if (@_branch_from) {
+               print STDERR '--branch|-b parameters are ignored when ',
+                       "--branch-all-refs|-B is passed\n";
+       }
+
+       # don't worry about rev-list on non-commit objects/tags,
+       # it shouldn't blow up if a ref is a blob or tree...
+       chomp(@_branch_from = `git-rev-parse --symbolic --all`);
+}
+
 # '<svn username> = real-name <email address>' mapping based on git-svnimport:
 sub load_authors {
        open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
@@ -1147,6 +1277,12 @@ sub load_authors {
        close $authors or croak $!;
 }
 
+sub svn_propget_base {
+       my ($p, $f) = @_;
+       $f .= '@BASE' if $_svn_pg_peg_revs;
+       return safe_qx(qw/svn propget/, $p, $f);
+}
+
 __END__
 
 Data structures: