git-svn: enable delta transfers during fetches when using SVN:: libs
[gitweb.git] / git-svn.perl
index f5c7d46341016a5ca77a52ad16fbcd8c3830678f..9b86d91266909fe70064f73faef3ac73f23e3993 100755 (executable)
@@ -21,6 +21,7 @@
 $ENV{LC_ALL} = 'C';
 $| = 1; # unbuffer STDOUT
 
+sub fatal (@) { print STDERR $@; exit 1 }
 # If SVN:: library support is added, please make the dependencies
 # optional and preserve the capability to use the command-line client.
 # use eval { require SVN::... } to make it lazy load
 memoize('cmt_metadata');
 memoize('get_commit_time');
 
-my ($SVN_PATH, $SVN, $SVN_LOG, $_use_lib);
+my ($SVN, $_use_lib);
+
+sub nag_lib {
+       print STDERR <<EOF;
+! Please consider installing the SVN Perl libraries (version 1.1.0 or
+! newer).  You will generally get better performance and fewer bugs,
+! especially if you:
+! 1) have a case-insensitive filesystem
+! 2) replace symlinks with files (and vice-versa) in commits
+
+EOF
+}
+
 $_use_lib = 1 unless $ENV{GIT_SVN_NO_LIB};
 libsvn_load();
+nag_lib() unless $_use_lib;
+
 my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS};
 my $sha1 = qr/[a-f\d]{40}/;
 my $sha1_short = qr/[a-f\d]{4,40}/;
@@ -52,7 +67,8 @@
        $_template, $_shared, $_no_default_regex, $_no_graft_copy,
        $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit,
        $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m,
-       $_merge, $_strategy, $_dry_run, $_ignore_nodate);
+       $_merge, $_strategy, $_dry_run, $_ignore_nodate, $_non_recursive,
+       $_username, $_config_dir, $_no_auth_cache, $_xfer_delta);
 my (@_branch_from, %tree_map, %users, %rusers, %equiv);
 my ($_svn_co_url_revs, $_svn_pg_peg_revs);
 my @repo_path_split_cache;
@@ -65,6 +81,9 @@
                'repack:i' => \$_repack,
                'no-metadata' => \$_no_metadata,
                'quiet|q' => \$_q,
+               'username=s' => \$_username,
+               'config-dir=s' => \$_config_dir,
+               'no-auth-cache' => \$_no_auth_cache,
                'ignore-nodate' => \$_ignore_nodate,
                'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
 
                          'incremental' => \$_incremental,
                          'oneline' => \$_oneline,
                          'show-commit' => \$_show_commit,
