git-svn: add the commit-diff command
[gitweb.git] / contrib / git-svn / git-svn.perl
index 88af9c570473697bc6970d76aab87628528b52fa..4bdc9766cdd04cb8a62f437c71fa04b4738312d7 100755 (executable)
 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');
+memoize('cmt_metadata');
+memoize('get_commit_time');
 
 my ($SVN_PATH, $SVN, $SVN_LOG, $_use_lib);
 $_use_lib = 1 unless $ENV{GIT_SVN_NO_LIB};
@@ -41,8 +44,9 @@
 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, $_cp_similarity,
+       $_find_copies_harder, $_l, $_cp_similarity, $_cp_remote,
        $_repack, $_repack_nr, $_repack_flags,
+       $_message, $_file,
        $_template, $_shared, $_no_default_regex, $_no_graft_copy,
        $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit,
        $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m);
                'tags|t=s' => \$_tags,
                'branches|b=s' => \$_branches );
 my %init_opts = ( 'template=s' => \$_template, 'shared' => \$_shared );
+my %cmt_opts = ( 'edit|e' => \$_edit,
+               'rmdir' => \$_rmdir,
+               'find-copies-harder' => \$_find_copies_harder,
+               'l=i' => \$_l,
+               'copy-similarity|C=i'=> \$_cp_similarity
+);
 
 # yes, 'native' sets "\n".  Patches to fix this for non-*nix systems welcome:
 my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" );
                          " (requires URL argument)",
                          \%init_opts ],
        commit => [ \&commit, "Commit git revisions to SVN",
-                       {       'stdin|' => \$_stdin,
-                               'edit|e' => \$_edit,
-                               'rmdir' => \$_rmdir,
-                               'find-copies-harder' => \$_find_copies_harder,
-                               'l=i' => \$_l,
-                               'copy-similarity|C=i'=> \$_cp_similarity,
-                               %fc_opts,
-                       } ],
+                       {       'stdin|' => \$_stdin, %cmt_opts, %fc_opts, } ],
        'show-ignore' => [ \&show_ignore, "Show svn:ignore listings",
                        { 'revision|r=i' => \$_revision } ],
        rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)",
                        { 'no-ignore-externals' => \$_no_ignore_ext,
+                         'copy-remote|remote=s' => \$_cp_remote,
                          'upgrade' => \$_upgrade } ],
        'graft-branches' => [ \&graft_branches,
                        'Detect merges/branches from already imported history',
                        { 'merge-rx|m' => \@_opt_m,
+                         'branch|b=s' => \@_branch_from,
+                         'branch-all-refs|B' => \$_branch_all_refs,
                          'no-default-regex' => \$_no_default_regex,
                          'no-graft-copy' => \$_no_graft_copy } ],
        'multi-init' => [ \&multi_init,
                          'show-commit' => \$_show_commit,
                          'authors-file|A=s' => \$_authors,
                        } ],
+       'commit-diff' => [ \&commit_diff, 'Commit a diff between two trees',
+                       { 'message|m=s' => \$_message,
+                         'file|F=s' => \$_file,
+                       %cmt_opts } ],
 );
 
 my $cmd;
 init_vars();
 load_authors() if $_authors;
 load_all_refs() if $_branch_all_refs;
-svn_compat_check();
-migration_check() unless $cmd =~ /^(?:init|multi-init)$/;
+svn_compat_check() unless $_use_lib;
+migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init)$/;
 $cmd{$cmd}->[0]->(@ARGV);
 exit 0;
 
