git-svn: remove optimized commit stuff for set-tree
[gitweb.git] / git-svn.perl
index ad2ef53f8d22e60e4dc8dfc6b20955439544660e..7e1a6552596fcfbb3833e1fb64b782a660ae9b85 100755 (executable)
@@ -10,7 +10,7 @@
 $VERSION = '@@GIT_VERSION@@';
 
 $ENV{GIT_DIR} ||= '.git';
-$Git::SVN::default_repo_id = 'git-svn';
+$Git::SVN::default_repo_id = 'svn';
 $Git::SVN::default_ref_id = $ENV{GIT_SVN_ID} || 'git-svn';
 
 $Git::SVN::Log::TZ = $ENV{TZ};
@@ -48,7 +48,6 @@ BEGIN
 
 my ($SVN);
 
-my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS};
 $sha1 = qr/[a-f\d]{40}/;
 $sha1_short = qr/[a-f\d]{4,40}/;
 my ($_stdin, $_help, $_edit,
@@ -57,11 +56,11 @@ BEGIN
        $_version,
        $_merge, $_strategy, $_dry_run,
        $_prefix);
-
+$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,
+my %fc_opts = ( 'follow-parent|follow!' => \$Git::SVN::_follow_parent,
                'authors-file|A=s' => \$_authors,
                'repack:i' => \$Git::SVN::_repack,
                'no-metadata' => \$Git::SVN::_no_metadata,
@@ -367,9 +366,10 @@ sub cmd_multi_init {
 sub cmd_multi_fetch {
        my $remotes = Git::SVN::read_all_remotes();
        foreach my $repo_id (sort keys %$remotes) {
-               my $url = $remotes->{$repo_id}->{url} or next;
-               my $fetch = $remotes->{$repo_id}->{fetch} or next;
-               Git::SVN::fetch_all($repo_id, $url, $fetch);
+               if ($remotes->{$repo_id}->{url} &&
+                   $remotes->{$repo_id}->{fetch}) {
+                       Git::SVN::fetch_all($repo_id, $remotes);
+               }
        }
 }
 
@@ -479,17 +479,23 @@ sub complete_url_ls_init {
                # don't try to init already existing refs
                unless ($gs) {
                        print "init $url/$path => $ref\n";
-                       $gs = Git::SVN->init($url, $path, undef, $ref);
+                       $gs = Git::SVN->init($url, $path, undef, $ref, 1);
+               }
+               if ($gs) {
+                       $remote_id = $gs->{repo_id};
+                       last;
                }
-               $remote_id ||= $gs->{repo_id} if $gs;
        }
        if (defined $remote_id) {
                $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.$remote_id.$n",
-                                       $remote_path);
+                                       "$remote_path:refs/remotes/$pfx*");
        }
 }
 
@@ -660,12 +666,67 @@ BEGIN
 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 = $path->full_path($p);
+               my $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 {
+                       $fetch->{$pathname} = $refname;
+               }
+       }
+}
+
 sub fetch_all {
-       my ($repo_id, $url, $fetch) = @_;
-       my @gs;
+       my ($repo_id, $remotes) = @_;
+       my $remote = $remotes->{$repo_id};
+       my $fetch = $remote->{fetch};
+       my $url = $remote->{url};
+       my (@gs, @globs);
        my $ra = Git::SVN::Ra->new($url);
+       my $uuid = $ra->uuid;
        my $head = $ra->get_latest_revnum;
        my $base = $head;
+
+       # 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 $f = "$ENV{GIT_DIR}/svn/.$uuid.$t";
+               if (open my $fh, '<', $f) {
+                       chomp(my $max_rev = <$fh>);
+                       close $fh or die "Error closing $f: $!\n";
+
+                       if ($max_rev !~ /^\d+$/) {
+                               die "$max_rev (in $f) is not an integer!\n";
+                       }
+                       $remote->{$t}->{max_rev} = $max_rev;
+                       $base = $max_rev if ($max_rev < $base);
+               }
+       }
+
        foreach my $p (sort keys %$fetch) {
                my $gs = Git::SVN->new($fetch->{$p}, $repo_id, $p);
                my $lr = $gs->rev_db_max;
@@ -674,8 +735,7 @@ sub fetch_all {
                }
                push @gs, $gs;
        }
