$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;
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}/;
+my $_esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
$_find_copies_harder, $_l, $_cp_similarity, $_cp_remote,
$_repack, $_repack_nr, $_repack_flags, $_q,
$_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, $_ignore_nodate, $_non_recursive);
+ $_merge, $_strategy, $_dry_run, $_ignore_nodate, $_non_recursive,
+ $_username, $_config_dir, $_no_auth_cache, $_xfer_delta,
+ $_pager, $_color);
my (@_branch_from, %tree_map, %users, %rusers, %equiv);
-my ($_svn_co_url_revs, $_svn_pg_peg_revs);
+my ($_svn_co_url_revs, $_svn_pg_peg_revs, $_svn_can_do_switch);
my @repo_path_split_cache;
my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext,
'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);
'no-graft-copy' => \$_no_graft_copy } ],
'multi-init' => [ \&multi_init,
'Initialize multiple trees (like git-svnimport)',
- { %multi_opts, %fc_opts } ],
+ { %multi_opts, %init_opts,
+ 'revision|r=i' => \$_revision,
+ 'username=s' => \$_username,
+ 'config-dir=s' => \$_config_dir,
+ 'no-auth-cache' => \$_no_auth_cache,
+ } ],
'multi-fetch' => [ \&multi_fetch,
'Fetch multiple trees (like git-svnimport)',
\%fc_opts ],
'show-commit' => \$_show_commit,
'non-recursive' => \$_non_recursive,
'authors-file|A=s' => \$_authors,
+ 'color' => \$_color,
+ 'pager=s' => \$_pager,
} ],
'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,
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;
$min = $max + 1;
$max += $inc;
$max = $head if ($max > $head);
+ $SVN = libsvn_connect($SVN_URL);
}
restore_index($index);
return { revision => $last_rev, commit => $last_commit };
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_LOG,
+ 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 {
}
}
+ config_pager();
my $pid = open(my $log,'-|');
defined $pid or croak $!;
if (!$pid) {
exec(git_svn_log_cmd($r_min,$r_max), @args) or croak $!;
}
- setup_pager();
+ run_pager();
my (@k, $c, $d);
while (<$log>) {
- if (/^commit ($sha1_short)/o) {
+ if (/^${_esc_color}commit ($sha1_short)/o) {
my $cmt = $1;
if ($c && cmt_showable($c) && $c->{r} != $r_last) {
$r_last = $c->{r};
}
$d = undef;
$c = { c => $cmt };
- } elsif (/^author (.+) (\d+) ([\-\+]?\d+)$/) {
+ } elsif (/^${_esc_color}author (.+) (\d+) ([\-\+]?\d+)$/) {
get_author_info($c, $1, $2, $3);
- } elsif (/^(?:tree|parent|committer) /) {
+ } elsif (/^${_esc_color}(?:tree|parent|committer) /) {
# ignore
- } elsif (/^:\d{6} \d{6} $sha1_short/o) {
+ } elsif (/^${_esc_color}:\d{6} \d{6} $sha1_short/o) {
push @{$c->{raw}}, $_;
- } elsif (/^[ACRMDT]\t/) {
- # we could add $SVN_PATH here, but that requires
+ } elsif (/^${_esc_color}[ACRMDT]\t/) {
+ # we could add $SVN->{svn_path} here, but that requires
# remote access at the moment (repo_path_split)...
- s#^([ACRMDT])\t# $1 #;
+ s#^(${_esc_color})([ACRMDT])\t#$1 $2 #;
push @{$c->{changed}}, $_;
- } elsif (/^diff /) {
+ } elsif (/^${_esc_color}diff /) {
$d = 1;
push @{$c->{diff}}, $_;
} elsif ($d) {
push @{$c->{diff}}, $_;
- } elsif (/^ (git-svn-id:.+)$/) {
+ } elsif (/^${_esc_color} (git-svn-id:.+)$/) {
($c->{url}, $c->{r}, undef) = extract_metadata($1);
- } elsif (s/^ //) {
+ } elsif (s/^${_esc_color} //) {
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_LOG, 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 #########################
return defined $c->{r};
}
+sub log_use_color {
+ return 1 if $_color;
+ my $dc;
+ chomp($dc = `git-repo-config --get diff.color`);
+ if ($dc eq 'auto') {
+ if (-t *STDOUT || (defined $_pager &&
+ `git-repo-config --bool --get pager.color` !~ /^false/)) {
+ return ($ENV{TERM} && $ENV{TERM} ne 'dumb');
+ }
+ return 0;
+ }
+ return 0 if $dc eq 'never';
+ return 1 if $dc eq 'always';
+ chomp($dc = `git-repo-config --bool --get diff.color`);
+ $dc eq 'true';
+}
+
sub git_svn_log_cmd {
my ($r_min, $r_max) = @_;
my @cmd = (qw/git-log --abbrev-commit --pretty=raw
--default/, "refs/remotes/$GIT_SVN");
push @cmd, '-r' unless $_non_recursive;
push @cmd, qw/--raw --name-status/ if $_verbose;
+ push @cmd, '--color' if log_use_color();
return @cmd unless defined $r_max;
if ($r_max == $r_min) {
push @cmd, '--max-count=1';
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, 2, 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 $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\-]+)/);
return ($1 * 60) + ($tz * 3600);
}
-sub setup_pager { # translated to Perl from pager.c
- return unless (-t *STDOUT);
- my $pager = $ENV{PAGER};
- if (!defined $pager) {
- $pager = 'less';
- } elsif (length $pager == 0 || $pager eq 'cat') {
- return;
+# adapted from pager.c
+sub config_pager {
+ $_pager ||= $ENV{GIT_PAGER} || $ENV{PAGER};
+ if (!defined $_pager) {
+ $_pager = 'less';
+ } elsif (length $_pager == 0 || $_pager eq 'cat') {
+ $_pager = undef;
}
+}
+
+sub run_pager {
+ return unless -t *STDOUT;
pipe my $rfd, my $wfd or return;
defined(my $pid = fork) or croak $!;
if (!$pid) {
return;
}
open STDIN, '<&', $rfd or croak $!;
- $ENV{LESS} ||= '-S';
- exec $pager or croak "Can't run pager: $!\n";;
+ $ENV{LESS} ||= 'FRSX';
+ exec $_pager or croak "Can't run pager: $! ($_pager)\n";
}
sub get_author_info {
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_can_do_switch {
+ unless (defined $_svn_can_do_switch) {
+ my $pool = SVN::Pool->new;
+ my $rep = eval {
+ $SVN->do_switch(1, '', 0, $SVN->{url},
+ SVN::Delta::Editor->new, $pool);
+ };
+ if ($@) {
+ $_svn_can_do_switch = 0;
+ } else {
+ $rep->abort_report($pool);
+ $_svn_can_do_switch = 1;
+ }
+ $pool->clear;
+ }
+ $_svn_can_do_switch;
+}
+
+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, $chg) = @_;
- my $p = $f;
- if (length $SVN_PATH > 0) {
- return unless ($p =~ s#^\Q$SVN_PATH\E/##);
- }
+ $f =~ s#^/##;
print "\t$chg\t$f\n" unless $_q;
my ($hash, $pid, $in, $out);
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;
+ my ($gui, $last_commit, $f, $q) = @_;
# remove entire directories.
if (safe_qx('git-ls-tree',$last_commit,'--',$f) =~ /^040000 tree/) {
defined(my $pid = open my $ls, '-|') or croak $!;
local $/ = "\0";
while (<$ls>) {
print $gui '0 ',0 x 40,"\t",$_ or croak $!;
+ print "\tD\t$_\n" unless $q;
}
+ print "\tD\t$f/\n" unless $q;
close $ls or croak $?;
} else {
print $gui '0 ',0 x 40,"\t",$f,"\0" or croak $!;
+ print "\tD\t$f\n" unless $q;
}
}
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, q => $_q });
+ 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;
+ unless ($ed->{git_commit_ok}) {
+ die "SVN connection failed somewhere...\n";
+ }
+ 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 %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);
+ process_rm($gui, $last_commit, $f, $_q);
next if $m eq 'D';
# 'R' can be file replacements, too, right?
}
my $t = $SVN->check_path($f, $rev, $pool);
if ($t == $SVN::Node::file) {
if ($m =~ /^[AMR]$/) {
- push @amr, [ $m, $f ];
+ $amr{$f} = $m;
} else {
die "Unrecognized action: $m, ($f r$rev)\n";
}
my @traversed = ();
libsvn_traverse($gui, '', $f, $rev, \@traversed);
foreach (@traversed) {
- push @amr, [ $m, $_ ]
+ $amr{$_} = $m;
}
}
$pool->clear;
}
- foreach (@amr) {
- libsvn_get_file($gui, $_->[1], $rev, $_->[0]);
+ foreach (keys %amr) {
+ libsvn_get_file($gui, $_, $rev, $amr{$_});
}
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;
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,
- 0, 1, 1, sub {$nr++}, $pool);
+ libsvn_get_log($SVN, [$path], $r0, $r1,
+ 0, 0, 1, sub {$nr++}, $pool);
$pool->clear;
} else {
my ($url, undef) = repo_path_split($SVN_URL);
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,
- $author, $date, $msg);
+ unless (libsvn_can_do_switch()) {
+ return libsvn_fetch_full($parent, $paths, $rev,
+ $author, $date, $msg);
+ }
+ # do_switch works with svn/trunk >= r22312, but that is not
+ # included with SVN 1.4.2 (the latest version at the moment),
+ # so we can't rely on it.
+ my $ra = libsvn_connect("$url/$branch_from");
+ my $ed = SVN::Git::Fetcher->new({c => $parent, q => $_q});
+ my $pool = SVN::Pool->new;
+ my $reporter = $ra->do_switch($rev, '', 1, $SVN->{url},
+ $ed, $pool);
+ my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
+ $reporter->set_path('', $r0, 0, @lock, $pool);
+ $reporter->finish_report($pool);
+ $pool->clear;
+ unless ($ed->{git_commit_ok}) {
+ die "SVN connection failed somewhere...\n";
+ }
+ return libsvn_log_entry($rev, $author, $date, $msg, [$parent]);
}
print STDERR "Nope, branch point not imported or unknown\n";
return undef;
sub libsvn_get_log {
my ($ra, @args) = @_;
+ $args[4]-- if $args[4] && $_xfer_delta && ! $_follow_parent;
if ($SVN::Core::VERSION le '1.2.0') {
splice(@args, 3, 1);
}
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({q => $_q});
+ 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;
+ unless ($ed->{git_commit_ok}) {
+ die "SVN connection failed somewhere...\n";
+ }
+ } 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);
+ my $ra = libsvn_connect($fullurl);
my @ret;
my $pool = SVN::Pool->new;
- my ($dirent, undef, undef) = $SVN->get_dir($path,
- $SVN->get_latest_revnum, $pool);
+ my $r = defined $_revision ? $_revision : $ra->get_latest_revnum;
+ my ($dirent, undef, undef) = $ra->get_dir('', $r, $pool);
foreach my $d (keys %$dirent) {
if ($dirent->{$d}->kind == $SVN::Node::dir) {
push @ret, "$d/"; # add '/' for compat with cli svn
# 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};
+ $self->{q} = $git_svn->{q};
+ require Digest::MD5;
+ $self;
+}
+
+sub delete_entry {
+ my ($self, $path, $rev, $pb) = @_;
+ process_rm($self->{gui}, $self->{c}, $path, $self->{q});
+ 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/);
+ unless (defined $mode && defined $blob) {
+ die "$path was not found in commit $self->{c} (r$rev)\n";
+ }
+ { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob,
+ pool => SVN::Pool->new, action => 'M' };
+}
+
+sub add_file {
+ my ($self, $path, $pb, $cp_path, $cp_rev) = @_;
+ { path => $path, mode_a => 100644, mode_b => 100644,
+ pool => SVN::Pool->new, action => 'A' };
+}
+
+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$fb->{action}\t$path\n" if $fb->{action} && ! $self->{q};
+ 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->{git_commit_ok} = 1;
+ $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};
}