X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=git-svn.perl;h=bfe5d6b97e97cba4df1196f40e8f9bad61b91d0d;hb=0dc03d6a30213e9aa0eb88886cee24b993a24a29;hp=b2931cd5aa99b6a0cf08fe5ba8485ad76b39847c;hpb=dadc6d2a0904e55ac5a5a810dffac4d44fff0b66;p=git.git diff --git a/git-svn.perl b/git-svn.perl index b2931cd5a..721a46817 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -9,9 +9,11 @@ use vars qw/ $AUTHOR $VERSION $AUTHOR = 'Eric Wong '; $VERSION = '@@GIT_VERSION@@'; +my $git_dir_user_set = 1 if defined $ENV{GIT_DIR}; $ENV{GIT_DIR} ||= '.git'; $Git::SVN::default_repo_id = 'svn'; $Git::SVN::default_ref_id = $ENV{GIT_SVN_ID} || 'git-svn'; +$Git::SVN::Ra::_log_window_size = 100; $Git::SVN::Log::TZ = $ENV{TZ}; $ENV{TZ} = 'UTC'; @@ -31,7 +33,7 @@ use Carp qw/croak/; use IO::File qw//; use File::Basename qw/dirname basename/; use File::Path qw/mkpath/; -use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/; +use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; use IPC::Open3; use Git; @@ -53,9 +55,9 @@ $sha1_short = qr/[a-f\d]{4,40}/; my ($_stdin, $_help, $_edit, $_message, $_file, $_template, $_shared, - $_version, $_fetch_all, - $_merge, $_strategy, $_dry_run, - $_prefix); + $_version, $_fetch_all, $_no_rebase, + $_merge, $_strategy, $_dry_run, $_local, + $_prefix, $_no_checkout, $_verbose); $Git::SVN::_follow_parent = 1; my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username, 'config-dir=s' => \$Git::SVN::Ra::config_dir, @@ -65,15 +67,23 @@ my %fc_opts = ( 'follow-parent|follow!' => \$Git::SVN::_follow_parent, 'repack:i' => \$Git::SVN::_repack, 'noMetadata' => \$Git::SVN::_no_metadata, 'useSvmProps' => \$Git::SVN::_use_svm_props, + 'useSvnsyncProps' => \$Git::SVN::_use_svnsync_props, + 'log-window-size=i' => \$Git::SVN::Ra::_log_window_size, + 'no-checkout' => \$_no_checkout, 'quiet|q' => \$_q, 'repack-flags|repack-args|repack-opts=s' => \$Git::SVN::_repack_flags, %remote_opts ); my ($_trunk, $_tags, $_branches); +my %icv; my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared, 'trunk|T=s' => \$_trunk, 'tags|t=s' => \$_tags, 'branches|b=s' => \$_branches, 'prefix=s' => \$_prefix, + 'no-metadata' => sub { $icv{noMetadata} = 1 }, + 'use-svm-props' => sub { $icv{useSvmProps} = 1 }, + 'use-svnsync-props' => sub { $icv{useSvnsyncProps} = 1 }, + 'rewrite-root=s' => sub { $icv{rewriteRoot} = $_[1] }, %remote_opts ); my %cmt_opts = ( 'edit|e' => \$_edit, 'rmdir' => \$SVN::Git::Editor::_rmdir, @@ -85,8 +95,11 @@ my %cmt_opts = ( 'edit|e' => \$_edit, my %cmd = ( fetch => [ \&cmd_fetch, "Download new revisions from SVN", { 'revision|r=s' => \$_revision, - 'all|a' => \$_fetch_all, + 'fetch-all|all' => \$_fetch_all, %fc_opts } ], + clone => [ \&cmd_clone, "Initialize and fetch revisions", + { 'revision|r=s' => \$_revision, + %fc_opts, %init_opts } ], init => [ \&cmd_init, "Initialize a repo for tracking" . " (requires URL argument)", \%init_opts ], @@ -98,7 +111,10 @@ my %cmd = ( 'Commit several diffs to merge with upstream', { 'merge|m|M' => \$_merge, 'strategy|s=s' => \$_strategy, + 'verbose|v' => \$_verbose, 'dry-run|n' => \$_dry_run, + 'fetch-all|all' => \$_fetch_all, + 'no-rebase' => \$_no_rebase, %cmt_opts, %fc_opts } ], 'set-tree' => [ \&cmd_set_tree, "Set an SVN repository to a git tree-ish", @@ -112,7 +128,8 @@ my %cmd = ( # no-op, we automatically run this anyways, 'Migrate configuration/metadata/layout from previous versions of git-svn', - \%remote_opts ], + { 'minimize' => \$Git::SVN::Migration::_minimize, + %remote_opts } ], 'log' => [ \&Git::SVN::Log::cmd_show_log, 'Show commit logs', { 'limit=i' => \$Git::SVN::Log::limit, 'revision|r=s' => \$_revision, @@ -125,6 +142,15 @@ my %cmd = ( 'color' => \$Git::SVN::Log::color, 'pager=s' => \$Git::SVN::Log::pager, } ], + 'find-rev' => [ \&cmd_find_rev, "Translate between SVN revision numbers and tree-ish", + { } ], + 'rebase' => [ \&cmd_rebase, "Fetch and rebase your working directory", + { 'merge|m|M' => \$_merge, + 'verbose|v' => \$_verbose, + 'strategy|s=s' => \$_strategy, + 'local|l' => \$_local, + 'fetch-all|all' => \$_fetch_all, + %fc_opts } ], 'commit-diff' => [ \&cmd_commit_diff, 'Commit a diff between two trees', { 'message|m=s' => \$_message, @@ -145,17 +171,42 @@ for (my $i = 0; $i < @ARGV; $i++) { my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd); read_repo_config(\%opts); +Getopt::Long::Configure('pass_through') if ($cmd && $cmd eq 'log'); my $rv = GetOptions(%opts, 'help|H|h' => \$_help, 'version|V' => \$_version, 'minimize-connections' => \$Git::SVN::Migration::_minimize, 'id|i=s' => \$Git::SVN::default_ref_id, - 'svn-remote|remote|R=s' => \$Git::SVN::default_repo_id); -exit 1 if (!$rv && $cmd ne 'log'); + 'svn-remote|remote|R=s' => sub { + $Git::SVN::no_reuse_existing = 1; + $Git::SVN::default_repo_id = $_[1] }); +exit 1 if (!$rv && $cmd && $cmd ne 'log'); usage(0) if $_help; version() if $_version; usage(1) unless defined $cmd; load_authors() if $_authors; -unless ($cmd =~ /^(?:init|multi-init|commit-diff)$/) { + +# make sure we're always running +unless ($cmd =~ /(?:clone|init|multi-init)$/) { + unless (-d $ENV{GIT_DIR}) { + if ($git_dir_user_set) { + die "GIT_DIR=$ENV{GIT_DIR} explicitly set, ", + "but it is not a directory\n"; + } + my $git_dir = delete $ENV{GIT_DIR}; + chomp(my $cdup = command_oneline(qw/rev-parse --show-cdup/)); + unless (length $cdup) { + die "Already at toplevel, but $git_dir ", + "not found '$cdup'\n"; + } + chdir $cdup or die "Unable to chdir up to '$cdup'\n"; + unless (-d $git_dir) { + die "$git_dir still not found after going to ", + "'$cdup'\n"; + } + $ENV{GIT_DIR} = $git_dir; + } +} +unless ($cmd =~ /^(?:clone|init|multi-init|commit-diff)$/) { Git::SVN::Migration::migration_check(); } Git::SVN::init_vars(); @@ -164,6 +215,7 @@ eval { $cmd{$cmd}->[0]->(@ARGV); }; fatal $@ if $@; +post_fetch_checkout(); exit 0; ####################### primary functions ###################### @@ -178,9 +230,11 @@ Usage: $0 [options] [arguments]\n foreach (sort keys %cmd) { next if $cmd && $cmd ne $_; + next if /^multi-/; # don't show deprecated commands print $fd ' ',pack('A17',$_),$cmd{$_}->[1],"\n"; foreach (keys %{$cmd{$_}->[2]}) { - next if /^multi-/; # don't show deprecated commands + # mixed-case options are for .git/config only + next if /[A-Z]/ && /^[a-z]+$/i; # prints out arguments as they should be passed: my $x = s#[:=]s$## ? '' : s#[:=]i$## ? '' : ''; print $fd ' ' x 21, join(', ', map { length $_ > 1 ? @@ -215,13 +269,33 @@ sub do_git_init_db { } command_noisy(@init_db); } + my $set; + my $pfx = "svn-remote.$Git::SVN::default_repo_id"; + foreach my $i (keys %icv) { + die "'$set' and '$i' cannot both be set\n" if $set; + next unless defined $icv{$i}; + command_noisy('config', "$pfx.$i", $icv{$i}); + $set = $i; + } } sub init_subdir { my $repo_path = shift or return; mkpath([$repo_path]) unless -d $repo_path; chdir $repo_path or die "Couldn't chdir to $repo_path: $!\n"; - $ENV{GIT_DIR} = $repo_path . "/.git"; + $ENV{GIT_DIR} = '.git'; +} + +sub cmd_clone { + my ($url, $path) = @_; + if (!defined $path && + (defined $_trunk || defined $_branches || defined $_tags) && + $url !~ m#^[a-z\+]+://#) { + $path = $url; + } + $path = basename($url) if !defined $path || !length $path; + cmd_init($url, $path); + Git::SVN::fetch_all($Git::SVN::default_repo_id); } sub cmd_init { @@ -243,7 +317,7 @@ sub cmd_fetch { } my ($remote) = @_; if (@_ > 1) { - die "Usage: $0 fetch [--all|-a] [svn-remote]\n"; + die "Usage: $0 fetch [--all] [svn-remote]\n"; } $remote ||= $Git::SVN::default_repo_id; if ($_fetch_all) { @@ -291,24 +365,13 @@ sub cmd_set_tree { sub cmd_dcommit { my $head = shift; $head ||= 'HEAD'; - my ($url, $rev, $uuid); - my ($fh, $ctx) = command_output_pipe('rev-list', $head); my @refs; - my $c; - while (<$fh>) { - $c = $_; - chomp $c; - ($url, $rev, $uuid) = cmt_metadata($c); - last if (defined $url && defined $rev && defined $uuid); - unshift @refs, $c; - } - close $fh; # most likely breaking the pipe - unless (defined $url && defined $rev && defined $uuid) { + my ($url, $rev, $uuid, $gs) = working_head_info($head, \@refs); + unless ($gs) { die "Unable to determine upstream SVN information from ", - "$head history:\n $ctx\n"; + "$head history\n"; } - my $gs = Git::SVN->find_by_url($url) or - die "Can't determine fetch information for $url\n"; + my $c = $refs[-1]; my $last_rev; foreach my $d (@refs) { if (!verify_ref("$d~1")) { @@ -330,7 +393,7 @@ sub cmd_dcommit { } else { my %ed_opts = ( r => $last_rev, log => get_commit_entry($d)->{log}, - ra => Git::SVN::Ra->new($url), + ra => Git::SVN::Ra->new($gs->full_url), tree_a => "$d~1", tree_b => $d, editor_cb => sub { @@ -343,30 +406,77 @@ sub cmd_dcommit { } } return if $_dry_run; - $gs->fetch; - # we always want to rebase against the current HEAD, not any - # head that was passed to us - my @diff = command('diff-tree', 'HEAD', $gs->refname, '--'); - my @finish; - if (@diff) { - @finish = qw/rebase/; - push @finish, qw/--merge/ if $_merge; - push @finish, "--strategy=$_strategy" if $_strategy; - print STDERR "W: HEAD and ", $gs->refname, " differ, ", - "using @finish:\n", "@diff"; + unless ($gs) { + warn "Could not determine fetch information for $url\n", + "Will not attempt to fetch and rebase commits.\n", + "This probably means you have useSvmProps and should\n", + "now resync your SVN::Mirror repository.\n"; + return; + } + $_fetch_all ? $gs->fetch_all : $gs->fetch; + unless ($_no_rebase) { + # we always want to rebase against the current HEAD, not any + # head that was passed to us + my @diff = command('diff-tree', 'HEAD', $gs->refname, '--'); + my @finish; + if (@diff) { + @finish = rebase_cmd(); + print STDERR "W: HEAD and ", $gs->refname, " differ, ", + "using @finish:\n", "@diff"; + } else { + print "No changes between current HEAD and ", + $gs->refname, "\nResetting to the latest ", + $gs->refname, "\n"; + @finish = qw/reset --mixed/; + } + command_noisy(@finish, $gs->refname); + } +} + +sub cmd_find_rev { + my $revision_or_hash = shift; + my $result; + if ($revision_or_hash =~ /^r\d+$/) { + my $head = shift; + $head ||= 'HEAD'; + my @refs; + my (undef, undef, undef, $gs) = working_head_info($head, \@refs); + unless ($gs) { + die "Unable to determine upstream SVN information from ", + "$head history\n"; + } + my $desired_revision = substr($revision_or_hash, 1); + $result = $gs->rev_db_get($desired_revision); } else { - print "No changes between current HEAD and ", - $gs->refname, "\nResetting to the latest ", - $gs->refname, "\n"; - @finish = qw/reset --mixed/; + my (undef, $rev, undef) = cmt_metadata($revision_or_hash); + $result = $rev; } - command_noisy(@finish, $gs->refname); + print "$result\n" if $result; +} + +sub cmd_rebase { + command_noisy(qw/update-index --refresh/); + my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); + unless ($gs) { + die "Unable to determine upstream SVN information from ", + "working tree history\n"; + } + if (command(qw/diff-index HEAD --/)) { + print STDERR "Cannot rebase with uncommited changes:\n"; + command_noisy('status'); + exit 1; + } + unless ($_local) { + $_fetch_all ? $gs->fetch_all : $gs->fetch; + } + command_noisy(rebase_cmd(), $gs->refname); } sub cmd_show_ignore { - my $gs = Git::SVN->new; + my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); + $gs ||= Git::SVN->new; my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum); - $gs->traverse_ignore(\*STDOUT, '', $r); + $gs->traverse_ignore(\*STDOUT, $gs->{path}, $r); } sub cmd_multi_init { @@ -374,12 +484,12 @@ sub cmd_multi_init { unless (defined $_trunk || defined $_branches || defined $_tags) { usage(1); } - do_git_init_db(); $_prefix = '' unless defined $_prefix; if (defined $url) { $url =~ s#/+$##; init_subdir(@_); } + do_git_init_db(); if (defined $_trunk) { my $trunk_ref = $_prefix . 'trunk'; # try both old-style and new-style lookups: @@ -457,6 +567,35 @@ sub cmd_commit_diff { ########################### utility functions ######################### +sub rebase_cmd { + my @cmd = qw/rebase/; + push @cmd, '-v' if $_verbose; + push @cmd, qw/--merge/ if $_merge; + push @cmd, "--strategy=$_strategy" if $_strategy; + @cmd; +} + +sub post_fetch_checkout { + return if $_no_checkout; + my $gs = $Git::SVN::_head or return; + return if verify_ref('refs/heads/master^0'); + + my $valid_head = verify_ref('HEAD^0'); + command_noisy(qw(update-ref refs/heads/master), $gs->refname); + return if ($valid_head || !verify_ref('HEAD^0')); + + return if $ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#; + my $index = $ENV{GIT_INDEX_FILE} || "$ENV{GIT_DIR}/index"; + return if -f $index; + + chomp(my $bare = `git config --bool --get core.bare`); + return if $bare eq 'true'; + return if command_oneline(qw/rev-parse --is-inside-git-dir/) eq 'true'; + command_noisy(qw/read-tree -m -u -v HEAD HEAD/); + print STDERR "Checked out HEAD:\n ", + $gs->full_url, " r", $gs->last_rev, "\n"; +} + sub complete_svn_url { my ($url, $path) = @_; $path =~ s#/+$##; @@ -487,48 +626,24 @@ sub complete_url_ls_init { "and a separate URL is not specified\n"); } } - my $r = defined $_revision ? $_revision : $ra->get_latest_revnum; - my ($dirent, undef, undef) = $ra->get_dir($repo_path, $r); my $url = $ra->{url}; - my $remote_id; - my $remote_path; - foreach my $d (sort keys %$dirent) { - next if ($dirent->{$d}->kind != $SVN::Node::dir); - my $path = "$repo_path/$d"; - my $ref = "$pfx$d"; - my $gs = eval { Git::SVN->new($ref) }; - # don't try to init already existing refs - unless ($gs) { - print "init $url/$path => $ref\n"; - $gs = Git::SVN->init($url, $path, undef, $ref, 1); - } - if ($gs) { - my $k = "svn-remote.$gs->{repo_id}.url"; - my $orig_url = eval { - command_oneline(qw/config --get/, $k) - }; - if ($orig_url && ($orig_url ne $gs->{url})) { - die "$k already set: $orig_url\n", - "wanted to set to: $gs->{url}\n"; - } - unless ($orig_url) { - command_oneline('config', $k, $gs->{url}); - } - $remote_id = $gs->{repo_id}; - last; - } + my $gs = Git::SVN->init($url, undef, undef, undef, 1); + my $k = "svn-remote.$gs->{repo_id}.url"; + my $orig_url = eval { command_oneline(qw/config --get/, $k) }; + if ($orig_url && ($orig_url ne $gs->{url})) { + die "$k already set: $orig_url\n", + "wanted to set to: $gs->{url}\n"; } - 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:refs/remotes/$pfx*"); + command_oneline('config', $k, $gs->{url}) unless $orig_url; + my $remote_path = "$ra->{svn_path}/$repo_path/*"; + $remote_path =~ s#/+#/#g; + $remote_path =~ s#^/##g; + my ($n) = ($switch =~ /^--(\w+)/); + if (length $pfx && $pfx !~ m#/$#) { + die "--prefix='$pfx' must have a trailing slash '/'\n"; } + command_noisy('config', "svn-remote.$gs->{repo_id}.$n", + "$remote_path:refs/remotes/$pfx*"); } sub verify_ref { @@ -679,11 +794,33 @@ sub cmt_metadata { command(qw/cat-file commit/, shift)))[-1]); } +sub working_head_info { + my ($head, $refs) = @_; + my ($fh, $ctx) = command_output_pipe('rev-list', $head); + while (my $hash = <$fh>) { + chomp($hash); + my ($url, $rev, $uuid) = cmt_metadata($hash); + if (defined $url && defined $rev) { + if (my $gs = Git::SVN->find_by_url($url)) { + my $c = $gs->rev_db_get($rev); + if ($c && $c eq $hash) { + close $fh; # break the pipe + return ($url, $rev, $uuid, $gs); + } + } + } + unshift @$refs, $hash if $refs; + } + command_close_pipe($fh, $ctx); + (undef, undef, undef, undef); +} + package Git::SVN; use strict; use warnings; use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent - $_repack $_repack_flags $_use_svm_props/; + $_repack $_repack_flags $_use_svm_props $_head + $_use_svnsync_props $no_reuse_existing/; use Carp qw/croak/; use File::Path qw/mkpath/; use File::Copy qw/copy/; @@ -704,7 +841,8 @@ BEGIN { # per [svn-remote "..."] section. Command-line options will *NOT* # override options set in an [svn-remote "..."] section my $e; - foreach (qw/follow_parent no_metadata use_svm_props/) { + foreach (qw/follow_parent no_metadata use_svm_props + use_svnsync_props/) { my $key = $_; $key =~ tr/_//d; $e .= "sub $_ { @@ -775,14 +913,21 @@ sub parse_revision_argument { sub fetch_all { my ($repo_id, $remotes) = @_; - my $remote = $remotes->{$repo_id}; + if (ref $repo_id) { + my $gs = $repo_id; + $repo_id = undef; + $repo_id = $gs->{repo_id}; + } + $remotes ||= read_all_remotes(); + my $remote = $remotes->{$repo_id} or + die "[svn-remote \"$repo_id\"] unknown\n"; my $fetch = $remote->{fetch}; - my $url = $remote->{url}; + my $url = $remote->{url} or die "svn-remote.$repo_id.url not defined\n"; my (@gs, @globs); my $ra = Git::SVN::Ra->new($url); my $uuid = $ra->get_uuid; my $head = $ra->get_latest_revnum; - my $base = $head; + my $base = defined $fetch ? $head : 0; # read the max revs for wildcard expansion (branches/*, tags/*) foreach my $t (qw/branches tags/) { @@ -792,6 +937,8 @@ sub fetch_all { "svn-remote.$repo_id.${t}-maxRev") }; if (defined $max_rev && ($max_rev < $base)) { $base = $max_rev; + } elsif (!defined $max_rev) { + $base = 0; } } @@ -868,6 +1015,7 @@ sub sanitize_remote_name { sub find_existing_remote { my ($url, $remotes) = @_; + return undef if $no_reuse_existing; my $existing; foreach my $repo_id (keys %$remotes) { my $u = $remotes->{$repo_id}->{url} or next; @@ -942,6 +1090,10 @@ sub init_remote_config { sub find_by_url { # repos_root and, path are optional my ($class, $full_url, $repos_root, $path) = @_; + + return undef unless defined $full_url; + remove_username($full_url); + remove_username($repos_root) if defined $repos_root; my $remotes = read_all_remotes(); if (defined $full_url && defined $repos_root && !defined $path) { $path = $full_url; @@ -949,6 +1101,7 @@ sub find_by_url { # repos_root and, path are optional } foreach my $repo_id (keys %$remotes) { my $u = $remotes->{$repo_id}->{url} or next; + remove_username($u); next if defined $repos_root && $repos_root ne $u; my $fetch = $remotes->{$repo_id}->{fetch} || {}; @@ -1014,10 +1167,7 @@ sub new { $self->{url} = command_oneline('config', '--get', "svn-remote.$repo_id.url") or die "Failed to read \"svn-remote.$repo_id.url\" in config\n"; - if ((-z $self->db_path || ! -e $self->db_path) && - ::verify_ref($self->refname.'^0')) { - $self->rebuild; - } + $self->rebuild; $self; } @@ -1043,9 +1193,12 @@ sub svm { $svm = { source => tmp_config('--get', "$section.svm-source"), uuid => tmp_config('--get', "$section.svm-uuid"), + replace => tmp_config('--get', "$section.svm-replace"), } }; - $self->{svm} = $svm if ($svm && $svm->{source} && $svm->{uuid}); + if ($svm && $svm->{source} && $svm->{uuid} && $svm->{replace}) { + $self->{svm} = $svm; + } $self->{svm}; } @@ -1054,68 +1207,124 @@ sub _set_svm_vars { return $ra if $self->svm; my @err = ( "useSvmProps set, but failed to read SVM properties\n", - "(svm:source, svm:mirror, svm:mirror) ", + "(svm:source, svm:uuid) ", "from the following URLs:\n" ); sub read_svm_props { - my ($self, $props) = @_; + my ($self, $ra, $path, $r) = @_; + my $props = ($ra->get_dir($path, $r))[2]; my $src = $props->{'svm:source'}; - my $mirror = $props->{'svm:mirror'}; my $uuid = $props->{'svm:uuid'}; - return undef if (!$src || !$mirror || !$uuid); + return undef if (!$src || !$uuid); - chomp($src, $mirror, $uuid); + chomp($src, $uuid); $uuid =~ m{^[0-9a-f\-]{30,}$} or die "doesn't look right - svm:uuid is '$uuid'\n"; - # don't know what a '!' is there for, also the - # username is of no interest - $src =~ s{/?!$}{$mirror}; + + # the '!' is used to mark the repos_root!/relative/path + $src =~ s{/?!/?}{/}; $src =~ s{/+$}{}; # no trailing slashes please + # username is of no interest $src =~ s{(^[a-z\+]*://)[^/@]*@}{$1}; + my $replace = $ra->{url}; + $replace .= "/$path" if length $path; + my $section = "svn-remote.$self->{repo_id}"; - tmp_config('--add', "$section.svm-source", $src); - tmp_config('--add', "$section.svm-uuid", $uuid); - $self->{svm} = { source => $src , uuid => $uuid }; - return 1; + tmp_config("$section.svm-source", $src); + tmp_config("$section.svm-replace", $replace); + tmp_config("$section.svm-uuid", $uuid); + $self->{svm} = { + source => $src, + uuid => $uuid, + replace => $replace + }; } my $r = $ra->get_latest_revnum; my $path = $self->{path}; - my @tried_a = ($path); + my %tried; while (length $path) { - if ($self->read_svm_props(($ra->get_dir($path, $r))[2])) { - return $ra; + unless ($tried{"$self->{url}/$path"}) { + return $ra if $self->read_svm_props($ra, $path, $r); + $tried{"$self->{url}/$path"} = 1; } - $path =~ s#/?[^/]+$## && push @tried_a, $path; - } - if ($self->read_svm_props(($ra->get_dir('', $r))[2])) { - return $ra; + $path =~ s#/?[^/]+$##; } + die "Path: '$path' should be ''\n" if $path ne ''; + return $ra if $self->read_svm_props($ra, $path, $r); + $tried{"$self->{url}/$path"} = 1; if ($ra->{repos_root} eq $self->{url}) { - die @err, map { " $self->{url}/$_\n" } @tried_a, "\n"; + die @err, (map { " $_\n" } keys %tried), "\n"; } # nope, make sure we're connected to the repository root: my $ok; my @tried_b; $path = $ra->{svn_path}; - $path =~ s#/?[^/]+$##; # we already tried this one above $ra = Git::SVN::Ra->new($ra->{repos_root}); while (length $path) { - $ok = $self->read_svm_props(($ra->get_dir($path, $r))[2]); - last if $ok; - $path =~ s#/?[^/]+$## && push @tried_b, $path; + unless ($tried{"$ra->{url}/$path"}) { + $ok = $self->read_svm_props($ra, $path, $r); + last if $ok; + $tried{"$ra->{url}/$path"} = 1; + } + $path =~ s#/?[^/]+$##; } - $ok = $self->read_svm_props(($ra->get_dir('', $r))[2]) unless $ok; + die "Path: '$path' should be ''\n" if $path ne ''; + $ok ||= $self->read_svm_props($ra, $path, $r); + $tried{"$ra->{url}/$path"} = 1; if (!$ok) { - die @err, map { " $self->{url}/$_\n" } @tried_a, "\n", - map { " $ra->{url}/$_\n" } @tried_b, "\n" + die @err, (map { " $_\n" } keys %tried), "\n"; } Git::SVN::Ra->new($self->{url}); } +sub svnsync { + my ($self) = @_; + return $self->{svnsync} if $self->{svnsync}; + + if ($self->no_metadata) { + die "Can't have both 'noMetadata' and ", + "'useSvnsyncProps' options set!\n"; + } + if ($self->rewrite_root) { + die "Can't have both 'useSvnsyncProps' and 'rewriteRoot' ", + "options set!\n"; + } + + my $svnsync; + # see if we have it in our config, first: + eval { + my $section = "svn-remote.$self->{repo_id}"; + $svnsync = { + url => tmp_config('--get', "$section.svnsync-url"), + uuid => tmp_config('--get', "$section.svnsync-uuid"), + } + }; + if ($svnsync && $svnsync->{url} && $svnsync->{uuid}) { + return $self->{svnsync} = $svnsync; + } + + my $err = "useSvnsyncProps set, but failed to read " . + "svnsync property: svn:sync-from-"; + my $rp = $self->ra->rev_proplist(0); + + my $url = $rp->{'svn:sync-from-url'} or die $err . "url\n"; + $url =~ m{^[a-z\+]+://} or + die "doesn't look right - svn:sync-from-url is '$url'\n"; + + my $uuid = $rp->{'svn:sync-from-uuid'} or die $err . "uuid\n"; + $uuid =~ m{^[0-9a-f\-]{30,}$} or + die "doesn't look right - svn:sync-from-uuid is '$uuid'\n"; + + my $section = "svn-remote.$self->{repo_id}"; + tmp_config('--add', "$section.svnsync-uuid", $uuid); + tmp_config('--add', "$section.svnsync-url", $url); + return $self->{svnsync} = { url => $url, uuid => $uuid }; +} + # this allows us to memoize our SVN::Ra UUID locally and avoid a # remote lookup (useful for 'git svn log'). sub ra_uuid { @@ -1141,6 +1350,9 @@ sub ra { if ($self->no_metadata) { die "Can't have both 'noMetadata' and ", "'useSvmProps' options set!\n"; + } elsif ($self->use_svnsync_props) { + die "Can't have both 'useSvnsyncProps' and ", + "'useSvmProps' options set!\n"; } $ra = $self->_set_svm_vars($ra); $self->{-want_revprops} = 1; @@ -1152,8 +1364,10 @@ sub rel_path { my ($self) = @_; my $repos_root = $self->ra->{repos_root}; return $self->{path} if ($self->{url} eq $repos_root); - die "BUG: rel_path failed! repos_root: $repos_root, Ra URL: ", - $self->ra->{url}, " path: $self->{path}, URL: $self->{url}\n"; + my $url = $self->{url} . + (length $self->{path} ? "/$self->{path}" : $self->{path}); + $url =~ s!^\Q$repos_root\E(?:/+|$)!!g; + $url; } sub traverse_ignore { @@ -1162,7 +1376,7 @@ sub traverse_ignore { my $ra = $self->ra; my ($dirent, undef, $props) = $ra->get_dir($path, $r); my $p = $path; - $p =~ s#^\Q$ra->{svn_path}\E/##; + $p =~ s#^\Q$self->{path}\E(/|$)##; print $fh length $p ? "\n# $p\n" : "\n# /\n"; if (my $s = $props->{'svn:ignore'}) { $s =~ s/[\r\n]+/\n/g; @@ -1176,7 +1390,7 @@ sub traverse_ignore { } } foreach (sort keys %$dirent) { - next if $dirent->{$_}->kind != $SVN::Node::dir; + next if $dirent->{$_}->{kind} != $SVN::Node::dir; $self->traverse_ignore($fh, "$path/$_", $r); } } @@ -1235,20 +1449,29 @@ sub get_fetch_range { sub tmp_config { my (@args) = @_; - my $config = "$ENV{GIT_DIR}/svn/config"; - unless (-f $config) { - open my $fh, '>', $config or - die "Can't open $config: $!\n"; - print $fh "; This file is used internally by git-svn\n" or - die "Couldn't write to $config: $!\n"; - print $fh "; You should not have to edit it\n" or - die "Couldn't write to $config: $!\n"; - close $fh or die "Couldn't close $config: $!\n"; + my $old_def_config = "$ENV{GIT_DIR}/svn/config"; + my $config = "$ENV{GIT_DIR}/svn/.metadata"; + if (-e $old_def_config && ! -e $config) { + rename $old_def_config, $config or + die "Failed rename $old_def_config => $config: $!\n"; } my $old_config = $ENV{GIT_CONFIG}; $ENV{GIT_CONFIG} = $config; $@ = undef; - my @ret = eval { command('config', @args) }; + my @ret = eval { + unless (-f $config) { + mkfile($config); + open my $fh, '>', $config or + die "Can't open $config: $!\n"; + print $fh "; This file is used internally by ", + "git-svn\n" or die + "Couldn't write to $config: $!\n"; + print $fh "; You should not have to edit it\n" or + die "Couldn't write to $config: $!\n"; + close $fh or die "Couldn't close $config: $!\n"; + } + command('config', @args); + }; my $err = $@; if (defined $old_config) { $ENV{GIT_CONFIG} = $old_config; @@ -1264,7 +1487,11 @@ sub tmp_index_do { my $old_index = $ENV{GIT_INDEX_FILE}; $ENV{GIT_INDEX_FILE} = $self->{index}; $@ = undef; - my @ret = eval { &$sub }; + my @ret = eval { + my ($dir, $base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#); + mkpath([$dir]) unless -d $dir; + &$sub; + }; my $err = $@; if (defined $old_index) { $ENV{GIT_INDEX_FILE} = $old_index; @@ -1283,10 +1510,11 @@ sub assert_index_clean { my $x = command_oneline('write-tree'); my ($y) = (command(qw/cat-file commit/, $treeish) =~ /^tree ($::sha1)/mo); - if ($y ne $x) { - unlink $self->{index} or croak $!; - command_noisy('read-tree', $treeish); - } + return if $y eq $x; + + warn "Index mismatch: $y != $x\nrereading $treeish\n"; + unlink $self->{index} or die "unlink $self->{index}: $!\n"; + command_noisy('read-tree', $treeish); $x = command_oneline('write-tree'); if ($y ne $x) { ::fatal "trees ($treeish) $y != $x\n", @@ -1322,6 +1550,26 @@ sub get_commit_parents { @ret; } +sub rewrite_root { + my ($self) = @_; + return $self->{-rewrite_root} if exists $self->{-rewrite_root}; + my $k = "svn-remote.$self->{repo_id}.rewriteRoot"; + my $rwr = eval { command_oneline(qw/config --get/, $k) }; + if ($rwr) { + $rwr =~ s#/+$##; + if ($rwr !~ m#^[a-z\+]+://#) { + die "$rwr is not a valid URL (key: $k)\n"; + } + } + $self->{-rewrite_root} = $rwr; +} + +sub metadata_url { + my ($self) = @_; + ($self->rewrite_root || $self->{url}) . + (length $self->{path} ? '/' . $self->{path} : ''); +} + sub full_url { my ($self) = @_; $self->{url} . (length $self->{path} ? '/' . $self->{path} : ''); @@ -1464,14 +1712,17 @@ sub find_parent_branch { } my ($r0, $parent) = $gs->find_rev_before($r, 1); if (!defined $r0 || !defined $parent) { - $gs->fetch(0, $r); + my ($base, $head) = parse_revision_argument(0, $r); + if ($base <= $r) { + $gs->fetch($base, $r); + } ($r0, $parent) = $gs->last_rev_commit; } if (defined $r0 && defined $parent) { print STDERR "Found branch parent: ($self->{ref_id}) $parent\n"; - $self->assert_index_clean($parent); my $ed; if ($self->ra->can_do_switch) { + $self->assert_index_clean($parent); print STDERR "Following parent with do_switch\n"; # do_switch works with svn/trunk >= r22312, but that # is not included with SVN 1.4.3 (the latest version @@ -1620,19 +1871,39 @@ sub make_log_entry { my ($name, $email) = defined $::users{$author} ? @{$::users{$author}} : ($author, undef); if (defined $headrev && $self->use_svm_props) { + if ($self->rewrite_root) { + die "Can't have both 'useSvmProps' and 'rewriteRoot' ", + "options set!\n"; + } my ($uuid, $r) = $headrev =~ m{^([a-f\d\-]{30,}):(\d+)$}; - if ($uuid ne $self->{svm}->{uuid}) { + # we don't want "SVM: initializing mirror for junk" ... + return undef if $r == 0; + my $svm = $self->svm; + if ($uuid ne $svm->{uuid}) { die "UUID mismatch on SVM path:\n", - "expected: $self->{svm}->{uuid}\n", + "expected: $svm->{uuid}\n", " got: $uuid\n"; } - my $full_url = $self->{svm}->{source}; - $full_url .= "/$self->{path}" if length $self->{path}; + my $full_url = $self->full_url; + $full_url =~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or + die "Failed to replace '$svm->{replace}' with ", + "'$svm->{source}' in $full_url\n"; + # throw away username for storing in records + remove_username($full_url); $log_entry{metadata} = "$full_url\@$r $uuid"; $log_entry{svm_revision} = $r; $email ||= "$author\@$uuid" + } elsif ($self->use_svnsync_props) { + my $full_url = $self->svnsync->{url}; + $full_url .= "/$self->{path}" if length $self->{path}; + remove_username($full_url); + my $uuid = $self->svnsync->{uuid}; + $log_entry{metadata} = "$full_url\@$rev $uuid"; + $email ||= "$author\@$uuid" } else { - $log_entry{metadata} = $self->full_url . "\@$rev " . + my $url = $self->metadata_url; + remove_username($url); + $log_entry{metadata} = "$url\@$rev " . $self->ra->get_uuid; $email ||= "$author\@" . $self->ra->get_uuid; } @@ -1676,6 +1947,8 @@ sub set_tree { sub rebuild { my ($self) = @_; my $db_path = $self->db_path; + return if (-e $db_path && ! -z $db_path); + return unless ::verify_ref($self->refname.'^0'); if (-f $self->{db_root}) { rename $self->{db_root}, $db_path or die "rename $self->{db_root} => $db_path failed: $!\n"; @@ -1688,12 +1961,14 @@ sub rebuild { my ($rev_list, $ctx) = command_output_pipe("rev-list", $self->refname); my $latest; my $full_url = $self->full_url; + remove_username($full_url); my $svn_uuid; while (<$rev_list>) { chomp; my $c = $_; die "Non-SHA1: $c\n" unless $c =~ /^$::sha1$/o; my ($url, $rev, $uuid) = ::cmt_metadata($c); + remove_username($url); # ignore merges (from set-tree) next if (!defined $rev || !$uuid); @@ -1786,6 +2061,7 @@ sub rev_db_set { } close $fh or croak $!; if ($update_ref) { + $_head = $self; command_noisy('update-ref', '-m', "r$rev", $self->refname, $commit); } @@ -1801,6 +2077,7 @@ sub rev_db_set { sub rev_db_max { my ($self) = @_; + $self->rebuild; my $db_path = $self->db_path; my @stat = stat $db_path or return 0; ($stat[7] % 41) == 0 or die "$db_path inconsistent size: $stat[7]\n"; @@ -1846,7 +2123,7 @@ sub _new { $_[1] = $repo_id = sanitize_remote_name($repo_id); my $dir = "$ENV{GIT_DIR}/svn/$ref_id"; $_[3] = $path = '' unless (defined $path); - mkpath([$dir]); + mkpath(["$ENV{GIT_DIR}/svn"]); bless { ref_id => $ref_id, dir => $dir, index => "$dir/index", path => $path, config => "$ENV{GIT_DIR}/svn/config", @@ -1865,6 +2142,10 @@ sub uri_encode { $f } +sub remove_username { + $_[0] =~ s{^([^:]*://)[^@]+@}{$1}; +} + package Git::SVN::Prompt; use strict; use warnings; @@ -2191,9 +2472,9 @@ sub close_file { my $got = $md5->hexdigest; die "Checksum mismatch: $path\n", "expected: $exp\n got: $got\n" if ($got ne $exp); - seek($fh, 0, 0) or croak $!; + sysseek($fh, 0, 0) or croak $!; if ($fb->{mode_b} == 120000) { - read($fh, my $buf, 5) == 5 or croak $!; + sysread($fh, my $buf, 5) == 5 or croak $!; $buf eq 'link ' or die "$path has mode 120000", "but is not a link\n"; } @@ -2599,16 +2880,15 @@ sub apply_diff { } package Git::SVN::Ra; -use vars qw/@ISA $config_dir/; +use vars qw/@ISA $config_dir $_log_window_size/; use strict; use warnings; -my ($can_do_switch); -my $RA; +my ($can_do_switch, %ignored_err, $RA); BEGIN { # enforce temporary pool usage for some simple functions my $e; - foreach (qw/get_latest_revnum get_uuid get_repos_root/) { + foreach (qw/rev_proplist get_latest_revnum get_uuid get_repos_root/) { $e .= "sub $_ { my \$self = shift; my \$pool = SVN::Pool->new; @@ -2617,35 +2897,14 @@ BEGIN { wantarray ? \@ret : \$ret[0]; }\n"; } - # get_dir needs $pool held in cache for dirents to work, - # check_path is cacheable and rev_proplist is close enough - # for our purposes. - foreach (qw/check_path get_dir rev_proplist/) { - $e .= "my \%${_}_cache; my \$${_}_rev = 0; sub $_ { - my \$self = shift; - my \$r = pop; - my \$k = join(\"\\0\", \@_); - if (my \$x = \$${_}_cache{\$r}->{\$k}) { - return wantarray ? \@\$x : \$x->[0]; - } - my \$pool = SVN::Pool->new; - my \@ret = \$self->SUPER::$_(\@_, \$r, \$pool); - if (\$r != \$${_}_rev) { - \%${_}_cache = ( pool => [] ); - \$${_}_rev = \$r; - } - \$${_}_cache{\$r}->{\$k} = \\\@ret; - push \@{\$${_}_cache{pool}}, \$pool; - wantarray ? \@ret : \$ret[0]; }\n"; - } - $e .= "\n1;"; - eval $e or die $@; + eval "$e; 1;" or die $@; } sub new { my ($class, $url) = @_; $url =~ s!/+$!!; return $RA if ($RA && $RA->{url} eq $url); + $RA->{pool}->clear if $RA; SVN::_Core::svn_config_ensure($config_dir, undef); my ($baton, $callbacks) = SVN::Core::auth_open_helper([ @@ -2671,9 +2930,47 @@ sub new { $self->{svn_path} = $url; $self->{repos_root} = $self->get_repos_root; $self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##; + $self->{cache} = { check_path => { r => 0, data => {} }, + get_dir => { r => 0, data => {} } }; $RA = bless $self, $class; } +sub check_path { + my ($self, $path, $r) = @_; + my $cache = $self->{cache}->{check_path}; + if ($r == $cache->{r} && exists $cache->{data}->{$path}) { + return $cache->{data}->{$path}; + } + my $pool = SVN::Pool->new; + my $t = $self->SUPER::check_path($path, $r, $pool); + $pool->clear; + if ($r != $cache->{r}) { + %{$cache->{data}} = (); + $cache->{r} = $r; + } + $cache->{data}->{$path} = $t; +} + +sub get_dir { + my ($self, $dir, $r) = @_; + my $cache = $self->{cache}->{get_dir}; + if ($r == $cache->{r}) { + if (my $x = $cache->{data}->{$dir}) { + return wantarray ? @$x : $x->[0]; + } + } + my $pool = SVN::Pool->new; + my ($d, undef, $props) = $self->SUPER::get_dir($dir, $r, $pool); + my %dirents = map { $_ => { kind => $d->{$_}->kind } } keys %$d; + $pool->clear; + if ($r != $cache->{r}) { + %{$cache->{data}} = (); + $cache->{r} = $r; + } + $cache->{data}->{$dir} = [ \%dirents, $r, $props ]; + wantarray ? (\%dirents, $r, $props) : \%dirents; +} + sub DESTROY { # do not call the real DESTROY since we store ourselves in $RA } @@ -2698,6 +2995,10 @@ sub gs_do_update { my $new = ($rev_a == $rev_b); my $path = $gs->{path}; + if ($new && -e $gs->{index}) { + unlink $gs->{index} or die + "Couldn't unlink index: $gs->{index}: $!\n"; + } my $pool = SVN::Pool->new; $editor->set_path_strip($path); my (@pc) = split m#/#, $path; @@ -2763,15 +3064,12 @@ sub gs_do_switch { sub gs_fetch_loop_common { my ($self, $base, $head, $gsv, $globs) = @_; return if ($base > $head); - my $inc = 1000; + my $inc = $_log_window_size; my ($min, $max) = ($base, $head < $base + $inc ? $head : $base + $inc); 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) { @@ -2847,6 +3145,9 @@ sub gs_fetch_loop_common { } next unless $gs->match_paths($paths, $r); $gs->{logged_rev_props} = $logged; + if (my $last_commit = $gs->last_commit) { + $gs->assert_index_clean($last_commit); + } my $log_entry = $gs->do_fetch($paths, $r); if ($log_entry) { $gs->do_git_commit($log_entry); @@ -2884,7 +3185,7 @@ sub match_globs { return unless scalar @x == 3; my $dirents = $x[0]; foreach my $de (keys %$dirents) { - next if $dirents->{$de}->kind != $SVN::Node::dir; + next if $dirents->{$de}->{kind} != $SVN::Node::dir; my $p = $g->{path}->full_path($de); next if $exists->{$p}; next if (length $g->{path}->{right} && @@ -2901,7 +3202,8 @@ sub match_globs { } } foreach (keys %$paths) { - if (/$g->{path}->{left_regex}/) { + if (/$g->{path}->{left_regex}/ && + !/$g->{path}->{regex}/) { next if $paths->{$_}->{action} !~ /^[AR]$/; get_dir_check($self, $exists, $g, $r); } @@ -2909,6 +3211,8 @@ sub match_globs { my $p = $1; my $pathname = $g->{path}->full_path($p); next if $exists->{$pathname}; + next if ($self->check_path($pathname, $r) != + $SVN::Node::dir); $exists->{$pathname} = Git::SVN->init( $self->{url}, $pathname, undef, $g->{ref}->full_path($p), 1); @@ -2969,6 +3273,16 @@ sub skip_unknown_revs { # 175007 - http(s):// (this repo required authorization, too...) # More codes may be discovered later... if ($errno == 175007 || $errno == 175002 || $errno == 160013) { + my $err_key = $err->expanded_message; + # revision numbers change every time, filter them out + $err_key =~ s/\d+/\0/g; + $err_key = "$errno\0$err_key"; + unless ($ignored_err{$err_key}) { + warn "W: Ignoring error from SVN, path probably ", + "does not exist: ($errno): ", + $err->expanded_message,"\n"; + $ignored_err{$err_key} = 1; + } return; } die "Error from SVN, ($errno): ", $err->expanded_message,"\n"; @@ -3003,12 +3317,19 @@ my $l_fmt; sub cmt_showable { my ($c) = @_; return 1 if defined $c->{r}; + + # big commit message got truncated by the 16k pretty buffer in rev-list if ($c->{l} && $c->{l}->[-1] eq "...\n" && $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) { + @{$c->{l}} = (); my @log = command(qw/cat-file commit/, $c->{c}); - shift @log while ($log[0] ne "\n"); + + # shift off the headers + shift @log while ($log[0] ne ''); shift @log; - @{$c->{l}} = grep !/^git-svn-id: /, @log; + + # TODO: make $c->{l} not have a trailing newline in the future + @{$c->{l}} = map { "$_\n" } grep !/^git-svn-id: /, @log; (undef, $c->{r}, undef) = ::extract_metadata( (grep(/^git-svn-id: /, @log))[-1]); @@ -3062,16 +3383,8 @@ sub git_svn_log_cmd { last; } - my $url; - my ($fh, $ctx) = command_output_pipe('rev-list', $head); - while (<$fh>) { - chomp; - $url = (::cmt_metadata($_))[0]; - last if defined $url; - } - close $fh; # break the pipe - - my $gs = Git::SVN->find_by_url($url) || Git::SVN->_new; + my ($url, $rev, $uuid, $gs) = ::working_head_info($head); + $gs ||= Git::SVN->_new; my @cmd = (qw/log --abbrev-commit --pretty=raw --default/, $gs->refname); push @cmd, '-r' unless $non_recursive; @@ -3235,7 +3548,7 @@ sub show_commit_normal { print "\n"; } - foreach my $x (qw/raw diff/) { + foreach my $x (qw/raw stat diff/) { if ($c->{$x}) { print "\n"; print $_ foreach @{$c->{$x}} @@ -3267,7 +3580,7 @@ sub cmd_show_log { @args = (git_svn_log_cmd($r_min, $r_max, @args), @args); my $log = command_output_pipe(@args); run_pager(); - my (@k, $c, $d); + my (@k, $c, $d, $stat); my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/; while (<$log>) { if (/^${esc_color}commit ($::sha1_short)/o) { @@ -3295,6 +3608,13 @@ sub cmd_show_log { push @{$c->{diff}}, $_; } elsif ($d) { push @{$c->{diff}}, $_; + } elsif (/^\ .+\ \|\s*\d+\ $esc_color[\+\-]* + $esc_color*[\+\-]*$esc_color$/x) { + $stat = 1; + push @{$c->{stat}}, $_; + } elsif ($stat && /^ \d+ files changed, \d+ insertions/) { + push @{$c->{stat}}, $_; + $stat = undef; } elsif (/^${esc_color} (git-svn-id:.+)$/o) { ($c->{url}, $c->{r}, undef) = ::extract_metadata($1); } elsif (s/^${esc_color} //o) {