-       return if (++$base > $head);
-       $ra->gs_fetch_loop_common($base, $head, @gs);
+       $ra->gs_fetch_loop_common($base, $head, \@gs, \@globs);
 }
 
 sub read_all_remotes {
@@ -685,6 +745,17 @@ sub read_all_remotes {
                        $r->{$1}->{fetch}->{$2} = $3;
                } 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,
+                                         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";
+                       }
                }
        }
        $r;
@@ -740,23 +811,29 @@ sub init_remote_config {
        my $r = read_all_remotes();
        my $existing = find_existing_remote($url, $r);
        if ($existing) {
-               print STDERR "Using existing ",
-                            "[svn-remote \"$existing\"]\n";
+               unless ($no_write) {
+                       print STDERR "Using existing ",
+                                    "[svn-remote \"$existing\"]\n";
+               }
                $self->{repo_id} = $existing;
        } else {
                my $min_url = Git::SVN::Ra->new($url)->minimize_url;
                $existing = find_existing_remote($min_url, $r);
                if ($existing) {
-                       print STDERR "Using existing ",
-                                    "[svn-remote \"$existing\"]\n";
+                       unless ($no_write) {
+                               print STDERR "Using existing ",
+                                            "[svn-remote \"$existing\"]\n";
+                       }
                        $self->{repo_id} = $existing;
                }
                if ($min_url ne $url) {
-                       print STDERR "Using higher level of URL: ",
-                                    "$url => $min_url\n";
+                       unless ($no_write) {
+                               print STDERR "Using higher level of URL: ",
+                                            "$url => $min_url\n";
+                       }
                        my $old_path = $self->{path};
                        $self->{path} = $url;
-                       $self->{path} =~ s!^\Q$min_url\E/*!!;
+                       $self->{path} =~ s!^\Q$min_url\E(/|$)!!;
                        if (length $old_path) {
                                $self->{path} .= "/$old_path";
                        }
@@ -852,10 +929,8 @@ 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;
+       die "BUG: rel_path failed! repos_root: $repos_root, Ra URL: ",
+           $self->ra->{url}, " path: $self->{path},  URL: $self->{url}\n";
 }
 
 sub traverse_ignore {
@@ -1059,14 +1134,27 @@ sub do_git_commit {
        return $commit;
 }
 
-sub revisions_eq {
-       my ($self, $r0, $r1) = @_;
-       return 1 if $r0 == $r1;
-       my $nr = 0;
-       $self->ra->get_log([$self->{path}], $r0, $r1,
-                          0, 0, 1, sub { $nr++ });
-       return 0 if ($nr > 1);
-       return 1;
+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;
+               }
+       }
+       return 0;
 }
 
 sub find_parent_branch {
@@ -1088,11 +1176,11 @@ sub find_parent_branch {
        my $i;
        while (@b_path_components) {
                $i = $paths->{'/'.join('/', @b_path_components)};
-               last if $i;
+               last if $i && defined $i->{copyfrom_path};
                unshift(@a_path_components, pop(@b_path_components));
        }
-       goto not_found unless defined $i;
-       my $branch_from = $i->{copyfrom_path} or goto not_found;
+       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);
@@ -1111,6 +1199,10 @@ sub find_parent_branch {
                my $u = $remotes->{$repo_id}->{url} or next;
                next if $url ne $u;
                my $fetch = $remotes->{$repo_id}->{fetch};
+               foreach (qw/branches tags/) {
+                       resolve_local_globs($url, $fetch,
+                                           $remotes->{$repo_id}->{$_});
+               }
                foreach my $f (keys %$fetch) {
                        next if $f ne $branch_from;
                        $gs = Git::SVN->new($fetch->{$f}, $repo_id, $f);
@@ -1132,7 +1224,7 @@ sub find_parent_branch {
                $gs->fetch(0, $r);
                ($r0, $parent) = $gs->last_rev_commit;
        }
-       if (defined $r0 && defined $parent && $gs->revisions_eq($r0, $r)) {
+       if (defined $r0 && defined $parent) {
                print STDERR "Found branch parent: ($self->{ref_id}) $parent\n";
                $self->assert_index_clean($parent);
                my $ed;
@@ -1155,21 +1247,6 @@ sub find_parent_branch {
                print STDERR "Successfully followed parent\n";
                return $self->make_log_entry($rev, [$parent], $ed);
        }
-not_found:
-       print STDERR "Branch parent for path: '/",
-                    $self->rel_path, "' @ r$rev not found:\n";
-       return undef unless $paths;
-       print STDERR "Changed paths:\n";
-       foreach my $x (sort keys %$paths) {
-               my $p = $paths->{$x};
-               print STDERR "\t$p->{action}\t$x";
-               if ($p->{copyfrom_path}) {
-                       print STDERR "(from $p->{copyfrom_path}: ",
-                                    "$p->{copyfrom_rev})";
-               }
-               print STDERR "\n";
-       }
-       print STDERR '-'x72, "\n";
        return undef;
 }
 
@@ -1177,11 +1254,19 @@ sub do_fetch {
        my ($self, $paths, $rev) = @_;
        my $ed;
        my ($last_rev, @parents);
-       if ($self->{last_commit}) {
+       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} = $self->{last_commit};
-               @parents = ($self->{last_commit});
+               $ed->{c} = $lc;
+               @parents = ($lc);
        } else {
                $last_rev = $rev;
                if (my $log_entry = $self->find_parent_branch($paths, $rev)) {
@@ -1265,15 +1350,21 @@ sub make_log_entry {
        print $un $_, "\n" foreach @$untracked;
        my %log_entry = ( parents => $parents || [], revision => $rev,
                          log => '');
-       my $rp = $self->ra->rev_proplist($rev);
-       foreach (sort keys %$rp) {
-               my $v = $rp->{$_};
-               if (/^svn:(author|date|log)$/) {
-                       $log_entry{$1} = $v;
-               } else {
-                       print $un "  rev_prop: ", uri_encode($_), ' ',
-                                 uri_encode($v), "\n";
+
+       my $logged = delete $self->{logged_rev_props};
+       if (!$logged || $self->{-want_extra_revprops}) {
+               my $rp = $self->ra->rev_proplist($rev);
+               foreach (sort keys %$rp) {
+                       my $v = $rp->{$_};
+                       if (/^svn:(author|date|log)$/) {
+                               $log_entry{$1} = $v;
+                       } else {
+                               print $un "  rev_prop: ", uri_encode($_), ' ',
+                                         uri_encode($v), "\n";
+                       }
                }
+       } else {
+               map { $log_entry{$_} = $logged->{$_} } keys %$logged;
        }
        close $un or croak $!;
 
@@ -1287,21 +1378,13 @@ 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);
-       return if ($base > $head);
-       $self->ra->gs_fetch_loop_common($base, $head, $self);
+       $self->ra->gs_fetch_loop_common($base, $head, [$self]);
 }
 
 sub set_tree_cb {
        my ($self, $log_entry, $tree, $rev, $date, $author) = @_;
-       # TODO: enable and test optimized commits:
-       if (0 && $rev == ($self->{last_rev} + 1)) {
-               $log_entry->{revision} = $rev;
-               $log_entry->{author} = $author;
-               $self->do_git_commit($log_entry, "$rev=$tree");
-       } else {
-               $self->{inject_parents} = { $rev => $tree };
-               $self->fetch(undef, undef);
-       }
+       $self->{inject_parents} = { $rev => $tree };
+       $self->fetch(undef, undef);
 }
 
 sub set_tree {
@@ -1644,7 +1727,7 @@ sub new {
 
 sub set_path_strip {
        my ($self, $path) = @_;
-       $self->{path_strip} = qr/^\Q$path\E\/?/;
+       $self->{path_strip} = qr/^\Q$path\E(\/|$)/ if length $path;
 }
 
 sub open_root {
@@ -1680,14 +1763,14 @@ sub delete_entry {
                while (<$ls>) {
                        chomp;
                        $self->{gii}->remove($_);
-                       print "\tD\t$_\n" unless $self->{q};
+                       print "\tD\t$_\n" unless $::_q;
                }
-               print "\tD\t$gpath/\n" unless $self->{q};
+               print "\tD\t$gpath/\n" unless $::_q;
                command_close_pipe($ls, $ctx);
                $self->{empty}->{$path} = 0
        } else {
                $self->{gii}->remove($gpath);
-               print "\tD\t$gpath\n" unless $self->{q};
+               print "\tD\t$gpath\n" unless $::_q;
        }
        undef;
 }
@@ -1823,7 +1906,7 @@ sub close_file {
        }
        $fb->{pool}->clear;
        $self->{gii}->update($fb->{mode_b}, $hash, $path) or croak $!;
-       print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $self->{q};
+       print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $::_q;
        undef;
 }
 
@@ -2219,8 +2302,7 @@ package Git::SVN::Ra;
 BEGIN {
        # enforce temporary pool usage for some simple functions
        my $e;
-       foreach (qw/get_latest_revnum rev_proplist get_file
-                   check_path get_dir get_uuid get_repos_root/) {
+       foreach (qw/get_latest_revnum get_uuid get_repos_root/) {
                $e .= "sub $_ {
                        my \$self = shift;
                        my \$pool = SVN::Pool->new;
@@ -2228,7 +2310,30 @@ BEGIN
                        \$pool->clear;
                        wantarray ? \@ret : \$ret[0]; }\n";
        }
-       eval $e;
+
+       # get_dir needs $pool held in cache for dirents to work,
+       # check_path is cacheable and rev_proplist is close enough
+       # for our purposes.
+       foreach (qw/check_path get_dir rev_proplist/) {
+               $e .= "my \%${_}_cache; my \$${_}_rev = 0; sub $_ {
+                       my \$self = shift;
+                       my \$r = pop;
+                       my \$k = join(\"\\0\", \@_);
+                       if (my \$x = \$${_}_cache{\$r}->{\$k}) {
+                               return wantarray ? \@\$x : \$x->[0];
+                       }
+                       my \$pool = SVN::Pool->new;
+                       my \@ret = \$self->SUPER::$_(\@_, \$r, \$pool);
+                       if (\$r != \$${_}_rev) {
+                               \%${_}_cache = ( pool => [] );
+                               \$${_}_rev = \$r;
+                       }
+                       \$${_}_cache{\$r}->{\$k} = \\\@ret;
+                       push \@{\$${_}_cache{pool}}, \$pool;
+                       wantarray ? \@ret : \$ret[0]; }\n";
+       }
+       $e .= "\n1;";
+       eval $e or die $@;
 }
 
 sub new {
@@ -2259,7 +2364,7 @@ sub 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->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;
        $RA = bless $self, $class;
 }
 
@@ -2292,14 +2397,6 @@ sub gs_do_update {
        my $new = ($rev_a == $rev_b);
        my $path = $gs->{path};
 
-       my $ta = $self->check_path($path, $rev_a);
-       my $tb = $new ? $ta : $self->check_path($path, $rev_b);
-       return 1 if ($tb != $SVN::Node::dir && $ta != $SVN::Node::dir);
-       if ($ta == $SVN::Node::none) {
-               $rev_a = $rev_b;
-               $new = 1;
-       }
-
        my $pool = SVN::Pool->new;
        $editor->set_path_strip($path);
        my (@pc) = split m#/#, $path;
@@ -2363,13 +2460,43 @@ sub gs_do_switch {
 }
 
 sub gs_fetch_loop_common {
-       my ($self, $base, $head, @gs) = @_;
+       my ($self, $base, $head, $gsv, $globs) = @_;
+       return if ($base > $head);
        my $inc = 1000;
        my ($min, $max) = ($base, $head < $base + $inc ? $head : $base + $inc);
-       foreach my $gs (@gs) {
+       my %common;
+       my $common_max = scalar @$gsv;
+
+       foreach my $gs (@$gsv) {
                if (my $last_commit = $gs->last_commit) {
                        $gs->assert_index_clean($last_commit);
                }
+               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;
+               }
        }
        while (1) {
                my %revs;
@@ -2379,16 +2506,15 @@ sub gs_fetch_loop_common {
                        ($err) = @_;
                        skip_unknown_revs($err);
                };
-               foreach my $gs (@gs) {
-                       $self->get_log([$gs->{path}], $min, $max, 0, 1, 1, sub
-                                      { my ($paths, $rev) = @_;
-                                        push @{$revs{$rev}},
-                                             [ $gs,
-                                               dup_changed_paths($paths) ] });
-
-                       next unless ($err && $max >= $head);
-
-                       print STDERR "Path '$gs->{path}' ",
+               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 ",
@@ -2397,12 +2523,10 @@ sub gs_fetch_loop_common {
                        my $hi = $max;
                        while (--$hi >= $min) {
                                my $ok;
-                               $self->get_log([$gs->{path}], $min, $hi,
-                                              0, 1, 1, sub {
-                                       my ($paths, $rev) = @_;
-                                       $ok = $rev;
-                                       push @{$revs{$rev}}, [ $gs,
-                                          dup_changed_paths($_[0])]});
+                               $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;
@@ -2410,20 +2534,39 @@ sub gs_fetch_loop_common {
                        }
                }
                $SVN::Error::handler = $err_handler;
+
+               my %exists = map { $_->{path} => $_ } @$gsv;
                foreach my $r (sort {$a <=> $b} keys %revs) {
-                       foreach (@{$revs{$r}}) {
-                               my ($gs, $paths) = @$_;
-                               my $lr = $gs->last_rev;
-                               next if defined $lr && $lr >= $r;
-                               next if defined $gs->rev_db_get($r);
-                               if (my $log_entry = $gs->do_fetch($paths, $r)) {
+                       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;
+                               my $log_entry = $gs->do_fetch($paths, $r);
+                               if ($log_entry) {
                                        $gs->do_git_commit($log_entry);
                                }
                        }
+                       foreach my $g (@$globs) {
+                               my $f = "$ENV{GIT_DIR}/svn/." .
+                                       $self->uuid . ".$g->{t}";
+                               open my $fh, '>', "$f.tmp" or
+                                     die "Can't open $f.tmp for writing: $!";
+                               print $fh "$r\n" or
+                                     die "Couldn't write to $f: $!\n";
+                               close $fh or die "Error closing $f: $!\n";
+                               rename "$f.tmp", $f or
+                                      die "Couldn't rename ",
+                                          "$f.tmp => $f: $!\n";
+                       }
                }
                # pre-fill the .rev_db since it'll eventually get filled in
                # with '0' x40 if something new gets committed
-               foreach my $gs (@gs) {
+               foreach my $gs (@$gsv) {
                        next if defined $gs->rev_db_get($max);
                        $gs->rev_db_set($max, 0 x40);
                }
@@ -2434,6 +2577,55 @@ sub gs_fetch_loop_common {
        }
 }
 
+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}/) {
+                               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};
+                       $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});
@@ -2945,7 +3137,10 @@ sub migrate_from_v2 {
        my $migrated = 0;
 
        foreach my $ref_id (sort keys %l_map) {
-               Git::SVN->init($l_map{$ref_id}, '', $ref_id, $ref_id);
+               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;
@@ -2970,7 +3165,7 @@ sub minimize_connections {
 
                my $root_ra = Git::SVN::Ra->new($ra->{repos_root});
                my $root_path = $ra->{url};
-               $root_path =~ s#^\Q$ra->{repos_root}\E/*##;
+               $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);
@@ -3074,10 +3269,71 @@ sub DESTROY {
        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()
 {
        log => 'whitespace-formatted log entry