rebase -i: Change function make_squash_message into update_squash_message
[gitweb.git] / git-svn.perl
index b4e8d44351c4b0a5744e41def1fd0858e3bbb6a6..957d44e63004cef131fca5c7dad75e7049d15842 100755 (executable)
 $Git::SVN::default_repo_id = 'svn';
 $Git::SVN::default_ref_id = $ENV{GIT_SVN_ID} || 'git-svn';
 $Git::SVN::Ra::_log_window_size = 100;
+$Git::SVN::_minimize_url = 'unset';
+
+if (! exists $ENV{SVN_SSH}) {
+       if (exists $ENV{GIT_SSH}) {
+               $ENV{SVN_SSH} = $ENV{GIT_SSH};
+               if ($^O eq 'msys') {
+                       $ENV{SVN_SSH} =~ s/\\/\\\\/g;
+               }
+       }
+}
 
 $Git::SVN::Log::TZ = $ENV{TZ};
 $ENV{TZ} = 'UTC';
@@ -31,6 +41,7 @@
 if ($SVN::Core::VERSION lt '1.1.0') {
        fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)";
 }
+my $can_compress = eval { require Compress::Zlib; 1};
 push @Git::SVN::Ra::ISA, 'SVN::Ra';
 push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor';
 push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor';
@@ -40,6 +51,7 @@
 use File::Basename qw/dirname basename/;
 use File::Path qw/mkpath/;
 use File::Spec;
+use File::Find;
 use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
 use IPC::Open3;
 use Git;
@@ -98,7 +110,7 @@ BEGIN
                   '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,
+                  '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 },
@@ -156,6 +168,9 @@ BEGIN
                             'Create a .gitignore per svn:ignore',
                             { 'revision|r=i' => \$_revision
                             } ],
+       'mkdirs' => [ \&cmd_mkdirs ,
+                     "recreate empty directories after a checkout",
+                     { 'revision|r=i' => \$_revision } ],
         'propget' => [ \&cmd_propget,
                       'Print the value of a property on a file or directory',
                       { 'revision|r=i' => \$_revision } ],
@@ -217,6 +232,10 @@ BEGIN
                     "Undo fetches back to the specified SVN revision",
                     { 'revision|r=s' => \$_revision,
                       'parent|p' => \$_fetch_parent } ],
+       'gc' => [ \&cmd_gc,
+                 "Compress unhandled.log files in .git/svn and remove " .
+                 "index files in .git/svn",
+               {} ],
 );
 
 my $cmd;
@@ -258,7 +277,7 @@ BEGIN
 
 my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
 
-read_repo_config(\%opts);
+read_git_config(\%opts);
 if ($cmd && ($cmd eq 'log' || $cmd eq 'blame')) {
        Getopt::Long::Configure('pass_through');
 }
@@ -393,6 +412,10 @@ sub cmd_init {
        init_subdir(@_);
        do_git_init_db();
 
+       if ($Git::SVN::_minimize_url eq 'unset') {
+               $Git::SVN::_minimize_url = 0;
+       }
+
        Git::SVN->init($url);
 }
 