+                         'non-recursive' => \$_non_recursive,
                          'authors-file|A=s' => \$_authors,
                        } ],
        'commit-diff' => [ \&commit_diff, 'Commit a diff between two trees',
                        { 'message|m=s' => \$_message,
                          'file|F=s' => \$_file,
+                         'revision|r=s' => \$_revision,
                        %cmt_opts } ],
        dcommit => [ \&dcommit, 'Commit several diffs to merge with upstream',
                        { 'merge|m|M' => \$_merge,
@@ -168,11 +189,11 @@ sub usage {
 
        foreach (sort keys %cmd) {
                next if $cmd && $cmd ne $_;
-               print $fd '  ',pack('A13',$_),$cmd{$_}->[1],"\n";
+               print $fd '  ',pack('A17',$_),$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 ?
+                       print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
                                                        "--$_" : "-$_" }
                                                split /\|/,$_)," $x\n";
                }
@@ -216,7 +237,7 @@ sub rebuild {
                my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`);
                next if (!@commit); # skip merges
                my ($url, $rev, $uuid) = extract_metadata($commit[$#commit]);
-               if (!$rev || !$uuid) {
+               if (!defined $rev || !$uuid) {
                        croak "Unable to extract revision or UUID from ",
                                "$c, $commit[$#commit]\n";
                }
@@ -361,10 +382,7 @@ sub fetch_cmd {
 sub fetch_lib {
        my (@parents) = @_;
        $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
-       my $repo;
-       ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
-       $SVN_LOG ||= libsvn_connect($repo);
-       $SVN ||= libsvn_connect($repo);
+       $SVN ||= libsvn_connect($SVN_URL);
        my ($last_rev, $last_commit) = svn_grab_base_rev();
        my ($base, $head) = libsvn_parse_revision($last_rev);
        if ($base > $head) {
@@ -406,7 +424,7 @@ sub fetch_lib {
                        # performance sucks with it enabled, so it's much
                        # faster to fetch revision ranges instead of relying
                        # on the limiter.
-                       libsvn_get_log($SVN_LOG, '/'.$SVN_PATH,
+                       libsvn_get_log(libsvn_dup_ra($SVN), [''],
                                        $min, $max, 0, 1, 1,
                                sub {
                                        my $log_msg;
@@ -508,7 +526,6 @@ sub commit_lib {
        my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
 
        my $repo;
-       ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
        set_svn_commit_env();
        foreach my $c (@revs) {
                my $log_msg = get_commit_message($c, $commit_msg);
@@ -517,13 +534,11 @@ sub commit_lib {
                # can't track down... (it's probably in the SVN code)
                defined(my $pid = open my $fh, '-|') or croak $!;
                if (!$pid) {
-                       $SVN_LOG = libsvn_connect($repo);
-                       $SVN = libsvn_connect($repo);
                        my $ed = SVN::Git::Editor->new(
                                        {       r => $r_last,
-                                               ra => $SVN,
+                                               ra => libsvn_dup_ra($SVN),
                                                c => $c,
-                                               svn_path => $SVN_PATH
+                                               svn_path => $SVN->{svn_path},
                                        },
                                        $SVN->get_commit_editor(
                                                $log_msg->{msg},
@@ -555,7 +570,7 @@ sub commit_lib {
                                $no = 1;
                        }
                }
-               close $fh or croak $?;
+               close $fh or exit 1;
                if (! defined $r_new && ! defined $cmt_new) {
                        unless ($no) {
                                die "Failed to parse revision information\n";
@@ -571,11 +586,28 @@ sub commit_lib {
 sub dcommit {
        my $gs = "refs/remotes/$GIT_SVN";
        chomp(my @refs = safe_qx(qw/git-rev-list --no-merges/, "$gs..HEAD"));
+       my $last_rev;
        foreach my $d (reverse @refs) {
+               if (quiet_run('git-rev-parse','--verify',"$d~1") != 0) {
+                       die "Commit $d\n",
+                           "has no parent commit, and therefore ",
+                           "nothing to diff against.\n",
+                           "You should be working from a repository ",
+                           "originally created by git-svn\n";
+               }
+               unless (defined $last_rev) {
+                       (undef, $last_rev, undef) = cmt_metadata("$d~1");
+                       unless (defined $last_rev) {
+                               die "Unable to extract revision information ",
+                                   "from commit $d~1\n";
+                       }
+               }
                if ($_dry_run) {
                        print "diff-tree $d~1 $d\n";
                } else {
-                       commit_diff("$d~1", $d);
+                       if (my $r = commit_diff("$d~1", $d, undef, $last_rev)) {
+                               $last_rev = $r;
+                       } # else: no changes, same $last_rev
                }
        }
        return if $_dry_run;
@@ -590,7 +622,7 @@ sub dcommit {
        } else {
                print "No changes between current HEAD and $gs\n",
                      "Hard resetting to the latest $gs\n";
-               @finish = qw/reset --hard/;
+               @finish = qw/reset --mixed/;
        }
        sys('git', @finish, $gs);
 }
@@ -624,10 +656,9 @@ sub show_ignore_cmd {
 
 sub show_ignore_lib {
        my $repo;
-       ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
-       $SVN ||= libsvn_connect($repo);
+       $SVN ||= libsvn_connect($SVN_URL);
        my $r = defined $_revision ? $_revision : $SVN->get_latest_revnum;
-       libsvn_traverse_ignore(\*STDOUT, $SVN_PATH, $r);
+       libsvn_traverse_ignore(\*STDOUT, $SVN->{svn_path}, $r);
 }
 
 sub graft_branches {
@@ -682,12 +713,17 @@ sub multi_init {
                }
                $_trunk = $url . $_trunk;
        }
+       my $ch_id;
        if ($GIT_SVN eq 'git-svn') {
-               print "GIT_SVN_ID set to 'trunk' for $_trunk\n";
+               $ch_id = 1;
                $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
        }
        init_vars();
-       init($_trunk);
+       unless (-d $GIT_SVN_DIR) {
+               print "GIT_SVN_ID set to 'trunk' for $_trunk\n" if $ch_id;
+               init($_trunk);
+               sys('git-repo-config', 'svn.trunk', $_trunk);
+       }
        complete_url_ls_init($url, $_branches, '--branches/-b', '');
        complete_url_ls_init($url, $_tags, '--tags/-t', 'tags/');
 }
@@ -747,13 +783,18 @@ sub show_log {
                        # ignore
                } elsif (/^:\d{6} \d{6} $sha1_short/o) {
                        push @{$c->{raw}}, $_;
+               } elsif (/^[ACRMDT]\t/) {
+                       # we could add $SVN->{svn_path} here, but that requires
+                       # remote access at the moment (repo_path_split)...
+                       s#^([ACRMDT])\t#   $1 #;
+                       push @{$c->{changed}}, $_;
                } elsif (/^diff /) {
                        $d = 1;
                        push @{$c->{diff}}, $_;
                } elsif ($d) {
                        push @{$c->{diff}}, $_;
                } elsif (/^    (git-svn-id:.+)$/) {
-                       (undef, $c->{r}, undef) = extract_metadata($1);
+                       ($c->{url}, $c->{r}, undef) = extract_metadata($1);
                } elsif (s/^    //) {
                        push @{$c->{l}}, $_;
                }
@@ -789,6 +830,14 @@ sub commit_diff {
                print STDERR "Needed URL or usable git-svn id command-line\n";
                commit_diff_usage();
        }
+       my $r = shift;
+       unless (defined $r) {
+               if (defined $_revision) {
+                       $r = $_revision
+               } else {
+                       die "-r|--revision is a required argument\n";
+               }
+       }
        if (defined $_message && defined $_file) {
                print STDERR "Both --message/-m and --file/-F specified ",
                                "for the commit message.\n",
@@ -801,26 +850,37 @@ sub commit_diff {
                $_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);
+       $SVN ||= libsvn_connect($SVN_URL);
+       if ($r eq 'HEAD') {
+               $r = $SVN->get_latest_revnum;
+       } elsif ($r !~ /^\d+$/) {
+               die "revision argument: $r not understood by git-svn\n";
+       }
        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
+       my $rev_committed;
+       my $ed = SVN::Git::Editor->new({        r => $r,
+                                               ra => libsvn_dup_ra($SVN),
+                                               c => $tb,
+                                               svn_path => $SVN->{svn_path}
                                        },
                                $SVN->get_commit_editor($_message,
-                                       sub {print "Committed $_[0]\n"},@lock)
+                                       sub {
+                                               $rev_committed = $_[0];
+                                               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;
-       }
+       eval {
+               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;
+               }
+       };
+       fatal "$@\n" if $@;
        $_message = $_file = undef;
+       return $rev_committed;
 }
 
 ########################### utility functions #########################
@@ -845,7 +905,8 @@ 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;
+       push @cmd, '-r' unless $_non_recursive;
+       push @cmd, qw/--raw --name-status/ if $_verbose;
        return @cmd unless defined $r_max;
        if ($r_max == $r_min) {
                push @cmd, '--max-count=1';
@@ -856,7 +917,7 @@ sub git_svn_log_cmd {
                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 (defined $c_min && defined $c_max) {
                        if ($r_max > $r_max) {
                                push @cmd, "$c_min..$c_max";
                        } else {
@@ -937,16 +998,21 @@ sub complete_url_ls_init {
                                print STDERR "W: Unrecognized URL: $u\n";
                                die "This should never happen\n";
                        }
+                       # don't try to init already existing refs
                        my $id = $pfx.$1;
-                       print "init $u => $id\n";
                        $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
                        init_vars();
-                       init($u);
+                       unless (-d $GIT_SVN_DIR) {
+                               print "init $u => $id\n";
+                               init($u);
+                       }
                }
                exit 0;
        }
        waitpid $pid, 0;
        croak $? if $?;
+       my ($n) = ($switch =~ /^--(\w+)/);
+       sys('git-repo-config', "svn.$n", $var);
 }
 
 sub common_prefix {
@@ -1076,8 +1142,7 @@ sub graft_file_copy_lib {
        my $tree_paths = $l_map->{$u};
        my $pfx = common_prefix([keys %$tree_paths]);
        my ($repo, $path) = repo_path_split($u.$pfx);
-       $SVN_LOG ||= libsvn_connect($repo);
-       $SVN ||= libsvn_connect($repo);
+       $SVN = libsvn_connect($repo);
 
        my ($base, $head) = libsvn_parse_revision();
        my $inc = 1000;
@@ -1086,7 +1151,8 @@ sub graft_file_copy_lib {
        $SVN::Error::handler = \&libsvn_skip_unknown_revs;
        while (1) {
                my $pool = SVN::Pool->new;
-               libsvn_get_log($SVN_LOG, "/$path", $min, $max, 0, 1, 1,
+               libsvn_get_log(libsvn_dup_ra($SVN), [$path],
+                              $min, $max, 0, 1, 1,
                        sub {
                                libsvn_graft_file_copies($grafts, $tree_paths,
                                                        $path, @_);
@@ -1196,13 +1262,9 @@ sub repo_path_split {
                        return ($u, $full_url);
                }
        }
-
        if ($_use_lib) {
                my $tmp = libsvn_connect($full_url);
-               my $url = $tmp->get_repos_root;
-               $full_url =~ s#^\Q$url\E/*##;
-               push @repo_path_split_cache, qr/^(\Q$url\E)/;
-               return ($url, $full_url);
+               return ($tmp->{repos_root}, $tmp->{svn_path});
        } else {
                my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i);
                $path =~ s#^/+##;
@@ -1470,10 +1532,13 @@ sub svn_checkout_tree {
                        apply_mod_line_blob($m);
                        svn_check_prop_executable($m);
                } elsif ($m->{chg} eq 'T') {
-                       sys(qw(svn rm --force),$m->{file_b});
-                       apply_mod_line_blob($m);
-                       sys(qw(svn add), $m->{file_b});
                        svn_check_prop_executable($m);
+                       apply_mod_line_blob($m);
+                       if ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) {
+                               sys(qw(svn propdel svn:special), $m->{file_b});
+                       } else {
+                               sys(qw(svn propset svn:special *),$m->{file_b});
+                       }
                } elsif ($m->{chg} eq 'A') {
                        svn_ensure_parent_path( $m->{file_b} );
                        apply_mod_line_blob($m);
@@ -2429,7 +2494,7 @@ sub extract_metadata {
        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) {
+       if (!defined $rev || !$uuid || !$url) {
                # some of the original repositories I made had
                # identifiers like this:
                ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
@@ -2551,6 +2616,12 @@ sub show_commit {
        }
 }
 
+sub show_commit_changed_paths {
+       my ($c) = @_;
+       return unless $c->{changed};
+       print "Changed paths:\n", @{$c->{changed}};
+}
+
 sub show_commit_normal {
        my ($c) = @_;
        print '-' x72, "\nr$c->{r} | ";
@@ -2560,7 +2631,8 @@ sub show_commit_normal {
        my $nr_line = 0;
 
        if (my $l = $c->{l}) {
-               while ($l->[$#$l] eq "\n" && $l->[($#$l - 1)] eq "\n") {
+               while ($l->[$#$l] eq "\n" && $#$l > 0
+                                         && $l->[($#$l - 1)] eq "\n") {
                        pop @$l;
                }
                $nr_line = scalar @$l;
@@ -2572,11 +2644,15 @@ sub show_commit_normal {
                        } else {
                                $nr_line .= ' lines';
                        }
-                       print $nr_line, "\n\n";
+                       print $nr_line, "\n";
+                       show_commit_changed_paths($c);
+                       print "\n";
                        print $_ foreach @$l;
                }
        } else {
-               print "1 line\n\n";
+               print "1 line\n";
+               show_commit_changed_paths($c);
+               print "\n";
 
        }
        foreach my $x (qw/raw diff/) {
@@ -2599,29 +2675,185 @@ sub libsvn_load {
                require SVN::Ra;
                require SVN::Delta;
                push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor';
+               push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor';
+               *SVN::Git::Fetcher::process_rm = *process_rm;
+               *SVN::Git::Fetcher::safe_qx = *safe_qx;
                my $kill_stupid_warnings = $SVN::Node::none.$SVN::Node::file.
                                        $SVN::Node::dir.$SVN::Node::unknown.
                                        $SVN::Node::none.$SVN::Node::file.
-                                       $SVN::Node::dir.$SVN::Node::unknown;
+                                       $SVN::Node::dir.$SVN::Node::unknown.
+                                       $SVN::Auth::SSL::CNMISMATCH.
+                                       $SVN::Auth::SSL::NOTYETVALID.
+                                       $SVN::Auth::SSL::EXPIRED.
+                                       $SVN::Auth::SSL::UNKNOWNCA.
+                                       $SVN::Auth::SSL::OTHER;
                1;
        };
 }
 
+sub _simple_prompt {
+       my ($cred, $realm, $default_username, $may_save, $pool) = @_;
+       $may_save = undef if $_no_auth_cache;
+       $default_username = $_username if defined $_username;
+       if (defined $default_username && length $default_username) {
+               if (defined $realm && length $realm) {
+                       print "Authentication realm: $realm\n";
+               }
+               $cred->username($default_username);
+       } else {
+               _username_prompt($cred, $realm, $may_save, $pool);
+       }
+       $cred->password(_read_password("Password for '" .
+                                      $cred->username . "': ", $realm));
+       $cred->may_save($may_save);
+       $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub _ssl_server_trust_prompt {
+       my ($cred, $realm, $failures, $cert_info, $may_save, $pool) = @_;
+       $may_save = undef if $_no_auth_cache;
+       print "Error validating server certificate for '$realm':\n";
+       if ($failures & $SVN::Auth::SSL::UNKNOWNCA) {
+               print " - The certificate is not issued by a trusted ",
+                     "authority. Use the\n",
+                     "   fingerprint to validate the certificate manually!\n";
+       }
+       if ($failures & $SVN::Auth::SSL::CNMISMATCH) {
+               print " - The certificate hostname does not match.\n";
+       }
+       if ($failures & $SVN::Auth::SSL::NOTYETVALID) {
+               print " - The certificate is not yet valid.\n";
+       }
+       if ($failures & $SVN::Auth::SSL::EXPIRED) {
+               print " - The certificate has expired.\n";
+       }
+       if ($failures & $SVN::Auth::SSL::OTHER) {
+               print " - The certificate has an unknown error.\n";
+       }
+       printf( "Certificate information:\n".
+               " - Hostname: %s\n".
+               " - Valid: from %s until %s\n".
+               " - Issuer: %s\n".
+               " - Fingerprint: %s\n",
+               map $cert_info->$_, qw(hostname valid_from valid_until
+                                      issuer_dname fingerprint) );
+       my $choice;
+prompt:
+       print $may_save ?
+             "(R)eject, accept (t)emporarily or accept (p)ermanently? " :
+             "(R)eject or accept (t)emporarily? ";
+       $choice = lc(substr(<STDIN> || 'R', 0, 1));
+       if ($choice =~ /^t$/i) {
+               $cred->may_save(undef);
+       } elsif ($choice =~ /^r$/i) {
+               return -1;
+       } elsif ($may_save && $choice =~ /^p$/i) {
+               $cred->may_save($may_save);
+       } else {
+               goto prompt;
+       }
+       $cred->accepted_failures($failures);
+       $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub _ssl_client_cert_prompt {
+       my ($cred, $realm, $may_save, $pool) = @_;
+       $may_save = undef if $_no_auth_cache;
+       print "Client certificate filename: ";
+       chomp(my $filename = <STDIN>);
+       $cred->cert_file($filename);
+       $cred->may_save($may_save);
+       $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub _ssl_client_cert_pw_prompt {
+       my ($cred, $realm, $may_save, $pool) = @_;
+       $may_save = undef if $_no_auth_cache;
+       $cred->password(_read_password("Password: ", $realm));
+       $cred->may_save($may_save);
+       $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub _username_prompt {
+       my ($cred, $realm, $may_save, $pool) = @_;
+       $may_save = undef if $_no_auth_cache;
+       if (defined $realm && length $realm) {
+               print "Authentication realm: $realm\n";
+       }
+       my $username;
+       if (defined $_username) {
+               $username = $_username;
+       } else {
+               print "Username: ";
+               chomp($username = <STDIN>);
+       }
+       $cred->username($username);
+       $cred->may_save($may_save);
+       $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub _read_password {
+       my ($prompt, $realm) = @_;
+       print $prompt;
+       require Term::ReadKey;
+       Term::ReadKey::ReadMode('noecho');
+       my $password = '';
+       while (defined(my $key = Term::ReadKey::ReadKey(0))) {
+               last if $key =~ /[\012\015]/; # \n\r
+               $password .= $key;
+       }
+       Term::ReadKey::ReadMode('restore');
+       print "\n";
+       $password;
+}
+
 sub libsvn_connect {
        my ($url) = @_;
-       my $auth = SVN::Core::auth_open([SVN::Client::get_simple_provider(),
-                         SVN::Client::get_ssl_server_trust_file_provider(),
-                         SVN::Client::get_username_provider()]);
-       my $s = eval { SVN::Ra->new(url => $url, auth => $auth) };
-       return $s;
+       SVN::_Core::svn_config_ensure($_config_dir, undef);
+       my ($baton, $callbacks) = SVN::Core::auth_open_helper([
+           SVN::Client::get_simple_provider(),
+           SVN::Client::get_ssl_server_trust_file_provider(),
+           SVN::Client::get_simple_prompt_provider(
+             \&_simple_prompt, 2),
+           SVN::Client::get_ssl_client_cert_prompt_provider(
+             \&_ssl_client_cert_prompt, 2),
+           SVN::Client::get_ssl_client_cert_pw_prompt_provider(
+             \&_ssl_client_cert_pw_prompt, 2),
+           SVN::Client::get_username_provider(),
+           SVN::Client::get_ssl_server_trust_prompt_provider(
+             \&_ssl_server_trust_prompt),
+           SVN::Client::get_username_prompt_provider(
+             \&_username_prompt, 2),
+         ]);
+       my $config = SVN::Core::config_get_config($_config_dir);
+       my $ra = SVN::Ra->new(url => $url, auth => $baton,
+                             config => $config,
+                             pool => SVN::Pool->new,
+                             auth_provider_callbacks => $callbacks);
+
+       my $df = $ENV{GIT_SVN_DELTA_FETCH};
+       if (defined $df) {
+               $_xfer_delta = $df;
+       } else {
+               $_xfer_delta = ($url =~ m#^file://#) ? undef : 1;
+       }
+       $ra->{svn_path} = $url;
+       $ra->{repos_root} = $ra->get_repos_root;
+       $ra->{svn_path} =~ s#^\Q$ra->{repos_root}\E/*##;
+       push @repo_path_split_cache, qr/^(\Q$ra->{repos_root}\E)/;
+       return $ra;
+}
+
+sub libsvn_dup_ra {
+       my ($ra) = @_;
+       SVN::Ra->new(map { $_ => $ra->{$_} } qw/config url
+                    auth auth_provider_callbacks repos_root svn_path/);
 }
 
 sub libsvn_get_file {
-       my ($gui, $f, $rev) = @_;
-       my $p = $f;
-       if (length $SVN_PATH > 0) {
-               return unless ($p =~ s#^\Q$SVN_PATH\E/##);
-       }
+       my ($gui, $f, $rev, $chg) = @_;
+       $f =~ s#^/##;
+       print "\t$chg\t$f\n" unless $_q;
 
        my ($hash, $pid, $in, $out);
        my $pool = SVN::Pool->new;
@@ -2657,7 +2889,7 @@ sub libsvn_get_file {
                waitpid $pid, 0;
                $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
        }
-       print $gui $mode,' ',$hash,"\t",$p,"\0" or croak $!;
+       print $gui $mode,' ',$hash,"\t",$f,"\0" or croak $!;
 }
 
 sub libsvn_log_entry {
@@ -2675,7 +2907,6 @@ sub libsvn_log_entry {
 
 sub process_rm {
        my ($gui, $last_commit, $f) = @_;
-       $f =~ s#^\Q$SVN_PATH\E/?## or return;
        # remove entire directories.
        if (safe_qx('git-ls-tree',$last_commit,'--',$f) =~ /^040000 tree/) {
                defined(my $pid = open my $ls, '-|') or croak $!;
@@ -2694,12 +2925,36 @@ sub process_rm {
 }
 
 sub libsvn_fetch {
+       $_xfer_delta ? libsvn_fetch_delta(@_) : libsvn_fetch_full(@_);
+}
+
+sub libsvn_fetch_delta {
+       my ($last_commit, $paths, $rev, $author, $date, $msg) = @_;
+       my $pool = SVN::Pool->new;
+       my $ed = SVN::Git::Fetcher->new({ c => $last_commit, ra => $SVN,
+                                         paths => $paths });
+       my $reporter = $SVN->do_update($rev, '', 1, $ed, $pool);
+       my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
+       my (undef, $last_rev, undef) = cmt_metadata($last_commit);
+       $reporter->set_path('', $last_rev, 0, @lock, $pool);
+       $reporter->finish_report($pool);
+       $pool->clear;
+       libsvn_log_entry($rev, $author, $date, $msg, [$last_commit]);
+}
+
+sub libsvn_fetch_full {
        my ($last_commit, $paths, $rev, $author, $date, $msg) = @_;
        open my $gui, '| git-update-index -z --index-info' or croak $!;
        my @amr;
+       my $p = $SVN->{svn_path};
        foreach my $f (keys %$paths) {
                my $m = $paths->{$f}->action();
-               $f =~ s#^/+##;
+               if (length $p) {
+                       $f =~ s#^/\Q$p\E/##;
+                       next if $f =~ m#^/#;
+               } else {
+                       $f =~ s#^/##;
+               }
                if ($m =~ /^[DR]$/) {
                        print "\t$m\t$f\n" unless $_q;
                        process_rm($gui, $last_commit, $f);
@@ -2724,8 +2979,7 @@ sub libsvn_fetch {
                $pool->clear;
        }
        foreach (@amr) {
-               print "\t$_->[0]\t$_->[1]\n" unless $_q;
-               libsvn_get_file($gui, $_->[1], $rev)
+               libsvn_get_file($gui, $_->[1], $rev, $_->[0]);
        }
        close $gui or croak $?;
        return libsvn_log_entry($rev, $author, $date, $msg, [$last_commit]);
@@ -2790,9 +3044,9 @@ sub libsvn_parse_revision {
 
 sub libsvn_traverse {
        my ($gui, $pfx, $path, $rev, $files) = @_;
-       my $cwd = "$pfx/$path";
+       my $cwd = length $pfx ? "$pfx/$path" : $path;
        my $pool = SVN::Pool->new;
-       $cwd =~ s#^/+##g;
+       $cwd =~ s#^\Q$SVN->{svn_path}\E##;
        my ($dirent, $r, $props) = $SVN->get_dir($cwd, $rev, $pool);
        foreach my $d (keys %$dirent) {
                my $t = $dirent->{$d}->kind;
@@ -2803,8 +3057,7 @@ sub libsvn_traverse {
                        if (defined $files) {
                                push @$files, $file;
                        } else {
-                               print "\tA\t$file\n" unless $_q;
-                               libsvn_get_file($gui, $file, $rev);
+                               libsvn_get_file($gui, $file, $rev, 'A');
                        }
                }
        }
@@ -2817,7 +3070,7 @@ sub libsvn_traverse_ignore {
        my $pool = SVN::Pool->new;
        my ($dirent, undef, $props) = $SVN->get_dir($path, $r, $pool);
        my $p = $path;
-       $p =~ s#^\Q$SVN_PATH\E/?##;
+       $p =~ s#^\Q$SVN->{svn_path}\E/##;
        print $fh length $p ? "\n# $p\n" : "\n# /\n";
        if (my $s = $props->{'svn:ignore'}) {
                $s =~ s/[\r\n]+/\n/g;
@@ -2844,7 +3097,7 @@ sub revisions_eq {
        if ($_use_lib) {
                # should be OK to use Pool here (r1 - r0) should be small
                my $pool = SVN::Pool->new;
-               libsvn_get_log($SVN, "/$path", $r0, $r1,
+               libsvn_get_log($SVN, [$path], $r0, $r1,
                                0, 1, 1, sub {$nr++}, $pool);
                $pool->clear;
        } else {
@@ -2859,7 +3112,7 @@ sub revisions_eq {
 
 sub libsvn_find_parent_branch {
        my ($paths, $rev, $author, $date, $msg) = @_;
-       my $svn_path = '/'.$SVN_PATH;
+       my $svn_path = '/'.$SVN->{svn_path};
 
        # look for a parent from another branch:
        my $i = $paths->{$svn_path} or return;
@@ -2870,7 +3123,7 @@ sub libsvn_find_parent_branch {
        $branch_from =~ s#^/##;
        my $l_map = {};
        read_url_paths_all($l_map, '', "$GIT_DIR/svn");
-       my $url = $SVN->{url};
+       my $url = $SVN->{repos_root};
        defined $l_map->{$url} or return;
        my $id = $l_map->{$url}->{$branch_from};
        if (!defined $id && $_follow_parent) {
@@ -2892,7 +3145,7 @@ sub libsvn_find_parent_branch {
                        $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
                        init_vars();
                        $SVN_URL = "$url/$branch_from";
-                       $SVN_LOG = $SVN = undef;
+                       $SVN = undef;
                        setup_git_svn();
                        # we can't assume SVN_URL exists at r+1:
                        $_revision = "0:$r";
@@ -2908,7 +3161,11 @@ sub libsvn_find_parent_branch {
                unlink $GIT_SVN_INDEX;
                print STDERR "Found branch parent: ($GIT_SVN) $parent\n";
                sys(qw/git-read-tree/, $parent);
-               return libsvn_fetch($parent, $paths, $rev,
+               # I can't seem to get do_switch() to work correctly with
+               # the SWIG interface (TypeError when passing switch_url...),
+               # so we'll unconditionally bypass the delta interface here
+               # for now
+               return libsvn_fetch_full($parent, $paths, $rev,
                                        $author, $date, $msg);
        }
        print STDERR "Nope, branch point not imported or unknown\n";
@@ -2928,9 +3185,19 @@ sub libsvn_new_tree {
                return $log_entry;
        }
        my ($paths, $rev, $author, $date, $msg) = @_;
-       open my $gui, '| git-update-index -z --index-info' or croak $!;
-       libsvn_traverse($gui, '', $SVN_PATH, $rev);
-       close $gui or croak $?;
+       if ($_xfer_delta) {
+               my $pool = SVN::Pool->new;
+               my $ed = SVN::Git::Fetcher->new({paths => $paths, ra => $SVN});
+               my $reporter = $SVN->do_update($rev, '', 1, $ed, $pool);
+               my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
+               $reporter->set_path('', $rev, 1, @lock, $pool);
+               $reporter->finish_report($pool);
+               $pool->clear;
+       } else {
+               open my $gui, '| git-update-index -z --index-info' or croak $!;
+               libsvn_traverse($gui, '', $SVN->{svn_path}, $rev);
+               close $gui or croak $?;
+       }
        return libsvn_log_entry($rev, $author, $date, $msg);
 }
 
@@ -3014,11 +3281,10 @@ sub libsvn_commit_cb {
 
 sub libsvn_ls_fullurl {
        my $fullurl = shift;
-       my ($repo, $path) = repo_path_split($fullurl);
-       $SVN ||= libsvn_connect($repo);
+       $SVN ||= libsvn_connect($fullurl);
        my @ret;
        my $pool = SVN::Pool->new;
-       my ($dirent, undef, undef) = $SVN->get_dir($path,
+       my ($dirent, undef, undef) = $SVN->get_dir($SVN->{svn_path},
                                                $SVN->get_latest_revnum, $pool);
        foreach my $d (keys %$dirent) {
                if ($dirent->{$d}->kind == $SVN::Node::dir) {
@@ -3040,8 +3306,9 @@ sub libsvn_skip_unknown_revs {
        # Wonderfully consistent library, eh?
        # 160013 - svn:// and file://
        # 175002 - http(s)://
+       # 175007 - http(s):// (this repo required authorization, too...)
        #   More codes may be discovered later...
-       if ($errno == 175002 || $errno == 160013) {
+       if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
                return;
        }
        croak "Error from SVN, ($errno): ", $err->expanded_message,"\n";
@@ -3095,11 +3362,153 @@ sub copy_remote_ref {
        my $ref = "refs/remotes/$GIT_SVN";
        if (safe_qx('git-ls-remote', $origin, $ref)) {
                sys(qw/git fetch/, $origin, "$ref:$ref");
-       } else {
+       } elsif ($_cp_remote && !$_upgrade) {
                die "Unable to find remote reference: ",
                                "refs/remotes/$GIT_SVN on $origin\n";
        }
 }
+package SVN::Git::Fetcher;
+use vars qw/@ISA/;
+use strict;
+use warnings;
+use Carp qw/croak/;
+use IO::File qw//;
+
+# file baton members: path, mode_a, mode_b, pool, fh, blob, base
+sub new {
+       my ($class, $git_svn) = @_;
+       my $self = SVN::Delta::Editor->new;
+       bless $self, $class;
+       open my $gui, '| git-update-index -z --index-info' or croak $!;
+       $self->{gui} = $gui;
+       $self->{c} = $git_svn->{c} if exists $git_svn->{c};
+       if (my $p = $git_svn->{paths} && $git_svn->{ra}) {
+               my $s = $git_svn->{ra}->{svn_path};
+               $s = length $s ? qr#^/\Q$s\E/# : qr#^/#;
+               $self->{paths} = { map { my $x = $_;
+                                        $x =~ s/$s//;
+                                        $x => $p->{$_} } keys %$p };
+       }
+       require Digest::MD5;
+       $self;
+}
+
+sub delete_entry {
+       my ($self, $path, $rev, $pb) = @_;
+       process_rm($self->{gui}, $self->{c}, $path);
+       undef;
+}
+
+sub open_file {
+       my ($self, $path, $pb, $rev) = @_;
+       my ($mode, $blob) = (safe_qx('git-ls-tree',$self->{c},'--',$path)
+                            =~ /^(\d{6}) blob ([a-f\d]{40})\t/);
+       { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob,
+         pool => SVN::Pool->new };
+}
+
+sub add_file {
+       my ($self, $path, $pb, $cp_path, $cp_rev) = @_;
+       { path => $path, mode_a => 100644, mode_b => 100644,
+         pool => SVN::Pool->new };
+}
+
+sub change_file_prop {
+       my ($self, $fb, $prop, $value) = @_;
+       if ($prop eq 'svn:executable') {
+               if ($fb->{mode_b} != 120000) {
+                       $fb->{mode_b} = defined $value ? 100755 : 100644;
+               }
+       } elsif ($prop eq 'svn:special') {
+               $fb->{mode_b} = defined $value ? 120000 : 100644;
+       }
+       undef;
+}
+
+sub apply_textdelta {
+       my ($self, $fb, $exp) = @_;
+       my $fh = IO::File->new_tmpfile;
+       $fh->autoflush(1);
+       # $fh gets auto-closed() by SVN::TxDelta::apply(),
+       # (but $base does not,) so dup() it for reading in close_file
+       open my $dup, '<&', $fh or croak $!;
+       my $base = IO::File->new_tmpfile;
+       $base->autoflush(1);
+       if ($fb->{blob}) {
+               defined (my $pid = fork) or croak $!;
+               if (!$pid) {
+                       open STDOUT, '>&', $base or croak $!;
+                       print STDOUT 'link ' if ($fb->{mode_a} == 120000);
+                       exec qw/git-cat-file blob/, $fb->{blob} or croak $!;
+               }
+               waitpid $pid, 0;
+               croak $? if $?;
+
+               if (defined $exp) {
+                       seek $base, 0, 0 or croak $!;
+                       my $md5 = Digest::MD5->new;
+                       $md5->addfile($base);
+                       my $got = $md5->hexdigest;
+                       die "Checksum mismatch: $fb->{path} $fb->{blob}\n",
+                           "expected: $exp\n",
+                           "     got: $got\n" if ($got ne $exp);
+               }
+       }
+       seek $base, 0, 0 or croak $!;
+       $fb->{fh} = $dup;
+       $fb->{base} = $base;
+       [ SVN::TxDelta::apply($base, $fh, undef, $fb->{path}, $fb->{pool}) ];
+}
+
+sub close_file {
+       my ($self, $fb, $exp) = @_;
+       my $hash;
+       my $path = $fb->{path};
+       if (my $fh = $fb->{fh}) {
+               seek($fh, 0, 0) or croak $!;
+               my $md5 = Digest::MD5->new;
+               $md5->addfile($fh);
+               my $got = $md5->hexdigest;
+               die "Checksum mismatch: $path\n",
+                   "expected: $exp\n    got: $got\n" if ($got ne $exp);
+               seek($fh, 0, 0) or croak $!;
+               if ($fb->{mode_b} == 120000) {
+                       read($fh, my $buf, 5) == 5 or croak $!;
+                       $buf eq 'link ' or die "$path has mode 120000",
+                                              "but is not a link\n";
+               }
+               defined(my $pid = open my $out,'-|') or die "Can't fork: $!\n";
+               if (!$pid) {
+                       open STDIN, '<&', $fh or croak $!;
+                       exec qw/git-hash-object -w --stdin/ or croak $!;
+               }
+               chomp($hash = do { local $/; <$out> });
+               close $out or croak $!;
+               close $fh or croak $!;
+               $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n";
+               close $fb->{base} or croak $!;
+       } else {
+               $hash = $fb->{blob} or die "no blob information\n";
+       }
+       $fb->{pool}->clear;
+       my $gui = $self->{gui};
+       print $gui "$fb->{mode_b} $hash\t$path\0" or croak $!;
+       print "\t", $self->{paths}->{$path}->action,
+             "\t$path\n" if defined $self->{paths}->{$path};
+       undef;
+}
+
+sub abort_edit {
+       my $self = shift;
+       close $self->{gui};
+       $self->SUPER::abort_edit(@_);
+}
+
+sub close_edit {
+       my $self = shift;
+       close $self->{gui} or croak;
+       $self->SUPER::close_edit(@_);
+}
 
 package SVN::Git::Editor;
 use vars qw/@ISA/;
@@ -3129,8 +3538,7 @@ sub split_path {
 }
 
 sub repo_path {
-       (defined $_[1] && length $_[1]) ? "$_[0]->{svn_path}/$_[1]"
-                                       : $_[0]->{svn_path}
+       (defined $_[1] && length $_[1]) ? $_[1] : ''
 }
 
 sub url_path {
@@ -3162,10 +3570,9 @@ 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;
-               my @dn = (@svn_path, (split m#/#, $_));
+               my @dn = split m#/#, $_;
                while (pop @dn) {
                        delete $rm->{join '/', @dn};
                }
@@ -3312,9 +3719,11 @@ sub chg_file {
        seek $fh, 0, 0 or croak $!;
 
        my $exp = $md5->hexdigest;
-       my $atd = $self->apply_textdelta($fbat, undef, $self->{pool});
-       my $got = SVN::TxDelta::send_stream($fh, @$atd, $self->{pool});
+       my $pool = SVN::Pool->new;
+       my $atd = $self->apply_textdelta($fbat, undef, $pool);
+       my $got = SVN::TxDelta::send_stream($fh, @$atd, $pool);
        die "Checksum mismatch\nexpected: $exp\ngot: $got\n" if ($got ne $exp);
+       $pool->clear;
 
        close $fh or croak $!;
 }