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