use vars qw/ $AUTHOR $VERSION
$SVN_URL $SVN_INFO $SVN_WC $SVN_UUID
$GIT_SVN_INDEX $GIT_SVN
- $GIT_DIR $REV_DIR/;
+ $GIT_DIR $REV_DIR $GIT_SVN_DIR/;
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
$VERSION = '1.1.0-pre';
$GIT_DIR = abs_path($ENV{GIT_DIR} || '.git');
$ENV{GIT_DIR} = $GIT_DIR;
+my $LC_ALL = $ENV{LC_ALL};
# make sure the svn binary gives consistent output between locales and TZs:
$ENV{TZ} = 'UTC';
$ENV{LC_ALL} = 'C';
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, $_version, $_upgrade, $_authors);
+ $_find_copies_harder, $_l, $_cp_similarity,
+ $_repack, $_repack_nr, $_repack_flags,
+ $_version, $_upgrade, $_authors, $_branch_all_refs);
my (@_branch_from, %tree_map, %users);
-my $_svn_co_url_revs;
+my ($_svn_co_url_revs, $_svn_pg_peg_revs);
+my @repo_path_split_cache;
my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext,
'branch|b=s' => \@_branch_from,
- 'authors-file|A=s' => \$_authors );
+ 'branch-all-refs|B' => \$_branch_all_refs,
+ 'authors-file|A=s' => \$_authors,
+ 'repack:i' => \$_repack,
+ 'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
# yes, 'native' sets "\n". Patches to fix this for non-*nix systems welcome:
my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" );
'rmdir' => \$_rmdir,
'find-copies-harder' => \$_find_copies_harder,
'l=i' => \$_l,
+ 'copy-similarity|C=i'=> \$_cp_similarity,
%fc_opts,
} ],
'show-ignore' => [ \&show_ignore, "Show svn:ignore listings", { } ],
my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
-# convert GetOpt::Long specs for use by git-repo-config
-foreach my $o (keys %opts) {
- my $v = $opts{$o};
- my ($key) = ($o =~ /^([a-z\-]+)/);
- $key =~ s/-//g;
- my $arg = 'git-repo-config';
- $arg .= ' --int' if ($o =~ /=i$/);
- $arg .= ' --bool' if ($o !~ /=[sfi]$/);
- if (ref $v eq 'ARRAY') {
- chomp(my @tmp = `$arg --get-all svn.$key`);
- @$v = @tmp if @tmp;
- } else {
- chomp(my $tmp = `$arg --get svn.$key`);
- if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) {
- $$v = $tmp;
- }
- }
-}
-
+read_repo_config(\%opts);
GetOptions(%opts, 'help|H|h' => \$_help,
'version|V' => \$_version,
'id|i=s' => \$GIT_SVN) or exit 1;
-$GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
-$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index";
-$SVN_URL = undef;
-$REV_DIR = "$GIT_DIR/$GIT_SVN/revs";
-$SVN_WC = "$GIT_DIR/$GIT_SVN/tree";
-
+set_default_vals();
usage(0) if $_help;
version() if $_version;
usage(1) unless defined $cmd;
+init_vars();
load_authors() if $_authors;
+load_all_refs() if $_branch_all_refs;
svn_compat_check();
+migration_check() unless $cmd eq 'init';
$cmd{$cmd}->[0]->(@ARGV);
exit 0;
print $fd ' ',pack('A13',$_),$cmd{$_}->[1],"\n";
foreach (keys %{$cmd{$_}->[2]}) {
# prints out arguments as they should be passed:
- my $x = s#=s$## ? '<arg>' : s#=i$## ? '<num>' : '';
+ my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
print $fd ' ' x 17, join(', ', map { length $_ > 1 ?
"--$_" : "-$_" }
split /\|/,$_)," $x\n";
$latest = $rev;
}
assert_revision_eq_or_unknown($rev, $c);
- sys('git-update-ref',"$GIT_SVN/revs/$rev",$c);
+ sys('git-update-ref',"svn/$GIT_SVN/revs/$rev",$c);
$newest_rev = $rev if ($rev > $newest_rev);
}
close $rev_list or croak $?;
sys(@svn_up,"-r$newest_rev");
$ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
index_changes();
- exec('git-write-tree');
+ exec('git-write-tree') or croak $!;
}
waitpid $pid, 0;
+ croak $? if $?;
if ($_upgrade) {
print STDERR <<"";
sub fetch {
my (@parents) = @_;
check_upgrade_needed();
- $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
+ $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL);
unless ($_revision) {
$_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD';
} else {
chdir $SVN_WC or croak $!;
read_uuid();
- $last_commit = file_to_s("$REV_DIR/$base->{revision}");
+ eval { $last_commit = file_to_s("$REV_DIR/$base->{revision}") };
+ # looks like a user manually cp'd and svn switch'ed
+ unless ($last_commit) {
+ sys(qw/svn revert -R ./);
+ assert_svn_wc_clean($base->{revision});
+ $last_commit = git_commit($base, @parents);
+ assert_tree($last_commit);
+ }
}
my @svn_up = qw(svn up);
push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
unless (-e "$GIT_DIR/refs/heads/master") {
sys(qw(git-update-ref refs/heads/master),$last_commit);
}
+ close $svn_log->{fh};
return $last;
}
}
chomp @revs;
- fetch();
- chdir $SVN_WC or croak $!;
+ chdir $SVN_WC or croak "Unable to chdir $SVN_WC: $!\n";
my $info = svn_info('.');
+ my $fetched = fetch();
+ if ($info->{Revision} != $fetched->{revision}) {
+ print STDERR "There are new revisions that were fetched ",
+ "and need to be merged (or acknowledged) ",
+ "before committing.\n";
+ exit 1;
+ }
+ $info = svn_info('.');
read_uuid($info);
my $svn_current_rev = $info->{'Last Changed Rev'};
foreach my $c (@revs) {
chomp(my @excludes = (<$fh>));
close $fh or croak $!;
- $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
+ $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
chdir $SVN_WC or croak $!;
my %ign;
File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){
s#^\./##;
- @{$ign{$_}} = safe_qx(qw(svn propget svn:ignore),$_);
+ @{$ign{$_}} = svn_propget_base('svn:ignore', $_);
}}, no_chdir=>1},'.');
print "\n# /\n";
my $info = shift || svn_info('.');
$SVN_UUID = $info->{'Repository UUID'} or
croak "Repository UUID unreadable\n";
- s_to_file($SVN_UUID,"$GIT_DIR/$GIT_SVN/info/uuid");
+ s_to_file($SVN_UUID,"$GIT_SVN_DIR/info/uuid");
+}
+
+sub quiet_run {
+ my $pid = fork;
+ defined $pid or croak $!;
+ if (!$pid) {
+ open my $null, '>', '/dev/null' or croak $!;
+ open STDERR, '>&', $null or croak $!;
+ open STDOUT, '>&', $null or croak $!;
+ exec @_ or croak $!;
+ }
+ waitpid $pid, 0;
+ return $?;
+}
+
+sub repo_path_split {
+ my $full_url = shift;
+ $full_url =~ s#/+$##;
+
+ foreach (@repo_path_split_cache) {
+ if ($full_url =~ s#$_##) {
+ my $u = $1;
+ $full_url =~ s#^/+##;
+ return ($u, $full_url);
+ }
+ }
+
+ my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i);
+ $path =~ s#^/+##;
+ my @paths = split(m#/+#, $path);
+
+ while (quiet_run(qw/svn ls --non-interactive/, $url)) {
+ my $n = shift @paths || last;
+ $url .= "/$n";
+ }
+ push @repo_path_split_cache, qr/^(\Q$url\E)/;
+ return ($url, $path);
}
sub setup_git_svn {
unless (-d $GIT_DIR) {
croak "GIT_DIR=$GIT_DIR does not exist!\n";
}
- mkpath(["$GIT_DIR/$GIT_SVN"]);
- mkpath(["$GIT_DIR/$GIT_SVN/info"]);
+ mkpath([$GIT_SVN_DIR]);
+ mkpath(["$GIT_SVN_DIR/info"]);
mkpath([$REV_DIR]);
- s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url");
+ s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url");
- open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!;
+ 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 {
my $pid = open my $diff_fh, '-|';
defined $pid or croak $!;
if ($pid == 0) {
- my @diff_tree = qw(git-diff-tree -z -r -C);
+ my @diff_tree = qw(git-diff-tree -z -r);
+ if ($_cp_similarity) {
+ push @diff_tree, "-C$_cp_similarity";
+ } else {
+ push @diff_tree, '-C';
+ }
push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
push @diff_tree, "-l$_l" if defined $_l;
exec(@diff_tree, $from, $treeish) or croak $!;
sub svn_commit_tree {
my ($svn_rev, $commit) = @_;
- my $commit_msg = "$GIT_DIR/$GIT_SVN/.svn-commit.tmp.$$";
+ my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
my %log_msg = ( msg => '' );
open my $msg, '>', $commit_msg or croak $!;
my ($oneline) = ($log_msg{msg} =~ /([^\n\r]+)/);
print "Committing $commit: $oneline\n";
+ if (defined $LC_ALL) {
+ $ENV{LC_ALL} = $LC_ALL;
+ } else {
+ delete $ENV{LC_ALL};
+ }
my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
- my ($committed) = grep(/^Committed revision \d+\./,@ci_output);
+ $ENV{LC_ALL} = 'C';
unlink $commit_msg;
- defined $committed or croak
+ my ($committed) = ($ci_output[$#ci_output] =~ /(\d+)/);
+ if (!defined $committed) {
+ my $out = join("\n",@ci_output);
+ print STDERR "W: Trouble parsing \`svn commit' output:\n\n",
+ $out, "\n\nAssuming English locale...";
+ ($committed) = ($out =~ /^Committed revision \d+\./sm);
+ defined $committed or die " FAILED!\n",
"Commit output failed to parse committed revision!\n",
- join("\n",@ci_output),"\n";
- my ($rev_committed) = ($committed =~ /^Committed revision (\d+)\./);
+ print STDERR " OK\n";
+ }
my @svn_up = qw(svn up);
push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
- if ($rev_committed == ($svn_rev + 1)) {
- push @svn_up, "-r$rev_committed";
+ if ($committed == ($svn_rev + 1)) {
+ push @svn_up, "-r$committed";
sys(@svn_up);
my $info = svn_info('.');
my $date = $info->{'Last Changed Date'} or die "Missing date\n";
- if ($info->{'Last Changed Rev'} != $rev_committed) {
- croak "$info->{'Last Changed Rev'} != $rev_committed\n"
+ if ($info->{'Last Changed Rev'} != $committed) {
+ croak "$info->{'Last Changed Rev'} != $committed\n"
}
my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
/(\d{4})\-(\d\d)\-(\d\d)\s
or croak "Failed to parse date: $date\n";
$log_msg{date} = "$tz $Y-$m-$d $H:$M:$S";
$log_msg{author} = $info->{'Last Changed Author'};
- $log_msg{revision} = $rev_committed;
+ $log_msg{revision} = $committed;
$log_msg{msg} .= "\n";
my $parent = file_to_s("$REV_DIR/$svn_rev");
git_commit(\%log_msg, $parent, $commit);
- return $rev_committed;
+ return $committed;
}
# resync immediately
push @svn_up, "-r$svn_rev";
sys(@svn_up);
- return fetch("$rev_committed=$commit")->{revision};
+ return fetch("$committed=$commit")->{revision};
}
# read the entire log into a temporary file (which is removed ASAP)
exec (qw(svn log), @log_args) or croak $!
}
waitpid $pid, 0;
- croak if $?;
+ croak $? if $?;
seek $log_fh, 0, 0 or croak $!;
return { state => 'sep', fh => $log_fh };
}
sub eol_cp {
my ($from, $to) = @_;
- my $es = safe_qx(qw/svn propget svn:eol-style/, $to);
+ my $es = svn_propget_base('svn:eol-style', $to);
open my $rfd, '<', $from or croak $!;
binmode $rfd or croak $!;
open my $wfd, '>', $to or croak $!;
binmode $wfd or croak $!;
my $eol = $EOL{$es} or undef;
- if ($eol) {
- print "$eol: $from => $to\n";
- }
my $buf;
+ use bytes;
while (1) {
my ($r, $w, $t);
defined($r = sysread($rfd, $buf, 4096)) or croak $!;
return unless $r;
- $buf =~ s/(?:\015|\012|\015\012)/$eol/gs if $eol;
+ if ($eol) {
+ if ($buf =~ /\015$/) {
+ my $c;
+ defined($r = sysread($rfd,$c,1)) or croak $!;
+ $buf .= $c if $r > 0;
+ }
+ $buf =~ s/(?:\015\012|\015|\012)/$eol/gs;
+ $r = length($buf);
+ }
for ($w = 0; $w < $r; $w += $t) {
$t = syswrite($wfd, $buf, $r - $w, $w) or croak $!;
}
}
+ no bytes;
}
sub do_update_index {
while (my $x = <$p>) {
chomp $x;
if (!$no_text_base && lstat $x && ! -l _ &&
- safe_qx(qw/svn propget svn:keywords/,$x)) {
+ svn_propget_base('svn:keywords', $x)) {
my $mode = -x _ ? 0755 : 0644;
my ($v,$d,$f) = File::Spec->splitpath($x);
my $tb = File::Spec->catfile($d, '.svn', 'tmp',
'remove',
$no_text_base);
do_update_index([qw/git-ls-files -z --others/,
- "--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude"],
+ "--exclude-from=$GIT_SVN_DIR/info/exclude"],
'add',
$no_text_base);
}
$ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
index_changes();
chomp(my $tree = `git-write-tree`);
- croak if $?;
+ croak $? if $?;
if (exists $tree_map{$tree}) {
my %seen_parent = map { $_ => 1 } @exec_parents;
foreach (@{$tree_map{$tree}}) {
exec @exec or croak $!;
}
waitpid($pid,0);
- croak if $?;
+ croak $? if $?;
$out_fh->flush == 0 or croak $!;
seek $out_fh, 0, 0 or croak $!;
close STDERR;
close STDOUT;
exec 'git-rev-parse','--verify',
- "refs/remotes/$GIT_SVN^0";
+ "refs/remotes/$GIT_SVN^0" or croak $!;
}
waitpid $pid, 0;
push @update_ref, $primary_parent unless $?;
}
sys(@update_ref);
- sys('git-update-ref',"$GIT_SVN/revs/$log_msg->{revision}",$commit);
+ sys('git-update-ref',"svn/$GIT_SVN/revs/$log_msg->{revision}",$commit);
print "r$log_msg->{revision} = $commit\n";
+ if ($_repack && (--$_repack_nr == 0)) {
+ $_repack_nr = $_repack;
+ sys("git repack $_repack_flags");
+ }
return $commit;
}
if ($pid == 0) {
open STDOUT, '>&', $blob_fh or croak $!;
- exec('git-cat-file','blob',$blob);
+ exec('git-cat-file','blob',$blob) or croak $!;
}
waitpid $pid, 0;
croak $? if $?;
my $pid = open my $child, '-|';
defined $pid or croak $!;
if ($pid == 0) {
- exec(@_) or croak $?;
+ exec(@_) or croak $!;
}
my @ret = (<$child>);
close $child or croak $?;
if (grep /usage: checkout URL\[\@REV\]/,@co_help) {
$_svn_co_url_revs = 1;
}
+ if (grep /\[TARGET\[\@REV\]\.\.\.\]/, `svn propget -h`) {
+ $_svn_pg_peg_revs = 1;
+ }
# I really, really hope nobody hits this...
unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) {
defined $pid or croak $!;
if ($pid == 0) {
close STDERR;
- exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $?;
+ exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $!;
}
my @ret = (<$child>);
close $child or croak $?;
# fills %tree_map with a reverse mapping of trees to commits. Useful
# for finding parents to commit on.
sub map_tree_joins {
+ my %seen;
foreach my $br (@_branch_from) {
my $pid = open my $pipe, '-|';
defined $pid or croak $!;
if ($pid == 0) {
- exec(qw(git-rev-list --pretty=raw), $br) or croak $?;
+ exec(qw(git-rev-list --topo-order --pretty=raw), $br)
+ or croak $!;
}
while (<$pipe>) {
if (/^commit ($sha1)$/o) {
my $commit = $1;
+
+ # if we've seen a commit,
+ # we've seen its parents
+ last if $seen{$commit};
my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o);
unless (defined $tree) {
die "Failed to parse commit $commit\n";
}
push @{$tree_map{$tree}}, $commit;
+ $seen{$commit} = 1;
}
}
- close $pipe or croak $?;
+ close $pipe; # we could be breaking the pipe early
}
}
+sub load_all_refs {
+ if (@_branch_from) {
+ print STDERR '--branch|-b parameters are ignored when ',
+ "--branch-all-refs|-B is passed\n";
+ }
+
+ # don't worry about rev-list on non-commit objects/tags,
+ # it shouldn't blow up if a ref is a blob or tree...
+ chomp(@_branch_from = `git-rev-parse --symbolic --all`);
+}
+
# '<svn username> = real-name <email address>' mapping based on git-svnimport:
sub load_authors {
open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
close $authors or croak $!;
}
+sub svn_propget_base {
+ my ($p, $f) = @_;
+ $f .= '@BASE' if $_svn_pg_peg_revs;
+ return safe_qx(qw/svn propget/, $p, $f);
+}
+
+sub migration_check {
+ return if (-d "$GIT_DIR/svn" || !-d $GIT_DIR);
+ print "Upgrading repository...\n";
+ unless (-d "$GIT_DIR/svn") {
+ mkdir "$GIT_DIR/svn" or croak $!;
+ }
+ print "Data from a previous version of git-svn exists, but\n\t",
+ "$GIT_SVN_DIR\n\t(required for this version ",
+ "($VERSION) of git-svn) does not.\n";
+
+ foreach my $x (`git-rev-parse --symbolic --all`) {
+ next unless $x =~ s#^refs/remotes/##;
+ chomp $x;
+ next unless -f "$GIT_DIR/$x/info/url";
+ my $u = eval { file_to_s("$GIT_DIR/$x/info/url") };
+ next unless $u;
+ 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");
+ }
+ print "Done upgrading.\n";
+}
+
+sub init_vars {
+ $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
+ $GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN";
+ $GIT_SVN_INDEX = "$GIT_SVN_DIR/index";
+ $SVN_URL = undef;
+ $REV_DIR = "$GIT_SVN_DIR/revs";
+ $SVN_WC = "$GIT_SVN_DIR/tree";
+}
+
+# convert GetOpt::Long specs for use by git-repo-config
+sub read_repo_config {
+ return unless -d $GIT_DIR;
+ my $opts = shift;
+ foreach my $o (keys %$opts) {
+ my $v = $opts->{$o};
+ my ($key) = ($o =~ /^([a-z\-]+)/);
+ $key =~ s/-//g;
+ my $arg = 'git-repo-config';
+ $arg .= ' --int' if ($o =~ /[:=]i$/);
+ $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
+ if (ref $v eq 'ARRAY') {
+ chomp(my @tmp = `$arg --get-all svn.$key`);
+ @$v = @tmp if @tmp;
+ } else {
+ chomp(my $tmp = `$arg --get svn.$key`);
+ if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) {
+ $$v = $tmp;
+ }
+ }
+ }
+}
+
+sub set_default_vals {
+ if (defined $_repack) {
+ $_repack = 1000 if ($_repack <= 0);
+ $_repack_nr = $_repack;
+ $_repack_flags ||= '';
+ }
+}
+
__END__
Data structures: