$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};
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,
$_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,
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);
+ }
}
}
# 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*");
}
}
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;
}
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 {
$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;
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";
}
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 {
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 {
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);
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);
$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;
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;
}
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)) {
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 $!;
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 {
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 {
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;
}
}
$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;
}
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;
\$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 {
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;
}
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;
}
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;
($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 ",
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;
}
}
$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);
}
}
}
+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});
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;
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);
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