X-Git-Url: https://git.octo.it/?a=blobdiff_plain;f=contrib%2Fgit-svn%2Fgit-svn.perl;h=2dce4e7b83e072a2272c8a62e49922c67f0849f2;hb=883d0a78d2a28add079f809ffb8dd077f4a766d8;hp=b3e0684c449cf3f3c0c3b23460c39a13d74ada34;hpb=d48f7168619932a389341f1f0c904ab6dc117b5e;p=git.git diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index b3e0684c..2dce4e7b 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -6,7 +6,7 @@ use strict; 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 '; $VERSION = '1.1.0-pre'; @@ -14,6 +14,7 @@ use Cwd qw/abs_path/; $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'; @@ -32,12 +33,15 @@ use POSIX qw/strftime/; 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, + $_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, + 'branch-all-refs|B' => \$_branch_all_refs, 'authors-file|A=s' => \$_authors ); # yes, 'native' sets "\n". Patches to fix this for non-*nix systems welcome: @@ -54,6 +58,7 @@ my %cmd = ( '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", { } ], @@ -96,16 +101,19 @@ GetOptions(%opts, 'help|H|h' => \$_help, 'id|i=s' => \$GIT_SVN) or exit 1; $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn'; -$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index"; +$GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN"; +$GIT_SVN_INDEX = "$GIT_SVN_DIR/index"; $SVN_URL = undef; -$REV_DIR = "$GIT_DIR/$GIT_SVN/revs"; -$SVN_WC = "$GIT_DIR/$GIT_SVN/tree"; +$REV_DIR = "$GIT_SVN_DIR/revs"; +$SVN_WC = "$GIT_SVN_DIR/tree"; usage(0) if $_help; version() if $_version; usage(1) unless defined $cmd; 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; @@ -195,7 +203,7 @@ sub rebuild { $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 $?; @@ -236,7 +244,7 @@ sub init { 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'; @@ -257,7 +265,14 @@ sub fetch { } 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; @@ -308,9 +323,16 @@ sub commit { } 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) { @@ -331,12 +353,12 @@ sub show_ignore { 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"; @@ -355,7 +377,44 @@ sub read_uuid { 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 { @@ -363,14 +422,17 @@ 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 { @@ -567,13 +629,17 @@ sub precommit_check { sub svn_checkout_tree { my ($svn_rev, $treeish) = @_; my $from = file_to_s("$REV_DIR/$svn_rev"); - assert_svn_wc_clean($svn_rev); assert_tree($from); print "diff-tree $from $treeish\n"; 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 $!; @@ -665,7 +731,7 @@ sub handle_rmdir { 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 $!; @@ -705,23 +771,34 @@ sub svn_commit_tree { 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 @@ -729,16 +806,16 @@ sub svn_commit_tree { 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) @@ -860,26 +937,33 @@ sub sys { system(@_) == 0 or croak $? } 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 { @@ -898,7 +982,7 @@ 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', @@ -924,7 +1008,7 @@ sub index_changes { '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); } @@ -1056,7 +1140,7 @@ sub git_commit { 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"; return $commit; } @@ -1136,6 +1220,9 @@ sub svn_compat_check { 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)))) { @@ -1183,24 +1270,42 @@ sub check_upgrade_needed { # 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`); } # ' = real-name ' mapping based on git-svnimport: @@ -1215,6 +1320,38 @@ sub load_authors { 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"; +} + __END__ Data structures: