72129de6a3a88021a27f4aaf3be3b4b46ce8c630
   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        chdir $SVN_WC or croak "Unable to chdir $SVN_WC: $!\n";
 313        my $info = svn_info('.');
 314        my $fetched = fetch();
 315        if ($info->{Revision} != $fetched->{revision}) {
 316                print STDERR "There are new revisions that were fetched ",
 317                                "and need to be merged (or acknowledged) ",
 318                                "before committing.\n";
 319                exit 1;
 320        }
 321        $info = svn_info('.');
 322        read_uuid($info);
 323        my $svn_current_rev =  $info->{'Last Changed Rev'};
 324        foreach my $c (@revs) {
 325                my $mods = svn_checkout_tree($svn_current_rev, $c);
 326                if (scalar @$mods == 0) {
 327                        print "Skipping, no changes detected\n";
 328                        next;
 329                }
 330                $svn_current_rev = svn_commit_tree($svn_current_rev, $c);
 331        }
 332        print "Done committing ",scalar @revs," revisions to SVN\n";
 333}
 334
 335sub show_ignore {
 336        require File::Find or die $!;
 337        my $exclude_file = "$GIT_DIR/info/exclude";
 338        open my $fh, '<', $exclude_file or croak $!;
 339        chomp(my @excludes = (<$fh>));
 340        close $fh or croak $!;
 341
 342        $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
 343        chdir $SVN_WC or croak $!;
 344        my %ign;
 345        File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){
 346                s#^\./##;
 347                @{$ign{$_}} = svn_propget_base('svn:ignore', $_);
 348                }}, no_chdir=>1},'.');
 349
 350        print "\n# /\n";
 351        foreach (@{$ign{'.'}}) { print '/',$_ if /\S/ }
 352        delete $ign{'.'};
 353        foreach my $i (sort keys %ign) {
 354                print "\n# ",$i,"\n";
 355                foreach (@{$ign{$i}}) { print '/',$i,'/',$_ if /\S/ }
 356        }
 357}
 358
 359########################### utility functions #########################
 360
 361sub read_uuid {
 362        return if $SVN_UUID;
 363        my $info = shift || svn_info('.');
 364        $SVN_UUID = $info->{'Repository UUID'} or
 365                                        croak "Repository UUID unreadable\n";
 366        s_to_file($SVN_UUID,"$GIT_DIR/$GIT_SVN/info/uuid");
 367}
 368
 369sub setup_git_svn {
 370        defined $SVN_URL or croak "SVN repository location required\n";
 371        unless (-d $GIT_DIR) {
 372                croak "GIT_DIR=$GIT_DIR does not exist!\n";
 373        }
 374        mkpath(["$GIT_DIR/$GIT_SVN"]);
 375        mkpath(["$GIT_DIR/$GIT_SVN/info"]);
 376        mkpath([$REV_DIR]);
 377        s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url");
 378
 379        open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!;
 380        print $fd '.svn',"\n";
 381        close $fd or croak $!;
 382}
 383
 384sub assert_svn_wc_clean {
 385        my ($svn_rev) = @_;
 386        croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
 387        my $lcr = svn_info('.')->{'Last Changed Rev'};
 388        if ($svn_rev != $lcr) {
 389                print STDERR "Checking for copy-tree ... ";
 390                my @diff = grep(/^Index: /,(safe_qx(qw(svn diff),
 391                                                "-r$lcr:$svn_rev")));
 392                if (@diff) {
 393                        croak "Nope!  Expected r$svn_rev, got r$lcr\n";
 394                } else {
 395                        print STDERR "OK!\n";
 396                }
 397        }
 398        my @status = grep(!/^Performing status on external/,(`svn status`));
 399        @status = grep(!/^\s*$/,@status);
 400        if (scalar @status) {
 401                print STDERR "Tree ($SVN_WC) is not clean:\n";
 402                print STDERR $_ foreach @status;
 403                croak;
 404        }
 405}
 406
 407sub assert_tree {
 408        my ($treeish) = @_;
 409        croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o;
 410        chomp(my $type = `git-cat-file -t $treeish`);
 411        my $expected;
 412        while ($type eq 'tag') {
 413                chomp(($treeish, $type) = `git-cat-file tag $treeish`);
 414        }
 415        if ($type eq 'commit') {
 416                $expected = (grep /^tree /,`git-cat-file commit $treeish`)[0];
 417                ($expected) = ($expected =~ /^tree ($sha1)$/);
 418                die "Unable to get tree from $treeish\n" unless $expected;
 419        } elsif ($type eq 'tree') {
 420                $expected = $treeish;
 421        } else {
 422                die "$treeish is a $type, expected tree, tag or commit\n";
 423        }
 424
 425        my $old_index = $ENV{GIT_INDEX_FILE};
 426        my $tmpindex = $GIT_SVN_INDEX.'.assert-tmp';
 427        if (-e $tmpindex) {
 428                unlink $tmpindex or croak $!;
 429        }
 430        $ENV{GIT_INDEX_FILE} = $tmpindex;
 431        index_changes(1);
 432        chomp(my $tree = `git-write-tree`);
 433        if ($old_index) {
 434                $ENV{GIT_INDEX_FILE} = $old_index;
 435        } else {
 436                delete $ENV{GIT_INDEX_FILE};
 437        }
 438        if ($tree ne $expected) {
 439                croak "Tree mismatch, Got: $tree, Expected: $expected\n";
 440        }
 441        unlink $tmpindex;
 442}
 443
 444sub parse_diff_tree {
 445        my $diff_fh = shift;
 446        local $/ = "\0";
 447        my $state = 'meta';
 448        my @mods;
 449        while (<$diff_fh>) {
 450                chomp $_; # this gets rid of the trailing "\0"
 451                if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
 452                                        $sha1\s($sha1)\s([MTCRAD])\d*$/xo) {
 453                        push @mods, {   mode_a => $1, mode_b => $2,
 454                                        sha1_b => $3, chg => $4 };
 455                        if ($4 =~ /^(?:C|R)$/) {
 456                                $state = 'file_a';
 457                        } else {
 458                                $state = 'file_b';
 459                        }
 460                } elsif ($state eq 'file_a') {
 461                        my $x = $mods[$#mods] or croak "Empty array\n";
 462                        if ($x->{chg} !~ /^(?:C|R)$/) {
 463                                croak "Error parsing $_, $x->{chg}\n";
 464                        }
 465                        $x->{file_a} = $_;
 466                        $state = 'file_b';
 467                } elsif ($state eq 'file_b') {
 468                        my $x = $mods[$#mods] or croak "Empty array\n";
 469                        if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
 470                                croak "Error parsing $_, $x->{chg}\n";
 471                        }
 472                        if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
 473                                croak "Error parsing $_, $x->{chg}\n";
 474                        }
 475                        $x->{file_b} = $_;
 476                        $state = 'meta';
 477                } else {
 478                        croak "Error parsing $_\n";
 479                }
 480        }
 481        close $diff_fh or croak $!;
 482
 483        return \@mods;
 484}
 485
 486sub svn_check_prop_executable {
 487        my $m = shift;
 488        return if -l $m->{file_b};
 489        if ($m->{mode_b} =~ /755$/) {
 490                chmod((0755 &~ umask),$m->{file_b}) or croak $!;
 491                if ($m->{mode_a} !~ /755$/) {
 492                        sys(qw(svn propset svn:executable 1), $m->{file_b});
 493                }
 494                -x $m->{file_b} or croak "$m->{file_b} is not executable!\n";
 495        } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
 496                sys(qw(svn propdel svn:executable), $m->{file_b});
 497                chmod((0644 &~ umask),$m->{file_b}) or croak $!;
 498                -x $m->{file_b} and croak "$m->{file_b} is executable!\n";
 499        }
 500}
 501
 502sub svn_ensure_parent_path {
 503        my $dir_b = dirname(shift);
 504        svn_ensure_parent_path($dir_b) if ($dir_b ne File::Spec->curdir);
 505        mkpath([$dir_b]) unless (-d $dir_b);
 506        sys(qw(svn add -N), $dir_b) unless (-d "$dir_b/.svn");
 507}
 508
 509sub precommit_check {
 510        my $mods = shift;
 511        my (%rm_file, %rmdir_check, %added_check);
 512
 513        my %o = ( D => 0, R => 1, C => 2, A => 3, M => 3, T => 3 );
 514        foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
 515                if ($m->{chg} eq 'R') {
 516                        if (-d $m->{file_b}) {
 517                                err_dir_to_file("$m->{file_a} => $m->{file_b}");
 518                        }
 519                        # dir/$file => dir/file/$file
 520                        my $dirname = dirname($m->{file_b});
 521                        while ($dirname ne File::Spec->curdir) {
 522                                if ($dirname ne $m->{file_a}) {
 523                                        $dirname = dirname($dirname);
 524                                        next;
 525                                }
 526                                err_file_to_dir("$m->{file_a} => $m->{file_b}");
 527                        }
 528                        # baz/zzz => baz (baz is a file)
 529                        $dirname = dirname($m->{file_a});
 530                        while ($dirname ne File::Spec->curdir) {
 531                                if ($dirname ne $m->{file_b}) {
 532                                        $dirname = dirname($dirname);
 533                                        next;
 534                                }
 535                                err_dir_to_file("$m->{file_a} => $m->{file_b}");
 536                        }
 537                }
 538                if ($m->{chg} =~ /^(D|R)$/) {
 539                        my $t = $1 eq 'D' ? 'file_b' : 'file_a';
 540                        $rm_file{ $m->{$t} } = 1;
 541                        my $dirname = dirname( $m->{$t} );
 542                        my $basename = basename( $m->{$t} );
 543                        $rmdir_check{$dirname}->{$basename} = 1;
 544                } elsif ($m->{chg} =~ /^(?:A|C)$/) {
 545                        if (-d $m->{file_b}) {
 546                                err_dir_to_file($m->{file_b});
 547                        }
 548                        my $dirname = dirname( $m->{file_b} );
 549                        my $basename = basename( $m->{file_b} );
 550                        $added_check{$dirname}->{$basename} = 1;
 551                        while ($dirname ne File::Spec->curdir) {
 552                                if ($rm_file{$dirname}) {
 553                                        err_file_to_dir($m->{file_b});
 554                                }
 555                                $dirname = dirname $dirname;
 556                        }
 557                }
 558        }
 559        return (\%rmdir_check, \%added_check);
 560
 561        sub err_dir_to_file {
 562                my $file = shift;
 563                print STDERR "Node change from directory to file ",
 564                                "is not supported by Subversion: ",$file,"\n";
 565                exit 1;
 566        }
 567        sub err_file_to_dir {
 568                my $file = shift;
 569                print STDERR "Node change from file to directory ",
 570                                "is not supported by Subversion: ",$file,"\n";
 571                exit 1;
 572        }
 573}
 574
 575sub svn_checkout_tree {
 576        my ($svn_rev, $treeish) = @_;
 577        my $from = file_to_s("$REV_DIR/$svn_rev");
 578        assert_tree($from);
 579        print "diff-tree $from $treeish\n";
 580        my $pid = open my $diff_fh, '-|';
 581        defined $pid or croak $!;
 582        if ($pid == 0) {
 583                my @diff_tree = qw(git-diff-tree -z -r -C);
 584                push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
 585                push @diff_tree, "-l$_l" if defined $_l;
 586                exec(@diff_tree, $from, $treeish) or croak $!;
 587        }
 588        my $mods = parse_diff_tree($diff_fh);
 589        unless (@$mods) {
 590                # git can do empty commits, but SVN doesn't allow it...
 591                return $mods;
 592        }
 593        my ($rm, $add) = precommit_check($mods);
 594
 595        my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
 596        foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
 597                if ($m->{chg} eq 'C') {
 598                        svn_ensure_parent_path( $m->{file_b} );
 599                        sys(qw(svn cp),         $m->{file_a}, $m->{file_b});
 600                        apply_mod_line_blob($m);
 601                        svn_check_prop_executable($m);
 602                } elsif ($m->{chg} eq 'D') {
 603                        sys(qw(svn rm --force), $m->{file_b});
 604                } elsif ($m->{chg} eq 'R') {
 605                        svn_ensure_parent_path( $m->{file_b} );
 606                        sys(qw(svn mv --force), $m->{file_a}, $m->{file_b});
 607                        apply_mod_line_blob($m);
 608                        svn_check_prop_executable($m);
 609                } elsif ($m->{chg} eq 'M') {
 610                        apply_mod_line_blob($m);
 611                        svn_check_prop_executable($m);
 612                } elsif ($m->{chg} eq 'T') {
 613                        sys(qw(svn rm --force),$m->{file_b});
 614                        apply_mod_line_blob($m);
 615                        sys(qw(svn add --force), $m->{file_b});
 616                        svn_check_prop_executable($m);
 617                } elsif ($m->{chg} eq 'A') {
 618                        svn_ensure_parent_path( $m->{file_b} );
 619                        apply_mod_line_blob($m);
 620                        sys(qw(svn add --force), $m->{file_b});
 621                        svn_check_prop_executable($m);
 622                } else {
 623                        croak "Invalid chg: $m->{chg}\n";
 624                }
 625        }
 626
 627        assert_tree($treeish);
 628        if ($_rmdir) { # remove empty directories
 629                handle_rmdir($rm, $add);
 630        }
 631        assert_tree($treeish);
 632        return $mods;
 633}
 634
 635# svn ls doesn't work with respect to the current working tree, but what's
 636# in the repository.  There's not even an option for it... *sigh*
 637# (added files don't show up and removed files remain in the ls listing)
 638sub svn_ls_current {
 639        my ($dir, $rm, $add) = @_;
 640        chomp(my @ls = safe_qx('svn','ls',$dir));
 641        my @ret = ();
 642        foreach (@ls) {
 643                s#/$##; # trailing slashes are evil
 644                push @ret, $_ unless $rm->{$dir}->{$_};
 645        }
 646        if (exists $add->{$dir}) {
 647                push @ret, keys %{$add->{$dir}};
 648        }
 649        return \@ret;
 650}
 651
 652sub handle_rmdir {
 653        my ($rm, $add) = @_;
 654
 655        foreach my $dir (sort {length $b <=> length $a} keys %$rm) {
 656                my $ls = svn_ls_current($dir, $rm, $add);
 657                next if (scalar @$ls);
 658                sys(qw(svn rm --force),$dir);
 659
 660                my $dn = dirname $dir;
 661                $rm->{ $dn }->{ basename $dir } = 1;
 662                $ls = svn_ls_current($dn, $rm, $add);
 663                while (scalar @$ls == 0 && $dn ne File::Spec->curdir) {
 664                        sys(qw(svn rm --force),$dn);
 665                        $dir = basename $dn;
 666                        $dn = dirname $dn;
 667                        $rm->{ $dn }->{ $dir } = 1;
 668                        $ls = svn_ls_current($dn, $rm, $add);
 669                }
 670        }
 671}
 672
 673sub svn_commit_tree {
 674        my ($svn_rev, $commit) = @_;
 675        my $commit_msg = "$GIT_DIR/$GIT_SVN/.svn-commit.tmp.$$";
 676        my %log_msg = ( msg => '' );
 677        open my $msg, '>', $commit_msg or croak $!;
 678
 679        chomp(my $type = `git-cat-file -t $commit`);
 680        if ($type eq 'commit') {
 681                my $pid = open my $msg_fh, '-|';
 682                defined $pid or croak $!;
 683
 684                if ($pid == 0) {
 685                        exec(qw(git-cat-file commit), $commit) or croak $!;
 686                }
 687                my $in_msg = 0;
 688                while (<$msg_fh>) {
 689                        if (!$in_msg) {
 690                                $in_msg = 1 if (/^\s*$/);
 691                        } elsif (/^git-svn-id: /) {
 692                                # skip this, we regenerate the correct one
 693                                # on re-fetch anyways
 694                        } else {
 695                                print $msg $_ or croak $!;
 696                        }
 697                }
 698                close $msg_fh or croak $!;
 699        }
 700        close $msg or croak $!;
 701
 702        if ($_edit || ($type eq 'tree')) {
 703                my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
 704                system($editor, $commit_msg);
 705        }
 706
 707        # file_to_s removes all trailing newlines, so just use chomp() here:
 708        open $msg, '<', $commit_msg or croak $!;
 709        { local $/; chomp($log_msg{msg} = <$msg>); }
 710        close $msg or croak $!;
 711
 712        my ($oneline) = ($log_msg{msg} =~ /([^\n\r]+)/);
 713        print "Committing $commit: $oneline\n";
 714
 715        if (defined $LC_ALL) {
 716                $ENV{LC_ALL} = $LC_ALL;
 717        } else {
 718                delete $ENV{LC_ALL};
 719        }
 720        my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
 721        $ENV{LC_ALL} = 'C';
 722        unlink $commit_msg;
 723        my ($committed) = ($ci_output[$#ci_output] =~ /(\d+)/);
 724        if (!defined $committed) {
 725                my $out = join("\n",@ci_output);
 726                print STDERR "W: Trouble parsing \`svn commit' output:\n\n",
 727                                $out, "\n\nAssuming English locale...";
 728                ($committed) = ($out =~ /^Committed revision \d+\./sm);
 729                defined $committed or die " FAILED!\n",
 730                        "Commit output failed to parse committed revision!\n",
 731                print STDERR " OK\n";
 732        }
 733
 734        my @svn_up = qw(svn up);
 735        push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
 736        if ($committed == ($svn_rev + 1)) {
 737                push @svn_up, "-r$committed";
 738                sys(@svn_up);
 739                my $info = svn_info('.');
 740                my $date = $info->{'Last Changed Date'} or die "Missing date\n";
 741                if ($info->{'Last Changed Rev'} != $committed) {
 742                        croak "$info->{'Last Changed Rev'} != $committed\n"
 743                }
 744                my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
 745                                        /(\d{4})\-(\d\d)\-(\d\d)\s
 746                                         (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
 747                                         or croak "Failed to parse date: $date\n";
 748                $log_msg{date} = "$tz $Y-$m-$d $H:$M:$S";
 749                $log_msg{author} = $info->{'Last Changed Author'};
 750                $log_msg{revision} = $committed;
 751                $log_msg{msg} .= "\n";
 752                my $parent = file_to_s("$REV_DIR/$svn_rev");
 753                git_commit(\%log_msg, $parent, $commit);
 754                return $committed;
 755        }
 756        # resync immediately
 757        push @svn_up, "-r$svn_rev";
 758        sys(@svn_up);
 759        return fetch("$committed=$commit")->{revision};
 760}
 761
 762# read the entire log into a temporary file (which is removed ASAP)
 763# and store the file handle + parser state
 764sub svn_log_raw {
 765        my (@log_args) = @_;
 766        my $log_fh = IO::File->new_tmpfile or croak $!;
 767        my $pid = fork;
 768        defined $pid or croak $!;
 769        if (!$pid) {
 770                open STDOUT, '>&', $log_fh or croak $!;
 771                exec (qw(svn log), @log_args) or croak $!
 772        }
 773        waitpid $pid, 0;
 774        croak if $?;
 775        seek $log_fh, 0, 0 or croak $!;
 776        return { state => 'sep', fh => $log_fh };
 777}
 778
 779sub next_log_entry {
 780        my $log = shift; # retval of svn_log_raw()
 781        my $ret = undef;
 782        my $fh = $log->{fh};
 783
 784        while (<$fh>) {
 785                chomp;
 786                if (/^\-{72}$/) {
 787                        if ($log->{state} eq 'msg') {
 788                                if ($ret->{lines}) {
 789                                        $ret->{msg} .= $_."\n";
 790                                        unless(--$ret->{lines}) {
 791                                                $log->{state} = 'sep';
 792                                        }
 793                                } else {
 794                                        croak "Log parse error at: $_\n",
 795                                                $ret->{revision},
 796                                                "\n";
 797                                }
 798                                next;
 799                        }
 800                        if ($log->{state} ne 'sep') {
 801                                croak "Log parse error at: $_\n",
 802                                        "state: $log->{state}\n",
 803                                        $ret->{revision},
 804                                        "\n";
 805                        }
 806                        $log->{state} = 'rev';
 807
 808                        # if we have an empty log message, put something there:
 809                        if ($ret) {
 810                                $ret->{msg} ||= "\n";
 811                                delete $ret->{lines};
 812                                return $ret;
 813                        }
 814                        next;
 815                }
 816                if ($log->{state} eq 'rev' && s/^r(\d+)\s*\|\s*//) {
 817                        my $rev = $1;
 818                        my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3);
 819                        ($lines) = ($lines =~ /(\d+)/);
 820                        my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
 821                                        /(\d{4})\-(\d\d)\-(\d\d)\s
 822                                         (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
 823                                         or croak "Failed to parse date: $date\n";
 824                        $ret = {        revision => $rev,
 825                                        date => "$tz $Y-$m-$d $H:$M:$S",
 826                                        author => $author,
 827                                        lines => $lines,
 828                                        msg => '' };
 829                        if (defined $_authors && ! defined $users{$author}) {
 830                                die "Author: $author not defined in ",
 831                                                "$_authors file\n";
 832                        }
 833                        $log->{state} = 'msg_start';
 834                        next;
 835                }
 836                # skip the first blank line of the message:
 837                if ($log->{state} eq 'msg_start' && /^$/) {
 838                        $log->{state} = 'msg';
 839                } elsif ($log->{state} eq 'msg') {
 840                        if ($ret->{lines}) {
 841                                $ret->{msg} .= $_."\n";
 842                                unless (--$ret->{lines}) {
 843                                        $log->{state} = 'sep';
 844                                }
 845                        } else {
 846                                croak "Log parse error at: $_\n",
 847                                        $ret->{revision},"\n";
 848                        }
 849                }
 850        }
 851        return $ret;
 852}
 853
 854sub svn_info {
 855        my $url = shift || $SVN_URL;
 856
 857        my $pid = open my $info_fh, '-|';
 858        defined $pid or croak $!;
 859
 860        if ($pid == 0) {
 861                exec(qw(svn info),$url) or croak $!;
 862        }
 863
 864        my $ret = {};
 865        # only single-lines seem to exist in svn info output
 866        while (<$info_fh>) {
 867                chomp $_;
 868                if (m#^([^:]+)\s*:\s*(\S.*)$#) {
 869                        $ret->{$1} = $2;
 870                        push @{$ret->{-order}}, $1;
 871                }
 872        }
 873        close $info_fh or croak $!;
 874        return $ret;
 875}
 876
 877sub sys { system(@_) == 0 or croak $? }
 878
 879sub eol_cp {
 880        my ($from, $to) = @_;
 881        my $es = svn_propget_base('svn:eol-style', $to);
 882        open my $rfd, '<', $from or croak $!;
 883        binmode $rfd or croak $!;
 884        open my $wfd, '>', $to or croak $!;
 885        binmode $wfd or croak $!;
 886
 887        my $eol = $EOL{$es} or undef;
 888        my $buf;
 889        use bytes;
 890        while (1) {
 891                my ($r, $w, $t);
 892                defined($r = sysread($rfd, $buf, 4096)) or croak $!;
 893                return unless $r;
 894                if ($eol) {
 895                        if ($buf =~ /\015$/) {
 896                                my $c;
 897                                defined($r = sysread($rfd,$c,1)) or croak $!;
 898                                $buf .= $c if $r > 0;
 899                        }
 900                        $buf =~ s/(?:\015\012|\015|\012)/$eol/gs;
 901                        $r = length($buf);
 902                }
 903                for ($w = 0; $w < $r; $w += $t) {
 904                        $t = syswrite($wfd, $buf, $r - $w, $w) or croak $!;
 905                }
 906        }
 907        no bytes;
 908}
 909
 910sub do_update_index {
 911        my ($z_cmd, $cmd, $no_text_base) = @_;
 912
 913        my $z = open my $p, '-|';
 914        defined $z or croak $!;
 915        unless ($z) { exec @$z_cmd or croak $! }
 916
 917        my $pid = open my $ui, '|-';
 918        defined $pid or croak $!;
 919        unless ($pid) {
 920                exec('git-update-index',"--$cmd",'-z','--stdin') or croak $!;
 921        }
 922        local $/ = "\0";
 923        while (my $x = <$p>) {
 924                chomp $x;
 925                if (!$no_text_base && lstat $x && ! -l _ &&
 926                                svn_propget_base('svn:keywords', $x)) {
 927                        my $mode = -x _ ? 0755 : 0644;
 928                        my ($v,$d,$f) = File::Spec->splitpath($x);
 929                        my $tb = File::Spec->catfile($d, '.svn', 'tmp',
 930                                                'text-base',"$f.svn-base");
 931                        $tb =~ s#^/##;
 932                        unless (-f $tb) {
 933                                $tb = File::Spec->catfile($d, '.svn',
 934                                                'text-base',"$f.svn-base");
 935                                $tb =~ s#^/##;
 936                        }
 937                        unlink $x or croak $!;
 938                        eol_cp($tb, $x);
 939                        chmod(($mode &~ umask), $x) or croak $!;
 940                }
 941                print $ui $x,"\0";
 942        }
 943        close $ui or croak $!;
 944}
 945
 946sub index_changes {
 947        my $no_text_base = shift;
 948        do_update_index([qw/git-diff-files --name-only -z/],
 949                        'remove',
 950                        $no_text_base);
 951        do_update_index([qw/git-ls-files -z --others/,
 952                              "--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude"],
 953                        'add',
 954                        $no_text_base);
 955}
 956
 957sub s_to_file {
 958        my ($str, $file, $mode) = @_;
 959        open my $fd,'>',$file or croak $!;
 960        print $fd $str,"\n" or croak $!;
 961        close $fd or croak $!;
 962        chmod ($mode &~ umask, $file) if (defined $mode);
 963}
 964
 965sub file_to_s {
 966        my $file = shift;
 967        open my $fd,'<',$file or croak "$!: file: $file\n";
 968        local $/;
 969        my $ret = <$fd>;
 970        close $fd or croak $!;
 971        $ret =~ s/\s*$//s;
 972        return $ret;
 973}
 974
 975sub assert_revision_unknown {
 976        my $revno = shift;
 977        if (-f "$REV_DIR/$revno") {
 978                croak "$REV_DIR/$revno already exists! ",
 979                                "Why are we refetching it?";
 980        }
 981}
 982
 983sub trees_eq {
 984        my ($x, $y) = @_;
 985        my @x = safe_qx('git-cat-file','commit',$x);
 986        my @y = safe_qx('git-cat-file','commit',$y);
 987        if (($y[0] ne $x[0]) || $x[0] !~ /^tree $sha1\n$/
 988                                || $y[0] !~ /^tree $sha1\n$/) {
 989                print STDERR "Trees not equal: $y[0] != $x[0]\n";
 990                return 0
 991        }
 992        return 1;
 993}
 994
 995sub assert_revision_eq_or_unknown {
 996        my ($revno, $commit) = @_;
 997        if (-f "$REV_DIR/$revno") {
 998                my $current = file_to_s("$REV_DIR/$revno");
 999                if (($commit ne $current) && !trees_eq($commit, $current)) {
1000                        croak "$REV_DIR/$revno already exists!\n",
1001                                "current: $current\nexpected: $commit\n";
1002                }
1003                return;
1004        }
1005}
1006
1007sub git_commit {
1008        my ($log_msg, @parents) = @_;
1009        assert_revision_unknown($log_msg->{revision});
1010        my $out_fh = IO::File->new_tmpfile or croak $!;
1011
1012        map_tree_joins() if (@_branch_from && !%tree_map);
1013
1014        # commit parents can be conditionally bound to a particular
1015        # svn revision via: "svn_revno=commit_sha1", filter them out here:
1016        my @exec_parents;
1017        foreach my $p (@parents) {
1018                next unless defined $p;
1019                if ($p =~ /^(\d+)=($sha1_short)$/o) {
1020                        if ($1 == $log_msg->{revision}) {
1021                                push @exec_parents, $2;
1022                        }
1023                } else {
1024                        push @exec_parents, $p if $p =~ /$sha1_short/o;
1025                }
1026        }
1027
1028        my $pid = fork;
1029        defined $pid or croak $!;
1030        if ($pid == 0) {
1031                $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
1032                index_changes();
1033                chomp(my $tree = `git-write-tree`);
1034                croak if $?;
1035                if (exists $tree_map{$tree}) {
1036                        my %seen_parent = map { $_ => 1 } @exec_parents;
1037                        foreach (@{$tree_map{$tree}}) {
1038                                # MAXPARENT is defined to 16 in commit-tree.c:
1039                                if ($seen_parent{$_} || @exec_parents > 16) {
1040                                        next;
1041                                }
1042                                push @exec_parents, $_;
1043                                $seen_parent{$_} = 1;
1044                        }
1045                }
1046                my $msg_fh = IO::File->new_tmpfile or croak $!;
1047                print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ",
1048                                        "$SVN_URL\@$log_msg->{revision}",
1049                                        " $SVN_UUID\n" or croak $!;
1050                $msg_fh->flush == 0 or croak $!;
1051                seek $msg_fh, 0, 0 or croak $!;
1052
1053                set_commit_env($log_msg);
1054
1055                my @exec = ('git-commit-tree',$tree);
1056                push @exec, '-p', $_  foreach @exec_parents;
1057                open STDIN, '<&', $msg_fh or croak $!;
1058                open STDOUT, '>&', $out_fh or croak $!;
1059                exec @exec or croak $!;
1060        }
1061        waitpid($pid,0);
1062        croak if $?;
1063
1064        $out_fh->flush == 0 or croak $!;
1065        seek $out_fh, 0, 0 or croak $!;
1066        chomp(my $commit = do { local $/; <$out_fh> });
1067        if ($commit !~ /^$sha1$/o) {
1068                croak "Failed to commit, invalid sha1: $commit\n";
1069        }
1070        my @update_ref = ('git-update-ref',"refs/remotes/$GIT_SVN",$commit);
1071        if (my $primary_parent = shift @exec_parents) {
1072                $pid = fork;
1073                defined $pid or croak $!;
1074                if (!$pid) {
1075                        close STDERR;
1076                        close STDOUT;
1077                        exec 'git-rev-parse','--verify',
1078                                                "refs/remotes/$GIT_SVN^0";
1079                }
1080                waitpid $pid, 0;
1081                push @update_ref, $primary_parent unless $?;
1082        }
1083        sys(@update_ref);
1084        sys('git-update-ref',"$GIT_SVN/revs/$log_msg->{revision}",$commit);
1085        print "r$log_msg->{revision} = $commit\n";
1086        return $commit;
1087}
1088
1089sub set_commit_env {
1090        my ($log_msg) = @_;
1091        my $author = $log_msg->{author};
1092        my ($name,$email) = defined $users{$author} ?  @{$users{$author}}
1093                                : ($author,"$author\@$SVN_UUID");
1094        $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name;
1095        $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email;
1096        $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date};
1097}
1098
1099sub apply_mod_line_blob {
1100        my $m = shift;
1101        if ($m->{mode_b} =~ /^120/) {
1102                blob_to_symlink($m->{sha1_b}, $m->{file_b});
1103        } else {
1104                blob_to_file($m->{sha1_b}, $m->{file_b});
1105        }
1106}
1107
1108sub blob_to_symlink {
1109        my ($blob, $link) = @_;
1110        defined $link or croak "\$link not defined!\n";
1111        croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
1112        if (-l $link || -f _) {
1113                unlink $link or croak $!;
1114        }
1115
1116        my $dest = `git-cat-file blob $blob`; # no newline, so no chomp
1117        symlink $dest, $link or croak $!;
1118}
1119
1120sub blob_to_file {
1121        my ($blob, $file) = @_;
1122        defined $file or croak "\$file not defined!\n";
1123        croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
1124        if (-l $file || -f _) {
1125                unlink $file or croak $!;
1126        }
1127
1128        open my $blob_fh, '>', $file or croak "$!: $file\n";
1129        my $pid = fork;
1130        defined $pid or croak $!;
1131
1132        if ($pid == 0) {
1133                open STDOUT, '>&', $blob_fh or croak $!;
1134                exec('git-cat-file','blob',$blob);
1135        }
1136        waitpid $pid, 0;
1137        croak $? if $?;
1138
1139        close $blob_fh or croak $!;
1140}
1141
1142sub safe_qx {
1143        my $pid = open my $child, '-|';
1144        defined $pid or croak $!;
1145        if ($pid == 0) {
1146                exec(@_) or croak $?;
1147        }
1148        my @ret = (<$child>);
1149        close $child or croak $?;
1150        die $? if $?; # just in case close didn't error out
1151        return wantarray ? @ret : join('',@ret);
1152}
1153
1154sub svn_compat_check {
1155        my @co_help = safe_qx(qw(svn co -h));
1156        unless (grep /ignore-externals/,@co_help) {
1157                print STDERR "W: Installed svn version does not support ",
1158                                "--ignore-externals\n";
1159                $_no_ignore_ext = 1;
1160        }
1161        if (grep /usage: checkout URL\[\@REV\]/,@co_help) {
1162                $_svn_co_url_revs = 1;
1163        }
1164        if (grep /\[TARGET\[\@REV\]\.\.\.\]/, `svn propget -h`) {
1165                $_svn_pg_peg_revs = 1;
1166        }
1167
1168        # I really, really hope nobody hits this...
1169        unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) {
1170                print STDERR <<'';
1171W: The installed svn version does not support the --stop-on-copy flag in
1172   the log command.
1173   Lets hope the directory you're tracking is not a branch or tag
1174   and was never moved within the repository...
1175
1176                $_no_stop_copy = 1;
1177        }
1178}
1179
1180# *sigh*, new versions of svn won't honor -r<rev> without URL@<rev>,
1181# (and they won't honor URL@<rev> without -r<rev>, too!)
1182sub svn_cmd_checkout {
1183        my ($url, $rev, $dir) = @_;
1184        my @cmd = ('svn','co', "-r$rev");
1185        push @cmd, '--ignore-externals' unless $_no_ignore_ext;
1186        $url .= "\@$rev" if $_svn_co_url_revs;
1187        sys(@cmd, $url, $dir);
1188}
1189
1190sub check_upgrade_needed {
1191        my $old = eval {
1192                my $pid = open my $child, '-|';
1193                defined $pid or croak $!;
1194                if ($pid == 0) {
1195                        close STDERR;
1196                        exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $?;
1197                }
1198                my @ret = (<$child>);
1199                close $child or croak $?;
1200                die $? if $?; # just in case close didn't error out
1201                return wantarray ? @ret : join('',@ret);
1202        };
1203        return unless $old;
1204        my $head = eval { safe_qx('git-rev-parse',"refs/remotes/$GIT_SVN") };
1205        if ($@ || !$head) {
1206                print STDERR "Please run: $0 rebuild --upgrade\n";
1207                exit 1;
1208        }
1209}
1210
1211# fills %tree_map with a reverse mapping of trees to commits.  Useful
1212# for finding parents to commit on.
1213sub map_tree_joins {
1214        foreach my $br (@_branch_from) {
1215                my $pid = open my $pipe, '-|';
1216                defined $pid or croak $!;
1217                if ($pid == 0) {
1218                        exec(qw(git-rev-list --pretty=raw), $br) or croak $?;
1219                }
1220                while (<$pipe>) {
1221                        if (/^commit ($sha1)$/o) {
1222                                my $commit = $1;
1223                                my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o);
1224                                unless (defined $tree) {
1225                                        die "Failed to parse commit $commit\n";
1226                                }
1227                                push @{$tree_map{$tree}}, $commit;
1228                        }
1229                }
1230                close $pipe or croak $?;
1231        }
1232}
1233
1234# '<svn username> = real-name <email address>' mapping based on git-svnimport:
1235sub load_authors {
1236        open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
1237        while (<$authors>) {
1238                chomp;
1239                next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
1240                my ($user, $name, $email) = ($1, $2, $3);
1241                $users{$user} = [$name, $email];
1242        }
1243        close $authors or croak $!;
1244}
1245
1246sub svn_propget_base {
1247        my ($p, $f) = @_;
1248        $f .= '@BASE' if $_svn_pg_peg_revs;
1249        return safe_qx(qw/svn propget/, $p, $f);
1250}
1251
1252__END__
1253
1254Data structures:
1255
1256$svn_log hashref (as returned by svn_log_raw)
1257{
1258        fh => file handle of the log file,
1259        state => state of the log file parser (sep/msg/rev/msg_start...)
1260}
1261
1262$log_msg hashref as returned by next_log_entry($svn_log)
1263{
1264        msg => 'whitespace-formatted log entry
1265',                                              # trailing newline is preserved
1266        revision => '8',                        # integer
1267        date => '2004-02-24T17:01:44.108345Z',  # commit date
1268        author => 'committer name'
1269};
1270
1271
1272@mods = array of diff-index line hashes, each element represents one line
1273        of diff-index output
1274
1275diff-index line ($m hash)
1276{
1277        mode_a => first column of diff-index output, no leading ':',
1278        mode_b => second column of diff-index output,
1279        sha1_b => sha1sum of the final blob,
1280        chg => change type [MCRADT],
1281        file_a => original file name of a file (iff chg is 'C' or 'R')
1282        file_b => new/current file name of a file (any chg)
1283}
1284;