use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/;
use File::Spec qw//;
use POSIX qw/strftime/;
+use IPC::Open3;
use Memoize;
memoize('revisions_eq');
my $sha1 = qr/[a-f\d]{40}/;
my $sha1_short = qr/[a-f\d]{4,40}/;
my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
- $_find_copies_harder, $_l, $_cp_similarity,
+ $_find_copies_harder, $_l, $_cp_similarity, $_cp_remote,
$_repack, $_repack_nr, $_repack_flags,
$_template, $_shared, $_no_default_regex, $_no_graft_copy,
$_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit,
{ 'revision|r=i' => \$_revision } ],
rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)",
{ 'no-ignore-externals' => \$_no_ignore_ext,
+ 'copy-remote|remote=s' => \$_cp_remote,
'upgrade' => \$_upgrade } ],
'graft-branches' => [ \&graft_branches,
'Detect merges/branches from already imported history',
load_authors() if $_authors;
load_all_refs() if $_branch_all_refs;
svn_compat_check();
-migration_check() unless $cmd =~ /^(?:init|multi-init)$/;
+migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init)$/;
$cmd{$cmd}->[0]->(@ARGV);
exit 0;
}
sub rebuild {
+ if (quiet_run(qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0")) {
+ copy_remote_ref();
+ }
$SVN_URL = shift or undef;
my $newest_rev = 0;
if ($_upgrade) {
defined(my $pid = fork) or croak $!;
if (!$pid) {
$SVN::Error::handler = \&libsvn_skip_unknown_revs;
- print "Fetching revisions $min .. $max\n";
# Yes I'm perfectly aware that the fourth argument
# below is the limit revisions number. Unfortunately
$log_msg, @parents);
}
});
- $SVN::Error::handler = sub { 'quiet warnings' };
exit 0;
}
waitpid $pid, 0;
my (@revs) = @_;
my ($r_last, $cmt_last) = svn_grab_base_rev();
defined $r_last or die "Must have an existing revision to commit\n";
- my $fetched = fetch_lib();
+ my $fetched = fetch();
if ($r_last != $fetched->{revision}) {
print STDERR "There are new revisions that were fetched ",
"and need to be merged (or acknowledged) ",
$no = 1;
}
}
- close $fh or croak $!;
+ close $fh or croak $?;
if (! defined $r_new && ! defined $cmt_new) {
unless ($no) {
die "Failed to parse revision information\n";
sub multi_fetch {
# try to do trunk first, since branches/tags
# may be descended from it.
- if (-d "$GIT_DIR/svn/trunk") {
- print "Fetching trunk\n";
- defined(my $pid = fork) or croak $!;
- if (!$pid) {
- $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
- init_vars();
- fetch(@_);
- exit 0;
- }
- waitpid $pid, 0;
- croak $? if $?;
+ if (-e "$GIT_DIR/svn/trunk/info/url") {
+ fetch_child_id('trunk', @_);
}
rec_fetch('', "$GIT_DIR/svn", @_);
}
my $pid = open(my $log,'-|');
defined $pid or croak $!;
if (!$pid) {
- my @rl = (qw/git-log --abbrev-commit --pretty=raw
- --default/, "remotes/$GIT_SVN");
- push @rl, '--raw' if $_verbose;
- exec(@rl, @args) or croak $!;
+ exec(git_svn_log_cmd($r_min,$r_max), @args) or croak $!;
}
setup_pager();
my (@k, $c, $d);
+
while (<$log>) {
if (/^commit ($sha1_short)/o) {
my $cmt = $1;
- if ($c && defined $c->{r} && $c->{r} != $r_last) {
+ if ($c && cmt_showable($c) && $c->{r} != $r_last) {
$r_last = $c->{r};
process_commit($c, $r_min, $r_max, \@k) or
goto out;
} elsif ($d) {
push @{$c->{diff}}, $_;
} elsif (/^ (git-svn-id:.+)$/) {
- my ($url, $rev, $uuid) = extract_metadata($1);
- $c->{r} = $rev;
+ (undef, $c->{r}, undef) = extract_metadata($1);
} elsif (s/^ //) {
push @{$c->{l}}, $_;
}
########################### utility functions #########################
+sub cmt_showable {
+ my ($c) = @_;
+ return 1 if defined $c->{r};
+ if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
+ $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
+ my @msg = safe_qx(qw/git-cat-file commit/, $c->{c});
+ shift @msg while ($msg[0] ne "\n");
+ shift @msg;
+ @{$c->{l}} = grep !/^git-svn-id: /, @msg;
+
+ (undef, $c->{r}, undef) = extract_metadata(
+ (grep(/^git-svn-id: /, @msg))[-1]);
+ }
+ return defined $c->{r};
+}
+
+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, '--summary' if $_verbose;
+ return @cmd unless defined $r_max;
+ if ($r_max == $r_min) {
+ push @cmd, '--max-count=1';
+ if (my $c = revdb_get($REVDB, $r_max)) {
+ push @cmd, $c;
+ }
+ } else {
+ 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 ($r_max > $r_max) {
+ push @cmd, "$c_min..$c_max";
+ } else {
+ push @cmd, "$c_max..$c_min";
+ }
+ } elsif ($r_max > $r_min) {
+ push @cmd, $c_max;
+ } else {
+ push @cmd, $c_min;
+ }
+ }
+ return @cmd;
+}
+
+sub fetch_child_id {
+ my $id = shift;
+ print "Fetching $id\n";
+ my $ref = "$GIT_DIR/refs/remotes/$id";
+ my $ca = file_to_s($ref) if (-r $ref);
+ defined(my $pid = fork) or croak $!;
+ if (!$pid) {
+ $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
+ init_vars();
+ fetch(@_);
+ exit 0;
+ }
+ waitpid $pid, 0;
+ croak $? if $?;
+ return unless $_repack || -r $ref;
+
+ my $cb = file_to_s($ref);
+
+ defined($pid = open my $fh, '-|') or croak $!;
+ my $url = file_to_s("$GIT_DIR/svn/$id/info/url");
+ $url = qr/\Q$url\E/;
+ if (!$pid) {
+ exec qw/git-rev-list --pretty=raw/,
+ $ca ? "$ca..$cb" : $cb or croak $!;
+ }
+ while (<$fh>) {
+ if (/^ git-svn-id: $url\@\d+ [a-f0-9\-]+$/) {
+ check_repack();
+ } elsif (/^ git-svn-id: \S+\@\d+ [a-f0-9\-]+$/) {
+ last;
+ }
+ }
+ close $fh;
+}
+
sub rec_fetch {
my ($pfx, $p, @args) = @_;
my @dir;
$pfx .= '/' if $pfx && $pfx !~ m!/$!;
my $id = $pfx . basename $_;
next if $id eq 'trunk';
- print "Fetching $id\n";
- defined(my $pid = fork) or croak $!;
- if (!$pid) {
- $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
- init_vars();
- fetch(@args);
- exit 0;
- }
- waitpid $pid, 0;
- croak $? if $?;
+ fetch_child_id($id, @args);
} elsif (-d $_) {
push @dir, $_;
}
$SVN_UUID = $info->{'Repository UUID'} or
croak "Repository UUID unreadable\n";
}
- s_to_file($SVN_UUID,"$GIT_SVN_DIR/info/uuid");
}
sub quiet_run {
close $fh;
s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url");
- open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!;
- print $fd '.svn',"\n";
- close $fd or croak $!;
- my ($url, $path) = repo_path_split($SVN_URL);
- s_to_file($url, "$GIT_SVN_DIR/info/repo_url");
- s_to_file($path, "$GIT_SVN_DIR/info/repo_path");
}
sub assert_svn_wc_clean {
croak "Error parsing $_\n";
}
}
- close $diff_fh or croak $!;
+ close $diff_fh or croak $?;
return \@mods;
}
} elsif ($m->{chg} eq 'T') {
sys(qw(svn rm --force),$m->{file_b});
apply_mod_line_blob($m);
- sys(qw(svn add --force), $m->{file_b});
+ sys(qw(svn add), $m->{file_b});
svn_check_prop_executable($m);
} elsif ($m->{chg} eq 'A') {
svn_ensure_parent_path( $m->{file_b} );
apply_mod_line_blob($m);
- sys(qw(svn add --force), $m->{file_b});
+ sys(qw(svn add), $m->{file_b});
svn_check_prop_executable($m);
} else {
croak "Invalid chg: $m->{chg}\n";
print $msg $_ or croak $!;
}
}
- close $msg_fh or croak $!;
+ close $msg_fh or croak $?;
}
close $msg or croak $!;
push @{$ret->{-order}}, $1;
}
}
- close $info_fh or croak $!;
+ close $info_fh or croak $?;
return $ret;
}
}
print $ui $x,"\0";
}
- close $ui or croak $!;
+ close $ui or croak $?;
}
sub index_changes {
return if $_use_lib;
+
+ if (!-f "$GIT_SVN_DIR/info/exclude") {
+ open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!;
+ print $fd '.svn',"\n";
+ close $fd or croak $!;
+ }
my $no_text_base = shift;
do_update_index([qw/git-diff-files --name-only -z/],
'remove',
# this output is read via pipe, do not change:
print "r$log_msg->{revision} = $commit\n";
+ check_repack();
+ return $commit;
+}
+
+sub check_repack {
if ($_repack && (--$_repack_nr == 0)) {
$_repack_nr = $_repack;
sys("git repack $_repack_flags");
}
- return $commit;
}
sub set_commit_env {
}
sub check_upgrade_needed {
+ if (!-r $REVDB) {
+ -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
+ open my $fh, '>>',$REVDB or croak $!;
+ close $fh;
+ }
my $old = eval {
my $pid = open my $child, '-|';
defined $pid or croak $!;
init_vars();
exit 0 if -r $REVDB;
print "Upgrading svn => git mapping...\n";
+ -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
open my $fh, '>>',$REVDB or croak $!;
close $fh;
rebuild();
my $dn = dirname("$GIT_DIR/svn/$x");
mkpath([$dn]) unless -d $dn;
rename "$GIT_DIR/$x", "$GIT_DIR/svn/$x" or croak "$!: $x";
- my ($url, $path) = repo_path_split($u);
- s_to_file($url, "$GIT_DIR/svn/$x/info/repo_url");
- s_to_file($path, "$GIT_DIR/svn/$x/info/repo_path");
}
migrate_revdb() if (-d $GIT_SVN_DIR && !-w $REVDB);
print "Done upgrading.\n";
sub find_rev_before {
my ($r, $id, $eq_ok) = @_;
my $f = "$GIT_DIR/svn/$id/.rev_db";
- # --$r unless $eq_ok;
+ return (undef,undef) unless -r $f;
+ --$r unless $eq_ok;
while ($r > 0) {
if (my $c = revdb_get($f, $r)) {
return ($r, $c);
if (defined $_repack) {
$_repack = 1000 if ($_repack <= 0);
$_repack_nr = $_repack;
- $_repack_flags ||= '';
+ $_repack_flags ||= '-d';
}
}
sub read_url_paths {
my $l_map = {};
git_svn_each(sub { my $x = shift;
- my $u = file_to_s("$GIT_DIR/svn/$x/info/repo_url");
- my $p = file_to_s("$GIT_DIR/svn/$x/info/repo_path");
- # we hate trailing slashes
- if ($u =~ s#(?:^\/+|\/+$)##g) {
- s_to_file($u,"$GIT_DIR/svn/$x/info/repo_url");
- }
- if ($p =~ s#(?:^\/+|\/+$)##g) {
- s_to_file($p,"$GIT_DIR/svn/$x/info/repo_path");
- }
+ my $url = file_to_s("$GIT_DIR/svn/$x/info/url");
+ my ($u, $p) = repo_path_split($url);
$l_map->{$u}->{$p} = $x;
});
return $l_map;
sub get_author_info {
my ($dest, $author, $t, $tz) = @_;
$author =~ s/(?:^\s*|\s*$)//g;
+ $dest->{a_raw} = $author;
my $_a;
if ($_authors) {
$_a = $rusers{$author} || undef;
my $p = $f;
return unless ($p =~ s#^\Q$SVN_PATH\E/?##);
- my $fd = IO::File->new_tmpfile or croak $!;
+ my ($hash, $pid, $in, $out);
my $pool = SVN::Pool->new;
- my ($r, $props) = $SVN->get_file($f, $rev, $fd, $pool);
+ defined($pid = open3($in, $out, '>&STDERR',
+ qw/git-hash-object -w --stdin/)) or croak $!;
+ my ($r, $props) = $SVN->get_file($f, $rev, $in, $pool);
+ $in->flush == 0 or croak $!;
+ close $in or croak $!;
$pool->clear;
- $fd->flush == 0 or croak $!;
- seek $fd, 0, 0 or croak $!;
- if (my $es = $props->{'svn:eol-style'}) {
- my $new_fd = IO::File->new_tmpfile or croak $!;
- eol_cp_fd($fd, $new_fd, $es);
- close $fd or croak $!;
- $fd = $new_fd;
- seek $fd, 0, 0 or croak $!;
- $fd->flush == 0 or croak $!;
- }
- my $mode = '100644';
- if (exists $props->{'svn:executable'}) {
- $mode = '100755';
- }
+ chomp($hash = do { local $/; <$out> });
+ close $out or croak $!;
+ waitpid $pid, 0;
+ $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
+
+ my $mode = exists $props->{'svn:executable'} ? '100755' : '100644';
if (exists $props->{'svn:special'}) {
$mode = '120000';
- local $/;
- my $link = <$fd>;
+ my $link = `git-cat-file blob $hash`;
$link =~ s/^link // or die "svn:special file with contents: <",
$link, "> is not understood\n";
- seek $fd, 0, 0 or croak $!;
- truncate $fd, 0 or croak $!;
- print $fd $link or croak $!;
- seek $fd, 0, 0 or croak $!;
- $fd->flush == 0 or croak $!;
- }
- my $pid = open my $ho, '-|';
- defined $pid or croak $!;
- if (!$pid) {
- open STDIN, '<&', $fd or croak $!;
- exec qw/git-hash-object -w --stdin/ or croak $!;
+ defined($pid = open3($in, $out, '>&STDERR',
+ qw/git-hash-object -w --stdin/)) or croak $!;
+ print $in $link;
+ $in->flush == 0 or croak $!;
+ close $in or croak $!;
+ chomp($hash = do { local $/; <$out> });
+ close $out or croak $!;
+ waitpid $pid, 0;
+ $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
}
- chomp(my $hash = do { local $/; <$ho> });
- close $ho or croak $?;
- $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
print $gui $mode,' ',$hash,"\t",$p,"\0" or croak $!;
- close $fd or croak $!;
}
sub libsvn_log_entry {
while (<$ls>) {
print $gui '0 ',0 x 40,"\t",$_ or croak $!;
}
- close $ls or croak $!;
+ close $ls or croak $?;
} else {
print $gui '0 ',0 x 40,"\t",$f,"\0" or croak $!;
}
$pool->clear;
}
libsvn_get_file($gui, $_, $rev) foreach (@amr);
- close $gui or croak $!;
+ close $gui or croak $?;
return libsvn_log_entry($rev, $author, $date, $msg, [$last_commit]);
}
close $fh;
if (defined $c && length $c) {
my ($url, $rev, $uuid) = extract_metadata((grep(/^git-svn-id: /,
- safe_qx(qw/git-cat-file commit/, $c)))[0]);
+ safe_qx(qw/git-cat-file commit/, $c)))[-1]);
return ($rev, $c);
}
return (undef, undef);
}
sub libsvn_find_parent_branch {
- return undef; # XXX this function is disabled atm (not tested enough)
my ($paths, $rev, $author, $date, $msg) = @_;
my $svn_path = '/'.$SVN_PATH;
# look for a parent from another branch:
- foreach (keys %$paths) {
- next if ($_ ne $svn_path);
- my $i = $paths->{$_};
- my $branch_from = $i->copyfrom_path or next;
- my $r = $i->copyfrom_rev;
- print STDERR "Found possible branch point: ",
- "$branch_from => $svn_path, $r\n";
- $branch_from =~ s#^/##;
- my $l_map = read_url_paths();
- my $url = $SVN->{url};
- defined $l_map->{$url} or next;
- my $id = $l_map->{$url}->{$branch_from} or next;
- my ($r0, $parent) = find_rev_before($r,$id,1);
- if (defined $r0 && defined $parent &&
- revisions_eq($branch_from, $r0, $r)) {
- unlink $GIT_SVN_INDEX;
- print STDERR "Found branch parent: $parent\n";
- sys(qw/git-read-tree/, $parent);
- return libsvn_fetch($parent, $paths, $rev,
- $author, $date, $msg);
- } else {
- print STDERR
- "Nope, branch point not imported or unknown\n";
- }
- }
+ my $i = $paths->{$svn_path} or return;
+ my $branch_from = $i->copyfrom_path or return;
+ my $r = $i->copyfrom_rev;
+ print STDERR "Found possible branch point: ",
+ "$branch_from => $svn_path, $r\n";
+ $branch_from =~ s#^/##;
+ my $l_map = read_url_paths();
+ my $url = $SVN->{url};
+ defined $l_map->{$url} or return;
+ my $id = $l_map->{$url}->{$branch_from} or return;
+ my ($r0, $parent) = find_rev_before($r,$id,1);
+ return unless (defined $r0 && defined $parent);
+ if (revisions_eq($branch_from, $r0, $r)) {
+ unlink $GIT_SVN_INDEX;
+ print STDERR "Found branch parent: $parent\n";
+ sys(qw/git-read-tree/, $parent);
+ return libsvn_fetch($parent, $paths, $rev,
+ $author, $date, $msg);
+ }
+ print STDERR "Nope, branch point not imported or unknown\n";
return undef;
}
my $pool = SVN::Pool->new;
libsvn_traverse($gui, '', $SVN_PATH, $rev, $pool);
$pool->clear;
- close $gui or croak $!;
+ close $gui or croak $?;
return libsvn_log_entry($rev, $author, $date, $msg);
}
exit 1;
}
} else {
- fetch_lib("$rev=$c");
+ fetch("$rev=$c");
}
}
# 175002 - http(s)://
# More codes may be discovered later...
if ($errno == 175002 || $errno == 160013) {
- print STDERR "directory non-existent\n";
return;
}
croak "Error from SVN, ($errno): ", $err->expanded_message,"\n";
return $ret;
}
+sub copy_remote_ref {
+ my $origin = $_cp_remote ? $_cp_remote : 'origin';
+ my $ref = "refs/remotes/$GIT_SVN";
+ if (safe_qx('git-ls-remote', $origin, $ref)) {
+ sys(qw/git fetch/, $origin, "$ref:$ref");
+ } else {
+ die "Unable to find remote reference: ",
+ "refs/remotes/$GIT_SVN on $origin\n";
+ }
+}
+
package SVN::Git::Editor;
use vars qw/@ISA/;
use strict;