test format-patch -s: make sure MIME content type is shown as needed
[gitweb.git] / git-svn.perl
index 7ffbf641392f398743337d602696c328cec80b79..ec25ea4231a062e05753f4f93a757a41062a0364 100755 (executable)
@@ -9,6 +9,7 @@
 $AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
 $VERSION = '@@GIT_VERSION@@';
 
+my $git_dir_user_set = 1 if defined $ENV{GIT_DIR};
 $ENV{GIT_DIR} ||= '.git';
 $Git::SVN::default_repo_id = 'svn';
 $Git::SVN::default_ref_id = $ENV{GIT_SVN_ID} || 'git-svn';
 use IO::File qw//;
 use File::Basename qw/dirname basename/;
 use File::Path qw/mkpath/;
-use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/;
+use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
 use IPC::Open3;
 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);
@@ -54,9 +57,9 @@ BEGIN
 my ($_stdin, $_help, $_edit,
        $_message, $_file,
        $_template, $_shared,
-       $_version, $_fetch_all,
-       $_merge, $_strategy, $_dry_run,
-       $_prefix, $_no_checkout);
+       $_version, $_fetch_all, $_no_rebase,
+       $_merge, $_strategy, $_dry_run, $_local,
+       $_prefix, $_no_checkout, $_verbose);
 $Git::SVN::_follow_parent = 1;
 my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
                     'config-dir=s' => \$Git::SVN::Ra::config_dir,
@@ -66,6 +69,7 @@ BEGIN
                'repack:i' => \$Git::SVN::_repack,
                'noMetadata' => \$Git::SVN::_no_metadata,
                'useSvmProps' => \$Git::SVN::_use_svm_props,
+               'useSvnsyncProps' => \$Git::SVN::_use_svnsync_props,
                'log-window-size=i' => \$Git::SVN::Ra::_log_window_size,
                'no-checkout' => \$_no_checkout,
                'quiet|q' => \$_q,
@@ -73,10 +77,17 @@ BEGIN
                   \$Git::SVN::_repack_flags,
                %remote_opts );
 
-my ($_trunk, $_tags, $_branches);
+my ($_trunk, $_tags, $_branches, $_stdlayout);
+my %icv;
 my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared,
                   'trunk|T=s' => \$_trunk, 'tags|t=s' => \$_tags,
                   'branches|b=s' => \$_branches, 'prefix=s' => \$_prefix,
