test format-patch -s: make sure MIME content type is shown as needed
[gitweb.git] / git-svn.perl
index de4e74a60d58500d175eeb15968e1579a9874c60..ec25ea4231a062e05753f4f93a757a41062a0364 100755 (executable)
 use warnings;
 use strict;
 use vars qw/   $AUTHOR $VERSION
-               $SVN_URL $SVN_INFO $SVN_WC $SVN_UUID
-               $GIT_SVN_INDEX $GIT_SVN
-               $GIT_DIR $GIT_SVN_DIR $REVDB/;
+               $sha1 $sha1_short $_revision
+               $_q $_authors %users/;
 $AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
 $VERSION = '@@GIT_VERSION@@';
 
-use Cwd qw/abs_path/;
-$GIT_DIR = abs_path($ENV{GIT_DIR} || '.git');
-$ENV{GIT_DIR} = $GIT_DIR;
+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';
+$Git::SVN::Ra::_log_window_size = 100;
 
-my $LC_ALL = $ENV{LC_ALL};
-my $TZ = $ENV{TZ};
-# make sure the svn binary gives consistent output between locales and TZs:
+$Git::SVN::Log::TZ = $ENV{TZ};
 $ENV{TZ} = 'UTC';
-$ENV{LC_ALL} = 'C';
 $| = 1; # unbuffer STDOUT
 
-sub fatal (@) { print STDERR $@; exit 1 }
-# If SVN:: library support is added, please make the dependencies
-# optional and preserve the capability to use the command-line client.
-# use eval { require SVN::... } to make it lazy load
-# We don't use any modules not in the standard Perl distribution:
+sub fatal (@) { print STDERR @_; exit 1 }
+require SVN::Core; # use()-ing this causes segfaults for me... *shrug*
+require SVN::Ra;
+require SVN::Delta;
+if ($SVN::Core::VERSION lt '1.1.0') {
+       fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)\n";
+}
+push @Git::SVN::Ra::ISA, 'SVN::Ra';
+push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor';
+push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor';
 use Carp qw/croak/;
 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 File::Spec qw//;
-use File::Copy qw/copy/;
-use POSIX qw/strftime/;
+use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
 use IPC::Open3;
-use Memoize;
-memoize('revisions_eq');
-memoize('cmt_metadata');
-memoize('get_commit_time');
-
-my ($SVN, $_use_lib);
-
-sub nag_lib {
-       print STDERR <<EOF;
-! Please consider installing the SVN Perl libraries (version 1.1.0 or
-! newer).  You will generally get better performance and fewer bugs,
-! especially if you:
-! 1) have a case-insensitive filesystem
-! 2) replace symlinks with files (and vice-versa) in commits
-
-EOF
+use Git;
+
+BEGIN {
+       # 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/) {
+               for my $package ( qw(SVN::Git::Editor SVN::Git::Fetcher
+                       Git::SVN::Migration Git::SVN::Log Git::SVN),
+                       __PACKAGE__) {
+                       *{"${package}::$_"} = \&{"Git::$_"};
+               }
+       }
 }
 
