$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
$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';
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;
$_message, $_file,
$_template, $_shared,
$_version, $_fetch_all,
- $_merge, $_strategy, $_dry_run,
- $_prefix);
+ $_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,
'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,
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 ],
'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,
%cmt_opts, %fc_opts } ],
'set-tree' => [ \&cmd_set_tree,
"Set an SVN repository to a git tree-ish",
# 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,
'color' => \$Git::SVN::Log::color,
'pager=s' => \$Git::SVN::Log::pager,
} ],
+ '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,
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();
$cmd{$cmd}->[0]->(@ARGV);
};
fatal $@ if $@;
+post_fetch_checkout();
exit 0;
####################### primary functions ######################
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$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
}
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 {
}
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) {
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);
+ my $c = $refs[-1];
my $last_rev;
foreach my $d (@refs) {
if (!verify_ref("$d~1")) {
"now resync your SVN::Mirror repository.\n";
return;
}
- $gs->fetch;
+ $_fetch_all ? $gs->fetch_all : $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;
+ @finish = rebase_cmd();
print STDERR "W: HEAD and ", $gs->refname, " differ, ",
"using @finish:\n", "@diff";
} else {
command_noisy(@finish, $gs->refname);
}
+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 {
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:
########################### 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#/+$##;
command(qw/cat-file commit/, shift)))[-1]);
}
+sub working_head_info {
+ my ($head, $refs) = @_;
+ my ($fh, $ctx) = command_output_pipe('rev-list', $head);
+ while (<$fh>) {
+ chomp;
+ my ($url, $rev, $uuid) = cmt_metadata($_);
+ 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 $_) {
+ close $fh; # break the pipe
+ return ($url, $rev, $uuid, $gs);
+ }
+ }
+ }
+ unshift @$refs, $_ 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/;
# 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 $_ {
sub fetch_all {
my ($repo_id, $remotes) = @_;
+ if (ref $repo_id) {
+ my $gs = $repo_id;
+ $repo_id = undef;
+ $repo_id = $gs->{repo_id};
+ }
+ $remotes ||= read_all_remotes();
my $remote = $remotes->{$repo_id} or
die "[svn-remote \"$repo_id\"] unknown\n";
my $fetch = $remote->{fetch};
"svn-remote.$repo_id.${t}-maxRev") };
if (defined $max_rev && ($max_rev < $base)) {
$base = $max_rev;
+ } elsif (!defined $max_rev) {
+ $base = 0;
}
}
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;
sub find_by_url { # repos_root and, path are optional
my ($class, $full_url, $repos_root, $path) = @_;
+ return undef unless defined $full_url;
my $remotes = read_all_remotes();
if (defined $full_url && defined $repos_root && !defined $path) {
$path = $full_url;
$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;
}
$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};
}
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 {
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;
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 {
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;
sub tmp_config {
my (@args) = @_;
- my $config = "$ENV{GIT_DIR}/svn/config";
+ 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;
@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} : '');
}
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
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};
+ my $uuid = $self->svnsync->{uuid};
+ $log_entry{metadata} = "$full_url\@$rev $uuid";
+ $email ||= "$author\@$uuid"
} else {
- $log_entry{metadata} = $self->full_url . "\@$rev " .
+ $log_entry{metadata} = $self->metadata_url. "\@$rev " .
$self->ra->get_uuid;
$email ||= "$author\@" . $self->ra->get_uuid;
}
sub rebuild {
my ($self) = @_;
my $db_path = $self->db_path;
+ return if (-e $db_path && ! -z $db_path);
+ return unless ::verify_ref($self->refname.'^0');
if (-f $self->{db_root}) {
rename $self->{db_root}, $db_path or die
"rename $self->{db_root} => $db_path failed: $!\n";
my ($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);
}
close $fh or croak $!;
if ($update_ref) {
+ $_head = $self;
command_noisy('update-ref', '-m', "r$rev",
$self->refname, $commit);
}
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";
$f
}
+sub remove_username {
+ $_[0] =~ s{^([^:]*://)[^@]+@}{$1};
+}
+
package Git::SVN::Prompt;
use strict;
use warnings;
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 ($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([
my $new = ($rev_a == $rev_b);
my $path = $gs->{path};
+ if ($new && -e $gs->{index}) {
+ unlink $gs->{index} or die
+ "Couldn't unlink index: $gs->{index}: $!\n";
+ }
my $pool = SVN::Pool->new;
$editor->set_path_strip($path);
my (@pc) = split m#/#, $path;
my $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);
# 175007 - http(s):// (this repo required authorization, too...)
# More codes may be discovered later...
if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
- warn "W: Ignoring error from SVN, path probably ",
- "does not exist: ($errno): ",
- $err->expanded_message,"\n";
+ 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";
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]);
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;
print "\n";
}
- foreach my $x (qw/raw diff/) {
+ foreach my $x (qw/raw stat diff/) {
if ($c->{$x}) {
print "\n";
print $_ foreach @{$c->{$x}}
@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) {
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) {