git-completion: add "git stash"
[gitweb.git] / git-svn.perl
index 7b5f8ab3be6639e682dc6b0d5a53412f1078ba54..ee7ef693fa9300082ca090ebf97f7e242043fb72 100755 (executable)
 use Git;
 
 BEGIN {
-       my $s;
+       # import functions from Git into our packages, en masse
+       no strict 'refs';
        foreach (qw/command command_oneline command_noisy command_output_pipe
                    command_input_pipe command_close_pipe/) {
-               $s .= "*SVN::Git::Editor::$_ = *SVN::Git::Fetcher::$_ = ".
-                     "*Git::SVN::Migration::$_ = ".
-                     "*Git::SVN::Log::$_ = *Git::SVN::$_ = *$_ = *Git::$_; ";
+               for my $package ( qw(SVN::Git::Editor SVN::Git::Fetcher
+                       Git::SVN::Migration Git::SVN::Log Git::SVN),
+                       __PACKAGE__) {
+                       *{"${package}::$_"} = \&{"Git::$_"};
+               }
        }
-       eval $s;
 }
 
 my ($SVN);
@@ -55,7 +57,7 @@ BEGIN
 my ($_stdin, $_help, $_edit,
        $_message, $_file,
        $_template, $_shared,
-       $_version, $_fetch_all,
+       $_version, $_fetch_all, $_no_rebase,
        $_merge, $_strategy, $_dry_run, $_local,
        $_prefix, $_no_checkout, $_verbose);
 $Git::SVN::_follow_parent = 1;
@@ -80,6 +82,7 @@ BEGIN
 my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared,
                   'trunk|T=s' => \$_trunk, 'tags|t=s' => \$_tags,
                   'branches|b=s' => \$_branches, 'prefix=s' => \$_prefix,
+                  'minimize-url|m' => \$Git::SVN::_minimize_url,
                  'no-metadata' => sub { $icv{noMetadata} = 1 },
                  'use-svm-props' => sub { $icv{useSvmProps} = 1 },
                  'use-svnsync-props' => sub { $icv{useSvnsyncProps} = 1 },
@@ -114,6 +117,7 @@ BEGIN
                          'verbose|v' => \$_verbose,
                          'dry-run|n' => \$_dry_run,
                          'fetch-all|all' => \$_fetch_all,
+                         'no-rebase' => \$_no_rebase,
                        %cmt_opts, %fc_opts } ],
        'set-tree' => [ \&cmd_set_tree,
                        "Set an SVN repository to a git tree-ish",
@@ -141,6 +145,8 @@ BEGIN
                          'color' => \$Git::SVN::Log::color,
                          'pager=s' => \$Git::SVN::Log::pager,
                        } ],
