git-svn: bugfix and optimize the 'log' command
[git.git] / contrib / git-svn / git-svn.perl
index 884969e..417fcf1 100755 (executable)
@@ -31,6 +31,7 @@ use File::Path qw/mkpath/;
 use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/;
 use File::Spec qw//;
 use POSIX qw/strftime/;
+use IPC::Open3;
 use Memoize;
 memoize('revisions_eq');
 
@@ -368,7 +369,6 @@ sub fetch_lib {
                defined(my $pid = fork) or croak $!;
                if (!$pid) {
                        $SVN::Error::handler = \&libsvn_skip_unknown_revs;
-                       print "Fetching revisions $min .. $max\n";
 
                        # Yes I'm perfectly aware that the fourth argument
                        # below is the limit revisions number.  Unfortunately
@@ -391,7 +391,6 @@ sub fetch_lib {
                                                        $log_msg, @parents);
                                        }
                                });
-                       $SVN::Error::handler = sub { 'quiet warnings' };
                        exit 0;
                }
                waitpid $pid, 0;
@@ -463,7 +462,7 @@ sub commit_lib {
        my (@revs) = @_;
        my ($r_last, $cmt_last) = svn_grab_base_rev();
        defined $r_last or die "Must have an existing revision to commit\n";
-       my $fetched = fetch_lib();
+       my $fetched = fetch();
        if ($r_last != $fetched->{revision}) {
                print STDERR "There are new revisions that were fetched ",
                                "and need to be merged (or acknowledged) ",
@@ -523,7 +522,7 @@ sub commit_lib {
                                $no = 1;
                        }
                }
-               close $fh or croak $!;
+               close $fh or croak $?;
                if (! defined $r_new && ! defined $cmt_new) {
                        unless ($no) {
                                die "Failed to parse revision information\n";
@@ -633,17 +632,8 @@ sub multi_init {
 sub multi_fetch {
        # try to do trunk first, since branches/tags
        # may be descended from it.
-       if (-d "$GIT_DIR/svn/trunk") {
-               print "Fetching trunk\n";
-               defined(my $pid = fork) or croak $!;
-               if (!$pid) {
-                       $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
-                       init_vars();
-                       fetch(@_);
-                       exit 0;
-               }
-               waitpid $pid, 0;
-               croak $? if $?;
+       if (-e "$GIT_DIR/svn/trunk/info/url") {
+               fetch_child_id('trunk', @_);
        }
        rec_fetch('', "$GIT_DIR/svn", @_);
 }
@@ -673,17 +663,15 @@ sub show_log {
        my $pid = open(my $log,'-|');
        defined $pid or croak $!;
        if (!$pid) {
-               my @rl = (qw/git-log --abbrev-commit --pretty=raw
-                               --default/, "remotes/$GIT_SVN");
-               push @rl, '--raw' if $_verbose;
-               exec(@rl, @args) or croak $!;
+               exec(git_svn_log_cmd($r_min,$r_max), @args) or croak $!;
        }
        setup_pager();
        my (@k, $c, $d);
+
        while (<$log>) {
                if (/^commit ($sha1_short)/o) {
                        my $cmt = $1;
-                       if ($c && defined $c->{r} && $c->{r} != $r_last) {
+                       if ($c && cmt_showable($c) && $c->{r} != $r_last) {
                                $r_last = $c->{r};
                                process_commit($c, $r_min, $r_max, \@k) or
                                                                goto out;
@@ -702,8 +690,7 @@ sub show_log {
                } elsif ($d) {
                        push @{$c->{diff}}, $_;
                } elsif (/^    (git-svn-id:.+)$/) {
-                       my ($url, $rev, $uuid) = extract_metadata($1);
-                       $c->{r} = $rev;
+                       (undef, $c->{r}, undef) = extract_metadata($1);
                } elsif (s/^    //) {
                        push @{$c->{l}}, $_;
                }
@@ -725,6 +712,87 @@ out:
 
 ########################### utility functions #########################
 
+sub cmt_showable {
+       my ($c) = @_;
+       return 1 if defined $c->{r};
+       if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
+                               $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
+               my @msg = safe_qx(qw/git-cat-file commit/, $c->{c});
+               shift @msg while ($msg[0] ne "\n");
+               shift @msg;
+               @{$c->{l}} = grep !/^git-svn-id: /, @msg;
+
+               (undef, $c->{r}, undef) = extract_metadata(
+                               (grep(/^git-svn-id: /, @msg))[-1]);
+       }
+       return defined $c->{r};
+}
+
+sub git_svn_log_cmd {
+       my ($r_min, $r_max) = @_;
+       my @cmd = (qw/git-log --abbrev-commit --pretty=raw
+                       --default/, "refs/remotes/$GIT_SVN");
+       push @cmd, '--summary' if $_verbose;
+       return @cmd unless defined $r_max;
+       if ($r_max == $r_min) {
+               push @cmd, '--max-count=1';
+               if (my $c = revdb_get($REVDB, $r_max)) {
+                       push @cmd, $c;
+               }
+       } else {
+               my ($c_min, $c_max);
+               $c_max = revdb_get($REVDB, $r_max);
+               $c_min = revdb_get($REVDB, $r_min);
+               if ($c_min && $c_max) {
+                       if ($r_max > $r_max) {
+                               push @cmd, "$c_min..$c_max";
+                       } else {
+                               push @cmd, "$c_max..$c_min";
+                       }
+               } elsif ($r_max > $r_min) {
+                       push @cmd, $c_max;
+               } else {
+                       push @cmd, $c_min;
+               }
+       }
+       return @cmd;
+}
+
+sub fetch_child_id {
+       my $id = shift;
+       print "Fetching $id\n";
+       my $ref = "$GIT_DIR/refs/remotes/$id";
+       my $ca = file_to_s($ref) if (-r $ref);
+       defined(my $pid = fork) or croak $!;
+       if (!$pid) {
+               $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
+               init_vars();
+               fetch(@_);
+               exit 0;
+       }
+       waitpid $pid, 0;
+       croak $? if $?;
+       return unless $_repack || -r $ref;
+
+       my $cb = file_to_s($ref);
+
+       defined($pid = open my $fh, '-|') or croak $!;
+       my $url = file_to_s("$GIT_DIR/svn/$id/info/url");
+       $url = qr/\Q$url\E/;
+       if (!$pid) {
+               exec qw/git-rev-list --pretty=raw/,
+                               $ca ? "$ca..$cb" : $cb or croak $!;
+       }
+       while (<$fh>) {
+               if (/^    git-svn-id: $url\@\d+ [a-f0-9\-]+$/) {
+                       check_repack();
+               } elsif (/^    git-svn-id: \S+\@\d+ [a-f0-9\-]+$/) {
+                       last;
+               }
+       }
+       close $fh;
+}
+
 sub rec_fetch {
        my ($pfx, $p, @args) = @_;
        my @dir;
@@ -733,16 +801,7 @@ sub rec_fetch {
                        $pfx .= '/' if $pfx && $pfx !~ m!/$!;
                        my $id = $pfx . basename $_;
                        next if $id eq 'trunk';
-                       print "Fetching $id\n";
-                       defined(my $pid = fork) or croak $!;
-                       if (!$pid) {
-                               $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
-                               init_vars();
-                               fetch(@args);
-                               exit 0;
-                       }
-                       waitpid $pid, 0;
-                       croak $? if $?;
+                       fetch_child_id($id, @args);
                } elsif (-d $_) {
                        push @dir, $_;
                }
@@ -943,7 +1002,6 @@ sub read_uuid {
                $SVN_UUID = $info->{'Repository UUID'} or
                                        croak "Repository UUID unreadable\n";
        }
-       s_to_file($SVN_UUID,"$GIT_SVN_DIR/info/uuid");
 }
 
 sub quiet_run {
@@ -1005,12 +1063,6 @@ sub setup_git_svn {
        close $fh;
        s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url");
 
-       open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!;
-       print $fd '.svn',"\n";
-       close $fd or croak $!;
-       my ($url, $path) = repo_path_split($SVN_URL);
-       s_to_file($url, "$GIT_SVN_DIR/info/repo_url");
-       s_to_file($path, "$GIT_SVN_DIR/info/repo_path");
 }
 
 sub assert_svn_wc_clean {
@@ -1113,7 +1165,7 @@ sub parse_diff_tree {
                        croak "Error parsing $_\n";
                }
        }
-       close $diff_fh or croak $!;
+       close $diff_fh or croak $?;
 
        return \@mods;
 }
@@ -1354,7 +1406,7 @@ sub get_commit_message {
                                print $msg $_ or croak $!;
                        }
                }
-               close $msg_fh or croak $!;
+               close $msg_fh or croak $?;
        }
        close $msg or croak $!;
 
@@ -1568,7 +1620,7 @@ sub svn_info {
                        push @{$ret->{-order}}, $1;
                }
        }
-       close $info_fh or croak $!;
+       close $info_fh or croak $?;
        return $ret;
 }
 
@@ -1644,11 +1696,17 @@ sub do_update_index {
                }
                print $ui $x,"\0";
        }
-       close $ui or croak $!;
+       close $ui or croak $?;
 }
 
 sub index_changes {
        return if $_use_lib;
+
+       if (!-f "$GIT_SVN_DIR/info/exclude") {
+               open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!;
+               print $fd '.svn',"\n";
+               close $fd or croak $!;
+       }
        my $no_text_base = shift;
        do_update_index([qw/git-diff-files --name-only -z/],
                        'remove',
@@ -1765,11 +1823,15 @@ sub git_commit {
 
        # this output is read via pipe, do not change:
        print "r$log_msg->{revision} = $commit\n";
+       check_repack();
+       return $commit;
+}
+
+sub check_repack {
        if ($_repack && (--$_repack_nr == 0)) {
                $_repack_nr = $_repack;
                sys("git repack $_repack_flags");
        }
-       return $commit;
 }
 
 sub set_commit_env {
@@ -1877,6 +1939,10 @@ sub svn_cmd_checkout {
 }
 
 sub check_upgrade_needed {
+       if (!-r $REVDB) {
+               open my $fh, '>>',$REVDB or croak $!;
+               close $fh;
+       }
        my $old = eval {
                my $pid = open my $child, '-|';
                defined $pid or croak $!;
@@ -2018,9 +2084,6 @@ sub migration_check {
                my $dn = dirname("$GIT_DIR/svn/$x");
                mkpath([$dn]) unless -d $dn;
                rename "$GIT_DIR/$x", "$GIT_DIR/svn/$x" or croak "$!: $x";
-               my ($url, $path) = repo_path_split($u);
-               s_to_file($url, "$GIT_DIR/svn/$x/info/repo_url");
-               s_to_file($path, "$GIT_DIR/svn/$x/info/repo_path");
        }
        migrate_revdb() if (-d $GIT_SVN_DIR && !-w $REVDB);
        print "Done upgrading.\n";
@@ -2029,7 +2092,8 @@ sub migration_check {
 sub find_rev_before {
        my ($r, $id, $eq_ok) = @_;
        my $f = "$GIT_DIR/svn/$id/.rev_db";
-       # --$r unless $eq_ok;
+       return (undef,undef) unless -r $f;
+       --$r unless $eq_ok;
        while ($r > 0) {
                if (my $c = revdb_get($f, $r)) {
                        return ($r, $c);
@@ -2075,7 +2139,7 @@ sub set_default_vals {
        if (defined $_repack) {
                $_repack = 1000 if ($_repack <= 0);
                $_repack_nr = $_repack;
-               $_repack_flags ||= '';
+               $_repack_flags ||= '-d';
        }
 }
 
@@ -2138,15 +2202,8 @@ sub write_grafts {
 sub read_url_paths {
        my $l_map = {};
        git_svn_each(sub { my $x = shift;
-                       my $u = file_to_s("$GIT_DIR/svn/$x/info/repo_url");
-                       my $p = file_to_s("$GIT_DIR/svn/$x/info/repo_path");
-                       # we hate trailing slashes
-                       if ($u =~ s#(?:^\/+|\/+$)##g) {
-                               s_to_file($u,"$GIT_DIR/svn/$x/info/repo_url");
-                       }
-                       if ($p =~ s#(?:^\/+|\/+$)##g) {
-                               s_to_file($p,"$GIT_DIR/svn/$x/info/repo_path");
-                       }
+                       my $url = file_to_s("$GIT_DIR/svn/$x/info/url");
+                       my ($u, $p) = repo_path_split($url);
                        $l_map->{$u}->{$p} = $x;
                        });
        return $l_map;
@@ -2192,6 +2249,7 @@ sub setup_pager { # translated to Perl from pager.c
 sub get_author_info {
        my ($dest, $author, $t, $tz) = @_;
        $author =~ s/(?:^\s*|\s*$)//g;
+       $dest->{a_raw} = $author;
        my $_a;
        if ($_authors) {
                $_a = $rusers{$author} || undef;
@@ -2322,47 +2380,36 @@ sub libsvn_get_file {
        my $p = $f;
        return unless ($p =~ s#^\Q$SVN_PATH\E/?##);
 
-       my $fd = IO::File->new_tmpfile or croak $!;
+       my ($hash, $pid, $in, $out);
        my $pool = SVN::Pool->new;
-       my ($r, $props) = $SVN->get_file($f, $rev, $fd, $pool);
+       defined($pid = open3($in, $out, '>&STDERR',
+                               qw/git-hash-object -w --stdin/)) or croak $!;
+       my ($r, $props) = $SVN->get_file($f, $rev, $in, $pool);
+       $in->flush == 0 or croak $!;
+       close $in or croak $!;
        $pool->clear;
-       $fd->flush == 0 or croak $!;
-       seek $fd, 0, 0 or croak $!;
-       if (my $es = $props->{'svn:eol-style'}) {
-               my $new_fd = IO::File->new_tmpfile or croak $!;
-               eol_cp_fd($fd, $new_fd, $es);
-               close $fd or croak $!;
-               $fd = $new_fd;
-               seek $fd, 0, 0 or croak $!;
-               $fd->flush == 0 or croak $!;
-       }
-       my $mode = '100644';
-       if (exists $props->{'svn:executable'}) {
-               $mode = '100755';
-       }
+       chomp($hash = do { local $/; <$out> });
+       close $out or croak $!;
+       waitpid $pid, 0;
+       $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
+
+       my $mode = exists $props->{'svn:executable'} ? '100755' : '100644';
        if (exists $props->{'svn:special'}) {
                $mode = '120000';
-               local $/;
-               my $link = <$fd>;
+               my $link = `git-cat-file blob $hash`;
                $link =~ s/^link // or die "svn:special file with contents: <",
                                                $link, "> is not understood\n";
-               seek $fd, 0, 0 or croak $!;
-               truncate $fd, 0 or croak $!;
-               print $fd $link or croak $!;
-               seek $fd, 0, 0 or croak $!;
-               $fd->flush == 0 or croak $!;
-       }
-       my $pid = open my $ho, '-|';
-       defined $pid or croak $!;
-       if (!$pid) {
-               open STDIN, '<&', $fd or croak $!;
-               exec qw/git-hash-object -w --stdin/ or croak $!;
+               defined($pid = open3($in, $out, '>&STDERR',
+                               qw/git-hash-object -w --stdin/)) or croak $!;
+               print $in $link;
+               $in->flush == 0 or croak $!;
+               close $in or croak $!;
+               chomp($hash = do { local $/; <$out> });
+               close $out or croak $!;
+               waitpid $pid, 0;
+               $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
        }
-       chomp(my $hash = do { local $/; <$ho> });
-       close $ho or croak $?;
-       $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
        print $gui $mode,' ',$hash,"\t",$p,"\0" or croak $!;
-       close $fd or croak $!;
 }
 
 sub libsvn_log_entry {
@@ -2391,7 +2438,7 @@ sub process_rm {
                while (<$ls>) {
                        print $gui '0 ',0 x 40,"\t",$_ or croak $!;
                }
-               close $ls or croak $!;
+               close $ls or croak $?;
        } else {
                print $gui '0 ',0 x 40,"\t",$f,"\0" or croak $!;
        }
@@ -2421,7 +2468,7 @@ sub libsvn_fetch {
                $pool->clear;
        }
        libsvn_get_file($gui, $_, $rev) foreach (@amr);
-       close $gui or croak $!;
+       close $gui or croak $?;
        return libsvn_log_entry($rev, $author, $date, $msg, [$last_commit]);
 }
 
@@ -2437,7 +2484,7 @@ sub svn_grab_base_rev {
        close $fh;
        if (defined $c && length $c) {
                my ($url, $rev, $uuid) = extract_metadata((grep(/^git-svn-id: /,
-                       safe_qx(qw/git-cat-file commit/, $c)))[0]);
+                       safe_qx(qw/git-cat-file commit/, $c)))[-1]);
                return ($rev, $c);
        }
        return (undef, undef);
@@ -2524,36 +2571,30 @@ sub revisions_eq {
 }
 
 sub libsvn_find_parent_branch {
-       return undef; # XXX this function is disabled atm (not tested enough)
        my ($paths, $rev, $author, $date, $msg) = @_;
        my $svn_path = '/'.$SVN_PATH;
 
        # look for a parent from another branch:
-       foreach (keys %$paths) {
-               next if ($_ ne $svn_path);
-               my $i = $paths->{$_};
-               my $branch_from = $i->copyfrom_path or next;
-               my $r = $i->copyfrom_rev;
-               print STDERR  "Found possible branch point: ",
-                                       "$branch_from => $svn_path, $r\n";
-               $branch_from =~ s#^/##;
-               my $l_map = read_url_paths();
-               my $url = $SVN->{url};
-               defined $l_map->{$url} or next;
-               my $id  = $l_map->{$url}->{$branch_from} or next;
-               my ($r0, $parent) = find_rev_before($r,$id,1);
-               if (defined $r0 && defined $parent &&
-                                       revisions_eq($branch_from, $r0, $r)) {
-                       unlink $GIT_SVN_INDEX;
-                       print STDERR "Found branch parent: $parent\n";
-                       sys(qw/git-read-tree/, $parent);
-                       return libsvn_fetch($parent, $paths, $rev,
-                                               $author, $date, $msg);
-               } else {
-                       print STDERR
-                               "Nope, branch point not imported or unknown\n";
-               }
-       }
+       my $i = $paths->{$svn_path} or return;
+       my $branch_from = $i->copyfrom_path or return;
+       my $r = $i->copyfrom_rev;
+       print STDERR  "Found possible branch point: ",
+                               "$branch_from => $svn_path, $r\n";
+       $branch_from =~ s#^/##;
+       my $l_map = read_url_paths();
+       my $url = $SVN->{url};
+       defined $l_map->{$url} or return;
+       my $id = $l_map->{$url}->{$branch_from} or return;
+       my ($r0, $parent) = find_rev_before($r,$id,1);
+       return unless (defined $r0 && defined $parent);
+       if (revisions_eq($branch_from, $r0, $r)) {
+               unlink $GIT_SVN_INDEX;
+               print STDERR "Found branch parent: $parent\n";
+               sys(qw/git-read-tree/, $parent);
+               return libsvn_fetch($parent, $paths, $rev,
+                                       $author, $date, $msg);
+       }
+       print STDERR "Nope, branch point not imported or unknown\n";
        return undef;
 }
 
@@ -2566,7 +2607,7 @@ sub libsvn_new_tree {
        my $pool = SVN::Pool->new;
        libsvn_traverse($gui, '', $SVN_PATH, $rev, $pool);
        $pool->clear;
-       close $gui or croak $!;
+       close $gui or croak $?;
        return libsvn_log_entry($rev, $author, $date, $msg);
 }
 
@@ -2640,7 +2681,7 @@ sub libsvn_commit_cb {
                        exit 1;
                }
        } else {
-               fetch_lib("$rev=$c");
+               fetch("$rev=$c");
        }
 }
 
@@ -2674,7 +2715,6 @@ sub libsvn_skip_unknown_revs {
        # 175002 - http(s)://
        #   More codes may be discovered later...
        if ($errno == 175002 || $errno == 160013) {
-               print STDERR "directory non-existent\n";
                return;
        }
        croak "Error from SVN, ($errno): ", $err->expanded_message,"\n";