@@ -173,6 +183,9 @@ sub version {
 }
 
 sub rebuild {
+       if (quiet_run(qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0")) {
+               copy_remote_ref();
+       }
        $SVN_URL = shift or undef;
        my $newest_rev = 0;
        if ($_upgrade) {
@@ -368,14 +381,14 @@ 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
                        # performance sucks with it enabled, so it's much
                        # faster to fetch revision ranges instead of relying
                        # on the limiter.
-                       $SVN_LOG->get_log( '/'.$SVN_PATH, $min, $max, 0, 1, 1,
+                       libsvn_get_log($SVN_LOG, '/'.$SVN_PATH,
+                                       $min, $max, 0, 1, 1,
                                sub {
                                        my $log_msg;
                                        if ($last_commit) {
@@ -391,7 +404,6 @@ sub fetch_lib {
                                                        $log_msg, @parents);
                                        }
                                });
-                       $SVN::Error::handler = sub { 'quiet warnings' };
                        exit 0;
                }
                waitpid $pid, 0;
@@ -463,7 +475,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) ",
@@ -476,17 +488,14 @@ sub commit_lib {
        my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
        my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
 
+       set_svn_commit_env();
        foreach my $c (@revs) {
+               my $log_msg = get_commit_message($c, $commit_msg);
+
                # fork for each commit because there's a memory leak I
                # can't track down... (it's probably in the SVN code)
                defined(my $pid = open my $fh, '-|') or croak $!;
                if (!$pid) {
-                       if (defined $LC_ALL) {
-                               $ENV{LC_ALL} = $LC_ALL;
-                       } else {
-                               delete $ENV{LC_ALL};
-                       }
-                       my $log_msg = get_commit_message($c, $commit_msg);
                        my $ed = SVN::Git::Editor->new(
                                        {       r => $r_last,
                                                ra => $SVN,
@@ -523,7 +532,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";
@@ -532,6 +541,7 @@ sub commit_lib {
                        ($r_last, $cmt_last) = ($r_new, $cmt_new);
                }
        }
+       $ENV{LC_ALL} = 'C';
        unlink $commit_msg;
 }
 
@@ -584,13 +594,14 @@ sub graft_branches {
        my $l_map = read_url_paths();
        my @re = map { qr/$_/is } @_opt_m if @_opt_m;
        unless ($_no_default_regex) {
-               push @re, (     qr/\b(?:merge|merging|merged)\s+(\S.+)/is,
-                               qr/\b(?:from|of)\s+(\S.+)/is );
+               push @re, (qr/\b(?:merge|merging|merged)\s+with\s+([\w\.\-]+)/i,
+                       qr/\b(?:merge|merging|merged)\s+([\w\.\-]+)/i,
+                       qr/\b(?:from|of)\s+([\w\.\-]+)/i );
        }
        foreach my $u (keys %$l_map) {
                if (@re) {
                        foreach my $p (keys %{$l_map->{$u}}) {
-                               graft_merge_msg($grafts,$l_map,$u,$p);
+                               graft_merge_msg($grafts,$l_map,$u,$p,@re);
                        }
                }
                unless ($_no_graft_copy) {
@@ -601,6 +612,7 @@ sub graft_branches {
                        }
                }
        }
+       graft_tree_joins($grafts);
 
        write_grafts($grafts, $comments, $gr_file);
        unlink "$gr_file~$gr_sha1" if $gr_sha1;
@@ -633,17 +645,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 +676,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 +703,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}}, $_;
                }
@@ -723,8 +723,138 @@ sub show_log {
        print '-' x72,"\n" unless $_incremental || $_oneline;
 }
 
+sub commit_diff_usage {
+       print STDERR "Usage: $0 commit-diff <tree-ish> <tree-ish> [<URL>]\n";
+       exit 1
+}
+
+sub commit_diff {
+       if (!$_use_lib) {
+               print STDERR "commit-diff must be used with SVN libraries\n";
+               exit 1;
+       }
+       my $ta = shift or commit_diff_usage();
+       my $tb = shift or commit_diff_usage();
+       if (!eval { $SVN_URL = shift || file_to_s("$GIT_SVN_DIR/info/url") }) {
+               print STDERR "Needed URL or usable git-svn id command-line\n";
+               commit_diff_usage();
+       }
+       if (defined $_message && defined $_file) {
+               print STDERR "Both --message/-m and --file/-F specified ",
+                               "for the commit message.\n",
+                               "I have no idea what you mean\n";
+               exit 1;
+       }
+       if (defined $_file) {
+               $_message = file_to_s($_message);
+       } else {
+               $_message ||= get_commit_message($tb,
+                                       "$GIT_DIR/.svn-commit.tmp.$$")->{msg};
+       }
+       my $repo;
+       ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
+       $SVN_LOG ||= libsvn_connect($repo);
+       $SVN ||= libsvn_connect($repo);
+       my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
+       my $ed = SVN::Git::Editor->new({        r => $SVN->get_latest_revnum,
+                                               ra => $SVN, c => $tb,
+                                               svn_path => $SVN_PATH
+                                       },
+                               $SVN->get_commit_editor($_message,
+                                       sub {print "Committed $_[0]\n"},@lock)
+                               );
+       my $mods = libsvn_checkout_tree($ta, $tb, $ed);
+       if (@$mods == 0) {
+               print "No changes\n$ta == $tb\n";
+               $ed->abort_edit;
+       } else {
+               $ed->close_edit;
+       }
+}
+
 ########################### 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 +863,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, $_;
                }
@@ -813,6 +934,77 @@ sub common_prefix {
        return '';
 }
 
+# grafts set here are 'stronger' in that they're based on actual tree
+# matches, and won't be deleted from merge-base checking in write_grafts()
+sub graft_tree_joins {
+       my $grafts = shift;
+       map_tree_joins() if (@_branch_from && !%tree_map);
+       return unless %tree_map;
+
+       git_svn_each(sub {
+               my $i = shift;
+               defined(my $pid = open my $fh, '-|') or croak $!;
+               if (!$pid) {
+                       exec qw/git-rev-list --pretty=raw/,
+                                       "refs/remotes/$i" or croak $!;
+               }
+               while (<$fh>) {
+                       next unless /^commit ($sha1)$/o;
+                       my $c = $1;
+                       my ($t) = (<$fh> =~ /^tree ($sha1)$/o);
+                       next unless $tree_map{$t};
+
+                       my $l;
+                       do {
+                               $l = readline $fh;
+                       } until ($l =~ /^committer (?:.+) (\d+) ([\-\+]?\d+)$/);
+
+                       my ($s, $tz) = ($1, $2);
+                       if ($tz =~ s/^\+//) {
+                               $s += tz_to_s_offset($tz);
+                       } elsif ($tz =~ s/^\-//) {
+                               $s -= tz_to_s_offset($tz);
+                       }
+
+                       my ($url_a, $r_a, $uuid_a) = cmt_metadata($c);
+
+                       foreach my $p (@{$tree_map{$t}}) {
+                               next if $p eq $c;
+                               my $mb = eval {
+                                       safe_qx('git-merge-base', $c, $p)
+                               };
+                               next unless ($@ || $?);
+                               if (defined $r_a) {
+                                       # see if SVN says it's a relative
+                                       my ($url_b, $r_b, $uuid_b) =
+                                                       cmt_metadata($p);
+                                       next if (defined $url_b &&
+                                                       defined $url_a &&
+                                                       ($url_a eq $url_b) &&
+                                                       ($uuid_a eq $uuid_b));
+                                       if ($uuid_a eq $uuid_b) {
+                                               if ($r_b < $r_a) {
+                                                       $grafts->{$c}->{$p} = 2;
+                                                       next;
+                                               } elsif ($r_b > $r_a) {
+                                                       $grafts->{$p}->{$c} = 2;
+                                                       next;
+                                               }
+                                       }
+                               }
+                               my $ct = get_commit_time($p);
+                               if ($ct < $s) {
+                                       $grafts->{$c}->{$p} = 2;
+                               } elsif ($ct > $s) {
+                                       $grafts->{$p}->{$c} = 2;
+                               }
+                               # what should we do when $ct == $s ?
+                       }
+               }
+               close $fh or croak $?;
+       });
+}
+
 # this isn't funky-filename safe, but good enough for now...
 sub graft_file_copy_cmd {
        my ($grafts, $l_map, $u) = @_;
@@ -859,7 +1051,7 @@ sub graft_file_copy_lib {
        $SVN::Error::handler = \&libsvn_skip_unknown_revs;
        while (1) {
                my $pool = SVN::Pool->new;
-               $SVN_LOG->get_log( "/$path", $min, $max, 0, 1, 1,
+               libsvn_get_log($SVN_LOG, "/$path", $min, $max, 0, 1, 1,
                        sub {
                                libsvn_graft_file_copies($grafts, $tree_paths,
                                                        $path, @_);
@@ -891,7 +1083,7 @@ sub process_merge_msg_matches {
                my $re = qr/\Q$w\E/i;
                foreach (keys %{$l_map->{$u}}) {
                        if (/$re/) {
-                               push @strong, $_;
+                               push @strong, $l_map->{$u}->{$_};
                                last;
                        }
                }
@@ -900,7 +1092,7 @@ sub process_merge_msg_matches {
                $re = qr/\Q$w\E/i;
                foreach (keys %{$l_map->{$u}}) {
                        if (/$re/) {
-                               push @strong, $_;
+                               push @strong, $l_map->{$u}->{$_};
                                last;
                        }
                }
@@ -913,7 +1105,7 @@ sub process_merge_msg_matches {
                return unless defined $rev;
        }
        foreach my $m (@strong) {
-               my ($r0, $s0) = find_rev_before($rev, $m);
+               my ($r0, $s0) = find_rev_before($rev, $m, 1);
                $grafts->{$c->{c}}->{$s0} = 1 if defined $s0;
        }
 }
@@ -943,7 +1135,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 {
@@ -1107,7 +1298,7 @@ sub parse_diff_tree {
                        croak "Error parsing $_\n";
                }
        }
-       close $diff_fh or croak $!;
+       close $diff_fh or croak $?;
 
        return \@mods;
 }
@@ -1248,12 +1439,12 @@ sub svn_checkout_tree {
                } elsif ($m->{chg} eq 'T') {
                        sys(qw(svn rm --force),$m->{file_b});
                        apply_mod_line_blob($m);
-                       sys(qw(svn add --force), $m->{file_b});
+                       sys(qw(svn add), $m->{file_b});
                        svn_check_prop_executable($m);
                } elsif ($m->{chg} eq 'A') {
                        svn_ensure_parent_path( $m->{file_b} );
                        apply_mod_line_blob($m);
-                       sys(qw(svn add --force), $m->{file_b});
+                       sys(qw(svn add), $m->{file_b});
                        svn_check_prop_executable($m);
                } else {
                        croak "Invalid chg: $m->{chg}\n";
@@ -1328,7 +1519,6 @@ sub get_commit_message {
        my %log_msg = ( msg => '' );
        open my $msg, '>', $commit_msg or croak $!;
 
-       print "commit: $commit\n";
        chomp(my $type = `git-cat-file -t $commit`);
        if ($type eq 'commit') {
                my $pid = open my $msg_fh, '-|';
@@ -1348,7 +1538,7 @@ sub get_commit_message {
                                print $msg $_ or croak $!;
                        }
                }
-               close $msg_fh or croak $!;
+               close $msg_fh or croak $?;
        }
        close $msg or croak $!;
 
@@ -1365,6 +1555,14 @@ sub get_commit_message {
        return \%log_msg;
 }
 
+sub set_svn_commit_env {
+       if (defined $LC_ALL) {
+               $ENV{LC_ALL} = $LC_ALL;
+       } else {
+               delete $ENV{LC_ALL};
+       }
+}
+
 sub svn_commit_tree {
        my ($last, $commit) = @_;
        my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
@@ -1372,11 +1570,7 @@ 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};
-       }
+       set_svn_commit_env();
        my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
        $ENV{LC_ALL} = 'C';
        unlink $commit_msg;
@@ -1562,7 +1756,7 @@ sub svn_info {
                        push @{$ret->{-order}}, $1;
                }
        }
-       close $info_fh or croak $!;
+       close $info_fh or croak $?;
        return $ret;
 }
 
@@ -1638,7 +1832,7 @@ sub do_update_index {
                }
                print $ui $x,"\0";
        }
-       close $ui or croak $!;
+       close $ui or croak $?;
 }
 
 sub index_changes {
@@ -1726,7 +1920,26 @@ sub git_commit {
                restore_index($index);
        }
        if (exists $tree_map{$tree}) {
-               push @tmp_parents, @{$tree_map{$tree}};
+               foreach my $p (@{$tree_map{$tree}}) {
+                       my $skip;
+                       foreach (@tmp_parents) {
+                               # see if a common parent is found
+                               my $mb = eval {
+                                       safe_qx('git-merge-base', $_, $p)
+                               };
+                               next if ($@ || $?);
+                               $skip = 1;
+                               last;
+                       }
+                       next if $skip;
+                       my ($url_p, $r_p, $uuid_p) = cmt_metadata($p);
+                       next if (($SVN_UUID eq $uuid_p) &&
+                                               ($log_msg->{revision} > $r_p));
+                       next if (defined $url_p && defined $SVN_URL &&
+                                               ($SVN_UUID eq $uuid_p) &&
+                                               ($url_p eq $SVN_URL));
+                       push @tmp_parents, $p;
+               }
        }
        foreach (@tmp_parents) {
                next if $seen_parent{$_};
@@ -1765,11 +1978,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 +2094,11 @@ sub svn_cmd_checkout {
 }
 
 sub check_upgrade_needed {
+       if (!-r $REVDB) {
+               -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
+               open my $fh, '>>',$REVDB or croak $!;
+               close $fh;
+       }
        my $old = eval {
                my $pid = open my $child, '-|';
                defined $pid or croak $!;
@@ -1986,6 +2208,7 @@ sub migrate_revdb {
                        init_vars();
                        exit 0 if -r $REVDB;
                        print "Upgrading svn => git mapping...\n";
+                       -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
                        open my $fh, '>>',$REVDB or croak $!;
                        close $fh;
                        rebuild();
@@ -2026,7 +2249,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);
@@ -2043,6 +2267,7 @@ sub init_vars {
        $GIT_SVN_INDEX = "$GIT_SVN_DIR/index";
        $SVN_URL = undef;
        $SVN_WC = "$GIT_SVN_DIR/tree";
+       %tree_map = ();
 }
 
 # convert GetOpt::Long specs for use by git-repo-config
@@ -2072,7 +2297,7 @@ sub set_default_vals {
        if (defined $_repack) {
                $_repack = 1000 if ($_repack <= 0);
                $_repack_nr = $_repack;
-               $_repack_flags ||= '';
+               $_repack_flags ||= '-d';
        }
 }
 
@@ -2110,6 +2335,7 @@ sub write_grafts {
                        print $fh $_ foreach @{$comments->{$c}};
                }
                my $p = $grafts->{$c};
+               my %x; # real parents
                delete $p->{$c}; # commits are not self-reproducing...
                my $pid = open my $ch, '-|';
                defined $pid or croak $!;
@@ -2117,13 +2343,41 @@ sub write_grafts {
                        exec(qw/git-cat-file commit/, $c) or croak $!;
                }
                while (<$ch>) {
-                       if (/^parent ([a-f\d]{40})/) {
-                               $p->{$1} = 1;
+                       if (/^parent ($sha1)/) {
+                               $x{$1} = $p->{$1} = 1;
                        } else {
-                               last unless /^\S/i;
+                               last unless /^\S/;
                        }
                }
                close $ch; # breaking the pipe
+
+               # if real parents are the only ones in the grafts, drop it
+               next if join(' ',sort keys %$p) eq join(' ',sort keys %x);
+
+               my (@ip, @jp, $mb);
+               my %del = %x;
+               @ip = @jp = keys %$p;
+               foreach my $i (@ip) {
+                       next if $del{$i} || $p->{$i} == 2;
+                       foreach my $j (@jp) {
+                               next if $i eq $j || $del{$j} || $p->{$j} == 2;
+                               $mb = eval { safe_qx('git-merge-base',$i,$j) };
+                               next unless $mb;
+                               chomp $mb;
+                               next if $x{$mb};
+                               if ($mb eq $j) {
+                                       delete $p->{$i};
+                                       $del{$i} = 1;
+                               } elsif ($mb eq $i) {
+                                       delete $p->{$j};
+                                       $del{$j} = 1;
+                               }
+                       }
+               }
+
+               # if real parents are the only ones in the grafts, drop it
+               next if join(' ',sort keys %$p) eq join(' ',sort keys %x);
+
                print $fh $c, ' ', join(' ', sort keys %$p),"\n";
        }
        if ($comments->{'END'}) {
@@ -2143,7 +2397,7 @@ sub read_url_paths {
 }
 
 sub extract_metadata {
-       my $id = shift;
+       my $id = shift or return (undef, undef, undef);
        my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
                                                        \s([a-f\d\-]+)$/x);
        if (!$rev || !$uuid || !$url) {
@@ -2154,6 +2408,31 @@ sub extract_metadata {
        return ($url, $rev, $uuid);
 }
 
+sub cmt_metadata {
+       return extract_metadata((grep(/^git-svn-id: /,
+               safe_qx(qw/git-cat-file commit/, shift)))[-1]);
+}
+
+sub get_commit_time {
+       my $cmt = shift;
+       defined(my $pid = open my $fh, '-|') or croak $!;
+       if (!$pid) {
+               exec qw/git-rev-list --pretty=raw -n1/, $cmt or croak $!;
+       }
+       while (<$fh>) {
+               /^committer\s(?:.+) (\d+) ([\-\+]?\d+)$/ or next;
+               my ($s, $tz) = ($1, $2);
+               if ($tz =~ s/^\+//) {
+                       $s += tz_to_s_offset($tz);
+               } elsif ($tz =~ s/^\-//) {
+                       $s -= tz_to_s_offset($tz);
+               }
+               close $fh;
+               return $s;
+       }
+       die "Can't get commit time for commit: $cmt\n";
+}
+
 sub tz_to_s_offset {
        my ($tz) = @_;
        $tz =~ s/(\d\d)$//;
@@ -2182,6 +2461,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;
@@ -2282,8 +2562,8 @@ sub libsvn_load {
        return unless $_use_lib;
        $_use_lib = eval {
                require SVN::Core;
-               if ($SVN::Core::VERSION lt '1.2.1') {
-                       die "Need SVN::Core 1.2.1 or better ",
+               if ($SVN::Core::VERSION lt '1.1.0') {
+                       die "Need SVN::Core 1.1.0 or better ",
                                        "(got $SVN::Core::VERSION) ",
                                        "Falling back to command-line svn\n";
                }
@@ -2312,47 +2592,42 @@ 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 $!;
+       # redirect STDOUT for SVN 1.1.x compatibility
+       open my $stdout, '>&', \*STDOUT or croak $!;
+       open STDOUT, '>&', $in or croak $!;
+       $| = 1; # not sure if this is necessary, better safe than sorry...
+       my ($r, $props) = $SVN->get_file($f, $rev, \*STDOUT, $pool);
+       $in->flush == 0 or croak $!;
+       open STDOUT, '>&', $stdout or croak $!;
+       close $in or croak $!;
+       close $stdout 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 {
@@ -2381,7 +2656,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 $!;
        }
@@ -2411,7 +2686,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]);
 }
 
@@ -2426,8 +2701,7 @@ sub svn_grab_base_rev {
        chomp(my $c = do { local $/; <$fh> });
        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]);
+               my ($url, $rev, $uuid) = cmt_metadata($c);
                return ($rev, $c);
        }
        return (undef, undef);
@@ -2501,7 +2775,8 @@ sub revisions_eq {
        if ($_use_lib) {
                # should be OK to use Pool here (r1 - r0) should be small
                my $pool = SVN::Pool->new;
-               $SVN->get_log("/$path", $r0, $r1, 0, 1, 1, sub {$nr++},$pool);
+               libsvn_get_log($SVN, "/$path", $r0, $r1,
+                               0, 1, 1, sub {$nr++}, $pool);
                $pool->clear;
        } else {
                my ($url, undef) = repo_path_split($SVN_URL);
@@ -2514,39 +2789,41 @@ 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;
 }
 
+sub libsvn_get_log {
+       my ($ra, @args) = @_;
+       if ($SVN::Core::VERSION le '1.2.0') {
+               splice(@args, 3, 1);
+       }
+       $ra->get_log(@args);
+}
+
 sub libsvn_new_tree {
        if (my $log_entry = libsvn_find_parent_branch(@_)) {
                return $log_entry;
@@ -2556,7 +2833,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);
 }
 
@@ -2580,6 +2857,10 @@ sub find_graft_path_parents {
                my $i = $tree_paths->{$x};
                my ($r, $parent) = find_rev_before($r0, $i, 1);
                if (defined $r && defined $parent && revisions_eq($x,$r,$r0)) {
+                       my ($url_b, undef, $uuid_b) = cmt_metadata($c);
+                       my ($url_a, undef, $uuid_a) = cmt_metadata($parent);
+                       next if ($url_a && $url_b && $url_a eq $url_b &&
+                                                       $uuid_b eq $uuid_a);
                        $grafts->{$c}->{$parent} = 1;
                }
        }
@@ -2630,7 +2911,7 @@ sub libsvn_commit_cb {
                        exit 1;
                }
        } else {
-               fetch_lib("$rev=$c");
+               fetch("$rev=$c");
        }
 }
 
@@ -2664,7 +2945,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";
@@ -2713,6 +2993,17 @@ sub revdb_get {
        return $ret;
 }
 
+sub copy_remote_ref {
+       my $origin = $_cp_remote ? $_cp_remote : 'origin';
+       my $ref = "refs/remotes/$GIT_SVN";
+       if (safe_qx('git-ls-remote', $origin, $ref)) {
+               sys(qw/git fetch/, $origin, "$ref:$ref");
+       } else {
+               die "Unable to find remote reference: ",
+                               "refs/remotes/$GIT_SVN on $origin\n";
+       }
+}
+
 package SVN::Git::Editor;
 use vars qw/@ISA/;
 use strict;
@@ -2774,13 +3065,20 @@ sub rmdirs {
                exec qw/git-ls-tree --name-only -r -z/, $self->{c} or croak $!;
        }
        local $/ = "\0";
+       my @svn_path = split m#/#, $self->{svn_path};
        while (<$fh>) {
                chomp;
-               $_ = $self->{svn_path} . '/' . $_;
-               my ($dn) = ($_ =~ m#^(.*?)/?(?:[^/]+)$#);
-               delete $rm->{$dn};
-               last unless %$rm;
+               my @dn = (@svn_path, (split m#/#, $_));
+               while (pop @dn) {
+                       delete $rm->{join '/', @dn};
+               }
+               unless (%$rm) {
+                       close $fh;
+                       return;
+               }
        }
+       close $fh;
+
        my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat});
        foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {
                $self->close_directory($bat->{$d}, $p);