+       'find-rev' => [ \&cmd_find_rev, "Translate between SVN revision numbers and tree-ish",
+                       { } ],
        'rebase' => [ \&cmd_rebase, "Fetch and rebase your working directory",
                        { 'merge|m|M' => \$_merge,
                          'verbose|v' => \$_verbose,
@@ -368,16 +374,9 @@ sub cmd_dcommit {
                die "Unable to determine upstream SVN information from ",
                    "$head history\n";
        }
-       my $c = $refs[-1];
        my $last_rev;
-       foreach my $d (@refs) {
-               if (!verify_ref("$d~1")) {
-                       fatal "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";
-               }
+       my ($linear_refs, $parents) = linearize_history($gs, \@refs);
+       foreach my $d (@$linear_refs) {
                unless (defined $last_rev) {
                        (undef, $last_rev, undef) = cmt_metadata("$d~1");
                        unless (defined $last_rev) {
@@ -390,7 +389,7 @@ sub cmd_dcommit {
                } else {
                        my %ed_opts = ( r => $last_rev,
                                        log => get_commit_entry($d)->{log},
-                                       ra => Git::SVN::Ra->new($url),
+                                       ra => Git::SVN::Ra->new($gs->full_url),
                                        tree_a => "$d~1",
                                        tree_b => $d,
                                        editor_cb => sub {
@@ -399,6 +398,9 @@ sub cmd_dcommit {
                                        svn_path => '');
                        if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
                                print "No changes\n$d~1 == $d\n";
+                       } elsif ($parents->{$d} && @{$parents->{$d}}) {
+                               $gs->{inject_parents_dcommit}->{$last_rev} =
+                                                              $parents->{$d};
                        }
                }
        }
@@ -411,21 +413,44 @@ sub cmd_dcommit {
                return;
        }
        $_fetch_all ? $gs->fetch_all : $gs->fetch;
-       # we always want to rebase against the current HEAD, not any
-       # head that was passed to us
-       my @diff = command('diff-tree', 'HEAD', $gs->refname, '--');
-       my @finish;
-       if (@diff) {
-               @finish = rebase_cmd();
-               print STDERR "W: HEAD and ", $gs->refname, " differ, ",
-                            "using @finish:\n", "@diff";
+       unless ($_no_rebase) {
+               # we always want to rebase against the current HEAD, not any
+               # head that was passed to us
+               my @diff = command('diff-tree', 'HEAD', $gs->refname, '--');
+               my @finish;
+               if (@diff) {
+                       @finish = rebase_cmd();
+                       print STDERR "W: HEAD and ", $gs->refname, " differ, ",
+                                    "using @finish:\n", "@diff";
+               } else {
+                       print "No changes between current HEAD and ",
+                             $gs->refname, "\nResetting to the latest ",
+                             $gs->refname, "\n";
+                       @finish = qw/reset --mixed/;
+               }
+               command_noisy(@finish, $gs->refname);
+       }
+}
+
+sub cmd_find_rev {
+       my $revision_or_hash = shift;
+       my $result;
+       if ($revision_or_hash =~ /^r\d+$/) {
+               my $head = shift;
+               $head ||= 'HEAD';
+               my @refs;
+               my (undef, undef, undef, $gs) = working_head_info($head, \@refs);
+               unless ($gs) {
+                       die "Unable to determine upstream SVN information from ",
+                           "$head history\n";
+               }
+               my $desired_revision = substr($revision_or_hash, 1);
+               $result = $gs->rev_db_get($desired_revision);
        } else {
-               print "No changes between current HEAD and ",
-                     $gs->refname, "\nResetting to the latest ",
-                     $gs->refname, "\n";
-               @finish = qw/reset --mixed/;
+               my (undef, $rev, undef) = cmt_metadata($revision_or_hash);
+               $result = $rev;
        }
-       command_noisy(@finish, $gs->refname);
+       print "$result\n" if $result;
 }
 
 sub cmd_rebase {
@@ -458,6 +483,11 @@ sub cmd_multi_init {
        unless (defined $_trunk || defined $_branches || defined $_tags) {
                usage(1);
        }
+
+       # there are currently some bugs that prevent multi-init/multi-fetch
+       # setups from working well without this.
+       $Git::SVN::_minimize_url = 1;
+
        $_prefix = '' unless defined $_prefix;
        if (defined $url) {
                $url =~ s#/+$##;
@@ -562,8 +592,7 @@ sub post_fetch_checkout {
        my $index = $ENV{GIT_INDEX_FILE} || "$ENV{GIT_DIR}/index";
        return if -f $index;
 
-       chomp(my $bare = `git config --bool --get core.bare`);
-       return if $bare eq 'true';
+       return if command_oneline(qw/rev-parse --is-inside-work-tree/) eq 'false';
        return if command_oneline(qw/rev-parse --is-inside-git-dir/) eq 'true';
        command_noisy(qw/read-tree -m -u -v HEAD HEAD/);
        print STDERR "Checked out HEAD:\n  ",
@@ -711,7 +740,7 @@ sub load_authors {
        my $log = $cmd eq 'log';
        while (<$authors>) {
                chomp;
-               next unless /^(\S+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
+               next unless /^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
                my ($user, $name, $email) = ($1, $2, $3);
                if ($log) {
                        $Git::SVN::Log::rusers{"$name <$email>"} = $user;
@@ -753,12 +782,12 @@ sub read_repo_config {
 
 sub extract_metadata {
        my $id = shift or return (undef, undef, undef);
-       my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
+       my ($url, $rev, $uuid) = ($id =~ /^\s*git-svn-id:\s+(.*)\@(\d+)
                                                        \s([a-f\d\-]+)$/x);
        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\-]+)/);
+               ($rev, $uuid) = ($id =~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
        }
        return ($url, $rev, $uuid);
 }
@@ -770,31 +799,93 @@ sub cmt_metadata {
 
 sub working_head_info {
        my ($head, $refs) = @_;
-       my ($fh, $ctx) = command_output_pipe('rev-list', $head);
-       while (my $hash = <$fh>) {
-               chomp($hash);
-               my ($url, $rev, $uuid) = cmt_metadata($hash);
+       my ($fh, $ctx) = command_output_pipe('log', $head);
+       my $hash;
+       my %max;
+       while (<$fh>) {
+               if ( m{^commit ($::sha1)$} ) {
+                       unshift @$refs, $hash if $hash and $refs;
+                       $hash = $1;
+                       next;
+               }
+               next unless s{^\s*(git-svn-id:)}{$1};
+               my ($url, $rev, $uuid) = extract_metadata($_);
                if (defined $url && defined $rev) {
+                       next if $max{$url} and $max{$url} < $rev;
                        if (my $gs = Git::SVN->find_by_url($url)) {
                                my $c = $gs->rev_db_get($rev);
                                if ($c && $c eq $hash) {
                                        close $fh; # break the pipe
                                        return ($url, $rev, $uuid, $gs);
+                               } else {
+                                       $max{$url} ||= $gs->rev_db_max;
                                }
                        }
                }
-               unshift @$refs, $hash if $refs;
        }
        command_close_pipe($fh, $ctx);
        (undef, undef, undef, undef);
 }
 
+sub read_commit_parents {
+       my ($parents, $c) = @_;
+       my ($fh, $ctx) = command_output_pipe(qw/cat-file commit/, $c);
+       while (<$fh>) {
+               chomp;
+               last if '';
+               /^parent ($sha1)/ or next;
+               push @{$parents->{$c}}, $1;
+       }
+       close $fh; # break the pipe
+}
+
+sub linearize_history {
+       my ($gs, $refs) = @_;
+       my %parents;
+       foreach my $c (@$refs) {
+               read_commit_parents(\%parents, $c);
+       }
+
+       my @linear_refs;
+       my %skip = ();
+       my $last_svn_commit = $gs->last_commit;
+       foreach my $c (reverse @$refs) {
+               next if $c eq $last_svn_commit;
+               last if $skip{$c};
+
+               unshift @linear_refs, $c;
+               $skip{$c} = 1;
+
+               # we only want the first parent to diff against for linear
+               # history, we save the rest to inject when we finalize the
+               # svn commit
+               my $fp_a = verify_ref("$c~1");
+               my $fp_b = shift @{$parents{$c}} if $parents{$c};
+               if (!$fp_a || !$fp_b) {
+                       die "Commit $c\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";
+               }
+               if ($fp_a ne $fp_b) {
+                       die "$c~1 = $fp_a, however parsing commit $c ",
+                           "revealed that:\n$c~1 = $fp_b\nBUG!\n";
+               }
+
+               foreach my $p (@{$parents{$c}}) {
+                       $skip{$p} = 1;
+               }
+       }
+       (\@linear_refs, \%parents);
+}
+
 package Git::SVN;
 use strict;
 use warnings;
 use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent
             $_repack $_repack_flags $_use_svm_props $_head
-            $_use_svnsync_props $no_reuse_existing/;
+            $_use_svnsync_props $no_reuse_existing $_minimize_url/;
 use Carp qw/croak/;
 use File::Path qw/mkpath/;
 use File::Copy qw/copy/;
@@ -814,26 +905,26 @@ BEGIN
        # some options are read globally, but can be overridden locally
        # per [svn-remote "..."] section.  Command-line options will *NOT*
        # override options set in an [svn-remote "..."] section
-       my $e;
-       foreach (qw/follow_parent no_metadata use_svm_props
-                   use_svnsync_props/) {
-               my $key = $_;
+       no strict 'refs';
+       for my $option (qw/follow_parent no_metadata use_svm_props
+                          use_svnsync_props/) {
+               my $key = $option;
                $key =~ tr/_//d;
-               $e .= "sub $_ {
-                       my (\$self) = \@_;
-                       return \$self->{-$_} if exists \$self->{-$_};
-                       my \$k = \"svn-remote.\$self->{repo_id}\.$key\";
-                       eval { command_oneline(qw/config --get/, \$k) };
-                       if (\$@) {
-                               \$self->{-$_} = \$Git::SVN::_$_;
+               my $prop = "-$option";
+               *$option = sub {
+                       my ($self) = @_;
+                       return $self->{$prop} if exists $self->{$prop};
+                       my $k = "svn-remote.$self->{repo_id}.$key";
+                       eval { command_oneline(qw/config --get/, $k) };
+                       if ($@) {
+                               $self->{$prop} = ${"Git::SVN::_$option"};
                        } else {
-                               my \$v = command_oneline(qw/config --bool/,\$k);
-                               \$self->{-$_} = \$v eq 'false' ? 0 : 1;
+                               my $v = command_oneline(qw/config --bool/,$k);
+                               $self->{$prop} = $v eq 'false' ? 0 : 1;
                        }
-                       return \$self->{-$_} }\n";
+                       return $self->{$prop};
+               }
        }
-       $e .= "1;\n";
-       eval $e or die $@;
 }
 
 my %LOCKFILES;
@@ -847,8 +938,8 @@ sub resolve_local_globs {
        foreach (command(qw#for-each-ref --format=%(refname) refs/remotes#)) {
                next unless m#^refs/remotes/$ref->{regex}$#;
                my $p = $1;
-               my $pathname = $path->full_path($p);
-               my $refname = $ref->full_path($p);
+               my $pathname = desanitize_refname($path->full_path($p));
+               my $refname = desanitize_refname($ref->full_path($p));
                if (my $existing = $fetch->{$pathname}) {
                        if ($existing ne $refname) {
                                die "Refspec conflict:\n",
@@ -935,7 +1026,9 @@ sub read_all_remotes {
        my $r = {};
        foreach (grep { s/^svn-remote\.// } command(qw/config -l/)) {
                if (m!^(.+)\.fetch=\s*(.*)\s*:\s*refs/remotes/(.+)\s*$!) {
-                       $r->{$1}->{fetch}->{$2} = $3;
+                       my ($remote, $local_ref, $remote_ref) = ($1, $2, $3);
+                       $local_ref =~ s{^/}{};
+                       $r->{$remote}->{fetch}->{$local_ref} = $remote_ref;
                } elsif (m!^(.+)\.url=\s*(.*)\s*$!) {
                        $r->{$1}->{url} = $2;
                } elsif (m!^(.+)\.(branches|tags)=
@@ -1011,7 +1104,7 @@ sub init_remote_config {
                                     "[svn-remote \"$existing\"]\n";
                }
                $self->{repo_id} = $existing;
-       } else {
+       } elsif ($_minimize_url) {
                my $min_url = Git::SVN::Ra->new($url)->minimize_url;
                $existing = find_existing_remote($min_url, $r);
                if ($existing) {
@@ -1055,6 +1148,7 @@ sub init_remote_config {
        unless ($no_write) {
                command_noisy('config',
                              "svn-remote.$self->{repo_id}.url", $url);
+               $self->{path} =~ s{^/}{};
                command_noisy('config', '--add',
                              "svn-remote.$self->{repo_id}.fetch",
                              "$self->{path}:".$self->refname);
@@ -1145,7 +1239,40 @@ sub new {
        $self;
 }
 
-sub refname { "refs/remotes/$_[0]->{ref_id}" }
+sub refname {
+       my ($refname) = "refs/remotes/$_[0]->{ref_id}" ;
+
+       # It cannot end with a slash /, we'll throw up on this because
+       # SVN can't have directories with a slash in their name, either:
+       if ($refname =~ m{/$}) {
+               die "ref: '$refname' ends with a trailing slash, this is ",
+                   "not permitted by git nor Subversion\n";
+       }
+
+       # It cannot have ASCII control character space, tilde ~, caret ^,
+       # colon :, question-mark ?, asterisk *, space, or open bracket [
+       # anywhere.
+       #
+       # Additionally, % must be escaped because it is used for escaping
+       # and we want our escaped refname to be reversible
+       $refname =~ s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;
+
+       # no slash-separated component can begin with a dot .
+       # /.* becomes /%2E*
+       $refname =~ s{/\.}{/%2E}g;
+
+       # It cannot have two consecutive dots .. anywhere
+       # .. becomes %2E%2E
+       $refname =~ s{\.\.}{%2E%2E}g;
+
+       return $refname;
+}
+
+sub desanitize_refname {
+       my ($refname) = @_;
+       $refname =~ s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;
+       return $refname;
+}
 
 sub svm_uuid {
        my ($self) = @_;
@@ -1364,7 +1491,7 @@ sub traverse_ignore {
                }
        }
        foreach (sort keys %$dirent) {
-               next if $dirent->{$_}->kind != $SVN::Node::dir;
+               next if $dirent->{$_}->{kind} != $SVN::Node::dir;
                $self->traverse_ignore($fh, "$path/$_", $r);
        }
 }
@@ -1425,7 +1552,7 @@ sub tmp_config {
        my (@args) = @_;
        my $old_def_config = "$ENV{GIT_DIR}/svn/config";
        my $config = "$ENV{GIT_DIR}/svn/.metadata";
-       if (-e $old_def_config && ! -e $config) {
+       if (! -f $config && -f $old_def_config) {
                rename $old_def_config, $config or
                       die "Failed rename $old_def_config => $config: $!\n";
        }
@@ -1509,6 +1636,11 @@ sub get_commit_parents {
        if (my $cur = ::verify_ref($self->refname.'^0')) {
                push @tmp, $cur;
        }
+       if (my $ipd = $self->{inject_parents_dcommit}) {
+               if (my $commit = delete $ipd->{$log_entry->{revision}}) {
+                       push @tmp, @$commit;
+               }
+       }
        push @tmp, $_ foreach (@{$log_entry->{parents}}, @tmp);
        while (my $p = shift @tmp) {
                next if $seen{$p};
@@ -1932,16 +2064,19 @@ sub rebuild {
                return;
        }
        print "Rebuilding $db_path ...\n";
-       my ($rev_list, $ctx) = command_output_pipe("rev-list", $self->refname);
+       my ($log, $ctx) = command_output_pipe("log", $self->refname);
        my $latest;
        my $full_url = $self->full_url;
        remove_username($full_url);
        my $svn_uuid;
-       while (<$rev_list>) {
-               chomp;
-               my $c = $_;
-               die "Non-SHA1: $c\n" unless $c =~ /^$::sha1$/o;
-               my ($url, $rev, $uuid) = ::cmt_metadata($c);
+       my $c;
+       while (<$log>) {
+               if ( m{^commit ($::sha1)$} ) {
+                       $c = $1;
+                       next;
+               }
+               next unless s{^\s*(git-svn-id:)}{$1};
+               my ($url, $rev, $uuid) = ::extract_metadata($_);
                remove_username($url);
 
                # ignore merges (from set-tree)
@@ -1959,7 +2094,7 @@ sub rebuild {
                $self->rev_db_set($rev, $c);
                print "r$rev = $c\n";
        }
-       command_close_pipe($rev_list, $ctx);
+       command_close_pipe($log, $ctx);
        print "Done rebuilding $db_path\n";
 }
 
@@ -2440,15 +2575,19 @@ sub close_file {
        my $hash;
        my $path = $self->git_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 (defined $exp) {
+                       seek($fh, 0, 0) or croak $!;
+                       my $md5 = Digest::MD5->new;
+                       $md5->addfile($fh);
+                       my $got = $md5->hexdigest;
+                       if ($got ne $exp) {
+                               die "Checksum mismatch: $path\n",
+                                   "expected: $exp\n    got: $got\n";
+                       }
+               }
+               sysseek($fh, 0, 0) or croak $!;
                if ($fb->{mode_b} == 120000) {
-                       read($fh, my $buf, 5) == 5 or croak $!;
+                       sysread($fh, my $buf, 5) == 5 or croak $!;
                        $buf eq 'link ' or die "$path has mode 120000",
                                               "but is not a link\n";
                }
@@ -2618,6 +2757,9 @@ sub repo_path {
 
 sub url_path {
        my ($self, $path) = @_;
+       if ($self->{url} =~ m#^https?://#) {
+               $path =~ s/([^a-zA-Z0-9_.-])/uc sprintf("%%%02x",ord($1))/eg;
+       }
        $self->{url} . '/' . $self->repo_path($path);
 }
 
@@ -2814,8 +2956,10 @@ sub close_edit {
        my ($self) = @_;
        my ($p,$bat) = ($self->{pool}, $self->{bat});
        foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {
+               next if $_ eq '';
                $self->close_directory($bat->{$_}, $p);
        }
+       $self->close_directory($bat->{''}, $p);
        $self->SUPER::close_edit($p);
        $p->clear;
 }
@@ -2861,46 +3005,23 @@ package Git::SVN::Ra;
 
 BEGIN {
        # enforce temporary pool usage for some simple functions
-       my $e;
-       foreach (qw/get_latest_revnum get_uuid get_repos_root/) {
-               $e .= "sub $_ {
-                       my \$self = shift;
-                       my \$pool = SVN::Pool->new;
-                       my \@ret = \$self->SUPER::$_(\@_,\$pool);
-                       \$pool->clear;
-                       wantarray ? \@ret : \$ret[0]; }\n";
-       }
-
-       # get_dir needs $pool held in cache for dirents to work,
-       # check_path is cacheable and rev_proplist is close enough
-       # for our purposes.
-       foreach (qw/check_path get_dir rev_proplist/) {
-               $e .= "my \%${_}_cache; my \$${_}_rev = 0; sub $_ {
-                       my \$self = shift;
-                       my \$r = pop;
-                       my \$k = join(\"\\0\", \@_);
-                       if (my \$x = \$${_}_cache{\$r}->{\$k}) {
-                               return wantarray ? \@\$x : \$x->[0];
-                       }
-                       my \$pool = SVN::Pool->new;
-                       my \@ret = \$self->SUPER::$_(\@_, \$r, \$pool);
-                       if (\$r != \$${_}_rev) {
-                               \%${_}_cache = ( pool => [] );
-                               \$${_}_rev = \$r;
-                       }
-                       \$${_}_cache{\$r}->{\$k} = \\\@ret;
-                       push \@{\$${_}_cache{pool}}, \$pool;
-                       wantarray ? \@ret : \$ret[0]; }\n";
+       no strict 'refs';
+       for my $f (qw/rev_proplist get_latest_revnum get_uuid get_repos_root/) {
+               my $SUPER = "SUPER::$f";
+               *$f = sub {
+                       my $self = shift;
+                       my $pool = SVN::Pool->new;
+                       my @ret = $self->$SUPER(@_,$pool);
+                       $pool->clear;
+                       wantarray ? @ret : $ret[0];
+               };
        }
-       $e .= "\n1;";
-       eval $e or die $@;
 }
 
 sub new {
        my ($class, $url) = @_;
        $url =~ s!/+$!!;
        return $RA if ($RA && $RA->{url} eq $url);
-       $RA->{pool}->clear if $RA;
 
        SVN::_Core::svn_config_ensure($config_dir, undef);
        my ($baton, $callbacks) = SVN::Core::auth_open_helper([
@@ -2908,6 +3029,7 @@ sub new {
            SVN::Client::get_ssl_server_trust_file_provider(),
            SVN::Client::get_simple_prompt_provider(
              \&Git::SVN::Prompt::simple, 2),
+           SVN::Client::get_ssl_client_cert_file_provider(),
            SVN::Client::get_ssl_client_cert_prompt_provider(
              \&Git::SVN::Prompt::ssl_client_cert, 2),
            SVN::Client::get_ssl_client_cert_pw_prompt_provider(
@@ -2919,6 +3041,7 @@ sub new {
              \&Git::SVN::Prompt::username, 2),
          ]);
        my $config = SVN::Core::config_get_config($config_dir);
+       $RA = undef;
        my $self = SVN::Ra->new(url => $url, auth => $baton,
                              config => $config,
                              pool => SVN::Pool->new,
@@ -2926,9 +3049,47 @@ sub new {
        $self->{svn_path} = $url;
        $self->{repos_root} = $self->get_repos_root;
        $self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;
+       $self->{cache} = { check_path => { r => 0, data => {} },
+                          get_dir => { r => 0, data => {} } };
        $RA = bless $self, $class;
 }
 
+sub check_path {
+       my ($self, $path, $r) = @_;
+       my $cache = $self->{cache}->{check_path};
+       if ($r == $cache->{r} && exists $cache->{data}->{$path}) {
+               return $cache->{data}->{$path};
+       }
+       my $pool = SVN::Pool->new;
+       my $t = $self->SUPER::check_path($path, $r, $pool);
+       $pool->clear;
+       if ($r != $cache->{r}) {
+               %{$cache->{data}} = ();
+               $cache->{r} = $r;
+       }
+       $cache->{data}->{$path} = $t;
+}
+
+sub get_dir {
+       my ($self, $dir, $r) = @_;
+       my $cache = $self->{cache}->{get_dir};
+       if ($r == $cache->{r}) {
+               if (my $x = $cache->{data}->{$dir}) {
+                       return wantarray ? @$x : $x->[0];
+               }
+       }
+       my $pool = SVN::Pool->new;
+       my ($d, undef, $props) = $self->SUPER::get_dir($dir, $r, $pool);
+       my %dirents = map { $_ => { kind => $d->{$_}->kind } } keys %$d;
+       $pool->clear;
+       if ($r != $cache->{r}) {
+               %{$cache->{data}} = ();
+               $cache->{r} = $r;
+       }
+       $cache->{data}->{$dir} = [ \%dirents, $r, $props ];
+       wantarray ? (\%dirents, $r, $props) : \%dirents;
+}
+
 sub DESTROY {
        # do not call the real DESTROY since we store ourselves in $RA
 }
@@ -3019,11 +3180,8 @@ sub gs_do_switch {
        $editor->{git_commit_ok};
 }
 
-sub gs_fetch_loop_common {
-       my ($self, $base, $head, $gsv, $globs) = @_;
-       return if ($base > $head);
-       my $inc = $_log_window_size;
-       my ($min, $max) = ($base, $head < $base + $inc ? $head : $base + $inc);
+sub longest_common_path {
+       my ($gsv, $globs) = @_;
        my %common;
        my $common_max = scalar @$gsv;
 
@@ -3055,6 +3213,15 @@ sub gs_fetch_loop_common {
                        last;
                }
        }
+       $longest_path;
+}
+
+sub gs_fetch_loop_common {
+       my ($self, $base, $head, $gsv, $globs) = @_;
+       return if ($base > $head);
+       my $inc = $_log_window_size;
+       my ($min, $max) = ($base, $head < $base + $inc ? $head : $base + $inc);
+       my $longest_path = longest_common_path($gsv, $globs);
        while (1) {
                my %revs;
                my $err;
@@ -3143,7 +3310,7 @@ sub match_globs {
                return unless scalar @x == 3;
                my $dirents = $x[0];
                foreach my $de (keys %$dirents) {
-                       next if $dirents->{$de}->kind != $SVN::Node::dir;
+                       next if $dirents->{$de}->{kind} != $SVN::Node::dir;
                        my $p = $g->{path}->full_path($de);
                        next if $exists->{$p};
                        next if (length $g->{path}->{right} &&