contrib / git-svn / git-svn.perlon commit git-svn: add --repack and --repack-flags= options (dc5869c)
   1#!/usr/bin/env perl
   2# Copyright (C) 2006, Eric Wong <normalperson@yhbt.net>
   3# License: GPL v2 or later
   4use warnings;
   5use strict;
   6use vars qw/    $AUTHOR $VERSION
   7                $SVN_URL $SVN_INFO $SVN_WC $SVN_UUID
   8                $GIT_SVN_INDEX $GIT_SVN
   9                $GIT_DIR $REV_DIR $GIT_SVN_DIR/;
  10$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
  11$VERSION = '1.1.0-pre';
  12
  13use Cwd qw/abs_path/;
  14$GIT_DIR = abs_path($ENV{GIT_DIR} || '.git');
  15$ENV{GIT_DIR} = $GIT_DIR;
  16
  17my $LC_ALL = $ENV{LC_ALL};
  18# make sure the svn binary gives consistent output between locales and TZs:
  19$ENV{TZ} = 'UTC';
  20$ENV{LC_ALL} = 'C';
  21
  22# If SVN:: library support is added, please make the dependencies
  23# optional and preserve the capability to use the command-line client.
  24# use eval { require SVN::... } to make it lazy load
  25# We don't use any modules not in the standard Perl distribution:
  26use Carp qw/croak/;
  27use IO::File qw//;
  28use File::Basename qw/dirname basename/;
  29use File::Path qw/mkpath/;
  30use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
  31use File::Spec qw//;
  32use POSIX qw/strftime/;
  33my $sha1 = qr/[a-f\d]{40}/;
  34my $sha1_short = qr/[a-f\d]{4,40}/;
  35my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
  36        $_find_copies_harder, $_l, $_cp_similarity,
  37        $_repack, $_repack_nr, $_repack_flags,
  38        $_version, $_upgrade, $_authors, $_branch_all_refs);
  39my (@_branch_from, %tree_map, %users);
  40my ($_svn_co_url_revs, $_svn_pg_peg_revs);
  41my @repo_path_split_cache;
  42
  43my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext,
  44                'branch|b=s' => \@_branch_from,
  45                'branch-all-refs|B' => \$_branch_all_refs,
  46                'authors-file|A=s' => \$_authors,
  47                'repack:i' => \$_repack,
  48                'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
  49
  50# yes, 'native' sets "\n".  Patches to fix this for non-*nix systems welcome:
  51my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" );
  52
  53my %cmd = (
  54        fetch => [ \&fetch, "Download new revisions from SVN",
  55                        { 'revision|r=s' => \$_revision, %fc_opts } ],
  56        init => [ \&init, "Initialize a repo for tracking" .
  57                          " (requires URL argument)", { } ],
  58        commit => [ \&commit, "Commit git revisions to SVN",
  59                        {       'stdin|' => \$_stdin,
  60                                'edit|e' => \$_edit,
  61                                'rmdir' => \$_rmdir,
  62                                'find-copies-harder' => \$_find_copies_harder,
  63                                'l=i' => \$_l,
  64                                'copy-similarity|C=i'=> \$_cp_similarity,
  65                                %fc_opts,
  66                        } ],
  67        'show-ignore' => [ \&show_ignore, "Show svn:ignore listings", { } ],
  68        rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)",
  69                        { 'no-ignore-externals' => \$_no_ignore_ext,
  70                          'upgrade' => \$_upgrade } ],
  71);
  72my $cmd;
  73for (my $i = 0; $i < @ARGV; $i++) {
  74        if (defined $cmd{$ARGV[$i]}) {
  75                $cmd = $ARGV[$i];
  76                splice @ARGV, $i, 1;
  77                last;
  78        }
  79};
  80
  81my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
  82
  83read_repo_config(\%opts);
  84GetOptions(%opts, 'help|H|h' => \$_help,
  85                'version|V' => \$_version,
  86                'id|i=s' => \$GIT_SVN) or exit 1;
  87
  88set_default_vals();
  89usage(0) if $_help;
  90version() if $_version;
  91usage(1) unless defined $cmd;
  92init_vars();
  93load_authors() if $_authors;
  94load_all_refs() if $_branch_all_refs;
  95svn_compat_check();
  96migration_check() unless $cmd eq 'init';
  97$cmd{$cmd}->[0]->(@ARGV);
  98exit 0;
  99
 100####################### primary functions ######################
 101sub usage {
 102        my $exit = shift || 0;
 103        my $fd = $exit ? \*STDERR : \*STDOUT;
 104        print $fd <<"";
 105git-svn - bidirectional operations between a single Subversion tree and git
 106Usage: $0 <command> [options] [arguments]\n
 107
 108        print $fd "Available commands:\n" unless $cmd;
 109
 110        foreach (sort keys %cmd) {
 111                next if $cmd && $cmd ne $_;
 112                print $fd '  ',pack('A13',$_),$cmd{$_}->[1],"\n";
 113                foreach (keys %{$cmd{$_}->[2]}) {
 114                        # prints out arguments as they should be passed:
 115                        my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
 116                        print $fd ' ' x 17, join(', ', map { length $_ > 1 ?
 117                                                        "--$_" : "-$_" }
 118                                                split /\|/,$_)," $x\n";
 119                }
 120        }
 121        print $fd <<"";
 122\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an
 123arbitrary identifier if you're tracking multiple SVN branches/repositories in
 124one git repository and want to keep them separate.  See git-svn(1) for more
 125information.
 126
 127        exit $exit;
 128}
 129
 130sub version {
 131        print "git-svn version $VERSION\n";
 132        exit 0;
 133}
 134
 135sub rebuild {
 136        $SVN_URL = shift or undef;
 137        my $newest_rev = 0;
 138        if ($_upgrade) {
 139                sys('git-update-ref',"refs/remotes/$GIT_SVN","$GIT_SVN-HEAD");
 140        } else {
 141                check_upgrade_needed();
 142        }
 143
 144        my $pid = open(my $rev_list,'-|');
 145        defined $pid or croak $!;
 146        if ($pid == 0) {
 147                exec("git-rev-list","refs/remotes/$GIT_SVN") or croak $!;
 148        }
 149        my $latest;
 150        while (<$rev_list>) {
 151                chomp;
 152                my $c = $_;
 153                croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o;
 154                my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`);
 155                next if (!@commit); # skip merges
 156                my $id = $commit[$#commit];
 157                my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
 158                                                \s([a-f\d\-]+)$/x);
 159                if (!$rev || !$uuid || !$url) {
 160                        # some of the original repositories I made had
 161                        # indentifiers like this:
 162                        ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)
 163                                                        \@([a-f\d\-]+)/x);
 164                        if (!$rev || !$uuid) {
 165                                croak "Unable to extract revision or UUID from ",
 166                                        "$c, $id\n";
 167                        }
 168                }
 169
 170                # if we merged or otherwise started elsewhere, this is
 171                # how we break out of it
 172                next if (defined $SVN_UUID && ($uuid ne $SVN_UUID));
 173                next if (defined $SVN_URL && defined $url && ($url ne $SVN_URL));
 174
 175                print "r$rev = $c\n";
 176                unless (defined $latest) {
 177                        if (!$SVN_URL && !$url) {
 178                                croak "SVN repository location required: $url\n";
 179                        }
 180                        $SVN_URL ||= $url;
 181                        $SVN_UUID ||= $uuid;
 182                        setup_git_svn();
 183                        $latest = $rev;
 184                }
 185                assert_revision_eq_or_unknown($rev, $c);
 186                sys('git-update-ref',"svn/$GIT_SVN/revs/$rev",$c);
 187                $newest_rev = $rev if ($rev > $newest_rev);
 188        }
 189        close $rev_list or croak $?;
 190        if (!chdir $SVN_WC) {
 191                svn_cmd_checkout($SVN_URL, $latest, $SVN_WC);
 192                chdir $SVN_WC or croak $!;
 193        }
 194
 195        $pid = fork;
 196        defined $pid or croak $!;
 197        if ($pid == 0) {
 198                my @svn_up = qw(svn up);
 199                push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
 200                sys(@svn_up,"-r$newest_rev");
 201                $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
 202                index_changes();
 203                exec('git-write-tree') or croak $!;
 204        }
 205        waitpid $pid, 0;
 206        croak $? if $?;
 207
 208        if ($_upgrade) {
 209                print STDERR <<"";
 210Keeping deprecated refs/head/$GIT_SVN-HEAD for now.  Please remove it
 211when you have upgraded your tools and habits to use refs/remotes/$GIT_SVN
 212
 213        }
 214}
 215
 216sub init {
 217        $SVN_URL = shift or die "SVN repository location required " .
 218                                "as a command-line argument\n";
 219        unless (-d $GIT_DIR) {
 220                sys('git-init-db');
 221        }
 222        setup_git_svn();
 223}
 224
 225sub fetch {
 226        my (@parents) = @_;
 227        check_upgrade_needed();
 228        $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
 229        my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL);
 230        unless ($_revision) {
 231                $_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD';
 232        }
 233        push @log_args, "-r$_revision";
 234        push @log_args, '--stop-on-copy' unless $_no_stop_copy;
 235
 236        my $svn_log = svn_log_raw(@log_args);
 237
 238        my $base = next_log_entry($svn_log) or croak "No base revision!\n";
 239        my $last_commit = undef;
 240        unless (-d $SVN_WC) {
 241                svn_cmd_checkout($SVN_URL,$base->{revision},$SVN_WC);
 242                chdir $SVN_WC or croak $!;
 243                read_uuid();
 244                $last_commit = git_commit($base, @parents);
 245                assert_tree($last_commit);
 246        } else {
 247                chdir $SVN_WC or croak $!;
 248                read_uuid();
 249                eval { $last_commit = file_to_s("$REV_DIR/$base->{revision}") };
 250                # looks like a user manually cp'd and svn switch'ed
 251                unless ($last_commit) {
 252                        sys(qw/svn revert -R ./);
 253                        assert_svn_wc_clean($base->{revision});
 254                        $last_commit = git_commit($base, @parents);
 255                        assert_tree($last_commit);
 256                }
 257        }
 258        my @svn_up = qw(svn up);
 259        push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
 260        my $last = $base;
 261        while (my $log_msg = next_log_entry($svn_log)) {
 262                assert_tree($last_commit);
 263                if ($last->{revision} >= $log_msg->{revision}) {
 264                        croak "Out of order: last >= current: ",
 265                                "$last->{revision} >= $log_msg->{revision}\n";
 266                }
 267                # Revert is needed for cases like:
 268                # https://svn.musicpd.org/Jamming/trunk (r166:167), but
 269                # I can't seem to reproduce something like that on a test...
 270                sys(qw/svn revert -R ./);
 271                assert_svn_wc_clean($last->{revision});
 272                sys(@svn_up,"-r$log_msg->{revision}");
 273                $last_commit = git_commit($log_msg, $last_commit, @parents);
 274                $last = $log_msg;
 275        }
 276        unless (-e "$GIT_DIR/refs/heads/master") {
 277                sys(qw(git-update-ref refs/heads/master),$last_commit);
 278        }
 279        close $svn_log->{fh};
 280        return $last;
 281}
 282
 283sub commit {
 284        my (@commits) = @_;
 285        check_upgrade_needed();
 286        if ($_stdin || !@commits) {
 287                print "Reading from stdin...\n";
 288                @commits = ();
 289                while (<STDIN>) {
 290                        if (/\b($sha1_short)\b/o) {
 291                                unshift @commits, $1;
 292                        }
 293                }
 294        }
 295        my @revs;
 296        foreach my $c (@commits) {
 297                chomp(my @tmp = safe_qx('git-rev-parse',$c));
 298                if (scalar @tmp == 1) {
 299                        push @revs, $tmp[0];
 300                } elsif (scalar @tmp > 1) {
 301                        push @revs, reverse (safe_qx('git-rev-list',@tmp));
 302                } else {
 303                        die "Failed to rev-parse $c\n";
 304                }
 305        }
 306        chomp @revs;
 307
 308        chdir $SVN_WC or croak "Unable to chdir $SVN_WC: $!\n";
 309        my $info = svn_info('.');
 310        my $fetched = fetch();
 311        if ($info->{Revision} != $fetched->{revision}) {
 312                print STDERR "There are new revisions that were fetched ",
 313                                "and need to be merged (or acknowledged) ",
 314                                "before committing.\n";
 315                exit 1;
 316        }
 317        $info = svn_info('.');
 318        read_uuid($info);
 319        my $svn_current_rev =  $info->{'Last Changed Rev'};
 320        foreach my $c (@revs) {
 321                my $mods = svn_checkout_tree($svn_current_rev, $c);
 322                if (scalar @$mods == 0) {
 323                        print "Skipping, no changes detected\n";
 324                        next;
 325                }
 326                $svn_current_rev = svn_commit_tree($svn_current_rev, $c);
 327        }
 328        print "Done committing ",scalar @revs," revisions to SVN\n";
 329}
 330
 331sub show_ignore {
 332        require File::Find or die $!;
 333        my $exclude_file = "$GIT_DIR/info/exclude";
 334        open my $fh, '<', $exclude_file or croak $!;
 335        chomp(my @excludes = (<$fh>));
 336        close $fh or croak $!;
 337
 338        $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
 339        chdir $SVN_WC or croak $!;
 340        my %ign;
 341        File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){
 342                s#^\./##;
 343                @{$ign{$_}} = svn_propget_base('svn:ignore', $_);
 344                }}, no_chdir=>1},'.');
 345
 346        print "\n# /\n";
 347        foreach (@{$ign{'.'}}) { print '/',$_ if /\S/ }
 348        delete $ign{'.'};
 349        foreach my $i (sort keys %ign) {
 350                print "\n# ",$i,"\n";
 351                foreach (@{$ign{$i}}) { print '/',$i,'/',$_ if /\S/ }
 352        }
 353}
 354
 355########################### utility functions #########################
 356
 357sub read_uuid {
 358        return if $SVN_UUID;
 359        my $info = shift || svn_info('.');
 360        $SVN_UUID = $info->{'Repository UUID'} or
 361                                        croak "Repository UUID unreadable\n";
 362        s_to_file($SVN_UUID,"$GIT_SVN_DIR/info/uuid");
 363}
 364
 365sub quiet_run {
 366        my $pid = fork;
 367        defined $pid or croak $!;
 368        if (!$pid) {
 369                open my $null, '>', '/dev/null' or croak $!;
 370                open STDERR, '>&', $null or croak $!;
 371                open STDOUT, '>&', $null or croak $!;
 372                exec @_ or croak $!;
 373        }
 374        waitpid $pid, 0;
 375        return $?;
 376}
 377
 378sub repo_path_split {
 379        my $full_url = shift;
 380        $full_url =~ s#/+$##;
 381
 382        foreach (@repo_path_split_cache) {
 383                if ($full_url =~ s#$_##) {
 384                        my $u = $1;
 385                        $full_url =~ s#^/+##;
 386                        return ($u, $full_url);
 387                }
 388        }
 389
 390        my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i);
 391        $path =~ s#^/+##;
 392        my @paths = split(m#/+#, $path);
 393
 394        while (quiet_run(qw/svn ls --non-interactive/, $url)) {
 395                my $n = shift @paths || last;
 396                $url .= "/$n";
 397        }
 398        push @repo_path_split_cache, qr/^(\Q$url\E)/;
 399        return ($url, $path);
 400}
 401
 402sub setup_git_svn {
 403        defined $SVN_URL or croak "SVN repository location required\n";
 404        unless (-d $GIT_DIR) {
 405                croak "GIT_DIR=$GIT_DIR does not exist!\n";
 406        }
 407        mkpath([$GIT_SVN_DIR]);
 408        mkpath(["$GIT_SVN_DIR/info"]);
 409        mkpath([$REV_DIR]);
 410        s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url");
 411
 412        open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!;
 413        print $fd '.svn',"\n";
 414        close $fd or croak $!;
 415        my ($url, $path) = repo_path_split($SVN_URL);
 416        s_to_file($url, "$GIT_SVN_DIR/info/repo_url");
 417        s_to_file($path, "$GIT_SVN_DIR/info/repo_path");
 418}
 419
 420sub assert_svn_wc_clean {
 421        my ($svn_rev) = @_;
 422        croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
 423        my $lcr = svn_info('.')->{'Last Changed Rev'};
 424        if ($svn_rev != $lcr) {
 425                print STDERR "Checking for copy-tree ... ";
 426                my @diff = grep(/^Index: /,(safe_qx(qw(svn diff),
 427                                                "-r$lcr:$svn_rev")));
 428                if (@diff) {
 429                        croak "Nope!  Expected r$svn_rev, got r$lcr\n";
 430                } else {
 431                        print STDERR "OK!\n";
 432                }
 433        }
 434        my @status = grep(!/^Performing status on external/,(`svn status`));
 435        @status = grep(!/^\s*$/,@status);
 436        if (scalar @status) {
 437                print STDERR "Tree ($SVN_WC) is not clean:\n";
 438                print STDERR $_ foreach @status;
 439                croak;
 440        }
 441}
 442
 443sub assert_tree {
 444        my ($treeish) = @_;
 445        croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o;
 446        chomp(my $type = `git-cat-file -t $treeish`);
 447        my $expected;
 448        while ($type eq 'tag') {
 449                chomp(($treeish, $type) = `git-cat-file tag $treeish`);
 450        }
 451        if ($type eq 'commit') {
 452                $expected = (grep /^tree /,`git-cat-file commit $treeish`)[0];
 453                ($expected) = ($expected =~ /^tree ($sha1)$/);
 454                die "Unable to get tree from $treeish\n" unless $expected;
 455        } elsif ($type eq 'tree') {
 456                $expected = $treeish;
 457        } else {
 458                die "$treeish is a $type, expected tree, tag or commit\n";
 459        }
 460
 461        my $old_index = $ENV{GIT_INDEX_FILE};
 462        my $tmpindex = $GIT_SVN_INDEX.'.assert-tmp';
 463        if (-e $tmpindex) {
 464                unlink $tmpindex or croak $!;
 465        }
 466        $ENV{GIT_INDEX_FILE} = $tmpindex;
 467        index_changes(1);
 468        chomp(my $tree = `git-write-tree`);
 469        if ($old_index) {
 470                $ENV{GIT_INDEX_FILE} = $old_index;
 471        } else {
 472                delete $ENV{GIT_INDEX_FILE};
 473        }
 474        if ($tree ne $expected) {
 475                croak "Tree mismatch, Got: $tree, Expected: $expected\n";
 476        }
 477        unlink $tmpindex;
 478}
 479
 480sub parse_diff_tree {
 481        my $diff_fh = shift;
 482        local $/ = "\0";
 483        my $state = 'meta';
 484        my @mods;
 485        while (<$diff_fh>) {
 486                chomp $_; # this gets rid of the trailing "\0"
 487                if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
 488                                        $sha1\s($sha1)\s([MTCRAD])\d*$/xo) {
 489                        push @mods, {   mode_a => $1, mode_b => $2,
 490                                        sha1_b => $3, chg => $4 };
 491                        if ($4 =~ /^(?:C|R)$/) {
 492                                $state = 'file_a';
 493                        } else {
 494                                $state = 'file_b';
 495                        }
 496                } elsif ($state eq 'file_a') {
 497                        my $x = $mods[$#mods] or croak "Empty array\n";
 498                        if ($x->{chg} !~ /^(?:C|R)$/) {
 499                                croak "Error parsing $_, $x->{chg}\n";
 500                        }
 501                        $x->{file_a} = $_;
 502                        $state = 'file_b';
 503                } elsif ($state eq 'file_b') {
 504                        my $x = $mods[$#mods] or croak "Empty array\n";
 505                        if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
 506                                croak "Error parsing $_, $x->{chg}\n";
 507                        }
 508                        if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
 509                                croak "Error parsing $_, $x->{chg}\n";
 510                        }
 511                        $x->{file_b} = $_;
 512                        $state = 'meta';
 513                } else {
 514                        croak "Error parsing $_\n";
 515                }
 516        }
 517        close $diff_fh or croak $!;
 518
 519        return \@mods;
 520}
 521
 522sub svn_check_prop_executable {
 523        my $m = shift;
 524        return if -l $m->{file_b};
 525        if ($m->{mode_b} =~ /755$/) {
 526                chmod((0755 &~ umask),$m->{file_b}) or croak $!;
 527                if ($m->{mode_a} !~ /755$/) {
 528                        sys(qw(svn propset svn:executable 1), $m->{file_b});
 529                }
 530                -x $m->{file_b} or croak "$m->{file_b} is not executable!\n";
 531        } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
 532                sys(qw(svn propdel svn:executable), $m->{file_b});
 533                chmod((0644 &~ umask),$m->{file_b}) or croak $!;
 534                -x $m->{file_b} and croak "$m->{file_b} is executable!\n";
 535        }
 536}
 537
 538sub svn_ensure_parent_path {
 539        my $dir_b = dirname(shift);
 540        svn_ensure_parent_path($dir_b) if ($dir_b ne File::Spec->curdir);
 541        mkpath([$dir_b]) unless (-d $dir_b);
 542        sys(qw(svn add -N), $dir_b) unless (-d "$dir_b/.svn");
 543}
 544
 545sub precommit_check {
 546        my $mods = shift;
 547        my (%rm_file, %rmdir_check, %added_check);
 548
 549        my %o = ( D => 0, R => 1, C => 2, A => 3, M => 3, T => 3 );
 550        foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
 551                if ($m->{chg} eq 'R') {
 552                        if (-d $m->{file_b}) {
 553                                err_dir_to_file("$m->{file_a} => $m->{file_b}");
 554                        }
 555                        # dir/$file => dir/file/$file
 556                        my $dirname = dirname($m->{file_b});
 557                        while ($dirname ne File::Spec->curdir) {
 558                                if ($dirname ne $m->{file_a}) {
 559                                        $dirname = dirname($dirname);
 560                                        next;
 561                                }
 562                                err_file_to_dir("$m->{file_a} => $m->{file_b}");
 563                        }
 564                        # baz/zzz => baz (baz is a file)
 565                        $dirname = dirname($m->{file_a});
 566                        while ($dirname ne File::Spec->curdir) {
 567                                if ($dirname ne $m->{file_b}) {
 568                                        $dirname = dirname($dirname);
 569                                        next;
 570                                }
 571                                err_dir_to_file("$m->{file_a} => $m->{file_b}");
 572                        }
 573                }
 574                if ($m->{chg} =~ /^(D|R)$/) {
 575                        my $t = $1 eq 'D' ? 'file_b' : 'file_a';
 576                        $rm_file{ $m->{$t} } = 1;
 577                        my $dirname = dirname( $m->{$t} );
 578                        my $basename = basename( $m->{$t} );
 579                        $rmdir_check{$dirname}->{$basename} = 1;
 580                } elsif ($m->{chg} =~ /^(?:A|C)$/) {
 581                        if (-d $m->{file_b}) {
 582                                err_dir_to_file($m->{file_b});
 583                        }
 584                        my $dirname = dirname( $m->{file_b} );
 585                        my $basename = basename( $m->{file_b} );
 586                        $added_check{$dirname}->{$basename} = 1;
 587                        while ($dirname ne File::Spec->curdir) {
 588                                if ($rm_file{$dirname}) {
 589                                        err_file_to_dir($m->{file_b});
 590                                }
 591                                $dirname = dirname $dirname;
 592                        }
 593                }
 594        }
 595        return (\%rmdir_check, \%added_check);
 596
 597        sub err_dir_to_file {
 598                my $file = shift;
 599                print STDERR "Node change from directory to file ",
 600                                "is not supported by Subversion: ",$file,"\n";
 601                exit 1;
 602        }
 603        sub err_file_to_dir {
 604                my $file = shift;
 605                print STDERR "Node change from file to directory ",
 606                                "is not supported by Subversion: ",$file,"\n";
 607                exit 1;
 608        }
 609}
 610
 611sub svn_checkout_tree {
 612        my ($svn_rev, $treeish) = @_;
 613        my $from = file_to_s("$REV_DIR/$svn_rev");
 614        assert_tree($from);
 615        print "diff-tree $from $treeish\n";
 616        my $pid = open my $diff_fh, '-|';
 617        defined $pid or croak $!;
 618        if ($pid == 0) {
 619                my @diff_tree = qw(git-diff-tree -z -r);
 620                if ($_cp_similarity) {
 621                        push @diff_tree, "-C$_cp_similarity";
 622                } else {
 623                        push @diff_tree, '-C';
 624                }
 625                push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
 626                push @diff_tree, "-l$_l" if defined $_l;
 627                exec(@diff_tree, $from, $treeish) or croak $!;
 628        }
 629        my $mods = parse_diff_tree($diff_fh);
 630        unless (@$mods) {
 631                # git can do empty commits, but SVN doesn't allow it...
 632                return $mods;
 633        }
 634        my ($rm, $add) = precommit_check($mods);
 635
 636        my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
 637        foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
 638                if ($m->{chg} eq 'C') {
 639                        svn_ensure_parent_path( $m->{file_b} );
 640                        sys(qw(svn cp),         $m->{file_a}, $m->{file_b});
 641                        apply_mod_line_blob($m);
 642                        svn_check_prop_executable($m);
 643                } elsif ($m->{chg} eq 'D') {
 644                        sys(qw(svn rm --force), $m->{file_b});
 645                } elsif ($m->{chg} eq 'R') {
 646                        svn_ensure_parent_path( $m->{file_b} );
 647                        sys(qw(svn mv --force), $m->{file_a}, $m->{file_b});
 648                        apply_mod_line_blob($m);
 649                        svn_check_prop_executable($m);
 650                } elsif ($m->{chg} eq 'M') {
 651                        apply_mod_line_blob($m);
 652                        svn_check_prop_executable($m);
 653                } elsif ($m->{chg} eq 'T') {
 654                        sys(qw(svn rm --force),$m->{file_b});
 655                        apply_mod_line_blob($m);
 656                        sys(qw(svn add --force), $m->{file_b});
 657                        svn_check_prop_executable($m);
 658                } elsif ($m->{chg} eq 'A') {
 659                        svn_ensure_parent_path( $m->{file_b} );
 660                        apply_mod_line_blob($m);
 661                        sys(qw(svn add --force), $m->{file_b});
 662                        svn_check_prop_executable($m);
 663                } else {
 664                        croak "Invalid chg: $m->{chg}\n";
 665                }
 666        }
 667
 668        assert_tree($treeish);
 669        if ($_rmdir) { # remove empty directories
 670                handle_rmdir($rm, $add);
 671        }
 672        assert_tree($treeish);
 673        return $mods;
 674}
 675
 676# svn ls doesn't work with respect to the current working tree, but what's
 677# in the repository.  There's not even an option for it... *sigh*
 678# (added files don't show up and removed files remain in the ls listing)
 679sub svn_ls_current {
 680        my ($dir, $rm, $add) = @_;
 681        chomp(my @ls = safe_qx('svn','ls',$dir));
 682        my @ret = ();
 683        foreach (@ls) {
 684                s#/$##; # trailing slashes are evil
 685                push @ret, $_ unless $rm->{$dir}->{$_};
 686        }
 687        if (exists $add->{$dir}) {
 688                push @ret, keys %{$add->{$dir}};
 689        }
 690        return \@ret;
 691}
 692
 693sub handle_rmdir {
 694        my ($rm, $add) = @_;
 695
 696        foreach my $dir (sort {length $b <=> length $a} keys %$rm) {
 697                my $ls = svn_ls_current($dir, $rm, $add);
 698                next if (scalar @$ls);
 699                sys(qw(svn rm --force),$dir);
 700
 701                my $dn = dirname $dir;
 702                $rm->{ $dn }->{ basename $dir } = 1;
 703                $ls = svn_ls_current($dn, $rm, $add);
 704                while (scalar @$ls == 0 && $dn ne File::Spec->curdir) {
 705                        sys(qw(svn rm --force),$dn);
 706                        $dir = basename $dn;
 707                        $dn = dirname $dn;
 708                        $rm->{ $dn }->{ $dir } = 1;
 709                        $ls = svn_ls_current($dn, $rm, $add);
 710                }
 711        }
 712}
 713
 714sub svn_commit_tree {
 715        my ($svn_rev, $commit) = @_;
 716        my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
 717        my %log_msg = ( msg => '' );
 718        open my $msg, '>', $commit_msg or croak $!;
 719
 720        chomp(my $type = `git-cat-file -t $commit`);
 721        if ($type eq 'commit') {
 722                my $pid = open my $msg_fh, '-|';
 723                defined $pid or croak $!;
 724
 725                if ($pid == 0) {
 726                        exec(qw(git-cat-file commit), $commit) or croak $!;
 727                }
 728                my $in_msg = 0;
 729                while (<$msg_fh>) {
 730                        if (!$in_msg) {
 731                                $in_msg = 1 if (/^\s*$/);
 732                        } elsif (/^git-svn-id: /) {
 733                                # skip this, we regenerate the correct one
 734                                # on re-fetch anyways
 735                        } else {
 736                                print $msg $_ or croak $!;
 737                        }
 738                }
 739                close $msg_fh or croak $!;
 740        }
 741        close $msg or croak $!;
 742
 743        if ($_edit || ($type eq 'tree')) {
 744                my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
 745                system($editor, $commit_msg);
 746        }
 747
 748        # file_to_s removes all trailing newlines, so just use chomp() here:
 749        open $msg, '<', $commit_msg or croak $!;
 750        { local $/; chomp($log_msg{msg} = <$msg>); }
 751        close $msg or croak $!;
 752
 753        my ($oneline) = ($log_msg{msg} =~ /([^\n\r]+)/);
 754        print "Committing $commit: $oneline\n";
 755
 756        if (defined $LC_ALL) {
 757                $ENV{LC_ALL} = $LC_ALL;
 758        } else {
 759                delete $ENV{LC_ALL};
 760        }
 761        my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
 762        $ENV{LC_ALL} = 'C';
 763        unlink $commit_msg;
 764        my ($committed) = ($ci_output[$#ci_output] =~ /(\d+)/);
 765        if (!defined $committed) {
 766                my $out = join("\n",@ci_output);
 767                print STDERR "W: Trouble parsing \`svn commit' output:\n\n",
 768                                $out, "\n\nAssuming English locale...";
 769                ($committed) = ($out =~ /^Committed revision \d+\./sm);
 770                defined $committed or die " FAILED!\n",
 771                        "Commit output failed to parse committed revision!\n",
 772                print STDERR " OK\n";
 773        }
 774
 775        my @svn_up = qw(svn up);
 776        push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
 777        if ($committed == ($svn_rev + 1)) {
 778                push @svn_up, "-r$committed";
 779                sys(@svn_up);
 780                my $info = svn_info('.');
 781                my $date = $info->{'Last Changed Date'} or die "Missing date\n";
 782                if ($info->{'Last Changed Rev'} != $committed) {
 783                        croak "$info->{'Last Changed Rev'} != $committed\n"
 784                }
 785                my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
 786                                        /(\d{4})\-(\d\d)\-(\d\d)\s
 787                                         (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
 788                                         or croak "Failed to parse date: $date\n";
 789                $log_msg{date} = "$tz $Y-$m-$d $H:$M:$S";
 790                $log_msg{author} = $info->{'Last Changed Author'};
 791                $log_msg{revision} = $committed;
 792                $log_msg{msg} .= "\n";
 793                my $parent = file_to_s("$REV_DIR/$svn_rev");
 794                git_commit(\%log_msg, $parent, $commit);
 795                return $committed;
 796        }
 797        # resync immediately
 798        push @svn_up, "-r$svn_rev";
 799        sys(@svn_up);
 800        return fetch("$committed=$commit")->{revision};
 801}
 802
 803# read the entire log into a temporary file (which is removed ASAP)
 804# and store the file handle + parser state
 805sub svn_log_raw {
 806        my (@log_args) = @_;
 807        my $log_fh = IO::File->new_tmpfile or croak $!;
 808        my $pid = fork;
 809        defined $pid or croak $!;
 810        if (!$pid) {
 811                open STDOUT, '>&', $log_fh or croak $!;
 812                exec (qw(svn log), @log_args) or croak $!
 813        }
 814        waitpid $pid, 0;
 815        croak $? if $?;
 816        seek $log_fh, 0, 0 or croak $!;
 817        return { state => 'sep', fh => $log_fh };
 818}
 819
 820sub next_log_entry {
 821        my $log = shift; # retval of svn_log_raw()
 822        my $ret = undef;
 823        my $fh = $log->{fh};
 824
 825        while (<$fh>) {
 826                chomp;
 827                if (/^\-{72}$/) {
 828                        if ($log->{state} eq 'msg') {
 829                                if ($ret->{lines}) {
 830                                        $ret->{msg} .= $_."\n";
 831                                        unless(--$ret->{lines}) {
 832                                                $log->{state} = 'sep';
 833                                        }
 834                                } else {
 835                                        croak "Log parse error at: $_\n",
 836                                                $ret->{revision},
 837                                                "\n";
 838                                }
 839                                next;
 840                        }
 841                        if ($log->{state} ne 'sep') {
 842                                croak "Log parse error at: $_\n",
 843                                        "state: $log->{state}\n",
 844                                        $ret->{revision},
 845                                        "\n";
 846                        }
 847                        $log->{state} = 'rev';
 848
 849                        # if we have an empty log message, put something there:
 850                        if ($ret) {
 851                                $ret->{msg} ||= "\n";
 852                                delete $ret->{lines};
 853                                return $ret;
 854                        }
 855                        next;
 856                }
 857                if ($log->{state} eq 'rev' && s/^r(\d+)\s*\|\s*//) {
 858                        my $rev = $1;
 859                        my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3);
 860                        ($lines) = ($lines =~ /(\d+)/);
 861                        my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
 862                                        /(\d{4})\-(\d\d)\-(\d\d)\s
 863                                         (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
 864                                         or croak "Failed to parse date: $date\n";
 865                        $ret = {        revision => $rev,
 866                                        date => "$tz $Y-$m-$d $H:$M:$S",
 867                                        author => $author,
 868                                        lines => $lines,
 869                                        msg => '' };
 870                        if (defined $_authors && ! defined $users{$author}) {
 871                                die "Author: $author not defined in ",
 872                                                "$_authors file\n";
 873                        }
 874                        $log->{state} = 'msg_start';
 875                        next;
 876                }
 877                # skip the first blank line of the message:
 878                if ($log->{state} eq 'msg_start' && /^$/) {
 879                        $log->{state} = 'msg';
 880                } elsif ($log->{state} eq 'msg') {
 881                        if ($ret->{lines}) {
 882                                $ret->{msg} .= $_."\n";
 883                                unless (--$ret->{lines}) {
 884                                        $log->{state} = 'sep';
 885                                }
 886                        } else {
 887                                croak "Log parse error at: $_\n",
 888                                        $ret->{revision},"\n";
 889                        }
 890                }
 891        }
 892        return $ret;
 893}
 894
 895sub svn_info {
 896        my $url = shift || $SVN_URL;
 897
 898        my $pid = open my $info_fh, '-|';
 899        defined $pid or croak $!;
 900
 901        if ($pid == 0) {
 902                exec(qw(svn info),$url) or croak $!;
 903        }
 904
 905        my $ret = {};
 906        # only single-lines seem to exist in svn info output
 907        while (<$info_fh>) {
 908                chomp $_;
 909                if (m#^([^:]+)\s*:\s*(\S.*)$#) {
 910                        $ret->{$1} = $2;
 911                        push @{$ret->{-order}}, $1;
 912                }
 913        }
 914        close $info_fh or croak $!;
 915        return $ret;
 916}
 917
 918sub sys { system(@_) == 0 or croak $? }
 919
 920sub eol_cp {
 921        my ($from, $to) = @_;
 922        my $es = svn_propget_base('svn:eol-style', $to);
 923        open my $rfd, '<', $from or croak $!;
 924        binmode $rfd or croak $!;
 925        open my $wfd, '>', $to or croak $!;
 926        binmode $wfd or croak $!;
 927
 928        my $eol = $EOL{$es} or undef;
 929        my $buf;
 930        use bytes;
 931        while (1) {
 932                my ($r, $w, $t);
 933                defined($r = sysread($rfd, $buf, 4096)) or croak $!;
 934                return unless $r;
 935                if ($eol) {
 936                        if ($buf =~ /\015$/) {
 937                                my $c;
 938                                defined($r = sysread($rfd,$c,1)) or croak $!;
 939                                $buf .= $c if $r > 0;
 940                        }
 941                        $buf =~ s/(?:\015\012|\015|\012)/$eol/gs;
 942                        $r = length($buf);
 943                }
 944                for ($w = 0; $w < $r; $w += $t) {
 945                        $t = syswrite($wfd, $buf, $r - $w, $w) or croak $!;
 946                }
 947        }
 948        no bytes;
 949}
 950
 951sub do_update_index {
 952        my ($z_cmd, $cmd, $no_text_base) = @_;
 953
 954        my $z = open my $p, '-|';
 955        defined $z or croak $!;
 956        unless ($z) { exec @$z_cmd or croak $! }
 957
 958        my $pid = open my $ui, '|-';
 959        defined $pid or croak $!;
 960        unless ($pid) {
 961                exec('git-update-index',"--$cmd",'-z','--stdin') or croak $!;
 962        }
 963        local $/ = "\0";
 964        while (my $x = <$p>) {
 965                chomp $x;
 966                if (!$no_text_base && lstat $x && ! -l _ &&
 967                                svn_propget_base('svn:keywords', $x)) {
 968                        my $mode = -x _ ? 0755 : 0644;
 969                        my ($v,$d,$f) = File::Spec->splitpath($x);
 970                        my $tb = File::Spec->catfile($d, '.svn', 'tmp',
 971                                                'text-base',"$f.svn-base");
 972                        $tb =~ s#^/##;
 973                        unless (-f $tb) {
 974                                $tb = File::Spec->catfile($d, '.svn',
 975                                                'text-base',"$f.svn-base");
 976                                $tb =~ s#^/##;
 977                        }
 978                        unlink $x or croak $!;
 979                        eol_cp($tb, $x);
 980                        chmod(($mode &~ umask), $x) or croak $!;
 981                }
 982                print $ui $x,"\0";
 983        }
 984        close $ui or croak $!;
 985}
 986
 987sub index_changes {
 988        my $no_text_base = shift;
 989        do_update_index([qw/git-diff-files --name-only -z/],
 990                        'remove',
 991                        $no_text_base);
 992        do_update_index([qw/git-ls-files -z --others/,
 993                                "--exclude-from=$GIT_SVN_DIR/info/exclude"],
 994                        'add',
 995                        $no_text_base);
 996}
 997
 998sub s_to_file {
 999        my ($str, $file, $mode) = @_;
1000        open my $fd,'>',$file or croak $!;
1001        print $fd $str,"\n" or croak $!;
1002        close $fd or croak $!;
1003        chmod ($mode &~ umask, $file) if (defined $mode);
1004}
1005
1006sub file_to_s {
1007        my $file = shift;
1008        open my $fd,'<',$file or croak "$!: file: $file\n";
1009        local $/;
1010        my $ret = <$fd>;
1011        close $fd or croak $!;
1012        $ret =~ s/\s*$//s;
1013        return $ret;
1014}
1015
1016sub assert_revision_unknown {
1017        my $revno = shift;
1018        if (-f "$REV_DIR/$revno") {
1019                croak "$REV_DIR/$revno already exists! ",
1020                                "Why are we refetching it?";
1021        }
1022}
1023
1024sub trees_eq {
1025        my ($x, $y) = @_;
1026        my @x = safe_qx('git-cat-file','commit',$x);
1027        my @y = safe_qx('git-cat-file','commit',$y);
1028        if (($y[0] ne $x[0]) || $x[0] !~ /^tree $sha1\n$/
1029                                || $y[0] !~ /^tree $sha1\n$/) {
1030                print STDERR "Trees not equal: $y[0] != $x[0]\n";
1031                return 0
1032        }
1033        return 1;
1034}
1035
1036sub assert_revision_eq_or_unknown {
1037        my ($revno, $commit) = @_;
1038        if (-f "$REV_DIR/$revno") {
1039                my $current = file_to_s("$REV_DIR/$revno");
1040                if (($commit ne $current) && !trees_eq($commit, $current)) {
1041                        croak "$REV_DIR/$revno already exists!\n",
1042                                "current: $current\nexpected: $commit\n";
1043                }
1044                return;
1045        }
1046}
1047
1048sub git_commit {
1049        my ($log_msg, @parents) = @_;
1050        assert_revision_unknown($log_msg->{revision});
1051        my $out_fh = IO::File->new_tmpfile or croak $!;
1052
1053        map_tree_joins() if (@_branch_from && !%tree_map);
1054
1055        # commit parents can be conditionally bound to a particular
1056        # svn revision via: "svn_revno=commit_sha1", filter them out here:
1057        my @exec_parents;
1058        foreach my $p (@parents) {
1059                next unless defined $p;
1060                if ($p =~ /^(\d+)=($sha1_short)$/o) {
1061                        if ($1 == $log_msg->{revision}) {
1062                                push @exec_parents, $2;
1063                        }
1064                } else {
1065                        push @exec_parents, $p if $p =~ /$sha1_short/o;
1066                }
1067        }
1068
1069        my $pid = fork;
1070        defined $pid or croak $!;
1071        if ($pid == 0) {
1072                $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
1073                index_changes();
1074                chomp(my $tree = `git-write-tree`);
1075                croak $? if $?;
1076                if (exists $tree_map{$tree}) {
1077                        my %seen_parent = map { $_ => 1 } @exec_parents;
1078                        foreach (@{$tree_map{$tree}}) {
1079                                # MAXPARENT is defined to 16 in commit-tree.c:
1080                                if ($seen_parent{$_} || @exec_parents > 16) {
1081                                        next;
1082                                }
1083                                push @exec_parents, $_;
1084                                $seen_parent{$_} = 1;
1085                        }
1086                }
1087                my $msg_fh = IO::File->new_tmpfile or croak $!;
1088                print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ",
1089                                        "$SVN_URL\@$log_msg->{revision}",
1090                                        " $SVN_UUID\n" or croak $!;
1091                $msg_fh->flush == 0 or croak $!;
1092                seek $msg_fh, 0, 0 or croak $!;
1093
1094                set_commit_env($log_msg);
1095
1096                my @exec = ('git-commit-tree',$tree);
1097                push @exec, '-p', $_  foreach @exec_parents;
1098                open STDIN, '<&', $msg_fh or croak $!;
1099                open STDOUT, '>&', $out_fh or croak $!;
1100                exec @exec or croak $!;
1101        }
1102        waitpid($pid,0);
1103        croak $? if $?;
1104
1105        $out_fh->flush == 0 or croak $!;
1106        seek $out_fh, 0, 0 or croak $!;
1107        chomp(my $commit = do { local $/; <$out_fh> });
1108        if ($commit !~ /^$sha1$/o) {
1109                croak "Failed to commit, invalid sha1: $commit\n";
1110        }
1111        my @update_ref = ('git-update-ref',"refs/remotes/$GIT_SVN",$commit);
1112        if (my $primary_parent = shift @exec_parents) {
1113                $pid = fork;
1114                defined $pid or croak $!;
1115                if (!$pid) {
1116                        close STDERR;
1117                        close STDOUT;
1118                        exec 'git-rev-parse','--verify',
1119                                        "refs/remotes/$GIT_SVN^0" or croak $!;
1120                }
1121                waitpid $pid, 0;
1122                push @update_ref, $primary_parent unless $?;
1123        }
1124        sys(@update_ref);
1125        sys('git-update-ref',"svn/$GIT_SVN/revs/$log_msg->{revision}",$commit);
1126        print "r$log_msg->{revision} = $commit\n";
1127        if ($_repack && (--$_repack_nr == 0)) {
1128                $_repack_nr = $_repack;
1129                sys("git repack $_repack_flags");
1130        }
1131        return $commit;
1132}
1133
1134sub set_commit_env {
1135        my ($log_msg) = @_;
1136        my $author = $log_msg->{author};
1137        my ($name,$email) = defined $users{$author} ?  @{$users{$author}}
1138                                : ($author,"$author\@$SVN_UUID");
1139        $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name;
1140        $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email;
1141        $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date};
1142}
1143
1144sub apply_mod_line_blob {
1145        my $m = shift;
1146        if ($m->{mode_b} =~ /^120/) {
1147                blob_to_symlink($m->{sha1_b}, $m->{file_b});
1148        } else {
1149                blob_to_file($m->{sha1_b}, $m->{file_b});
1150        }
1151}
1152
1153sub blob_to_symlink {
1154        my ($blob, $link) = @_;
1155        defined $link or croak "\$link not defined!\n";
1156        croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
1157        if (-l $link || -f _) {
1158                unlink $link or croak $!;
1159        }
1160
1161        my $dest = `git-cat-file blob $blob`; # no newline, so no chomp
1162        symlink $dest, $link or croak $!;
1163}
1164
1165sub blob_to_file {
1166        my ($blob, $file) = @_;
1167        defined $file or croak "\$file not defined!\n";
1168        croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
1169        if (-l $file || -f _) {
1170                unlink $file or croak $!;
1171        }
1172
1173        open my $blob_fh, '>', $file or croak "$!: $file\n";
1174        my $pid = fork;
1175        defined $pid or croak $!;
1176
1177        if ($pid == 0) {
1178                open STDOUT, '>&', $blob_fh or croak $!;
1179                exec('git-cat-file','blob',$blob) or croak $!;
1180        }
1181        waitpid $pid, 0;
1182        croak $? if $?;
1183
1184        close $blob_fh or croak $!;
1185}
1186
1187sub safe_qx {
1188        my $pid = open my $child, '-|';
1189        defined $pid or croak $!;
1190        if ($pid == 0) {
1191                exec(@_) or croak $!;
1192        }
1193        my @ret = (<$child>);
1194        close $child or croak $?;
1195        die $? if $?; # just in case close didn't error out
1196        return wantarray ? @ret : join('',@ret);
1197}
1198
1199sub svn_compat_check {
1200        my @co_help = safe_qx(qw(svn co -h));
1201        unless (grep /ignore-externals/,@co_help) {
1202                print STDERR "W: Installed svn version does not support ",
1203                                "--ignore-externals\n";
1204                $_no_ignore_ext = 1;
1205        }
1206        if (grep /usage: checkout URL\[\@REV\]/,@co_help) {
1207                $_svn_co_url_revs = 1;
1208        }
1209        if (grep /\[TARGET\[\@REV\]\.\.\.\]/, `svn propget -h`) {
1210                $_svn_pg_peg_revs = 1;
1211        }
1212
1213        # I really, really hope nobody hits this...
1214        unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) {
1215                print STDERR <<'';
1216W: The installed svn version does not support the --stop-on-copy flag in
1217   the log command.
1218   Lets hope the directory you're tracking is not a branch or tag
1219   and was never moved within the repository...
1220
1221                $_no_stop_copy = 1;
1222        }
1223}
1224
1225# *sigh*, new versions of svn won't honor -r<rev> without URL@<rev>,
1226# (and they won't honor URL@<rev> without -r<rev>, too!)
1227sub svn_cmd_checkout {
1228        my ($url, $rev, $dir) = @_;
1229        my @cmd = ('svn','co', "-r$rev");
1230        push @cmd, '--ignore-externals' unless $_no_ignore_ext;
1231        $url .= "\@$rev" if $_svn_co_url_revs;
1232        sys(@cmd, $url, $dir);
1233}
1234
1235sub check_upgrade_needed {
1236        my $old = eval {
1237                my $pid = open my $child, '-|';
1238                defined $pid or croak $!;
1239                if ($pid == 0) {
1240                        close STDERR;
1241                        exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $!;
1242                }
1243                my @ret = (<$child>);
1244                close $child or croak $?;
1245                die $? if $?; # just in case close didn't error out
1246                return wantarray ? @ret : join('',@ret);
1247        };
1248        return unless $old;
1249        my $head = eval { safe_qx('git-rev-parse',"refs/remotes/$GIT_SVN") };
1250        if ($@ || !$head) {
1251                print STDERR "Please run: $0 rebuild --upgrade\n";
1252                exit 1;
1253        }
1254}
1255
1256# fills %tree_map with a reverse mapping of trees to commits.  Useful
1257# for finding parents to commit on.
1258sub map_tree_joins {
1259        my %seen;
1260        foreach my $br (@_branch_from) {
1261                my $pid = open my $pipe, '-|';
1262                defined $pid or croak $!;
1263                if ($pid == 0) {
1264                        exec(qw(git-rev-list --topo-order --pretty=raw), $br)
1265                                                                or croak $!;
1266                }
1267                while (<$pipe>) {
1268                        if (/^commit ($sha1)$/o) {
1269                                my $commit = $1;
1270
1271                                # if we've seen a commit,
1272                                # we've seen its parents
1273                                last if $seen{$commit};
1274                                my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o);
1275                                unless (defined $tree) {
1276                                        die "Failed to parse commit $commit\n";
1277                                }
1278                                push @{$tree_map{$tree}}, $commit;
1279                                $seen{$commit} = 1;
1280                        }
1281                }
1282                close $pipe; # we could be breaking the pipe early
1283        }
1284}
1285
1286sub load_all_refs {
1287        if (@_branch_from) {
1288                print STDERR '--branch|-b parameters are ignored when ',
1289                        "--branch-all-refs|-B is passed\n";
1290        }
1291
1292        # don't worry about rev-list on non-commit objects/tags,
1293        # it shouldn't blow up if a ref is a blob or tree...
1294        chomp(@_branch_from = `git-rev-parse --symbolic --all`);
1295}
1296
1297# '<svn username> = real-name <email address>' mapping based on git-svnimport:
1298sub load_authors {
1299        open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
1300        while (<$authors>) {
1301                chomp;
1302                next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
1303                my ($user, $name, $email) = ($1, $2, $3);
1304                $users{$user} = [$name, $email];
1305        }
1306        close $authors or croak $!;
1307}
1308
1309sub svn_propget_base {
1310        my ($p, $f) = @_;
1311        $f .= '@BASE' if $_svn_pg_peg_revs;
1312        return safe_qx(qw/svn propget/, $p, $f);
1313}
1314
1315sub migration_check {
1316        return if (-d "$GIT_DIR/svn" || !-d $GIT_DIR);
1317        print "Upgrading repository...\n";
1318        unless (-d "$GIT_DIR/svn") {
1319                mkdir "$GIT_DIR/svn" or croak $!;
1320        }
1321        print "Data from a previous version of git-svn exists, but\n\t",
1322                                "$GIT_SVN_DIR\n\t(required for this version ",
1323                                "($VERSION) of git-svn) does not.\n";
1324
1325        foreach my $x (`git-rev-parse --symbolic --all`) {
1326                next unless $x =~ s#^refs/remotes/##;
1327                chomp $x;
1328                next unless -f "$GIT_DIR/$x/info/url";
1329                my $u = eval { file_to_s("$GIT_DIR/$x/info/url") };
1330                next unless $u;
1331                my $dn = dirname("$GIT_DIR/svn/$x");
1332                mkpath([$dn]) unless -d $dn;
1333                rename "$GIT_DIR/$x", "$GIT_DIR/svn/$x" or croak "$!: $x";
1334                my ($url, $path) = repo_path_split($u);
1335                s_to_file($url, "$GIT_DIR/svn/$x/info/repo_url");
1336                s_to_file($path, "$GIT_DIR/svn/$x/info/repo_path");
1337        }
1338        print "Done upgrading.\n";
1339}
1340
1341sub init_vars {
1342        $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
1343        $GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN";
1344        $GIT_SVN_INDEX = "$GIT_SVN_DIR/index";
1345        $SVN_URL = undef;
1346        $REV_DIR = "$GIT_SVN_DIR/revs";
1347        $SVN_WC = "$GIT_SVN_DIR/tree";
1348}
1349
1350# convert GetOpt::Long specs for use by git-repo-config
1351sub read_repo_config {
1352        return unless -d $GIT_DIR;
1353        my $opts = shift;
1354        foreach my $o (keys %$opts) {
1355                my $v = $opts->{$o};
1356                my ($key) = ($o =~ /^([a-z\-]+)/);
1357                $key =~ s/-//g;
1358                my $arg = 'git-repo-config';
1359                $arg .= ' --int' if ($o =~ /[:=]i$/);
1360                $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
1361                if (ref $v eq 'ARRAY') {
1362                        chomp(my @tmp = `$arg --get-all svn.$key`);
1363                        @$v = @tmp if @tmp;
1364                } else {
1365                        chomp(my $tmp = `$arg --get svn.$key`);
1366                        if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) {
1367                                $$v = $tmp;
1368                        }
1369                }
1370        }
1371}
1372
1373sub set_default_vals {
1374        if (defined $_repack) {
1375                $_repack = 1000 if ($_repack <= 0);
1376                $_repack_nr = $_repack;
1377                $_repack_flags ||= '';
1378        }
1379}
1380
1381__END__
1382
1383Data structures:
1384
1385$svn_log hashref (as returned by svn_log_raw)
1386{
1387        fh => file handle of the log file,
1388        state => state of the log file parser (sep/msg/rev/msg_start...)
1389}
1390
1391$log_msg hashref as returned by next_log_entry($svn_log)
1392{
1393        msg => 'whitespace-formatted log entry
1394',                                              # trailing newline is preserved
1395        revision => '8',                        # integer
1396        date => '2004-02-24T17:01:44.108345Z',  # commit date
1397        author => 'committer name'
1398};
1399
1400
1401@mods = array of diff-index line hashes, each element represents one line
1402        of diff-index output
1403
1404diff-index line ($m hash)
1405{
1406        mode_a => first column of diff-index output, no leading ':',
1407        mode_b => second column of diff-index output,
1408        sha1_b => sha1sum of the final blob,
1409        chg => change type [MCRADT],
1410        file_a => original file name of a file (iff chg is 'C' or 'R')
1411        file_b => new/current file name of a file (any chg)
1412}
1413;