X-Git-Url: https://git.octo.it/?a=blobdiff_plain;f=contrib%2Fgit-svn%2Fgit-svn.perl;h=3e5733eed91c35dd0c52ebcc57b9d560373f89f5;hb=03823184247215fba1dd9c9c39659d08dea3bad7;hp=69b6be33e7d19e3b60fee0d973ac107bba4ce3a9;hpb=7f60b228601becaa0bca2017521e1f27d5af70b1;p=git.git diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 69b6be33..3e5733ee 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -4,19 +4,12 @@ use warnings; use strict; use vars qw/ $AUTHOR $VERSION - $SVN_URL $SVN_INFO $SVN_WC + $SVN_URL $SVN_INFO $SVN_WC $SVN_UUID $GIT_SVN_INDEX $GIT_SVN $GIT_DIR $REV_DIR/; $AUTHOR = 'Eric Wong '; -$VERSION = '0.10.0'; +$VERSION = '0.11.0'; $GIT_DIR = $ENV{GIT_DIR} || "$ENV{PWD}/.git"; -$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"; - # make sure the svn binary gives consistent output between locales and TZs: $ENV{TZ} = 'UTC'; $ENV{LC_ALL} = 'C'; @@ -37,6 +30,7 @@ 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); my (@_branch_from, %tree_map, %users); +my $_svn_co_url_revs; my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext, 'branch|b=s' => \@_branch_from, @@ -67,23 +61,24 @@ for (my $i = 0; $i < @ARGV; $i++) { } }; -# we may be called as git-svn-(command), or git-svn(command). -foreach (keys %cmd) { - if (/git\-svn\-?($_)(?:\.\w+)?$/) { - $cmd = $1; - last; - } -} +my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd); -my %opts; -%opts = %{$cmd{$cmd}->[2]} if (defined $cmd); +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"; -GetOptions(%opts, 'help|H|h' => \$_help, 'version|V' => \$_version ) or exit 1; usage(0) if $_help; version() if $_version; usage(1) unless defined $cmd; load_authors() if $_authors; -svn_check_ignore_externals(); +svn_compat_check(); $cmd{$cmd}->[0]->(@ARGV); exit 0; @@ -94,15 +89,25 @@ sub usage { print $fd <<""; git-svn - bidirectional operations between a single Subversion tree and git Usage: $0 [options] [arguments]\n -Available commands: + + print $fd "Available commands:\n" unless $cmd; foreach (sort keys %cmd) { + next if $cmd && $cmd ne $_; print $fd ' ',pack('A13',$_),$cmd{$_}->[1],"\n"; + foreach (keys %{$cmd{$_}->[2]}) { + # prints out arguments as they should be passed: + my $x = s#=s$## ? '' : s#=i$## ? '' : ''; + print $fd ' ' x 17, join(', ', map { length $_ > 1 ? + "--$_" : "-$_" } + split /\|/,$_)," $x\n"; + } } print $fd <<""; -\nGIT_SVN_ID may be set in the environment to an arbitrary identifier if -you're tracking multiple SVN branches/repositories in one git repository -and want to keep them separate. See git-svn(1) for more information. +\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an +arbitrary identifier if you're tracking multiple SVN branches/repositories in +one git repository and want to keep them separate. See git-svn(1) for more +information. exit $exit; } @@ -114,7 +119,6 @@ sub version { sub rebuild { $SVN_URL = shift or undef; - my $repo_uuid; my $newest_rev = 0; if ($_upgrade) { sys('git-update-ref',"refs/remotes/$GIT_SVN","$GIT_SVN-HEAD"); @@ -150,8 +154,8 @@ sub rebuild { # if we merged or otherwise started elsewhere, this is # how we break out of it - next if (defined $repo_uuid && ($uuid ne $repo_uuid)); - next if (defined $SVN_URL && ($url ne $SVN_URL)); + next if (defined $SVN_UUID && ($uuid ne $SVN_UUID)); + next if (defined $SVN_URL && defined $url && ($url ne $SVN_URL)); print "r$rev = $c\n"; unless (defined $latest) { @@ -159,7 +163,8 @@ sub rebuild { croak "SVN repository location required: $url\n"; } $SVN_URL ||= $url; - $repo_uuid ||= setup_git_svn(); + $SVN_UUID ||= $uuid; + setup_git_svn(); $latest = $rev; } assert_revision_eq_or_unknown($rev, $c); @@ -168,9 +173,7 @@ sub rebuild { } close $rev_list or croak $?; if (!chdir $SVN_WC) { - my @svn_co = ('svn','co',"-r$latest"); - push @svn_co, '--ignore-externals' unless $_no_ignore_ext; - sys(@svn_co, $SVN_URL, $SVN_WC); + svn_cmd_checkout($SVN_URL, $latest, $SVN_WC); chdir $SVN_WC or croak $!; } @@ -214,35 +217,38 @@ sub fetch { push @log_args, '--stop-on-copy' unless $_no_stop_copy; my $svn_log = svn_log_raw(@log_args); - @$svn_log = sort { $a->{revision} <=> $b->{revision} } @$svn_log; - my $base = shift @$svn_log or croak "No base revision!\n"; + my $base = next_log_entry($svn_log) or croak "No base revision!\n"; my $last_commit = undef; unless (-d $SVN_WC) { - my @svn_co = ('svn','co',"-r$base->{revision}"); - push @svn_co,'--ignore-externals' unless $_no_ignore_ext; - sys(@svn_co, $SVN_URL, $SVN_WC); + svn_cmd_checkout($SVN_URL,$base->{revision},$SVN_WC); chdir $SVN_WC or croak $!; + read_uuid(); $last_commit = git_commit($base, @parents); assert_svn_wc_clean($base->{revision}, $last_commit); } else { chdir $SVN_WC or croak $!; + read_uuid(); $last_commit = file_to_s("$REV_DIR/$base->{revision}"); } my @svn_up = qw(svn up); push @svn_up, '--ignore-externals' unless $_no_ignore_ext; - my $last_rev = $base->{revision}; - foreach my $log_msg (@$svn_log) { - assert_svn_wc_clean($last_rev, $last_commit); - $last_rev = $log_msg->{revision}; - sys(@svn_up,"-r$last_rev"); + my $last = $base; + while (my $log_msg = next_log_entry($svn_log)) { + assert_svn_wc_clean($last->{revision}, $last_commit); + if ($last->{revision} >= $log_msg->{revision}) { + croak "Out of order: last >= current: ", + "$last->{revision} >= $log_msg->{revision}\n"; + } + sys(@svn_up,"-r$log_msg->{revision}"); $last_commit = git_commit($log_msg, $last_commit, @parents); + $last = $log_msg; } - assert_svn_wc_clean($last_rev, $last_commit); + 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); } - return pop @$svn_log; + return $last; } sub commit { @@ -252,7 +258,7 @@ sub commit { print "Reading from stdin...\n"; @commits = (); while () { - if (/\b($sha1_short)\b/) { + if (/\b($sha1_short)\b/o) { unshift @commits, $1; } } @@ -272,7 +278,9 @@ sub commit { fetch(); chdir $SVN_WC or croak $!; - my $svn_current_rev = svn_info('.')->{'Last Changed Rev'}; + my $info = svn_info('.'); + read_uuid($info); + my $svn_current_rev = $info->{'Last Changed Rev'}; foreach my $c (@revs) { my $mods = svn_checkout_tree($svn_current_rev, $c); if (scalar @$mods == 0) { @@ -311,6 +319,14 @@ sub show_ignore { ########################### utility functions ######################### +sub read_uuid { + return if $SVN_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"); +} + sub setup_git_svn { defined $SVN_URL or croak "SVN repository location required\n"; unless (-d $GIT_DIR) { @@ -320,24 +336,27 @@ sub setup_git_svn { mkpath(["$GIT_DIR/$GIT_SVN/info"]); mkpath([$REV_DIR]); s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url"); - my $uuid = svn_info($SVN_URL)->{'Repository UUID'} or - croak "Repository UUID unreadable\n"; - s_to_file($uuid,"$GIT_DIR/$GIT_SVN/info/uuid"); open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!; print $fd '.svn',"\n"; close $fd or croak $!; - return $uuid; } sub assert_svn_wc_clean { my ($svn_rev, $treeish) = @_; croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/); croak "$treeish is not a sha1!\n" unless ($treeish =~ /^$sha1$/o); - my $svn_info = svn_info('.'); - if ($svn_rev != $svn_info->{'Last Changed Rev'}) { - croak "Expected r$svn_rev, got r", - $svn_info->{'Last Changed Rev'},"\n"; + 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) { + croak "Nope! Expected r$svn_rev, got r$lcr\n"; + } else { + print STDERR "OK!\n"; + } } my @status = grep(!/^Performing status on external/,(`svn status`)); @status = grep(!/^\s*$/,@status); @@ -692,49 +711,61 @@ sub svn_commit_tree { return fetch("$rev_committed=$commit")->{revision}; } +# read the entire log into a temporary file (which is removed ASAP) +# and store the file handle + parser state sub svn_log_raw { my (@log_args) = @_; - my $pid = open my $log_fh,'-|'; + my $log_fh = IO::File->new_tmpfile or croak $!; + my $pid = fork; defined $pid or croak $!; - - if ($pid == 0) { + if (!$pid) { + open STDOUT, '>&', $log_fh or croak $!; exec (qw(svn log), @log_args) or croak $! } + waitpid $pid, 0; + croak if $?; + seek $log_fh, 0, 0 or croak $!; + return { state => 'sep', fh => $log_fh }; +} + +sub next_log_entry { + my $log = shift; # retval of svn_log_raw() + my $ret = undef; + my $fh = $log->{fh}; - my @svn_log; - my $state = 'sep'; - while (<$log_fh>) { + while (<$fh>) { chomp; if (/^\-{72}$/) { - if ($state eq 'msg') { - if ($svn_log[$#svn_log]->{lines}) { - $svn_log[$#svn_log]->{msg} .= $_."\n"; - unless(--$svn_log[$#svn_log]->{lines}) { - $state = 'sep'; + if ($log->{state} eq 'msg') { + if ($ret->{lines}) { + $ret->{msg} .= $_."\n"; + unless(--$ret->{lines}) { + $log->{state} = 'sep'; } } else { croak "Log parse error at: $_\n", - $svn_log[$#svn_log]->{revision}, + $ret->{revision}, "\n"; } next; } - if ($state ne 'sep') { + if ($log->{state} ne 'sep') { croak "Log parse error at: $_\n", - "state: $state\n", - $svn_log[$#svn_log]->{revision}, + "state: $log->{state}\n", + $ret->{revision}, "\n"; } - $state = 'rev'; + $log->{state} = 'rev'; # if we have an empty log message, put something there: - if (@svn_log) { - $svn_log[$#svn_log]->{msg} ||= "\n"; - delete $svn_log[$#svn_log]->{lines}; + if ($ret) { + $ret->{msg} ||= "\n"; + delete $ret->{lines}; + return $ret; } next; } - if ($state eq 'rev' && s/^r(\d+)\s*\|\s*//) { + if ($log->{state} eq 'rev' && s/^r(\d+)\s*\|\s*//) { my $rev = $1; my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3); ($lines) = ($lines =~ /(\d+)/); @@ -742,36 +773,34 @@ sub svn_log_raw { /(\d{4})\-(\d\d)\-(\d\d)\s (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x) or croak "Failed to parse date: $date\n"; - my %log_msg = ( revision => $rev, + $ret = { revision => $rev, date => "$tz $Y-$m-$d $H:$M:$S", author => $author, lines => $lines, - msg => '' ); + msg => '' }; if (defined $_authors && ! defined $users{$author}) { die "Author: $author not defined in ", "$_authors file\n"; } - push @svn_log, \%log_msg; - $state = 'msg_start'; + $log->{state} = 'msg_start'; next; } # skip the first blank line of the message: - if ($state eq 'msg_start' && /^$/) { - $state = 'msg'; - } elsif ($state eq 'msg') { - if ($svn_log[$#svn_log]->{lines}) { - $svn_log[$#svn_log]->{msg} .= $_."\n"; - unless (--$svn_log[$#svn_log]->{lines}) { - $state = 'sep'; + if ($log->{state} eq 'msg_start' && /^$/) { + $log->{state} = 'msg'; + } elsif ($log->{state} eq 'msg') { + if ($ret->{lines}) { + $ret->{msg} .= $_."\n"; + unless (--$ret->{lines}) { + $log->{state} = 'sep'; } } else { croak "Log parse error at: $_\n", - $svn_log[$#svn_log]->{revision},"\n"; + $ret->{revision},"\n"; } } } - close $log_fh or croak $?; - return \@svn_log; + return $ret; } sub svn_info { @@ -834,11 +863,23 @@ sub assert_revision_unknown { } } +sub trees_eq { + my ($x, $y) = @_; + my @x = safe_qx('git-cat-file','commit',$x); + my @y = safe_qx('git-cat-file','commit',$y); + if (($y[0] ne $x[0]) || $x[0] !~ /^tree $sha1\n$/ + || $y[0] !~ /^tree $sha1\n$/) { + print STDERR "Trees not equal: $y[0] != $x[0]\n"; + return 0 + } + return 1; +} + sub assert_revision_eq_or_unknown { my ($revno, $commit) = @_; if (-f "$REV_DIR/$revno") { my $current = file_to_s("$REV_DIR/$revno"); - if ($commit ne $current) { + if (($commit ne $current) && !trees_eq($commit, $current)) { croak "$REV_DIR/$revno already exists!\n", "current: $current\nexpected: $commit\n"; } @@ -850,9 +891,6 @@ sub git_commit { my ($log_msg, @parents) = @_; assert_revision_unknown($log_msg->{revision}); my $out_fh = IO::File->new_tmpfile or croak $!; - my $info = svn_info('.'); - my $uuid = $info->{'Repository UUID'}; - defined $uuid or croak "Unable to get Repository UUID\n"; map_tree_joins() if (@_branch_from && !%tree_map); @@ -891,11 +929,11 @@ sub git_commit { my $msg_fh = IO::File->new_tmpfile or croak $!; print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ", "$SVN_URL\@$log_msg->{revision}", - " $uuid\n" or croak $!; + " $SVN_UUID\n" or croak $!; $msg_fh->flush == 0 or croak $!; seek $msg_fh, 0, 0 or croak $!; - set_commit_env($log_msg, $uuid); + set_commit_env($log_msg); my @exec = ('git-commit-tree',$tree); push @exec, '-p', $_ foreach @exec_parents; @@ -914,7 +952,16 @@ sub git_commit { } my @update_ref = ('git-update-ref',"refs/remotes/$GIT_SVN",$commit); if (my $primary_parent = shift @exec_parents) { - push @update_ref, $primary_parent; + $pid = fork; + defined $pid or croak $!; + if (!$pid) { + close STDERR; + close STDOUT; + exec 'git-rev-parse','--verify', + "refs/remotes/$GIT_SVN^0"; + } + waitpid $pid, 0; + push @update_ref, $primary_parent unless $?; } sys(@update_ref); sys('git-update-ref',"$GIT_SVN/revs/$log_msg->{revision}",$commit); @@ -923,10 +970,10 @@ sub git_commit { } sub set_commit_env { - my ($log_msg, $uuid) = @_; + my ($log_msg) = @_; my $author = $log_msg->{author}; my ($name,$email) = defined $users{$author} ? @{$users{$author}} - : ($author,"$author\@$uuid"); + : ($author,"$author\@$SVN_UUID"); $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name; $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email; $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date}; @@ -987,13 +1034,37 @@ sub safe_qx { return wantarray ? @ret : join('',@ret); } -sub svn_check_ignore_externals { - return if $_no_ignore_ext; - unless (grep /ignore-externals/,(safe_qx(qw(svn co -h)))) { +sub svn_compat_check { + my @co_help = safe_qx(qw(svn co -h)); + unless (grep /ignore-externals/,@co_help) { print STDERR "W: Installed svn version does not support ", "--ignore-externals\n"; $_no_ignore_ext = 1; } + if (grep /usage: checkout URL\[\@REV\]/,@co_help) { + $_svn_co_url_revs = 1; + } + + # I really, really hope nobody hits this... + unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) { + print STDERR <<''; +W: The installed svn version does not support the --stop-on-copy flag in + the log command. + Lets hope the directory you're tracking is not a branch or tag + and was never moved within the repository... + + $_no_stop_copy = 1; + } +} + +# *sigh*, new versions of svn won't honor -r without URL@, +# (and they won't honor URL@ without -r, too!) +sub svn_cmd_checkout { + my ($url, $rev, $dir) = @_; + my @cmd = ('svn','co', "-r$rev"); + push @cmd, '--ignore-externals' unless $_no_ignore_ext; + $url .= "\@$rev" if $_svn_co_url_revs; + sys(@cmd, $url, $dir); } sub check_upgrade_needed { @@ -1056,9 +1127,13 @@ __END__ Data structures: -@svn_log = array of log_msg hashes +$svn_log hashref (as returned by svn_log_raw) +{ + fh => file handle of the log file, + state => state of the log file parser (sep/msg/rev/msg_start...) +} -$log_msg hash +$log_msg hashref as returned by next_log_entry($svn_log) { msg => 'whitespace-formatted log entry ', # trailing newline is preserved