-$_use_lib = 1 unless $ENV{GIT_SVN_NO_LIB};
-libsvn_load();
-nag_lib() unless $_use_lib;
-
-my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS};
-my $sha1 = qr/[a-f\d]{40}/;
-my $sha1_short = qr/[a-f\d]{4,40}/;
-my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
-       $_find_copies_harder, $_l, $_cp_similarity, $_cp_remote,
-       $_repack, $_repack_nr, $_repack_flags, $_q,
-       $_message, $_file, $_follow_parent, $_no_metadata,
-       $_template, $_shared, $_no_default_regex, $_no_graft_copy,
-       $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit,
-       $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m,
-       $_merge, $_strategy, $_dry_run, $_ignore_nodate, $_non_recursive,
-       $_username, $_config_dir, $_no_auth_cache);
-my (@_branch_from, %tree_map, %users, %rusers, %equiv);
-my ($_svn_co_url_revs, $_svn_pg_peg_revs);
-my @repo_path_split_cache;
-
-my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext,
-               'branch|b=s' => \@_branch_from,
-               'follow-parent|follow' => \$_follow_parent,
-               'branch-all-refs|B' => \$_branch_all_refs,
+my ($SVN);
+
+$sha1 = qr/[a-f\d]{40}/;
+$sha1_short = qr/[a-f\d]{4,40}/;
+my ($_stdin, $_help, $_edit,
+       $_message, $_file,
+       $_template, $_shared,
+       $_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,
+                    'no-auth-cache' => \$Git::SVN::Prompt::_no_auth_cache );
+my %fc_opts = ( 'follow-parent|follow!' => \$Git::SVN::_follow_parent,
                'authors-file|A=s' => \$_authors,
-               'repack:i' => \$_repack,
-               'no-metadata' => \$_no_metadata,
+               '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,
-               'username=s' => \$_username,
-               'config-dir=s' => \$_config_dir,
-               'no-auth-cache' => \$_no_auth_cache,
-               'ignore-nodate' => \$_ignore_nodate,
-               'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
-
-my ($_trunk, $_tags, $_branches);
-my %multi_opts = ( 'trunk|T=s' => \$_trunk,
-               'tags|t=s' => \$_tags,
-               'branches|b=s' => \$_branches );
-my %init_opts = ( 'template=s' => \$_template, 'shared' => \$_shared );
+               'repack-flags|repack-args|repack-opts=s' =>
+                  \$Git::SVN::_repack_flags,
+               %remote_opts );
+
+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' => \$_rmdir,
-               'find-copies-harder' => \$_find_copies_harder,
-               'l=i' => \$_l,
-               'copy-similarity|C=i'=> \$_cp_similarity
+               'rmdir' => \$SVN::Git::Editor::_rmdir,
+               'find-copies-harder' => \$SVN::Git::Editor::_find_copies_harder,
+               'l=i' => \$SVN::Git::Editor::_rename_limit,
+               'copy-similarity|C=i'=> \$SVN::Git::Editor::_cp_similarity
 );
 
 my %cmd = (
-       fetch => [ \&fetch, "Download new revisions from SVN",
-                       { 'revision|r=s' => \$_revision, %fc_opts } ],
-       init => [ \&init, "Initialize a repo for tracking" .
+       fetch => [ \&cmd_fetch, "Download new revisions from SVN",
+                       { 'revision|r=s' => \$_revision,
+                         '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 ],
-       commit => [ \&commit, "Commit git revisions to SVN",
-                       {       'stdin|' => \$_stdin, %cmt_opts, %fc_opts, } ],
-       'show-ignore' => [ \&show_ignore, "Show svn:ignore listings",
+       'multi-init' => [ \&cmd_multi_init,
+                         "Deprecated alias for ".
+                         "'$0 init -T<trunk> -b<branches> -t<tags>'",
+                         \%init_opts ],
+       dcommit => [ \&cmd_dcommit,
+                    '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",
+                       { 'stdin|' => \$_stdin, %cmt_opts, %fc_opts, } ],
+       'show-ignore' => [ \&cmd_show_ignore, "Show svn:ignore listings",
                        { 'revision|r=i' => \$_revision } ],
-       rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)",
-                       { 'no-ignore-externals' => \$_no_ignore_ext,
-                         'copy-remote|remote=s' => \$_cp_remote,
-                         'upgrade' => \$_upgrade } ],
-       'graft-branches' => [ \&graft_branches,
-                       'Detect merges/branches from already imported history',
-                       { 'merge-rx|m' => \@_opt_m,
-                         'branch|b=s' => \@_branch_from,
-                         'branch-all-refs|B' => \$_branch_all_refs,
-                         'no-default-regex' => \$_no_default_regex,
-                         'no-graft-copy' => \$_no_graft_copy } ],
-       'multi-init' => [ \&multi_init,
-                       'Initialize multiple trees (like git-svnimport)',
-                       { %multi_opts, %fc_opts } ],
-       'multi-fetch' => [ \&multi_fetch,
-                       'Fetch multiple trees (like git-svnimport)',
-                       \%fc_opts ],
-       'log' => [ \&show_log, 'Show commit logs',
-                       { 'limit=i' => \$_limit,
+       'multi-fetch' => [ \&cmd_multi_fetch,
+                          "Deprecated alias for $0 fetch --all",
+                          { 'revision|r=s' => \$_revision, %fc_opts } ],
+       'migrate' => [ sub { },
+                      # no-op, we automatically run this anyways,
+                      'Migrate configuration/metadata/layout from
+                       previous versions of git-svn',
+                       { 'minimize' => \$Git::SVN::Migration::_minimize,
+                        %remote_opts } ],
+       'log' => [ \&Git::SVN::Log::cmd_show_log, 'Show commit logs',
+                       { 'limit=i' => \$Git::SVN::Log::limit,
                          'revision|r=s' => \$_revision,
-                         'verbose|v' => \$_verbose,
-                         'incremental' => \$_incremental,
-                         'oneline' => \$_oneline,
-                         'show-commit' => \$_show_commit,
-                         'non-recursive' => \$_non_recursive,
+                         'verbose|v' => \$Git::SVN::Log::verbose,
+                         'incremental' => \$Git::SVN::Log::incremental,
+                         'oneline' => \$Git::SVN::Log::oneline,
+                         'show-commit' => \$Git::SVN::Log::show_commit,
+                         'non-recursive' => \$Git::SVN::Log::non_recursive,
                          'authors-file|A=s' => \$_authors,
+                         'color' => \$Git::SVN::Log::color,
+                         'pager=s' => \$Git::SVN::Log::pager,
                        } ],
-       'commit-diff' => [ \&commit_diff, 'Commit a diff between two trees',
+       '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,
                          'file|F=s' => \$_file,
                          'revision|r=s' => \$_revision,
                        %cmt_opts } ],
-       dcommit => [ \&dcommit, 'Commit several diffs to merge with upstream',
-                       { 'merge|m|M' => \$_merge,
-                         'strategy|s=s' => \$_strategy,
-                         'dry-run|n' => \$_dry_run,
-                       %cmt_opts } ],
 );
 
 my $cmd;
@@ -160,21 +175,51 @@ sub nag_lib {
 my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
 
 read_repo_config(\%opts);
-my $rv = GetOptions(%opts, 'help|H|h' => \$_help,
-                               'version|V' => \$_version,
-                               'id|i=s' => \$GIT_SVN);
-exit 1 if (!$rv && $cmd ne 'log');
+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' => sub {
+                       $Git::SVN::no_reuse_existing = 1;
+                       $Git::SVN::default_repo_id = $_[1] });
+exit 1 if (!$rv && $cmd && $cmd ne 'log');
 
-set_default_vals();
 usage(0) if $_help;
 version() if $_version;
 usage(1) unless defined $cmd;
-init_vars();
 load_authors() if $_authors;
-load_all_refs() if $_branch_all_refs;
-svn_compat_check() unless $_use_lib;
-migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init|commit-diff)$/;
-$cmd{$cmd}->[0]->(@ARGV);
+
+# 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();
+eval {
+       Git::SVN::verify_remotes_sanity();
+       $cmd{$cmd}->[0]->(@ARGV);
+};
+fatal $@ if $@;
+post_fetch_checkout();
 exit 0;
 
 ####################### primary functions ######################
@@ -189,8 +234,11 @@ sub usage {
 
        foreach (sort keys %cmd) {
                next if $cmd && $cmd ne $_;
+               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 ?
@@ -208,256 +256,89 @@ sub usage {
 }
 
 sub version {
-       print "git-svn version $VERSION\n";
+       print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n";
        exit 0;
 }
 
-sub rebuild {
-       if (quiet_run(qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0")) {
-               copy_remote_ref();
-       }
-       $SVN_URL = shift or undef;
-       my $newest_rev = 0;
-       if ($_upgrade) {
-               sys('git-update-ref',"refs/remotes/$GIT_SVN","$GIT_SVN-HEAD");
-       } else {
-               check_upgrade_needed();
-       }
-
-       my $pid = open(my $rev_list,'-|');
-       defined $pid or croak $!;
-       if ($pid == 0) {
-               exec("git-rev-list","refs/remotes/$GIT_SVN") or croak $!;
-       }
-       my $latest;
-       while (<$rev_list>) {
-               chomp;
-               my $c = $_;
-               croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o;
-               my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`);
-               next if (!@commit); # skip merges
-               my ($url, $rev, $uuid) = extract_metadata($commit[$#commit]);
-               if (!defined $rev || !$uuid) {
-                       croak "Unable to extract revision or UUID from ",
-                               "$c, $commit[$#commit]\n";
-               }
-
-               # if we merged or otherwise started elsewhere, this is
-               # how we break out of it
-               next if (defined $SVN_UUID && ($uuid ne $SVN_UUID));
-               next if (defined $SVN_URL && defined $url && ($url ne $SVN_URL));
-
-               unless (defined $latest) {
-                       if (!$SVN_URL && !$url) {
-                               croak "SVN repository location required: $url\n";
+sub do_git_init_db {
+       unless (-d $ENV{GIT_DIR}) {
+               my @init_db = ('init');
+               push @init_db, "--template=$_template" if defined $_template;
+               if (defined $_shared) {
+                       if ($_shared =~ /[a-z]/) {
+                               push @init_db, "--shared=$_shared";
+                       } else {
+                               push @init_db, "--shared";
                        }
-                       $SVN_URL ||= $url;
-                       $SVN_UUID ||= $uuid;
-                       setup_git_svn();
-                       $latest = $rev;
                }
-               revdb_set($REVDB, $rev, $c);
-               print "r$rev = $c\n";
-               $newest_rev = $rev if ($rev > $newest_rev);
+               command_noisy(@init_db);
        }
-       close $rev_list or croak $?;
-
-       goto out if $_use_lib;
-       if (!chdir $SVN_WC) {
-               svn_cmd_checkout($SVN_URL, $latest, $SVN_WC);
-               chdir $SVN_WC or croak $!;
+       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;
        }
+}
 
-       $pid = fork;
-       defined $pid or croak $!;
-       if ($pid == 0) {
-               my @svn_up = qw(svn up);
-               push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
-               sys(@svn_up,"-r$newest_rev");
-               $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
-               index_changes();
-               exec('git-write-tree') or croak $!;
-       }
-       waitpid $pid, 0;
-       croak $? if $?;
-out:
-       if ($_upgrade) {
-               print STDERR <<"";
-Keeping deprecated refs/head/$GIT_SVN-HEAD for now.  Please remove it
-when you have upgraded your tools and habits to use refs/remotes/$GIT_SVN
+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} = '.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 init {
-       my $url = shift or die "SVN repository location required " .
-                               "as a command-line argument\n";
-       $url =~ s!/+$!!; # strip trailing slash
-
-       if (my $repo_path = shift) {
-               unless (-d $repo_path) {
-                       mkpath([$repo_path]);
-               }
-               $GIT_DIR = $ENV{GIT_DIR} = $repo_path . "/.git";
-               init_vars();
+sub cmd_init {
+       if (defined $_stdlayout) {
+               $_trunk = 'trunk' if (!defined $_trunk);
+               $_tags = 'tags' if (!defined $_tags);
+               $_branches = 'branches' if (!defined $_branches);
        }
-
-       $SVN_URL = $url;
-       unless (-d $GIT_DIR) {
-               my @init_db = ('git-init-db');
-               push @init_db, "--template=$_template" if defined $_template;
-               push @init_db, "--shared" if defined $_shared;
-               sys(@init_db);
+       if (defined $_trunk || defined $_branches || defined $_tags) {
+               return cmd_multi_init(@_);
        }
-       setup_git_svn();
-}
+       my $url = shift or die "SVN repository location required ",
+                              "as a command-line argument\n";
+       init_subdir(@_);
+       do_git_init_db();
 
-sub fetch {
-       check_upgrade_needed();
-       $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
-       my $ret = $_use_lib ? fetch_lib(@_) : fetch_cmd(@_);
-       if ($ret->{commit} && quiet_run(qw(git-rev-parse --verify
-                                               refs/heads/master^0))) {
-               sys(qw(git-update-ref refs/heads/master),$ret->{commit});
-       }
-       return $ret;
+       Git::SVN->init($url);
 }
 
-sub fetch_cmd {
-       my (@parents) = @_;
-       my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL);
-       unless ($_revision) {
-               $_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD';
-       }
-       push @log_args, "-r$_revision";
-       push @log_args, '--stop-on-copy' unless $_no_stop_copy;
-
-       my $svn_log = svn_log_raw(@log_args);
-
-       my $base = next_log_entry($svn_log) or croak "No base revision!\n";
-       # don't need last_revision from grab_base_rev() because
-       # user could've specified a different revision to skip (they
-       # didn't want to import certain revisions into git for whatever
-       # reason, so trust $base->{revision} instead.
-       my (undef, $last_commit) = svn_grab_base_rev();
-       unless (-d $SVN_WC) {
-               svn_cmd_checkout($SVN_URL,$base->{revision},$SVN_WC);
-               chdir $SVN_WC or croak $!;
-               read_uuid();
-               $last_commit = git_commit($base, @parents);
-               assert_tree($last_commit);
-       } else {
-               chdir $SVN_WC or croak $!;
-               read_uuid();
-               # looks like a user manually cp'd and svn switch'ed
-               unless ($last_commit) {
-                       sys(qw/svn revert -R ./);
-                       assert_svn_wc_clean($base->{revision});
-                       $last_commit = git_commit($base, @parents);
-                       assert_tree($last_commit);
-               }
-       }
-       my @svn_up = qw(svn up);
-       push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
-       my $last = $base;
-       while (my $log_msg = next_log_entry($svn_log)) {
-               if ($last->{revision} >= $log_msg->{revision}) {
-                       croak "Out of order: last >= current: ",
-                               "$last->{revision} >= $log_msg->{revision}\n";
-               }
-               # Revert is needed for cases like:
-               # https://svn.musicpd.org/Jamming/trunk (r166:167), but
-               # I can't seem to reproduce something like that on a test...
-               sys(qw/svn revert -R ./);
-               assert_svn_wc_clean($last->{revision});
-               sys(@svn_up,"-r$log_msg->{revision}");
-               $last_commit = git_commit($log_msg, $last_commit, @parents);
-               $last = $log_msg;
-       }
-       close $svn_log->{fh};
-       $last->{commit} = $last_commit;
-       return $last;
-}
-
-sub fetch_lib {
-       my (@parents) = @_;
-       $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
-       $SVN ||= libsvn_connect($SVN_URL);
-       my ($last_rev, $last_commit) = svn_grab_base_rev();
-       my ($base, $head) = libsvn_parse_revision($last_rev);
-       if ($base > $head) {
-               return { revision => $last_rev, commit => $last_commit }
-       }
-       my $index = set_index($GIT_SVN_INDEX);
-
-       # limit ourselves and also fork() since get_log won't release memory
-       # after processing a revision and SVN stuff seems to leak
-       my $inc = 1000;
-       my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
-       read_uuid();
-       if (defined $last_commit) {
-               unless (-e $GIT_SVN_INDEX) {
-                       sys(qw/git-read-tree/, $last_commit);
-               }
-               chomp (my $x = `git-write-tree`);
-               my ($y) = (`git-cat-file commit $last_commit`
-                                                       =~ /^tree ($sha1)/m);
-               if ($y ne $x) {
-                       unlink $GIT_SVN_INDEX or croak $!;
-                       sys(qw/git-read-tree/, $last_commit);
-               }
-               chomp ($x = `git-write-tree`);
-               if ($y ne $x) {
-                       print STDERR "trees ($last_commit) $y != $x\n",
-                                "Something is seriously wrong...\n";
-               }
+sub cmd_fetch {
+       if (grep /^\d+=./, @_) {
+               die "'<rev>=<commit>' fetch arguments are ",
+                   "no longer supported.\n";
        }
-       while (1) {
-               # fork, because using SVN::Pool with get_log() still doesn't
-               # seem to help enough to keep memory usage down.
-               defined(my $pid = fork) or croak $!;
-               if (!$pid) {
-                       $SVN::Error::handler = \&libsvn_skip_unknown_revs;
-
-                       # Yes I'm perfectly aware that the fourth argument
-                       # below is the limit revisions number.  Unfortunately
-                       # performance sucks with it enabled, so it's much
-                       # faster to fetch revision ranges instead of relying
-                       # on the limiter.
-                       libsvn_get_log(libsvn_dup_ra($SVN), [''],
-                                       $min, $max, 0, 1, 1,
-                               sub {
-                                       my $log_msg;
-                                       if ($last_commit) {
-                                               $log_msg = libsvn_fetch(
-                                                       $last_commit, @_);
-                                               $last_commit = git_commit(
-                                                       $log_msg,
-                                                       $last_commit,
-                                                       @parents);
-                                       } else {
-                                               $log_msg = libsvn_new_tree(@_);
-                                               $last_commit = git_commit(
-                                                       $log_msg, @parents);
-                                       }
-                               });
-                       exit 0;
-               }
-               waitpid $pid, 0;
-               croak $? if $?;
-               ($last_rev, $last_commit) = svn_grab_base_rev();
-               last if ($max >= $head);
-               $min = $max + 1;
-               $max += $inc;
-               $max = $head if ($max > $head);
+       my ($remote) = @_;
+       if (@_ > 1) {
+               die "Usage: $0 fetch [--all] [svn-remote]\n";
+       }
+       $remote ||= $Git::SVN::default_repo_id;
+       if ($_fetch_all) {
+               cmd_multi_fetch();
+       } else {
+               Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes());
        }
-       restore_index($index);
-       return { revision => $last_rev, commit => $last_commit };
 }
 
-sub commit {
+sub cmd_set_tree {
        my (@commits) = @_;
-       check_upgrade_needed();
        if ($_stdin || !@commits) {
                print "Reading from stdin...\n";
                @commits = ();
@@ -469,1567 +350,1418 @@ sub commit {
        }
        my @revs;
        foreach my $c (@commits) {
-               chomp(my @tmp = safe_qx('git-rev-parse',$c));
+               my @tmp = command('rev-parse',$c);
                if (scalar @tmp == 1) {
                        push @revs, $tmp[0];
                } elsif (scalar @tmp > 1) {
-                       push @revs, reverse (safe_qx('git-rev-list',@tmp));
+                       push @revs, reverse(command('rev-list',@tmp));
                } else {
-                       die "Failed to rev-parse $c\n";
+                       fatal "Failed to rev-parse $c\n";
                }
        }
-       chomp @revs;
-       $_use_lib ? commit_lib(@revs) : commit_cmd(@revs);
-       print "Done committing ",scalar @revs," revisions to SVN\n";
-}
-
-sub commit_cmd {
-       my (@revs) = @_;
-
-       chdir $SVN_WC or croak "Unable to chdir $SVN_WC: $!\n";
-       my $info = svn_info('.');
-       my $fetched = fetch();
-       if ($info->{Revision} != $fetched->{revision}) {
-               print STDERR "There are new revisions that were fetched ",
-                               "and need to be merged (or acknowledged) ",
-                               "before committing.\n";
-               exit 1;
-       }
-       $info = svn_info('.');
-       read_uuid($info);
-       my $last = $fetched;
-       foreach my $c (@revs) {
-               my $mods = svn_checkout_tree($last, $c);
-               if (scalar @$mods == 0) {
-                       print "Skipping, no changes detected\n";
-                       next;
-               }
-               $last = svn_commit_tree($last, $c);
+       my $gs = Git::SVN->new;
+       my ($r_last, $cmt_last) = $gs->last_rev_commit;
+       $gs->fetch;
+       if (defined $gs->{last_rev} && $r_last != $gs->{last_rev}) {
+               fatal "There are new revisions that were fetched ",
+                     "and need to be merged (or acknowledged) ",
+                     "before committing.\nlast rev: $r_last\n",
+                     " current: $gs->{last_rev}\n";
        }
+       $gs->set_tree($_) foreach @revs;
+       print "Done committing ",scalar @revs," revisions to SVN\n";
 }
 
-sub commit_lib {
-       my (@revs) = @_;
-       my ($r_last, $cmt_last) = svn_grab_base_rev();
-       defined $r_last or die "Must have an existing revision to commit\n";
-       my $fetched = fetch();
-       if ($r_last != $fetched->{revision}) {
-               print STDERR "There are new revisions that were fetched ",
-                               "and need to be merged (or acknowledged) ",
-                               "before committing.\n",
-                               "last rev: $r_last\n",
-                               " current: $fetched->{revision}\n";
-               exit 1;
-       }
-       read_uuid();
-       my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
-       my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
-
-       my $repo;
-       set_svn_commit_env();
-       foreach my $c (@revs) {
-               my $log_msg = get_commit_message($c, $commit_msg);
-
-               # fork for each commit because there's a memory leak I
-               # can't track down... (it's probably in the SVN code)
-               defined(my $pid = open my $fh, '-|') or croak $!;
-               if (!$pid) {
-                       my $ed = SVN::Git::Editor->new(
-                                       {       r => $r_last,
-                                               ra => libsvn_dup_ra($SVN),
-                                               c => $c,
-                                               svn_path => $SVN->{svn_path},
-                                       },
-                                       $SVN->get_commit_editor(
-                                               $log_msg->{msg},
-                                               sub {
-                                                       libsvn_commit_cb(
-                                                               @_, $c,
-                                                               $log_msg->{msg},
-                                                               $r_last,
-                                                               $cmt_last)
-                                               },
-                                               @lock)
-                                       );
-                       my $mods = libsvn_checkout_tree($cmt_last, $c, $ed);
-                       if (@$mods == 0) {
-                               print "No changes\nr$r_last = $cmt_last\n";
-                               $ed->abort_edit;
-                       } else {
-                               $ed->close_edit;
-                       }
-                       exit 0;
-               }
-               my ($r_new, $cmt_new, $no);
-               while (<$fh>) {
-                       print $_;
-                       chomp;
-                       if (/^r(\d+) = ($sha1)$/o) {
-                               ($r_new, $cmt_new) = ($1, $2);
-                       } elsif ($_ eq 'No changes') {
-                               $no = 1;
-                       }
-               }
-               close $fh or exit 1;
-               if (! defined $r_new && ! defined $cmt_new) {
-                       unless ($no) {
-                               die "Failed to parse revision information\n";
-                       }
-               } else {
-                       ($r_last, $cmt_last) = ($r_new, $cmt_new);
-               }
+sub cmd_dcommit {
+       my $head = shift;
+       $head ||= 'HEAD';
+       my @refs;
+       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";
        }
-       $ENV{LC_ALL} = 'C';
-       unlink $commit_msg;
-}
-
-sub dcommit {
-       my $gs = "refs/remotes/$GIT_SVN";
-       chomp(my @refs = safe_qx(qw/git-rev-list --no-merges/, "$gs..HEAD"));
        my $last_rev;
-       foreach my $d (reverse @refs) {
-               if (quiet_run('git-rev-parse','--verify',"$d~1") != 0) {
-                       die "Commit $d\n",
-                           "has no parent commit, and therefore ",
-                           "nothing to diff against.\n",
-                           "You should be working from a repository ",
-                           "originally created by git-svn\n";
-               }
+       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) {
-                               die "Unable to extract revision information ",
-                                   "from commit $d~1\n";
+                               fatal "Unable to extract revision information ",
+                                     "from commit $d~1\n";
                        }
                }
                if ($_dry_run) {
                        print "diff-tree $d~1 $d\n";
                } else {
-                       if (my $r = commit_diff("$d~1", $d, undef, $last_rev)) {
-                               $last_rev = $r;
-                       } # else: no changes, same $last_rev
-               }
-       }
-       return if $_dry_run;
-       fetch();
-       my @diff = safe_qx(qw/git-diff-tree HEAD/, $gs);
-       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 differ, using @finish:\n", @diff;
-       } else {
-               print "No changes between current HEAD and $gs\n",
-                     "Hard resetting to the latest $gs\n";
-               @finish = qw/reset --mixed/;
-       }
-       sys('git', @finish, $gs);
-}
-
-sub show_ignore {
-       $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
-       $_use_lib ? show_ignore_lib() : show_ignore_cmd();
-}
-
-sub show_ignore_cmd {
-       require File::Find or die $!;
-       if (defined $_revision) {
-               die "-r/--revision option doesn't work unless the Perl SVN ",
-                       "libraries are used\n";
-       }
-       chdir $SVN_WC or croak $!;
-       my %ign;
-       File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){
-               s#^\./##;
-               @{$ign{$_}} = svn_propget_base('svn:ignore', $_);
-               }}, no_chdir=>1},'.');
-
-       print "\n# /\n";
-       foreach (@{$ign{'.'}}) { print '/',$_ if /\S/ }
-       delete $ign{'.'};
-       foreach my $i (sort keys %ign) {
-               print "\n# ",$i,"\n";
-               foreach (@{$ign{$i}}) { print '/',$i,'/',$_ if /\S/ }
-       }
-}
-
-sub show_ignore_lib {
-       my $repo;
-       $SVN ||= libsvn_connect($SVN_URL);
-       my $r = defined $_revision ? $_revision : $SVN->get_latest_revnum;
-       libsvn_traverse_ignore(\*STDOUT, $SVN->{svn_path}, $r);
-}
-
-sub graft_branches {
-       my $gr_file = "$GIT_DIR/info/grafts";
-       my ($grafts, $comments) = read_grafts($gr_file);
-       my $gr_sha1;
-
-       if (%$grafts) {
-               # temporarily disable our grafts file to make this idempotent
-               chomp($gr_sha1 = safe_qx(qw/git-hash-object -w/,$gr_file));
-               rename $gr_file, "$gr_file~$gr_sha1" or croak $!;
-       }
-
-       my $l_map = read_url_paths();
-       my @re = map { qr/$_/is } @_opt_m if @_opt_m;
-       unless ($_no_default_regex) {
-               push @re, (qr/\b(?:merge|merging|merged)\s+with\s+([\w\.\-]+)/i,
-                       qr/\b(?:merge|merging|merged)\s+([\w\.\-]+)/i,
-                       qr/\b(?:from|of)\s+([\w\.\-]+)/i );
-       }
-       foreach my $u (keys %$l_map) {
-               if (@re) {
-                       foreach my $p (keys %{$l_map->{$u}}) {
-                               graft_merge_msg($grafts,$l_map,$u,$p,@re);
+                       my $cmt_rev;
+                       my %ed_opts = ( r => $last_rev,
+                                       log => get_commit_entry($d)->{log},
+                                       ra => Git::SVN::Ra->new($gs->full_url),
+                                       tree_a => "$d~1",
+                                       tree_b => $d,
+                                       editor_cb => sub {
+                                              print "Committed r$_[0]\n";
+                                              $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};
                        }
-               }
-               unless ($_no_graft_copy) {
-                       if ($_use_lib) {
-                               graft_file_copy_lib($grafts,$l_map,$u);
+                       $_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 {
-                               graft_file_copy_cmd($grafts,$l_map,$u);
+                               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;
                }
        }
-       graft_tree_joins($grafts);
-
-       write_grafts($grafts, $comments, $gr_file);
-       unlink "$gr_file~$gr_sha1" if $gr_sha1;
 }
 
-sub multi_init {
-       my $url = shift;
-       $_trunk ||= 'trunk';
-       $_trunk =~ s#/+$##;
-       $url =~ s#/+$## if $url;
-       if ($_trunk !~ m#^[a-z\+]+://#) {
-               $_trunk = '/' . $_trunk if ($_trunk !~ m#^/#);
-               unless ($url) {
-                       print STDERR "E: '$_trunk' is not a complete URL ",
-                               "and a separate URL is not specified\n";
-                       exit 1;
+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";
                }
-               $_trunk = $url . $_trunk;
-       }
-       my $ch_id;
-       if ($GIT_SVN eq 'git-svn') {
-               $ch_id = 1;
-               $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
-       }
-       init_vars();
-       unless (-d $GIT_SVN_DIR) {
-               print "GIT_SVN_ID set to 'trunk' for $_trunk\n" if $ch_id;
-               init($_trunk);
-               sys('git-repo-config', 'svn.trunk', $_trunk);
+               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;
        }
-       complete_url_ls_init($url, $_branches, '--branches/-b', '');
-       complete_url_ls_init($url, $_tags, '--tags/-t', 'tags/');
+       print "$result\n" if $result;
 }
 
-sub multi_fetch {
-       # try to do trunk first, since branches/tags
-       # may be descended from it.
-       if (-e "$GIT_DIR/svn/trunk/info/url") {
-               fetch_child_id('trunk', @_);
+sub cmd_rebase {
+       command_noisy(qw/update-index --refresh/);
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       unless ($gs) {
+               die "Unable to determine upstream SVN information from ",
+                   "working tree history\n";
        }
-       rec_fetch('', "$GIT_DIR/svn", @_);
-}
-
-sub show_log {
-       my (@args) = @_;
-       my ($r_min, $r_max);
-       my $r_last = -1; # prevent dupes
-       rload_authors() if $_authors;
-       if (defined $TZ) {
-               $ENV{TZ} = $TZ;
-       } else {
-               delete $ENV{TZ};
+       if (command(qw/diff-index HEAD --/)) {
+               print STDERR "Cannot rebase with uncommited changes:\n";
+               command_noisy('status');
+               exit 1;
        }
-       if (defined $_revision) {
-               if ($_revision =~ /^(\d+):(\d+)$/) {
-                       ($r_min, $r_max) = ($1, $2);
-               } elsif ($_revision =~ /^\d+$/) {
-                       $r_min = $r_max = $_revision;
-               } else {
-                       print STDERR "-r$_revision is not supported, use ",
-                               "standard \'git log\' arguments instead\n";
-                       exit 1;
-               }
+       unless ($_local) {
+               $_fetch_all ? $gs->fetch_all : $gs->fetch;
        }
+       command_noisy(rebase_cmd(), $gs->refname);
+}
 
-       my $pid = open(my $log,'-|');
-       defined $pid or croak $!;
-       if (!$pid) {
-               exec(git_svn_log_cmd($r_min,$r_max), @args) or croak $!;
-       }
-       setup_pager();
-       my (@k, $c, $d);
+sub cmd_show_ignore {
+       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, $gs->{path}, $r);
+}
 
-       while (<$log>) {
-               if (/^commit ($sha1_short)/o) {
-                       my $cmt = $1;
-                       if ($c && cmt_showable($c) && $c->{r} != $r_last) {
-                               $r_last = $c->{r};
-                               process_commit($c, $r_min, $r_max, \@k) or
-                                                               goto out;
-                       }
-                       $d = undef;
-                       $c = { c => $cmt };
-               } elsif (/^author (.+) (\d+) ([\-\+]?\d+)$/) {
-                       get_author_info($c, $1, $2, $3);
-               } elsif (/^(?:tree|parent|committer) /) {
-                       # ignore
-               } elsif (/^:\d{6} \d{6} $sha1_short/o) {
-                       push @{$c->{raw}}, $_;
-               } elsif (/^[ACRMDT]\t/) {
-                       # we could add $SVN->{svn_path} here, but that requires
-                       # remote access at the moment (repo_path_split)...
-                       s#^([ACRMDT])\t#   $1 #;
-                       push @{$c->{changed}}, $_;
-               } elsif (/^diff /) {
-                       $d = 1;
-                       push @{$c->{diff}}, $_;
-               } elsif ($d) {
-                       push @{$c->{diff}}, $_;
-               } elsif (/^    (git-svn-id:.+)$/) {
-                       ($c->{url}, $c->{r}, undef) = extract_metadata($1);
-               } elsif (s/^    //) {
-                       push @{$c->{l}}, $_;
+sub cmd_multi_init {
+       my $url = shift;
+       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#/+$##;
+               init_subdir(@_);
+       }
+       do_git_init_db();
+       if (defined $_trunk) {
+               my $trunk_ref = $_prefix . 'trunk';
+               # try both old-style and new-style lookups:
+               my $gs_trunk = eval { Git::SVN->new($trunk_ref) };
+               unless ($gs_trunk) {
+                       my ($trunk_url, $trunk_path) =
+                                             complete_svn_url($url, $_trunk);
+                       $gs_trunk = Git::SVN->init($trunk_url, $trunk_path,
+                                                  undef, $trunk_ref);
                }
        }
-       if ($c && defined $c->{r} && $c->{r} != $r_last) {
-               $r_last = $c->{r};
-               process_commit($c, $r_min, $r_max, \@k);
-       }
-       if (@k) {
-               my $swap = $r_max;
-               $r_max = $r_min;
-               $r_min = $swap;
-               process_commit($_, $r_min, $r_max) foreach reverse @k;
-       }
-out:
-       close $log;
-       print '-' x72,"\n" unless $_incremental || $_oneline;
+       return unless defined $_branches || defined $_tags;
+       my $ra = $url ? Git::SVN::Ra->new($url) : undef;
+       complete_url_ls_init($ra, $_branches, '--branches/-b', $_prefix);
+       complete_url_ls_init($ra, $_tags, '--tags/-t', $_prefix . 'tags/');
 }
 
-sub commit_diff_usage {
-       print STDERR "Usage: $0 commit-diff <tree-ish> <tree-ish> [<URL>]\n";
-       exit 1
+sub cmd_multi_fetch {
+       my $remotes = Git::SVN::read_all_remotes();
+       foreach my $repo_id (sort keys %$remotes) {
+               if ($remotes->{$repo_id}->{url}) {
+                       Git::SVN::fetch_all($repo_id, $remotes);
+               }
+       }
 }
 
-sub commit_diff {
-       if (!$_use_lib) {
-               print STDERR "commit-diff must be used with SVN libraries\n";
-               exit 1;
-       }
-       my $ta = shift or commit_diff_usage();
-       my $tb = shift or commit_diff_usage();
-       if (!eval { $SVN_URL = shift || file_to_s("$GIT_SVN_DIR/info/url") }) {
-               print STDERR "Needed URL or usable git-svn id command-line\n";
-               commit_diff_usage();
-       }
-       my $r = shift;
-       unless (defined $r) {
-               if (defined $_revision) {
-                       $r = $_revision
-               } else {
-                       die "-r|--revision is a required argument\n";
+# this command is special because it requires no metadata
+sub cmd_commit_diff {
+       my ($ta, $tb, $url) = @_;
+       my $usage = "Usage: $0 commit-diff -r<revision> ".
+                   "<tree-ish> <tree-ish> [<URL>]\n";
+       fatal($usage) if (!defined $ta || !defined $tb);
+       my $svn_path;
+       if (!defined $url) {
+               my $gs = eval { Git::SVN->new };
+               if (!$gs) {
+                       fatal("Needed URL or usable git-svn --id in ",
+                             "the command-line\n", $usage);
                }
+               $url = $gs->{url};
+               $svn_path = $gs->{path};
+       }
+       unless (defined $_revision) {
+               fatal("-r|--revision is a required argument\n", $usage);
        }
        if (defined $_message && defined $_file) {
-               print STDERR "Both --message/-m and --file/-F specified ",
-                               "for the commit message.\n",
-                               "I have no idea what you mean\n";
-               exit 1;
+               fatal("Both --message/-m and --file/-F specified ",
+                     "for the commit message.\n",
+                     "I have no idea what you mean\n");
        }
        if (defined $_file) {
                $_message = file_to_s($_file);
        } else {
-               $_message ||= get_commit_message($tb,
-                                       "$GIT_DIR/.svn-commit.tmp.$$")->{msg};
+               $_message ||= get_commit_entry($tb)->{log};
        }
-       $SVN ||= libsvn_connect($SVN_URL);
+       my $ra ||= Git::SVN::Ra->new($url);
+       $svn_path ||= $ra->{svn_path};
+       my $r = $_revision;
        if ($r eq 'HEAD') {
-               $r = $SVN->get_latest_revnum;
+               $r = $ra->get_latest_revnum;
        } elsif ($r !~ /^\d+$/) {
                die "revision argument: $r not understood by git-svn\n";
        }
-       my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
-       my $rev_committed;
-       my $ed = SVN::Git::Editor->new({        r => $r,
-                                               ra => libsvn_dup_ra($SVN),
-                                               c => $tb,
-                                               svn_path => $SVN->{svn_path}
-                                       },
-                               $SVN->get_commit_editor($_message,
-                                       sub {
-                                               $rev_committed = $_[0];
-                                               print "Committed $_[0]\n";
-                                       }, @lock)
-                               );
-       eval {
-               my $mods = libsvn_checkout_tree($ta, $tb, $ed);
-               if (@$mods == 0) {
-                       print "No changes\n$ta == $tb\n";
-                       $ed->abort_edit;
-               } else {
-                       $ed->close_edit;
-               }
-       };
-       fatal "$@\n" if $@;
-       $_message = $_file = undef;
-       return $rev_committed;
+       my %ed_opts = ( r => $r,
+                       log => $_message,
+                       ra => $ra,
+                       tree_a => $ta,
+                       tree_b => $tb,
+                       editor_cb => sub { print "Committed r$_[0]\n" },
+                       svn_path => $svn_path );
+       if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
+               print "No changes\n$ta == $tb\n";
+       }
 }
 
 ########################### utility functions #########################
 
-sub cmt_showable {
-       my ($c) = @_;
-       return 1 if defined $c->{r};
-       if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
-                               $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
-               my @msg = safe_qx(qw/git-cat-file commit/, $c->{c});
-               shift @msg while ($msg[0] ne "\n");
-               shift @msg;
-               @{$c->{l}} = grep !/^git-svn-id: /, @msg;
-
-               (undef, $c->{r}, undef) = extract_metadata(
-                               (grep(/^git-svn-id: /, @msg))[-1]);
+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;
+       return if verify_ref('refs/heads/master^0');
+
+       my $valid_head = verify_ref('HEAD^0');
+       command_noisy(qw(update-ref refs/heads/master), $gs->refname);
+       return if ($valid_head || !verify_ref('HEAD^0'));
+
+       return if $ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;
+       my $index = $ENV{GIT_INDEX_FILE} || "$ENV{GIT_DIR}/index";
+       return if -f $index;
+
+       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  ",
+                    $gs->full_url, " r", $gs->last_rev, "\n";
+}
+
+sub complete_svn_url {
+       my ($url, $path) = @_;
+       $path =~ s#/+$##;
+       if ($path !~ m#^[a-z\+]+://#) {
+               if (!defined $url || $url !~ m#^[a-z\+]+://#) {
+                       fatal("E: '$path' is not a complete URL ",
+                             "and a separate URL is not specified\n");
+               }
+               return ($url, $path);
        }
-       return defined $c->{r};
+       return ($path, '');
 }
 
-sub git_svn_log_cmd {
-       my ($r_min, $r_max) = @_;
-       my @cmd = (qw/git-log --abbrev-commit --pretty=raw
-                       --default/, "refs/remotes/$GIT_SVN");
-       push @cmd, '-r' unless $_non_recursive;
-       push @cmd, qw/--raw --name-status/ if $_verbose;
-       return @cmd unless defined $r_max;
-       if ($r_max == $r_min) {
-               push @cmd, '--max-count=1';
-               if (my $c = revdb_get($REVDB, $r_max)) {
-                       push @cmd, $c;
-               }
+sub complete_url_ls_init {
+       my ($ra, $repo_path, $switch, $pfx) = @_;
+       unless ($repo_path) {
+               print STDERR "W: $switch not specified\n";
+               return;
+       }
+       $repo_path =~ s#/+$##;
+       if ($repo_path =~ m#^[a-z\+]+://#) {
+               $ra = Git::SVN::Ra->new($repo_path);
+               $repo_path = '';
        } else {
-               my ($c_min, $c_max);
-               $c_max = revdb_get($REVDB, $r_max);
-               $c_min = revdb_get($REVDB, $r_min);
-               if (defined $c_min && defined $c_max) {
-                       if ($r_max > $r_max) {
-                               push @cmd, "$c_min..$c_max";
-                       } else {
-                               push @cmd, "$c_max..$c_min";
-                       }
-               } elsif ($r_max > $r_min) {
-                       push @cmd, $c_max;
-               } else {
-                       push @cmd, $c_min;
+               $repo_path =~ s#^/+##;
+               unless ($ra) {
+                       fatal("E: '$repo_path' is not a complete URL ",
+                             "and a separate URL is not specified\n");
                }
        }
-       return @cmd;
+       my $url = $ra->{url};
+       my $gs = Git::SVN->init($url, undef, undef, undef, 1);
+       my $k = "svn-remote.$gs->{repo_id}.url";
+       my $orig_url = eval { command_oneline(qw/config --get/, $k) };
+       if ($orig_url && ($orig_url ne $gs->{url})) {
+               die "$k already set: $orig_url\n",
+                   "wanted to set to: $gs->{url}\n";
+       }
+       command_oneline('config', $k, $gs->{url}) unless $orig_url;
+       my $remote_path = "$ra->{svn_path}/$repo_path/*";
+       $remote_path =~ s#/+#/#g;
+       $remote_path =~ s#^/##g;
+       my ($n) = ($switch =~ /^--(\w+)/);
+       if (length $pfx && $pfx !~ m#/$#) {
+               die "--prefix='$pfx' must have a trailing slash '/'\n";
+       }
+       command_noisy('config', "svn-remote.$gs->{repo_id}.$n",
+                               "$remote_path:refs/remotes/$pfx*");
 }
 
-sub fetch_child_id {
-       my $id = shift;
-       print "Fetching $id\n";
-       my $ref = "$GIT_DIR/refs/remotes/$id";
-       defined(my $pid = open my $fh, '-|') or croak $!;
-       if (!$pid) {
-               $_repack = undef;
-               $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
-               init_vars();
-               fetch(@_);
-               exit 0;
+sub verify_ref {
+       my ($ref) = @_;
+       eval { command_oneline([ 'rev-parse', '--verify', $ref ],
+                              { STDERR => 0 }); };
+}
+
+sub get_tree_from_treeish {
+       my ($treeish) = @_;
+       # $treeish can be a symbolic ref, too:
+       my $type = command_oneline(qw/cat-file -t/, $treeish);
+       my $expected;
+       while ($type eq 'tag') {
+               ($treeish, $type) = command(qw/cat-file tag/, $treeish);
        }
-       while (<$fh>) {
-               print $_;
-               check_repack() if (/^r\d+ = $sha1/);
+       if ($type eq 'commit') {
+               $expected = (grep /^tree /, command(qw/cat-file commit/,
+                                                   $treeish))[0];
+               ($expected) = ($expected =~ /^tree ($sha1)$/o);
+               die "Unable to get tree from $treeish\n" unless $expected;
+       } elsif ($type eq 'tree') {
+               $expected = $treeish;
+       } else {
+               die "$treeish is a $type, expected tree, tag or commit\n";
        }
-       close $fh or croak $?;
+       return $expected;
 }
 
-sub rec_fetch {
-       my ($pfx, $p, @args) = @_;
-       my @dir;
-       foreach (sort <$p/*>) {
-               if (-r "$_/info/url") {
-                       $pfx .= '/' if $pfx && $pfx !~ m!/$!;
-                       my $id = $pfx . basename $_;
-                       next if $id eq 'trunk';
-                       fetch_child_id($id, @args);
-               } elsif (-d $_) {
-                       push @dir, $_;
-               }
-       }
-       foreach (@dir) {
-               my $x = $_;
-               $x =~ s!^\Q$GIT_DIR\E/svn/!!;
-               rec_fetch($x, $_);
-       }
-}
+sub get_commit_entry {
+       my ($treeish) = shift;
+       my %log_entry = ( log => '', tree => get_tree_from_treeish($treeish) );
+       my $commit_editmsg = "$ENV{GIT_DIR}/COMMIT_EDITMSG";
+       my $commit_msg = "$ENV{GIT_DIR}/COMMIT_MSG";
+       open my $log_fh, '>', $commit_editmsg or croak $!;
 
-sub complete_url_ls_init {
-       my ($url, $var, $switch, $pfx) = @_;
-       unless ($var) {
-               print STDERR "W: $switch not specified\n";
-               return;
-       }
-       $var =~ s#/+$##;
-       if ($var !~ m#^[a-z\+]+://#) {
-               $var = '/' . $var if ($var !~ m#^/#);
-               unless ($url) {
-                       print STDERR "E: '$var' is not a complete URL ",
-                               "and a separate URL is not specified\n";
-                       exit 1;
-               }
-               $var = $url . $var;
-       }
-       chomp(my @ls = $_use_lib ? libsvn_ls_fullurl($var)
-                               : safe_qx(qw/svn ls --non-interactive/, $var));
-       my $old = $GIT_SVN;
-       defined(my $pid = fork) or croak $!;
-       if (!$pid) {
-               foreach my $u (map { "$var/$_" } (grep m!/$!, @ls)) {
-                       $u =~ s#/+$##;
-                       if ($u !~ m!\Q$var\E/(.+)$!) {
-                               print STDERR "W: Unrecognized URL: $u\n";
-                               die "This should never happen\n";
-                       }
-                       # don't try to init already existing refs
-                       my $id = $pfx.$1;
-                       $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
-                       init_vars();
-                       unless (-d $GIT_SVN_DIR) {
-                               print "init $u => $id\n";
-                               init($u);
+       my $type = command_oneline(qw/cat-file -t/, $treeish);
+       if ($type eq 'commit' || $type eq 'tag') {
+               my ($msg_fh, $ctx) = command_output_pipe('cat-file',
+                                                        $type, $treeish);
+               my $in_msg = 0;
+               while (<$msg_fh>) {
+                       if (!$in_msg) {
+                               $in_msg = 1 if (/^\s*$/);
+                       } elsif (/^git-svn-id: /) {
+                               # skip this for now, we regenerate the
+                               # correct one on re-fetch anyways
+                               # TODO: set *:merge properties or like...
+                       } else {
+                               print $log_fh $_ or croak $!;
                        }
                }
-               exit 0;
+               command_close_pipe($msg_fh, $ctx);
        }
-       waitpid $pid, 0;
-       croak $? if $?;
-       my ($n) = ($switch =~ /^--(\w+)/);
-       sys('git-repo-config', "svn.$n", $var);
-}
+       close $log_fh or croak $!;
 
-sub common_prefix {
-       my $paths = shift;
-       my %common;
-       foreach (@$paths) {
-               my @tmp = split m#/#, $_;
-               my $p = '';
-               while (my $x = shift @tmp) {
-                       $p .= "/$x";
-                       $common{$p} ||= 0;
-                       $common{$p}++;
-               }
-       }
-       foreach (sort {length $b <=> length $a} keys %common) {
-               if ($common{$_} == @$paths) {
-                       return $_;
-               }
+       if ($_edit || ($type eq 'tree')) {
+               my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
+               # TODO: strip out spaces, comments, like git-commit.sh
+               system($editor, $commit_editmsg);
        }
-       return '';
+       rename $commit_editmsg, $commit_msg or croak $!;
+       open $log_fh, '<', $commit_msg or croak $!;
+       { local $/; chomp($log_entry{log} = <$log_fh>); }
+       close $log_fh or croak $!;
+       unlink $commit_msg;
+       \%log_entry;
 }
 
-# grafts set here are 'stronger' in that they're based on actual tree
-# matches, and won't be deleted from merge-base checking in write_grafts()
-sub graft_tree_joins {
-       my $grafts = shift;
-       map_tree_joins() if (@_branch_from && !%tree_map);
-       return unless %tree_map;
-
-       git_svn_each(sub {
-               my $i = shift;
-               defined(my $pid = open my $fh, '-|') or croak $!;
-               if (!$pid) {
-                       exec qw/git-rev-list --pretty=raw/,
-                                       "refs/remotes/$i" or croak $!;
-               }
-               while (<$fh>) {
-                       next unless /^commit ($sha1)$/o;
-                       my $c = $1;
-                       my ($t) = (<$fh> =~ /^tree ($sha1)$/o);
-                       next unless $tree_map{$t};
-
-                       my $l;
-                       do {
-                               $l = readline $fh;
-                       } until ($l =~ /^committer (?:.+) (\d+) ([\-\+]?\d+)$/);
-
-                       my ($s, $tz) = ($1, $2);
-                       if ($tz =~ s/^\+//) {
-                               $s += tz_to_s_offset($tz);
-                       } elsif ($tz =~ s/^\-//) {
-                               $s -= tz_to_s_offset($tz);
-                       }
-
-                       my ($url_a, $r_a, $uuid_a) = cmt_metadata($c);
+sub s_to_file {
+       my ($str, $file, $mode) = @_;
+       open my $fd,'>',$file or croak $!;
+       print $fd $str,"\n" or croak $!;
+       close $fd or croak $!;
+       chmod ($mode &~ umask, $file) if (defined $mode);
+}
 
-                       foreach my $p (@{$tree_map{$t}}) {
-                               next if $p eq $c;
-                               my $mb = eval {
-                                       safe_qx('git-merge-base', $c, $p)
-                               };
-                               next unless ($@ || $?);
-                               if (defined $r_a) {
-                                       # see if SVN says it's a relative
-                                       my ($url_b, $r_b, $uuid_b) =
-                                                       cmt_metadata($p);
-                                       next if (defined $url_b &&
-                                                       defined $url_a &&
-                                                       ($url_a eq $url_b) &&
-                                                       ($uuid_a eq $uuid_b));
-                                       if ($uuid_a eq $uuid_b) {
-                                               if ($r_b < $r_a) {
-                                                       $grafts->{$c}->{$p} = 2;
-                                                       next;
-                                               } elsif ($r_b > $r_a) {
-                                                       $grafts->{$p}->{$c} = 2;
-                                                       next;
-                                               }
-                                       }
-                               }
-                               my $ct = get_commit_time($p);
-                               if ($ct < $s) {
-                                       $grafts->{$c}->{$p} = 2;
-                               } elsif ($ct > $s) {
-                                       $grafts->{$p}->{$c} = 2;
-                               }
-                               # what should we do when $ct == $s ?
-                       }
-               }
-               close $fh or croak $?;
-       });
+sub file_to_s {
+       my $file = shift;
+       open my $fd,'<',$file or croak "$!: file: $file\n";
+       local $/;
+       my $ret = <$fd>;
+       close $fd or croak $!;
+       $ret =~ s/\s*$//s;
+       return $ret;
 }
 
-# this isn't funky-filename safe, but good enough for now...
-sub graft_file_copy_cmd {
-       my ($grafts, $l_map, $u) = @_;
-       my $paths = $l_map->{$u};
-       my $pfx = common_prefix([keys %$paths]);
-       $SVN_URL ||= $u.$pfx;
-       my $pid = open my $fh, '-|';
-       defined $pid or croak $!;
-       unless ($pid) {
-               my @exec = qw/svn log -v/;
-               push @exec, "-r$_revision" if defined $_revision;
-               exec @exec, $u.$pfx or croak $!;
-       }
-       my ($r, $mp) = (undef, undef);
-       while (<$fh>) {
+# '<svn username> = real-name <email address>' mapping based on git-svnimport:
+sub load_authors {
+       open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
+       my $log = $cmd eq 'log';
+       while (<$authors>) {
                chomp;
-               if (/^\-{72}$/) {
-                       $mp = $r = undef;
-               } elsif (/^r(\d+) \| /) {
-                       $r = $1 unless defined $r;
-               } elsif (/^Changed paths:/) {
-                       $mp = 1;
-               } elsif ($mp && m#^   [AR] /(\S.*?) \(from /(\S+?):(\d+)\)$#) {
-                       my ($p1, $p0, $r0) = ($1, $2, $3);
-                       my $c = find_graft_path_commit($paths, $p1, $r);
-                       next unless $c;
-                       find_graft_path_parents($grafts, $paths, $c, $p0, $r0);
-               }
-       }
-}
-
-sub graft_file_copy_lib {
-       my ($grafts, $l_map, $u) = @_;
-       my $tree_paths = $l_map->{$u};
-       my $pfx = common_prefix([keys %$tree_paths]);
-       my ($repo, $path) = repo_path_split($u.$pfx);
-       $SVN = libsvn_connect($repo);
-
-       my ($base, $head) = libsvn_parse_revision();
-       my $inc = 1000;
-       my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
-       my $eh = $SVN::Error::handler;
-       $SVN::Error::handler = \&libsvn_skip_unknown_revs;
-       while (1) {
-               my $pool = SVN::Pool->new;
-               libsvn_get_log(libsvn_dup_ra($SVN), [$path],
-                              $min, $max, 0, 1, 1,
-                       sub {
-                               libsvn_graft_file_copies($grafts, $tree_paths,
-                                                       $path, @_);
-                       }, $pool);
-               $pool->clear;
-               last if ($max >= $head);
-               $min = $max + 1;
-               $max += $inc;
-               $max = $head if ($max > $head);
+               next unless /^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
+               my ($user, $name, $email) = ($1, $2, $3);
+               if ($log) {
+                       $Git::SVN::Log::rusers{"$name <$email>"} = $user;
+               } else {
+                       $users{$user} = [$name, $email];
+               }
        }
-       $SVN::Error::handler = $eh;
+       close $authors or croak $!;
 }
 
-sub process_merge_msg_matches {
-       my ($grafts, $l_map, $u, $p, $c, @matches) = @_;
-       my (@strong, @weak);
-       foreach (@matches) {
-               # merging with ourselves is not interesting
-               next if $_ eq $p;
-               if ($l_map->{$u}->{$_}) {
-                       push @strong, $_;
+# convert GetOpt::Long specs for use by git-config
+sub read_repo_config {
+       return unless -d $ENV{GIT_DIR};
+       my $opts = shift;
+       my @config_only;
+       foreach my $o (keys %$opts) {
+               # if we have mixedCase and a long option-only, then
+               # it's a config-only variable that we don't need for
+               # the command-line.
+               push @config_only, $o if ($o =~ /[A-Z]/ && $o =~ /^[a-z]+$/i);
+               my $v = $opts->{$o};
+               my ($key) = ($o =~ /^([a-zA-Z\-]+)/);
+               $key =~ s/-//g;
+               my $arg = 'git-config';
+               $arg .= ' --int' if ($o =~ /[:=]i$/);
+               $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
+               if (ref $v eq 'ARRAY') {
+                       chomp(my @tmp = `$arg --get-all svn.$key`);
+                       @$v = @tmp if @tmp;
                } else {
-                       push @weak, $_;
-               }
-       }
-       foreach my $w (@weak) {
-               last if @strong;
-               # no exact match, use branch name as regexp.
-               my $re = qr/\Q$w\E/i;
-               foreach (keys %{$l_map->{$u}}) {
-                       if (/$re/) {
-                               push @strong, $l_map->{$u}->{$_};
-                               last;
-                       }
-               }
-               last if @strong;
-               $w = basename($w);
-               $re = qr/\Q$w\E/i;
-               foreach (keys %{$l_map->{$u}}) {
-                       if (/$re/) {
-                               push @strong, $l_map->{$u}->{$_};
-                               last;
+                       chomp(my $tmp = `$arg --get svn.$key`);
+                       if ($tmp && !($arg =~ / --bool/ && $tmp eq 'false')) {
+                               $$v = $tmp;
                        }
                }
        }
-       my ($rev) = ($c->{m} =~ /^git-svn-id:\s(?:\S+?)\@(\d+)
-                                       \s(?:[a-f\d\-]+)$/xsm);
-       unless (defined $rev) {
-               ($rev) = ($c->{m} =~/^git-svn-id:\s(\d+)
-                                       \@(?:[a-f\d\-]+)/xsm);
-               return unless defined $rev;
-       }
-       foreach my $m (@strong) {
-               my ($r0, $s0) = find_rev_before($rev, $m, 1);
-               $grafts->{$c->{c}}->{$s0} = 1 if defined $s0;
+       delete @$opts{@config_only} if @config_only;
+}
+
+sub extract_metadata {
+       my $id = shift or return (undef, undef, undef);
+       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 =~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
        }
+       return ($url, $rev, $uuid);
 }
 
-sub graft_merge_msg {
-       my ($grafts, $l_map, $u, $p, @re) = @_;
+sub cmt_metadata {
+       return extract_metadata((grep(/^git-svn-id: /,
+               command(qw/cat-file commit/, shift)))[-1]);
+}
 
-       my $x = $l_map->{$u}->{$p};
-       my $rl = rev_list_raw($x);
-       while (my $c = next_rev_list_entry($rl)) {
-               foreach my $re (@re) {
-                       my (@br) = ($c->{m} =~ /$re/g);
-                       next unless @br;
-                       process_merge_msg_matches($grafts,$l_map,$u,$p,$c,@br);
+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_uuid {
-       return if $SVN_UUID;
-       if ($_use_lib) {
-               my $pool = SVN::Pool->new;
-               $SVN_UUID = $SVN->get_uuid($pool);
-               $pool->clear;
-       } else {
-               my $info = shift || svn_info('.');
-               $SVN_UUID = $info->{'Repository UUID'} or
-                                       croak "Repository UUID unreadable\n";
-       }
+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 quiet_run {
-       my $pid = fork;
-       defined $pid or croak $!;
-       if (!$pid) {
-               open my $null, '>', '/dev/null' or croak $!;
-               open STDERR, '>&', $null or croak $!;
-               open STDOUT, '>&', $null or croak $!;
-               exec @_ or croak $!;
+sub linearize_history {
+       my ($gs, $refs) = @_;
+       my %parents;
+       foreach my $c (@$refs) {
+               read_commit_parents(\%parents, $c);
        }
-       waitpid $pid, 0;
-       return $?;
-}
 
-sub repo_path_split {
-       my $full_url = shift;
-       $full_url =~ s#/+$##;
+       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;
 
-       foreach (@repo_path_split_cache) {
-               if ($full_url =~ s#$_##) {
-                       my $u = $1;
-                       $full_url =~ s#^/+##;
-                       return ($u, $full_url);
+               # 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;
                }
        }
-       if ($_use_lib) {
-               my $tmp = libsvn_connect($full_url);
-               return ($tmp->{repos_root}, $tmp->{svn_path});
-       } else {
-               my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i);
-               $path =~ s#^/+##;
-               my @paths = split(m#/+#, $path);
-               while (quiet_run(qw/svn ls --non-interactive/, $url)) {
-                       my $n = shift @paths || last;
-                       $url .= "/$n";
-               }
-               push @repo_path_split_cache, qr/^(\Q$url\E)/;
-               $path = join('/',@paths);
-               return ($url, $path);
-       }
+       (\@linear_refs, \%parents);
 }
 
-sub setup_git_svn {
-       defined $SVN_URL or croak "SVN repository location required\n";
-       unless (-d $GIT_DIR) {
-               croak "GIT_DIR=$GIT_DIR does not exist!\n";
-       }
-       mkpath([$GIT_SVN_DIR]);
-       mkpath(["$GIT_SVN_DIR/info"]);
-       open my $fh, '>>',$REVDB or croak $!;
-       close $fh;
-       s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url");
+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 $_minimize_url/;
+use Carp qw/croak/;
+use File::Path qw/mkpath/;
+use File::Copy qw/copy/;
+use IPC::Open3;
 
+my $_repack_nr;
+# properties that we do not log:
+my %SKIP_PROP;
+BEGIN {
+       %SKIP_PROP = map { $_ => 1 } qw/svn:wc:ra_dav:version-url
+                                       svn:special svn:executable
+                                       svn:entry:committed-rev
+                                       svn:entry:last-author
+                                       svn:entry:uuid
+                                       svn:entry:committed-date/;
+
+       # 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
+       no strict 'refs';
+       for my $option (qw/follow_parent no_metadata use_svm_props
+                          use_svnsync_props/) {
+               my $key = $option;
+               $key =~ tr/_//d;
+               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->{$prop} = $v eq 'false' ? 0 : 1;
+                       }
+                       return $self->{$prop};
+               }
+       }
 }
 
-sub assert_svn_wc_clean {
-       return if $_use_lib;
-       my ($svn_rev) = @_;
-       croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
-       my $lcr = svn_info('.')->{'Last Changed Rev'};
-       if ($svn_rev != $lcr) {
-               print STDERR "Checking for copy-tree ... ";
-               my @diff = grep(/^Index: /,(safe_qx(qw(svn diff),
-                                               "-r$lcr:$svn_rev")));
-               if (@diff) {
-                       croak "Nope!  Expected r$svn_rev, got r$lcr\n";
+my %LOCKFILES;
+END { unlink keys %LOCKFILES if %LOCKFILES }
+
+sub resolve_local_globs {
+       my ($url, $fetch, $glob_spec) = @_;
+       return unless defined $glob_spec;
+       my $ref = $glob_spec->{ref};
+       my $path = $glob_spec->{path};
+       foreach (command(qw#for-each-ref --format=%(refname) refs/remotes#)) {
+               next unless m#^refs/remotes/$ref->{regex}$#;
+               my $p = $1;
+               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",
+                                   "existing: refs/remotes/$existing\n",
+                                   " globbed: refs/remotes/$refname\n";
+                       }
+                       my $u = (::cmt_metadata("refs/remotes/$refname"))[0];
+                       $u =~ s!^\Q$url\E(/|$)!! or die
+                         "refs/remotes/$refname: '$url' not found in '$u'\n";
+                       if ($pathname ne $u) {
+                               warn "W: Refspec glob conflict ",
+                                    "(ref: refs/remotes/$refname):\n",
+                                    "expected path: $pathname\n",
+                                    "    real path: $u\n",
+                                    "Continuing ahead with $u\n";
+                               next;
+                       }
                } else {
-                       print STDERR "OK!\n";
+                       $fetch->{$pathname} = $refname;
                }
        }
-       my @status = grep(!/^Performing status on external/,(`svn status`));
-       @status = grep(!/^\s*$/,@status);
-       @status = grep(!/^X/,@status) if $_no_ignore_ext;
-       if (scalar @status) {
-               print STDERR "Tree ($SVN_WC) is not clean:\n";
-               print STDERR $_ foreach @status;
-               croak;
-       }
 }
 
-sub get_tree_from_treeish {
-       my ($treeish) = @_;
-       croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o;
-       chomp(my $type = `git-cat-file -t $treeish`);
-       my $expected;
-       while ($type eq 'tag') {
-               chomp(($treeish, $type) = `git-cat-file tag $treeish`);
-       }
-       if ($type eq 'commit') {
-               $expected = (grep /^tree /,`git-cat-file commit $treeish`)[0];
-               ($expected) = ($expected =~ /^tree ($sha1)$/);
-               die "Unable to get tree from $treeish\n" unless $expected;
-       } elsif ($type eq 'tree') {
-               $expected = $treeish;
-       } else {
-               die "$treeish is a $type, expected tree, tag or commit\n";
+sub parse_revision_argument {
+       my ($base, $head) = @_;
+       if (!defined $::_revision || $::_revision eq 'BASE:HEAD') {
+               return ($base, $head);
+       }
+       return ($1, $2) if ($::_revision =~ /^(\d+):(\d+)$/);
+       return ($::_revision, $::_revision) if ($::_revision =~ /^\d+$/);
+       return ($head, $head) if ($::_revision eq 'HEAD');
+       return ($base, $1) if ($::_revision =~ /^BASE:(\d+)$/);
+       return ($1, $head) if ($::_revision =~ /^(\d+):HEAD$/);
+       die "revision argument: $::_revision not understood by git-svn\n";
+}
+
+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};
+       my $url = $remote->{url} or die "svn-remote.$repo_id.url not defined\n";
+       my (@gs, @globs);
+       my $ra = Git::SVN::Ra->new($url);
+       my $uuid = $ra->get_uuid;
+       my $head = $ra->get_latest_revnum;
+       my $base = defined $fetch ? $head : 0;
+
+       # read the max revs for wildcard expansion (branches/*, tags/*)
+       foreach my $t (qw/branches tags/) {
+               defined $remote->{$t} or next;
+               push @globs, $remote->{$t};
+               my $max_rev = eval { tmp_config(qw/--int --get/,
+                                        "svn-remote.$repo_id.${t}-maxRev") };
+               if (defined $max_rev && ($max_rev < $base)) {
+                       $base = $max_rev;
+               } elsif (!defined $max_rev) {
+                       $base = 0;
+               }
        }
-       return $expected;
-}
-
-sub assert_tree {
-       return if $_use_lib;
-       my ($treeish) = @_;
-       my $expected = get_tree_from_treeish($treeish);
 
-       my $tmpindex = $GIT_SVN_INDEX.'.assert-tmp';
-       if (-e $tmpindex) {
-               unlink $tmpindex or croak $!;
-       }
-       my $old_index = set_index($tmpindex);
-       index_changes(1);
-       chomp(my $tree = `git-write-tree`);
-       restore_index($old_index);
-       if ($tree ne $expected) {
-               croak "Tree mismatch, Got: $tree, Expected: $expected\n";
+       if ($fetch) {
+               foreach my $p (sort keys %$fetch) {
+                       my $gs = Git::SVN->new($fetch->{$p}, $repo_id, $p);
+                       my $lr = $gs->rev_db_max;
+                       if (defined $lr) {
+                               $base = $lr if ($lr < $base);
+                       }
+                       push @gs, $gs;
+               }
        }
-       unlink $tmpindex;
-}
 
-sub parse_diff_tree {
-       my $diff_fh = shift;
-       local $/ = "\0";
-       my $state = 'meta';
-       my @mods;
-       while (<$diff_fh>) {
-               chomp $_; # this gets rid of the trailing "\0"
-               if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
-                                       $sha1\s($sha1)\s([MTCRAD])\d*$/xo) {
-                       push @mods, {   mode_a => $1, mode_b => $2,
-                                       sha1_b => $3, chg => $4 };
-                       if ($4 =~ /^(?:C|R)$/) {
-                               $state = 'file_a';
-                       } else {
-                               $state = 'file_b';
-                       }
-               } elsif ($state eq 'file_a') {
-                       my $x = $mods[$#mods] or croak "Empty array\n";
-                       if ($x->{chg} !~ /^(?:C|R)$/) {
-                               croak "Error parsing $_, $x->{chg}\n";
-                       }
-                       $x->{file_a} = $_;
-                       $state = 'file_b';
-               } elsif ($state eq 'file_b') {
-                       my $x = $mods[$#mods] or croak "Empty array\n";
-                       if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
-                               croak "Error parsing $_, $x->{chg}\n";
-                       }
-                       if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
-                               croak "Error parsing $_, $x->{chg}\n";
+       ($base, $head) = parse_revision_argument($base, $head);
+       $ra->gs_fetch_loop_common($base, $head, \@gs, \@globs);
+}
+
+sub read_all_remotes {
+       my $r = {};
+       foreach (grep { s/^svn-remote\.// } command(qw/config -l/)) {
+               if (m!^(.+)\.fetch=\s*(.*)\s*:\s*refs/remotes/(.+)\s*$!) {
+                       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)=
+                          (.*):refs/remotes/(.+)\s*$/!x) {
+                       my ($p, $g) = ($3, $4);
+                       my $rs = $r->{$1}->{$2} = {
+                                         t => $2,
+                                         remote => $1,
+                                         path => Git::SVN::GlobSpec->new($p),
+                                         ref => Git::SVN::GlobSpec->new($g) };
+                       if (length($rs->{ref}->{right}) != 0) {
+                               die "The '*' glob character must be the last ",
+                                   "character of '$g'\n";
                        }
-                       $x->{file_b} = $_;
-                       $state = 'meta';
-               } else {
-                       croak "Error parsing $_\n";
                }
        }
-       close $diff_fh or croak $?;
+       $r;
+}
 
-       return \@mods;
+sub init_vars {
+       if (defined $_repack) {
+               $_repack = 1000 if ($_repack <= 0);
+               $_repack_nr = $_repack;
+               $_repack_flags ||= '-d';
+       }
 }
 
-sub svn_check_prop_executable {
-       my $m = shift;
-       return if -l $m->{file_b};
-       if ($m->{mode_b} =~ /755$/) {
-               chmod((0755 &~ umask),$m->{file_b}) or croak $!;
-               if ($m->{mode_a} !~ /755$/) {
-                       sys(qw(svn propset svn:executable 1), $m->{file_b});
+sub verify_remotes_sanity {
+       return unless -d $ENV{GIT_DIR};
+       my %seen;
+       foreach (command(qw/config -l/)) {
+               if (m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {
+                       if ($seen{$1}) {
+                               die "Remote ref refs/remote/$1 is tracked by",
+                                   "\n  \"$_\"\nand\n  \"$seen{$1}\"\n",
+                                   "Please resolve this ambiguity in ",
+                                   "your git configuration file before ",
+                                   "continuing\n";
+                       }
+                       $seen{$1} = $_;
                }
-               -x $m->{file_b} or croak "$m->{file_b} is not executable!\n";
-       } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
-               sys(qw(svn propdel svn:executable), $m->{file_b});
-               chmod((0644 &~ umask),$m->{file_b}) or croak $!;
-               -x $m->{file_b} and croak "$m->{file_b} is executable!\n";
        }
 }
 
-sub svn_ensure_parent_path {
-       my $dir_b = dirname(shift);
-       svn_ensure_parent_path($dir_b) if ($dir_b ne File::Spec->curdir);
-       mkpath([$dir_b]) unless (-d $dir_b);
-       sys(qw(svn add -N), $dir_b) unless (-d "$dir_b/.svn");
+# we allow more chars than remotes2config.sh...
+sub sanitize_remote_name {
+       my ($name) = @_;
+       $name =~ tr{A-Za-z0-9:,/+-}{.}c;
+       $name;
 }
 
-sub precommit_check {
-       my $mods = shift;
-       my (%rm_file, %rmdir_check, %added_check);
+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;
+               next if $u ne $url;
+               $existing = $repo_id;
+               last;
+       }
+       $existing;
+}
 
-       my %o = ( D => 0, R => 1, C => 2, A => 3, M => 3, T => 3 );
-       foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
-               if ($m->{chg} eq 'R') {
-                       if (-d $m->{file_b}) {
-                               err_dir_to_file("$m->{file_a} => $m->{file_b}");
-                       }
-                       # dir/$file => dir/file/$file
-                       my $dirname = dirname($m->{file_b});
-                       while ($dirname ne File::Spec->curdir) {
-                               if ($dirname ne $m->{file_a}) {
-                                       $dirname = dirname($dirname);
-                                       next;
-                               }
-                               err_file_to_dir("$m->{file_a} => $m->{file_b}");
-                       }
-                       # baz/zzz => baz (baz is a file)
-                       $dirname = dirname($m->{file_a});
-                       while ($dirname ne File::Spec->curdir) {
-                               if ($dirname ne $m->{file_b}) {
-                                       $dirname = dirname($dirname);
-                                       next;
-                               }
-                               err_dir_to_file("$m->{file_a} => $m->{file_b}");
+sub init_remote_config {
+       my ($self, $url, $no_write) = @_;
+       $url =~ s!/+$!!; # strip trailing slash
+       my $r = read_all_remotes();
+       my $existing = find_existing_remote($url, $r);
+       if ($existing) {
+               unless ($no_write) {
+                       print STDERR "Using existing ",
+                                    "[svn-remote \"$existing\"]\n";
+               }
+               $self->{repo_id} = $existing;
+       } elsif ($_minimize_url) {
+               my $min_url = Git::SVN::Ra->new($url)->minimize_url;
+               $existing = find_existing_remote($min_url, $r);
+               if ($existing) {
+                       unless ($no_write) {
+                               print STDERR "Using existing ",
+                                            "[svn-remote \"$existing\"]\n";
                        }
+                       $self->{repo_id} = $existing;
                }
-               if ($m->{chg} =~ /^(D|R)$/) {
-                       my $t = $1 eq 'D' ? 'file_b' : 'file_a';
-                       $rm_file{ $m->{$t} } = 1;
-                       my $dirname = dirname( $m->{$t} );
-                       my $basename = basename( $m->{$t} );
-                       $rmdir_check{$dirname}->{$basename} = 1;
-               } elsif ($m->{chg} =~ /^(?:A|C)$/) {
-                       if (-d $m->{file_b}) {
-                               err_dir_to_file($m->{file_b});
+               if ($min_url ne $url) {
+                       unless ($no_write) {
+                               print STDERR "Using higher level of URL: ",
+                                            "$url => $min_url\n";
                        }
-                       my $dirname = dirname( $m->{file_b} );
-                       my $basename = basename( $m->{file_b} );
-                       $added_check{$dirname}->{$basename} = 1;
-                       while ($dirname ne File::Spec->curdir) {
-                               if ($rm_file{$dirname}) {
-                                       err_file_to_dir($m->{file_b});
-                               }
-                               $dirname = dirname $dirname;
+                       my $old_path = $self->{path};
+                       $self->{path} = $url;
+                       $self->{path} =~ s!^\Q$min_url\E(/|$)!!;
+                       if (length $old_path) {
+                               $self->{path} .= "/$old_path";
                        }
+                       $url = $min_url;
                }
        }
-       return (\%rmdir_check, \%added_check);
-
-       sub err_dir_to_file {
-               my $file = shift;
-               print STDERR "Node change from directory to file ",
-                               "is not supported by Subversion: ",$file,"\n";
-               exit 1;
+       my $orig_url;
+       if (!$existing) {
+               # verify that we aren't overwriting anything:
+               $orig_url = eval {
+                       command_oneline('config', '--get',
+                                       "svn-remote.$self->{repo_id}.url")
+               };
+               if ($orig_url && ($orig_url ne $url)) {
+                       die "svn-remote.$self->{repo_id}.url already set: ",
+                           "$orig_url\nwanted to set to: $url\n";
+               }
        }
-       sub err_file_to_dir {
-               my $file = shift;
-               print STDERR "Node change from file to directory ",
-                               "is not supported by Subversion: ",$file,"\n";
-               exit 1;
+       my ($xrepo_id, $xpath) = find_ref($self->refname);
+       if (defined $xpath) {
+               die "svn-remote.$xrepo_id.fetch already set to track ",
+                   "$xpath:refs/remotes/", $self->refname, "\n";
+       }
+       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);
+       }
+       $self->{url} = $url;
+}
+
+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;
+               $path =~ s#^\Q$repos_root\E(?:/|$)##;
+       }
+       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} || {};
+               foreach (qw/branches tags/) {
+                       resolve_local_globs($u, $fetch,
+                                           $remotes->{$repo_id}->{$_});
+               }
+               my $p = $path;
+               unless (defined $p) {
+                       $p = $full_url;
+                       $p =~ s#^\Q$u\E(?:/|$)## or next;
+               }
+               foreach my $f (keys %$fetch) {
+                       next if $f ne $p;
+                       return Git::SVN->new($fetch->{$f}, $repo_id, $f);
+               }
        }
+       undef;
 }
 
-
-sub get_diff {
-       my ($from, $treeish) = @_;
-       assert_tree($from);
-       print "diff-tree $from $treeish\n";
-       my $pid = open my $diff_fh, '-|';
-       defined $pid or croak $!;
-       if ($pid == 0) {
-               my @diff_tree = qw(git-diff-tree -z -r);
-               if ($_cp_similarity) {
-                       push @diff_tree, "-C$_cp_similarity";
-               } else {
-                       push @diff_tree, '-C';
+sub init {
+       my ($class, $url, $path, $repo_id, $ref_id, $no_write) = @_;
+       my $self = _new($class, $repo_id, $ref_id, $path);
+       if (defined $url) {
+               $self->init_remote_config($url, $no_write);
+       }
+       $self;
+}
+
+sub find_ref {
+       my ($ref_id) = @_;
+       foreach (command(qw/config -l/)) {
+               next unless m!^svn-remote\.(.+)\.fetch=
+                             \s*(.*)\s*:\s*refs/remotes/(.+)\s*$!x;
+               my ($repo_id, $path, $ref) = ($1, $2, $3);
+               if ($ref eq $ref_id) {
+                       $path = '' if ($path =~ m#^\./?#);
+                       return ($repo_id, $path);
                }
-               push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
-               push @diff_tree, "-l$_l" if defined $_l;
-               exec(@diff_tree, $from, $treeish) or croak $!;
        }
-       return parse_diff_tree($diff_fh);
+       (undef, undef, undef);
 }
 
-sub svn_checkout_tree {
-       my ($from, $treeish) = @_;
-       my $mods = get_diff($from->{commit}, $treeish);
-       return $mods unless (scalar @$mods);
-       my ($rm, $add) = precommit_check($mods);
-
-       my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
-       foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
-               if ($m->{chg} eq 'C') {
-                       svn_ensure_parent_path( $m->{file_b} );
-                       sys(qw(svn cp),         $m->{file_a}, $m->{file_b});
-                       apply_mod_line_blob($m);
-                       svn_check_prop_executable($m);
-               } elsif ($m->{chg} eq 'D') {
-                       sys(qw(svn rm --force), $m->{file_b});
-               } elsif ($m->{chg} eq 'R') {
-                       svn_ensure_parent_path( $m->{file_b} );
-                       sys(qw(svn mv --force), $m->{file_a}, $m->{file_b});
-                       apply_mod_line_blob($m);
-                       svn_check_prop_executable($m);
-               } elsif ($m->{chg} eq 'M') {
-                       apply_mod_line_blob($m);
-                       svn_check_prop_executable($m);
-               } elsif ($m->{chg} eq 'T') {
-                       svn_check_prop_executable($m);
-                       apply_mod_line_blob($m);
-                       if ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) {
-                               sys(qw(svn propdel svn:special), $m->{file_b});
-                       } else {
-                               sys(qw(svn propset svn:special *),$m->{file_b});
-                       }
-               } elsif ($m->{chg} eq 'A') {
-                       svn_ensure_parent_path( $m->{file_b} );
-                       apply_mod_line_blob($m);
-                       sys(qw(svn add), $m->{file_b});
-                       svn_check_prop_executable($m);
-               } else {
-                       croak "Invalid chg: $m->{chg}\n";
+sub new {
+       my ($class, $ref_id, $repo_id, $path) = @_;
+       if (defined $ref_id && !defined $repo_id && !defined $path) {
+               ($repo_id, $path) = find_ref($ref_id);
+               if (!defined $repo_id) {
+                       die "Could not find a \"svn-remote.*.fetch\" key ",
+                           "in the repository configuration matching: ",
+                           "refs/remotes/$ref_id\n";
                }
        }
-
-       assert_tree($treeish);
-       if ($_rmdir) { # remove empty directories
-               handle_rmdir($rm, $add);
+       my $self = _new($class, $repo_id, $ref_id, $path);
+       if (!defined $self->{path} || !length $self->{path}) {
+               my $fetch = command_oneline('config', '--get',
+                                           "svn-remote.$repo_id.fetch",
+                                           ":refs/remotes/$ref_id\$") or
+                    die "Failed to read \"svn-remote.$repo_id.fetch\" ",
+                        "\":refs/remotes/$ref_id\$\" in config\n";
+               ($self->{path}, undef) = split(/\s*:\s*/, $fetch);
        }
-       assert_tree($treeish);
-       return $mods;
+       $self->{url} = command_oneline('config', '--get',
+                                      "svn-remote.$repo_id.url") or
+                  die "Failed to read \"svn-remote.$repo_id.url\" in config\n";
+       $self->rebuild;
+       $self;
 }
 
-sub libsvn_checkout_tree {
-       my ($from, $treeish, $ed) = @_;
-       my $mods = get_diff($from, $treeish);
-       return $mods unless (scalar @$mods);
-       my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
-       foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
-               my $f = $m->{chg};
-               if (defined $o{$f}) {
-                       $ed->$f($m, $_q);
-               } else {
-                       croak "Invalid change type: $f\n";
-               }
-       }
-       $ed->rmdirs($_q) if $_rmdir;
-       return $mods;
-}
+sub refname {
+       my ($refname) = "refs/remotes/$_[0]->{ref_id}" ;
 
-# svn ls doesn't work with respect to the current working tree, but what's
-# in the repository.  There's not even an option for it... *sigh*
-# (added files don't show up and removed files remain in the ls listing)
-sub svn_ls_current {
-       my ($dir, $rm, $add) = @_;
-       chomp(my @ls = safe_qx('svn','ls',$dir));
-       my @ret = ();
-       foreach (@ls) {
-               s#/$##; # trailing slashes are evil
-               push @ret, $_ unless $rm->{$dir}->{$_};
-       }
-       if (exists $add->{$dir}) {
-               push @ret, keys %{$add->{$dir}};
+       # 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";
        }
-       return \@ret;
-}
 
-sub handle_rmdir {
-       my ($rm, $add) = @_;
+       # 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;
 
-       foreach my $dir (sort {length $b <=> length $a} keys %$rm) {
-               my $ls = svn_ls_current($dir, $rm, $add);
-               next if (scalar @$ls);
-               sys(qw(svn rm --force),$dir);
+       # It cannot have two consecutive dots .. anywhere
+       # .. becomes %2E%2E
+       $refname =~ s{\.\.}{%2E%2E}g;
 
-               my $dn = dirname $dir;
-               $rm->{ $dn }->{ basename $dir } = 1;
-               $ls = svn_ls_current($dn, $rm, $add);
-               while (scalar @$ls == 0 && $dn ne File::Spec->curdir) {
-                       sys(qw(svn rm --force),$dn);
-                       $dir = basename $dn;
-                       $dn = dirname $dn;
-                       $rm->{ $dn }->{ $dir } = 1;
-                       $ls = svn_ls_current($dn, $rm, $add);
-               }
-       }
+       return $refname;
 }
 
-sub get_commit_message {
-       my ($commit, $commit_msg) = (@_);
-       my %log_msg = ( msg => '' );
-       open my $msg, '>', $commit_msg or croak $!;
+sub desanitize_refname {
+       my ($refname) = @_;
+       $refname =~ s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;
+       return $refname;
+}
 
-       chomp(my $type = `git-cat-file -t $commit`);
-       if ($type eq 'commit' || $type eq 'tag') {
-               my $pid = open my $msg_fh, '-|';
-               defined $pid or croak $!;
+sub svm_uuid {
+       my ($self) = @_;
+       return $self->{svm}->{uuid} if $self->svm;
+       $self->ra;
+       unless ($self->{svm}) {
+               die "SVM UUID not cached, and reading remotely failed\n";
+       }
+       $self->{svm}->{uuid};
+}
 
-               if ($pid == 0) {
-                       exec('git-cat-file', $type, $commit) or croak $!;
+sub svm {
+       my ($self) = @_;
+       return $self->{svm} if $self->{svm};
+       my $svm;
+       # see if we have it in our config, first:
+       eval {
+               my $section = "svn-remote.$self->{repo_id}";
+               $svm = {
+                 source => tmp_config('--get', "$section.svm-source"),
+                 uuid => tmp_config('--get', "$section.svm-uuid"),
+                 replace => tmp_config('--get', "$section.svm-replace"),
                }
-               my $in_msg = 0;
-               while (<$msg_fh>) {
-                       if (!$in_msg) {
-                               $in_msg = 1 if (/^\s*$/);
-                       } elsif (/^git-svn-id: /) {
-                               # skip this, we regenerate the correct one
-                               # on re-fetch anyways
-                       } else {
-                               print $msg $_ or croak $!;
-                       }
+       };
+       if ($svm && $svm->{source} && $svm->{uuid} && $svm->{replace}) {
+               $self->{svm} = $svm;
+       }
+       $self->{svm};
+}
+
+sub _set_svm_vars {
+       my ($self, $ra) = @_;
+       return $ra if $self->svm;
+
+       my @err = ( "useSvmProps set, but failed to read SVM properties\n",
+                   "(svm:source, svm:uuid) ",
+                   "from the following URLs:\n" );
+       sub read_svm_props {
+               my ($self, $ra, $path, $r) = @_;
+               my $props = ($ra->get_dir($path, $r))[2];
+               my $src = $props->{'svm:source'};
+               my $uuid = $props->{'svm:uuid'};
+               return undef if (!$src || !$uuid);
+
+               chomp($src, $uuid);
+
+               $uuid =~ m{^[0-9a-f\-]{30,}$}
+                   or die "doesn't look right - svm:uuid is '$uuid'\n";
+
+               # 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("$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;
+       while (length $path) {
+               unless ($tried{"$self->{url}/$path"}) {
+                       return $ra if $self->read_svm_props($ra, $path, $r);
+                       $tried{"$self->{url}/$path"} = 1;
                }
-               close $msg_fh or croak $?;
+               $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 { "  $_\n" } keys %tried), "\n";
+       }
+
+       # nope, make sure we're connected to the repository root:
+       my $ok;
+       my @tried_b;
+       $path = $ra->{svn_path};
+       $ra = Git::SVN::Ra->new($ra->{repos_root});
+       while (length $path) {
+               unless ($tried{"$ra->{url}/$path"}) {
+                       $ok = $self->read_svm_props($ra, $path, $r);
+                       last if $ok;
+                       $tried{"$ra->{url}/$path"} = 1;
+               }
+               $path =~ s#/?[^/]+$##;
        }
-       close $msg or croak $!;
-
-       if ($_edit || ($type eq 'tree')) {
-               my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
-               system($editor, $commit_msg);
+       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 { "  $_\n" } keys %tried), "\n";
        }
+       Git::SVN::Ra->new($self->{url});
+}
 
-       # file_to_s removes all trailing newlines, so just use chomp() here:
-       open $msg, '<', $commit_msg or croak $!;
-       { local $/; chomp($log_msg{msg} = <$msg>); }
-       close $msg or croak $!;
+sub svnsync {
+       my ($self) = @_;
+       return $self->{svnsync} if $self->{svnsync};
 
-       return \%log_msg;
-}
+       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";
+       }
 
-sub set_svn_commit_env {
-       if (defined $LC_ALL) {
-               $ENV{LC_ALL} = $LC_ALL;
-       } else {
-               delete $ENV{LC_ALL};
+       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;
        }
-}
 
-sub svn_commit_tree {
-       my ($last, $commit) = @_;
-       my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
-       my $log_msg = get_commit_message($commit, $commit_msg);
-       my ($oneline) = ($log_msg->{msg} =~ /([^\n\r]+)/);
-       print "Committing $commit: $oneline\n";
+       my $err = "useSvnsyncProps set, but failed to read " .
+                 "svnsync property: svn:sync-from-";
+       my $rp = $self->ra->rev_proplist(0);
 
-       set_svn_commit_env();
-       my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
-       $ENV{LC_ALL} = 'C';
-       unlink $commit_msg;
-       my ($committed) = ($ci_output[$#ci_output] =~ /(\d+)/);
-       if (!defined $committed) {
-               my $out = join("\n",@ci_output);
-               print STDERR "W: Trouble parsing \`svn commit' output:\n\n",
-                               $out, "\n\nAssuming English locale...";
-               ($committed) = ($out =~ /^Committed revision \d+\./sm);
-               defined $committed or die " FAILED!\n",
-                       "Commit output failed to parse committed revision!\n",
-               print STDERR " OK\n";
-       }
-
-       my @svn_up = qw(svn up);
-       push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
-       if ($_optimize_commits && ($committed == ($last->{revision} + 1))) {
-               push @svn_up, "-r$committed";
-               sys(@svn_up);
-               my $info = svn_info('.');
-               my $date = $info->{'Last Changed Date'} or die "Missing date\n";
-               if ($info->{'Last Changed Rev'} != $committed) {
-                       croak "$info->{'Last Changed Rev'} != $committed\n"
-               }
-               my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
-                                       /(\d{4})\-(\d\d)\-(\d\d)\s
-                                        (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
-                                        or croak "Failed to parse date: $date\n";
-               $log_msg->{date} = "$tz $Y-$m-$d $H:$M:$S";
-               $log_msg->{author} = $info->{'Last Changed Author'};
-               $log_msg->{revision} = $committed;
-               $log_msg->{msg} .= "\n";
-               $log_msg->{parents} = [ $last->{commit} ];
-               $log_msg->{commit} = git_commit($log_msg, $commit);
-               return $log_msg;
-       }
-       # resync immediately
-       push @svn_up, "-r$last->{revision}";
-       sys(@svn_up);
-       return fetch("$committed=$commit");
-}
-
-sub rev_list_raw {
-       my (@args) = @_;
-       my $pid = open my $fh, '-|';
-       defined $pid or croak $!;
-       if (!$pid) {
-               exec(qw/git-rev-list --pretty=raw/, @args) or croak $!;
-       }
-       return { fh => $fh, t => { } };
+       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 };
 }
 
-sub next_rev_list_entry {
-       my $rl = shift;
-       my $fh = $rl->{fh};
-       my $x = $rl->{t};
-       while (<$fh>) {
-               if (/^commit ($sha1)$/o) {
-                       if ($x->{c}) {
-                               $rl->{t} = { c => $1 };
-                               return $x;
-                       } else {
-                               $x->{c} = $1;
-                       }
-               } elsif (/^parent ($sha1)$/o) {
-                       $x->{p}->{$1} = 1;
-               } elsif (s/^    //) {
-                       $x->{m} ||= '';
-                       $x->{m} .= $_;
+# this allows us to memoize our SVN::Ra UUID locally and avoid a
+# remote lookup (useful for 'git svn log').
+sub ra_uuid {
+       my ($self) = @_;
+       unless ($self->{ra_uuid}) {
+               my $key = "svn-remote.$self->{repo_id}.uuid";
+               my $uuid = eval { tmp_config('--get', $key) };
+               if (!$@ && $uuid && $uuid =~ /^([a-f\d\-]{30,})$/) {
+                       $self->{ra_uuid} = $uuid;
+               } else {
+                       die "ra_uuid called without URL\n" unless $self->{url};
+                       $self->{ra_uuid} = $self->ra->get_uuid;
+                       tmp_config('--add', $key, $self->{ra_uuid});
                }
        }
-       return ($x != $rl->{t}) ? $x : undef;
+       $self->{ra_uuid};
 }
 
-# read the entire log into a temporary file (which is removed ASAP)
-# and store the file handle + parser state
-sub svn_log_raw {
-       my (@log_args) = @_;
-       my $log_fh = IO::File->new_tmpfile or croak $!;
-       my $pid = fork;
-       defined $pid or croak $!;
-       if (!$pid) {
-               open STDOUT, '>&', $log_fh or croak $!;
-               exec (qw(svn log), @log_args) or croak $!
+sub ra {
+       my ($self) = shift;
+       my $ra = Git::SVN::Ra->new($self->{url});
+       if ($self->use_svm_props && !$self->{svm}) {
+               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;
        }
-       waitpid $pid, 0;
-       croak $? if $?;
-       seek $log_fh, 0, 0 or croak $!;
-       return { state => 'sep', fh => $log_fh };
+       $ra;
 }
 
-sub next_log_entry {
-       my $log = shift; # retval of svn_log_raw()
-       my $ret = undef;
-       my $fh = $log->{fh};
-
-       while (<$fh>) {
-               chomp;
-               if (/^\-{72}$/) {
-                       if ($log->{state} eq 'msg') {
-                               if ($ret->{lines}) {
-                                       $ret->{msg} .= $_."\n";
-                                       unless(--$ret->{lines}) {
-                                               $log->{state} = 'sep';
-                                       }
-                               } else {
-                                       croak "Log parse error at: $_\n",
-                                               $ret->{revision},
-                                               "\n";
-                               }
-                               next;
-                       }
-                       if ($log->{state} ne 'sep') {
-                               croak "Log parse error at: $_\n",
-                                       "state: $log->{state}\n",
-                                       $ret->{revision},
-                                       "\n";
-                       }
-                       $log->{state} = 'rev';
+sub rel_path {
+       my ($self) = @_;
+       my $repos_root = $self->ra->{repos_root};
+       return $self->{path} if ($self->{url} eq $repos_root);
+       my $url = $self->{url} .
+                 (length $self->{path} ? "/$self->{path}" : $self->{path});
+       $url =~ s!^\Q$repos_root\E(?:/+|$)!!g;
+       $url;
+}
 
-                       # if we have an empty log message, put something there:
-                       if ($ret) {
-                               $ret->{msg} ||= "\n";
-                               delete $ret->{lines};
-                               return $ret;
-                       }
-                       next;
-               }
-               if ($log->{state} eq 'rev' && s/^r(\d+)\s*\|\s*//) {
-                       my $rev = $1;
-                       my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3);
-                       ($lines) = ($lines =~ /(\d+)/);
-                       $date = '1970-01-01 00:00:00 +0000'
-                               if ($_ignore_nodate && $date eq '(no date)');
-                       my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
-                                       /(\d{4})\-(\d\d)\-(\d\d)\s
-                                        (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
-                                        or croak "Failed to parse date: $date\n";
-                       $ret = {        revision => $rev,
-                                       date => "$tz $Y-$m-$d $H:$M:$S",
-                                       author => $author,
-                                       lines => $lines,
-                                       msg => '' };
-                       if (defined $_authors && ! defined $users{$author}) {
-                               die "Author: $author not defined in ",
-                                               "$_authors file\n";
-                       }
-                       $log->{state} = 'msg_start';
-                       next;
-               }
-               # skip the first blank line of the message:
-               if ($log->{state} eq 'msg_start' && /^$/) {
-                       $log->{state} = 'msg';
-               } elsif ($log->{state} eq 'msg') {
-                       if ($ret->{lines}) {
-                               $ret->{msg} .= $_."\n";
-                               unless (--$ret->{lines}) {
-                                       $log->{state} = 'sep';
-                               }
-                       } else {
-                               croak "Log parse error at: $_\n",
-                                       $ret->{revision},"\n";
-                       }
+sub traverse_ignore {
+       my ($self, $fh, $path, $r) = @_;
+       $path =~ s#^/+##g;
+       my $ra = $self->ra;
+       my ($dirent, undef, $props) = $ra->get_dir($path, $r);
+       my $p = $path;
+       $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;
+               chomp $s;
+               if (length $p == 0) {
+                       $s =~ s#\n#\n/$p#g;
+                       print $fh "/$s\n";
+               } else {
+                       $s =~ s#\n#\n/$p/#g;
+                       print $fh "/$p/$s\n";
                }
        }
-       return $ret;
+       foreach (sort keys %$dirent) {
+               next if $dirent->{$_}->{kind} != $SVN::Node::dir;
+               $self->traverse_ignore($fh, "$path/$_", $r);
+       }
 }
 
-sub svn_info {
-       my $url = shift || $SVN_URL;
-
-       my $pid = open my $info_fh, '-|';
-       defined $pid or croak $!;
-
-       if ($pid == 0) {
-               exec(qw(svn info),$url) or croak $!;
-       }
+sub last_rev { ($_[0]->last_rev_commit)[0] }
+sub last_commit { ($_[0]->last_rev_commit)[1] }
 
-       my $ret = {};
-       # only single-lines seem to exist in svn info output
-       while (<$info_fh>) {
-               chomp $_;
-               if (m#^([^:]+)\s*:\s*(\S.*)$#) {
-                       $ret->{$1} = $2;
-                       push @{$ret->{-order}}, $1;
+# returns the newest SVN revision number and newest commit SHA1
+sub last_rev_commit {
+       my ($self) = @_;
+       if (defined $self->{last_rev} && defined $self->{last_commit}) {
+               return ($self->{last_rev}, $self->{last_commit});
+       }
+       my $c = ::verify_ref($self->refname.'^0');
+       if ($c && !$self->use_svm_props && !$self->no_metadata) {
+               my $rev = (::cmt_metadata($c))[1];
+               if (defined $rev) {
+                       ($self->{last_rev}, $self->{last_commit}) = ($rev, $c);
+                       return ($rev, $c);
                }
        }
-       close $info_fh or croak $?;
-       return $ret;
+       my $db_path = $self->db_path;
+       unless (-e $db_path) {
+               ($self->{last_rev}, $self->{last_commit}) = (undef, undef);
+               return (undef, undef);
+       }
+       my $offset = -41; # from tail
+       my $rl;
+       open my $fh, '<', $db_path or croak "$db_path not readable: $!\n";
+       sysseek($fh, $offset, 2); # don't care for errors
+       sysread($fh, $rl, 41) == 41 or return (undef, undef);
+       chomp $rl;
+       while (('0' x40) eq $rl && sysseek($fh, 0, 1) != 0) {
+               $offset -= 41;
+               sysseek($fh, $offset, 2); # don't care for errors
+               sysread($fh, $rl, 41) == 41 or return (undef, undef);
+               chomp $rl;
+       }
+       if ($c && $c ne $rl) {
+               die "$db_path and ", $self->refname,
+                   " inconsistent!:\n$c != $rl\n";
+       }
+       my $rev = sysseek($fh, 0, 1) or croak $!;
+       $rev =  ($rev - 41) / 41;
+       close $fh or croak $!;
+       ($self->{last_rev}, $self->{last_commit}) = ($rev, $c);
+       return ($rev, $c);
 }
 
-sub sys { system(@_) == 0 or croak $? }
-
-sub do_update_index {
-       my ($z_cmd, $cmd, $no_text_base) = @_;
-
-       my $z = open my $p, '-|';
-       defined $z or croak $!;
-       unless ($z) { exec @$z_cmd or croak $! }
+sub get_fetch_range {
+       my ($self, $min, $max) = @_;
+       $max ||= $self->ra->get_latest_revnum;
+       $min ||= $self->rev_db_max;
+       (++$min, $max);
+}
 
-       my $pid = open my $ui, '|-';
-       defined $pid or croak $!;
-       unless ($pid) {
-               exec('git-update-index',"--$cmd",'-z','--stdin') or croak $!;
-       }
-       local $/ = "\0";
-       while (my $x = <$p>) {
-               chomp $x;
-               if (!$no_text_base && lstat $x && ! -l _ &&
-                               svn_propget_base('svn:keywords', $x)) {
-                       my $mode = -x _ ? 0755 : 0644;
-                       my ($v,$d,$f) = File::Spec->splitpath($x);
-                       my $tb = File::Spec->catfile($d, '.svn', 'tmp',
-                                               'text-base',"$f.svn-base");
-                       $tb =~ s#^/##;
-                       unless (-f $tb) {
-                               $tb = File::Spec->catfile($d, '.svn',
-                                               'text-base',"$f.svn-base");
-                               $tb =~ s#^/##;
-                       }
-                       my @s = stat($x);
-                       unlink $x or croak $!;
-                       copy($tb, $x);
-                       chmod(($mode &~ umask), $x) or croak $!;
-                       utime $s[8], $s[9], $x;
+sub tmp_config {
+       my (@args) = @_;
+       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;
+       my @ret = eval {
+               unless (-f $config) {
+                       mkfile($config);
+                       open my $fh, '>', $config or
+                           die "Can't open $config: $!\n";
+                       print $fh "; This file is used internally by ",
+                                 "git-svn\n" or die
+                                 "Couldn't write to $config: $!\n";
+                       print $fh "; You should not have to edit it\n" or
+                             die "Couldn't write to $config: $!\n";
+                       close $fh or die "Couldn't close $config: $!\n";
                }
-               print $ui $x,"\0";
+               command('config', @args);
+       };
+       my $err = $@;
+       if (defined $old_config) {
+               $ENV{GIT_CONFIG} = $old_config;
+       } else {
+               delete $ENV{GIT_CONFIG};
        }
-       close $ui or croak $?;
+       die $err if $err;
+       wantarray ? @ret : $ret[0];
 }
 
-sub index_changes {
-       return if $_use_lib;
-
-       if (!-f "$GIT_SVN_DIR/info/exclude") {
-               open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!;
-               print $fd '.svn',"\n";
-               close $fd or croak $!;
+sub tmp_index_do {
+       my ($self, $sub) = @_;
+       my $old_index = $ENV{GIT_INDEX_FILE};
+       $ENV{GIT_INDEX_FILE} = $self->{index};
+       $@ = undef;
+       my @ret = eval {
+               my ($dir, $base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);
+               mkpath([$dir]) unless -d $dir;
+               &$sub;
+       };
+       my $err = $@;
+       if (defined $old_index) {
+               $ENV{GIT_INDEX_FILE} = $old_index;
+       } else {
+               delete $ENV{GIT_INDEX_FILE};
        }
-       my $no_text_base = shift;
-       do_update_index([qw/git-diff-files --name-only -z/],
-                       'remove',
-                       $no_text_base);
-       do_update_index([qw/git-ls-files -z --others/,
-                               "--exclude-from=$GIT_SVN_DIR/info/exclude"],
-                       'add',
-                       $no_text_base);
+       die $err if $err;
+       wantarray ? @ret : $ret[0];
 }
 
-sub s_to_file {
-       my ($str, $file, $mode) = @_;
-       open my $fd,'>',$file or croak $!;
-       print $fd $str,"\n" or croak $!;
-       close $fd or croak $!;
-       chmod ($mode &~ umask, $file) if (defined $mode);
-}
+sub assert_index_clean {
+       my ($self, $treeish) = @_;
 
-sub file_to_s {
-       my $file = shift;
-       open my $fd,'<',$file or croak "$!: file: $file\n";
-       local $/;
-       my $ret = <$fd>;
-       close $fd or croak $!;
-       $ret =~ s/\s*$//s;
-       return $ret;
+       $self->tmp_index_do(sub {
+               command_noisy('read-tree', $treeish) unless -e $self->{index};
+               my $x = command_oneline('write-tree');
+               my ($y) = (command(qw/cat-file commit/, $treeish) =~
+                          /^tree ($::sha1)/mo);
+               return if $y eq $x;
+
+               warn "Index mismatch: $y != $x\nrereading $treeish\n";
+               unlink $self->{index} or die "unlink $self->{index}: $!\n";
+               command_noisy('read-tree', $treeish);
+               $x = command_oneline('write-tree');
+               if ($y ne $x) {
+                       ::fatal "trees ($treeish) $y != $x\n",
+                               "Something is seriously wrong...\n";
+               }
+       });
 }
 
-sub assert_revision_unknown {
-       my $r = shift;
-       if (my $c = revdb_get($REVDB, $r)) {
-               croak "$r = $c already exists! Why are we refetching it?";
+sub get_commit_parents {
+       my ($self, $log_entry) = @_;
+       my (%seen, @ret, @tmp);
+       # legacy support for 'set-tree'; this is only used by set_tree_cb:
+       if (my $ip = $self->{inject_parents}) {
+               if (my $commit = delete $ip->{$log_entry->{revision}}) {
+                       push @tmp, $commit;
+               }
+       }
+       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};
+               $seen{$p} = 1;
+               push @ret, $p;
+               # MAXPARENT is defined to 16 in commit-tree.c:
+               last if @ret >= 16;
+       }
+       if (@tmp) {
+               die "r$log_entry->{revision}: No room for parents:\n\t",
+                   join("\n\t", @tmp), "\n";
        }
+       @ret;
 }
 
-sub trees_eq {
-       my ($x, $y) = @_;
-       my @x = safe_qx('git-cat-file','commit',$x);
-       my @y = safe_qx('git-cat-file','commit',$y);
-       if (($y[0] ne $x[0]) || $x[0] !~ /^tree $sha1\n$/
-                               || $y[0] !~ /^tree $sha1\n$/) {
-               print STDERR "Trees not equal: $y[0] != $x[0]\n";
-               return 0
+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";
+               }
        }
-       return 1;
+       $self->{-rewrite_root} = $rwr;
 }
 
-sub git_commit {
-       my ($log_msg, @parents) = @_;
-       assert_revision_unknown($log_msg->{revision});
-       map_tree_joins() if (@_branch_from && !%tree_map);
+sub metadata_url {
+       my ($self) = @_;
+       ($self->rewrite_root || $self->{url}) .
+          (length $self->{path} ? '/' . $self->{path} : '');
+}
 
-       my (@tmp_parents, @exec_parents, %seen_parent);
-       if (my $lparents = $log_msg->{parents}) {
-               @tmp_parents = @$lparents
-       }
-       # commit parents can be conditionally bound to a particular
-       # svn revision via: "svn_revno=commit_sha1", filter them out here:
-       foreach my $p (@parents) {
-               next unless defined $p;
-               if ($p =~ /^(\d+)=($sha1_short)$/o) {
-                       if ($1 == $log_msg->{revision}) {
-                               push @tmp_parents, $2;
-                       }
-               } else {
-                       push @tmp_parents, $p if $p =~ /$sha1_short/o;
-               }
-       }
-       my $tree = $log_msg->{tree};
-       if (!defined $tree) {
-               my $index = set_index($GIT_SVN_INDEX);
-               index_changes();
-               chomp($tree = `git-write-tree`);
-               croak $? if $?;
-               restore_index($index);
-       }
+sub full_url {
+       my ($self) = @_;
+       $self->{url} . (length $self->{path} ? '/' . $self->{path} : '');
+}
 
-       # just in case we clobber the existing ref, we still want that ref
-       # as our parent:
-       if (my $cur = eval { file_to_s("$GIT_DIR/refs/remotes/$GIT_SVN") }) {
-               push @tmp_parents, $cur;
+sub do_git_commit {
+       my ($self, $log_entry) = @_;
+       my $lr = $self->last_rev;
+       if (defined $lr && $lr >= $log_entry->{revision}) {
+               die "Last fetched revision of ", $self->refname,
+                   " was r$lr, but we are about to fetch: ",
+                   "r$log_entry->{revision}!\n";
+       }
+       if (my $c = $self->rev_db_get($log_entry->{revision})) {
+               croak "$log_entry->{revision} = $c already exists! ",
+                     "Why are we refetching it?\n";
        }
+       $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $log_entry->{name};
+       $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} =
+                                                         $log_entry->{email};
+       $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_entry->{date};
 
-       if (exists $tree_map{$tree}) {
-               foreach my $p (@{$tree_map{$tree}}) {
-                       my $skip;
-                       foreach (@tmp_parents) {
-                               # see if a common parent is found
-                               my $mb = eval {
-                                       safe_qx('git-merge-base', $_, $p)
-                               };
-                               next if ($@ || $?);
-                               $skip = 1;
-                               last;
-                       }
-                       next if $skip;
-                       my ($url_p, $r_p, $uuid_p) = cmt_metadata($p);
-                       next if (($SVN_UUID eq $uuid_p) &&
-                                               ($log_msg->{revision} > $r_p));
-                       next if (defined $url_p && defined $SVN_URL &&
-                                               ($SVN_UUID eq $uuid_p) &&
-                                               ($url_p eq $SVN_URL));
-                       push @tmp_parents, $p;
-               }
-       }
-       foreach (@tmp_parents) {
-               next if $seen_parent{$_};
-               $seen_parent{$_} = 1;
-               push @exec_parents, $_;
-               # MAXPARENT is defined to 16 in commit-tree.c:
-               last if @exec_parents > 16;
+       my $tree = $log_entry->{tree};
+       if (!defined $tree) {
+               $tree = $self->tmp_index_do(sub {
+                                           command_oneline('write-tree') });
        }
+       die "Tree is not a valid sha1: $tree\n" if $tree !~ /^$::sha1$/o;
 
-       set_commit_env($log_msg);
        my @exec = ('git-commit-tree', $tree);
-       push @exec, '-p', $_  foreach @exec_parents;
+       foreach ($self->get_commit_parents($log_entry)) {
+               push @exec, '-p', $_;
+       }
        defined(my $pid = open3(my $msg_fh, my $out_fh, '>&STDERR', @exec))
-                                                               or croak $!;
-       print $msg_fh $log_msg->{msg} or croak $!;
-       unless ($_no_metadata) {
-               print $msg_fh "\ngit-svn-id: $SVN_URL\@$log_msg->{revision}",
-                                       " $SVN_UUID\n" or croak $!;
+                                                                  or croak $!;
+       print $msg_fh $log_entry->{log} or croak $!;
+       unless ($self->no_metadata) {
+               print $msg_fh "\ngit-svn-id: $log_entry->{metadata}\n"
+                             or croak $!;
        }
        $msg_fh->flush == 0 or croak $!;
        close $msg_fh or croak $!;
@@ -2037,668 +1769,557 @@ sub git_commit {
        close $out_fh or croak $!;
        waitpid $pid, 0;
        croak $? if $?;
-       if ($commit !~ /^$sha1$/o) {
+       if ($commit !~ /^$::sha1$/o) {
                die "Failed to commit, invalid sha1: $commit\n";
        }
-       sys('git-update-ref',"refs/remotes/$GIT_SVN",$commit);
-       revdb_set($REVDB, $log_msg->{revision}, $commit);
 
-       # this output is read via pipe, do not change:
-       print "r$log_msg->{revision} = $commit\n";
-       check_repack();
-       return $commit;
-}
+       $self->rev_db_set($log_entry->{revision}, $commit, 1);
 
-sub check_repack {
-       if ($_repack && (--$_repack_nr == 0)) {
+       $self->{last_rev} = $log_entry->{revision};
+       $self->{last_commit} = $commit;
+       print "r$log_entry->{revision}";
+       if (defined $log_entry->{svm_revision}) {
+                print " (\@$log_entry->{svm_revision})";
+                $self->rev_db_set($log_entry->{svm_revision}, $commit,
+                                  0, $self->svm_uuid);
+       }
+       print " = $commit ($self->{ref_id})\n";
+       if (defined $_repack && (--$_repack_nr == 0)) {
                $_repack_nr = $_repack;
-               sys("git repack $_repack_flags");
+               # repack doesn't use any arguments with spaces in them, does it?
+               print "Running git repack $_repack_flags ...\n";
+               command_noisy('repack', split(/\s+/, $_repack_flags));
+               print "Done repacking\n";
        }
+       return $commit;
 }
 
-sub set_commit_env {
-       my ($log_msg) = @_;
-       my $author = $log_msg->{author};
-       if (!defined $author || length $author == 0) {
-               $author = '(no author)';
+sub match_paths {
+       my ($self, $paths, $r) = @_;
+       return 1 if $self->{path} eq '';
+       if (my $path = $paths->{"/$self->{path}"}) {
+               return ($path->{action} eq 'D') ? 0 : 1;
+       }
+       $self->{path_regex} ||= qr/^\/\Q$self->{path}\E\//;
+       if (grep /$self->{path_regex}/, keys %$paths) {
+               return 1;
+       }
+       my $c = '';
+       foreach (split m#/#, $self->{path}) {
+               $c .= "/$_";
+               next unless ($paths->{$c} &&
+                            ($paths->{$c}->{action} =~ /^[AR]$/));
+               if ($self->ra->check_path($self->{path}, $r) ==
+                   $SVN::Node::dir) {
+                       return 1;
+               }
        }
-       my ($name,$email) = defined $users{$author} ?  @{$users{$author}}
-                               : ($author,"$author\@$SVN_UUID");
-       $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name;
-       $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email;
-       $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date};
+       return 0;
 }
 
-sub apply_mod_line_blob {
-       my $m = shift;
-       if ($m->{mode_b} =~ /^120/) {
-               blob_to_symlink($m->{sha1_b}, $m->{file_b});
-       } else {
-               blob_to_file($m->{sha1_b}, $m->{file_b});
+sub find_parent_branch {
+       my ($self, $paths, $rev) = @_;
+       return undef unless $self->follow_parent;
+       unless (defined $paths) {
+               my $err_handler = $SVN::Error::handler;
+               $SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;
+               $self->ra->get_log([$self->{path}], $rev, $rev, 0, 1, 1, sub {
+                                  $paths =
+                                     Git::SVN::Ra::dup_changed_paths($_[0]) });
+               $SVN::Error::handler = $err_handler;
+       }
+       return undef unless defined $paths;
+
+       # look for a parent from another branch:
+       my @b_path_components = split m#/#, $self->rel_path;
+       my @a_path_components;
+       my $i;
+       while (@b_path_components) {
+               $i = $paths->{'/'.join('/', @b_path_components)};
+               last if $i && defined $i->{copyfrom_path};
+               unshift(@a_path_components, pop(@b_path_components));
+       }
+       return undef unless defined $i && defined $i->{copyfrom_path};
+       my $branch_from = $i->{copyfrom_path};
+       if (@a_path_components) {
+               print STDERR "branch_from: $branch_from => ";
+               $branch_from .= '/'.join('/', @a_path_components);
+               print STDERR $branch_from, "\n";
+       }
+       my $r = $i->{copyfrom_rev};
+       my $repos_root = $self->ra->{repos_root};
+       my $url = $self->ra->{url};
+       my $new_url = $repos_root . $branch_from;
+       print STDERR  "Found possible branch point: ",
+                     "$new_url => ", $self->full_url, ", $r\n";
+       $branch_from =~ s#^/##;
+       my $gs = Git::SVN->find_by_url($new_url, $repos_root, $branch_from);
+       unless ($gs) {
+               my $ref_id = $self->{ref_id};
+               $ref_id =~ s/\@\d+$//;
+               $ref_id .= "\@$r";
+               # just grow a tail if we're not unique enough :x
+               $ref_id .= '-' while find_ref($ref_id);
+               print STDERR "Initializing parent: $ref_id\n";
+               $gs = Git::SVN->init($new_url, '', $ref_id, $ref_id, 1);
+       }
+       my ($r0, $parent) = $gs->find_rev_before($r, 1);
+       if (!defined $r0 || !defined $parent) {
+               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";
+               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
+                       # at the moment), so we can't rely on it
+                       $self->{last_commit} = $parent;
+                       $ed = SVN::Git::Fetcher->new($self);
+                       $gs->ra->gs_do_switch($r0, $rev, $gs,
+                                             $self->full_url, $ed)
+                         or die "SVN connection failed somewhere...\n";
+               } else {
+                       print STDERR "Following parent with do_update\n";
+                       $ed = SVN::Git::Fetcher->new($self);
+                       $self->ra->gs_do_update($rev, $rev, $self, $ed)
+                         or die "SVN connection failed somewhere...\n";
+               }
+               print STDERR "Successfully followed parent\n";
+               return $self->make_log_entry($rev, [$parent], $ed);
        }
+       return undef;
 }
 
-sub blob_to_symlink {
-       my ($blob, $link) = @_;
-       defined $link or croak "\$link not defined!\n";
-       croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
-       if (-l $link || -f _) {
-               unlink $link or croak $!;
+sub do_fetch {
+       my ($self, $paths, $rev) = @_;
+       my $ed;
+       my ($last_rev, @parents);
+       if (my $lc = $self->last_commit) {
+               # we can have a branch that was deleted, then re-added
+               # under the same name but copied from another path, in
+               # which case we'll have multiple parents (we don't
+               # want to break the original ref, nor lose copypath info):
+               if (my $log_entry = $self->find_parent_branch($paths, $rev)) {
+                       push @{$log_entry->{parents}}, $lc;
+                       return $log_entry;
+               }
+               $ed = SVN::Git::Fetcher->new($self);
+               $last_rev = $self->{last_rev};
+               $ed->{c} = $lc;
+               @parents = ($lc);
+       } else {
+               $last_rev = $rev;
+               if (my $log_entry = $self->find_parent_branch($paths, $rev)) {
+                       return $log_entry;
+               }
+               $ed = SVN::Git::Fetcher->new($self);
+       }
+       unless ($self->ra->gs_do_update($last_rev, $rev, $self, $ed)) {
+               die "SVN connection failed somewhere...\n";
+       }
+       $self->make_log_entry($rev, \@parents, $ed);
+}
+
+sub get_untracked {
+       my ($self, $ed) = @_;
+       my @out;
+       my $h = $ed->{empty};
+       foreach (sort keys %$h) {
+               my $act = $h->{$_} ? '+empty_dir' : '-empty_dir';
+               push @out, "  $act: " . uri_encode($_);
+               warn "W: $act: $_\n";
+       }
+       foreach my $t (qw/dir_prop file_prop/) {
+               $h = $ed->{$t} or next;
+               foreach my $path (sort keys %$h) {
+                       my $ppath = $path eq '' ? '.' : $path;
+                       foreach my $prop (sort keys %{$h->{$path}}) {
+                               next if $SKIP_PROP{$prop};
+                               my $v = $h->{$path}->{$prop};
+                               my $t_ppath_prop = "$t: " .
+                                                   uri_encode($ppath) . ' ' .
+                                                   uri_encode($prop);
+                               if (defined $v) {
+                                       push @out, "  +$t_ppath_prop " .
+                                                  uri_encode($v);
+                               } else {
+                                       push @out, "  -$t_ppath_prop";
+                               }
+                       }
+               }
+       }
+       foreach my $t (qw/absent_file absent_directory/) {
+               $h = $ed->{$t} or next;
+               foreach my $parent (sort keys %$h) {
+                       foreach my $path (sort @{$h->{$parent}}) {
+                               push @out, "  $t: " .
+                                          uri_encode("$parent/$path");
+                               warn "W: $t: $parent/$path ",
+                                    "Insufficient permissions?\n";
+                       }
+               }
        }
+       \@out;
+}
 
-       my $dest = `git-cat-file blob $blob`; # no newline, so no chomp
-       symlink $dest, $link or croak $!;
+sub parse_svn_date {
+       my $date = shift || return '+0000 1970-01-01 00:00:00';
+       my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
+                                           (\d\d)\:(\d\d)\:(\d\d).\d+Z$/x) or
+                                        croak "Unable to parse date: $date\n";
+       "+0000 $Y-$m-$d $H:$M:$S";
 }
 
-sub blob_to_file {
-       my ($blob, $file) = @_;
-       defined $file or croak "\$file not defined!\n";
-       croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
-       if (-l $file || -f _) {
-               unlink $file or croak $!;
+sub check_author {
+       my ($author) = @_;
+       if (!defined $author || length $author == 0) {
+               $author = '(no author)';
        }
+       if (defined $::_authors && ! defined $::users{$author}) {
+               die "Author: $author not defined in $::_authors file\n";
+       }
+       $author;
+}
 
-       open my $blob_fh, '>', $file or croak "$!: $file\n";
-       my $pid = fork;
-       defined $pid or croak $!;
+sub make_log_entry {
+       my ($self, $rev, $parents, $ed) = @_;
+       my $untracked = $self->get_untracked($ed);
 
-       if ($pid == 0) {
-               open STDOUT, '>&', $blob_fh or croak $!;
-               exec('git-cat-file','blob',$blob) or croak $!;
+       open my $un, '>>', "$self->{dir}/unhandled.log" or croak $!;
+       print $un "r$rev\n" or croak $!;
+       print $un $_, "\n" foreach @$untracked;
+       my %log_entry = ( parents => $parents || [], revision => $rev,
+                         log => '');
+
+       my $headrev;
+       my $logged = delete $self->{logged_rev_props};
+       if (!$logged || $self->{-want_revprops}) {
+               my $rp = $self->ra->rev_proplist($rev);
+               foreach (sort keys %$rp) {
+                       my $v = $rp->{$_};
+                       if (/^svn:(author|date|log)$/) {
+                               $log_entry{$1} = $v;
+                       } elsif ($_ eq 'svm:headrev') {
+                               $headrev = $v;
+                       } else {
+                               print $un "  rev_prop: ", uri_encode($_), ' ',
+                                         uri_encode($v), "\n";
+                       }
+               }
+       } else {
+               map { $log_entry{$_} = $logged->{$_} } keys %$logged;
+       }
+       close $un or croak $!;
+
+       $log_entry{date} = parse_svn_date($log_entry{date});
+       $log_entry{log} .= "\n";
+       my $author = $log_entry{author} = check_author($log_entry{author});
+       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+)$};
+               # 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: $svm->{uuid}\n",
+                           "     got: $uuid\n";
+               }
+               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 {
+               my $url = $self->metadata_url;
+               remove_username($url);
+               $log_entry{metadata} = "$url\@$rev " .
+                                      $self->ra->get_uuid;
+               $email ||= "$author\@" . $self->ra->get_uuid;
        }
-       waitpid $pid, 0;
-       croak $? if $?;
+       $log_entry{name} = $name;
+       $log_entry{email} = $email;
+       \%log_entry;
+}
 
-       close $blob_fh or croak $!;
+sub fetch {
+       my ($self, $min_rev, $max_rev, @parents) = @_;
+       my ($last_rev, $last_commit) = $self->last_rev_commit;
+       my ($base, $head) = $self->get_fetch_range($min_rev, $max_rev);
+       $self->ra->gs_fetch_loop_common($base, $head, [$self]);
 }
 
-sub safe_qx {
-       my $pid = open my $child, '-|';
-       defined $pid or croak $!;
-       if ($pid == 0) {
-               exec(@_) or croak $!;
-       }
-       my @ret = (<$child>);
-       close $child or croak $?;
-       die $? if $?; # just in case close didn't error out
-       return wantarray ? @ret : join('',@ret);
+sub set_tree_cb {
+       my ($self, $log_entry, $tree, $rev, $date, $author) = @_;
+       $self->{inject_parents} = { $rev => $tree };
+       $self->fetch(undef, undef);
 }
 
-sub svn_compat_check {
-       if ($_follow_parent) {
-               print STDERR 'E: --follow-parent functionality is only ',
-                               "available when SVN libraries are used\n";
-               exit 1;
+sub set_tree {
+       my ($self, $tree) = (shift, shift);
+       my $log_entry = ::get_commit_entry($tree);
+       unless ($self->{last_rev}) {
+               fatal("Must have an existing revision to commit\n");
        }
-       my @co_help = safe_qx(qw(svn co -h));
-       unless (grep /ignore-externals/,@co_help) {
-               print STDERR "W: Installed svn version does not support ",
-                               "--ignore-externals\n";
-               $_no_ignore_ext = 1;
+       my %ed_opts = ( r => $self->{last_rev},
+                       log => $log_entry->{log},
+                       ra => $self->ra,
+                       tree_a => $self->{last_commit},
+                       tree_b => $tree,
+                       editor_cb => sub {
+                              $self->set_tree_cb($log_entry, $tree, @_) },
+                       svn_path => $self->{path} );
+       if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
+               print "No changes\nr$self->{last_rev} = $tree\n";
        }
-       if (grep /usage: checkout URL\[\@REV\]/,@co_help) {
-               $_svn_co_url_revs = 1;
+}
+
+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";
+               my ($dir, $base) = ($db_path =~ m#^(.*?)/?([^/]+)$#);
+               symlink $base, $self->{db_root} or die
+                    "symlink $base => $self->{db_root} failed: $!\n";
+               return;
        }
-       if (grep /\[TARGET\[\@REV\]\.\.\.\]/, `svn propget -h`) {
-               $_svn_pg_peg_revs = 1;
+       print "Rebuilding $db_path ...\n";
+       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;
+       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);
+
+               # if we merged or otherwise started elsewhere, this is
+               # how we break out of it
+               if ((defined $svn_uuid && ($uuid ne $svn_uuid)) ||
+                   ($full_url && $url && ($url ne $full_url))) {
+                       next;
+               }
+               $latest ||= $rev;
+               $svn_uuid ||= $uuid;
+
+               $self->rev_db_set($rev, $c);
+               print "r$rev = $c\n";
        }
+       command_close_pipe($log, $ctx);
+       print "Done rebuilding $db_path\n";
+}
 
-       # I really, really hope nobody hits this...
-       unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) {
-               print STDERR <<'';
-W: The installed svn version does not support the --stop-on-copy flag in
-   the log command.
-   Lets hope the directory you're tracking is not a branch or tag
-   and was never moved within the repository...
+# rev_db:
+# Tie::File seems to be prone to offset errors if revisions get sparse,
+# it's not that fast, either.  Tie::File is also not in Perl 5.6.  So
+# one of my favorite modules is out :<  Next up would be one of the DBM
+# modules, but I'm not sure which is most portable...  So I'll just
+# go with something that's plain-text, but still capable of
+# being randomly accessed.  So here's my ultra-simple fixed-width
+# database.  All records are 40 characters + "\n", so it's easy to seek
+# to a revision: (41 * rev) is the byte offset.
+# A record of 40 0s denotes an empty revision.
+# And yes, it's still pretty fast (faster than Tie::File).
+# These files are disposable unless noMetadata or useSvmProps is set
 
-               $_no_stop_copy = 1;
+sub _rev_db_set {
+       my ($fh, $rev, $commit) = @_;
+       my $offset = $rev * 41;
+       # assume that append is the common case:
+       seek $fh, 0, 2 or croak $!;
+       my $pos = tell $fh;
+       if ($pos < $offset) {
+               for (1 .. (($offset - $pos) / 41)) {
+                       print $fh (('0' x 40),"\n") or croak $!;
+               }
+       }
+       seek $fh, $offset, 0 or croak $!;
+       print $fh $commit,"\n" or croak $!;
+}
+
+sub mkfile {
+       my ($path) = @_;
+       unless (-e $path) {
+               my ($dir, $base) = ($path =~ m#^(.*?)/?([^/]+)$#);
+               mkpath([$dir]) unless -d $dir;
+               open my $fh, '>>', $path or die "Couldn't create $path: $!\n";
+               close $fh or die "Couldn't close (create) $path: $!\n";
+       }
+}
+
+sub rev_db_set {
+       my ($self, $rev, $commit, $update_ref, $uuid) = @_;
+       length $commit == 40 or die "arg3 must be a full SHA1 hexsum\n";
+       my $db = $self->db_path($uuid);
+       my $db_lock = "$db.lock";
+       my $sig;
+       if ($update_ref) {
+               $SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} =
+                           $SIG{USR1} = $SIG{USR2} = sub { $sig = $_[0] };
+       }
+       mkfile($db);
+
+       $LOCKFILES{$db_lock} = 1;
+       my $sync;
+       # both of these options make our .rev_db file very, very important
+       # and we can't afford to lose it because rebuild() won't work
+       if ($self->use_svm_props || $self->no_metadata) {
+               $sync = 1;
+               copy($db, $db_lock) or die "rev_db_set(@_): ",
+                                          "Failed to copy: ",
+                                          "$db => $db_lock ($!)\n";
+       } else {
+               rename $db, $db_lock or die "rev_db_set(@_): ",
+                                           "Failed to rename: ",
+                                           "$db => $db_lock ($!)\n";
+       }
+       open my $fh, '+<', $db_lock or die "Couldn't open $db_lock: $!\n";
+       _rev_db_set($fh, $rev, $commit);
+       if ($sync) {
+               $fh->flush or die "Couldn't flush $db_lock: $!\n";
+               $fh->sync or die "Couldn't sync $db_lock: $!\n";
+       }
+       close $fh or croak $!;
+       if ($update_ref) {
+               $_head = $self;
+               command_noisy('update-ref', '-m', "r$rev",
+                             $self->refname, $commit);
+       }
+       rename $db_lock, $db or die "rev_db_set(@_): ", "Failed to rename: ",
+                                   "$db_lock => $db ($!)\n";
+       delete $LOCKFILES{$db_lock};
+       if ($update_ref) {
+               $SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} =
+                           $SIG{USR1} = $SIG{USR2} = 'DEFAULT';
+               kill $sig, $$ if defined $sig;
        }
 }
 
-# *sigh*, new versions of svn won't honor -r<rev> without URL@<rev>,
-# (and they won't honor URL@<rev> without -r<rev>, too!)
-sub svn_cmd_checkout {
-       my ($url, $rev, $dir) = @_;
-       my @cmd = ('svn','co', "-r$rev");
-       push @cmd, '--ignore-externals' unless $_no_ignore_ext;
-       $url .= "\@$rev" if $_svn_co_url_revs;
-       sys(@cmd, $url, $dir);
+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";
+       my $max = $stat[7] / 41;
+       (($max > 0) ? $max - 1 : 0);
 }
 
-sub check_upgrade_needed {
-       if (!-r $REVDB) {
-               -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
-               open my $fh, '>>',$REVDB or croak $!;
-               close $fh;
-       }
-       my $old = eval {
-               my $pid = open my $child, '-|';
-               defined $pid or croak $!;
-               if ($pid == 0) {
-                       close STDERR;
-                       exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $!;
-               }
-               my @ret = (<$child>);
-               close $child or croak $?;
-               die $? if $?; # just in case close didn't error out
-               return wantarray ? @ret : join('',@ret);
-       };
-       return unless $old;
-       my $head = eval { safe_qx('git-rev-parse',"refs/remotes/$GIT_SVN") };
-       if ($@ || !$head) {
-               print STDERR "Please run: $0 rebuild --upgrade\n";
-               exit 1;
+sub rev_db_get {
+       my ($self, $rev, $uuid) = @_;
+       my $ret;
+       my $offset = $rev * 41;
+       my $db_path = $self->db_path($uuid);
+       return undef unless -e $db_path;
+       open my $fh, '<', $db_path or croak $!;
+       if (sysseek($fh, $offset, 0) == $offset) {
+               my $read = sysread($fh, $ret, 40);
+               $ret = undef if ($read != 40 || $ret eq ('0'x40));
        }
+       close $fh or croak $!;
+       $ret;
 }
 
-# fills %tree_map with a reverse mapping of trees to commits.  Useful
-# for finding parents to commit on.
-sub map_tree_joins {
-       my %seen;
-       foreach my $br (@_branch_from) {
-               my $pid = open my $pipe, '-|';
-               defined $pid or croak $!;
-               if ($pid == 0) {
-                       exec(qw(git-rev-list --topo-order --pretty=raw), $br)
-                                                               or croak $!;
-               }
-               while (<$pipe>) {
-                       if (/^commit ($sha1)$/o) {
-                               my $commit = $1;
-
-                               # if we've seen a commit,
-                               # we've seen its parents
-                               last if $seen{$commit};
-                               my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o);
-                               unless (defined $tree) {
-                                       die "Failed to parse commit $commit\n";
-                               }
-                               push @{$tree_map{$tree}}, $commit;
-                               $seen{$commit} = 1;
-                       }
+sub find_rev_before {
+       my ($self, $rev, $eq_ok) = @_;
+       --$rev unless $eq_ok;
+       while ($rev > 0) {
+               if (my $c = $self->rev_db_get($rev)) {
+                       return ($rev, $c);
                }
-               close $pipe; # we could be breaking the pipe early
+               --$rev;
        }
+       return (undef, undef);
 }
 
-sub load_all_refs {
-       if (@_branch_from) {
-               print STDERR '--branch|-b parameters are ignored when ',
-                       "--branch-all-refs|-B is passed\n";
+sub _new {
+       my ($class, $repo_id, $ref_id, $path) = @_;
+       unless (defined $repo_id && length $repo_id) {
+               $repo_id = $Git::SVN::default_repo_id;
        }
+       unless (defined $ref_id && length $ref_id) {
+               $_[2] = $ref_id = $Git::SVN::default_ref_id;
+       }
+       $_[1] = $repo_id = sanitize_remote_name($repo_id);
+       my $dir = "$ENV{GIT_DIR}/svn/$ref_id";
+       $_[3] = $path = '' unless (defined $path);
+       mkpath(["$ENV{GIT_DIR}/svn"]);
+       bless {
+               ref_id => $ref_id, dir => $dir, index => "$dir/index",
+               path => $path, config => "$ENV{GIT_DIR}/svn/config",
+               db_root => "$dir/.rev_db", repo_id => $repo_id }, $class;
+}
 
-       # don't worry about rev-list on non-commit objects/tags,
-       # it shouldn't blow up if a ref is a blob or tree...
-       chomp(@_branch_from = `git-rev-parse --symbolic --all`);
+sub db_path {
+       my ($self, $uuid) = @_;
+       $uuid ||= $self->ra_uuid;
+       "$self->{db_root}.$uuid";
 }
 
-# '<svn username> = real-name <email address>' mapping based on git-svnimport:
-sub load_authors {
-       open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
-       while (<$authors>) {
-               chomp;
-               next unless /^(\S+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
-               my ($user, $name, $email) = ($1, $2, $3);
-               $users{$user} = [$name, $email];
-       }
-       close $authors or croak $!;
-}
-
-sub rload_authors {
-       open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
-       while (<$authors>) {
-               chomp;
-               next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
-               my ($user, $name, $email) = ($1, $2, $3);
-               $rusers{"$name <$email>"} = $user;
-       }
-       close $authors or croak $!;
-}
-
-sub svn_propget_base {
-       my ($p, $f) = @_;
-       $f .= '@BASE' if $_svn_pg_peg_revs;
-       return safe_qx(qw/svn propget/, $p, $f);
-}
-
-sub git_svn_each {
-       my $sub = shift;
-       foreach (`git-rev-parse --symbolic --all`) {
-               next unless s#^refs/remotes/##;
-               chomp $_;
-               next unless -f "$GIT_DIR/svn/$_/info/url";
-               &$sub($_);
-       }
-}
-
-sub migrate_revdb {
-       git_svn_each(sub {
-               my $id = shift;
-               defined(my $pid = fork) or croak $!;
-               if (!$pid) {
-                       $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
-                       init_vars();
-                       exit 0 if -r $REVDB;
-                       print "Upgrading svn => git mapping...\n";
-                       -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
-                       open my $fh, '>>',$REVDB or croak $!;
-                       close $fh;
-                       rebuild();
-                       print "Done upgrading. You may now delete the ",
-                               "deprecated $GIT_SVN_DIR/revs directory\n";
-                       exit 0;
-               }
-               waitpid $pid, 0;
-               croak $? if $?;
-       });
-}
-
-sub migration_check {
-       migrate_revdb() unless (-e $REVDB);
-       return if (-d "$GIT_DIR/svn" || !-d $GIT_DIR);
-       print "Upgrading repository...\n";
-       unless (-d "$GIT_DIR/svn") {
-               mkdir "$GIT_DIR/svn" or croak $!;
-       }
-       print "Data from a previous version of git-svn exists, but\n\t",
-                               "$GIT_SVN_DIR\n\t(required for this version ",
-                               "($VERSION) of git-svn) does not.\n";
-
-       foreach my $x (`git-rev-parse --symbolic --all`) {
-               next unless $x =~ s#^refs/remotes/##;
-               chomp $x;
-               next unless -f "$GIT_DIR/$x/info/url";
-               my $u = eval { file_to_s("$GIT_DIR/$x/info/url") };
-               next unless $u;
-               my $dn = dirname("$GIT_DIR/svn/$x");
-               mkpath([$dn]) unless -d $dn;
-               rename "$GIT_DIR/$x", "$GIT_DIR/svn/$x" or croak "$!: $x";
-       }
-       migrate_revdb() if (-d $GIT_SVN_DIR && !-w $REVDB);
-       print "Done upgrading.\n";
-}
-
-sub find_rev_before {
-       my ($r, $id, $eq_ok) = @_;
-       my $f = "$GIT_DIR/svn/$id/.rev_db";
-       return (undef,undef) unless -r $f;
-       --$r unless $eq_ok;
-       while ($r > 0) {
-               if (my $c = revdb_get($f, $r)) {
-                       return ($r, $c);
-               }
-               --$r;
-       }
-       return (undef, undef);
-}
-
-sub init_vars {
-       $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
-       $GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN";
-       $REVDB = "$GIT_SVN_DIR/.rev_db";
-       $GIT_SVN_INDEX = "$GIT_SVN_DIR/index";
-       $SVN_URL = undef;
-       $SVN_WC = "$GIT_SVN_DIR/tree";
-       %tree_map = ();
-}
-
-# convert GetOpt::Long specs for use by git-repo-config
-sub read_repo_config {
-       return unless -d $GIT_DIR;
-       my $opts = shift;
-       foreach my $o (keys %$opts) {
-               my $v = $opts->{$o};
-               my ($key) = ($o =~ /^([a-z\-]+)/);
-               $key =~ s/-//g;
-               my $arg = 'git-repo-config';
-               $arg .= ' --int' if ($o =~ /[:=]i$/);
-               $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
-               if (ref $v eq 'ARRAY') {
-                       chomp(my @tmp = `$arg --get-all svn.$key`);
-                       @$v = @tmp if @tmp;
-               } else {
-                       chomp(my $tmp = `$arg --get svn.$key`);
-                       if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) {
-                               $$v = $tmp;
-                       }
-               }
-       }
-}
-
-sub set_default_vals {
-       if (defined $_repack) {
-               $_repack = 1000 if ($_repack <= 0);
-               $_repack_nr = $_repack;
-               $_repack_flags ||= '-d';
-       }
-}
-
-sub read_grafts {
-       my $gr_file = shift;
-       my ($grafts, $comments) = ({}, {});
-       if (open my $fh, '<', $gr_file) {
-               my @tmp;
-               while (<$fh>) {
-                       if (/^($sha1)\s+/) {
-                               my $c = $1;
-                               if (@tmp) {
-                                       @{$comments->{$c}} = @tmp;
-                                       @tmp = ();
-                               }
-                               foreach my $p (split /\s+/, $_) {
-                                       $grafts->{$c}->{$p} = 1;
-                               }
-                       } else {
-                               push @tmp, $_;
-                       }
-               }
-               close $fh or croak $!;
-               @{$comments->{'END'}} = @tmp if @tmp;
-       }
-       return ($grafts, $comments);
-}
-
-sub write_grafts {
-       my ($grafts, $comments, $gr_file) = @_;
-
-       open my $fh, '>', $gr_file or croak $!;
-       foreach my $c (sort keys %$grafts) {
-               if ($comments->{$c}) {
-                       print $fh $_ foreach @{$comments->{$c}};
-               }
-               my $p = $grafts->{$c};
-               my %x; # real parents
-               delete $p->{$c}; # commits are not self-reproducing...
-               my $pid = open my $ch, '-|';
-               defined $pid or croak $!;
-               if (!$pid) {
-                       exec(qw/git-cat-file commit/, $c) or croak $!;
-               }
-               while (<$ch>) {
-                       if (/^parent ($sha1)/) {
-                               $x{$1} = $p->{$1} = 1;
-                       } else {
-                               last unless /^\S/;
-                       }
-               }
-               close $ch; # breaking the pipe
-
-               # if real parents are the only ones in the grafts, drop it
-               next if join(' ',sort keys %$p) eq join(' ',sort keys %x);
-
-               my (@ip, @jp, $mb);
-               my %del = %x;
-               @ip = @jp = keys %$p;
-               foreach my $i (@ip) {
-                       next if $del{$i} || $p->{$i} == 2;
-                       foreach my $j (@jp) {
-                               next if $i eq $j || $del{$j} || $p->{$j} == 2;
-                               $mb = eval { safe_qx('git-merge-base',$i,$j) };
-                               next unless $mb;
-                               chomp $mb;
-                               next if $x{$mb};
-                               if ($mb eq $j) {
-                                       delete $p->{$i};
-                                       $del{$i} = 1;
-                               } elsif ($mb eq $i) {
-                                       delete $p->{$j};
-                                       $del{$j} = 1;
-                               }
-                       }
-               }
-
-               # if real parents are the only ones in the grafts, drop it
-               next if join(' ',sort keys %$p) eq join(' ',sort keys %x);
-
-               print $fh $c, ' ', join(' ', sort keys %$p),"\n";
-       }
-       if ($comments->{'END'}) {
-               print $fh $_ foreach @{$comments->{'END'}};
-       }
-       close $fh or croak $!;
-}
-
-sub read_url_paths_all {
-       my ($l_map, $pfx, $p) = @_;
-       my @dir;
-       foreach (<$p/*>) {
-               if (-r "$_/info/url") {
-                       $pfx .= '/' if $pfx && $pfx !~ m!/$!;
-                       my $id = $pfx . basename $_;
-                       my $url = file_to_s("$_/info/url");
-                       my ($u, $p) = repo_path_split($url);
-                       $l_map->{$u}->{$p} = $id;
-               } elsif (-d $_) {
-                       push @dir, $_;
-               }
-       }
-       foreach (@dir) {
-               my $x = $_;
-               $x =~ s!^\Q$GIT_DIR\E/svn/!!o;
-               read_url_paths_all($l_map, $x, $_);
-       }
-}
-
-# this one only gets ids that have been imported, not new ones
-sub read_url_paths {
-       my $l_map = {};
-       git_svn_each(sub { my $x = shift;
-                       my $url = file_to_s("$GIT_DIR/svn/$x/info/url");
-                       my ($u, $p) = repo_path_split($url);
-                       $l_map->{$u}->{$p} = $x;
-                       });
-       return $l_map;
-}
-
-sub extract_metadata {
-       my $id = shift or return (undef, undef, undef);
-       my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
-                                                       \s([a-f\d\-]+)$/x);
-       if (!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\-]+)/);
-       }
-       return ($url, $rev, $uuid);
-}
-
-sub cmt_metadata {
-       return extract_metadata((grep(/^git-svn-id: /,
-               safe_qx(qw/git-cat-file commit/, shift)))[-1]);
-}
-
-sub get_commit_time {
-       my $cmt = shift;
-       defined(my $pid = open my $fh, '-|') or croak $!;
-       if (!$pid) {
-               exec qw/git-rev-list --pretty=raw -n1/, $cmt or croak $!;
-       }
-       while (<$fh>) {
-               /^committer\s(?:.+) (\d+) ([\-\+]?\d+)$/ or next;
-               my ($s, $tz) = ($1, $2);
-               if ($tz =~ s/^\+//) {
-                       $s += tz_to_s_offset($tz);
-               } elsif ($tz =~ s/^\-//) {
-                       $s -= tz_to_s_offset($tz);
-               }
-               close $fh;
-               return $s;
-       }
-       die "Can't get commit time for commit: $cmt\n";
+sub uri_encode {
+       my ($f) = @_;
+       $f =~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;
+       $f
 }
 
-sub tz_to_s_offset {
-       my ($tz) = @_;
-       $tz =~ s/(\d\d)$//;
-       return ($1 * 60) + ($tz * 3600);
-}
-
-sub setup_pager { # translated to Perl from pager.c
-       return unless (-t *STDOUT);
-       my $pager = $ENV{PAGER};
-       if (!defined $pager) {
-               $pager = 'less';
-       } elsif (length $pager == 0 || $pager eq 'cat') {
-               return;
-       }
-       pipe my $rfd, my $wfd or return;
-       defined(my $pid = fork) or croak $!;
-       if (!$pid) {
-               open STDOUT, '>&', $wfd or croak $!;
-               return;
-       }
-       open STDIN, '<&', $rfd or croak $!;
-       $ENV{LESS} ||= '-S';
-       exec $pager or croak "Can't run pager: $!\n";;
-}
-
-sub get_author_info {
-       my ($dest, $author, $t, $tz) = @_;
-       $author =~ s/(?:^\s*|\s*$)//g;
-       $dest->{a_raw} = $author;
-       my $_a;
-       if ($_authors) {
-               $_a = $rusers{$author} || undef;
-       }
-       if (!$_a) {
-               ($_a) = ($author =~ /<([^>]+)\@[^>]+>$/);
-       }
-       $dest->{t} = $t;
-       $dest->{tz} = $tz;
-       $dest->{a} = $_a;
-       # Date::Parse isn't in the standard Perl distro :(
-       if ($tz =~ s/^\+//) {
-               $t += tz_to_s_offset($tz);
-       } elsif ($tz =~ s/^\-//) {
-               $t -= tz_to_s_offset($tz);
-       }
-       $dest->{t_utc} = $t;
-}
-
-sub process_commit {
-       my ($c, $r_min, $r_max, $defer) = @_;
-       if (defined $r_min && defined $r_max) {
-               if ($r_min == $c->{r} && $r_min == $r_max) {
-                       show_commit($c);
-                       return 0;
-               }
-               return 1 if $r_min == $r_max;
-               if ($r_min < $r_max) {
-                       # we need to reverse the print order
-                       return 0 if (defined $_limit && --$_limit < 0);
-                       push @$defer, $c;
-                       return 1;
-               }
-               if ($r_min != $r_max) {
-                       return 1 if ($r_min < $c->{r});
-                       return 1 if ($r_max > $c->{r});
-               }
-       }
-       return 0 if (defined $_limit && --$_limit < 0);
-       show_commit($c);
-       return 1;
-}
-
-sub show_commit {
-       my $c = shift;
-       if ($_oneline) {
-               my $x = "\n";
-               if (my $l = $c->{l}) {
-                       while ($l->[0] =~ /^\s*$/) { shift @$l }
-                       $x = $l->[0];
-               }
-               $_l_fmt ||= 'A' . length($c->{r});
-               print 'r',pack($_l_fmt, $c->{r}),' | ';
-               print "$c->{c} | " if $_show_commit;
-               print $x;
-       } else {
-               show_commit_normal($c);
-       }
-}
-
-sub show_commit_changed_paths {
-       my ($c) = @_;
-       return unless $c->{changed};
-       print "Changed paths:\n", @{$c->{changed}};
-}
-
-sub show_commit_normal {
-       my ($c) = @_;
-       print '-' x72, "\nr$c->{r} | ";
-       print "$c->{c} | " if $_show_commit;
-       print "$c->{a} | ", strftime("%Y-%m-%d %H:%M:%S %z (%a, %d %b %Y)",
-                                localtime($c->{t_utc})), ' | ';
-       my $nr_line = 0;
-
-       if (my $l = $c->{l}) {
-               while ($l->[$#$l] eq "\n" && $#$l > 0
-                                         && $l->[($#$l - 1)] eq "\n") {
-                       pop @$l;
-               }
-               $nr_line = scalar @$l;
-               if (!$nr_line) {
-                       print "1 line\n\n\n";
-               } else {
-                       if ($nr_line == 1) {
-                               $nr_line = '1 line';
-                       } else {
-                               $nr_line .= ' lines';
-                       }
-                       print $nr_line, "\n";
-                       show_commit_changed_paths($c);
-                       print "\n";
-                       print $_ foreach @$l;
-               }
-       } else {
-               print "1 line\n";
-               show_commit_changed_paths($c);
-               print "\n";
-
-       }
-       foreach my $x (qw/raw diff/) {
-               if ($c->{$x}) {
-                       print "\n";
-                       print $_ foreach @{$c->{$x}}
-               }
-       }
+sub remove_username {
+       $_[0] =~ s{^([^:]*://)[^@]+@}{$1};
 }
 
-sub libsvn_load {
-       return unless $_use_lib;
-       $_use_lib = eval {
-               require SVN::Core;
-               if ($SVN::Core::VERSION lt '1.1.0') {
-                       die "Need SVN::Core 1.1.0 or better ",
-                                       "(got $SVN::Core::VERSION) ",
-                                       "Falling back to command-line svn\n";
-               }
-               require SVN::Ra;
-               require SVN::Delta;
-               push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor';
-               my $kill_stupid_warnings = $SVN::Node::none.$SVN::Node::file.
-                                       $SVN::Node::dir.$SVN::Node::unknown.
-                                       $SVN::Node::none.$SVN::Node::file.
-                                       $SVN::Node::dir.$SVN::Node::unknown.
-                                       $SVN::Auth::SSL::CNMISMATCH.
-                                       $SVN::Auth::SSL::NOTYETVALID.
-                                       $SVN::Auth::SSL::EXPIRED.
-                                       $SVN::Auth::SSL::UNKNOWNCA.
-                                       $SVN::Auth::SSL::OTHER;
-               1;
-       };
-}
+package Git::SVN::Prompt;
+use strict;
+use warnings;
+require SVN::Core;
+use vars qw/$_no_auth_cache $_username/;
 
-sub _simple_prompt {
+sub simple {
        my ($cred, $realm, $default_username, $may_save, $pool) = @_;
        $may_save = undef if $_no_auth_cache;
        $default_username = $_username if defined $_username;
        if (defined $default_username && length $default_username) {
                if (defined $realm && length $realm) {
-                       print "Authentication realm: $realm\n";
+                       print STDERR "Authentication realm: $realm\n";
+                       STDERR->flush;
                }
                $cred->username($default_username);
        } else {
-               _username_prompt($cred, $realm, $may_save, $pool);
+               username($cred, $realm, $may_save, $pool);
        }
        $cred->password(_read_password("Password for '" .
                                       $cred->username . "': ", $realm));
@@ -2706,39 +2327,41 @@ sub _simple_prompt {
        $SVN::_Core::SVN_NO_ERROR;
 }
 
-sub _ssl_server_trust_prompt {
+sub ssl_server_trust {
        my ($cred, $realm, $failures, $cert_info, $may_save, $pool) = @_;
        $may_save = undef if $_no_auth_cache;
-       print "Error validating server certificate for '$realm':\n";
+       print STDERR "Error validating server certificate for '$realm':\n";
        if ($failures & $SVN::Auth::SSL::UNKNOWNCA) {
-               print " - The certificate is not issued by a trusted ",
+               print STDERR " - The certificate is not issued by a trusted ",
                      "authority. Use the\n",
                      "   fingerprint to validate the certificate manually!\n";
        }
        if ($failures & $SVN::Auth::SSL::CNMISMATCH) {
-               print " - The certificate hostname does not match.\n";
+               print STDERR " - The certificate hostname does not match.\n";
        }
        if ($failures & $SVN::Auth::SSL::NOTYETVALID) {
-               print " - The certificate is not yet valid.\n";
+               print STDERR " - The certificate is not yet valid.\n";
        }
        if ($failures & $SVN::Auth::SSL::EXPIRED) {
-               print " - The certificate has expired.\n";
+               print STDERR " - The certificate has expired.\n";
        }
        if ($failures & $SVN::Auth::SSL::OTHER) {
-               print " - The certificate has an unknown error.\n";
+               print STDERR " - The certificate has an unknown error.\n";
        }
-       printf( "Certificate information:\n".
+       printf STDERR
+               "Certificate information:\n".
                " - Hostname: %s\n".
                " - Valid: from %s until %s\n".
                " - Issuer: %s\n".
                " - Fingerprint: %s\n",
                map $cert_info->$_, qw(hostname valid_from valid_until
-                                      issuer_dname fingerprint) );
+                                      issuer_dname fingerprint);
        my $choice;
 prompt:
-       print $may_save ?
+       print STDERR $may_save ?
              "(R)eject, accept (t)emporarily or accept (p)ermanently? " :
              "(R)eject or accept (t)emporarily? ";
+       STDERR->flush;
        $choice = lc(substr(<STDIN> || 'R', 0, 1));
        if ($choice =~ /^t$/i) {
                $cred->may_save(undef);
@@ -2753,17 +2376,18 @@ sub _ssl_server_trust_prompt {
        $SVN::_Core::SVN_NO_ERROR;
 }
 
-sub _ssl_client_cert_prompt {
+sub ssl_client_cert {
        my ($cred, $realm, $may_save, $pool) = @_;
        $may_save = undef if $_no_auth_cache;
-       print "Client certificate filename: ";
+       print STDERR "Client certificate filename: ";
+       STDERR->flush;
        chomp(my $filename = <STDIN>);
        $cred->cert_file($filename);
        $cred->may_save($may_save);
        $SVN::_Core::SVN_NO_ERROR;
 }
 
-sub _ssl_client_cert_pw_prompt {
+sub ssl_client_cert_pw {
        my ($cred, $realm, $may_save, $pool) = @_;
        $may_save = undef if $_no_auth_cache;
        $cred->password(_read_password("Password: ", $realm));
@@ -2771,17 +2395,18 @@ sub _ssl_client_cert_pw_prompt {
        $SVN::_Core::SVN_NO_ERROR;
 }
 
-sub _username_prompt {
+sub username {
        my ($cred, $realm, $may_save, $pool) = @_;
        $may_save = undef if $_no_auth_cache;
        if (defined $realm && length $realm) {
-               print "Authentication realm: $realm\n";
+               print STDERR "Authentication realm: $realm\n";
        }
        my $username;
        if (defined $_username) {
                $username = $_username;
        } else {
-               print "Username: ";
+               print STDERR "Username: ";
+               STDERR->flush;
                chomp($username = <STDIN>);
        }
        $cred->username($username);
@@ -2791,7 +2416,8 @@ sub _username_prompt {
 
 sub _read_password {
        my ($prompt, $realm) = @_;
-       print $prompt;
+       print STDERR $prompt;
+       STDERR->flush;
        require Term::ReadKey;
        Term::ReadKey::ReadMode('noecho');
        my $password = '';
@@ -2800,551 +2426,371 @@ sub _read_password {
                $password .= $key;
        }
        Term::ReadKey::ReadMode('restore');
-       print "\n";
+       print STDERR "\n";
+       STDERR->flush;
        $password;
 }
 
-sub libsvn_connect {
-       my ($url) = @_;
-       SVN::_Core::svn_config_ensure($_config_dir, undef);
-       my ($baton, $callbacks) = SVN::Core::auth_open_helper([
-           SVN::Client::get_simple_provider(),
-           SVN::Client::get_ssl_server_trust_file_provider(),
-           SVN::Client::get_simple_prompt_provider(
-             \&_simple_prompt, 2),
-           SVN::Client::get_ssl_client_cert_prompt_provider(
-             \&_ssl_client_cert_prompt, 2),
-           SVN::Client::get_ssl_client_cert_pw_prompt_provider(
-             \&_ssl_client_cert_pw_prompt, 2),
-           SVN::Client::get_username_provider(),
-           SVN::Client::get_ssl_server_trust_prompt_provider(
-             \&_ssl_server_trust_prompt),
-           SVN::Client::get_username_prompt_provider(
-             \&_username_prompt, 2),
-         ]);
-       my $ra = SVN::Ra->new(url => $url, auth => $baton,
-                             pool => SVN::Pool->new,
-                             auth_provider_callbacks => $callbacks);
-       $ra->{svn_path} = $url;
-       $ra->{repos_root} = $ra->get_repos_root;
-       $ra->{svn_path} =~ s#^\Q$ra->{repos_root}\E/*##;
-       push @repo_path_split_cache, qr/^(\Q$ra->{repos_root}\E)/;
-       return $ra;
+package main;
+
+{
+       my $kill_stupid_warnings = $SVN::Node::none.$SVN::Node::file.
+                               $SVN::Node::dir.$SVN::Node::unknown.
+                               $SVN::Node::none.$SVN::Node::file.
+                               $SVN::Node::dir.$SVN::Node::unknown.
+                               $SVN::Auth::SSL::CNMISMATCH.
+                               $SVN::Auth::SSL::NOTYETVALID.
+                               $SVN::Auth::SSL::EXPIRED.
+                               $SVN::Auth::SSL::UNKNOWNCA.
+                               $SVN::Auth::SSL::OTHER;
+}
+
+package SVN::Git::Fetcher;
+use vars qw/@ISA/;
+use strict;
+use warnings;
+use Carp qw/croak/;
+use IO::File qw//;
+use Digest::MD5;
+
+# file baton members: path, mode_a, mode_b, pool, fh, blob, base
+sub new {
+       my ($class, $git_svn) = @_;
+       my $self = SVN::Delta::Editor->new;
+       bless $self, $class;
+       $self->{c} = $git_svn->{last_commit} if exists $git_svn->{last_commit};
+       $self->{empty} = {};
+       $self->{dir_prop} = {};
+       $self->{file_prop} = {};
+       $self->{absent_dir} = {};
+       $self->{absent_file} = {};
+       $self->{gii} = $git_svn->tmp_index_do(sub { Git::IndexInfo->new });
+       $self;
 }
 
-sub libsvn_dup_ra {
-       my ($ra) = @_;
-       SVN::Ra->new(map { $_ => $ra->{$_} }
-                    qw/url auth auth_provider_callbacks repos_root svn_path/);
+sub set_path_strip {
+       my ($self, $path) = @_;
+       $self->{path_strip} = qr/^\Q$path\E(\/|$)/ if length $path;
 }
 
-sub libsvn_get_file {
-       my ($gui, $f, $rev, $chg) = @_;
-       $f =~ s#^/##;
-       print "\t$chg\t$f\n" unless $_q;
+sub open_root {
+       { path => '' };
+}
 
-       my ($hash, $pid, $in, $out);
-       my $pool = SVN::Pool->new;
-       defined($pid = open3($in, $out, '>&STDERR',
-                               qw/git-hash-object -w --stdin/)) or croak $!;
-       # redirect STDOUT for SVN 1.1.x compatibility
-       open my $stdout, '>&', \*STDOUT or croak $!;
-       open STDOUT, '>&', $in or croak $!;
-       my ($r, $props) = $SVN->get_file($f, $rev, \*STDOUT, $pool);
-       $in->flush == 0 or croak $!;
-       open STDOUT, '>&', $stdout or croak $!;
-       close $in or croak $!;
-       close $stdout or croak $!;
-       $pool->clear;
-       chomp($hash = do { local $/; <$out> });
-       close $out or croak $!;
-       waitpid $pid, 0;
-       $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
-
-       my $mode = exists $props->{'svn:executable'} ? '100755' : '100644';
-       if (exists $props->{'svn:special'}) {
-               $mode = '120000';
-               my $link = `git-cat-file blob $hash`;
-               $link =~ s/^link // or die "svn:special file with contents: <",
-                                               $link, "> is not understood\n";
-               defined($pid = open3($in, $out, '>&STDERR',
-                               qw/git-hash-object -w --stdin/)) or croak $!;
-               print $in $link;
-               $in->flush == 0 or croak $!;
-               close $in or croak $!;
-               chomp($hash = do { local $/; <$out> });
-               close $out or croak $!;
-               waitpid $pid, 0;
-               $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
-       }
-       print $gui $mode,' ',$hash,"\t",$f,"\0" or croak $!;
+sub open_directory {
+       my ($self, $path, $pb, $rev) = @_;
+       { path => $path };
 }
 
-sub libsvn_log_entry {
-       my ($rev, $author, $date, $msg, $parents) = @_;
-       my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
-                                        (\d\d)\:(\d\d)\:(\d\d).\d+Z$/x)
-                               or die "Unable to parse date: $date\n";
-       if (defined $_authors && ! defined $users{$author}) {
-               die "Author: $author not defined in $_authors file\n";
+sub git_path {
+       my ($self, $path) = @_;
+       if ($self->{path_strip}) {
+               $path =~ s!$self->{path_strip}!! or
+                 die "Failed to strip path '$path' ($self->{path_strip})\n";
        }
-       $msg = '' if ($rev == 0 && !defined $msg);
-       return { revision => $rev, date => "+0000 $Y-$m-$d $H:$M:$S",
-               author => $author, msg => $msg."\n", parents => $parents || [] }
+       $path;
 }
 
-sub process_rm {
-       my ($gui, $last_commit, $f) = @_;
+sub delete_entry {
+       my ($self, $path, $rev, $pb) = @_;
+
+       my $gpath = $self->git_path($path);
+       return undef if ($gpath eq '');
+
        # remove entire directories.
-       if (safe_qx('git-ls-tree',$last_commit,'--',$f) =~ /^040000 tree/) {
-               defined(my $pid = open my $ls, '-|') or croak $!;
-               if (!$pid) {
-                       exec(qw/git-ls-tree -r --name-only -z/,
-                               $last_commit,'--',$f) or croak $!;
-               }
+       if (command('ls-tree', $self->{c}, '--', $gpath) =~ /^040000 tree/) {
+               my ($ls, $ctx) = command_output_pipe(qw/ls-tree
+                                                    -r --name-only -z/,
+                                                    $self->{c}, '--', $gpath);
                local $/ = "\0";
                while (<$ls>) {
-                       print $gui '0 ',0 x 40,"\t",$_ or croak $!;
+                       chomp;
+                       $self->{gii}->remove($_);
+                       print "\tD\t$_\n" unless $::_q;
                }
-               close $ls or croak $?;
+               print "\tD\t$gpath/\n" unless $::_q;
+               command_close_pipe($ls, $ctx);
+               $self->{empty}->{$path} = 0
        } else {
-               print $gui '0 ',0 x 40,"\t",$f,"\0" or croak $!;
+               $self->{gii}->remove($gpath);
+               print "\tD\t$gpath\n" unless $::_q;
        }
+       undef;
 }
 
-sub libsvn_fetch {
-       my ($last_commit, $paths, $rev, $author, $date, $msg) = @_;
-       open my $gui, '| git-update-index -z --index-info' or croak $!;
-       my @amr;
-       my $p = $SVN->{svn_path};
-       foreach my $f (keys %$paths) {
-               my $m = $paths->{$f}->action();
-               if (length $p) {
-                       $f =~ s#^/\Q$p\E/##;
-                       next if $f =~ m#^/#;
-               } else {
-                       $f =~ s#^/##;
-               }
-               if ($m =~ /^[DR]$/) {
-                       print "\t$m\t$f\n" unless $_q;
-                       process_rm($gui, $last_commit, $f);
-                       next if $m eq 'D';
-                       # 'R' can be file replacements, too, right?
-               }
-               my $pool = SVN::Pool->new;
-               my $t = $SVN->check_path($f, $rev, $pool);
-               if ($t == $SVN::Node::file) {
-                       if ($m =~ /^[AMR]$/) {
-                               push @amr, [ $m, $f ];
-                       } else {
-                               die "Unrecognized action: $m, ($f r$rev)\n";
-                       }
-               } elsif ($t == $SVN::Node::dir && $m =~ /^[AR]$/) {
-                       my @traversed = ();
-                       libsvn_traverse($gui, '', $f, $rev, \@traversed);
-                       foreach (@traversed) {
-                               push @amr, [ $m, $_ ]
-                       }
-               }
-               $pool->clear;
+sub open_file {
+       my ($self, $path, $pb, $rev) = @_;
+       my $gpath = $self->git_path($path);
+       my ($mode, $blob) = (command('ls-tree', $self->{c}, '--', $gpath)
+                            =~ /^(\d{6}) blob ([a-f\d]{40})\t/);
+       unless (defined $mode && defined $blob) {
+               die "$path was not found in commit $self->{c} (r$rev)\n";
        }
-       foreach (@amr) {
-               libsvn_get_file($gui, $_->[1], $rev, $_->[0]);
-       }
-       close $gui or croak $?;
-       return libsvn_log_entry($rev, $author, $date, $msg, [$last_commit]);
+       { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob,
+         pool => SVN::Pool->new, action => 'M' };
 }
 
-sub svn_grab_base_rev {
-       defined(my $pid = open my $fh, '-|') or croak $!;
-       if (!$pid) {
-               open my $null, '>', '/dev/null' or croak $!;
-               open STDERR, '>&', $null or croak $!;
-               exec qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0"
-                                                               or croak $!;
-       }
-       chomp(my $c = do { local $/; <$fh> });
-       close $fh;
-       if (defined $c && length $c) {
-               my ($url, $rev, $uuid) = cmt_metadata($c);
-               return ($rev, $c) if defined $rev;
-       }
-       if ($_no_metadata) {
-               my $offset = -41; # from tail
-               my $rl;
-               open my $fh, '<', $REVDB or
-                       die "--no-metadata specified and $REVDB not readable\n";
-               seek $fh, $offset, 2;
-               $rl = readline $fh;
-               defined $rl or return (undef, undef);
-               chomp $rl;
-               while ($c ne $rl && tell $fh != 0) {
-                       $offset -= 41;
-                       seek $fh, $offset, 2;
-                       $rl = readline $fh;
-                       defined $rl or return (undef, undef);
-                       chomp $rl;
-               }
-               my $rev = tell $fh;
-               croak $! if ($rev < -1);
-               $rev =  ($rev - 41) / 41;
-               close $fh or croak $!;
-               return ($rev, $c);
-       }
-       return (undef, undef);
+sub add_file {
+       my ($self, $path, $pb, $cp_path, $cp_rev) = @_;
+       my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
+       delete $self->{empty}->{$dir};
+       { path => $path, mode_a => 100644, mode_b => 100644,
+         pool => SVN::Pool->new, action => 'A' };
 }
 
-sub libsvn_parse_revision {
-       my $base = shift;
-       my $head = $SVN->get_latest_revnum();
-       if (!defined $_revision || $_revision eq 'BASE:HEAD') {
-               return ($base + 1, $head) if (defined $base);
-               return (0, $head);
-       }
-       return ($1, $2) if ($_revision =~ /^(\d+):(\d+)$/);
-       return ($_revision, $_revision) if ($_revision =~ /^\d+$/);
-       if ($_revision =~ /^BASE:(\d+)$/) {
-               return ($base + 1, $1) if (defined $base);
-               return (0, $head);
-       }
-       return ($1, $head) if ($_revision =~ /^(\d+):HEAD$/);
-       die "revision argument: $_revision not understood by git-svn\n",
-               "Try using the command-line svn client instead\n";
+sub add_directory {
+       my ($self, $path, $cp_path, $cp_rev) = @_;
+       my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
+       delete $self->{empty}->{$dir};
+       $self->{empty}->{$path} = 1;
+       { path => $path };
 }
 
-sub libsvn_traverse {
-       my ($gui, $pfx, $path, $rev, $files) = @_;
-       my $cwd = length $pfx ? "$pfx/$path" : $path;
-       my $pool = SVN::Pool->new;
-       $cwd =~ s#^\Q$SVN->{svn_path}\E##;
-       my ($dirent, $r, $props) = $SVN->get_dir($cwd, $rev, $pool);
-       foreach my $d (keys %$dirent) {
-               my $t = $dirent->{$d}->kind;
-               if ($t == $SVN::Node::dir) {
-                       libsvn_traverse($gui, $cwd, $d, $rev, $files);
-               } elsif ($t == $SVN::Node::file) {
-                       my $file = "$cwd/$d";
-                       if (defined $files) {
-                               push @$files, $file;
-                       } else {
-                               libsvn_get_file($gui, $file, $rev, 'A');
-                       }
-               }
-       }
-       $pool->clear;
+sub change_dir_prop {
+       my ($self, $db, $prop, $value) = @_;
+       $self->{dir_prop}->{$db->{path}} ||= {};
+       $self->{dir_prop}->{$db->{path}}->{$prop} = $value;
+       undef;
 }
 
-sub libsvn_traverse_ignore {
-       my ($fh, $path, $r) = @_;
-       $path =~ s#^/+##g;
-       my $pool = SVN::Pool->new;
-       my ($dirent, undef, $props) = $SVN->get_dir($path, $r, $pool);
-       my $p = $path;
-       $p =~ s#^\Q$SVN->{svn_path}\E/##;
-       print $fh length $p ? "\n# $p\n" : "\n# /\n";
-       if (my $s = $props->{'svn:ignore'}) {
-               $s =~ s/[\r\n]+/\n/g;
-               chomp $s;
-               if (length $p == 0) {
-                       $s =~ s#\n#\n/$p#g;
-                       print $fh "/$s\n";
-               } else {
-                       $s =~ s#\n#\n/$p/#g;
-                       print $fh "/$p/$s\n";
-               }
-       }
-       foreach (sort keys %$dirent) {
-               next if $dirent->{$_}->kind != $SVN::Node::dir;
-               libsvn_traverse_ignore($fh, "$path/$_", $r);
-       }
-       $pool->clear;
+sub absent_directory {
+       my ($self, $path, $pb) = @_;
+       $self->{absent_dir}->{$pb->{path}} ||= [];
+       push @{$self->{absent_dir}->{$pb->{path}}}, $path;
+       undef;
 }
 
-sub revisions_eq {
-       my ($path, $r0, $r1) = @_;
-       return 1 if $r0 == $r1;
-       my $nr = 0;
-       if ($_use_lib) {
-               # should be OK to use Pool here (r1 - r0) should be small
-               my $pool = SVN::Pool->new;
-               libsvn_get_log($SVN, [$path], $r0, $r1,
-                               0, 1, 1, sub {$nr++}, $pool);
-               $pool->clear;
-       } else {
-               my ($url, undef) = repo_path_split($SVN_URL);
-               my $svn_log = svn_log_raw("$url/$path","-r$r0:$r1");
-               while (next_log_entry($svn_log)) { $nr++ }
-               close $svn_log->{fh};
-       }
-       return 0 if ($nr > 1);
-       return 1;
+sub absent_file {
+       my ($self, $path, $pb) = @_;
+       $self->{absent_file}->{$pb->{path}} ||= [];
+       push @{$self->{absent_file}->{$pb->{path}}}, $path;
+       undef;
 }
 
-sub libsvn_find_parent_branch {
-       my ($paths, $rev, $author, $date, $msg) = @_;
-       my $svn_path = '/'.$SVN->{svn_path};
-
-       # look for a parent from another branch:
-       my $i = $paths->{$svn_path} or return;
-       my $branch_from = $i->copyfrom_path or return;
-       my $r = $i->copyfrom_rev;
-       print STDERR  "Found possible branch point: ",
-                               "$branch_from => $svn_path, $r\n";
-       $branch_from =~ s#^/##;
-       my $l_map = {};
-       read_url_paths_all($l_map, '', "$GIT_DIR/svn");
-       my $url = $SVN->{repos_root};
-       defined $l_map->{$url} or return;
-       my $id = $l_map->{$url}->{$branch_from};
-       if (!defined $id && $_follow_parent) {
-               print STDERR "Following parent: $branch_from\@$r\n";
-               # auto create a new branch and follow it
-               $id = basename($branch_from);
-               $id .= '@'.$r if -r "$GIT_DIR/svn/$id";
-               while (-r "$GIT_DIR/svn/$id") {
-                       # just grow a tail if we're not unique enough :x
-                       $id .= '-';
-               }
-       }
-       return unless defined $id;
-
-       my ($r0, $parent) = find_rev_before($r,$id,1);
-       if ($_follow_parent && (!defined $r0 || !defined $parent)) {
-               defined(my $pid = fork) or croak $!;
+sub change_file_prop {
+       my ($self, $fb, $prop, $value) = @_;
+       if ($prop eq 'svn:executable') {
+               if ($fb->{mode_b} != 120000) {
+                       $fb->{mode_b} = defined $value ? 100755 : 100644;
+               }
+       } elsif ($prop eq 'svn:special') {
+               $fb->{mode_b} = defined $value ? 120000 : 100644;
+       } else {
+               $self->{file_prop}->{$fb->{path}} ||= {};
+               $self->{file_prop}->{$fb->{path}}->{$prop} = $value;
+       }
+       undef;
+}
+
+sub apply_textdelta {
+       my ($self, $fb, $exp) = @_;
+       my $fh = IO::File->new_tmpfile;
+       $fh->autoflush(1);
+       # $fh gets auto-closed() by SVN::TxDelta::apply(),
+       # (but $base does not,) so dup() it for reading in close_file
+       open my $dup, '<&', $fh or croak $!;
+       my $base = IO::File->new_tmpfile;
+       $base->autoflush(1);
+       if ($fb->{blob}) {
+               defined (my $pid = fork) or croak $!;
                if (!$pid) {
-                       $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
-                       init_vars();
-                       $SVN_URL = "$url/$branch_from";
-                       $SVN = undef;
-                       setup_git_svn();
-                       # we can't assume SVN_URL exists at r+1:
-                       $_revision = "0:$r";
-                       fetch_lib();
-                       exit 0;
+                       open STDOUT, '>&', $base or croak $!;
+                       print STDOUT 'link ' if ($fb->{mode_a} == 120000);
+                       exec qw/git-cat-file blob/, $fb->{blob} or croak $!;
                }
                waitpid $pid, 0;
                croak $? if $?;
-               ($r0, $parent) = find_rev_before($r,$id,1);
-       }
-       return unless (defined $r0 && defined $parent);
-       if (revisions_eq($branch_from, $r0, $r)) {
-               unlink $GIT_SVN_INDEX;
-               print STDERR "Found branch parent: ($GIT_SVN) $parent\n";
-               sys(qw/git-read-tree/, $parent);
-               return libsvn_fetch($parent, $paths, $rev,
-                                       $author, $date, $msg);
-       }
-       print STDERR "Nope, branch point not imported or unknown\n";
-       return undef;
-}
 
-sub libsvn_get_log {
-       my ($ra, @args) = @_;
-       if ($SVN::Core::VERSION le '1.2.0') {
-               splice(@args, 3, 1);
+               if (defined $exp) {
+                       seek $base, 0, 0 or croak $!;
+                       my $md5 = Digest::MD5->new;
+                       $md5->addfile($base);
+                       my $got = $md5->hexdigest;
+                       die "Checksum mismatch: $fb->{path} $fb->{blob}\n",
+                           "expected: $exp\n",
+                           "     got: $got\n" if ($got ne $exp);
+               }
        }
-       $ra->get_log(@args);
-}
-
-sub libsvn_new_tree {
-       if (my $log_entry = libsvn_find_parent_branch(@_)) {
-               return $log_entry;
+       seek $base, 0, 0 or croak $!;
+       $fb->{fh} = $dup;
+       $fb->{base} = $base;
+       [ SVN::TxDelta::apply($base, $fh, undef, $fb->{path}, $fb->{pool}) ];
+}
+
+sub close_file {
+       my ($self, $fb, $exp) = @_;
+       my $hash;
+       my $path = $self->git_path($fb->{path});
+       if (my $fh = $fb->{fh}) {
+               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) {
+                       sysread($fh, my $buf, 5) == 5 or croak $!;
+                       $buf eq 'link ' or die "$path has mode 120000",
+                                              "but is not a link\n";
+               }
+               defined(my $pid = open my $out,'-|') or die "Can't fork: $!\n";
+               if (!$pid) {
+                       open STDIN, '<&', $fh or croak $!;
+                       exec qw/git-hash-object -w --stdin/ or croak $!;
+               }
+               chomp($hash = do { local $/; <$out> });
+               close $out or croak $!;
+               close $fh or croak $!;
+               $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n";
+               close $fb->{base} or croak $!;
+       } else {
+               $hash = $fb->{blob} or die "no blob information\n";
        }
-       my ($paths, $rev, $author, $date, $msg) = @_;
-       open my $gui, '| git-update-index -z --index-info' or croak $!;
-       libsvn_traverse($gui, '', $SVN->{svn_path}, $rev);
-       close $gui or croak $?;
-       return libsvn_log_entry($rev, $author, $date, $msg);
+       $fb->{pool}->clear;
+       $self->{gii}->update($fb->{mode_b}, $hash, $path) or croak $!;
+       print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $::_q;
+       undef;
 }
 
-sub find_graft_path_commit {
-       my ($tree_paths, $p1, $r1) = @_;
-       foreach my $x (keys %$tree_paths) {
-               next unless ($p1 =~ /^\Q$x\E/);
-               my $i = $tree_paths->{$x};
-               my ($r0, $parent) = find_rev_before($r1,$i,1);
-               return $parent if (defined $r0 && $r0 == $r1);
-               print STDERR "r$r1 of $i not imported\n";
-               next;
-       }
-       return undef;
+sub abort_edit {
+       my $self = shift;
+       $self->{nr} = $self->{gii}->{nr};
+       delete $self->{gii};
+       $self->SUPER::abort_edit(@_);
 }
 
-sub find_graft_path_parents {
-       my ($grafts, $tree_paths, $c, $p0, $r0) = @_;
-       foreach my $x (keys %$tree_paths) {
-               next unless ($p0 =~ /^\Q$x\E/);
-               my $i = $tree_paths->{$x};
-               my ($r, $parent) = find_rev_before($r0, $i, 1);
-               if (defined $r && defined $parent && revisions_eq($x,$r,$r0)) {
-                       my ($url_b, undef, $uuid_b) = cmt_metadata($c);
-                       my ($url_a, undef, $uuid_a) = cmt_metadata($parent);
-                       next if ($url_a && $url_b && $url_a eq $url_b &&
-                                                       $uuid_b eq $uuid_a);
-                       $grafts->{$c}->{$parent} = 1;
-               }
-       }
+sub close_edit {
+       my $self = shift;
+       $self->{git_commit_ok} = 1;
+       $self->{nr} = $self->{gii}->{nr};
+       delete $self->{gii};
+       $self->SUPER::close_edit(@_);
 }
 
-sub libsvn_graft_file_copies {
-       my ($grafts, $tree_paths, $path, $paths, $rev) = @_;
-       foreach (keys %$paths) {
-               my $i = $paths->{$_};
-               my ($m, $p0, $r0) = ($i->action, $i->copyfrom_path,
-                                       $i->copyfrom_rev);
-               next unless (defined $p0 && defined $r0);
+package SVN::Git::Editor;
+use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;
+use strict;
+use warnings;
+use Carp qw/croak/;
+use IO::File;
+use Digest::MD5;
 
-               my $p1 = $_;
-               $p1 =~ s#^/##;
-               $p0 =~ s#^/##;
-               my $c = find_graft_path_commit($tree_paths, $p1, $rev);
-               next unless $c;
-               find_graft_path_parents($grafts, $tree_paths, $c, $p0, $r0);
+sub new {
+       my ($class, $opts) = @_;
+       foreach (qw/svn_path r ra tree_a tree_b log editor_cb/) {
+               die "$_ required!\n" unless (defined $opts->{$_});
        }
-}
-
-sub set_index {
-       my $old = $ENV{GIT_INDEX_FILE};
-       $ENV{GIT_INDEX_FILE} = shift;
-       return $old;
-}
 
-sub restore_index {
-       my ($old) = @_;
-       if (defined $old) {
-               $ENV{GIT_INDEX_FILE} = $old;
-       } else {
-               delete $ENV{GIT_INDEX_FILE};
+       my $pool = SVN::Pool->new;
+       my $mods = generate_diff($opts->{tree_a}, $opts->{tree_b});
+       my $types = check_diff_paths($opts->{ra}, $opts->{svn_path},
+                                    $opts->{r}, $mods);
+
+       # $opts->{ra} functions should not be used after this:
+       my @ce  = $opts->{ra}->get_commit_editor($opts->{log},
+                                               $opts->{editor_cb}, $pool);
+       my $self = SVN::Delta::Editor->new(@ce, $pool);
+       bless $self, $class;
+       foreach (qw/svn_path r tree_a tree_b/) {
+               $self->{$_} = $opts->{$_};
        }
+       $self->{url} = $opts->{ra}->{url};
+       $self->{mods} = $mods;
+       $self->{types} = $types;
+       $self->{pool} = $pool;
+       $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) };
+       $self->{rm} = { };
+       $self->{path_prefix} = length $self->{svn_path} ?
+                              "$self->{svn_path}/" : '';
+       return $self;
 }
 
-sub libsvn_commit_cb {
-       my ($rev, $date, $committer, $c, $msg, $r_last, $cmt_last) = @_;
-       if ($_optimize_commits && $rev == ($r_last + 1)) {
-               my $log = libsvn_log_entry($rev,$committer,$date,$msg);
-               $log->{tree} = get_tree_from_treeish($c);
-               my $cmt = git_commit($log, $cmt_last, $c);
-               my @diff = safe_qx('git-diff-tree', $cmt, $c);
-               if (@diff) {
-                       print STDERR "Trees differ: $cmt $c\n",
-                                       join('',@diff),"\n";
-                       exit 1;
-               }
+sub generate_diff {
+       my ($tree_a, $tree_b) = @_;
+       my @diff_tree = qw(diff-tree -z -r);
+       if ($_cp_similarity) {
+               push @diff_tree, "-C$_cp_similarity";
        } else {
-               fetch("$rev=$c");
+               push @diff_tree, '-C';
        }
-}
-
-sub libsvn_ls_fullurl {
-       my $fullurl = shift;
-       $SVN ||= libsvn_connect($fullurl);
-       my @ret;
-       my $pool = SVN::Pool->new;
-       my ($dirent, undef, undef) = $SVN->get_dir($SVN->{svn_path},
-                                               $SVN->get_latest_revnum, $pool);
-       foreach my $d (keys %$dirent) {
-               if ($dirent->{$d}->kind == $SVN::Node::dir) {
-                       push @ret, "$d/"; # add '/' for compat with cli svn
+       push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
+       push @diff_tree, "-l$_rename_limit" if defined $_rename_limit;
+       push @diff_tree, $tree_a, $tree_b;
+       my ($diff_fh, $ctx) = command_output_pipe(@diff_tree);
+       local $/ = "\0";
+       my $state = 'meta';
+       my @mods;
+       while (<$diff_fh>) {
+               chomp $_; # this gets rid of the trailing "\0"
+               if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
+                                       $::sha1\s($::sha1)\s
+                                       ([MTCRAD])\d*$/xo) {
+                       push @mods, {   mode_a => $1, mode_b => $2,
+                                       sha1_b => $3, chg => $4 };
+                       if ($4 =~ /^(?:C|R)$/) {
+                               $state = 'file_a';
+                       } else {
+                               $state = 'file_b';
+                       }
+               } elsif ($state eq 'file_a') {
+                       my $x = $mods[$#mods] or croak "Empty array\n";
+                       if ($x->{chg} !~ /^(?:C|R)$/) {
+                               croak "Error parsing $_, $x->{chg}\n";
+                       }
+                       $x->{file_a} = $_;
+                       $state = 'file_b';
+               } elsif ($state eq 'file_b') {
+                       my $x = $mods[$#mods] or croak "Empty array\n";
+                       if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
+                               croak "Error parsing $_, $x->{chg}\n";
+                       }
+                       if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
+                               croak "Error parsing $_, $x->{chg}\n";
+                       }
+                       $x->{file_b} = $_;
+                       $state = 'meta';
+               } else {
+                       croak "Error parsing $_\n";
                }
        }
-       $pool->clear;
-       return @ret;
+       command_close_pipe($diff_fh, $ctx);
+       \@mods;
 }
 
+sub check_diff_paths {
+       my ($ra, $pfx, $rev, $mods) = @_;
+       my %types;
+       $pfx .= '/' if length $pfx;
 
-sub libsvn_skip_unknown_revs {
-       my $err = shift;
-       my $errno = $err->apr_err();
-       # Maybe the branch we're tracking didn't
-       # exist when the repo started, so it's
-       # not an error if it doesn't, just continue
-       #
-       # Wonderfully consistent library, eh?
-       # 160013 - svn:// and file://
-       # 175002 - http(s)://
-       # 175007 - http(s):// (this repo required authorization, too...)
-       #   More codes may be discovered later...
-       if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
-               return;
-       }
-       croak "Error from SVN, ($errno): ", $err->expanded_message,"\n";
-};
-
-# Tie::File seems to be prone to offset errors if revisions get sparse,
-# it's not that fast, either.  Tie::File is also not in Perl 5.6.  So
-# one of my favorite modules is out :<  Next up would be one of the DBM
-# modules, but I'm not sure which is most portable...  So I'll just
-# go with something that's plain-text, but still capable of
-# being randomly accessed.  So here's my ultra-simple fixed-width
-# database.  All records are 40 characters + "\n", so it's easy to seek
-# to a revision: (41 * rev) is the byte offset.
-# A record of 40 0s denotes an empty revision.
-# And yes, it's still pretty fast (faster than Tie::File).
-sub revdb_set {
-       my ($file, $rev, $commit) = @_;
-       length $commit == 40 or croak "arg3 must be a full SHA1 hexsum\n";
-       open my $fh, '+<', $file or croak $!;
-       my $offset = $rev * 41;
-       # assume that append is the common case:
-       seek $fh, 0, 2 or croak $!;
-       my $pos = tell $fh;
-       if ($pos < $offset) {
-               print $fh (('0' x 40),"\n") x (($offset - $pos) / 41);
-       }
-       seek $fh, $offset, 0 or croak $!;
-       print $fh $commit,"\n";
-       close $fh or croak $!;
-}
-
-sub revdb_get {
-       my ($file, $rev) = @_;
-       my $ret;
-       my $offset = $rev * 41;
-       open my $fh, '<', $file or croak $!;
-       seek $fh, $offset, 0;
-       if (tell $fh == $offset) {
-               $ret = readline $fh;
-               if (defined $ret) {
-                       chomp $ret;
-                       $ret = undef if ($ret =~ /^0{40}$/);
+       sub type_diff_paths {
+               my ($ra, $types, $path, $rev) = @_;
+               my @p = split m#/+#, $path;
+               my $c = shift @p;
+               unless (defined $types->{$c}) {
+                       $types->{$c} = $ra->check_path($c, $rev);
+               }
+               while (@p) {
+                       $c .= '/' . shift @p;
+                       next if defined $types->{$c};
+                       $types->{$c} = $ra->check_path($c, $rev);
                }
        }
-       close $fh or croak $!;
-       return $ret;
-}
-
-sub copy_remote_ref {
-       my $origin = $_cp_remote ? $_cp_remote : 'origin';
-       my $ref = "refs/remotes/$GIT_SVN";
-       if (safe_qx('git-ls-remote', $origin, $ref)) {
-               sys(qw/git fetch/, $origin, "$ref:$ref");
-       } elsif ($_cp_remote && !$_upgrade) {
-               die "Unable to find remote reference: ",
-                               "refs/remotes/$GIT_SVN on $origin\n";
-       }
-}
-
-package SVN::Git::Editor;
-use vars qw/@ISA/;
-use strict;
-use warnings;
-use Carp qw/croak/;
-use IO::File;
 
-sub new {
-       my $class = shift;
-       my $git_svn = shift;
-       my $self = SVN::Delta::Editor->new(@_);
-       bless $self, $class;
-       foreach (qw/svn_path c r ra /) {
-               die "$_ required!\n" unless (defined $git_svn->{$_});
-               $self->{$_} = $git_svn->{$_};
+       foreach my $m (@$mods) {
+               foreach my $f (qw/file_a file_b/) {
+                       next unless defined $m->{$f};
+                       my ($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);
+                       if (length $pfx.$dir && ! defined $types{$dir}) {
+                               type_diff_paths($ra, \%types, $pfx.$dir, $rev);
+                       }
+               }
        }
-       $self->{pool} = SVN::Pool->new;
-       $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) };
-       $self->{rm} = { };
-       require Digest::MD5;
-       return $self;
+       \%types;
 }
 
 sub split_path {
@@ -3352,16 +2798,20 @@ sub split_path {
 }
 
 sub repo_path {
-       (defined $_[1] && length $_[1]) ? $_[1] : ''
+       my ($self, $path) = @_;
+       $self->{path_prefix}.(defined $path ? $path : '');
 }
 
 sub url_path {
        my ($self, $path) = @_;
-       $self->{ra}->{url} . '/' . $self->repo_path($path);
+       if ($self->{url} =~ m#^https?://#) {
+               $path =~ s/([^a-zA-Z0-9_.-])/uc sprintf("%%%02x",ord($1))/eg;
+       }
+       $self->{url} . '/' . $self->repo_path($path);
 }
 
 sub rmdirs {
-       my ($self, $q) = @_;
+       my ($self) = @_;
        my $rm = $self->{rm};
        delete $rm->{''}; # we never delete the url we're tracking
        return unless %$rm;
@@ -3379,10 +2829,8 @@ sub rmdirs {
        delete $rm->{''}; # we never delete the url we're tracking
        return unless %$rm;
 
-       defined(my $pid = open my $fh,'-|') or croak $!;
-       if (!$pid) {
-               exec qw/git-ls-tree --name-only -r -z/, $self->{c} or croak $!;
-       }
+       my ($fh, $ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,
+                                            $self->{tree_b});
        local $/ = "\0";
        while (<$fh>) {
                chomp;
@@ -3395,13 +2843,13 @@ sub rmdirs {
                        return;
                }
        }
-       close $fh;
+       command_close_pipe($fh, $ctx);
 
        my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat});
        foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {
                $self->close_directory($bat->{$d}, $p);
                my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#);
-               print "\tD+\t/$d/\n" unless $q;
+               print "\tD+\t$d/\n" unless $::_q;
                $self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p);
                delete $bat->{$d};
        }
@@ -3409,9 +2857,10 @@ sub rmdirs {
 
 sub open_or_add_dir {
        my ($self, $full_path, $baton) = @_;
-       my $p = SVN::Pool->new;
-       my $t = $self->{ra}->check_path($full_path, $self->{r}, $p);
-       $p->clear;
+       my $t = $self->{types}->{$full_path};
+       if (!defined $t) {
+               die "$full_path not known in r$self->{r} or we have a bug!\n";
+       }
        if ($t == $SVN::Node::none) {
                return $self->add_directory($full_path, $baton,
                                                undef, -1, $self->{pool});
@@ -3428,9 +2877,9 @@ sub open_or_add_dir {
 sub ensure_path {
        my ($self, $path) = @_;
        my $bat = $self->{bat};
-       $path = $self->repo_path($path);
-       return $bat->{''} unless (length $path);
-       my @p = split m#/+#, $path;
+       my $repo_path = $self->repo_path($path);
+       return $bat->{''} unless (length $repo_path);
+       my @p = split m#/+#, $repo_path;
        my $c = shift @p;
        $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''});
        while (@p) {
@@ -3442,23 +2891,23 @@ sub ensure_path {
 }
 
 sub A {
-       my ($self, $m, $q) = @_;
+       my ($self, $m) = @_;
        my ($dir, $file) = split_path($m->{file_b});
        my $pbat = $self->ensure_path($dir);
        my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
                                        undef, -1);
-       print "\tA\t$m->{file_b}\n" unless $q;
+       print "\tA\t$m->{file_b}\n" unless $::_q;
        $self->chg_file($fbat, $m);
        $self->close_file($fbat,undef,$self->{pool});
 }
 
 sub C {
-       my ($self, $m, $q) = @_;
+       my ($self, $m) = @_;
        my ($dir, $file) = split_path($m->{file_b});
        my $pbat = $self->ensure_path($dir);
        my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
                                $self->url_path($m->{file_a}), $self->{r});
-       print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $q;
+       print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
        $self->chg_file($fbat, $m);
        $self->close_file($fbat,undef,$self->{pool});
 }
@@ -3472,12 +2921,12 @@ sub delete_entry {
 }
 
 sub R {
-       my ($self, $m, $q) = @_;
+       my ($self, $m) = @_;
        my ($dir, $file) = split_path($m->{file_b});
        my $pbat = $self->ensure_path($dir);
        my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
                                $self->url_path($m->{file_a}), $self->{r});
-       print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $q;
+       print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
        $self->chg_file($fbat, $m);
        $self->close_file($fbat,undef,$self->{pool});
 
@@ -3487,12 +2936,12 @@ sub R {
 }
 
 sub M {
-       my ($self, $m, $q) = @_;
+       my ($self, $m) = @_;
        my ($dir, $file) = split_path($m->{file_b});
        my $pbat = $self->ensure_path($dir);
        my $fbat = $self->open_file($self->repo_path($m->{file_b}),
                                $pbat,$self->{r},$self->{pool});
-       print "\t$m->{chg}\t$m->{file_b}\n" unless $q;
+       print "\t$m->{chg}\t$m->{file_b}\n" unless $::_q;
        $self->chg_file($fbat, $m);
        $self->close_file($fbat,undef,$self->{pool});
 }
@@ -3543,10 +2992,10 @@ sub chg_file {
 }
 
 sub D {
-       my ($self, $m, $q) = @_;
+       my ($self, $m) = @_;
        my ($dir, $file) = split_path($m->{file_b});
        my $pbat = $self->ensure_path($dir);
-       print "\tD\t$m->{file_b}\n" unless $q;
+       print "\tD\t$m->{file_b}\n" unless $::_q;
        $self->delete_entry($m->{file_b}, $pbat);
 }
 
@@ -3554,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;
 }
@@ -3563,22 +3014,1157 @@ sub close_edit {
 sub abort_edit {
        my ($self) = @_;
        $self->SUPER::abort_edit($self->{pool});
+}
+
+sub DESTROY {
+       my $self = shift;
+       $self->SUPER::DESTROY(@_);
        $self->{pool}->clear;
 }
 
-__END__
+# this drives the editor
+sub apply_diff {
+       my ($self) = @_;
+       my $mods = $self->{mods};
+       my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
+       foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
+               my $f = $m->{chg};
+               if (defined $o{$f}) {
+                       $self->$f($m);
+               } else {
+                       fatal("Invalid change type: $f\n");
+               }
+       }
+       $self->rmdirs if $_rmdir;
+       if (@$mods == 0) {
+               $self->abort_edit;
+       } else {
+               $self->close_edit;
+       }
+       return scalar @$mods;
+}
 
-Data structures:
+package Git::SVN::Ra;
+use vars qw/@ISA $config_dir $_log_window_size/;
+use strict;
+use warnings;
+my ($ra_invalid, $can_do_switch, %ignored_err, $RA);
 
-$svn_log hashref (as returned by svn_log_raw)
-{
-       fh => file handle of the log file,
-       state => state of the log file parser (sep/msg/rev/msg_start...)
+BEGIN {
+       # enforce temporary pool usage for some simple functions
+       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];
+               };
+       }
+}
+
+sub new {
+       my ($class, $url) = @_;
+       $url =~ s!/+$!!;
+       return $RA if ($RA && $RA->{url} eq $url);
+
+       SVN::_Core::svn_config_ensure($config_dir, undef);
+       my ($baton, $callbacks) = SVN::Core::auth_open_helper([
+           SVN::Client::get_simple_provider(),
+           SVN::Client::get_ssl_server_trust_file_provider(),
+           SVN::Client::get_simple_prompt_provider(
+             \&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(
+             \&Git::SVN::Prompt::ssl_client_cert_pw, 2),
+           SVN::Client::get_username_provider(),
+           SVN::Client::get_ssl_server_trust_prompt_provider(
+             \&Git::SVN::Prompt::ssl_server_trust),
+           SVN::Client::get_username_prompt_provider(
+             \&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,
+                             auth_provider_callbacks => $callbacks);
+       $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;
 }
 
-$log_msg hashref as returned by next_log_entry($svn_log)
+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
+}
+
+sub get_log {
+       my ($self, @args) = @_;
+       my $pool = SVN::Pool->new;
+       splice(@args, 3, 1) if ($SVN::Core::VERSION le '1.2.0');
+       my $ret = $self->SUPER::get_log(@args, $pool);
+       $pool->clear;
+       $ret;
+}
+
+sub get_commit_editor {
+       my ($self, $log, $cb, $pool) = @_;
+       my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
+       $self->SUPER::get_commit_editor($log, $cb, @lock, $pool);
+}
+
+sub gs_do_update {
+       my ($self, $rev_a, $rev_b, $gs, $editor) = @_;
+       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;
+       my $reporter = $self->do_update($rev_b, (@pc ? shift @pc : ''),
+                                       1, $editor, $pool);
+       my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
+
+       # Since we can't rely on svn_ra_reparent being available, we'll
+       # just have to do some magic with set_path to make it so
+       # we only want a partial path.
+       my $sp = '';
+       my $final = join('/', @pc);
+       while (@pc) {
+               $reporter->set_path($sp, $rev_b, 0, @lock, $pool);
+               $sp .= '/' if length $sp;
+               $sp .= shift @pc;
+       }
+       die "BUG: '$sp' != '$final'\n" if ($sp ne $final);
+
+       $reporter->set_path($sp, $rev_a, $new, @lock, $pool);
+
+       $reporter->finish_report($pool);
+       $pool->clear;
+       $editor->{git_commit_ok};
+}
+
+# this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and
+# svn_ra_reparent didn't work before 1.4)
+sub gs_do_switch {
+       my ($self, $rev_a, $rev_b, $gs, $url_b, $editor) = @_;
+       my $path = $gs->{path};
+       my $pool = SVN::Pool->new;
+
+       my $full_url = $self->{url};
+       my $old_url = $full_url;
+       $full_url .= "/$path" if length $path;
+       my ($ra, $reparented);
+       if ($old_url ne $full_url) {
+               if ($old_url !~ m#^svn(\+ssh)?://#) {
+                       SVN::_Ra::svn_ra_reparent($self->{session}, $full_url,
+                                                 $pool);
+                       $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;
+       my $reporter = $ra->do_switch($rev_b, '', 1, $url_b, $editor, $pool);
+       my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
+       $reporter->set_path('', $rev_a, 0, @lock, $pool);
+       $reporter->finish_report($pool);
+
+       if ($reparented) {
+               SVN::_Ra::svn_ra_reparent($self->{session}, $old_url, $pool);
+               $self->{url} = $old_url;
+       }
+
+       $pool->clear;
+       $editor->{git_commit_ok};
+}
+
+sub longest_common_path {
+       my ($gsv, $globs) = @_;
+       my %common;
+       my $common_max = scalar @$gsv;
+
+       foreach my $gs (@$gsv) {
+               my @tmp = split m#/#, $gs->{path};
+               my $p = '';
+               foreach (@tmp) {
+                       $p .= length($p) ? "/$_" : $_;
+                       $common{$p} ||= 0;
+                       $common{$p}++;
+               }
+       }
+       $globs ||= [];
+       $common_max += scalar @$globs;
+       foreach my $glob (@$globs) {
+               my @tmp = split m#/#, $glob->{path}->{left};
+               my $p = '';
+               foreach (@tmp) {
+                       $p .= length($p) ? "/$_" : $_;
+                       $common{$p} ||= 0;
+                       $common{$p}++;
+               }
+       }
+
+       my $longest_path = '';
+       foreach (sort {length $b <=> length $a} keys %common) {
+               if ($common{$_} == $common_max) {
+                       $longest_path = $_;
+                       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;
+               my $err_handler = $SVN::Error::handler;
+               $SVN::Error::handler = sub {
+                       ($err) = @_;
+                       skip_unknown_revs($err);
+               };
+               sub _cb {
+                       my ($paths, $r, $author, $date, $log) = @_;
+                       [ dup_changed_paths($paths),
+                         { author => $author, date => $date, log => $log } ];
+               }
+               $self->get_log([$longest_path], $min, $max, 0, 1, 1,
+                              sub { $revs{$_[1]} = _cb(@_) });
+               if ($err && $max >= $head) {
+                       print STDERR "Path '$longest_path' ",
+                                    "was probably deleted:\n",
+                                    $err->expanded_message,
+                                    "\nWill attempt to follow ",
+                                    "revisions r$min .. r$max ",
+                                    "committed before the deletion\n";
+                       my $hi = $max;
+                       while (--$hi >= $min) {
+                               my $ok;
+                               $self->get_log([$longest_path], $min, $hi,
+                                              0, 1, 1, sub {
+                                              $ok ||= $_[1];
+                                              $revs{$_[1]} = _cb(@_) });
+                               if ($ok) {
+                                       print STDERR "r$min .. r$ok OK\n";
+                                       last;
+                               }
+                       }
+               }
+               $SVN::Error::handler = $err_handler;
+
+               my %exists = map { $_->{path} => $_ } @$gsv;
+               foreach my $r (sort {$a <=> $b} keys %revs) {
+                       my ($paths, $logged) = @{$revs{$r}};
+
+                       foreach my $gs ($self->match_globs(\%exists, $paths,
+                                                          $globs, $r)) {
+                               if ($gs->rev_db_max >= $r) {
+                                       next;
+                               }
+                               next unless $gs->match_paths($paths, $r);
+                               $gs->{logged_rev_props} = $logged;
+                               if (my $last_commit = $gs->last_commit) {
+                                       $gs->assert_index_clean($last_commit);
+                               }
+                               my $log_entry = $gs->do_fetch($paths, $r);
+                               if ($log_entry) {
+                                       $gs->do_git_commit($log_entry);
+                               }
+                       }
+                       foreach my $g (@$globs) {
+                               my $k = "svn-remote.$g->{remote}." .
+                                       "$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
+               foreach my $gs (@$gsv) {
+                       next if defined $gs->rev_db_get($max);
+                       $gs->rev_db_set($max, 0 x40);
+               }
+               foreach my $g (@$globs) {
+                       my $k = "svn-remote.$g->{remote}.$g->{t}-maxRev";
+                       Git::SVN::tmp_config($k, $max);
+               }
+               last if $max >= $head;
+               $min = $max + 1;
+               $max += $inc;
+               $max = $head if ($max > $head);
+       }
+}
+
+sub match_globs {
+       my ($self, $exists, $paths, $globs, $r) = @_;
+
+       sub get_dir_check {
+               my ($self, $exists, $g, $r) = @_;
+               my @x = eval { $self->get_dir($g->{path}->{left}, $r) };
+               return unless scalar @x == 3;
+               my $dirents = $x[0];
+               foreach my $de (keys %$dirents) {
+                       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} &&
+                                ($self->check_path($p, $r) !=
+                                 $SVN::Node::dir));
+                       $exists->{$p} = Git::SVN->init($self->{url}, $p, undef,
+                                        $g->{ref}->full_path($de), 1);
+               }
+       }
+       foreach my $g (@$globs) {
+               if (my $path = $paths->{"/$g->{path}->{left}"}) {
+                       if ($path->{action} =~ /^[AR]$/) {
+                               get_dir_check($self, $exists, $g, $r);
+                       }
+               }
+               foreach (keys %$paths) {
+                       if (/$g->{path}->{left_regex}/ &&
+                           !/$g->{path}->{regex}/) {
+                               next if $paths->{$_}->{action} !~ /^[AR]$/;
+                               get_dir_check($self, $exists, $g, $r);
+                       }
+                       next unless /$g->{path}->{regex}/;
+                       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);
+               }
+               my $c = '';
+               foreach (split m#/#, $g->{path}->{left}) {
+                       $c .= "/$_";
+                       next unless ($paths->{$c} &&
+                                    ($paths->{$c}->{action} =~ /^[AR]$/));
+                       get_dir_check($self, $exists, $g, $r);
+               }
+       }
+       values %$exists;
+}
+
+sub minimize_url {
+       my ($self) = @_;
+       return $self->{url} if ($self->{url} eq $self->{repos_root});
+       my $url = $self->{repos_root};
+       my @components = split(m!/!, $self->{svn_path});
+       my $c = '';
+       do {
+               $url .= "/$c" if length $c;
+               eval { (ref $self)->new($url)->get_latest_revnum };
+       } while ($@ && ($c = shift @components));
+       $url;
+}
+
+sub can_do_switch {
+       my $self = shift;
+       unless (defined $can_do_switch) {
+               my $pool = SVN::Pool->new;
+               my $rep = eval {
+                       $self->do_switch(1, '', 0, $self->{url},
+                                        SVN::Delta::Editor->new, $pool);
+               };
+               if ($@) {
+                       $can_do_switch = 0;
+               } else {
+                       $rep->abort_report($pool);
+                       $can_do_switch = 1;
+               }
+               $pool->clear;
+       }
+       $can_do_switch;
+}
+
+sub skip_unknown_revs {
+       my ($err) = @_;
+       my $errno = $err->apr_err();
+       # Maybe the branch we're tracking didn't
+       # exist when the repo started, so it's
+       # not an error if it doesn't, just continue
+       #
+       # Wonderfully consistent library, eh?
+       # 160013 - svn:// and file://
+       # 175002 - http(s)://
+       # 175007 - http(s):// (this repo required authorization, too...)
+       #   More codes may be discovered later...
+       if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
+               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";
+}
+
+# svn_log_changed_path_t objects passed to get_log are likely to be
+# overwritten even if only the refs are copied to an external variable,
+# so we should dup the structures in their entirety.  Using an externally
+# passed pool (instead of our temporary and quickly cleared pool in
+# Git::SVN::Ra) does not help matters at all...
+sub dup_changed_paths {
+       my ($paths) = @_;
+       return undef unless $paths;
+       my %ret;
+       foreach my $p (keys %$paths) {
+               my $i = $paths->{$p};
+               my %s = map { $_ => $i->$_ }
+                             qw/copyfrom_path copyfrom_rev action/;
+               $ret{$p} = \%s;
+       }
+       \%ret;
+}
+
+package Git::SVN::Log;
+use strict;
+use warnings;
+use POSIX qw/strftime/;
+use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline
+            %rusers $show_commit $incremental/;
+my $l_fmt;
+
+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 off the headers
+               shift @log while ($log[0] ne '');
+               shift @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]);
+       }
+       return defined $c->{r};
+}
+
+sub log_use_color {
+       return 1 if $color;
+       my ($dc, $dcvar);
+       $dcvar = 'color.diff';
+       $dc = `git-config --get $dcvar`;
+       if ($dc eq '') {
+               # nothing at all; fallback to "diff.color"
+               $dcvar = 'diff.color';
+               $dc = `git-config --get $dcvar`;
+       }
+       chomp($dc);
+       if ($dc eq 'auto') {
+               my $pc;
+               $pc = `git-config --get color.pager`;
+               if ($pc eq '') {
+                       # does not have it -- fallback to pager.color
+                       $pc = `git-config --bool --get pager.color`;
+               }
+               else {
+                       $pc = `git-config --bool --get color.pager`;
+                       if ($?) {
+                               $pc = 'false';
+                       }
+               }
+               chomp($pc);
+               if (-t *STDOUT || (defined $pager && $pc eq 'true')) {
+                       return ($ENV{TERM} && $ENV{TERM} ne 'dumb');
+               }
+               return 0;
+       }
+       return 0 if $dc eq 'never';
+       return 1 if $dc eq 'always';
+       chomp($dc = `git-config --bool --get $dcvar`);
+       return ($dc eq 'true');
+}
+
+sub git_svn_log_cmd {
+       my ($r_min, $r_max, @args) = @_;
+       my $head = 'HEAD';
+       my (@files, @log_opts);
+       foreach my $x (@args) {
+               if ($x eq '--' || @files) {
+                       push @files, $x;
+               } else {
+                       if (::verify_ref("$x^0")) {
+                               $head = $x;
+                       } else {
+                               push @log_opts, $x;
+                       }
+               }
+       }
+
+       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();
+       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;
+               }
+       } 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);
+               if (defined $c_min && defined $c_max) {
+                       if ($r_max > $r_max) {
+                               push @cmd, "$c_min..$c_max";
+                       } else {
+                               push @cmd, "$c_max..$c_min";
+                       }
+               } elsif ($r_max > $r_min) {
+                       push @cmd, $c_max;
+               } else {
+                       push @cmd, $c_min;
+               }
+       }
+       return (@cmd, @files);
+}
+
+# adapted from pager.c
+sub config_pager {
+       $pager ||= $ENV{GIT_PAGER} || $ENV{PAGER};
+       if (!defined $pager) {
+               $pager = 'less';
+       } elsif (length $pager == 0 || $pager eq 'cat') {
+               $pager = undef;
+       }
+}
+
+sub run_pager {
+       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) {
+               open STDOUT, '>&', $wfd or
+                                    ::fatal "Can't redirect to stdout: $!\n";
+               return;
+       }
+       open STDIN, '<&', $rfd or ::fatal "Can't redirect stdin: $!\n";
+       $ENV{LESS} ||= 'FRSX';
+       exec $pager or ::fatal "Can't run pager: $! ($pager)\n";
+}
+
+sub tz_to_s_offset {
+       my ($tz) = @_;
+       $tz =~ s/(\d\d)$//;
+       return ($1 * 60) + ($tz * 3600);
+}
+
+sub get_author_info {
+       my ($dest, $author, $t, $tz) = @_;
+       $author =~ s/(?:^\s*|\s*$)//g;
+       $dest->{a_raw} = $author;
+       my $au;
+       if ($::_authors) {
+               $au = $rusers{$author} || undef;
+       }
+       if (!$au) {
+               ($au) = ($author =~ /<([^>]+)\@[^>]+>$/);
+       }
+       $dest->{t} = $t;
+       $dest->{tz} = $tz;
+       $dest->{a} = $au;
+       # Date::Parse isn't in the standard Perl distro :(
+       if ($tz =~ s/^\+//) {
+               $t += tz_to_s_offset($tz);
+       } elsif ($tz =~ s/^\-//) {
+               $t -= tz_to_s_offset($tz);
+       }
+       $dest->{t_utc} = $t;
+}
+
+sub process_commit {
+       my ($c, $r_min, $r_max, $defer) = @_;
+       if (defined $r_min && defined $r_max) {
+               if ($r_min == $c->{r} && $r_min == $r_max) {
+                       show_commit($c);
+                       return 0;
+               }
+               return 1 if $r_min == $r_max;
+               if ($r_min < $r_max) {
+                       # we need to reverse the print order
+                       return 0 if (defined $limit && --$limit < 0);
+                       push @$defer, $c;
+                       return 1;
+               }
+               if ($r_min != $r_max) {
+                       return 1 if ($r_min < $c->{r});
+                       return 1 if ($r_max > $c->{r});
+               }
+       }
+       return 0 if (defined $limit && --$limit < 0);
+       show_commit($c);
+       return 1;
+}
+
+sub show_commit {
+       my $c = shift;
+       if ($oneline) {
+               my $x = "\n";
+               if (my $l = $c->{l}) {
+                       while ($l->[0] =~ /^\s*$/) { shift @$l }
+                       $x = $l->[0];
+               }
+               $l_fmt ||= 'A' . length($c->{r});
+               print 'r',pack($l_fmt, $c->{r}),' | ';
+               print "$c->{c} | " if $show_commit;
+               print $x;
+       } else {
+               show_commit_normal($c);
+       }
+}
+
+sub show_commit_changed_paths {
+       my ($c) = @_;
+       return unless $c->{changed};
+       print "Changed paths:\n", @{$c->{changed}};
+}
+
+sub show_commit_normal {
+       my ($c) = @_;
+       print '-' x72, "\nr$c->{r} | ";
+       print "$c->{c} | " if $show_commit;
+       print "$c->{a} | ", strftime("%Y-%m-%d %H:%M:%S %z (%a, %d %b %Y)",
+                                localtime($c->{t_utc})), ' | ';
+       my $nr_line = 0;
+
+       if (my $l = $c->{l}) {
+               while ($l->[$#$l] eq "\n" && $#$l > 0
+                                         && $l->[($#$l - 1)] eq "\n") {
+                       pop @$l;
+               }
+               $nr_line = scalar @$l;
+               if (!$nr_line) {
+                       print "1 line\n\n\n";
+               } else {
+                       if ($nr_line == 1) {
+                               $nr_line = '1 line';
+                       } else {
+                               $nr_line .= ' lines';
+                       }
+                       print $nr_line, "\n";
+                       show_commit_changed_paths($c);
+                       print "\n";
+                       print $_ foreach @$l;
+               }
+       } else {
+               print "1 line\n";
+               show_commit_changed_paths($c);
+               print "\n";
+
+       }
+       foreach my $x (qw/raw stat diff/) {
+               if ($c->{$x}) {
+                       print "\n";
+                       print $_ foreach @{$c->{$x}}
+               }
+       }
+}
+
+sub cmd_show_log {
+       my (@args) = @_;
+       my ($r_min, $r_max);
+       my $r_last = -1; # prevent dupes
+       if (defined $TZ) {
+               $ENV{TZ} = $TZ;
+       } else {
+               delete $ENV{TZ};
+       }
+       if (defined $::_revision) {
+               if ($::_revision =~ /^(\d+):(\d+)$/) {
+                       ($r_min, $r_max) = ($1, $2);
+               } elsif ($::_revision =~ /^\d+$/) {
+                       $r_min = $r_max = $::_revision;
+               } else {
+                       ::fatal "-r$::_revision is not supported, use ",
+                               "standard \'git log\' arguments instead\n";
+               }
+       }
+
+       config_pager();
+       @args = git_svn_log_cmd($r_min, $r_max, @args);
+       my $log = command_output_pipe(@args);
+       run_pager();
+       my (@k, $c, $d, $stat);
+       my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
+       while (<$log>) {
+               if (/^${esc_color}commit ($::sha1_short)/o) {
+                       my $cmt = $1;
+                       if ($c && cmt_showable($c) && $c->{r} != $r_last) {
+                               $r_last = $c->{r};
+                               process_commit($c, $r_min, $r_max, \@k) or
+                                                               goto out;
+                       }
+                       $d = undef;
+                       $c = { c => $cmt };
+               } elsif (/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {
+                       get_author_info($c, $1, $2, $3);
+               } elsif (/^${esc_color}(?:tree|parent|committer) /o) {
+                       # ignore
+               } elsif (/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {
+                       push @{$c->{raw}}, $_;
+               } elsif (/^${esc_color}[ACRMDT]\t/) {
+                       # we could add $SVN->{svn_path} here, but that requires
+                       # remote access at the moment (repo_path_split)...
+                       s#^(${esc_color})([ACRMDT])\t#$1   $2 #o;
+                       push @{$c->{changed}}, $_;
+               } elsif (/^${esc_color}diff /o) {
+                       $d = 1;
+                       push @{$c->{diff}}, $_;
+               } elsif ($d) {
+                       push @{$c->{diff}}, $_;
+               } elsif (/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*
+                         $esc_color*[\+\-]*$esc_color$/x) {
+                       $stat = 1;
+                       push @{$c->{stat}}, $_;
+               } elsif ($stat && /^ \d+ files changed, \d+ insertions/) {
+                       push @{$c->{stat}}, $_;
+                       $stat = undef;
+               } elsif (/^${esc_color}    (git-svn-id:.+)$/o) {
+                       ($c->{url}, $c->{r}, undef) = ::extract_metadata($1);
+               } elsif (s/^${esc_color}    //o) {
+                       push @{$c->{l}}, $_;
+               }
+       }
+       if ($c && defined $c->{r} && $c->{r} != $r_last) {
+               $r_last = $c->{r};
+               process_commit($c, $r_min, $r_max, \@k);
+       }
+       if (@k) {
+               my $swap = $r_max;
+               $r_max = $r_min;
+               $r_min = $swap;
+               process_commit($_, $r_min, $r_max) foreach reverse @k;
+       }
+out:
+       close $log;
+       print '-' x72,"\n" unless $incremental || $oneline;
+}
+
+package Git::SVN::Migration;
+# these version numbers do NOT correspond to actual version numbers
+# of git nor git-svn.  They are just relative.
+#
+# v0 layout: .git/$id/info/url, refs/heads/$id-HEAD
+#
+# v1 layout: .git/$id/info/url, refs/remotes/$id
+#
+# v2 layout: .git/svn/$id/info/url, refs/remotes/$id
+#
+# v3 layout: .git/svn/$id, refs/remotes/$id
+#            - info/url may remain for backwards compatibility
+#            - this is what we migrate up to this layout automatically,
+#            - this will be used by git svn init on single branches
+# v3.1 layout (auto migrated):
+#            - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink
+#              for backwards compatibility
+#
+# v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id
+#            - this is only created for newly multi-init-ed
+#              repositories.  Similar in spirit to the
+#              --use-separate-remotes option in git-clone (now default)
+#            - we do not automatically migrate to this (following
+#              the example set by core git)
+use strict;
+use warnings;
+use Carp qw/croak/;
+use File::Path qw/mkpath/;
+use File::Basename qw/dirname basename/;
+use vars qw/$_minimize/;
+
+sub migrate_from_v0 {
+       my $git_dir = $ENV{GIT_DIR};
+       return undef unless -d $git_dir;
+       my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);
+       my $migrated = 0;
+       while (<$fh>) {
+               chomp;
+               my ($id, $orig_ref) = ($_, $_);
+               next unless $id =~ s#^refs/heads/(.+)-HEAD$#$1#;
+               next unless -f "$git_dir/$id/info/url";
+               my $new_ref = "refs/remotes/$id";
+               if (::verify_ref("$new_ref^0")) {
+                       print STDERR "W: $orig_ref is probably an old ",
+                                    "branch used by an ancient version of ",
+                                    "git-svn.\n",
+                                    "However, $new_ref also exists.\n",
+                                    "We will not be able ",
+                                    "to use this branch until this ",
+                                    "ambiguity is resolved.\n";
+                       next;
+               }
+               print STDERR "Migrating from v0 layout...\n" if !$migrated;
+               print STDERR "Renaming ref: $orig_ref => $new_ref\n";
+               command_noisy('update-ref', $new_ref, $orig_ref);
+               command_noisy('update-ref', '-d', $orig_ref, $orig_ref);
+               $migrated++;
+       }
+       command_close_pipe($fh, $ctx);
+       print STDERR "Done migrating from v0 layout...\n" if $migrated;
+       $migrated;
+}
+
+sub migrate_from_v1 {
+       my $git_dir = $ENV{GIT_DIR};
+       my $migrated = 0;
+       return $migrated unless -d $git_dir;
+       my $svn_dir = "$git_dir/svn";
+
+       # just in case somebody used 'svn' as their $id at some point...
+       return $migrated if -d $svn_dir && ! -f "$svn_dir/info/url";
+
+       print STDERR "Migrating from a git-svn v1 layout...\n";
+       mkpath([$svn_dir]);
+       print STDERR "Data from a previous version of git-svn exists, but\n\t",
+                    "$svn_dir\n\t(required for this version ",
+                    "($::VERSION) of git-svn) does not. exist\n";
+       my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);
+       while (<$fh>) {
+               my $x = $_;
+               next unless $x =~ s#^refs/remotes/##;
+               chomp $x;
+               next unless -f "$git_dir/$x/info/url";
+               my $u = eval { ::file_to_s("$git_dir/$x/info/url") };
+               next unless $u;
+               my $dn = dirname("$git_dir/svn/$x");
+               mkpath([$dn]) unless -d $dn;
+               if ($x eq 'svn') { # they used 'svn' as GIT_SVN_ID:
+                       mkpath(["$git_dir/svn/svn"]);
+                       print STDERR " - $git_dir/$x/info => ",
+                                       "$git_dir/svn/$x/info\n";
+                       rename "$git_dir/$x/info", "$git_dir/svn/$x/info" or
+                              croak "$!: $x";
+                       # don't worry too much about these, they probably
+                       # don't exist with repos this old (save for index,
+                       # and we can easily regenerate that)
+                       foreach my $f (qw/unhandled.log index .rev_db/) {
+                               rename "$git_dir/$x/$f", "$git_dir/svn/$x/$f";
+                       }
+               } else {
+                       print STDERR " - $git_dir/$x => $git_dir/svn/$x\n";
+                       rename "$git_dir/$x", "$git_dir/svn/$x" or
+                              croak "$!: $x";
+               }
+               $migrated++;
+       }
+       command_close_pipe($fh, $ctx);
+       print STDERR "Done migrating from a git-svn v1 layout\n";
+       $migrated;
+}
+
+sub read_old_urls {
+       my ($l_map, $pfx, $path) = @_;
+       my @dir;
+       foreach (<$path/*>) {
+               if (-r "$_/info/url") {
+                       $pfx .= '/' if $pfx && $pfx !~ m!/$!;
+                       my $ref_id = $pfx . basename $_;
+                       my $url = ::file_to_s("$_/info/url");
+                       $l_map->{$ref_id} = $url;
+               } elsif (-d $_) {
+                       push @dir, $_;
+               }
+       }
+       foreach (@dir) {
+               my $x = $_;
+               $x =~ s!^\Q$ENV{GIT_DIR}\E/svn/!!o;
+               read_old_urls($l_map, $x, $_);
+       }
+}
+
+sub migrate_from_v2 {
+       my @cfg = command(qw/config -l/);
+       return if grep /^svn-remote\..+\.url=/, @cfg;
+       my %l_map;
+       read_old_urls(\%l_map, '', "$ENV{GIT_DIR}/svn");
+       my $migrated = 0;
+
+       foreach my $ref_id (sort keys %l_map) {
+               eval { Git::SVN->init($l_map{$ref_id}, '', undef, $ref_id) };
+               if ($@) {
+                       Git::SVN->init($l_map{$ref_id}, '', $ref_id, $ref_id);
+               }
+               $migrated++;
+       }
+       $migrated;
+}
+
+sub minimize_connections {
+       my $r = Git::SVN::read_all_remotes();
+       my $new_urls = {};
+       my $root_repos = {};
+       foreach my $repo_id (keys %$r) {
+               my $url = $r->{$repo_id}->{url} or next;
+               my $fetch = $r->{$repo_id}->{fetch} or next;
+               my $ra = Git::SVN::Ra->new($url);
+
+               # skip existing cases where we already connect to the root
+               if (($ra->{url} eq $ra->{repos_root}) ||
+                   (Git::SVN::sanitize_remote_name($ra->{repos_root}) eq
+                    $repo_id)) {
+                       $root_repos->{$ra->{url}} = $repo_id;
+                       next;
+               }
+
+               my $root_ra = Git::SVN::Ra->new($ra->{repos_root});
+               my $root_path = $ra->{url};
+               $root_path =~ s#^\Q$ra->{repos_root}\E(/|$)##;
+               foreach my $path (keys %$fetch) {
+                       my $ref_id = $fetch->{$path};
+                       my $gs = Git::SVN->new($ref_id, $repo_id, $path);
+
+                       # make sure we can read when connecting to
+                       # a higher level of a repository
+                       my ($last_rev, undef) = $gs->last_rev_commit;
+                       if (!defined $last_rev) {
+                               $last_rev = eval {
+                                       $root_ra->get_latest_revnum;
+                               };
+                               next if $@;
+                       }
+                       my $new = $root_path;
+                       $new .= length $path ? "/$path" : '';
+                       eval {
+                               $root_ra->get_log([$new], $last_rev, $last_rev,
+                                                 0, 0, 1, sub { });
+                       };
+                       next if $@;
+                       $new_urls->{$ra->{repos_root}}->{$new} =
+                               { ref_id => $ref_id,
+                                 old_repo_id => $repo_id,
+                                 old_path => $path };
+               }
+       }
+
+       my @emptied;
+       foreach my $url (keys %$new_urls) {
+               # see if we can re-use an existing [svn-remote "repo_id"]
+               # instead of creating a(n ugly) new section:
+               my $repo_id = $root_repos->{$url} ||
+                             Git::SVN::sanitize_remote_name($url);
+
+               my $fetch = $new_urls->{$url};
+               foreach my $path (keys %$fetch) {
+                       my $x = $fetch->{$path};
+                       Git::SVN->init($url, $path, $repo_id, $x->{ref_id});
+                       my $pfx = "svn-remote.$x->{old_repo_id}";
+
+                       my $old_fetch = quotemeta("$x->{old_path}:".
+                                                 "refs/remotes/$x->{ref_id}");
+                       command_noisy(qw/config --unset/,
+                                     "$pfx.fetch", '^'. $old_fetch . '$');
+                       delete $r->{$x->{old_repo_id}}->
+                              {fetch}->{$x->{old_path}};
+                       if (!keys %{$r->{$x->{old_repo_id}}->{fetch}}) {
+                               command_noisy(qw/config --unset/,
+                                             "$pfx.url");
+                               push @emptied, $x->{old_repo_id}
+                       }
+               }
+       }
+       if (@emptied) {
+               my $file = $ENV{GIT_CONFIG} || $ENV{GIT_CONFIG_LOCAL} ||
+                          "$ENV{GIT_DIR}/config";
+               print STDERR <<EOF;
+The following [svn-remote] sections in your config file ($file) are empty
+and can be safely removed:
+EOF
+               print STDERR "[svn-remote \"$_\"]\n" foreach @emptied;
+       }
+}
+
+sub migration_check {
+       migrate_from_v0();
+       migrate_from_v1();
+       migrate_from_v2();
+       minimize_connections() if $_minimize;
+}
+
+package Git::IndexInfo;
+use strict;
+use warnings;
+use Git qw/command_input_pipe command_close_pipe/;
+
+sub new {
+       my ($class) = @_;
+       my ($gui, $ctx) = command_input_pipe(qw/update-index -z --index-info/);
+       bless { gui => $gui, ctx => $ctx, nr => 0}, $class;
+}
+
+sub remove {
+       my ($self, $path) = @_;
+       if (print { $self->{gui} } '0 ', 0 x 40, "\t", $path, "\0") {
+               return ++$self->{nr};
+       }
+       undef;
+}
+
+sub update {
+       my ($self, $mode, $hash, $path) = @_;
+       if (print { $self->{gui} } $mode, ' ', $hash, "\t", $path, "\0") {
+               return ++$self->{nr};
+       }
+       undef;
+}
+
+sub DESTROY {
+       my ($self) = @_;
+       command_close_pipe($self->{gui}, $self->{ctx});
+}
+
+package Git::SVN::GlobSpec;
+use strict;
+use warnings;
+
+sub new {
+       my ($class, $glob) = @_;
+       my $re = $glob;
+       $re =~ s!/+$!!g; # no need for trailing slashes
+       my $nr = ($re =~ s!^(.*)\*(.*)$!\(\[^/\]+\)!g);
+       my ($left, $right) = ($1, $2);
+       if ($nr > 1) {
+               die "Only one '*' wildcard expansion ",
+                   "is supported (got $nr): '$glob'\n";
+       } elsif ($nr == 0) {
+               die "One '*' is needed for glob: '$glob'\n";
+       }
+       $re = quotemeta($left) . $re . quotemeta($right);
+       if (length $left && !($left =~ s!/+$!!g)) {
+               die "Missing trailing '/' on left side of: '$glob' ($left)\n";
+       }
+       if (length $right && !($right =~ s!^/+!!g)) {
+               die "Missing leading '/' on right side of: '$glob' ($right)\n";
+       }
+       my $left_re = qr/^\/\Q$left\E(\/|$)/;
+       bless { left => $left, right => $right, left_regex => $left_re,
+               regex => qr/$re/, glob => $glob }, $class;
+}
+
+sub full_path {
+       my ($self, $path) = @_;
+       return (length $self->{left} ? "$self->{left}/" : '') .
+              $path . (length $self->{right} ? "/$self->{right}" : '');
+}
+
+__END__
+
+Data structures:
+
+
+$remotes = { # returned by read_all_remotes()
+       'svn' => {
+               # svn-remote.svn.url=https://svn.musicpd.org
+               url => 'https://svn.musicpd.org',
+               # svn-remote.svn.fetch=mpd/trunk:trunk
+               fetch => {
+                       'mpd/trunk' => 'trunk',
+               },
+               # svn-remote.svn.tags=mpd/tags/*:tags/*
+               tags => {
+                       path => {
+                               left => 'mpd/tags',
+                               right => '',
+                               regex => qr!mpd/tags/([^/]+)$!,
+                               glob => 'tags/*',
+                       },
+                       ref => {
+                               left => 'tags',
+                               right => '',
+                               regex => qr!tags/([^/]+)$!,
+                               glob => 'tags/*',
+                       },
+               }
+       }
+};
+
+$log_entry hashref as returned by libsvn_log_entry()
 {
-       msg => 'whitespace-formatted log entry
+       log => 'whitespace-formatted log entry
 ',                                             # trailing newline is preserved
        revision => '8',                        # integer
        date => '2004-02-24T17:01:44.108345Z',  # commit date
@@ -3586,6 +4172,7 @@ sub abort_edit {
 };
 
 
+# this is generated by generate_diff();
 @mods = array of diff-index line hashes, each element represents one line
        of diff-index output