+                  'stdlayout|s' => \$_stdlayout,
+                  '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 },
+                 'rewrite-root=s' => sub { $icv{rewriteRoot} = $_[1] },
                   %remote_opts );
 my %cmt_opts = ( 'edit|e' => \$_edit,
                'rmdir' => \$SVN::Git::Editor::_rmdir,
@@ -88,8 +99,11 @@ BEGIN
 my %cmd = (
        fetch => [ \&cmd_fetch, "Download new revisions from SVN",
                        { 'revision|r=s' => \$_revision,
-                         'all|a' => \$_fetch_all,
+                         'fetch-all|all' => \$_fetch_all,
                           %fc_opts } ],
+       clone => [ \&cmd_clone, "Initialize and fetch revisions",
+                       { 'revision|r=s' => \$_revision,
+                          %fc_opts, %init_opts } ],
        init => [ \&cmd_init, "Initialize a repo for tracking" .
                          " (requires URL argument)",
                          \%init_opts ],
@@ -101,7 +115,10 @@ BEGIN
                     'Commit several diffs to merge with upstream',
                        { 'merge|m|M' => \$_merge,
                          'strategy|s=s' => \$_strategy,
+                         '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",
@@ -129,6 +146,15 @@ 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,
+                         'strategy|s=s' => \$_strategy,
+                         'local|l' => \$_local,
+                         'fetch-all|all' => \$_fetch_all,
+                         %fc_opts } ],
        'commit-diff' => [ \&cmd_commit_diff,
                           'Commit a diff between two trees',
                        { 'message|m=s' => \$_message,
@@ -149,17 +175,42 @@ BEGIN
 my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
 
 read_repo_config(\%opts);
+Getopt::Long::Configure('pass_through') if ($cmd && $cmd eq 'log');
 my $rv = GetOptions(%opts, 'help|H|h' => \$_help, 'version|V' => \$_version,
                     'minimize-connections' => \$Git::SVN::Migration::_minimize,
                     'id|i=s' => \$Git::SVN::default_ref_id,
-                    'svn-remote|remote|R=s' => \$Git::SVN::default_repo_id);
-exit 1 if (!$rv && $cmd ne 'log');
+                    'svn-remote|remote|R=s' => sub {
+                       $Git::SVN::no_reuse_existing = 1;
+                       $Git::SVN::default_repo_id = $_[1] });
+exit 1 if (!$rv && $cmd && $cmd ne 'log');
 
 usage(0) if $_help;
 version() if $_version;
 usage(1) unless defined $cmd;
 load_authors() if $_authors;
-unless ($cmd =~ /^(?:init|multi-init|commit-diff)$/) {
+
+# make sure we're always running
+unless ($cmd =~ /(?:clone|init|multi-init)$/) {
+       unless (-d $ENV{GIT_DIR}) {
+               if ($git_dir_user_set) {
+                       die "GIT_DIR=$ENV{GIT_DIR} explicitly set, ",
+                           "but it is not a directory\n";
+               }
+               my $git_dir = delete $ENV{GIT_DIR};
+               chomp(my $cdup = command_oneline(qw/rev-parse --show-cdup/));
+               unless (length $cdup) {
+                       die "Already at toplevel, but $git_dir ",
+                           "not found '$cdup'\n";
+               }
+               chdir $cdup or die "Unable to chdir up to '$cdup'\n";
+               unless (-d $git_dir) {
+                       die "$git_dir still not found after going to ",
+                           "'$cdup'\n";
+               }
+               $ENV{GIT_DIR} = $git_dir;
+       }
+}
+unless ($cmd =~ /^(?:clone|init|multi-init|commit-diff)$/) {
        Git::SVN::Migration::migration_check();
 }
 Git::SVN::init_vars();
@@ -186,6 +237,8 @@ sub usage {
                next if /^multi-/; # don't show deprecated commands
                print $fd '  ',pack('A17',$_),$cmd{$_}->[1],"\n";
                foreach (keys %{$cmd{$_}->[2]}) {
+                       # mixed-case options are for .git/config only
+                       next if /[A-Z]/ && /^[a-z]+$/i;
                        # prints out arguments as they should be passed:
                        my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
                        print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
@@ -220,16 +273,42 @@ sub do_git_init_db {
                }
                command_noisy(@init_db);
        }
+       my $set;
+       my $pfx = "svn-remote.$Git::SVN::default_repo_id";
+       foreach my $i (keys %icv) {
+               die "'$set' and '$i' cannot both be set\n" if $set;
+               next unless defined $icv{$i};
+               command_noisy('config', "$pfx.$i", $icv{$i});
+               $set = $i;
+       }
 }
 
 sub init_subdir {
        my $repo_path = shift or return;
        mkpath([$repo_path]) unless -d $repo_path;
        chdir $repo_path or die "Couldn't chdir to $repo_path: $!\n";
-       $ENV{GIT_DIR} = $repo_path . "/.git";
+       $ENV{GIT_DIR} = '.git';
+}
+
+sub cmd_clone {
+       my ($url, $path) = @_;
+       if (!defined $path &&
+           (defined $_trunk || defined $_branches || defined $_tags ||
+            defined $_stdlayout) &&
+           $url !~ m#^[a-z\+]+://#) {
+               $path = $url;
+       }
+       $path = basename($url) if !defined $path || !length $path;
+       cmd_init($url, $path);
+       Git::SVN::fetch_all($Git::SVN::default_repo_id);
 }
 
 sub cmd_init {
+       if (defined $_stdlayout) {
+               $_trunk = 'trunk' if (!defined $_trunk);
+               $_tags = 'tags' if (!defined $_tags);
+               $_branches = 'branches' if (!defined $_branches);
+       }
        if (defined $_trunk || defined $_branches || defined $_tags) {
                return cmd_multi_init(@_);
        }
@@ -248,7 +327,7 @@ sub cmd_fetch {
        }
        my ($remote) = @_;
        if (@_ > 1) {
-               die "Usage: $0 fetch [--all|-a] [svn-remote]\n";
+               die "Usage: $0 fetch [--all] [svn-remote]\n";
        }
        $remote ||= $Git::SVN::default_repo_id;
        if ($_fetch_all) {
@@ -296,32 +375,23 @@ sub cmd_set_tree {
 sub cmd_dcommit {
        my $head = shift;
        $head ||= 'HEAD';
-       my ($url, $rev, $uuid);
-       my ($fh, $ctx) = command_output_pipe('rev-list', $head);
        my @refs;
-       my $c;
-       while (<$fh>) {
-               $c = $_;
-               chomp $c;
-               ($url, $rev, $uuid) = cmt_metadata($c);
-               last if (defined $url && defined $rev && defined $uuid);
-               unshift @refs, $c;
-       }
-       close $fh; # most likely breaking the pipe
-       unless (defined $url && defined $rev && defined $uuid) {
+       my ($url, $rev, $uuid, $gs) = working_head_info($head, \@refs);
+       print "Committing to $url ...\n";
+       unless ($gs) {
                die "Unable to determine upstream SVN information from ",
-                   "$head history:\n  $ctx\n";
+                   "$head history\n";
        }
-       my $gs = Git::SVN->find_by_url($url);
        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);
+       if ($_no_rebase && scalar(@$linear_refs) > 1) {
+               warn "Attempting to commit more than one change while ",
+                    "--no-rebase is enabled.\n",
+                    "If these changes depend on each other, re-running ",
+                    "without --no-rebase will be required."
+       }
+       while (1) {
+               my $d = shift @$linear_refs or last;
                unless (defined $last_rev) {
                        (undef, $last_rev, undef) = cmt_metadata("$d~1");
                        unless (defined $last_rev) {
@@ -332,52 +402,132 @@ sub cmd_dcommit {
                if ($_dry_run) {
                        print "diff-tree $d~1 $d\n";
                } else {
+                       my $cmt_rev;
                        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 {
                                               print "Committed r$_[0]\n";
-                                              $last_rev = $_[0]; },
+                                              $cmt_rev = $_[0];
+                                       },
                                        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}->{$cmt_rev} =
+                                                              $parents->{$d};
                        }
+                       $_fetch_all ? $gs->fetch_all : $gs->fetch;
+                       next if $_no_rebase;
+
+                       # we always want to rebase against the current HEAD,
+                       # not any head that was passed to us
+                       my @diff = command('diff-tree', $d,
+                                          $gs->refname, '--');
+                       my @finish;
+                       if (@diff) {
+                               @finish = rebase_cmd();
+                               print STDERR "W: $d and ", $gs->refname,
+                                            " differ, using @finish:\n",
+                                            join("\n", @diff), "\n";
+                       } 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);
+                       if (@diff) {
+                               @refs = ();
+                               my ($url_, $rev_, $uuid_, $gs_) =
+                                             working_head_info($head, \@refs);
+                               my ($linear_refs_, $parents_) =
+                                             linearize_history($gs_, \@refs);
+                               if (scalar(@$linear_refs) !=
+                                   scalar(@$linear_refs_)) {
+                                       fatal "# of revisions changed ",
+                                         "\nbefore:\n",
+                                         join("\n", @$linear_refs),
+                                         "\n\nafter:\n",
+                                         join("\n", @$linear_refs_), "\n",
+                                         'If you are attempting to commit ',
+                                         "merges, try running:\n\t",
+                                         'git rebase --interactive',
+                                         '--preserve-merges ',
+                                         $gs->refname,
+                                         "\nBefore dcommitting";
+                               }
+                               if ($url_ ne $url) {
+                                       fatal "URL mismatch after rebase: ",
+                                             "$url_ != $url";
+                               }
+                               if ($uuid_ ne $uuid) {
+                                       fatal "uuid mismatch after rebase: ",
+                                             "$uuid_ != $uuid";
+                               }
+                               # remap parents
+                               my (%p, @l, $i);
+                               for ($i = 0; $i < scalar @$linear_refs; $i++) {
+                                       my $new = $linear_refs_->[$i] or next;
+                                       $p{$new} =
+                                               $parents->{$linear_refs->[$i]};
+                                       push @l, $new;
+                               }
+                               $parents = \%p;
+                               $linear_refs = \@l;
+                       }
+                       $last_rev = $cmt_rev;
+               }
+       }
+}
+
+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 {
+               my (undef, $rev, undef) = cmt_metadata($revision_or_hash);
+               $result = $rev;
        }
-       return if $_dry_run;
+       print "$result\n" if $result;
+}
+
+sub cmd_rebase {
+       command_noisy(qw/update-index --refresh/);
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
        unless ($gs) {
-               warn "Could not determine fetch information for $url\n",
-                    "Will not attempt to fetch and rebase commits.\n",
-                    "This probably means you have useSvmProps and should\n",
-                    "now resync your SVN::Mirror repository.\n";
-               return;
+               die "Unable to determine upstream SVN information from ",
+                   "working tree history\n";
        }
-       $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 = qw/rebase/;
-               push @finish, qw/--merge/ if $_merge;
-               push @finish, "--strategy=$_strategy" if $_strategy;
-               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/;
+       if (command(qw/diff-index HEAD --/)) {
+               print STDERR "Cannot rebase with uncommited changes:\n";
+               command_noisy('status');
+               exit 1;
        }
-       command_noisy(@finish, $gs->refname);
+       unless ($_local) {
+               $_fetch_all ? $gs->fetch_all : $gs->fetch;
+       }
+       command_noisy(rebase_cmd(), $gs->refname);
 }
 
 sub cmd_show_ignore {
-       my $gs = Git::SVN->new;
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       $gs ||= Git::SVN->new;
        my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
-       $gs->traverse_ignore(\*STDOUT, '', $r);
+       $gs->traverse_ignore(\*STDOUT, $gs->{path}, $r);
 }
 
 sub cmd_multi_init {
@@ -385,12 +535,17 @@ sub cmd_multi_init {
        unless (defined $_trunk || defined $_branches || defined $_tags) {
                usage(1);
        }
-       do_git_init_db();
+
+       # 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#/+$##;
                init_subdir(@_);
        }
+       do_git_init_db();
        if (defined $_trunk) {
                my $trunk_ref = $_prefix . 'trunk';
                # try both old-style and new-style lookups:
@@ -468,6 +623,14 @@ sub cmd_commit_diff {
 
 ########################### utility functions #########################
 
+sub rebase_cmd {
+       my @cmd = qw/rebase/;
+       push @cmd, '-v' if $_verbose;
+       push @cmd, qw/--merge/ if $_merge;
+       push @cmd, "--strategy=$_strategy" if $_strategy;
+       @cmd;
+}
+
 sub post_fetch_checkout {
        return if $_no_checkout;
        my $gs = $Git::SVN::_head or return;
@@ -481,8 +644,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  ",
@@ -630,7 +792,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;
@@ -672,12 +834,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);
 }
@@ -687,11 +849,90 @@ sub cmt_metadata {
                command(qw/cat-file commit/, shift)))[-1]);
 }
 
+sub working_head_info {
+       my ($head, $refs) = @_;
+       my ($fh, $ctx) = command_output_pipe('log', '--no-color', $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;
+                               }
+                       }
+               }
+       }
+       command_close_pipe($fh, $ctx);
+       (undef, undef, undef, undef);
+}
+
+sub read_commit_parents {
+       my ($parents, $c) = @_;
+       chomp(my $p = command_oneline(qw/rev-list --parents -1/, $c));
+       $p =~ s/^($c)\s*// or die "rev-list --parents -1 $c failed!\n";
+       @{$parents->{$c}} = split(/ /, $p);
+}
+
+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/;
+            $_repack $_repack_flags $_use_svm_props $_head
+            $_use_svnsync_props $no_reuse_existing $_minimize_url/;
 use Carp qw/croak/;
 use File::Path qw/mkpath/;
 use File::Copy qw/copy/;
@@ -711,25 +952,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/) {
-               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;
@@ -743,8 +985,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",
@@ -783,6 +1025,12 @@ sub parse_revision_argument {
 
 sub fetch_all {
        my ($repo_id, $remotes) = @_;
+       if (ref $repo_id) {
+               my $gs = $repo_id;
+               $repo_id = undef;
+               $repo_id = $gs->{repo_id};
+       }
+       $remotes ||= read_all_remotes();
        my $remote = $remotes->{$repo_id} or
                     die "[svn-remote \"$repo_id\"] unknown\n";
        my $fetch = $remote->{fetch};
@@ -801,6 +1049,8 @@ sub fetch_all {
                                         "svn-remote.$repo_id.${t}-maxRev") };
                if (defined $max_rev && ($max_rev < $base)) {
                        $base = $max_rev;
+               } elsif (!defined $max_rev) {
+                       $base = 0;
                }
        }
 
@@ -823,7 +1073,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)=
@@ -877,6 +1129,7 @@ sub sanitize_remote_name {
 
 sub find_existing_remote {
        my ($url, $remotes) = @_;
+       return undef if $no_reuse_existing;
        my $existing;
        foreach my $repo_id (keys %$remotes) {
                my $u = $remotes->{$repo_id}->{url} or next;
@@ -898,7 +1151,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) {
@@ -942,6 +1195,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);
@@ -951,6 +1205,10 @@ sub init_remote_config {
 
 sub find_by_url { # repos_root and, path are optional
        my ($class, $full_url, $repos_root, $path) = @_;
+
+       return undef unless defined $full_url;
+       remove_username($full_url);
+       remove_username($repos_root) if defined $repos_root;
        my $remotes = read_all_remotes();
        if (defined $full_url && defined $repos_root && !defined $path) {
                $path = $full_url;
@@ -958,6 +1216,7 @@ sub find_by_url { # repos_root and, path are optional
        }
        foreach my $repo_id (keys %$remotes) {
                my $u = $remotes->{$repo_id}->{url} or next;
+               remove_username($u);
                next if defined $repos_root && $repos_root ne $u;
 
                my $fetch = $remotes->{$repo_id}->{fetch} || {};
@@ -1023,14 +1282,44 @@ sub new {
        $self->{url} = command_oneline('config', '--get',
                                       "svn-remote.$repo_id.url") or
                   die "Failed to read \"svn-remote.$repo_id.url\" in config\n";
-       if ((-z $self->db_path || ! -e $self->db_path) &&
-           ::verify_ref($self->refname.'^0')) {
-               $self->rebuild;
-       }
+       $self->rebuild;
        $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) = @_;
@@ -1052,9 +1341,12 @@ sub svm {
                $svm = {
                  source => tmp_config('--get', "$section.svm-source"),
                  uuid => tmp_config('--get', "$section.svm-uuid"),
+                 replace => tmp_config('--get', "$section.svm-replace"),
                }
        };
-       $self->{svm} = $svm if ($svm && $svm->{source} && $svm->{uuid});
+       if ($svm && $svm->{source} && $svm->{uuid} && $svm->{replace}) {
+               $self->{svm} = $svm;
+       }
        $self->{svm};
 }
 
@@ -1063,68 +1355,124 @@ sub _set_svm_vars {
        return $ra if $self->svm;
 
        my @err = ( "useSvmProps set, but failed to read SVM properties\n",
-                   "(svm:source, svm:mirror, svm:mirror) ",
+                   "(svm:source, svm:uuid) ",
                    "from the following URLs:\n" );
        sub read_svm_props {
-               my ($self, $props) = @_;
+               my ($self, $ra, $path, $r) = @_;
+               my $props = ($ra->get_dir($path, $r))[2];
                my $src = $props->{'svm:source'};
-               my $mirror = $props->{'svm:mirror'};
                my $uuid = $props->{'svm:uuid'};
-               return undef if (!$src || !$mirror || !$uuid);
+               return undef if (!$src || !$uuid);
 
-               chomp($src, $mirror, $uuid);
+               chomp($src, $uuid);
 
                $uuid =~ m{^[0-9a-f\-]{30,}$}
                    or die "doesn't look right - svm:uuid is '$uuid'\n";
-               # don't know what a '!' is there for, also the
-               # username is of no interest
-               $src =~ s{/?!$}{$mirror};
+
+               # the '!' is used to mark the repos_root!/relative/path
+               $src =~ s{/?!/?}{/};
                $src =~ s{/+$}{}; # no trailing slashes please
+               # username is of no interest
                $src =~ s{(^[a-z\+]*://)[^/@]*@}{$1};
 
+               my $replace = $ra->{url};
+               $replace .= "/$path" if length $path;
+
                my $section = "svn-remote.$self->{repo_id}";
-               tmp_config('--add', "$section.svm-source", $src);
-               tmp_config('--add', "$section.svm-uuid", $uuid);
-               $self->{svm} = { source => $src , uuid => $uuid };
-               return 1;
+               tmp_config("$section.svm-source", $src);
+               tmp_config("$section.svm-replace", $replace);
+               tmp_config("$section.svm-uuid", $uuid);
+               $self->{svm} = {
+                       source => $src,
+                       uuid => $uuid,
+                       replace => $replace
+               };
        }
 
        my $r = $ra->get_latest_revnum;
        my $path = $self->{path};
-       my @tried_a = ($path);
+       my %tried;
        while (length $path) {
-               if ($self->read_svm_props(($ra->get_dir($path, $r))[2])) {
-                       return $ra;
+               unless ($tried{"$self->{url}/$path"}) {
+                       return $ra if $self->read_svm_props($ra, $path, $r);
+                       $tried{"$self->{url}/$path"} = 1;
                }
-               $path =~ s#/?[^/]+$## && push @tried_a, $path;
-       }
-       if ($self->read_svm_props(($ra->get_dir('', $r))[2])) {
-               return $ra;
+               $path =~ s#/?[^/]+$##;
        }
+       die "Path: '$path' should be ''\n" if $path ne '';
+       return $ra if $self->read_svm_props($ra, $path, $r);
+       $tried{"$self->{url}/$path"} = 1;
 
        if ($ra->{repos_root} eq $self->{url}) {
-               die @err, map { "  $self->{url}/$_\n" } @tried_a, "\n";
+               die @err, (map { "  $_\n" } keys %tried), "\n";
        }
 
        # nope, make sure we're connected to the repository root:
        my $ok;
        my @tried_b;
        $path = $ra->{svn_path};
-       $path =~ s#/?[^/]+$##; # we already tried this one above
        $ra = Git::SVN::Ra->new($ra->{repos_root});
        while (length $path) {
-               $ok = $self->read_svm_props(($ra->get_dir($path, $r))[2]);
-               last if $ok;
-               $path =~ s#/?[^/]+$## && push @tried_b, $path;
+               unless ($tried{"$ra->{url}/$path"}) {
+                       $ok = $self->read_svm_props($ra, $path, $r);
+                       last if $ok;
+                       $tried{"$ra->{url}/$path"} = 1;
+               }
+               $path =~ s#/?[^/]+$##;
        }
-       $ok = $self->read_svm_props(($ra->get_dir('', $r))[2]) unless $ok;
+       die "Path: '$path' should be ''\n" if $path ne '';
+       $ok ||= $self->read_svm_props($ra, $path, $r);
+       $tried{"$ra->{url}/$path"} = 1;
        if (!$ok) {
-               die @err, map { "  $self->{url}/$_\n" } @tried_a, "\n",
-                         map { "  $ra->{url}/$_\n" } @tried_b, "\n"
+               die @err, (map { "  $_\n" } keys %tried), "\n";
        }
        Git::SVN::Ra->new($self->{url});
 }
 
+sub svnsync {
+       my ($self) = @_;
+       return $self->{svnsync} if $self->{svnsync};
+
+       if ($self->no_metadata) {
+               die "Can't have both 'noMetadata' and ",
+                   "'useSvnsyncProps' options set!\n";
+       }
+       if ($self->rewrite_root) {
+               die "Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",
+                   "options set!\n";
+       }
+
+       my $svnsync;
+       # see if we have it in our config, first:
+       eval {
+               my $section = "svn-remote.$self->{repo_id}";
+               $svnsync = {
+                 url => tmp_config('--get', "$section.svnsync-url"),
+                 uuid => tmp_config('--get', "$section.svnsync-uuid"),
+               }
+       };
+       if ($svnsync && $svnsync->{url} && $svnsync->{uuid}) {
+               return $self->{svnsync} = $svnsync;
+       }
+
+       my $err = "useSvnsyncProps set, but failed to read " .
+                 "svnsync property: svn:sync-from-";
+       my $rp = $self->ra->rev_proplist(0);
+
+       my $url = $rp->{'svn:sync-from-url'} or die $err . "url\n";
+       $url =~ m{^[a-z\+]+://} or
+                  die "doesn't look right - svn:sync-from-url is '$url'\n";
+
+       my $uuid = $rp->{'svn:sync-from-uuid'} or die $err . "uuid\n";
+       $uuid =~ m{^[0-9a-f\-]{30,}$} or
+                  die "doesn't look right - svn:sync-from-uuid is '$uuid'\n";
+
+       my $section = "svn-remote.$self->{repo_id}";
+       tmp_config('--add', "$section.svnsync-uuid", $uuid);
+       tmp_config('--add', "$section.svnsync-url", $url);
+       return $self->{svnsync} = { url => $url, uuid => $uuid };
+}
+
 # this allows us to memoize our SVN::Ra UUID locally and avoid a
 # remote lookup (useful for 'git svn log').
 sub ra_uuid {
@@ -1150,6 +1498,9 @@ sub ra {
                if ($self->no_metadata) {
                        die "Can't have both 'noMetadata' and ",
                            "'useSvmProps' options set!\n";
+               } elsif ($self->use_svnsync_props) {
+                       die "Can't have both 'useSvnsyncProps' and ",
+                           "'useSvmProps' options set!\n";
                }
                $ra = $self->_set_svm_vars($ra);
                $self->{-want_revprops} = 1;
@@ -1161,8 +1512,10 @@ sub rel_path {
        my ($self) = @_;
        my $repos_root = $self->ra->{repos_root};
        return $self->{path} if ($self->{url} eq $repos_root);
-       die "BUG: rel_path failed! repos_root: $repos_root, Ra URL: ",
-           $self->ra->{url}, " path: $self->{path},  URL: $self->{url}\n";
+       my $url = $self->{url} .
+                 (length $self->{path} ? "/$self->{path}" : $self->{path});
+       $url =~ s!^\Q$repos_root\E(?:/+|$)!!g;
+       $url;
 }
 
 sub traverse_ignore {
@@ -1171,7 +1524,7 @@ sub traverse_ignore {
        my $ra = $self->ra;
        my ($dirent, undef, $props) = $ra->get_dir($path, $r);
        my $p = $path;
-       $p =~ s#^\Q$ra->{svn_path}\E/##;
+       $p =~ s#^\Q$self->{path}\E(/|$)##;
        print $fh length $p ? "\n# $p\n" : "\n# /\n";
        if (my $s = $props->{'svn:ignore'}) {
                $s =~ s/[\r\n]+/\n/g;
@@ -1185,7 +1538,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);
        }
 }
@@ -1244,7 +1597,12 @@ sub get_fetch_range {
 
 sub tmp_config {
        my (@args) = @_;
-       my $config = "$ENV{GIT_DIR}/svn/config";
+       my $old_def_config = "$ENV{GIT_DIR}/svn/config";
+       my $config = "$ENV{GIT_DIR}/svn/.metadata";
+       if (! -f $config && -f $old_def_config) {
+               rename $old_def_config, $config or
+                      die "Failed rename $old_def_config => $config: $!\n";
+       }
        my $old_config = $ENV{GIT_CONFIG};
        $ENV{GIT_CONFIG} = $config;
        $@ = undef;
@@ -1325,6 +1683,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};
@@ -1340,6 +1703,26 @@ sub get_commit_parents {
        @ret;
 }
 
+sub rewrite_root {
+       my ($self) = @_;
+       return $self->{-rewrite_root} if exists $self->{-rewrite_root};
+       my $k = "svn-remote.$self->{repo_id}.rewriteRoot";
+       my $rwr = eval { command_oneline(qw/config --get/, $k) };
+       if ($rwr) {
+               $rwr =~ s#/+$##;
+               if ($rwr !~ m#^[a-z\+]+://#) {
+                       die "$rwr is not a valid URL (key: $k)\n";
+               }
+       }
+       $self->{-rewrite_root} = $rwr;
+}
+
+sub metadata_url {
+       my ($self) = @_;
+       ($self->rewrite_root || $self->{url}) .
+          (length $self->{path} ? '/' . $self->{path} : '');
+}
+
 sub full_url {
        my ($self) = @_;
        $self->{url} . (length $self->{path} ? '/' . $self->{path} : '');
@@ -1482,14 +1865,17 @@ sub find_parent_branch {
        }
        my ($r0, $parent) = $gs->find_rev_before($r, 1);
        if (!defined $r0 || !defined $parent) {
-               $gs->fetch(0, $r);
+               my ($base, $head) = parse_revision_argument(0, $r);
+               if ($base <= $r) {
+                       $gs->fetch($base, $r);
+               }
                ($r0, $parent) = $gs->last_rev_commit;
        }
        if (defined $r0 && defined $parent) {
                print STDERR "Found branch parent: ($self->{ref_id}) $parent\n";
-               $self->assert_index_clean($parent);
                my $ed;
                if ($self->ra->can_do_switch) {
+                       $self->assert_index_clean($parent);
                        print STDERR "Following parent with do_switch\n";
                        # do_switch works with svn/trunk >= r22312, but that
                        # is not included with SVN 1.4.3 (the latest version
@@ -1638,19 +2024,39 @@ sub make_log_entry {
        my ($name, $email) = defined $::users{$author} ? @{$::users{$author}}
                                                       : ($author, undef);
        if (defined $headrev && $self->use_svm_props) {
+               if ($self->rewrite_root) {
+                       die "Can't have both 'useSvmProps' and 'rewriteRoot' ",
+                           "options set!\n";
+               }
                my ($uuid, $r) = $headrev =~ m{^([a-f\d\-]{30,}):(\d+)$};
-               if ($uuid ne $self->{svm}->{uuid}) {
+               # we don't want "SVM: initializing mirror for junk" ...
+               return undef if $r == 0;
+               my $svm = $self->svm;
+               if ($uuid ne $svm->{uuid}) {
                        die "UUID mismatch on SVM path:\n",
-                           "expected: $self->{svm}->{uuid}\n",
+                           "expected: $svm->{uuid}\n",
                            "     got: $uuid\n";
                }
-               my $full_url = $self->{svm}->{source};
-               $full_url .= "/$self->{path}" if length $self->{path};
+               my $full_url = $self->full_url;
+               $full_url =~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or
+                            die "Failed to replace '$svm->{replace}' with ",
+                                "'$svm->{source}' in $full_url\n";
+               # throw away username for storing in records
+               remove_username($full_url);
                $log_entry{metadata} = "$full_url\@$r $uuid";
                $log_entry{svm_revision} = $r;
                $email ||= "$author\@$uuid"
+       } elsif ($self->use_svnsync_props) {
+               my $full_url = $self->svnsync->{url};
+               $full_url .= "/$self->{path}" if length $self->{path};
+               remove_username($full_url);
+               my $uuid = $self->svnsync->{uuid};
+               $log_entry{metadata} = "$full_url\@$rev $uuid";
+               $email ||= "$author\@$uuid"
        } else {
-               $log_entry{metadata} = $self->full_url . "\@$rev " .
+               my $url = $self->metadata_url;
+               remove_username($url);
+               $log_entry{metadata} = "$url\@$rev " .
                                       $self->ra->get_uuid;
                $email ||= "$author\@" . $self->ra->get_uuid;
        }
@@ -1694,6 +2100,8 @@ sub set_tree {
 sub rebuild {
        my ($self) = @_;
        my $db_path = $self->db_path;
+       return if (-e $db_path && ! -z $db_path);
+       return unless ::verify_ref($self->refname.'^0');
        if (-f $self->{db_root}) {
                rename $self->{db_root}, $db_path or die
                     "rename $self->{db_root} => $db_path failed: $!\n";
@@ -1703,15 +2111,20 @@ 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", '--no-color', $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)
                next if (!defined $rev || !$uuid);
@@ -1728,7 +2141,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";
 }
 
@@ -1820,6 +2233,7 @@ sub rev_db_set {
 
 sub rev_db_max {
        my ($self) = @_;
+       $self->rebuild;
        my $db_path = $self->db_path;
        my @stat = stat $db_path or return 0;
        ($stat[7] % 41) == 0 or die "$db_path inconsistent size: $stat[7]\n";
@@ -1884,6 +2298,10 @@ sub uri_encode {
        $f
 }
 
+sub remove_username {
+       $_[0] =~ s{^([^:]*://)[^@]+@}{$1};
+}
+
 package Git::SVN::Prompt;
 use strict;
 use warnings;
@@ -2204,15 +2622,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";
                }
@@ -2382,6 +2804,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);
 }
 
@@ -2578,8 +3003,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;
 }
@@ -2621,44 +3048,21 @@ package Git::SVN::Ra;
 use vars qw/@ISA $config_dir $_log_window_size/;
 use strict;
 use warnings;
-my ($can_do_switch);
-my $RA;
+my ($ra_invalid, $can_do_switch, %ignored_err, $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 {
@@ -2672,6 +3076,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(
@@ -2683,6 +3088,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,
@@ -2690,9 +3096,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
 }
@@ -2717,6 +3161,10 @@ sub gs_do_update {
        my $new = ($rev_a == $rev_b);
        my $path = $gs->{path};
 
+       if ($new && -e $gs->{index}) {
+               unlink $gs->{index} or die
+                 "Couldn't unlink index: $gs->{index}: $!\n";
+       }
        my $pool = SVN::Pool->new;
        $editor->set_path_strip($path);
        my (@pc) = split m#/#, $path;
@@ -2761,7 +3209,11 @@ sub gs_do_switch {
                        $self->{url} = $full_url;
                        $reparented = 1;
                } else {
+                       $_[0] = undef;
+                       $self = undef;
+                       $RA = undef;
                        $ra = Git::SVN::Ra->new($full_url);
+                       $ra_invalid = 1;
                }
        }
        $ra ||= $self;
@@ -2779,11 +3231,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;
 
@@ -2815,6 +3264,16 @@ 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);
+       my $ra_url = $self->{url};
        while (1) {
                my %revs;
                my $err;
@@ -2876,6 +3335,13 @@ sub gs_fetch_loop_common {
                                        "$g->{t}-maxRev";
                                Git::SVN::tmp_config($k, $r);
                        }
+                       if ($ra_invalid) {
+                               $_[0] = undef;
+                               $self = undef;
+                               $RA = undef;
+                               $self = Git::SVN::Ra->new($ra_url);
+                               $ra_invalid = undef;
+                       }
                }
                # pre-fill the .rev_db since it'll eventually get filled in
                # with '0' x40 if something new gets committed
@@ -2903,7 +3369,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} &&
@@ -2929,6 +3395,8 @@ sub match_globs {
                        my $p = $1;
                        my $pathname = $g->{path}->full_path($p);
                        next if $exists->{$pathname};
+                       next if ($self->check_path($pathname, $r) !=
+                                $SVN::Node::dir);
                        $exists->{$pathname} = Git::SVN->init(
                                              $self->{url}, $pathname, undef,
                                              $g->{ref}->full_path($p), 1);
@@ -2989,9 +3457,16 @@ sub skip_unknown_revs {
        # 175007 - http(s):// (this repo required authorization, too...)
        #   More codes may be discovered later...
        if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
-               warn "W: Ignoring error from SVN, path probably ",
-                    "does not exist: ($errno): ",
-                    $err->expanded_message,"\n";
+               my $err_key = $err->expanded_message;
+               # revision numbers change every time, filter them out
+               $err_key =~ s/\d+/\0/g;
+               $err_key = "$errno\0$err_key";
+               unless ($ignored_err{$err_key}) {
+                       warn "W: Ignoring error from SVN, path probably ",
+                            "does not exist: ($errno): ",
+                            $err->expanded_message,"\n";
+                       $ignored_err{$err_key} = 1;
+               }
                return;
        }
        die "Error from SVN, ($errno): ", $err->expanded_message,"\n";
@@ -3026,12 +3501,19 @@ package Git::SVN::Log;
 sub cmt_showable {
        my ($c) = @_;
        return 1 if defined $c->{r};
+
+       # big commit message got truncated by the 16k pretty buffer in rev-list
        if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
                                $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
+               @{$c->{l}} = ();
                my @log = command(qw/cat-file commit/, $c->{c});
-               shift @log while ($log[0] ne "\n");
+
+               # shift off the headers
+               shift @log while ($log[0] ne '');
                shift @log;
-               @{$c->{l}} = grep !/^git-svn-id: /, @log;
+
+               # TODO: make $c->{l} not have a trailing newline in the future
+               @{$c->{l}} = map { "$_\n" } grep !/^git-svn-id: /, @log;
 
                (undef, $c->{r}, undef) = ::extract_metadata(
                                (grep(/^git-svn-id: /, @log))[-1]);
@@ -3078,35 +3560,33 @@ sub log_use_color {
 sub git_svn_log_cmd {
        my ($r_min, $r_max, @args) = @_;
        my $head = 'HEAD';
+       my (@files, @log_opts);
        foreach my $x (@args) {
-               last if $x eq '--';
-               next unless ::verify_ref("$x^0");
-               $head = $x;
-               last;
-       }
-
-       my $url;
-       my ($fh, $ctx) = command_output_pipe('rev-list', $head);
-       while (<$fh>) {
-               chomp;
-               $url = (::cmt_metadata($_))[0];
-               last if defined $url;
+               if ($x eq '--' || @files) {
+                       push @files, $x;
+               } else {
+                       if (::verify_ref("$x^0")) {
+                               $head = $x;
+                       } else {
+                               push @log_opts, $x;
+                       }
+               }
        }
-       close $fh; # break the pipe
 
-       my $gs = Git::SVN->find_by_url($url) || Git::SVN->_new;
+       my ($url, $rev, $uuid, $gs) = ::working_head_info($head);
+       $gs ||= Git::SVN->_new;
        my @cmd = (qw/log --abbrev-commit --pretty=raw --default/,
                   $gs->refname);
        push @cmd, '-r' unless $non_recursive;
        push @cmd, qw/--raw --name-status/ if $verbose;
        push @cmd, '--color' if log_use_color();
-       return @cmd unless defined $r_max;
-       if ($r_max == $r_min) {
+       push @cmd, @log_opts;
+       if (defined $r_max && $r_max == $r_min) {
                push @cmd, '--max-count=1';
                if (my $c = $gs->rev_db_get($r_max)) {
                        push @cmd, $c;
                }
-       } else {
+       } elsif (defined $r_max) {
                my ($c_min, $c_max);
                $c_max = $gs->rev_db_get($r_max);
                $c_min = $gs->rev_db_get($r_min);
@@ -3122,7 +3602,7 @@ sub git_svn_log_cmd {
                        push @cmd, $c_min;
                }
        }
-       return @cmd;
+       return (@cmd, @files);
 }
 
 # adapted from pager.c
@@ -3136,7 +3616,7 @@ sub config_pager {
 }
 
 sub run_pager {
-       return unless -t *STDOUT;
+       return unless -t *STDOUT && defined $pager;
        pipe my $rfd, my $wfd or return;
        defined(my $pid = fork) or ::fatal "Can't fork: $!\n";
        if (!$pid) {
@@ -3287,7 +3767,7 @@ sub cmd_show_log {
        }
 
        config_pager();
-       @args = (git_svn_log_cmd($r_min, $r_max, @args), @args);
+       @args = git_svn_log_cmd($r_min, $r_max, @args);
        my $log = command_output_pipe(@args);
        run_pager();
        my (@k, $c, $d, $stat);