@@ -405,6 +428,7 @@ sub cmd_fetch {
        if (@_ > 1) {
                die "Usage: $0 fetch [--all] [--parent] [svn-remote]\n";
        }
+       $Git::SVN::no_reuse_existing = undef;
        if ($_fetch_parent) {
                my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
                unless ($gs) {
@@ -583,8 +607,15 @@ sub cmd_dcommit {
                                          "\nBefore dcommitting";
                                }
                                if ($url_ ne $expect_url) {
-                                       fatal "URL mismatch after rebase: ",
-                                             "$url_ != $expect_url";
+                                       if ($url_ eq $gs->metadata_url) {
+                                               print
+                                                 "Accepting rewritten URL:",
+                                                 " $url_\n";
+                                       } else {
+                                               fatal
+                                                 "URL mismatch after rebase:",
+                                                 " $url_ != $expect_url";
+                                       }
                                }
                                if ($uuid_ ne $uuid) {
                                        fatal "uuid mismatch after rebase: ",
@@ -655,9 +686,22 @@ sub cmd_branch {
                        }
                }
                unless (defined $glob) {
-                       die "Unknown ",
-                           $_tag ? "tag" : "branch",
-                           " destination $_branch_dest\n";
+                       my $dest_re = qr/\b\Q$_branch_dest\E\b/;
+                       foreach my $g (@{$allglobs}) {
+                               $g->{path}->{left} =~ /$dest_re/ or next;
+                               if (defined $glob) {
+                                       die "Ambiguous destination: ",
+                                           $_branch_dest, "\nmatches both '",
+                                           $glob->{path}->{left}, "' and '",
+                                           $g->{path}->{left}, "'\n";
+                               }
+                               $glob = $g;
+                       }
+                       unless (defined $glob) {
+                               die "Unknown ",
+                                   $_tag ? "tag" : "branch",
+                                   " destination $_branch_dest\n";
+                       }
                }
        }
        my ($lft, $rgt) = @{ $glob->{path} }{qw/left right/};
@@ -729,6 +773,7 @@ sub cmd_rebase {
                $_fetch_all ? $gs->fetch_all : $gs->fetch;
        }
        command_noisy(rebase_cmd(), $gs->refname);
+       $gs->mkemptydirs;
 }
 
 sub cmd_show_ignore {
@@ -740,6 +785,7 @@ sub cmd_show_ignore {
                print STDOUT "\n# $path\n";
                my $s = $props->{'svn:ignore'} or return;
                $s =~ s/[\r\n]+/\n/g;
+               $s =~ s/^\n+//;
                chomp $s;
                $s =~ s#^#$path#gm;
                print STDOUT "$s\n";
@@ -777,6 +823,7 @@ sub cmd_create_ignore {
                open(GITIGNORE, '>', $ignore)
                  or fatal("Failed to open `$ignore' for writing: $!");
                $s =~ s/[\r\n]+/\n/g;
+               $s =~ s/^\n+//;
                chomp $s;
                # Prefix all patterns so that the ignore doesn't apply
                # to sub-directories.
@@ -788,6 +835,12 @@ sub cmd_create_ignore {
        });
 }
 
+sub cmd_mkdirs {
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       $gs ||= Git::SVN->new;
+       $gs->mkemptydirs($_revision);
+}
+
 sub canonicalize_path {
        my ($path) = @_;
        my $dot_slash_added = 0;
@@ -876,10 +929,6 @@ sub cmd_multi_init {
                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 = canonicalize_url($url);
@@ -887,7 +936,7 @@ sub cmd_multi_init {
        }
        do_git_init_db();
        if (defined $_trunk) {
-               my $trunk_ref = $_prefix . 'trunk';
+               my $trunk_ref = 'refs/remotes/' . $_prefix . 'trunk';
                # try both old-style and new-style lookups:
                my $gs_trunk = eval { Git::SVN->new($trunk_ref) };
                unless ($gs_trunk) {
@@ -908,6 +957,7 @@ sub cmd_multi_init {
 }
 
 sub cmd_multi_fetch {
+       $Git::SVN::no_reuse_existing = undef;
        my $remotes = Git::SVN::read_all_remotes();
        foreach my $repo_id (sort keys %$remotes) {
                if ($remotes->{$repo_id}->{url}) {
@@ -1111,6 +1161,14 @@ sub cmd_reset {
        print "r$r = $c ($gs->{ref_id})\n";
 }
 
+sub cmd_gc {
+       if (!$can_compress) {
+               warn "Compress::Zlib could not be found; unhandled.log " .
+                    "files will not be compressed.\n";
+       }
+       find({ wanted => \&gc_directory, no_chdir => 1}, "$ENV{GIT_DIR}/svn");
+}
+
 ########################### utility functions #########################
 
 sub rebase_cmd {
@@ -1126,6 +1184,17 @@ sub post_fetch_checkout {
        my $gs = $Git::SVN::_head or return;
        return if verify_ref('refs/heads/master^0');
 
+       # look for "trunk" ref if it exists
+       my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}};
+       my $fetch = $remote->{fetch};
+       if ($fetch) {
+               foreach my $p (keys %$fetch) {
+                       basename($fetch->{$p}) eq 'trunk' or next;
+                       $gs = Git::SVN->new($fetch->{$p}, $gs->{repo_id}, $p);
+                       last;
+               }
+       }
+
        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'));
@@ -1139,6 +1208,7 @@ sub post_fetch_checkout {
        command_noisy(qw/read-tree -m -u -v HEAD HEAD/);
        print STDERR "Checked out HEAD:\n  ",
                     $gs->full_url, " r", $gs->last_rev, "\n";
+       $gs->mkemptydirs($gs->last_rev);
 }
 
 sub complete_svn_url {
@@ -1180,7 +1250,8 @@ sub complete_url_ls_init {
                    "wanted to set to: $gs->{url}\n";
        }
        command_oneline('config', $k, $gs->{url}) unless $orig_url;
-       my $remote_path = "$ra->{svn_path}/$repo_path";
+       my $remote_path = "$gs->{path}/$repo_path";
+       $remote_path =~ s{%([0-9A-F]{2})}{chr hex($1)}ieg;
        $remote_path =~ s#/+#/#g;
        $remote_path =~ s#^/##g;
        $remote_path .= "/*" if $remote_path !~ /\*/;
@@ -1263,9 +1334,8 @@ sub get_commit_entry {
        close $log_fh or croak $!;
 
        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);
+               chomp(my $editor = command_oneline(qw(var GIT_EDITOR)));
+               system('sh', '-c', $editor.' "$@"', $editor, $commit_editmsg);
        }
        rename $commit_editmsg, $commit_msg or croak $!;
        {
@@ -1332,8 +1402,7 @@ sub load_authors {
 }
 
 # convert GetOpt::Long specs for use by git-config
-sub read_repo_config {
-       return unless -d $ENV{GIT_DIR};
+sub read_git_config {
        my $opts = shift;
        my @config_only;
        foreach my $o (keys %$opts) {
@@ -1363,11 +1432,11 @@ sub read_repo_config {
 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);
+                                                       \s([a-f\d\-]+)$/ix);
        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\-]+)/);
+               ($rev, $uuid) = ($id =~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/i);
        }
        return ($url, $rev, $uuid);
 }
@@ -1531,6 +1600,25 @@ sub md5sum {
        return $md5->hexdigest();
 }
 
+sub gc_directory {
+       if ($can_compress && -f $_ && basename($_) eq "unhandled.log") {
+               my $out_filename = $_ . ".gz";
+               open my $in_fh, "<", $_ or die "Unable to open $_: $!\n";
+               binmode $in_fh;
+               my $gz = Compress::Zlib::gzopen($out_filename, "ab") or
+                               die "Unable to open $out_filename: $!\n";
+
+               my $res;
+               while ($res = sysread($in_fh, my $str, 1024)) {
+                       $gz->gzwrite($str) or
+                               die "Unable to write: ".$gz->gzerror()."!\n";
+               }
+               unlink $_ or die "unlink $File::Find::name: $!\n";
+       } elsif (-f $_ && basename($_) eq "index") {
+               unlink $_ or die "unlink $_: $!\n";
+       }
+}
+
 package Git::SVN;
 use strict;
 use warnings;
@@ -1594,23 +1682,23 @@ sub resolve_local_globs {
        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}$#;
+       foreach (command(qw#for-each-ref --format=%(refname) refs/#)) {
+               next unless m#^$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";
+                                   "existing: $existing\n",
+                                   " globbed: $refname\n";
                        }
-                       my $u = (::cmt_metadata("refs/remotes/$refname"))[0];
+                       my $u = (::cmt_metadata("$refname"))[0];
                        $u =~ s!^\Q$url\E(/|$)!! or die
-                         "refs/remotes/$refname: '$url' not found in '$u'\n";
+                         "$refname: '$url' not found in '$u'\n";
                        if ($pathname ne $u) {
                                warn "W: Refspec glob conflict ",
-                                    "(ref: refs/remotes/$refname):\n",
+                                    "(ref: $refname):\n",
                                     "expected path: $pathname\n",
                                     "    real path: $u\n",
                                     "Continuing ahead with $u\n";
@@ -1651,6 +1739,7 @@ sub fetch_all {
        my $ra = Git::SVN::Ra->new($url);
        my $uuid = $ra->get_uuid;
        my $head = $ra->get_latest_revnum;
+       $ra->get_log("", $head, 0, 1, 0, 1, sub { $head = $_[1] });
        my $base = defined $fetch ? $head : 0;
 
        # read the max revs for wildcard expansion (branches/*, tags/*)
@@ -1687,33 +1776,35 @@ sub read_all_remotes {
        my $use_svm_props = eval { command_oneline(qw/config --bool
            svn.useSvmProps/) };
        $use_svm_props = $use_svm_props eq 'true' if $use_svm_props;
+       my $svn_refspec = qr{\s*(.*?)\s*:\s*(.+?)\s*};
        foreach (grep { s/^svn-remote\.// } command(qw/config -l/)) {
-               if (m!^(.+)\.fetch=\s*(.*)\s*:\s*(.+)\s*$!) {
-                       my ($remote, $local_ref, $_remote_ref) = ($1, $2, $3);
-                       die("svn-remote.$remote: remote ref '$_remote_ref' "
-                           . "must start with 'refs/remotes/'\n")
-                               unless $_remote_ref =~ m{^refs/remotes/(.+)};
-                       my $remote_ref = $1;
-                       $local_ref =~ s{^/}{};
+               if (m!^(.+)\.fetch=$svn_refspec$!) {
+                       my ($remote, $local_ref, $remote_ref) = ($1, $2, $3);
+                       die("svn-remote.$remote: remote ref '$remote_ref' "
+                           . "must start with 'refs/'\n")
+                               unless $remote_ref =~ m{^refs/};
                        $r->{$remote}->{fetch}->{$local_ref} = $remote_ref;
                        $r->{$remote}->{svm} = {} if $use_svm_props;
                } elsif (m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {
                        $r->{$1}->{svm} = {};
                } elsif (m!^(.+)\.url=\s*(.*)\s*$!) {
                        $r->{$1}->{url} = $2;
-               } elsif (m!^(.+)\.(branches|tags)=
-                          (.*):refs/remotes/(.+)\s*$/!x) {
-                       my ($p, $g) = ($3, $4);
+               } elsif (m!^(.+)\.(branches|tags)=$svn_refspec$!) {
+                       my ($remote, $t, $local_ref, $remote_ref) =
+                                                            ($1, $2, $3, $4);
+                       die("svn-remote.$remote: remote ref '$remote_ref' ($t) "
+                           . "must start with 'refs/'\n")
+                               unless $remote_ref =~ m{^refs/};
                        my $rs = {
-                           t => $2,
-                           remote => $1,
-                           path => Git::SVN::GlobSpec->new($p),
-                           ref => Git::SVN::GlobSpec->new($g) };
+                           t => $t,
+                           remote => $remote,
+                           path => Git::SVN::GlobSpec->new($local_ref),
+                           ref => Git::SVN::GlobSpec->new($remote_ref) };
                        if (length($rs->{ref}->{right}) != 0) {
                                die "The '*' glob character must be the last ",
-                                   "character of '$g'\n";
+                                   "character of '$remote_ref'\n";
                        }
-                       push @{ $r->{$1}->{$2} }, $rs;
+                       push @{ $r->{$remote}->{$t} }, $rs;
                }
        }
 
@@ -1821,14 +1912,15 @@ sub init_remote_config {
                }
        }
        my ($xrepo_id, $xpath) = find_ref($self->refname);
-       if (defined $xpath) {
+       if (!$no_write && defined $xpath) {
                die "svn-remote.$xrepo_id.fetch already set to track ",
-                   "$xpath:refs/remotes/", $self->refname, "\n";
+                   "$xpath:", $self->refname, "\n";
        }
        unless ($no_write) {
                command_noisy('config',
                              "svn-remote.$self->{repo_id}.url", $url);
                $self->{path} =~ s{^/}{};
+               $self->{path} =~ s{%([0-9A-F]{2})}{chr hex($1)}ieg;
                command_noisy('config', '--add',
                              "svn-remote.$self->{repo_id}.fetch",
                              "$self->{path}:".$self->refname);
@@ -1898,7 +1990,7 @@ sub find_ref {
        my ($ref_id) = @_;
        foreach (command(qw/config -l/)) {
                next unless m!^svn-remote\.(.+)\.fetch=
-                             \s*(.*)\s*:\s*refs/remotes/(.+)\s*$!x;
+                             \s*(.*?)\s*:\s*(.+?)\s*$!x;
                my ($repo_id, $path, $ref) = ($1, $2, $3);
                if ($ref eq $ref_id) {
                        $path = '' if ($path =~ m#^\./?#);
@@ -1915,16 +2007,16 @@ sub new {
                if (!defined $repo_id) {
                        die "Could not find a \"svn-remote.*.fetch\" key ",
                            "in the repository configuration matching: ",
-                           "refs/remotes/$ref_id\n";
+                           "$ref_id\n";
                }
        }
        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
+                                           ":$ref_id\$") or
                     die "Failed to read \"svn-remote.$repo_id.fetch\" ",
-                        "\":refs/remotes/$ref_id\$\" in config\n";
+                        "\":$ref_id\$\" in config\n";
                ($self->{path}, undef) = split(/\s*:\s*/, $fetch);
        }
        $self->{url} = command_oneline('config', '--get',
@@ -1935,7 +2027,7 @@ sub new {
 }
 
 sub refname {
-       my ($refname) = "refs/remotes/$_[0]->{ref_id}" ;
+       my ($refname) = $_[0]->{ref_id} ;
 
        # It cannot end with a slash /, we'll throw up on this because
        # SVN can't have directories with a slash in their name, either:
@@ -2014,7 +2106,7 @@ sub _set_svm_vars {
 
                chomp($src, $uuid);
 
-               $uuid =~ m{^[0-9a-f\-]{30,}$}
+               $uuid =~ m{^[0-9a-f\-]{30,}$}i
                    or die "doesn't look right - svm:uuid is '$uuid'\n";
 
                # the '!' is used to mark the repos_root!/relative/path
@@ -2100,7 +2192,7 @@ sub svnsync {
                   die "doesn't look right - svn:sync-from-url is '$url'\n";
 
                my $uuid = tmp_config('--get', "$section.svnsync-uuid");
-               ($uuid) = ($uuid =~ m{^([0-9a-f\-]{30,})$}) or
+               ($uuid) = ($uuid =~ m{^([0-9a-f\-]{30,})$}i) or
                   die "doesn't look right - svn:sync-from-uuid is '$uuid'\n";
 
                $svnsync = { url => $url, uuid => $uuid }
@@ -2118,7 +2210,7 @@ sub svnsync {
                   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) = ($uuid =~ m{^([0-9a-f\-]{30,})$}) or
+       ($uuid) = ($uuid =~ m{^([0-9a-f\-]{30,})$}i) or
                   die "doesn't look right - svn:sync-from-uuid is '$uuid'\n";
 
        my $section = "svn-remote.$self->{repo_id}";
@@ -2134,7 +2226,7 @@ sub ra_uuid {
        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,})$/) {
+               if (!$@ && $uuid && $uuid =~ /^([a-f\d\-]{30,})$/i) {
                        $self->{ra_uuid} = $uuid;
                } else {
                        die "ra_uuid called without URL\n" unless $self->{url};
@@ -2177,16 +2269,6 @@ sub ra {
        $ra;
 }
 
-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;
-}
-
 # prop_walk(PATH, REV, SUB)
 # -------------------------
 # Recursively traverse PATH at revision REV and invoke SUB for each
@@ -2512,10 +2594,7 @@ sub match_paths {
        if (my $path = $paths->{"/$self->{path}"}) {
                return ($path->{action} eq 'D') ? 0 : 1;
        }
-       my $repos_root = $self->ra->{repos_root};
-       my $extended_path = $self->{url} . '/' . $self->{path};
-       $extended_path =~ s#^\Q$repos_root\E(/|$)##;
-       $self->{path_regex} ||= qr/^\/\Q$extended_path\E\//;
+       $self->{path_regex} ||= qr/^\/\Q$self->{path}\E\//;
        if (grep /$self->{path_regex}/, keys %$paths) {
                return 1;
        }
@@ -2545,7 +2624,7 @@ sub find_parent_branch {
        return undef unless defined $paths;
 
        # look for a parent from another branch:
-       my @b_path_components = split m#/#, $self->rel_path;
+       my @b_path_components = split m#/#, $self->{path};
        my @a_path_components;
        my $i;
        while (@b_path_components) {
@@ -2563,11 +2642,12 @@ sub find_parent_branch {
        my $r = $i->{copyfrom_rev};
        my $repos_root = $self->ra->{repos_root};
        my $url = $self->ra->{url};
-       my $new_url = $repos_root . $branch_from;
+       my $new_url = $url . $branch_from;
        print STDERR  "Found possible branch point: ",
-                     "$new_url => ", $self->full_url, ", $r\n";
+                     "$new_url => ", $self->full_url, ", $r\n"
+                     unless $::_q > 1;
        $branch_from =~ s#^/##;
-       my $gs = $self->other_gs($new_url, $url, $repos_root,
+       my $gs = $self->other_gs($new_url, $url,
                                 $branch_from, $r, $self->{ref_id});
        my ($r0, $parent) = $gs->find_rev_before($r, 1);
        {
@@ -2586,11 +2666,13 @@ sub find_parent_branch {
                ($r0, $parent) = $gs->find_rev_before($r, 1);
        }
        if (defined $r0 && defined $parent) {
-               print STDERR "Found branch parent: ($self->{ref_id}) $parent\n";
+               print STDERR "Found branch parent: ($self->{ref_id}) $parent\n"
+                            unless $::_q > 1;
                my $ed;
                if ($self->ra->can_do_switch) {
                        $self->assert_index_clean($parent);
-                       print STDERR "Following parent with do_switch\n";
+                       print STDERR "Following parent with do_switch\n"
+                                    unless $::_q > 1;
                        # 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
@@ -2605,18 +2687,20 @@ sub find_parent_branch {
                        print STDERR "Trees match:\n",
                                     "  $new_url\@$r0\n",
                                     "  ${\$self->full_url}\@$rev\n",
-                                    "Following parent with no changes\n";
+                                    "Following parent with no changes\n"
+                                    unless $::_q > 1;
                        $self->tmp_index_do(sub {
                            command_noisy('read-tree', $parent);
                        });
                        $self->{last_commit} = $parent;
                } else {
-                       print STDERR "Following parent with do_update\n";
+                       print STDERR "Following parent with do_update\n"
+                                    unless $::_q > 1;
                        $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";
+               print STDERR "Successfully followed parent\n" unless $::_q > 1;
                return $self->make_log_entry($rev, [$parent], $ed);
        }
        return undef;
@@ -2652,6 +2736,37 @@ sub do_fetch {
        $self->make_log_entry($rev, \@parents, $ed);
 }
 
+sub mkemptydirs {
+       my ($self, $r) = @_;
+       my %empty_dirs = ();
+
+       open my $fh, '<', "$self->{dir}/unhandled.log" or return;
+       binmode $fh or croak "binmode: $!";
+       while (<$fh>) {
+               if (defined $r && /^r(\d+)$/) {
+                       last if $1 > $r;
+               } elsif (/^  \+empty_dir: (.+)$/) {
+                       $empty_dirs{$1} = 1;
+               } elsif (/^  \-empty_dir: (.+)$/) {
+                       delete $empty_dirs{$1};
+               }
+       }
+       close $fh;
+
+       my $strip = qr/\A\Q$self->{path}\E(?:\/|$)/;
+       foreach my $d (sort keys %empty_dirs) {
+               $d = uri_decode($d);
+               $d =~ s/$strip//;
+               next if -d $d;
+               if (-e _) {
+                       warn "$d exists but is not a directory\n";
+               } else {
+                       print "creating empty directory: $d\n";
+                       mkpath([$d]);
+               }
+       }
+}
+
 sub get_untracked {
        my ($self, $ed) = @_;
        my @out;
@@ -2752,16 +2867,16 @@ sub parse_svn_date {
 }
 
 sub other_gs {
-       my ($self, $new_url, $url, $repos_root,
+       my ($self, $new_url, $url,
            $branch_from, $r, $old_ref_id) = @_;
-       my $gs = Git::SVN->find_by_url($new_url, $repos_root, $branch_from);
+       my $gs = Git::SVN->find_by_url($new_url, $url, $branch_from);
        unless ($gs) {
                my $ref_id = $old_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";
+               print STDERR "Initializing parent: $ref_id\n" unless $::_q > 1;
                my ($u, $p, $repo_id) = ($new_url, '', $ref_id);
                if ($u =~ s#^\Q$url\E(/|$)##) {
                        $p = $u;
@@ -2775,6 +2890,7 @@ sub other_gs {
 
 sub call_authors_prog {
        my ($orig_author) = @_;
+       $orig_author = command_oneline('rev-parse', '--sq-quote', $orig_author);
        my $author = `$::_authors_prog $orig_author`;
        if ($? != 0) {
                die "$::_authors_prog failed with exit code $?\n"
@@ -2804,14 +2920,160 @@ sub check_author {
        $author;
 }
 
+sub find_extra_svk_parents {
+       my ($self, $ed, $tickets, $parents) = @_;
+       # aha!  svk:merge property changed...
+       my @tickets = split "\n", $tickets;
+       my @known_parents;
+       for my $ticket ( @tickets ) {
+               my ($uuid, $path, $rev) = split /:/, $ticket;
+               if ( $uuid eq $self->ra_uuid ) {
+                       my $url = $self->rewrite_root || $self->{url};
+                       my $repos_root = $url;
+                       my $branch_from = $path;
+                       $branch_from =~ s{^/}{};
+                       my $gs = $self->other_gs($repos_root."/".$branch_from,
+                                                $url,
+                                                $branch_from,
+                                                $rev,
+                                                $self->{ref_id});
+                       if ( my $commit = $gs->rev_map_get($rev, $uuid) ) {
+                               # wahey!  we found it, but it might be
+                               # an old one (!)
+                               push @known_parents, $commit;
+                       }
+               }
+       }
+       for my $parent ( @known_parents ) {
+               my @cmd = ('rev-list', $parent, map { "^$_" } @$parents );
+               my ($msg_fh, $ctx) = command_output_pipe(@cmd);
+               my $new;
+               while ( <$msg_fh> ) {
+                       $new=1;last;
+               }
+               command_close_pipe($msg_fh, $ctx);
+               if ( $new ) {
+                       print STDERR
+                           "Found merge parent (svk:merge ticket): $parent\n";
+                       push @$parents, $parent;
+               }
+       }
+}
+
+# note: this function should only be called if the various dirprops
+# have actually changed
+sub find_extra_svn_parents {
+       my ($self, $ed, $mergeinfo, $parents) = @_;
+       # aha!  svk:merge property changed...
+
+       # We first search for merged tips which are not in our
+       # history.  Then, we figure out which git revisions are in
+       # that tip, but not this revision.  If all of those revisions
+       # are now marked as merge, we can add the tip as a parent.
+       my @merges = split "\n", $mergeinfo;
+       my @merge_tips;
+       my @merged_commit_ranges;
+       my $url = $self->rewrite_root || $self->{url};
+       for my $merge ( @merges ) {
+               my ($source, $revs) = split ":", $merge;
+               my $path = $source;
+               $path =~ s{^/}{};
+               my $gs = Git::SVN->find_by_url($url.$source, $url, $path);
+               if ( !$gs ) {
+                       warn "Couldn't find revmap for $url$source\n";
+                       next;
+               }
+               my @ranges = split ",", $revs;
+               my ($tip, $tip_commit);
+               # find the tip
+               for my $range ( @ranges ) {
+                       my ($bottom, $top) = split "-", $range;
+                       $top ||= $bottom;
+                       my $bottom_commit =
+                               $gs->rev_map_get($bottom, $self->ra_uuid) ||
+                               $gs->rev_map_get($bottom+1, $self->ra_uuid);
+                       my $top_commit;
+                       for (; !$top_commit && $top >= $bottom; --$top) {
+                               $top_commit =
+                                       $gs->rev_map_get($top, $self->ra_uuid);
+                       }
+
+                       unless ($top_commit and $bottom_commit) {
+                               warn "W:unknown path/rev in svn:mergeinfo "
+                                       ."dirprop: $source:$range\n";
+                               next;
+                       }
+
+                       push @merged_commit_ranges,
+                               "$bottom_commit..$top_commit";
+
+                       if ( !defined $tip or $top > $tip ) {
+                               $tip = $top;
+                               $tip_commit = $top_commit;
+                       }
+               }
+               unless (!$tip_commit or
+                               grep { $_ eq $tip_commit } @$parents ) {
+                       push @merge_tips, $tip_commit;
+               } else {
+                       push @merge_tips, undef;
+               }
+       }
+       for my $merge_tip ( @merge_tips ) {
+               my $spec = shift @merges;
+               next unless $merge_tip;
+               my @cmd = ('rev-list', "-1", $merge_tip,
+                          "--not", @$parents );
+               my ($msg_fh, $ctx) = command_output_pipe(@cmd);
+               my $new;
+               while ( <$msg_fh> ) {
+                       $new=1;last;
+               }
+               command_close_pipe($msg_fh, $ctx);
+               if ( $new ) {
+                       push @cmd, @merged_commit_ranges;
+                       my ($msg_fh, $ctx) = command_output_pipe(@cmd);
+                       my $unmerged;
+                       while ( <$msg_fh> ) {
+                               $unmerged=1;last;
+                       }
+                       command_close_pipe($msg_fh, $ctx);
+                       if ( $unmerged ) {
+                               warn "W:svn cherry-pick ignored ($spec)\n";
+                       } else {
+                               warn
+                                 "Found merge parent (svn:mergeinfo prop): ",
+                                 $merge_tip, "\n";
+                               push @$parents, $merge_tip;
+                       }
+               }
+       }
+}
+
 sub make_log_entry {
        my ($self, $rev, $parents, $ed) = @_;
        my $untracked = $self->get_untracked($ed);
 
+       my @parents = @$parents;
+       my $ps = $ed->{path_strip} || "";
+       for my $path ( grep { m/$ps/ } %{$ed->{dir_prop}} ) {
+               my $props = $ed->{dir_prop}{$path};
+               if ( $props->{"svk:merge"} ) {
+                       $self->find_extra_svk_parents
+                               ($ed, $props->{"svk:merge"}, \@parents);
+               }
+               if ( $props->{"svn:mergeinfo"} ) {
+                       $self->find_extra_svn_parents
+                               ($ed,
+                                $props->{"svn:mergeinfo"},
+                                \@parents);
+               }
+       }
+
        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,
+       my %log_entry = ( parents => \@parents, revision => $rev,
                          log => '');
 
        my $headrev;
@@ -2865,7 +3127,7 @@ sub make_log_entry {
                        die "Can't have both 'useSvmProps' and 'rewriteRoot' ",
                            "options set!\n";
                }
-               my ($uuid, $r) = $headrev =~ m{^([a-f\d\-]{30,}):(\d+)$};
+               my ($uuid, $r) = $headrev =~ m{^([a-f\d\-]{30,}):(\d+)$}i;
                # we don't want "SVM: initializing mirror for junk" ...
                return undef if $r == 0;
                my $svm = $self->svm;
@@ -3227,7 +3489,7 @@ sub _rev_map_get {
                my $i = int(($l/24 + $u/24) / 2) * 24;
                sysseek($fh, $i, SEEK_SET) or croak "seek: $!";
                sysread($fh, my $buf, 24) == 24 or croak "read: $!";
-               my ($r, $c) = unpack('NH40', $buf);
+               my ($r, $c) = unpack(rev_map_fmt, $buf);
 
                if ($r < $rev) {
                        $l = $i + 24;
@@ -3282,12 +3544,24 @@ sub _new {
                $repo_id = $Git::SVN::default_repo_id;
        }
        unless (defined $ref_id && length $ref_id) {
-               $_[2] = $ref_id = $Git::SVN::default_ref_id;
+               $_prefix = '' unless defined($_prefix);
+               $_[2] = $ref_id =
+                            "refs/remotes/$_prefix$Git::SVN::default_ref_id";
        }
        $_[1] = $repo_id;
        my $dir = "$ENV{GIT_DIR}/svn/$ref_id";
+
+       # Older repos imported by us used $GIT_DIR/svn/foo instead of
+       # $GIT_DIR/svn/refs/remotes/foo when tracking refs/remotes/foo
+       if ($ref_id =~ m{^refs/remotes/(.*)}) {
+               my $old_dir = "$ENV{GIT_DIR}/svn/$1";
+               if (-d $old_dir && ! -d $dir) {
+                       $dir = $old_dir;
+               }
+       }
+
        $_[3] = $path = '' unless (defined $path);
-       mkpath(["$ENV{GIT_DIR}/svn"]);
+       mkpath([$dir]);
        bless {
                ref_id => $ref_id, dir => $dir, index => "$dir/index",
                path => $path, config => "$ENV{GIT_DIR}/svn/config",
@@ -3325,6 +3599,12 @@ sub uri_encode {
        $f
 }
 
+sub uri_decode {
+       my ($f) = @_;
+       $f =~ s#%([0-9a-fA-F]{2})#chr(hex($1))#eg;
+       $f
+}
+
 sub remove_username {
        $_[0] =~ s{^([^:]*://)[^@]+@}{$1};
 }
@@ -3970,7 +4250,7 @@ sub repo_path {
 sub url_path {
        my ($self, $path) = @_;
        if ($self->{url} =~ m#^https?://#) {
-               $path =~ s/([^~a-zA-Z0-9_.-])/uc sprintf("%%%02x",ord($1))/eg;
+               $path =~ s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;
        }
        $self->{url} . '/' . $self->repo_path($path);
 }
@@ -4436,14 +4716,22 @@ sub get_log {
        # externally passed pool (instead of our temporary and quickly cleared
        # pool in Git::SVN::Ra) does not help matters at all...
        my $receiver = pop @args;
+       my $prefix = "/".$self->{svn_path};
+       $prefix =~ s#/+($)##;
+       my $prefix_regex = qr#^\Q$prefix\E#;
        push(@args, sub {
                my ($paths) = $_[0];
                return &$receiver(@_) unless $paths;
                $_[0] = ();
                foreach my $p (keys %$paths) {
                        my $i = $paths->{$p};
-                       my %s = map { $_ => $i->$_ }
-                                     qw/copyfrom_path copyfrom_rev action/;
+                       # Make path relative to our url, not repos_root
+                       $p =~ s/$prefix_regex//;
+                       my %s = map { $_ => $i->$_; }
+                               qw/copyfrom_path copyfrom_rev action/;
+                       if ($s{'copyfrom_path'}) {
+                               $s{'copyfrom_path'} =~ s/$prefix_regex//;
+                       }
                        $_[0]{$p} = \%s;
                }
                &$receiver(@_);
@@ -4534,10 +4822,12 @@ sub gs_do_switch {
 
        my $full_url = $self->{url};
        my $old_url = $full_url;
-       $full_url .= '/' . escape_uri_only($path) if length $path;
+       $full_url .= '/' . $path if length $path;
        my ($ra, $reparented);
 
-       if ($old_url =~ m#^svn(\+ssh)?://#) {
+       if ($old_url =~ m#^svn(\+ssh)?://# ||
+           ($full_url =~ m#^https?://# &&
+            escape_url($full_url) ne $full_url)) {
                $_[0] = undef;
                $self = undef;
                $RA = undef;
@@ -4786,7 +5076,11 @@ sub minimize_url {
        my $c = '';
        do {
                $url .= "/$c" if length $c;
-               eval { (ref $self)->new($url)->get_latest_revnum };
+               eval {
+                       my $ra = (ref $self)->new($url);
+                       my $latest = $ra->get_latest_revnum;
+                       $ra->get_log("", $latest, 0, 1, 0, 1, sub {});
+               };
        } while ($@ && ($c = shift @components));
        $url;
 }
@@ -4929,10 +5223,8 @@ sub git_svn_log_cmd {
 
 # 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') {
+       chomp(my $pager = command_oneline(qw(var GIT_PAGER)));
+       if ($pager eq 'cat') {
                $pager = undef;
        }
        $ENV{GIT_PAGER_IN_USE} = defined($pager);
@@ -5446,7 +5738,7 @@ sub minimize_connections {
                        my $pfx = "svn-remote.$x->{old_repo_id}";
 
                        my $old_fetch = quotemeta("$x->{old_path}:".
-                                                 "refs/remotes/$x->{ref_id}");
+                                                 "$x->{ref_id}");
                        command_noisy(qw/config --unset/,
                                      "$pfx.fetch", '^'. $old_fetch . '$');
                        delete $r->{$x->{old_repo_id}}->
@@ -5515,7 +5807,7 @@ sub new {
        my ($class, $glob) = @_;
        my $re = $glob;
        $re =~ s!/+$!!g; # no need for trailing slashes
-       $re =~ m!^([^*]*)(\*(?:/\*)*)([^*]*)$!;
+       $re =~ m!^([^*]*)(\*(?:/\*)*)(.*)$!;
        my $temp = $re;
        my ($left, $right) = ($1, $3);
        $re = $2;