X-Git-Url: https://git.octo.it/?a=blobdiff_plain;f=contrib%2Fgit-svn%2Fgit-svn.perl;h=54f3d6312eadce9ae5fc59139ccd7e728c479025;hb=6dfbe5163e26e3e1126c9b08c3cb38195e92a82c;hp=3e5733eed91c35dd0c52ebcc57b9d560373f89f5;hpb=f4e96f97e85c145616bc2e23e9441bc0daa2906e;p=git.git diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 3e5733ee..54f3d631 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -8,8 +8,13 @@ use vars qw/ $AUTHOR $VERSION $GIT_SVN_INDEX $GIT_SVN $GIT_DIR $REV_DIR/; $AUTHOR = 'Eric Wong '; -$VERSION = '0.11.0'; -$GIT_DIR = $ENV{GIT_DIR} || "$ENV{PWD}/.git"; +$VERSION = '1.1.0-pre'; + +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'; @@ -28,23 +33,31 @@ 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 %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: +my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" ); + my %cmd = ( fetch => [ \&fetch, "Download new revisions from SVN", { 'revision|r=s' => \$_revision, %fc_opts } ], - init => [ \&init, "Initialize and fetch (import)", { } ], + init => [ \&init, "Initialize a repo for tracking" . + " (requires URL argument)", { } ], commit => [ \&commit, "Commit git revisions to SVN", { 'stdin|' => \$_stdin, 'edit|e' => \$_edit, '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", { } ], @@ -63,13 +76,31 @@ for (my $i = 0; $i < @ARGV; $i++) { 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; + } + } +} + 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"; -$ENV{GIT_DIR} ||= $GIT_DIR; $SVN_URL = undef; $REV_DIR = "$GIT_DIR/$GIT_SVN/revs"; $SVN_WC = "$GIT_DIR/$GIT_SVN/tree"; @@ -78,6 +109,7 @@ 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(); $cmd{$cmd}->[0]->(@ARGV); exit 0; @@ -184,7 +216,7 @@ sub rebuild { push @svn_up, '--ignore-externals' unless $_no_ignore_ext; sys(@svn_up,"-r$newest_rev"); $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX; - git_addremove(); + index_changes(); exec('git-write-tree'); } waitpid $pid, 0; @@ -198,7 +230,8 @@ when you have upgraded your tools and habits to use refs/remotes/$GIT_SVN } sub init { - $SVN_URL = shift or croak "SVN repository location required\n"; + $SVN_URL = shift or die "SVN repository location required " . + "as a command-line argument\n"; unless (-d $GIT_DIR) { sys('git-init-db'); } @@ -225,26 +258,37 @@ sub fetch { chdir $SVN_WC or croak $!; read_uuid(); $last_commit = git_commit($base, @parents); - assert_svn_wc_clean($base->{revision}, $last_commit); + assert_tree($last_commit); } 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; my $last = $base; while (my $log_msg = next_log_entry($svn_log)) { - assert_svn_wc_clean($last->{revision}, $last_commit); + assert_tree($last_commit); if ($last->{revision} >= $log_msg->{revision}) { croak "Out of order: last >= current: ", "$last->{revision} >= $log_msg->{revision}\n"; } + # Revert is needed for cases like: + # https://svn.musicpd.org/Jamming/trunk (r166:167), but + # I can't seem to reproduce something like that on a test... + sys(qw/svn revert -R ./); + assert_svn_wc_clean($last->{revision}); sys(@svn_up,"-r$log_msg->{revision}"); $last_commit = git_commit($log_msg, $last_commit, @parents); $last = $log_msg; } - assert_svn_wc_clean($last->{revision}, $last_commit); unless (-e "$GIT_DIR/refs/heads/master") { sys(qw(git-update-ref refs/heads/master),$last_commit); } @@ -276,9 +320,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) { @@ -290,7 +341,6 @@ sub commit { $svn_current_rev = svn_commit_tree($svn_current_rev, $c); } print "Done committing ",scalar @revs," revisions to SVN\n"; - } sub show_ignore { @@ -305,7 +355,7 @@ sub show_ignore { 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"; @@ -343,13 +393,11 @@ sub setup_git_svn { } sub assert_svn_wc_clean { - my ($svn_rev, $treeish) = @_; + my ($svn_rev) = @_; croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/); - croak "$treeish is not a sha1!\n" unless ($treeish =~ /^$sha1$/o); my $lcr = svn_info('.')->{'Last Changed Rev'}; if ($svn_rev != $lcr) { print STDERR "Checking for copy-tree ... "; - # use my @diff = grep(/^Index: /,(safe_qx(qw(svn diff), "-r$lcr:$svn_rev"))); if (@diff) { @@ -365,7 +413,6 @@ sub assert_svn_wc_clean { print STDERR $_ foreach @status; croak; } - assert_tree($treeish); } sub assert_tree { @@ -392,7 +439,7 @@ sub assert_tree { unlink $tmpindex or croak $!; } $ENV{GIT_INDEX_FILE} = $tmpindex; - git_addremove(); + index_changes(1); chomp(my $tree = `git-write-tree`); if ($old_index) { $ENV{GIT_INDEX_FILE} = $old_index; @@ -402,6 +449,7 @@ sub assert_tree { if ($tree ne $expected) { croak "Tree mismatch, Got: $tree, Expected: $expected\n"; } + unlink $tmpindex; } sub parse_diff_tree { @@ -538,12 +586,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,$from); + 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 $!; @@ -675,23 +728,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 @@ -699,16 +763,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) @@ -828,13 +892,82 @@ sub svn_info { sub sys { system(@_) == 0 or croak $? } -sub git_addremove { - system( "git-diff-files --name-only -z ". - " | git-update-index --remove -z --stdin && ". - "git-ls-files -z --others ". - "'--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude'". - " | git-update-index --add -z --stdin" - ) == 0 or croak $? +sub eol_cp { + my ($from, $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; + my $buf; + use bytes; + while (1) { + my ($r, $w, $t); + defined($r = sysread($rfd, $buf, 4096)) or croak $!; + return unless $r; + 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 { + my ($z_cmd, $cmd, $no_text_base) = @_; + + my $z = open my $p, '-|'; + defined $z or croak $!; + unless ($z) { exec @$z_cmd or croak $! } + + my $pid = open my $ui, '|-'; + defined $pid or croak $!; + unless ($pid) { + exec('git-update-index',"--$cmd",'-z','--stdin') or croak $!; + } + local $/ = "\0"; + while (my $x = <$p>) { + chomp $x; + if (!$no_text_base && lstat $x && ! -l _ && + 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', + 'text-base',"$f.svn-base"); + $tb =~ s#^/##; + unless (-f $tb) { + $tb = File::Spec->catfile($d, '.svn', + 'text-base',"$f.svn-base"); + $tb =~ s#^/##; + } + unlink $x or croak $!; + eol_cp($tb, $x); + chmod(($mode &~ umask), $x) or croak $!; + } + print $ui $x,"\0"; + } + close $ui or croak $!; +} + +sub index_changes { + my $no_text_base = shift; + do_update_index([qw/git-diff-files --name-only -z/], + 'remove', + $no_text_base); + do_update_index([qw/git-ls-files -z --others/, + "--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude"], + 'add', + $no_text_base); } sub s_to_file { @@ -912,7 +1045,7 @@ sub git_commit { defined $pid or croak $!; if ($pid == 0) { $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX; - git_addremove(); + index_changes(); chomp(my $tree = `git-write-tree`); croak if $?; if (exists $tree_map{$tree}) { @@ -1044,6 +1177,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)))) { @@ -1091,26 +1227,44 @@ 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: sub load_authors { open my $authors, '<', $_authors or die "Can't open $_authors $!\n"; @@ -1123,6 +1277,12 @@ 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); +} + __END__ Data structures: