$ENV{LC_ALL} = 'C';
$| = 1; # unbuffer STDOUT
+sub fatal (@) { print STDERR $@; exit 1 }
# If SVN:: library support is added, please make the dependencies
# optional and preserve the capability to use the command-line client.
# use eval { require SVN::... } to make it lazy load
memoize('cmt_metadata');
memoize('get_commit_time');
-my ($SVN_PATH, $SVN, $SVN_LOG, $_use_lib);
+my ($SVN, $_use_lib);
+
+sub nag_lib {
+ print STDERR <<EOF;
+! Please consider installing the SVN Perl libraries (version 1.1.0 or
+! newer). You will generally get better performance and fewer bugs,
+! especially if you:
+! 1) have a case-insensitive filesystem
+! 2) replace symlinks with files (and vice-versa) in commits
+
+EOF
+}
+
$_use_lib = 1 unless $ENV{GIT_SVN_NO_LIB};
libsvn_load();
+nag_lib() unless $_use_lib;
+
my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS};
my $sha1 = qr/[a-f\d]{40}/;
my $sha1_short = qr/[a-f\d]{4,40}/;
$_template, $_shared, $_no_default_regex, $_no_graft_copy,
$_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit,
$_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m,
- $_merge, $_strategy, $_dry_run);
+ $_merge, $_strategy, $_dry_run, $_ignore_nodate, $_non_recursive,
+ $_username, $_config_dir, $_no_auth_cache, $_xfer_delta);
my (@_branch_from, %tree_map, %users, %rusers, %equiv);
my ($_svn_co_url_revs, $_svn_pg_peg_revs);
my @repo_path_split_cache;
'repack:i' => \$_repack,
'no-metadata' => \$_no_metadata,
'quiet|q' => \$_q,
+ 'username=s' => \$_username,
+ 'config-dir=s' => \$_config_dir,
+ 'no-auth-cache' => \$_no_auth_cache,
+ 'ignore-nodate' => \$_ignore_nodate,
'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
my ($_trunk, $_tags, $_branches);
'incremental' => \$_incremental,
'oneline' => \$_oneline,
'show-commit' => \$_show_commit,
+ 'non-recursive' => \$_non_recursive,
'authors-file|A=s' => \$_authors,
} ],
'commit-diff' => [ \&commit_diff, 'Commit a diff between two trees',
{ 'message|m=s' => \$_message,
'file|F=s' => \$_file,
+ 'revision|r=s' => \$_revision,
%cmt_opts } ],
dcommit => [ \&dcommit, 'Commit several diffs to merge with upstream',
{ 'merge|m|M' => \$_merge,
foreach (sort keys %cmd) {
next if $cmd && $cmd ne $_;
- print $fd ' ',pack('A13',$_),$cmd{$_}->[1],"\n";
+ print $fd ' ',pack('A17',$_),$cmd{$_}->[1],"\n";
foreach (keys %{$cmd{$_}->[2]}) {
# prints out arguments as they should be passed:
my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
- print $fd ' ' x 17, join(', ', map { length $_ > 1 ?
+ print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
"--$_" : "-$_" }
split /\|/,$_)," $x\n";
}
my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`);
next if (!@commit); # skip merges
my ($url, $rev, $uuid) = extract_metadata($commit[$#commit]);
- if (!$rev || !$uuid) {
+ if (!defined $rev || !$uuid) {
croak "Unable to extract revision or UUID from ",
"$c, $commit[$#commit]\n";
}
sub fetch_lib {
my (@parents) = @_;
$SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
- my $repo;
- ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
- $SVN_LOG ||= libsvn_connect($repo);
- $SVN ||= libsvn_connect($repo);
+ $SVN ||= libsvn_connect($SVN_URL);
my ($last_rev, $last_commit) = svn_grab_base_rev();
my ($base, $head) = libsvn_parse_revision($last_rev);
if ($base > $head) {
# performance sucks with it enabled, so it's much
# faster to fetch revision ranges instead of relying
# on the limiter.
- libsvn_get_log($SVN_LOG, '/'.$SVN_PATH,
+ libsvn_get_log(libsvn_dup_ra($SVN), [''],
$min, $max, 0, 1, 1,
sub {
my $log_msg;
my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
my $repo;
- ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
set_svn_commit_env();
foreach my $c (@revs) {
my $log_msg = get_commit_message($c, $commit_msg);
# can't track down... (it's probably in the SVN code)
defined(my $pid = open my $fh, '-|') or croak $!;
if (!$pid) {
- $SVN_LOG = libsvn_connect($repo);
- $SVN = libsvn_connect($repo);
my $ed = SVN::Git::Editor->new(
{ r => $r_last,
- ra => $SVN,
+ ra => libsvn_dup_ra($SVN),
c => $c,
- svn_path => $SVN_PATH
+ svn_path => $SVN->{svn_path},
},
$SVN->get_commit_editor(
$log_msg->{msg},
$no = 1;
}
}
- close $fh or croak $?;
+ close $fh or exit 1;
if (! defined $r_new && ! defined $cmt_new) {
unless ($no) {
die "Failed to parse revision information\n";
sub dcommit {
my $gs = "refs/remotes/$GIT_SVN";
chomp(my @refs = safe_qx(qw/git-rev-list --no-merges/, "$gs..HEAD"));
+ my $last_rev;
foreach my $d (reverse @refs) {
+ if (quiet_run('git-rev-parse','--verify',"$d~1") != 0) {
+ die "Commit $d\n",
+ "has no parent commit, and therefore ",
+ "nothing to diff against.\n",
+ "You should be working from a repository ",
+ "originally created by git-svn\n";
+ }
+ unless (defined $last_rev) {
+ (undef, $last_rev, undef) = cmt_metadata("$d~1");
+ unless (defined $last_rev) {
+ die "Unable to extract revision information ",
+ "from commit $d~1\n";
+ }
+ }
if ($_dry_run) {
print "diff-tree $d~1 $d\n";
} else {
- commit_diff("$d~1", $d);
+ if (my $r = commit_diff("$d~1", $d, undef, $last_rev)) {
+ $last_rev = $r;
+ } # else: no changes, same $last_rev
}
}
return if $_dry_run;
} else {
print "No changes between current HEAD and $gs\n",
"Hard resetting to the latest $gs\n";
- @finish = qw/reset --hard/;
+ @finish = qw/reset --mixed/;
}
sys('git', @finish, $gs);
}
sub show_ignore_lib {
my $repo;
- ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
- $SVN ||= libsvn_connect($repo);
+ $SVN ||= libsvn_connect($SVN_URL);
my $r = defined $_revision ? $_revision : $SVN->get_latest_revnum;
- libsvn_traverse_ignore(\*STDOUT, $SVN_PATH, $r);
+ libsvn_traverse_ignore(\*STDOUT, $SVN->{svn_path}, $r);
}
sub graft_branches {
}
$_trunk = $url . $_trunk;
}
+ my $ch_id;
if ($GIT_SVN eq 'git-svn') {
- print "GIT_SVN_ID set to 'trunk' for $_trunk\n";
+ $ch_id = 1;
$GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
}
init_vars();
- init($_trunk);
+ unless (-d $GIT_SVN_DIR) {
+ print "GIT_SVN_ID set to 'trunk' for $_trunk\n" if $ch_id;
+ init($_trunk);
+ sys('git-repo-config', 'svn.trunk', $_trunk);
+ }
complete_url_ls_init($url, $_branches, '--branches/-b', '');
complete_url_ls_init($url, $_tags, '--tags/-t', 'tags/');
}
# ignore
} elsif (/^:\d{6} \d{6} $sha1_short/o) {
push @{$c->{raw}}, $_;
+ } elsif (/^[ACRMDT]\t/) {
+ # we could add $SVN->{svn_path} here, but that requires
+ # remote access at the moment (repo_path_split)...
+ s#^([ACRMDT])\t# $1 #;
+ push @{$c->{changed}}, $_;
} elsif (/^diff /) {
$d = 1;
push @{$c->{diff}}, $_;
} elsif ($d) {
push @{$c->{diff}}, $_;
} elsif (/^ (git-svn-id:.+)$/) {
- (undef, $c->{r}, undef) = extract_metadata($1);
+ ($c->{url}, $c->{r}, undef) = extract_metadata($1);
} elsif (s/^ //) {
push @{$c->{l}}, $_;
}
print STDERR "Needed URL or usable git-svn id command-line\n";
commit_diff_usage();
}
+ my $r = shift;
+ unless (defined $r) {
+ if (defined $_revision) {
+ $r = $_revision
+ } else {
+ die "-r|--revision is a required argument\n";
+ }
+ }
if (defined $_message && defined $_file) {
print STDERR "Both --message/-m and --file/-F specified ",
"for the commit message.\n",
$_message ||= get_commit_message($tb,
"$GIT_DIR/.svn-commit.tmp.$$")->{msg};
}
- my $repo;
- ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
- $SVN_LOG ||= libsvn_connect($repo);
- $SVN ||= libsvn_connect($repo);
+ $SVN ||= libsvn_connect($SVN_URL);
+ if ($r eq 'HEAD') {
+ $r = $SVN->get_latest_revnum;
+ } elsif ($r !~ /^\d+$/) {
+ die "revision argument: $r not understood by git-svn\n";
+ }
my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
- my $ed = SVN::Git::Editor->new({ r => $SVN->get_latest_revnum,
- ra => $SVN, c => $tb,
- svn_path => $SVN_PATH
+ my $rev_committed;
+ my $ed = SVN::Git::Editor->new({ r => $r,
+ ra => libsvn_dup_ra($SVN),
+ c => $tb,
+ svn_path => $SVN->{svn_path}
},
$SVN->get_commit_editor($_message,
- sub {print "Committed $_[0]\n"},@lock)
+ sub {
+ $rev_committed = $_[0];
+ print "Committed $_[0]\n";
+ }, @lock)
);
- my $mods = libsvn_checkout_tree($ta, $tb, $ed);
- if (@$mods == 0) {
- print "No changes\n$ta == $tb\n";
- $ed->abort_edit;
- } else {
- $ed->close_edit;
- }
+ eval {
+ my $mods = libsvn_checkout_tree($ta, $tb, $ed);
+ if (@$mods == 0) {
+ print "No changes\n$ta == $tb\n";
+ $ed->abort_edit;
+ } else {
+ $ed->close_edit;
+ }
+ };
+ fatal "$@\n" if $@;
+ $_message = $_file = undef;
+ return $rev_committed;
}
########################### utility functions #########################
my ($r_min, $r_max) = @_;
my @cmd = (qw/git-log --abbrev-commit --pretty=raw
--default/, "refs/remotes/$GIT_SVN");
- push @cmd, '--summary' if $_verbose;
+ push @cmd, '-r' unless $_non_recursive;
+ push @cmd, qw/--raw --name-status/ if $_verbose;
return @cmd unless defined $r_max;
if ($r_max == $r_min) {
push @cmd, '--max-count=1';
my ($c_min, $c_max);
$c_max = revdb_get($REVDB, $r_max);
$c_min = revdb_get($REVDB, $r_min);
- if ($c_min && $c_max) {
+ if (defined $c_min && defined $c_max) {
if ($r_max > $r_max) {
push @cmd, "$c_min..$c_max";
} else {
print STDERR "W: Unrecognized URL: $u\n";
die "This should never happen\n";
}
+ # don't try to init already existing refs
my $id = $pfx.$1;
- print "init $u => $id\n";
$GIT_SVN = $ENV{GIT_SVN_ID} = $id;
init_vars();
- init($u);
+ unless (-d $GIT_SVN_DIR) {
+ print "init $u => $id\n";
+ init($u);
+ }
}
exit 0;
}
waitpid $pid, 0;
croak $? if $?;
+ my ($n) = ($switch =~ /^--(\w+)/);
+ sys('git-repo-config', "svn.$n", $var);
}
sub common_prefix {
my $tree_paths = $l_map->{$u};
my $pfx = common_prefix([keys %$tree_paths]);
my ($repo, $path) = repo_path_split($u.$pfx);
- $SVN_LOG ||= libsvn_connect($repo);
- $SVN ||= libsvn_connect($repo);
+ $SVN = libsvn_connect($repo);
my ($base, $head) = libsvn_parse_revision();
my $inc = 1000;
$SVN::Error::handler = \&libsvn_skip_unknown_revs;
while (1) {
my $pool = SVN::Pool->new;
- libsvn_get_log($SVN_LOG, "/$path", $min, $max, 0, 1, 1,
+ libsvn_get_log(libsvn_dup_ra($SVN), [$path],
+ $min, $max, 0, 1, 1,
sub {
libsvn_graft_file_copies($grafts, $tree_paths,
$path, @_);
return ($u, $full_url);
}
}
-
if ($_use_lib) {
my $tmp = libsvn_connect($full_url);
- my $url = $tmp->get_repos_root;
- $full_url =~ s#^\Q$url\E/*##;
- push @repo_path_split_cache, qr/^(\Q$url\E)/;
- return ($url, $full_url);
+ return ($tmp->{repos_root}, $tmp->{svn_path});
} else {
my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i);
$path =~ s#^/+##;
}
my @status = grep(!/^Performing status on external/,(`svn status`));
@status = grep(!/^\s*$/,@status);
+ @status = grep(!/^X/,@status) if $_no_ignore_ext;
if (scalar @status) {
print STDERR "Tree ($SVN_WC) is not clean:\n";
print STDERR $_ foreach @status;
apply_mod_line_blob($m);
svn_check_prop_executable($m);
} elsif ($m->{chg} eq 'T') {
- sys(qw(svn rm --force),$m->{file_b});
- apply_mod_line_blob($m);
- sys(qw(svn add), $m->{file_b});
svn_check_prop_executable($m);
+ apply_mod_line_blob($m);
+ if ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) {
+ sys(qw(svn propdel svn:special), $m->{file_b});
+ } else {
+ sys(qw(svn propset svn:special *),$m->{file_b});
+ }
} elsif ($m->{chg} eq 'A') {
svn_ensure_parent_path( $m->{file_b} );
apply_mod_line_blob($m);
my $rev = $1;
my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3);
($lines) = ($lines =~ /(\d+)/);
+ $date = '1970-01-01 00:00:00 +0000'
+ if ($_ignore_nodate && $date eq '(no date)');
my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
/(\d{4})\-(\d\d)\-(\d\d)\s
(\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
while (<$authors>) {
chomp;
- next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
+ next unless /^(\S+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
my ($user, $name, $email) = ($1, $2, $3);
$users{$user} = [$name, $email];
}
my $id = shift or return (undef, undef, undef);
my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
\s([a-f\d\-]+)$/x);
- if (!$rev || !$uuid || !$url) {
+ if (!defined $rev || !$uuid || !$url) {
# some of the original repositories I made had
# identifiers like this:
($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
}
}
+sub show_commit_changed_paths {
+ my ($c) = @_;
+ return unless $c->{changed};
+ print "Changed paths:\n", @{$c->{changed}};
+}
+
sub show_commit_normal {
my ($c) = @_;
print '-' x72, "\nr$c->{r} | ";
my $nr_line = 0;
if (my $l = $c->{l}) {
- while ($l->[$#$l] eq "\n" && $l->[($#$l - 1)] eq "\n") {
+ while ($l->[$#$l] eq "\n" && $#$l > 0
+ && $l->[($#$l - 1)] eq "\n") {
pop @$l;
}
$nr_line = scalar @$l;
} else {
$nr_line .= ' lines';
}
- print $nr_line, "\n\n";
+ print $nr_line, "\n";
+ show_commit_changed_paths($c);
+ print "\n";
print $_ foreach @$l;
}
} else {
- print "1 line\n\n";
+ print "1 line\n";
+ show_commit_changed_paths($c);
+ print "\n";
}
foreach my $x (qw/raw diff/) {
require SVN::Ra;
require SVN::Delta;
push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor';
+ push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor';
+ *SVN::Git::Fetcher::process_rm = *process_rm;
+ *SVN::Git::Fetcher::safe_qx = *safe_qx;
my $kill_stupid_warnings = $SVN::Node::none.$SVN::Node::file.
$SVN::Node::dir.$SVN::Node::unknown.
$SVN::Node::none.$SVN::Node::file.
- $SVN::Node::dir.$SVN::Node::unknown;
+ $SVN::Node::dir.$SVN::Node::unknown.
+ $SVN::Auth::SSL::CNMISMATCH.
+ $SVN::Auth::SSL::NOTYETVALID.
+ $SVN::Auth::SSL::EXPIRED.
+ $SVN::Auth::SSL::UNKNOWNCA.
+ $SVN::Auth::SSL::OTHER;
1;
};
}
+sub _simple_prompt {
+ my ($cred, $realm, $default_username, $may_save, $pool) = @_;
+ $may_save = undef if $_no_auth_cache;
+ $default_username = $_username if defined $_username;
+ if (defined $default_username && length $default_username) {
+ if (defined $realm && length $realm) {
+ print "Authentication realm: $realm\n";
+ }
+ $cred->username($default_username);
+ } else {
+ _username_prompt($cred, $realm, $may_save, $pool);
+ }
+ $cred->password(_read_password("Password for '" .
+ $cred->username . "': ", $realm));
+ $cred->may_save($may_save);
+ $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub _ssl_server_trust_prompt {
+ my ($cred, $realm, $failures, $cert_info, $may_save, $pool) = @_;
+ $may_save = undef if $_no_auth_cache;
+ print "Error validating server certificate for '$realm':\n";
+ if ($failures & $SVN::Auth::SSL::UNKNOWNCA) {
+ print " - The certificate is not issued by a trusted ",
+ "authority. Use the\n",
+ " fingerprint to validate the certificate manually!\n";
+ }
+ if ($failures & $SVN::Auth::SSL::CNMISMATCH) {
+ print " - The certificate hostname does not match.\n";
+ }
+ if ($failures & $SVN::Auth::SSL::NOTYETVALID) {
+ print " - The certificate is not yet valid.\n";
+ }
+ if ($failures & $SVN::Auth::SSL::EXPIRED) {
+ print " - The certificate has expired.\n";
+ }
+ if ($failures & $SVN::Auth::SSL::OTHER) {
+ print " - The certificate has an unknown error.\n";
+ }
+ printf( "Certificate information:\n".
+ " - Hostname: %s\n".
+ " - Valid: from %s until %s\n".
+ " - Issuer: %s\n".
+ " - Fingerprint: %s\n",
+ map $cert_info->$_, qw(hostname valid_from valid_until
+ issuer_dname fingerprint) );
+ my $choice;
+prompt:
+ print $may_save ?
+ "(R)eject, accept (t)emporarily or accept (p)ermanently? " :
+ "(R)eject or accept (t)emporarily? ";
+ $choice = lc(substr(<STDIN> || 'R', 0, 1));
+ if ($choice =~ /^t$/i) {
+ $cred->may_save(undef);
+ } elsif ($choice =~ /^r$/i) {
+ return -1;
+ } elsif ($may_save && $choice =~ /^p$/i) {
+ $cred->may_save($may_save);
+ } else {
+ goto prompt;
+ }
+ $cred->accepted_failures($failures);
+ $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub _ssl_client_cert_prompt {
+ my ($cred, $realm, $may_save, $pool) = @_;
+ $may_save = undef if $_no_auth_cache;
+ print "Client certificate filename: ";
+ chomp(my $filename = <STDIN>);
+ $cred->cert_file($filename);
+ $cred->may_save($may_save);
+ $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub _ssl_client_cert_pw_prompt {
+ my ($cred, $realm, $may_save, $pool) = @_;
+ $may_save = undef if $_no_auth_cache;
+ $cred->password(_read_password("Password: ", $realm));
+ $cred->may_save($may_save);
+ $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub _username_prompt {
+ my ($cred, $realm, $may_save, $pool) = @_;
+ $may_save = undef if $_no_auth_cache;
+ if (defined $realm && length $realm) {
+ print "Authentication realm: $realm\n";
+ }
+ my $username;
+ if (defined $_username) {
+ $username = $_username;
+ } else {
+ print "Username: ";
+ chomp($username = <STDIN>);
+ }
+ $cred->username($username);
+ $cred->may_save($may_save);
+ $SVN::_Core::SVN_NO_ERROR;
+}
+
+sub _read_password {
+ my ($prompt, $realm) = @_;
+ print $prompt;
+ require Term::ReadKey;
+ Term::ReadKey::ReadMode('noecho');
+ my $password = '';
+ while (defined(my $key = Term::ReadKey::ReadKey(0))) {
+ last if $key =~ /[\012\015]/; # \n\r
+ $password .= $key;
+ }
+ Term::ReadKey::ReadMode('restore');
+ print "\n";
+ $password;
+}
+
sub libsvn_connect {
my ($url) = @_;
- my $auth = SVN::Core::auth_open([SVN::Client::get_simple_provider(),
- SVN::Client::get_ssl_server_trust_file_provider(),
- SVN::Client::get_username_provider()]);
- my $s = eval { SVN::Ra->new(url => $url, auth => $auth) };
- return $s;
+ SVN::_Core::svn_config_ensure($_config_dir, undef);
+ my ($baton, $callbacks) = SVN::Core::auth_open_helper([
+ SVN::Client::get_simple_provider(),
+ SVN::Client::get_ssl_server_trust_file_provider(),
+ SVN::Client::get_simple_prompt_provider(
+ \&_simple_prompt, 2),
+ SVN::Client::get_ssl_client_cert_prompt_provider(
+ \&_ssl_client_cert_prompt, 2),
+ SVN::Client::get_ssl_client_cert_pw_prompt_provider(
+ \&_ssl_client_cert_pw_prompt, 2),
+ SVN::Client::get_username_provider(),
+ SVN::Client::get_ssl_server_trust_prompt_provider(
+ \&_ssl_server_trust_prompt),
+ SVN::Client::get_username_prompt_provider(
+ \&_username_prompt, 2),
+ ]);
+ my $config = SVN::Core::config_get_config($_config_dir);
+ my $ra = SVN::Ra->new(url => $url, auth => $baton,
+ config => $config,
+ pool => SVN::Pool->new,
+ auth_provider_callbacks => $callbacks);
+
+ my $df = $ENV{GIT_SVN_DELTA_FETCH};
+ if (defined $df) {
+ $_xfer_delta = $df;
+ } else {
+ $_xfer_delta = ($url =~ m#^file://#) ? undef : 1;
+ }
+ $ra->{svn_path} = $url;
+ $ra->{repos_root} = $ra->get_repos_root;
+ $ra->{svn_path} =~ s#^\Q$ra->{repos_root}\E/*##;
+ push @repo_path_split_cache, qr/^(\Q$ra->{repos_root}\E)/;
+ return $ra;
+}
+
+sub libsvn_dup_ra {
+ my ($ra) = @_;
+ SVN::Ra->new(map { $_ => $ra->{$_} } qw/config url
+ auth auth_provider_callbacks repos_root svn_path/);
}
sub libsvn_get_file {
- my ($gui, $f, $rev) = @_;
- my $p = $f;
- if (length $SVN_PATH > 0) {
- return unless ($p =~ s#^\Q$SVN_PATH\E/##);
- }
+ my ($gui, $f, $rev, $chg) = @_;
+ $f =~ s#^/##;
+ print "\t$chg\t$f\n" unless $_q;
my ($hash, $pid, $in, $out);
my $pool = SVN::Pool->new;
waitpid $pid, 0;
$hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
}
- print $gui $mode,' ',$hash,"\t",$p,"\0" or croak $!;
+ print $gui $mode,' ',$hash,"\t",$f,"\0" or croak $!;
}
sub libsvn_log_entry {
sub process_rm {
my ($gui, $last_commit, $f) = @_;
- $f =~ s#^\Q$SVN_PATH\E/?## or return;
# remove entire directories.
if (safe_qx('git-ls-tree',$last_commit,'--',$f) =~ /^040000 tree/) {
defined(my $pid = open my $ls, '-|') or croak $!;
}
sub libsvn_fetch {
+ $_xfer_delta ? libsvn_fetch_delta(@_) : libsvn_fetch_full(@_);
+}
+
+sub libsvn_fetch_delta {
+ my ($last_commit, $paths, $rev, $author, $date, $msg) = @_;
+ my $pool = SVN::Pool->new;
+ my $ed = SVN::Git::Fetcher->new({ c => $last_commit, ra => $SVN,
+ paths => $paths });
+ my $reporter = $SVN->do_update($rev, '', 1, $ed, $pool);
+ my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
+ my (undef, $last_rev, undef) = cmt_metadata($last_commit);
+ $reporter->set_path('', $last_rev, 0, @lock, $pool);
+ $reporter->finish_report($pool);
+ $pool->clear;
+ libsvn_log_entry($rev, $author, $date, $msg, [$last_commit]);
+}
+
+sub libsvn_fetch_full {
my ($last_commit, $paths, $rev, $author, $date, $msg) = @_;
open my $gui, '| git-update-index -z --index-info' or croak $!;
my @amr;
+ my $p = $SVN->{svn_path};
foreach my $f (keys %$paths) {
my $m = $paths->{$f}->action();
- $f =~ s#^/+##;
+ if (length $p) {
+ $f =~ s#^/\Q$p\E/##;
+ next if $f =~ m#^/#;
+ } else {
+ $f =~ s#^/##;
+ }
if ($m =~ /^[DR]$/) {
print "\t$m\t$f\n" unless $_q;
process_rm($gui, $last_commit, $f);
$pool->clear;
}
foreach (@amr) {
- print "\t$_->[0]\t$_->[1]\n" unless $_q;
- libsvn_get_file($gui, $_->[1], $rev)
+ libsvn_get_file($gui, $_->[1], $rev, $_->[0]);
}
close $gui or croak $?;
return libsvn_log_entry($rev, $author, $date, $msg, [$last_commit]);
sub libsvn_traverse {
my ($gui, $pfx, $path, $rev, $files) = @_;
- my $cwd = "$pfx/$path";
+ my $cwd = length $pfx ? "$pfx/$path" : $path;
my $pool = SVN::Pool->new;
- $cwd =~ s#^/+##g;
+ $cwd =~ s#^\Q$SVN->{svn_path}\E##;
my ($dirent, $r, $props) = $SVN->get_dir($cwd, $rev, $pool);
foreach my $d (keys %$dirent) {
my $t = $dirent->{$d}->kind;
if (defined $files) {
push @$files, $file;
} else {
- print "\tA\t$file\n" unless $_q;
- libsvn_get_file($gui, $file, $rev);
+ libsvn_get_file($gui, $file, $rev, 'A');
}
}
}
my $pool = SVN::Pool->new;
my ($dirent, undef, $props) = $SVN->get_dir($path, $r, $pool);
my $p = $path;
- $p =~ s#^\Q$SVN_PATH\E/?##;
+ $p =~ s#^\Q$SVN->{svn_path}\E/##;
print $fh length $p ? "\n# $p\n" : "\n# /\n";
if (my $s = $props->{'svn:ignore'}) {
$s =~ s/[\r\n]+/\n/g;
if ($_use_lib) {
# should be OK to use Pool here (r1 - r0) should be small
my $pool = SVN::Pool->new;
- libsvn_get_log($SVN, "/$path", $r0, $r1,
+ libsvn_get_log($SVN, [$path], $r0, $r1,
0, 1, 1, sub {$nr++}, $pool);
$pool->clear;
} else {
sub libsvn_find_parent_branch {
my ($paths, $rev, $author, $date, $msg) = @_;
- my $svn_path = '/'.$SVN_PATH;
+ my $svn_path = '/'.$SVN->{svn_path};
# look for a parent from another branch:
my $i = $paths->{$svn_path} or return;
$branch_from =~ s#^/##;
my $l_map = {};
read_url_paths_all($l_map, '', "$GIT_DIR/svn");
- my $url = $SVN->{url};
+ my $url = $SVN->{repos_root};
defined $l_map->{$url} or return;
my $id = $l_map->{$url}->{$branch_from};
if (!defined $id && $_follow_parent) {
$GIT_SVN = $ENV{GIT_SVN_ID} = $id;
init_vars();
$SVN_URL = "$url/$branch_from";
- $SVN_LOG = $SVN = undef;
+ $SVN = undef;
setup_git_svn();
# we can't assume SVN_URL exists at r+1:
$_revision = "0:$r";
unlink $GIT_SVN_INDEX;
print STDERR "Found branch parent: ($GIT_SVN) $parent\n";
sys(qw/git-read-tree/, $parent);
- return libsvn_fetch($parent, $paths, $rev,
+ # I can't seem to get do_switch() to work correctly with
+ # the SWIG interface (TypeError when passing switch_url...),
+ # so we'll unconditionally bypass the delta interface here
+ # for now
+ return libsvn_fetch_full($parent, $paths, $rev,
$author, $date, $msg);
}
print STDERR "Nope, branch point not imported or unknown\n";
return $log_entry;
}
my ($paths, $rev, $author, $date, $msg) = @_;
- open my $gui, '| git-update-index -z --index-info' or croak $!;
- libsvn_traverse($gui, '', $SVN_PATH, $rev);
- close $gui or croak $?;
+ if ($_xfer_delta) {
+ my $pool = SVN::Pool->new;
+ my $ed = SVN::Git::Fetcher->new({paths => $paths, ra => $SVN});
+ my $reporter = $SVN->do_update($rev, '', 1, $ed, $pool);
+ my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
+ $reporter->set_path('', $rev, 1, @lock, $pool);
+ $reporter->finish_report($pool);
+ $pool->clear;
+ } else {
+ open my $gui, '| git-update-index -z --index-info' or croak $!;
+ libsvn_traverse($gui, '', $SVN->{svn_path}, $rev);
+ close $gui or croak $?;
+ }
return libsvn_log_entry($rev, $author, $date, $msg);
}
sub libsvn_ls_fullurl {
my $fullurl = shift;
- my ($repo, $path) = repo_path_split($fullurl);
- $SVN ||= libsvn_connect($repo);
+ $SVN ||= libsvn_connect($fullurl);
my @ret;
my $pool = SVN::Pool->new;
- my ($dirent, undef, undef) = $SVN->get_dir($path,
+ my ($dirent, undef, undef) = $SVN->get_dir($SVN->{svn_path},
$SVN->get_latest_revnum, $pool);
foreach my $d (keys %$dirent) {
if ($dirent->{$d}->kind == $SVN::Node::dir) {
# Wonderfully consistent library, eh?
# 160013 - svn:// and file://
# 175002 - http(s)://
+ # 175007 - http(s):// (this repo required authorization, too...)
# More codes may be discovered later...
- if ($errno == 175002 || $errno == 160013) {
+ if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
return;
}
croak "Error from SVN, ($errno): ", $err->expanded_message,"\n";
my $ref = "refs/remotes/$GIT_SVN";
if (safe_qx('git-ls-remote', $origin, $ref)) {
sys(qw/git fetch/, $origin, "$ref:$ref");
- } else {
+ } elsif ($_cp_remote && !$_upgrade) {
die "Unable to find remote reference: ",
"refs/remotes/$GIT_SVN on $origin\n";
}
}
+package SVN::Git::Fetcher;
+use vars qw/@ISA/;
+use strict;
+use warnings;
+use Carp qw/croak/;
+use IO::File qw//;
+
+# file baton members: path, mode_a, mode_b, pool, fh, blob, base
+sub new {
+ my ($class, $git_svn) = @_;
+ my $self = SVN::Delta::Editor->new;
+ bless $self, $class;
+ open my $gui, '| git-update-index -z --index-info' or croak $!;
+ $self->{gui} = $gui;
+ $self->{c} = $git_svn->{c} if exists $git_svn->{c};
+ if (my $p = $git_svn->{paths} && $git_svn->{ra}) {
+ my $s = $git_svn->{ra}->{svn_path};
+ $s = length $s ? qr#^/\Q$s\E/# : qr#^/#;
+ $self->{paths} = { map { my $x = $_;
+ $x =~ s/$s//;
+ $x => $p->{$_} } keys %$p };
+ }
+ require Digest::MD5;
+ $self;
+}
+
+sub delete_entry {
+ my ($self, $path, $rev, $pb) = @_;
+ process_rm($self->{gui}, $self->{c}, $path);
+ undef;
+}
+
+sub open_file {
+ my ($self, $path, $pb, $rev) = @_;
+ my ($mode, $blob) = (safe_qx('git-ls-tree',$self->{c},'--',$path)
+ =~ /^(\d{6}) blob ([a-f\d]{40})\t/);
+ { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob,
+ pool => SVN::Pool->new };
+}
+
+sub add_file {
+ my ($self, $path, $pb, $cp_path, $cp_rev) = @_;
+ { path => $path, mode_a => 100644, mode_b => 100644,
+ pool => SVN::Pool->new };
+}
+
+sub change_file_prop {
+ my ($self, $fb, $prop, $value) = @_;
+ if ($prop eq 'svn:executable') {
+ if ($fb->{mode_b} != 120000) {
+ $fb->{mode_b} = defined $value ? 100755 : 100644;
+ }
+ } elsif ($prop eq 'svn:special') {
+ $fb->{mode_b} = defined $value ? 120000 : 100644;
+ }
+ undef;
+}
+
+sub apply_textdelta {
+ my ($self, $fb, $exp) = @_;
+ my $fh = IO::File->new_tmpfile;
+ $fh->autoflush(1);
+ # $fh gets auto-closed() by SVN::TxDelta::apply(),
+ # (but $base does not,) so dup() it for reading in close_file
+ open my $dup, '<&', $fh or croak $!;
+ my $base = IO::File->new_tmpfile;
+ $base->autoflush(1);
+ if ($fb->{blob}) {
+ defined (my $pid = fork) or croak $!;
+ if (!$pid) {
+ open STDOUT, '>&', $base or croak $!;
+ print STDOUT 'link ' if ($fb->{mode_a} == 120000);
+ exec qw/git-cat-file blob/, $fb->{blob} or croak $!;
+ }
+ waitpid $pid, 0;
+ croak $? if $?;
+
+ if (defined $exp) {
+ seek $base, 0, 0 or croak $!;
+ my $md5 = Digest::MD5->new;
+ $md5->addfile($base);
+ my $got = $md5->hexdigest;
+ die "Checksum mismatch: $fb->{path} $fb->{blob}\n",
+ "expected: $exp\n",
+ " got: $got\n" if ($got ne $exp);
+ }
+ }
+ seek $base, 0, 0 or croak $!;
+ $fb->{fh} = $dup;
+ $fb->{base} = $base;
+ [ SVN::TxDelta::apply($base, $fh, undef, $fb->{path}, $fb->{pool}) ];
+}
+
+sub close_file {
+ my ($self, $fb, $exp) = @_;
+ my $hash;
+ my $path = $fb->{path};
+ if (my $fh = $fb->{fh}) {
+ seek($fh, 0, 0) or croak $!;
+ my $md5 = Digest::MD5->new;
+ $md5->addfile($fh);
+ 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 $!;
+ if ($fb->{mode_b} == 120000) {
+ read($fh, my $buf, 5) == 5 or croak $!;
+ $buf eq 'link ' or die "$path has mode 120000",
+ "but is not a link\n";
+ }
+ defined(my $pid = open my $out,'-|') or die "Can't fork: $!\n";
+ if (!$pid) {
+ open STDIN, '<&', $fh or croak $!;
+ exec qw/git-hash-object -w --stdin/ or croak $!;
+ }
+ chomp($hash = do { local $/; <$out> });
+ close $out or croak $!;
+ close $fh or croak $!;
+ $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n";
+ close $fb->{base} or croak $!;
+ } else {
+ $hash = $fb->{blob} or die "no blob information\n";
+ }
+ $fb->{pool}->clear;
+ my $gui = $self->{gui};
+ print $gui "$fb->{mode_b} $hash\t$path\0" or croak $!;
+ print "\t", $self->{paths}->{$path}->action,
+ "\t$path\n" if defined $self->{paths}->{$path};
+ undef;
+}
+
+sub abort_edit {
+ my $self = shift;
+ close $self->{gui};
+ $self->SUPER::abort_edit(@_);
+}
+
+sub close_edit {
+ my $self = shift;
+ close $self->{gui} or croak;
+ $self->SUPER::close_edit(@_);
+}
package SVN::Git::Editor;
use vars qw/@ISA/;
}
sub repo_path {
- (defined $_[1] && length $_[1]) ? "$_[0]->{svn_path}/$_[1]"
- : $_[0]->{svn_path}
+ (defined $_[1] && length $_[1]) ? $_[1] : ''
}
sub url_path {
exec qw/git-ls-tree --name-only -r -z/, $self->{c} or croak $!;
}
local $/ = "\0";
- my @svn_path = split m#/#, $self->{svn_path};
while (<$fh>) {
chomp;
- my @dn = (@svn_path, (split m#/#, $_));
+ my @dn = split m#/#, $_;
while (pop @dn) {
delete $rm->{join '/', @dn};
}
seek $fh, 0, 0 or croak $!;
my $exp = $md5->hexdigest;
- my $atd = $self->apply_textdelta($fbat, undef, $self->{pool});
- my $got = SVN::TxDelta::send_stream($fh, @$atd, $self->{pool});
+ my $pool = SVN::Pool->new;
+ my $atd = $self->apply_textdelta($fbat, undef, $pool);
+ my $got = SVN::TxDelta::send_stream($fh, @$atd, $pool);
die "Checksum mismatch\nexpected: $exp\ngot: $got\n" if ($got ne $exp);
+ $pool->clear;
close $fh or croak $!;
}