0fc386a7158c14e93f9f3c4fa19e01603aa9b05a
   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 $GIT_SVN_DIR $REVDB/;
  10$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
  11$VERSION = '@@GIT_VERSION@@';
  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};
  18my $TZ = $ENV{TZ};
  19# make sure the svn binary gives consistent output between locales and TZs:
  20$ENV{TZ} = 'UTC';
  21$ENV{LC_ALL} = 'C';
  22$| = 1; # unbuffer STDOUT
  23
  24# properties that we do not log:
  25my %SKIP = ( 'svn:wc:ra_dav:version-url' => 1,
  26             'svn:special' => 1,
  27             'svn:executable' => 1,
  28             'svn:entry:committed-rev' => 1,
  29             'svn:entry:last-author' => 1,
  30             'svn:entry:uuid' => 1,
  31             'svn:entry:committed-date' => 1,
  32);
  33
  34sub fatal (@) { print STDERR @_; exit 1 }
  35require SVN::Core; # use()-ing this causes segfaults for me... *shrug*
  36require SVN::Ra;
  37require SVN::Delta;
  38if ($SVN::Core::VERSION lt '1.1.0') {
  39        fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)\n";
  40}
  41push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor';
  42push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor';
  43*SVN::Git::Fetcher::process_rm = *process_rm;
  44use Carp qw/croak/;
  45use IO::File qw//;
  46use File::Basename qw/dirname basename/;
  47use File::Path qw/mkpath/;
  48use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/;
  49use POSIX qw/strftime/;
  50use IPC::Open3;
  51use Memoize;
  52use Git qw/command command_oneline command_noisy
  53           command_output_pipe command_input_pipe command_close_pipe/;
  54memoize('revisions_eq');
  55memoize('cmt_metadata');
  56memoize('get_commit_time');
  57
  58my ($SVN);
  59
  60my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS};
  61my $sha1 = qr/[a-f\d]{40}/;
  62my $sha1_short = qr/[a-f\d]{4,40}/;
  63my $_esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
  64my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
  65        $_find_copies_harder, $_l, $_cp_similarity, $_cp_remote,
  66        $_repack, $_repack_nr, $_repack_flags, $_q,
  67        $_message, $_file, $_follow_parent, $_no_metadata,
  68        $_template, $_shared, $_no_default_regex, $_no_graft_copy,
  69        $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit,
  70        $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m,
  71        $_merge, $_strategy, $_dry_run, $_ignore_nodate, $_non_recursive,
  72        $_username, $_config_dir, $_no_auth_cache,
  73        $_pager, $_color);
  74my (@_branch_from, %tree_map, %users, %rusers, %equiv);
  75my ($_svn_can_do_switch);
  76my @repo_path_split_cache;
  77
  78my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext,
  79                'branch|b=s' => \@_branch_from,
  80                'follow-parent|follow' => \$_follow_parent,
  81                'branch-all-refs|B' => \$_branch_all_refs,
  82                'authors-file|A=s' => \$_authors,
  83                'repack:i' => \$_repack,
  84                'no-metadata' => \$_no_metadata,
  85                'quiet|q' => \$_q,
  86                'username=s' => \$_username,
  87                'config-dir=s' => \$_config_dir,
  88                'no-auth-cache' => \$_no_auth_cache,
  89                'ignore-nodate' => \$_ignore_nodate,
  90                'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
  91
  92my ($_trunk, $_tags, $_branches);
  93my %multi_opts = ( 'trunk|T=s' => \$_trunk,
  94                'tags|t=s' => \$_tags,
  95                'branches|b=s' => \$_branches );
  96my %init_opts = ( 'template=s' => \$_template, 'shared' => \$_shared );
  97my %cmt_opts = ( 'edit|e' => \$_edit,
  98                'rmdir' => \$_rmdir,
  99                'find-copies-harder' => \$_find_copies_harder,
 100                'l=i' => \$_l,
 101                'copy-similarity|C=i'=> \$_cp_similarity
 102);
 103
 104my %cmd = (
 105        fetch => [ \&fetch, "Download new revisions from SVN",
 106                        { 'revision|r=s' => \$_revision, %fc_opts } ],
 107        init => [ \&init, "Initialize a repo for tracking" .
 108                          " (requires URL argument)",
 109                          \%init_opts ],
 110        dcommit => [ \&dcommit, 'Commit several diffs to merge with upstream',
 111                        { 'merge|m|M' => \$_merge,
 112                          'strategy|s=s' => \$_strategy,
 113                          'dry-run|n' => \$_dry_run,
 114                        %cmt_opts, %fc_opts } ],
 115        'set-tree' => [ \&commit, "Set an SVN repository to a git tree-ish",
 116                        {       'stdin|' => \$_stdin, %cmt_opts, %fc_opts, } ],
 117        'show-ignore' => [ \&show_ignore, "Show svn:ignore listings",
 118                        { 'revision|r=i' => \$_revision } ],
 119        rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)",
 120                        { 'no-ignore-externals' => \$_no_ignore_ext,
 121                          'copy-remote|remote=s' => \$_cp_remote,
 122                          'upgrade' => \$_upgrade } ],
 123        'graft-branches' => [ \&graft_branches,
 124                        'Detect merges/branches from already imported history',
 125                        { 'merge-rx|m' => \@_opt_m,
 126                          'branch|b=s' => \@_branch_from,
 127                          'branch-all-refs|B' => \$_branch_all_refs,
 128                          'no-default-regex' => \$_no_default_regex,
 129                          'no-graft-copy' => \$_no_graft_copy } ],
 130        'multi-init' => [ \&multi_init,
 131                        'Initialize multiple trees (like git-svnimport)',
 132                        { %multi_opts, %init_opts,
 133                         'revision|r=i' => \$_revision,
 134                         'username=s' => \$_username,
 135                         'config-dir=s' => \$_config_dir,
 136                         'no-auth-cache' => \$_no_auth_cache,
 137                        } ],
 138        'multi-fetch' => [ \&multi_fetch,
 139                        'Fetch multiple trees (like git-svnimport)',
 140                        \%fc_opts ],
 141        'log' => [ \&show_log, 'Show commit logs',
 142                        { 'limit=i' => \$_limit,
 143                          'revision|r=s' => \$_revision,
 144                          'verbose|v' => \$_verbose,
 145                          'incremental' => \$_incremental,
 146                          'oneline' => \$_oneline,
 147                          'show-commit' => \$_show_commit,
 148                          'non-recursive' => \$_non_recursive,
 149                          'authors-file|A=s' => \$_authors,
 150                          'color' => \$_color,
 151                          'pager=s' => \$_pager,
 152                        } ],
 153        'commit-diff' => [ \&commit_diff, 'Commit a diff between two trees',
 154                        { 'message|m=s' => \$_message,
 155                          'file|F=s' => \$_file,
 156                          'revision|r=s' => \$_revision,
 157                        %cmt_opts } ],
 158);
 159
 160my $cmd;
 161for (my $i = 0; $i < @ARGV; $i++) {
 162        if (defined $cmd{$ARGV[$i]}) {
 163                $cmd = $ARGV[$i];
 164                splice @ARGV, $i, 1;
 165                last;
 166        }
 167};
 168
 169my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
 170
 171read_repo_config(\%opts);
 172my $rv = GetOptions(%opts, 'help|H|h' => \$_help,
 173                                'version|V' => \$_version,
 174                                'id|i=s' => \$GIT_SVN);
 175exit 1 if (!$rv && $cmd ne 'log');
 176
 177set_default_vals();
 178usage(0) if $_help;
 179version() if $_version;
 180usage(1) unless defined $cmd;
 181init_vars();
 182load_authors() if $_authors;
 183load_all_refs() if $_branch_all_refs;
 184migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init|commit-diff)$/;
 185$cmd{$cmd}->[0]->(@ARGV);
 186exit 0;
 187
 188####################### primary functions ######################
 189sub usage {
 190        my $exit = shift || 0;
 191        my $fd = $exit ? \*STDERR : \*STDOUT;
 192        print $fd <<"";
 193git-svn - bidirectional operations between a single Subversion tree and git
 194Usage: $0 <command> [options] [arguments]\n
 195
 196        print $fd "Available commands:\n" unless $cmd;
 197
 198        foreach (sort keys %cmd) {
 199                next if $cmd && $cmd ne $_;
 200                print $fd '  ',pack('A17',$_),$cmd{$_}->[1],"\n";
 201                foreach (keys %{$cmd{$_}->[2]}) {
 202                        # prints out arguments as they should be passed:
 203                        my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
 204                        print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
 205                                                        "--$_" : "-$_" }
 206                                                split /\|/,$_)," $x\n";
 207                }
 208        }
 209        print $fd <<"";
 210\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an
 211arbitrary identifier if you're tracking multiple SVN branches/repositories in
 212one git repository and want to keep them separate.  See git-svn(1) for more
 213information.
 214
 215        exit $exit;
 216}
 217
 218sub version {
 219        print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n";
 220        exit 0;
 221}
 222
 223sub rebuild {
 224        if (!verify_ref("refs/remotes/$GIT_SVN^0")) {
 225                copy_remote_ref();
 226        }
 227        $SVN_URL = shift or undef;
 228        my $newest_rev = 0;
 229        if ($_upgrade) {
 230                command_noisy('update-ref',"refs/remotes/$GIT_SVN","
 231                              $GIT_SVN-HEAD");
 232        } else {
 233                check_upgrade_needed();
 234        }
 235
 236        my ($rev_list, $ctx) = command_output_pipe("rev-list",
 237                                                   "refs/remotes/$GIT_SVN");
 238        my $latest;
 239        while (<$rev_list>) {
 240                chomp;
 241                my $c = $_;
 242                croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o;
 243                my @commit = grep(/^git-svn-id: /,
 244                                  command(qw/cat-file commit/, $c));
 245                next if (!@commit); # skip merges
 246                my ($url, $rev, $uuid) = extract_metadata($commit[$#commit]);
 247                if (!defined $rev || !$uuid) {
 248                        croak "Unable to extract revision or UUID from ",
 249                                "$c, $commit[$#commit]\n";
 250                }
 251
 252                # if we merged or otherwise started elsewhere, this is
 253                # how we break out of it
 254                next if (defined $SVN_UUID && ($uuid ne $SVN_UUID));
 255                next if (defined $SVN_URL && defined $url && ($url ne $SVN_URL));
 256
 257                unless (defined $latest) {
 258                        if (!$SVN_URL && !$url) {
 259                                croak "SVN repository location required: $url\n";
 260                        }
 261                        $SVN_URL ||= $url;
 262                        $SVN_UUID ||= $uuid;
 263                        setup_git_svn();
 264                        $latest = $rev;
 265                }
 266                revdb_set($REVDB, $rev, $c);
 267                print "r$rev = $c\n";
 268                $newest_rev = $rev if ($rev > $newest_rev);
 269        }
 270        command_close_pipe($rev_list, $ctx);
 271}
 272
 273sub init {
 274        my $url = shift or die "SVN repository location required " .
 275                                "as a command-line argument\n";
 276        $url =~ s!/+$!!; # strip trailing slash
 277
 278        if (my $repo_path = shift) {
 279                unless (-d $repo_path) {
 280                        mkpath([$repo_path]);
 281                }
 282                $GIT_DIR = $ENV{GIT_DIR} = $repo_path . "/.git";
 283                init_vars();
 284        }
 285
 286        $SVN_URL = $url;
 287        unless (-d $GIT_DIR) {
 288                my @init_db = ('init-db');
 289                push @init_db, "--template=$_template" if defined $_template;
 290                push @init_db, "--shared" if defined $_shared;
 291                command_noisy(@init_db);
 292        }
 293        setup_git_svn();
 294}
 295
 296sub fetch {
 297        check_upgrade_needed();
 298        $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
 299        my $ret = fetch_lib(@_);
 300        if ($ret->{commit} && !verify_ref('refs/heads/master^0')) {
 301                command_noisy(qw(update-ref refs/heads/master),$ret->{commit});
 302        }
 303        return $ret;
 304}
 305
 306sub fetch_lib {
 307        my (@parents) = @_;
 308        $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
 309        $SVN ||= libsvn_connect($SVN_URL);
 310        my ($last_rev, $last_commit) = svn_grab_base_rev();
 311        my ($base, $head) = libsvn_parse_revision($last_rev);
 312        if ($base > $head) {
 313                return { revision => $last_rev, commit => $last_commit }
 314        }
 315        my $index = set_index($GIT_SVN_INDEX);
 316
 317        # limit ourselves and also fork() since get_log won't release memory
 318        # after processing a revision and SVN stuff seems to leak
 319        my $inc = 1000;
 320        my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
 321        read_uuid();
 322        if (defined $last_commit) {
 323                unless (-e $GIT_SVN_INDEX) {
 324                        command_noisy('read-tree', $last_commit);
 325                }
 326                my $x = command_oneline('write-tree');
 327                my ($y) = (command(qw/cat-file commit/, $last_commit)
 328                                                        =~ /^tree ($sha1)/m);
 329                if ($y ne $x) {
 330                        unlink $GIT_SVN_INDEX or croak $!;
 331                        command_noisy('read-tree', $last_commit);
 332                }
 333                $x = command_oneline('write-tree');
 334                if ($y ne $x) {
 335                        print STDERR "trees ($last_commit) $y != $x\n",
 336                                 "Something is seriously wrong...\n";
 337                }
 338        }
 339        while (1) {
 340                # fork, because using SVN::Pool with get_log() still doesn't
 341                # seem to help enough to keep memory usage down.
 342                defined(my $pid = fork) or croak $!;
 343                if (!$pid) {
 344                        $SVN::Error::handler = \&libsvn_skip_unknown_revs;
 345
 346                        # Yes I'm perfectly aware that the fourth argument
 347                        # below is the limit revisions number.  Unfortunately
 348                        # performance sucks with it enabled, so it's much
 349                        # faster to fetch revision ranges instead of relying
 350                        # on the limiter.
 351                        libsvn_get_log(libsvn_dup_ra($SVN), [''],
 352                                        $min, $max, 0, 1, 1,
 353                                sub {
 354                                        my $log_msg;
 355                                        if ($last_commit) {
 356                                                $log_msg = libsvn_fetch(
 357                                                        $last_commit, @_);
 358                                                $last_commit = git_commit(
 359                                                        $log_msg,
 360                                                        $last_commit,
 361                                                        @parents);
 362                                        } else {
 363                                                $log_msg = libsvn_new_tree(@_);
 364                                                $last_commit = git_commit(
 365                                                        $log_msg, @parents);
 366                                        }
 367                                });
 368                        exit 0;
 369                }
 370                waitpid $pid, 0;
 371                croak $? if $?;
 372                ($last_rev, $last_commit) = svn_grab_base_rev();
 373                last if ($max >= $head);
 374                $min = $max + 1;
 375                $max += $inc;
 376                $max = $head if ($max > $head);
 377                $SVN = libsvn_connect($SVN_URL);
 378        }
 379        restore_index($index);
 380        return { revision => $last_rev, commit => $last_commit };
 381}
 382
 383sub commit {
 384        my (@commits) = @_;
 385        check_upgrade_needed();
 386        if ($_stdin || !@commits) {
 387                print "Reading from stdin...\n";
 388                @commits = ();
 389                while (<STDIN>) {
 390                        if (/\b($sha1_short)\b/o) {
 391                                unshift @commits, $1;
 392                        }
 393                }
 394        }
 395        my @revs;
 396        foreach my $c (@commits) {
 397                my @tmp = command('rev-parse',$c);
 398                if (scalar @tmp == 1) {
 399                        push @revs, $tmp[0];
 400                } elsif (scalar @tmp > 1) {
 401                        push @revs, reverse(command('rev-list',@tmp));
 402                } else {
 403                        die "Failed to rev-parse $c\n";
 404                }
 405        }
 406        commit_lib(@revs);
 407        print "Done committing ",scalar @revs," revisions to SVN\n";
 408}
 409
 410sub commit_lib {
 411        my (@revs) = @_;
 412        my ($r_last, $cmt_last) = svn_grab_base_rev();
 413        defined $r_last or die "Must have an existing revision to commit\n";
 414        my $fetched = fetch();
 415        if ($r_last != $fetched->{revision}) {
 416                print STDERR "There are new revisions that were fetched ",
 417                                "and need to be merged (or acknowledged) ",
 418                                "before committing.\n",
 419                                "last rev: $r_last\n",
 420                                " current: $fetched->{revision}\n";
 421                exit 1;
 422        }
 423        read_uuid();
 424        my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
 425        my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
 426
 427        my $repo;
 428        set_svn_commit_env();
 429        foreach my $c (@revs) {
 430                my $log_msg = get_commit_message($c, $commit_msg);
 431
 432                # fork for each commit because there's a memory leak I
 433                # can't track down... (it's probably in the SVN code)
 434                defined(my $pid = open my $fh, '-|') or croak $!;
 435                if (!$pid) {
 436                        my $ed = SVN::Git::Editor->new(
 437                                        {       r => $r_last,
 438                                                ra => libsvn_dup_ra($SVN),
 439                                                c => $c,
 440                                                svn_path => $SVN->{svn_path},
 441                                        },
 442                                        $SVN->get_commit_editor(
 443                                                $log_msg->{msg},
 444                                                sub {
 445                                                        libsvn_commit_cb(
 446                                                                @_, $c,
 447                                                                $log_msg->{msg},
 448                                                                $r_last,
 449                                                                $cmt_last)
 450                                                },
 451                                                @lock)
 452                                        );
 453                        my $mods = libsvn_checkout_tree($cmt_last, $c, $ed);
 454                        if (@$mods == 0) {
 455                                print "No changes\nr$r_last = $cmt_last\n";
 456                                $ed->abort_edit;
 457                        } else {
 458                                $ed->close_edit;
 459                        }
 460                        exit 0;
 461                }
 462                my ($r_new, $cmt_new, $no);
 463                while (<$fh>) {
 464                        print $_;
 465                        chomp;
 466                        if (/^r(\d+) = ($sha1)$/o) {
 467                                ($r_new, $cmt_new) = ($1, $2);
 468                        } elsif ($_ eq 'No changes') {
 469                                $no = 1;
 470                        }
 471                }
 472                close $fh or exit 1;
 473                if (! defined $r_new && ! defined $cmt_new) {
 474                        unless ($no) {
 475                                die "Failed to parse revision information\n";
 476                        }
 477                } else {
 478                        ($r_last, $cmt_last) = ($r_new, $cmt_new);
 479                }
 480        }
 481        $ENV{LC_ALL} = 'C';
 482        unlink $commit_msg;
 483}
 484
 485sub dcommit {
 486        my $head = shift || 'HEAD';
 487        my $gs = "refs/remotes/$GIT_SVN";
 488        my @refs = command(qw/rev-list --no-merges/, "$gs..$head");
 489        my $last_rev;
 490        foreach my $d (reverse @refs) {
 491                if (!verify_ref("$d~1")) {
 492                        die "Commit $d\n",
 493                            "has no parent commit, and therefore ",
 494                            "nothing to diff against.\n",
 495                            "You should be working from a repository ",
 496                            "originally created by git-svn\n";
 497                }
 498                unless (defined $last_rev) {
 499                        (undef, $last_rev, undef) = cmt_metadata("$d~1");
 500                        unless (defined $last_rev) {
 501                                die "Unable to extract revision information ",
 502                                    "from commit $d~1\n";
 503                        }
 504                }
 505                if ($_dry_run) {
 506                        print "diff-tree $d~1 $d\n";
 507                } else {
 508                        if (my $r = commit_diff("$d~1", $d, undef, $last_rev)) {
 509                                $last_rev = $r;
 510                        } # else: no changes, same $last_rev
 511                }
 512        }
 513        return if $_dry_run;
 514        fetch();
 515        my @diff = command('diff-tree', 'HEAD', $gs, '--');
 516        my @finish;
 517        if (@diff) {
 518                @finish = qw/rebase/;
 519                push @finish, qw/--merge/ if $_merge;
 520                push @finish, "--strategy=$_strategy" if $_strategy;
 521                print STDERR "W: HEAD and $gs differ, using @finish:\n", @diff;
 522        } else {
 523                print "No changes between current HEAD and $gs\n",
 524                      "Resetting to the latest $gs\n";
 525                @finish = qw/reset --mixed/;
 526        }
 527        command_noisy(@finish, $gs);
 528}
 529
 530sub show_ignore {
 531        $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
 532        my $repo;
 533        $SVN ||= libsvn_connect($SVN_URL);
 534        my $r = defined $_revision ? $_revision : $SVN->get_latest_revnum;
 535        libsvn_traverse_ignore(\*STDOUT, $SVN->{svn_path}, $r);
 536}
 537
 538sub graft_branches {
 539        my $gr_file = "$GIT_DIR/info/grafts";
 540        my ($grafts, $comments) = read_grafts($gr_file);
 541        my $gr_sha1;
 542
 543        if (%$grafts) {
 544                # temporarily disable our grafts file to make this idempotent
 545                chomp($gr_sha1 = command(qw/hash-object -w/,$gr_file));
 546                rename $gr_file, "$gr_file~$gr_sha1" or croak $!;
 547        }
 548
 549        my $l_map = read_url_paths();
 550        my @re = map { qr/$_/is } @_opt_m if @_opt_m;
 551        unless ($_no_default_regex) {
 552                push @re, (qr/\b(?:merge|merging|merged)\s+with\s+([\w\.\-]+)/i,
 553                        qr/\b(?:merge|merging|merged)\s+([\w\.\-]+)/i,
 554                        qr/\b(?:from|of)\s+([\w\.\-]+)/i );
 555        }
 556        foreach my $u (keys %$l_map) {
 557                if (@re) {
 558                        foreach my $p (keys %{$l_map->{$u}}) {
 559                                graft_merge_msg($grafts,$l_map,$u,$p,@re);
 560                        }
 561                }
 562                unless ($_no_graft_copy) {
 563                        graft_file_copy_lib($grafts,$l_map,$u);
 564                }
 565        }
 566        graft_tree_joins($grafts);
 567
 568        write_grafts($grafts, $comments, $gr_file);
 569        unlink "$gr_file~$gr_sha1" if $gr_sha1;
 570}
 571
 572sub multi_init {
 573        my $url = shift;
 574        unless (defined $_trunk || defined $_branches || defined $_tags) {
 575                usage(1);
 576        }
 577        if (defined $_trunk) {
 578                my $trunk_url = complete_svn_url($url, $_trunk);
 579                my $ch_id;
 580                if ($GIT_SVN eq 'git-svn') {
 581                        $ch_id = 1;
 582                        $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
 583                }
 584                init_vars();
 585                unless (-d $GIT_SVN_DIR) {
 586                        if ($ch_id) {
 587                                print "GIT_SVN_ID set to 'trunk' for ",
 588                                      "$trunk_url ($_trunk)\n";
 589                        }
 590                        init($trunk_url);
 591                        command_noisy('repo-config', 'svn.trunk', $trunk_url);
 592                }
 593        }
 594        complete_url_ls_init($url, $_branches, '--branches/-b', '');
 595        complete_url_ls_init($url, $_tags, '--tags/-t', 'tags/');
 596}
 597
 598sub multi_fetch {
 599        # try to do trunk first, since branches/tags
 600        # may be descended from it.
 601        if (-e "$GIT_DIR/svn/trunk/info/url") {
 602                fetch_child_id('trunk', @_);
 603        }
 604        rec_fetch('', "$GIT_DIR/svn", @_);
 605}
 606
 607sub show_log {
 608        my (@args) = @_;
 609        my ($r_min, $r_max);
 610        my $r_last = -1; # prevent dupes
 611        rload_authors() if $_authors;
 612        if (defined $TZ) {
 613                $ENV{TZ} = $TZ;
 614        } else {
 615                delete $ENV{TZ};
 616        }
 617        if (defined $_revision) {
 618                if ($_revision =~ /^(\d+):(\d+)$/) {
 619                        ($r_min, $r_max) = ($1, $2);
 620                } elsif ($_revision =~ /^\d+$/) {
 621                        $r_min = $r_max = $_revision;
 622                } else {
 623                        print STDERR "-r$_revision is not supported, use ",
 624                                "standard \'git log\' arguments instead\n";
 625                        exit 1;
 626                }
 627        }
 628
 629        config_pager();
 630        @args = (git_svn_log_cmd($r_min, $r_max), @args);
 631        my $log = command_output_pipe(@args);
 632        run_pager();
 633        my (@k, $c, $d);
 634
 635        while (<$log>) {
 636                if (/^${_esc_color}commit ($sha1_short)/o) {
 637                        my $cmt = $1;
 638                        if ($c && cmt_showable($c) && $c->{r} != $r_last) {
 639                                $r_last = $c->{r};
 640                                process_commit($c, $r_min, $r_max, \@k) or
 641                                                                goto out;
 642                        }
 643                        $d = undef;
 644                        $c = { c => $cmt };
 645                } elsif (/^${_esc_color}author (.+) (\d+) ([\-\+]?\d+)$/) {
 646                        get_author_info($c, $1, $2, $3);
 647                } elsif (/^${_esc_color}(?:tree|parent|committer) /) {
 648                        # ignore
 649                } elsif (/^${_esc_color}:\d{6} \d{6} $sha1_short/o) {
 650                        push @{$c->{raw}}, $_;
 651                } elsif (/^${_esc_color}[ACRMDT]\t/) {
 652                        # we could add $SVN->{svn_path} here, but that requires
 653                        # remote access at the moment (repo_path_split)...
 654                        s#^(${_esc_color})([ACRMDT])\t#$1   $2 #;
 655                        push @{$c->{changed}}, $_;
 656                } elsif (/^${_esc_color}diff /) {
 657                        $d = 1;
 658                        push @{$c->{diff}}, $_;
 659                } elsif ($d) {
 660                        push @{$c->{diff}}, $_;
 661                } elsif (/^${_esc_color}    (git-svn-id:.+)$/) {
 662                        ($c->{url}, $c->{r}, undef) = extract_metadata($1);
 663                } elsif (s/^${_esc_color}    //) {
 664                        push @{$c->{l}}, $_;
 665                }
 666        }
 667        if ($c && defined $c->{r} && $c->{r} != $r_last) {
 668                $r_last = $c->{r};
 669                process_commit($c, $r_min, $r_max, \@k);
 670        }
 671        if (@k) {
 672                my $swap = $r_max;
 673                $r_max = $r_min;
 674                $r_min = $swap;
 675                process_commit($_, $r_min, $r_max) foreach reverse @k;
 676        }
 677out:
 678        eval { command_close_pipe($log) };
 679        print '-' x72,"\n" unless $_incremental || $_oneline;
 680}
 681
 682sub commit_diff_usage {
 683        print STDERR "Usage: $0 commit-diff <tree-ish> <tree-ish> [<URL>]\n";
 684        exit 1
 685}
 686
 687sub commit_diff {
 688        my $ta = shift or commit_diff_usage();
 689        my $tb = shift or commit_diff_usage();
 690        if (!eval { $SVN_URL = shift || file_to_s("$GIT_SVN_DIR/info/url") }) {
 691                print STDERR "Needed URL or usable git-svn id command-line\n";
 692                commit_diff_usage();
 693        }
 694        my $r = shift;
 695        unless (defined $r) {
 696                if (defined $_revision) {
 697                        $r = $_revision
 698                } else {
 699                        die "-r|--revision is a required argument\n";
 700                }
 701        }
 702        if (defined $_message && defined $_file) {
 703                print STDERR "Both --message/-m and --file/-F specified ",
 704                                "for the commit message.\n",
 705                                "I have no idea what you mean\n";
 706                exit 1;
 707        }
 708        if (defined $_file) {
 709                $_message = file_to_s($_file);
 710        } else {
 711                $_message ||= get_commit_message($tb,
 712                                        "$GIT_DIR/.svn-commit.tmp.$$")->{msg};
 713        }
 714        $SVN ||= libsvn_connect($SVN_URL);
 715        if ($r eq 'HEAD') {
 716                $r = $SVN->get_latest_revnum;
 717        } elsif ($r !~ /^\d+$/) {
 718                die "revision argument: $r not understood by git-svn\n";
 719        }
 720        my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
 721        my $rev_committed;
 722        my $ed = SVN::Git::Editor->new({        r => $r,
 723                                                ra => libsvn_dup_ra($SVN),
 724                                                c => $tb,
 725                                                svn_path => $SVN->{svn_path}
 726                                        },
 727                                $SVN->get_commit_editor($_message,
 728                                        sub {
 729                                                $rev_committed = $_[0];
 730                                                print "Committed $_[0]\n";
 731                                        }, @lock)
 732                                );
 733        eval {
 734                my $mods = libsvn_checkout_tree($ta, $tb, $ed);
 735                if (@$mods == 0) {
 736                        print "No changes\n$ta == $tb\n";
 737                        $ed->abort_edit;
 738                } else {
 739                        $ed->close_edit;
 740                }
 741        };
 742        fatal "$@\n" if $@;
 743        $_message = $_file = undef;
 744        return $rev_committed;
 745}
 746
 747########################### utility functions #########################
 748
 749sub cmt_showable {
 750        my ($c) = @_;
 751        return 1 if defined $c->{r};
 752        if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
 753                                $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
 754                my @msg = command(qw/cat-file commit/, $c->{c});
 755                shift @msg while ($msg[0] ne "\n");
 756                shift @msg;
 757                @{$c->{l}} = grep !/^git-svn-id: /, @msg;
 758
 759                (undef, $c->{r}, undef) = extract_metadata(
 760                                (grep(/^git-svn-id: /, @msg))[-1]);
 761        }
 762        return defined $c->{r};
 763}
 764
 765sub log_use_color {
 766        return 1 if $_color;
 767        my ($dc, $dcvar);
 768        $dcvar = 'color.diff';
 769        $dc = `git-repo-config --get $dcvar`;
 770        if ($dc eq '') {
 771                # nothing at all; fallback to "diff.color"
 772                $dcvar = 'diff.color';
 773                $dc = `git-repo-config --get $dcvar`;
 774        }
 775        chomp($dc);
 776        if ($dc eq 'auto') {
 777                my $pc;
 778                $pc = `git-repo-config --get color.pager`;
 779                if ($pc eq '') {
 780                        # does not have it -- fallback to pager.color
 781                        $pc = `git-repo-config --bool --get pager.color`;
 782                }
 783                else {
 784                        $pc = `git-repo-config --bool --get color.pager`;
 785                        if ($?) {
 786                                $pc = 'false';
 787                        }
 788                }
 789                chomp($pc);
 790                if (-t *STDOUT || (defined $_pager && $pc eq 'true')) {
 791                        return ($ENV{TERM} && $ENV{TERM} ne 'dumb');
 792                }
 793                return 0;
 794        }
 795        return 0 if $dc eq 'never';
 796        return 1 if $dc eq 'always';
 797        chomp($dc = `git-repo-config --bool --get $dcvar`);
 798        return ($dc eq 'true');
 799}
 800
 801sub git_svn_log_cmd {
 802        my ($r_min, $r_max) = @_;
 803        my @cmd = (qw/log --abbrev-commit --pretty=raw
 804                        --default/, "refs/remotes/$GIT_SVN");
 805        push @cmd, '-r' unless $_non_recursive;
 806        push @cmd, qw/--raw --name-status/ if $_verbose;
 807        push @cmd, '--color' if log_use_color();
 808        return @cmd unless defined $r_max;
 809        if ($r_max == $r_min) {
 810                push @cmd, '--max-count=1';
 811                if (my $c = revdb_get($REVDB, $r_max)) {
 812                        push @cmd, $c;
 813                }
 814        } else {
 815                my ($c_min, $c_max);
 816                $c_max = revdb_get($REVDB, $r_max);
 817                $c_min = revdb_get($REVDB, $r_min);
 818                if (defined $c_min && defined $c_max) {
 819                        if ($r_max > $r_max) {
 820                                push @cmd, "$c_min..$c_max";
 821                        } else {
 822                                push @cmd, "$c_max..$c_min";
 823                        }
 824                } elsif ($r_max > $r_min) {
 825                        push @cmd, $c_max;
 826                } else {
 827                        push @cmd, $c_min;
 828                }
 829        }
 830        return @cmd;
 831}
 832
 833sub fetch_child_id {
 834        my $id = shift;
 835        print "Fetching $id\n";
 836        my $ref = "$GIT_DIR/refs/remotes/$id";
 837        defined(my $pid = open my $fh, '-|') or croak $!;
 838        if (!$pid) {
 839                $_repack = undef;
 840                $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
 841                init_vars();
 842                fetch(@_);
 843                exit 0;
 844        }
 845        while (<$fh>) {
 846                print $_;
 847                check_repack() if (/^r\d+ = $sha1/);
 848        }
 849        close $fh or croak $?;
 850}
 851
 852sub rec_fetch {
 853        my ($pfx, $p, @args) = @_;
 854        my @dir;
 855        foreach (sort <$p/*>) {
 856                if (-r "$_/info/url") {
 857                        $pfx .= '/' if $pfx && $pfx !~ m!/$!;
 858                        my $id = $pfx . basename $_;
 859                        next if $id eq 'trunk';
 860                        fetch_child_id($id, @args);
 861                } elsif (-d $_) {
 862                        push @dir, $_;
 863                }
 864        }
 865        foreach (@dir) {
 866                my $x = $_;
 867                $x =~ s!^\Q$GIT_DIR\E/svn/!!;
 868                rec_fetch($x, $_);
 869        }
 870}
 871
 872sub complete_svn_url {
 873        my ($url, $path) = @_;
 874        $path =~ s#/+$##;
 875        $url =~ s#/+$## if $url;
 876        if ($path !~ m#^[a-z\+]+://#) {
 877                $path = '/' . $path if ($path !~ m#^/#);
 878                if (!defined $url || $url !~ m#^[a-z\+]+://#) {
 879                        fatal("E: '$path' is not a complete URL ",
 880                              "and a separate URL is not specified\n");
 881                }
 882                $path = $url . $path;
 883        }
 884        return $path;
 885}
 886
 887sub complete_url_ls_init {
 888        my ($url, $path, $switch, $pfx) = @_;
 889        unless ($path) {
 890                print STDERR "W: $switch not specified\n";
 891                return;
 892        }
 893        my $full_url = complete_svn_url($url, $path);
 894        my @ls = libsvn_ls_fullurl($full_url);
 895        defined(my $pid = fork) or croak $!;
 896        if (!$pid) {
 897                foreach my $u (map { "$full_url/$_" } (grep m!/$!, @ls)) {
 898                        $u =~ s#/+$##;
 899                        if ($u !~ m!\Q$full_url\E/(.+)$!) {
 900                                print STDERR "W: Unrecognized URL: $u\n";
 901                                die "This should never happen\n";
 902                        }
 903                        # don't try to init already existing refs
 904                        my $id = $pfx.$1;
 905                        $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
 906                        init_vars();
 907                        unless (-d $GIT_SVN_DIR) {
 908                                print "init $u => $id\n";
 909                                init($u);
 910                        }
 911                }
 912                exit 0;
 913        }
 914        waitpid $pid, 0;
 915        croak $? if $?;
 916        my ($n) = ($switch =~ /^--(\w+)/);
 917        command_noisy('repo-config', "svn.$n", $full_url);
 918}
 919
 920sub common_prefix {
 921        my $paths = shift;
 922        my %common;
 923        foreach (@$paths) {
 924                my @tmp = split m#/#, $_;
 925                my $p = '';
 926                while (my $x = shift @tmp) {
 927                        $p .= "/$x";
 928                        $common{$p} ||= 0;
 929                        $common{$p}++;
 930                }
 931        }
 932        foreach (sort {length $b <=> length $a} keys %common) {
 933                if ($common{$_} == @$paths) {
 934                        return $_;
 935                }
 936        }
 937        return '';
 938}
 939
 940# grafts set here are 'stronger' in that they're based on actual tree
 941# matches, and won't be deleted from merge-base checking in write_grafts()
 942sub graft_tree_joins {
 943        my $grafts = shift;
 944        map_tree_joins() if (@_branch_from && !%tree_map);
 945        return unless %tree_map;
 946
 947        git_svn_each(sub {
 948                my $i = shift;
 949                my @args = (qw/rev-list --pretty=raw/, "refs/remotes/$i");
 950                my ($fh, $ctx) = command_output_pipe(@args);
 951                while (<$fh>) {
 952                        next unless /^commit ($sha1)$/o;
 953                        my $c = $1;
 954                        my ($t) = (<$fh> =~ /^tree ($sha1)$/o);
 955                        next unless $tree_map{$t};
 956
 957                        my $l;
 958                        do {
 959                                $l = readline $fh;
 960                        } until ($l =~ /^committer (?:.+) (\d+) ([\-\+]?\d+)$/);
 961
 962                        my ($s, $tz) = ($1, $2);
 963                        if ($tz =~ s/^\+//) {
 964                                $s += tz_to_s_offset($tz);
 965                        } elsif ($tz =~ s/^\-//) {
 966                                $s -= tz_to_s_offset($tz);
 967                        }
 968
 969                        my ($url_a, $r_a, $uuid_a) = cmt_metadata($c);
 970
 971                        foreach my $p (@{$tree_map{$t}}) {
 972                                next if $p eq $c;
 973                                my $mb = eval { command('merge-base', $c, $p) };
 974                                next unless ($@ || $?);
 975                                if (defined $r_a) {
 976                                        # see if SVN says it's a relative
 977                                        my ($url_b, $r_b, $uuid_b) =
 978                                                        cmt_metadata($p);
 979                                        next if (defined $url_b &&
 980                                                        defined $url_a &&
 981                                                        ($url_a eq $url_b) &&
 982                                                        ($uuid_a eq $uuid_b));
 983                                        if ($uuid_a eq $uuid_b) {
 984                                                if ($r_b < $r_a) {
 985                                                        $grafts->{$c}->{$p} = 2;
 986                                                        next;
 987                                                } elsif ($r_b > $r_a) {
 988                                                        $grafts->{$p}->{$c} = 2;
 989                                                        next;
 990                                                }
 991                                        }
 992                                }
 993                                my $ct = get_commit_time($p);
 994                                if ($ct < $s) {
 995                                        $grafts->{$c}->{$p} = 2;
 996                                } elsif ($ct > $s) {
 997                                        $grafts->{$p}->{$c} = 2;
 998                                }
 999                                # what should we do when $ct == $s ?
1000                        }
1001                }
1002                command_close_pipe($fh, $ctx);
1003        });
1004}
1005
1006sub graft_file_copy_lib {
1007        my ($grafts, $l_map, $u) = @_;
1008        my $tree_paths = $l_map->{$u};
1009        my $pfx = common_prefix([keys %$tree_paths]);
1010        my ($repo, $path) = repo_path_split($u.$pfx);
1011        $SVN = libsvn_connect($repo);
1012
1013        my ($base, $head) = libsvn_parse_revision();
1014        my $inc = 1000;
1015        my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
1016        my $eh = $SVN::Error::handler;
1017        $SVN::Error::handler = \&libsvn_skip_unknown_revs;
1018        while (1) {
1019                my $pool = SVN::Pool->new;
1020                libsvn_get_log(libsvn_dup_ra($SVN), [$path],
1021                               $min, $max, 0, 2, 1,
1022                        sub {
1023                                libsvn_graft_file_copies($grafts, $tree_paths,
1024                                                        $path, @_);
1025                        }, $pool);
1026                $pool->clear;
1027                last if ($max >= $head);
1028                $min = $max + 1;
1029                $max += $inc;
1030                $max = $head if ($max > $head);
1031        }
1032        $SVN::Error::handler = $eh;
1033}
1034
1035sub process_merge_msg_matches {
1036        my ($grafts, $l_map, $u, $p, $c, @matches) = @_;
1037        my (@strong, @weak);
1038        foreach (@matches) {
1039                # merging with ourselves is not interesting
1040                next if $_ eq $p;
1041                if ($l_map->{$u}->{$_}) {
1042                        push @strong, $_;
1043                } else {
1044                        push @weak, $_;
1045                }
1046        }
1047        foreach my $w (@weak) {
1048                last if @strong;
1049                # no exact match, use branch name as regexp.
1050                my $re = qr/\Q$w\E/i;
1051                foreach (keys %{$l_map->{$u}}) {
1052                        if (/$re/) {
1053                                push @strong, $l_map->{$u}->{$_};
1054                                last;
1055                        }
1056                }
1057                last if @strong;
1058                $w = basename($w);
1059                $re = qr/\Q$w\E/i;
1060                foreach (keys %{$l_map->{$u}}) {
1061                        if (/$re/) {
1062                                push @strong, $l_map->{$u}->{$_};
1063                                last;
1064                        }
1065                }
1066        }
1067        my ($rev) = ($c->{m} =~ /^git-svn-id:\s(?:\S+?)\@(\d+)
1068                                        \s(?:[a-f\d\-]+)$/xsm);
1069        unless (defined $rev) {
1070                ($rev) = ($c->{m} =~/^git-svn-id:\s(\d+)
1071                                        \@(?:[a-f\d\-]+)/xsm);
1072                return unless defined $rev;
1073        }
1074        foreach my $m (@strong) {
1075                my ($r0, $s0) = find_rev_before($rev, $m, 1);
1076                $grafts->{$c->{c}}->{$s0} = 1 if defined $s0;
1077        }
1078}
1079
1080sub graft_merge_msg {
1081        my ($grafts, $l_map, $u, $p, @re) = @_;
1082
1083        my $x = $l_map->{$u}->{$p};
1084        my $rl = rev_list_raw($x);
1085        while (my $c = next_rev_list_entry($rl)) {
1086                foreach my $re (@re) {
1087                        my (@br) = ($c->{m} =~ /$re/g);
1088                        next unless @br;
1089                        process_merge_msg_matches($grafts,$l_map,$u,$p,$c,@br);
1090                }
1091        }
1092}
1093
1094sub read_uuid {
1095        return if $SVN_UUID;
1096        my $pool = SVN::Pool->new;
1097        $SVN_UUID = $SVN->get_uuid($pool);
1098        $pool->clear;
1099}
1100
1101sub verify_ref {
1102        my ($ref) = @_;
1103        eval { command_oneline([ 'rev-parse', '--verify', $ref ],
1104                               { STDERR => 0 }); };
1105}
1106
1107sub repo_path_split {
1108        my $full_url = shift;
1109        $full_url =~ s#/+$##;
1110
1111        foreach (@repo_path_split_cache) {
1112                if ($full_url =~ s#$_##) {
1113                        my $u = $1;
1114                        $full_url =~ s#^/+##;
1115                        return ($u, $full_url);
1116                }
1117        }
1118        my $tmp = libsvn_connect($full_url);
1119        return ($tmp->{repos_root}, $tmp->{svn_path});
1120}
1121
1122sub setup_git_svn {
1123        defined $SVN_URL or croak "SVN repository location required\n";
1124        unless (-d $GIT_DIR) {
1125                croak "GIT_DIR=$GIT_DIR does not exist!\n";
1126        }
1127        mkpath([$GIT_SVN_DIR]);
1128        mkpath(["$GIT_SVN_DIR/info"]);
1129        open my $fh, '>>',$REVDB or croak $!;
1130        close $fh;
1131        s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url");
1132
1133}
1134
1135sub get_tree_from_treeish {
1136        my ($treeish) = @_;
1137        croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o;
1138        my $type = command_oneline(qw/cat-file -t/, $treeish);
1139        my $expected;
1140        while ($type eq 'tag') {
1141                ($treeish, $type) = command(qw/cat-file tag/, $treeish);
1142        }
1143        if ($type eq 'commit') {
1144                $expected = (grep /^tree /, command(qw/cat-file commit/,
1145                                                    $treeish))[0];
1146                ($expected) = ($expected =~ /^tree ($sha1)$/);
1147                die "Unable to get tree from $treeish\n" unless $expected;
1148        } elsif ($type eq 'tree') {
1149                $expected = $treeish;
1150        } else {
1151                die "$treeish is a $type, expected tree, tag or commit\n";
1152        }
1153        return $expected;
1154}
1155
1156sub get_diff {
1157        my ($from, $treeish) = @_;
1158        print "diff-tree $from $treeish\n";
1159        my @diff_tree = qw(diff-tree -z -r);
1160        if ($_cp_similarity) {
1161                push @diff_tree, "-C$_cp_similarity";
1162        } else {
1163                push @diff_tree, '-C';
1164        }
1165        push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
1166        push @diff_tree, "-l$_l" if defined $_l;
1167        push @diff_tree, $from, $treeish;
1168        my ($diff_fh, $ctx) = command_output_pipe(@diff_tree);
1169        local $/ = "\0";
1170        my $state = 'meta';
1171        my @mods;
1172        while (<$diff_fh>) {
1173                chomp $_; # this gets rid of the trailing "\0"
1174                if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
1175                                        $sha1\s($sha1)\s([MTCRAD])\d*$/xo) {
1176                        push @mods, {   mode_a => $1, mode_b => $2,
1177                                        sha1_b => $3, chg => $4 };
1178                        if ($4 =~ /^(?:C|R)$/) {
1179                                $state = 'file_a';
1180                        } else {
1181                                $state = 'file_b';
1182                        }
1183                } elsif ($state eq 'file_a') {
1184                        my $x = $mods[$#mods] or croak "Empty array\n";
1185                        if ($x->{chg} !~ /^(?:C|R)$/) {
1186                                croak "Error parsing $_, $x->{chg}\n";
1187                        }
1188                        $x->{file_a} = $_;
1189                        $state = 'file_b';
1190                } elsif ($state eq 'file_b') {
1191                        my $x = $mods[$#mods] or croak "Empty array\n";
1192                        if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
1193                                croak "Error parsing $_, $x->{chg}\n";
1194                        }
1195                        if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
1196                                croak "Error parsing $_, $x->{chg}\n";
1197                        }
1198                        $x->{file_b} = $_;
1199                        $state = 'meta';
1200                } else {
1201                        croak "Error parsing $_\n";
1202                }
1203        }
1204        command_close_pipe($diff_fh, $ctx);
1205        return \@mods;
1206}
1207
1208sub libsvn_checkout_tree {
1209        my ($from, $treeish, $ed) = @_;
1210        my $mods = get_diff($from, $treeish);
1211        return $mods unless (scalar @$mods);
1212        my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
1213        foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
1214                my $f = $m->{chg};
1215                if (defined $o{$f}) {
1216                        $ed->$f($m, $_q);
1217                } else {
1218                        croak "Invalid change type: $f\n";
1219                }
1220        }
1221        $ed->rmdirs($_q) if $_rmdir;
1222        return $mods;
1223}
1224
1225sub get_commit_message {
1226        my ($commit, $commit_msg) = (@_);
1227        my %log_msg = ( msg => '' );
1228        open my $msg, '>', $commit_msg or croak $!;
1229
1230        my $type = command_oneline(qw/cat-file -t/, $commit);
1231        if ($type eq 'commit' || $type eq 'tag') {
1232                my ($msg_fh, $ctx) = command_output_pipe('cat-file',
1233                                                         $type, $commit);
1234                my $in_msg = 0;
1235                while (<$msg_fh>) {
1236                        if (!$in_msg) {
1237                                $in_msg = 1 if (/^\s*$/);
1238                        } elsif (/^git-svn-id: /) {
1239                                # skip this, we regenerate the correct one
1240                                # on re-fetch anyways
1241                        } else {
1242                                print $msg $_ or croak $!;
1243                        }
1244                }
1245                command_close_pipe($msg_fh, $ctx);
1246        }
1247        close $msg or croak $!;
1248
1249        if ($_edit || ($type eq 'tree')) {
1250                my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
1251                system($editor, $commit_msg);
1252        }
1253
1254        # file_to_s removes all trailing newlines, so just use chomp() here:
1255        open $msg, '<', $commit_msg or croak $!;
1256        { local $/; chomp($log_msg{msg} = <$msg>); }
1257        close $msg or croak $!;
1258
1259        return \%log_msg;
1260}
1261
1262sub set_svn_commit_env {
1263        if (defined $LC_ALL) {
1264                $ENV{LC_ALL} = $LC_ALL;
1265        } else {
1266                delete $ENV{LC_ALL};
1267        }
1268}
1269
1270sub rev_list_raw {
1271        my ($fh, $c) = command_output_pipe(qw/rev-list --pretty=raw/, @_);
1272        return { fh => $fh, ctx => $c, t => { } };
1273}
1274
1275sub next_rev_list_entry {
1276        my $rl = shift;
1277        my $fh = $rl->{fh};
1278        my $x = $rl->{t};
1279        while (<$fh>) {
1280                if (/^commit ($sha1)$/o) {
1281                        if ($x->{c}) {
1282                                $rl->{t} = { c => $1 };
1283                                return $x;
1284                        } else {
1285                                $x->{c} = $1;
1286                        }
1287                } elsif (/^parent ($sha1)$/o) {
1288                        $x->{p}->{$1} = 1;
1289                } elsif (s/^    //) {
1290                        $x->{m} ||= '';
1291                        $x->{m} .= $_;
1292                }
1293        }
1294        command_close_pipe($fh, $rl->{ctx});
1295        return ($x != $rl->{t}) ? $x : undef;
1296}
1297
1298sub s_to_file {
1299        my ($str, $file, $mode) = @_;
1300        open my $fd,'>',$file or croak $!;
1301        print $fd $str,"\n" or croak $!;
1302        close $fd or croak $!;
1303        chmod ($mode &~ umask, $file) if (defined $mode);
1304}
1305
1306sub file_to_s {
1307        my $file = shift;
1308        open my $fd,'<',$file or croak "$!: file: $file\n";
1309        local $/;
1310        my $ret = <$fd>;
1311        close $fd or croak $!;
1312        $ret =~ s/\s*$//s;
1313        return $ret;
1314}
1315
1316sub assert_revision_unknown {
1317        my $r = shift;
1318        if (my $c = revdb_get($REVDB, $r)) {
1319                croak "$r = $c already exists! Why are we refetching it?";
1320        }
1321}
1322
1323sub git_commit {
1324        my ($log_msg, @parents) = @_;
1325        assert_revision_unknown($log_msg->{revision});
1326        map_tree_joins() if (@_branch_from && !%tree_map);
1327
1328        my (@tmp_parents, @exec_parents, %seen_parent);
1329        if (my $lparents = $log_msg->{parents}) {
1330                @tmp_parents = @$lparents
1331        }
1332        # commit parents can be conditionally bound to a particular
1333        # svn revision via: "svn_revno=commit_sha1", filter them out here:
1334        foreach my $p (@parents) {
1335                next unless defined $p;
1336                if ($p =~ /^(\d+)=($sha1_short)$/o) {
1337                        if ($1 == $log_msg->{revision}) {
1338                                push @tmp_parents, $2;
1339                        }
1340                } else {
1341                        push @tmp_parents, $p if $p =~ /$sha1_short/o;
1342                }
1343        }
1344        my $tree = $log_msg->{tree};
1345        if (!defined $tree) {
1346                my $index = set_index($GIT_SVN_INDEX);
1347                $tree = command_oneline('write-tree');
1348                croak $? if $?;
1349                restore_index($index);
1350        }
1351        # just in case we clobber the existing ref, we still want that ref
1352        # as our parent:
1353        if (my $cur = verify_ref("refs/remotes/$GIT_SVN^0")) {
1354                chomp $cur;
1355                push @tmp_parents, $cur;
1356        }
1357
1358        if (exists $tree_map{$tree}) {
1359                foreach my $p (@{$tree_map{$tree}}) {
1360                        my $skip;
1361                        foreach (@tmp_parents) {
1362                                # see if a common parent is found
1363                                my $mb = eval { command('merge-base', $_, $p) };
1364                                next if ($@ || $?);
1365                                $skip = 1;
1366                                last;
1367                        }
1368                        next if $skip;
1369                        my ($url_p, $r_p, $uuid_p) = cmt_metadata($p);
1370                        next if (($SVN_UUID eq $uuid_p) &&
1371                                                ($log_msg->{revision} > $r_p));
1372                        next if (defined $url_p && defined $SVN_URL &&
1373                                                ($SVN_UUID eq $uuid_p) &&
1374                                                ($url_p eq $SVN_URL));
1375                        push @tmp_parents, $p;
1376                }
1377        }
1378        foreach (@tmp_parents) {
1379                next if $seen_parent{$_};
1380                $seen_parent{$_} = 1;
1381                push @exec_parents, $_;
1382                # MAXPARENT is defined to 16 in commit-tree.c:
1383                last if @exec_parents > 16;
1384        }
1385
1386        set_commit_env($log_msg);
1387        my @exec = ('git-commit-tree', $tree);
1388        push @exec, '-p', $_  foreach @exec_parents;
1389        defined(my $pid = open3(my $msg_fh, my $out_fh, '>&STDERR', @exec))
1390                                                                or croak $!;
1391        print $msg_fh $log_msg->{msg} or croak $!;
1392        unless ($_no_metadata) {
1393                print $msg_fh "\ngit-svn-id: $SVN_URL\@$log_msg->{revision}",
1394                                        " $SVN_UUID\n" or croak $!;
1395        }
1396        $msg_fh->flush == 0 or croak $!;
1397        close $msg_fh or croak $!;
1398        chomp(my $commit = do { local $/; <$out_fh> });
1399        close $out_fh or croak $!;
1400        waitpid $pid, 0;
1401        croak $? if $?;
1402        if ($commit !~ /^$sha1$/o) {
1403                die "Failed to commit, invalid sha1: $commit\n";
1404        }
1405        command_noisy('update-ref',"refs/remotes/$GIT_SVN",$commit);
1406        revdb_set($REVDB, $log_msg->{revision}, $commit);
1407
1408        # this output is read via pipe, do not change:
1409        print "r$log_msg->{revision} = $commit\n";
1410        check_repack();
1411        return $commit;
1412}
1413
1414sub check_repack {
1415        if ($_repack && (--$_repack_nr == 0)) {
1416                $_repack_nr = $_repack;
1417                # repack doesn't use any arguments with spaces in them, does it?
1418                command_noisy('repack', split(/\s+/, $_repack_flags));
1419        }
1420}
1421
1422sub set_commit_env {
1423        my ($log_msg) = @_;
1424        my $author = $log_msg->{author};
1425        if (!defined $author || length $author == 0) {
1426                $author = '(no author)';
1427        }
1428        my ($name,$email) = defined $users{$author} ?  @{$users{$author}}
1429                                : ($author,"$author\@$SVN_UUID");
1430        $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name;
1431        $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email;
1432        $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date};
1433}
1434
1435sub check_upgrade_needed {
1436        if (!-r $REVDB) {
1437                -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
1438                open my $fh, '>>',$REVDB or croak $!;
1439                close $fh;
1440        }
1441        return unless eval {
1442                command([qw/rev-parse --verify/,"$GIT_SVN-HEAD^0"],
1443                        {STDERR => 0});
1444        };
1445        my $head = eval { command('rev-parse',"refs/remotes/$GIT_SVN") };
1446        if ($@ || !$head) {
1447                print STDERR "Please run: $0 rebuild --upgrade\n";
1448                exit 1;
1449        }
1450}
1451
1452# fills %tree_map with a reverse mapping of trees to commits.  Useful
1453# for finding parents to commit on.
1454sub map_tree_joins {
1455        my %seen;
1456        foreach my $br (@_branch_from) {
1457                my $pipe = command_output_pipe(qw/rev-list
1458                                            --topo-order --pretty=raw/, $br);
1459                while (<$pipe>) {
1460                        if (/^commit ($sha1)$/o) {
1461                                my $commit = $1;
1462
1463                                # if we've seen a commit,
1464                                # we've seen its parents
1465                                last if $seen{$commit};
1466                                my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o);
1467                                unless (defined $tree) {
1468                                        die "Failed to parse commit $commit\n";
1469                                }
1470                                push @{$tree_map{$tree}}, $commit;
1471                                $seen{$commit} = 1;
1472                        }
1473                }
1474                eval { command_close_pipe($pipe) };
1475        }
1476}
1477
1478sub load_all_refs {
1479        if (@_branch_from) {
1480                print STDERR '--branch|-b parameters are ignored when ',
1481                        "--branch-all-refs|-B is passed\n";
1482        }
1483
1484        # don't worry about rev-list on non-commit objects/tags,
1485        # it shouldn't blow up if a ref is a blob or tree...
1486        @_branch_from = command(qw/rev-parse --symbolic --all/);
1487}
1488
1489# '<svn username> = real-name <email address>' mapping based on git-svnimport:
1490sub load_authors {
1491        open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
1492        while (<$authors>) {
1493                chomp;
1494                next unless /^(\S+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
1495                my ($user, $name, $email) = ($1, $2, $3);
1496                $users{$user} = [$name, $email];
1497        }
1498        close $authors or croak $!;
1499}
1500
1501sub rload_authors {
1502        open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
1503        while (<$authors>) {
1504                chomp;
1505                next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
1506                my ($user, $name, $email) = ($1, $2, $3);
1507                $rusers{"$name <$email>"} = $user;
1508        }
1509        close $authors or croak $!;
1510}
1511
1512sub git_svn_each {
1513        my $sub = shift;
1514        foreach (command(qw/rev-parse --symbolic --all/)) {
1515                next unless s#^refs/remotes/##;
1516                chomp $_;
1517                next unless -f "$GIT_DIR/svn/$_/info/url";
1518                &$sub($_);
1519        }
1520}
1521
1522sub migrate_revdb {
1523        git_svn_each(sub {
1524                my $id = shift;
1525                defined(my $pid = fork) or croak $!;
1526                if (!$pid) {
1527                        $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
1528                        init_vars();
1529                        exit 0 if -r $REVDB;
1530                        print "Upgrading svn => git mapping...\n";
1531                        -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
1532                        open my $fh, '>>',$REVDB or croak $!;
1533                        close $fh;
1534                        rebuild();
1535                        print "Done upgrading. You may now delete the ",
1536                                "deprecated $GIT_SVN_DIR/revs directory\n";
1537                        exit 0;
1538                }
1539                waitpid $pid, 0;
1540                croak $? if $?;
1541        });
1542}
1543
1544sub migration_check {
1545        migrate_revdb() unless (-e $REVDB);
1546        return if (-d "$GIT_DIR/svn" || !-d $GIT_DIR);
1547        print "Upgrading repository...\n";
1548        unless (-d "$GIT_DIR/svn") {
1549                mkdir "$GIT_DIR/svn" or croak $!;
1550        }
1551        print "Data from a previous version of git-svn exists, but\n\t",
1552                                "$GIT_SVN_DIR\n\t(required for this version ",
1553                                "($VERSION) of git-svn) does not.\n";
1554
1555        foreach my $x (command(qw/rev-parse --symbolic --all/)) {
1556                next unless $x =~ s#^refs/remotes/##;
1557                chomp $x;
1558                next unless -f "$GIT_DIR/$x/info/url";
1559                my $u = eval { file_to_s("$GIT_DIR/$x/info/url") };
1560                next unless $u;
1561                my $dn = dirname("$GIT_DIR/svn/$x");
1562                mkpath([$dn]) unless -d $dn;
1563                rename "$GIT_DIR/$x", "$GIT_DIR/svn/$x" or croak "$!: $x";
1564        }
1565        migrate_revdb() if (-d $GIT_SVN_DIR && !-w $REVDB);
1566        print "Done upgrading.\n";
1567}
1568
1569sub find_rev_before {
1570        my ($r, $id, $eq_ok) = @_;
1571        my $f = "$GIT_DIR/svn/$id/.rev_db";
1572        return (undef,undef) unless -r $f;
1573        --$r unless $eq_ok;
1574        while ($r > 0) {
1575                if (my $c = revdb_get($f, $r)) {
1576                        return ($r, $c);
1577                }
1578                --$r;
1579        }
1580        return (undef, undef);
1581}
1582
1583sub init_vars {
1584        $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
1585        $GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN";
1586        $REVDB = "$GIT_SVN_DIR/.rev_db";
1587        $GIT_SVN_INDEX = "$GIT_SVN_DIR/index";
1588        $SVN_URL = undef;
1589        $SVN_WC = "$GIT_SVN_DIR/tree";
1590        %tree_map = ();
1591}
1592
1593# convert GetOpt::Long specs for use by git-repo-config
1594sub read_repo_config {
1595        return unless -d $GIT_DIR;
1596        my $opts = shift;
1597        foreach my $o (keys %$opts) {
1598                my $v = $opts->{$o};
1599                my ($key) = ($o =~ /^([a-z\-]+)/);
1600                $key =~ s/-//g;
1601                my $arg = 'git-repo-config';
1602                $arg .= ' --int' if ($o =~ /[:=]i$/);
1603                $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
1604                if (ref $v eq 'ARRAY') {
1605                        chomp(my @tmp = `$arg --get-all svn.$key`);
1606                        @$v = @tmp if @tmp;
1607                } else {
1608                        chomp(my $tmp = `$arg --get svn.$key`);
1609                        if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) {
1610                                $$v = $tmp;
1611                        }
1612                }
1613        }
1614}
1615
1616sub set_default_vals {
1617        if (defined $_repack) {
1618                $_repack = 1000 if ($_repack <= 0);
1619                $_repack_nr = $_repack;
1620                $_repack_flags ||= '-d';
1621        }
1622}
1623
1624sub read_grafts {
1625        my $gr_file = shift;
1626        my ($grafts, $comments) = ({}, {});
1627        if (open my $fh, '<', $gr_file) {
1628                my @tmp;
1629                while (<$fh>) {
1630                        if (/^($sha1)\s+/) {
1631                                my $c = $1;
1632                                if (@tmp) {
1633                                        @{$comments->{$c}} = @tmp;
1634                                        @tmp = ();
1635                                }
1636                                foreach my $p (split /\s+/, $_) {
1637                                        $grafts->{$c}->{$p} = 1;
1638                                }
1639                        } else {
1640                                push @tmp, $_;
1641                        }
1642                }
1643                close $fh or croak $!;
1644                @{$comments->{'END'}} = @tmp if @tmp;
1645        }
1646        return ($grafts, $comments);
1647}
1648
1649sub write_grafts {
1650        my ($grafts, $comments, $gr_file) = @_;
1651
1652        open my $fh, '>', $gr_file or croak $!;
1653        foreach my $c (sort keys %$grafts) {
1654                if ($comments->{$c}) {
1655                        print $fh $_ foreach @{$comments->{$c}};
1656                }
1657                my $p = $grafts->{$c};
1658                my %x; # real parents
1659                delete $p->{$c}; # commits are not self-reproducing...
1660                my $ch = command_output_pipe(qw/cat-file commit/, $c);
1661                while (<$ch>) {
1662                        if (/^parent ($sha1)/) {
1663                                $x{$1} = $p->{$1} = 1;
1664                        } else {
1665                                last unless /^\S/;
1666                        }
1667                }
1668                eval { command_close_pipe($ch) }; # breaking the pipe
1669
1670                # if real parents are the only ones in the grafts, drop it
1671                next if join(' ',sort keys %$p) eq join(' ',sort keys %x);
1672
1673                my (@ip, @jp, $mb);
1674                my %del = %x;
1675                @ip = @jp = keys %$p;
1676                foreach my $i (@ip) {
1677                        next if $del{$i} || $p->{$i} == 2;
1678                        foreach my $j (@jp) {
1679                                next if $i eq $j || $del{$j} || $p->{$j} == 2;
1680                                $mb = eval { command('merge-base', $i, $j) };
1681                                next unless $mb;
1682                                chomp $mb;
1683                                next if $x{$mb};
1684                                if ($mb eq $j) {
1685                                        delete $p->{$i};
1686                                        $del{$i} = 1;
1687                                } elsif ($mb eq $i) {
1688                                        delete $p->{$j};
1689                                        $del{$j} = 1;
1690                                }
1691                        }
1692                }
1693
1694                # if real parents are the only ones in the grafts, drop it
1695                next if join(' ',sort keys %$p) eq join(' ',sort keys %x);
1696
1697                print $fh $c, ' ', join(' ', sort keys %$p),"\n";
1698        }
1699        if ($comments->{'END'}) {
1700                print $fh $_ foreach @{$comments->{'END'}};
1701        }
1702        close $fh or croak $!;
1703}
1704
1705sub read_url_paths_all {
1706        my ($l_map, $pfx, $p) = @_;
1707        my @dir;
1708        foreach (<$p/*>) {
1709                if (-r "$_/info/url") {
1710                        $pfx .= '/' if $pfx && $pfx !~ m!/$!;
1711                        my $id = $pfx . basename $_;
1712                        my $url = file_to_s("$_/info/url");
1713                        my ($u, $p) = repo_path_split($url);
1714                        $l_map->{$u}->{$p} = $id;
1715                } elsif (-d $_) {
1716                        push @dir, $_;
1717                }
1718        }
1719        foreach (@dir) {
1720                my $x = $_;
1721                $x =~ s!^\Q$GIT_DIR\E/svn/!!o;
1722                read_url_paths_all($l_map, $x, $_);
1723        }
1724}
1725
1726# this one only gets ids that have been imported, not new ones
1727sub read_url_paths {
1728        my $l_map = {};
1729        git_svn_each(sub { my $x = shift;
1730                        my $url = file_to_s("$GIT_DIR/svn/$x/info/url");
1731                        my ($u, $p) = repo_path_split($url);
1732                        $l_map->{$u}->{$p} = $x;
1733                        });
1734        return $l_map;
1735}
1736
1737sub extract_metadata {
1738        my $id = shift or return (undef, undef, undef);
1739        my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
1740                                                        \s([a-f\d\-]+)$/x);
1741        if (!defined $rev || !$uuid || !$url) {
1742                # some of the original repositories I made had
1743                # identifiers like this:
1744                ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
1745        }
1746        return ($url, $rev, $uuid);
1747}
1748
1749sub cmt_metadata {
1750        return extract_metadata((grep(/^git-svn-id: /,
1751                command(qw/cat-file commit/, shift)))[-1]);
1752}
1753
1754sub get_commit_time {
1755        my $cmt = shift;
1756        my $fh = command_output_pipe(qw/rev-list --pretty=raw -n1/, $cmt);
1757        while (<$fh>) {
1758                /^committer\s(?:.+) (\d+) ([\-\+]?\d+)$/ or next;
1759                my ($s, $tz) = ($1, $2);
1760                if ($tz =~ s/^\+//) {
1761                        $s += tz_to_s_offset($tz);
1762                } elsif ($tz =~ s/^\-//) {
1763                        $s -= tz_to_s_offset($tz);
1764                }
1765                eval { command_close_pipe($fh) };
1766                return $s;
1767        }
1768        die "Can't get commit time for commit: $cmt\n";
1769}
1770
1771sub tz_to_s_offset {
1772        my ($tz) = @_;
1773        $tz =~ s/(\d\d)$//;
1774        return ($1 * 60) + ($tz * 3600);
1775}
1776
1777# adapted from pager.c
1778sub config_pager {
1779        $_pager ||= $ENV{GIT_PAGER} || $ENV{PAGER};
1780        if (!defined $_pager) {
1781                $_pager = 'less';
1782        } elsif (length $_pager == 0 || $_pager eq 'cat') {
1783                $_pager = undef;
1784        }
1785}
1786
1787sub run_pager {
1788        return unless -t *STDOUT;
1789        pipe my $rfd, my $wfd or return;
1790        defined(my $pid = fork) or croak $!;
1791        if (!$pid) {
1792                open STDOUT, '>&', $wfd or croak $!;
1793                return;
1794        }
1795        open STDIN, '<&', $rfd or croak $!;
1796        $ENV{LESS} ||= 'FRSX';
1797        exec $_pager or croak "Can't run pager: $! ($_pager)\n";
1798}
1799
1800sub get_author_info {
1801        my ($dest, $author, $t, $tz) = @_;
1802        $author =~ s/(?:^\s*|\s*$)//g;
1803        $dest->{a_raw} = $author;
1804        my $_a;
1805        if ($_authors) {
1806                $_a = $rusers{$author} || undef;
1807        }
1808        if (!$_a) {
1809                ($_a) = ($author =~ /<([^>]+)\@[^>]+>$/);
1810        }
1811        $dest->{t} = $t;
1812        $dest->{tz} = $tz;
1813        $dest->{a} = $_a;
1814        # Date::Parse isn't in the standard Perl distro :(
1815        if ($tz =~ s/^\+//) {
1816                $t += tz_to_s_offset($tz);
1817        } elsif ($tz =~ s/^\-//) {
1818                $t -= tz_to_s_offset($tz);
1819        }
1820        $dest->{t_utc} = $t;
1821}
1822
1823sub process_commit {
1824        my ($c, $r_min, $r_max, $defer) = @_;
1825        if (defined $r_min && defined $r_max) {
1826                if ($r_min == $c->{r} && $r_min == $r_max) {
1827                        show_commit($c);
1828                        return 0;
1829                }
1830                return 1 if $r_min == $r_max;
1831                if ($r_min < $r_max) {
1832                        # we need to reverse the print order
1833                        return 0 if (defined $_limit && --$_limit < 0);
1834                        push @$defer, $c;
1835                        return 1;
1836                }
1837                if ($r_min != $r_max) {
1838                        return 1 if ($r_min < $c->{r});
1839                        return 1 if ($r_max > $c->{r});
1840                }
1841        }
1842        return 0 if (defined $_limit && --$_limit < 0);
1843        show_commit($c);
1844        return 1;
1845}
1846
1847sub show_commit {
1848        my $c = shift;
1849        if ($_oneline) {
1850                my $x = "\n";
1851                if (my $l = $c->{l}) {
1852                        while ($l->[0] =~ /^\s*$/) { shift @$l }
1853                        $x = $l->[0];
1854                }
1855                $_l_fmt ||= 'A' . length($c->{r});
1856                print 'r',pack($_l_fmt, $c->{r}),' | ';
1857                print "$c->{c} | " if $_show_commit;
1858                print $x;
1859        } else {
1860                show_commit_normal($c);
1861        }
1862}
1863
1864sub show_commit_changed_paths {
1865        my ($c) = @_;
1866        return unless $c->{changed};
1867        print "Changed paths:\n", @{$c->{changed}};
1868}
1869
1870sub show_commit_normal {
1871        my ($c) = @_;
1872        print '-' x72, "\nr$c->{r} | ";
1873        print "$c->{c} | " if $_show_commit;
1874        print "$c->{a} | ", strftime("%Y-%m-%d %H:%M:%S %z (%a, %d %b %Y)",
1875                                 localtime($c->{t_utc})), ' | ';
1876        my $nr_line = 0;
1877
1878        if (my $l = $c->{l}) {
1879                while ($l->[$#$l] eq "\n" && $#$l > 0
1880                                          && $l->[($#$l - 1)] eq "\n") {
1881                        pop @$l;
1882                }
1883                $nr_line = scalar @$l;
1884                if (!$nr_line) {
1885                        print "1 line\n\n\n";
1886                } else {
1887                        if ($nr_line == 1) {
1888                                $nr_line = '1 line';
1889                        } else {
1890                                $nr_line .= ' lines';
1891                        }
1892                        print $nr_line, "\n";
1893                        show_commit_changed_paths($c);
1894                        print "\n";
1895                        print $_ foreach @$l;
1896                }
1897        } else {
1898                print "1 line\n";
1899                show_commit_changed_paths($c);
1900                print "\n";
1901
1902        }
1903        foreach my $x (qw/raw diff/) {
1904                if ($c->{$x}) {
1905                        print "\n";
1906                        print $_ foreach @{$c->{$x}}
1907                }
1908        }
1909}
1910
1911sub _simple_prompt {
1912        my ($cred, $realm, $default_username, $may_save, $pool) = @_;
1913        $may_save = undef if $_no_auth_cache;
1914        $default_username = $_username if defined $_username;
1915        if (defined $default_username && length $default_username) {
1916                if (defined $realm && length $realm) {
1917                        print "Authentication realm: $realm\n";
1918                }
1919                $cred->username($default_username);
1920        } else {
1921                _username_prompt($cred, $realm, $may_save, $pool);
1922        }
1923        $cred->password(_read_password("Password for '" .
1924                                       $cred->username . "': ", $realm));
1925        $cred->may_save($may_save);
1926        $SVN::_Core::SVN_NO_ERROR;
1927}
1928
1929sub _ssl_server_trust_prompt {
1930        my ($cred, $realm, $failures, $cert_info, $may_save, $pool) = @_;
1931        $may_save = undef if $_no_auth_cache;
1932        print "Error validating server certificate for '$realm':\n";
1933        if ($failures & $SVN::Auth::SSL::UNKNOWNCA) {
1934                print " - The certificate is not issued by a trusted ",
1935                      "authority. Use the\n",
1936                      "   fingerprint to validate the certificate manually!\n";
1937        }
1938        if ($failures & $SVN::Auth::SSL::CNMISMATCH) {
1939                print " - The certificate hostname does not match.\n";
1940        }
1941        if ($failures & $SVN::Auth::SSL::NOTYETVALID) {
1942                print " - The certificate is not yet valid.\n";
1943        }
1944        if ($failures & $SVN::Auth::SSL::EXPIRED) {
1945                print " - The certificate has expired.\n";
1946        }
1947        if ($failures & $SVN::Auth::SSL::OTHER) {
1948                print " - The certificate has an unknown error.\n";
1949        }
1950        printf( "Certificate information:\n".
1951                " - Hostname: %s\n".
1952                " - Valid: from %s until %s\n".
1953                " - Issuer: %s\n".
1954                " - Fingerprint: %s\n",
1955                map $cert_info->$_, qw(hostname valid_from valid_until
1956                                       issuer_dname fingerprint) );
1957        my $choice;
1958prompt:
1959        print $may_save ?
1960              "(R)eject, accept (t)emporarily or accept (p)ermanently? " :
1961              "(R)eject or accept (t)emporarily? ";
1962        $choice = lc(substr(<STDIN> || 'R', 0, 1));
1963        if ($choice =~ /^t$/i) {
1964                $cred->may_save(undef);
1965        } elsif ($choice =~ /^r$/i) {
1966                return -1;
1967        } elsif ($may_save && $choice =~ /^p$/i) {
1968                $cred->may_save($may_save);
1969        } else {
1970                goto prompt;
1971        }
1972        $cred->accepted_failures($failures);
1973        $SVN::_Core::SVN_NO_ERROR;
1974}
1975
1976sub _ssl_client_cert_prompt {
1977        my ($cred, $realm, $may_save, $pool) = @_;
1978        $may_save = undef if $_no_auth_cache;
1979        print "Client certificate filename: ";
1980        chomp(my $filename = <STDIN>);
1981        $cred->cert_file($filename);
1982        $cred->may_save($may_save);
1983        $SVN::_Core::SVN_NO_ERROR;
1984}
1985
1986sub _ssl_client_cert_pw_prompt {
1987        my ($cred, $realm, $may_save, $pool) = @_;
1988        $may_save = undef if $_no_auth_cache;
1989        $cred->password(_read_password("Password: ", $realm));
1990        $cred->may_save($may_save);
1991        $SVN::_Core::SVN_NO_ERROR;
1992}
1993
1994sub _username_prompt {
1995        my ($cred, $realm, $may_save, $pool) = @_;
1996        $may_save = undef if $_no_auth_cache;
1997        if (defined $realm && length $realm) {
1998                print "Authentication realm: $realm\n";
1999        }
2000        my $username;
2001        if (defined $_username) {
2002                $username = $_username;
2003        } else {
2004                print "Username: ";
2005                chomp($username = <STDIN>);
2006        }
2007        $cred->username($username);
2008        $cred->may_save($may_save);
2009        $SVN::_Core::SVN_NO_ERROR;
2010}
2011
2012sub _read_password {
2013        my ($prompt, $realm) = @_;
2014        print $prompt;
2015        require Term::ReadKey;
2016        Term::ReadKey::ReadMode('noecho');
2017        my $password = '';
2018        while (defined(my $key = Term::ReadKey::ReadKey(0))) {
2019                last if $key =~ /[\012\015]/; # \n\r
2020                $password .= $key;
2021        }
2022        Term::ReadKey::ReadMode('restore');
2023        print "\n";
2024        $password;
2025}
2026
2027sub libsvn_connect {
2028        my ($url) = @_;
2029        SVN::_Core::svn_config_ensure($_config_dir, undef);
2030        my ($baton, $callbacks) = SVN::Core::auth_open_helper([
2031            SVN::Client::get_simple_provider(),
2032            SVN::Client::get_ssl_server_trust_file_provider(),
2033            SVN::Client::get_simple_prompt_provider(
2034              \&_simple_prompt, 2),
2035            SVN::Client::get_ssl_client_cert_prompt_provider(
2036              \&_ssl_client_cert_prompt, 2),
2037            SVN::Client::get_ssl_client_cert_pw_prompt_provider(
2038              \&_ssl_client_cert_pw_prompt, 2),
2039            SVN::Client::get_username_provider(),
2040            SVN::Client::get_ssl_server_trust_prompt_provider(
2041              \&_ssl_server_trust_prompt),
2042            SVN::Client::get_username_prompt_provider(
2043              \&_username_prompt, 2),
2044          ]);
2045        my $config = SVN::Core::config_get_config($_config_dir);
2046        my $ra = SVN::Ra->new(url => $url, auth => $baton,
2047                              config => $config,
2048                              pool => SVN::Pool->new,
2049                              auth_provider_callbacks => $callbacks);
2050        $ra->{svn_path} = $url;
2051        $ra->{repos_root} = $ra->get_repos_root;
2052        $ra->{svn_path} =~ s#^\Q$ra->{repos_root}\E/*##;
2053        push @repo_path_split_cache, qr/^(\Q$ra->{repos_root}\E)/;
2054        return $ra;
2055}
2056
2057sub libsvn_can_do_switch {
2058        unless (defined $_svn_can_do_switch) {
2059                my $pool = SVN::Pool->new;
2060                my $rep = eval {
2061                        $SVN->do_switch(1, '', 0, $SVN->{url},
2062                                        SVN::Delta::Editor->new, $pool);
2063                };
2064                if ($@) {
2065                        $_svn_can_do_switch = 0;
2066                } else {
2067                        $rep->abort_report($pool);
2068                        $_svn_can_do_switch = 1;
2069                }
2070                $pool->clear;
2071        }
2072        $_svn_can_do_switch;
2073}
2074
2075sub libsvn_dup_ra {
2076        my ($ra) = @_;
2077        SVN::Ra->new(map { $_ => $ra->{$_} } qw/config url
2078                     auth auth_provider_callbacks repos_root svn_path/);
2079}
2080
2081sub uri_encode {
2082        my ($f) = @_;
2083        $f =~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;
2084        $f
2085}
2086
2087sub uri_decode {
2088        my ($f) = @_;
2089        $f =~ tr/+/ /;
2090        $f =~ s/%([A-F0-9]{2})/chr hex($1)/ge;
2091        $f
2092}
2093
2094sub libsvn_log_entry {
2095        my ($rev, $author, $date, $msg, $parents, $untracked) = @_;
2096        my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
2097                                         (\d\d)\:(\d\d)\:(\d\d).\d+Z$/x)
2098                                or die "Unable to parse date: $date\n";
2099        if (defined $author && length $author > 0 &&
2100            defined $_authors && ! defined $users{$author}) {
2101                die "Author: $author not defined in $_authors file\n";
2102        }
2103        $msg = '' if ($rev == 0 && !defined $msg);
2104
2105        open my $un, '>>', "$GIT_SVN_DIR/unhandled.log" or croak $!;
2106        my $h;
2107        print $un "r$rev\n" or croak $!;
2108        $h = $untracked->{empty};
2109        foreach (sort keys %$h) {
2110                my $act = $h->{$_} ? '+empty_dir' : '-empty_dir';
2111                print $un "  $act: ", uri_encode($_), "\n" or croak $!;
2112                warn "W: $act: $_\n";
2113        }
2114        foreach my $t (qw/dir_prop file_prop/) {
2115                $h = $untracked->{$t} or next;
2116                foreach my $path (sort keys %$h) {
2117                        my $ppath = $path eq '' ? '.' : $path;
2118                        foreach my $prop (sort keys %{$h->{$path}}) {
2119                                next if $SKIP{$prop};
2120                                my $v = $h->{$path}->{$prop};
2121                                if (defined $v) {
2122                                        print $un "  +$t: ",
2123                                                  uri_encode($ppath), ' ',
2124                                                  uri_encode($prop), ' ',
2125                                                  uri_encode($v), "\n"
2126                                                  or croak $!;
2127                                } else {
2128                                        print $un "  -$t: ",
2129                                                  uri_encode($ppath), ' ',
2130                                                  uri_encode($prop), "\n"
2131                                                  or croak $!;
2132                                }
2133                        }
2134                }
2135        }
2136        foreach my $t (qw/absent_file absent_directory/) {
2137                $h = $untracked->{$t} or next;
2138                foreach my $parent (sort keys %$h) {
2139                        foreach my $path (sort @{$h->{$parent}}) {
2140                                print $un "  $t: ",
2141                                      uri_encode("$parent/$path"), "\n"
2142                                      or croak $!;
2143                                warn "W: $t: $parent/$path ",
2144                                     "Insufficient permissions?\n";
2145                        }
2146                }
2147        }
2148
2149        # revprops (make this optional? it's an extra network trip...)
2150        my $pool = SVN::Pool->new;
2151        my $rp = $SVN->rev_proplist($rev, $pool);
2152        foreach (sort keys %$rp) {
2153                next if /^svn:(?:author|date|log)$/;
2154                print $un "  rev_prop: ", uri_encode($_), ' ',
2155                          uri_encode($rp->{$_}), "\n";
2156        }
2157        $pool->clear;
2158        close $un or croak $!;
2159
2160        { revision => $rev, date => "+0000 $Y-$m-$d $H:$M:$S",
2161          author => $author, msg => $msg."\n", parents => $parents || [],
2162          revprops => $rp }
2163}
2164
2165sub process_rm {
2166        my ($gui, $last_commit, $f, $q) = @_;
2167        # remove entire directories.
2168        if (command('ls-tree',$last_commit,'--',$f) =~ /^040000 tree/) {
2169                my ($ls, $ctx) = command_output_pipe(qw/ls-tree
2170                                                     -r --name-only -z/,
2171                                                     $last_commit,'--',$f);
2172                local $/ = "\0";
2173                while (<$ls>) {
2174                        print $gui '0 ',0 x 40,"\t",$_ or croak $!;
2175                        print "\tD\t$_\n" unless $q;
2176                }
2177                print "\tD\t$f/\n" unless $q;
2178                command_close_pipe($ls, $ctx);
2179                return $SVN::Node::dir;
2180        } else {
2181                print $gui '0 ',0 x 40,"\t",$f,"\0" or croak $!;
2182                print "\tD\t$f\n" unless $q;
2183                return $SVN::Node::file;
2184        }
2185}
2186
2187sub libsvn_fetch {
2188        my ($last_commit, $paths, $rev, $author, $date, $msg) = @_;
2189        my $pool = SVN::Pool->new;
2190        my $ed = SVN::Git::Fetcher->new({ c => $last_commit, q => $_q });
2191        my $reporter = $SVN->do_update($rev, '', 1, $ed, $pool);
2192        my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
2193        my (undef, $last_rev, undef) = cmt_metadata($last_commit);
2194        $reporter->set_path('', $last_rev, 0, @lock, $pool);
2195        $reporter->finish_report($pool);
2196        $pool->clear;
2197        unless ($ed->{git_commit_ok}) {
2198                die "SVN connection failed somewhere...\n";
2199        }
2200        libsvn_log_entry($rev, $author, $date, $msg, [$last_commit], $ed);
2201}
2202
2203sub svn_grab_base_rev {
2204        my $c = eval { command_oneline([qw/rev-parse --verify/,
2205                                        "refs/remotes/$GIT_SVN^0"],
2206                                        { STDERR => 0 }) };
2207        if (defined $c && length $c) {
2208                my ($url, $rev, $uuid) = cmt_metadata($c);
2209                return ($rev, $c) if defined $rev;
2210        }
2211        if ($_no_metadata) {
2212                my $offset = -41; # from tail
2213                my $rl;
2214                open my $fh, '<', $REVDB or
2215                        die "--no-metadata specified and $REVDB not readable\n";
2216                seek $fh, $offset, 2;
2217                $rl = readline $fh;
2218                defined $rl or return (undef, undef);
2219                chomp $rl;
2220                while ($c ne $rl && tell $fh != 0) {
2221                        $offset -= 41;
2222                        seek $fh, $offset, 2;
2223                        $rl = readline $fh;
2224                        defined $rl or return (undef, undef);
2225                        chomp $rl;
2226                }
2227                my $rev = tell $fh;
2228                croak $! if ($rev < -1);
2229                $rev =  ($rev - 41) / 41;
2230                close $fh or croak $!;
2231                return ($rev, $c);
2232        }
2233        return (undef, undef);
2234}
2235
2236sub libsvn_parse_revision {
2237        my $base = shift;
2238        my $head = $SVN->get_latest_revnum();
2239        if (!defined $_revision || $_revision eq 'BASE:HEAD') {
2240                return ($base + 1, $head) if (defined $base);
2241                return (0, $head);
2242        }
2243        return ($1, $2) if ($_revision =~ /^(\d+):(\d+)$/);
2244        return ($_revision, $_revision) if ($_revision =~ /^\d+$/);
2245        if ($_revision =~ /^BASE:(\d+)$/) {
2246                return ($base + 1, $1) if (defined $base);
2247                return (0, $head);
2248        }
2249        return ($1, $head) if ($_revision =~ /^(\d+):HEAD$/);
2250        die "revision argument: $_revision not understood by git-svn\n",
2251                "Try using the command-line svn client instead\n";
2252}
2253
2254sub libsvn_traverse_ignore {
2255        my ($fh, $path, $r) = @_;
2256        $path =~ s#^/+##g;
2257        my $pool = SVN::Pool->new;
2258        my ($dirent, undef, $props) = $SVN->get_dir($path, $r, $pool);
2259        my $p = $path;
2260        $p =~ s#^\Q$SVN->{svn_path}\E/##;
2261        print $fh length $p ? "\n# $p\n" : "\n# /\n";
2262        if (my $s = $props->{'svn:ignore'}) {
2263                $s =~ s/[\r\n]+/\n/g;
2264                chomp $s;
2265                if (length $p == 0) {
2266                        $s =~ s#\n#\n/$p#g;
2267                        print $fh "/$s\n";
2268                } else {
2269                        $s =~ s#\n#\n/$p/#g;
2270                        print $fh "/$p/$s\n";
2271                }
2272        }
2273        foreach (sort keys %$dirent) {
2274                next if $dirent->{$_}->kind != $SVN::Node::dir;
2275                libsvn_traverse_ignore($fh, "$path/$_", $r);
2276        }
2277        $pool->clear;
2278}
2279
2280sub revisions_eq {
2281        my ($path, $r0, $r1) = @_;
2282        return 1 if $r0 == $r1;
2283        my $nr = 0;
2284        # should be OK to use Pool here (r1 - r0) should be small
2285        my $pool = SVN::Pool->new;
2286        libsvn_get_log($SVN, [$path], $r0, $r1,
2287                        0, 0, 1, sub {$nr++}, $pool);
2288        $pool->clear;
2289        return 0 if ($nr > 1);
2290        return 1;
2291}
2292
2293sub libsvn_find_parent_branch {
2294        my ($paths, $rev, $author, $date, $msg) = @_;
2295        my $svn_path = '/'.$SVN->{svn_path};
2296
2297        # look for a parent from another branch:
2298        my $i = $paths->{$svn_path} or return;
2299        my $branch_from = $i->copyfrom_path or return;
2300        my $r = $i->copyfrom_rev;
2301        print STDERR  "Found possible branch point: ",
2302                                "$branch_from => $svn_path, $r\n";
2303        $branch_from =~ s#^/##;
2304        my $l_map = {};
2305        read_url_paths_all($l_map, '', "$GIT_DIR/svn");
2306        my $url = $SVN->{repos_root};
2307        defined $l_map->{$url} or return;
2308        my $id = $l_map->{$url}->{$branch_from};
2309        if (!defined $id && $_follow_parent) {
2310                print STDERR "Following parent: $branch_from\@$r\n";
2311                # auto create a new branch and follow it
2312                $id = basename($branch_from);
2313                $id .= '@'.$r if -r "$GIT_DIR/svn/$id";
2314                while (-r "$GIT_DIR/svn/$id") {
2315                        # just grow a tail if we're not unique enough :x
2316                        $id .= '-';
2317                }
2318        }
2319        return unless defined $id;
2320
2321        my ($r0, $parent) = find_rev_before($r,$id,1);
2322        if ($_follow_parent && (!defined $r0 || !defined $parent)) {
2323                defined(my $pid = fork) or croak $!;
2324                if (!$pid) {
2325                        $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
2326                        init_vars();
2327                        $SVN_URL = "$url/$branch_from";
2328                        $SVN = undef;
2329                        setup_git_svn();
2330                        # we can't assume SVN_URL exists at r+1:
2331                        $_revision = "0:$r";
2332                        fetch_lib();
2333                        exit 0;
2334                }
2335                waitpid $pid, 0;
2336                croak $? if $?;
2337                ($r0, $parent) = find_rev_before($r,$id,1);
2338        }
2339        return unless (defined $r0 && defined $parent);
2340        if (revisions_eq($branch_from, $r0, $r)) {
2341                unlink $GIT_SVN_INDEX;
2342                print STDERR "Found branch parent: ($GIT_SVN) $parent\n";
2343                command_noisy('read-tree', $parent);
2344                unless (libsvn_can_do_switch()) {
2345                        return _libsvn_new_tree($paths, $rev, $author, $date,
2346                                                $msg, [$parent]);
2347                }
2348                # do_switch works with svn/trunk >= r22312, but that is not
2349                # included with SVN 1.4.2 (the latest version at the moment),
2350                # so we can't rely on it.
2351                my $ra = libsvn_connect("$url/$branch_from");
2352                my $ed = SVN::Git::Fetcher->new({c => $parent, q => $_q });
2353                my $pool = SVN::Pool->new;
2354                my $reporter = $ra->do_switch($rev, '', 1, $SVN->{url},
2355                                              $ed, $pool);
2356                my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
2357                $reporter->set_path('', $r0, 0, @lock, $pool);
2358                $reporter->finish_report($pool);
2359                $pool->clear;
2360                unless ($ed->{git_commit_ok}) {
2361                        die "SVN connection failed somewhere...\n";
2362                }
2363                return libsvn_log_entry($rev, $author, $date, $msg, [$parent]);
2364        }
2365        print STDERR "Nope, branch point not imported or unknown\n";
2366        return undef;
2367}
2368
2369sub libsvn_get_log {
2370        my ($ra, @args) = @_;
2371        $args[4]-- if $args[4] && ! $_follow_parent;
2372        if ($SVN::Core::VERSION le '1.2.0') {
2373                splice(@args, 3, 1);
2374        }
2375        $ra->get_log(@args);
2376}
2377
2378sub libsvn_new_tree {
2379        if (my $log_entry = libsvn_find_parent_branch(@_)) {
2380                return $log_entry;
2381        }
2382        my ($paths, $rev, $author, $date, $msg) = @_; # $pool is last
2383        _libsvn_new_tree($paths, $rev, $author, $date, $msg, []);
2384}
2385
2386sub _libsvn_new_tree {
2387        my ($paths, $rev, $author, $date, $msg, $parents) = @_;
2388        my $pool = SVN::Pool->new;
2389        my $ed = SVN::Git::Fetcher->new({q => $_q});
2390        my $reporter = $SVN->do_update($rev, '', 1, $ed, $pool);
2391        my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
2392        $reporter->set_path('', $rev, 1, @lock, $pool);
2393        $reporter->finish_report($pool);
2394        $pool->clear;
2395        unless ($ed->{git_commit_ok}) {
2396                die "SVN connection failed somewhere...\n";
2397        }
2398        libsvn_log_entry($rev, $author, $date, $msg, $parents, $ed);
2399}
2400
2401sub find_graft_path_commit {
2402        my ($tree_paths, $p1, $r1) = @_;
2403        foreach my $x (keys %$tree_paths) {
2404                next unless ($p1 =~ /^\Q$x\E/);
2405                my $i = $tree_paths->{$x};
2406                my ($r0, $parent) = find_rev_before($r1,$i,1);
2407                return $parent if (defined $r0 && $r0 == $r1);
2408                print STDERR "r$r1 of $i not imported\n";
2409                next;
2410        }
2411        return undef;
2412}
2413
2414sub find_graft_path_parents {
2415        my ($grafts, $tree_paths, $c, $p0, $r0) = @_;
2416        foreach my $x (keys %$tree_paths) {
2417                next unless ($p0 =~ /^\Q$x\E/);
2418                my $i = $tree_paths->{$x};
2419                my ($r, $parent) = find_rev_before($r0, $i, 1);
2420                if (defined $r && defined $parent && revisions_eq($x,$r,$r0)) {
2421                        my ($url_b, undef, $uuid_b) = cmt_metadata($c);
2422                        my ($url_a, undef, $uuid_a) = cmt_metadata($parent);
2423                        next if ($url_a && $url_b && $url_a eq $url_b &&
2424                                                        $uuid_b eq $uuid_a);
2425                        $grafts->{$c}->{$parent} = 1;
2426                }
2427        }
2428}
2429
2430sub libsvn_graft_file_copies {
2431        my ($grafts, $tree_paths, $path, $paths, $rev) = @_;
2432        foreach (keys %$paths) {
2433                my $i = $paths->{$_};
2434                my ($m, $p0, $r0) = ($i->action, $i->copyfrom_path,
2435                                        $i->copyfrom_rev);
2436                next unless (defined $p0 && defined $r0);
2437
2438                my $p1 = $_;
2439                $p1 =~ s#^/##;
2440                $p0 =~ s#^/##;
2441                my $c = find_graft_path_commit($tree_paths, $p1, $rev);
2442                next unless $c;
2443                find_graft_path_parents($grafts, $tree_paths, $c, $p0, $r0);
2444        }
2445}
2446
2447sub set_index {
2448        my $old = $ENV{GIT_INDEX_FILE};
2449        $ENV{GIT_INDEX_FILE} = shift;
2450        return $old;
2451}
2452
2453sub restore_index {
2454        my ($old) = @_;
2455        if (defined $old) {
2456                $ENV{GIT_INDEX_FILE} = $old;
2457        } else {
2458                delete $ENV{GIT_INDEX_FILE};
2459        }
2460}
2461
2462sub libsvn_commit_cb {
2463        my ($rev, $date, $committer, $c, $msg, $r_last, $cmt_last) = @_;
2464        if ($_optimize_commits && $rev == ($r_last + 1)) {
2465                my $log = libsvn_log_entry($rev,$committer,$date,$msg);
2466                $log->{tree} = get_tree_from_treeish($c);
2467                my $cmt = git_commit($log, $cmt_last, $c);
2468                my @diff = command('diff-tree', $cmt, $c);
2469                if (@diff) {
2470                        print STDERR "Trees differ: $cmt $c\n",
2471                                        join('',@diff),"\n";
2472                        exit 1;
2473                }
2474        } else {
2475                fetch("$rev=$c");
2476        }
2477}
2478
2479sub libsvn_ls_fullurl {
2480        my $fullurl = shift;
2481        my $ra = libsvn_connect($fullurl);
2482        my @ret;
2483        my $pool = SVN::Pool->new;
2484        my $r = defined $_revision ? $_revision : $ra->get_latest_revnum;
2485        my ($dirent, undef, undef) = $ra->get_dir('', $r, $pool);
2486        foreach my $d (sort keys %$dirent) {
2487                if ($dirent->{$d}->kind == $SVN::Node::dir) {
2488                        push @ret, "$d/"; # add '/' for compat with cli svn
2489                }
2490        }
2491        $pool->clear;
2492        return @ret;
2493}
2494
2495
2496sub libsvn_skip_unknown_revs {
2497        my $err = shift;
2498        my $errno = $err->apr_err();
2499        # Maybe the branch we're tracking didn't
2500        # exist when the repo started, so it's
2501        # not an error if it doesn't, just continue
2502        #
2503        # Wonderfully consistent library, eh?
2504        # 160013 - svn:// and file://
2505        # 175002 - http(s)://
2506        # 175007 - http(s):// (this repo required authorization, too...)
2507        #   More codes may be discovered later...
2508        if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
2509                return;
2510        }
2511        croak "Error from SVN, ($errno): ", $err->expanded_message,"\n";
2512};
2513
2514# Tie::File seems to be prone to offset errors if revisions get sparse,
2515# it's not that fast, either.  Tie::File is also not in Perl 5.6.  So
2516# one of my favorite modules is out :<  Next up would be one of the DBM
2517# modules, but I'm not sure which is most portable...  So I'll just
2518# go with something that's plain-text, but still capable of
2519# being randomly accessed.  So here's my ultra-simple fixed-width
2520# database.  All records are 40 characters + "\n", so it's easy to seek
2521# to a revision: (41 * rev) is the byte offset.
2522# A record of 40 0s denotes an empty revision.
2523# And yes, it's still pretty fast (faster than Tie::File).
2524sub revdb_set {
2525        my ($file, $rev, $commit) = @_;
2526        length $commit == 40 or croak "arg3 must be a full SHA1 hexsum\n";
2527        open my $fh, '+<', $file or croak $!;
2528        my $offset = $rev * 41;
2529        # assume that append is the common case:
2530        seek $fh, 0, 2 or croak $!;
2531        my $pos = tell $fh;
2532        if ($pos < $offset) {
2533                print $fh (('0' x 40),"\n") x (($offset - $pos) / 41);
2534        }
2535        seek $fh, $offset, 0 or croak $!;
2536        print $fh $commit,"\n";
2537        close $fh or croak $!;
2538}
2539
2540sub revdb_get {
2541        my ($file, $rev) = @_;
2542        my $ret;
2543        my $offset = $rev * 41;
2544        open my $fh, '<', $file or croak $!;
2545        seek $fh, $offset, 0;
2546        if (tell $fh == $offset) {
2547                $ret = readline $fh;
2548                if (defined $ret) {
2549                        chomp $ret;
2550                        $ret = undef if ($ret =~ /^0{40}$/);
2551                }
2552        }
2553        close $fh or croak $!;
2554        return $ret;
2555}
2556
2557sub copy_remote_ref {
2558        my $origin = $_cp_remote ? $_cp_remote : 'origin';
2559        my $ref = "refs/remotes/$GIT_SVN";
2560        if (command('ls-remote', $origin, $ref)) {
2561                command_noisy('fetch', $origin, "$ref:$ref");
2562        } elsif ($_cp_remote && !$_upgrade) {
2563                die "Unable to find remote reference: ",
2564                                "refs/remotes/$GIT_SVN on $origin\n";
2565        }
2566}
2567
2568{
2569        my $kill_stupid_warnings = $SVN::Node::none.$SVN::Node::file.
2570                                $SVN::Node::dir.$SVN::Node::unknown.
2571                                $SVN::Node::none.$SVN::Node::file.
2572                                $SVN::Node::dir.$SVN::Node::unknown.
2573                                $SVN::Auth::SSL::CNMISMATCH.
2574                                $SVN::Auth::SSL::NOTYETVALID.
2575                                $SVN::Auth::SSL::EXPIRED.
2576                                $SVN::Auth::SSL::UNKNOWNCA.
2577                                $SVN::Auth::SSL::OTHER;
2578}
2579
2580package SVN::Git::Fetcher;
2581use vars qw/@ISA/;
2582use strict;
2583use warnings;
2584use Carp qw/croak/;
2585use IO::File qw//;
2586use Git qw/command command_oneline command_noisy
2587           command_output_pipe command_input_pipe command_close_pipe/;
2588
2589# file baton members: path, mode_a, mode_b, pool, fh, blob, base
2590sub new {
2591        my ($class, $git_svn) = @_;
2592        my $self = SVN::Delta::Editor->new;
2593        bless $self, $class;
2594        $self->{c} = $git_svn->{c} if exists $git_svn->{c};
2595        $self->{q} = $git_svn->{q};
2596        $self->{empty} = {};
2597        $self->{dir_prop} = {};
2598        $self->{file_prop} = {};
2599        $self->{absent_dir} = {};
2600        $self->{absent_file} = {};
2601        ($self->{gui}, $self->{ctx}) = command_input_pipe(
2602                                             qw/update-index -z --index-info/);
2603        require Digest::MD5;
2604        $self;
2605}
2606
2607sub open_root {
2608        { path => '' };
2609}
2610
2611sub open_directory {
2612        my ($self, $path, $pb, $rev) = @_;
2613        { path => $path };
2614}
2615
2616sub delete_entry {
2617        my ($self, $path, $rev, $pb) = @_;
2618        my $t = process_rm($self->{gui}, $self->{c}, $path, $self->{q});
2619        $self->{empty}->{$path} = 0 if $t == $SVN::Node::dir;
2620        undef;
2621}
2622
2623sub open_file {
2624        my ($self, $path, $pb, $rev) = @_;
2625        my ($mode, $blob) = (command('ls-tree', $self->{c}, '--',$path)
2626                             =~ /^(\d{6}) blob ([a-f\d]{40})\t/);
2627        unless (defined $mode && defined $blob) {
2628                die "$path was not found in commit $self->{c} (r$rev)\n";
2629        }
2630        { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob,
2631          pool => SVN::Pool->new, action => 'M' };
2632}
2633
2634sub add_file {
2635        my ($self, $path, $pb, $cp_path, $cp_rev) = @_;
2636        my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
2637        delete $self->{empty}->{$dir};
2638        { path => $path, mode_a => 100644, mode_b => 100644,
2639          pool => SVN::Pool->new, action => 'A' };
2640}
2641
2642sub add_directory {
2643        my ($self, $path, $cp_path, $cp_rev) = @_;
2644        my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
2645        delete $self->{empty}->{$dir};
2646        $self->{empty}->{$path} = 1;
2647        { path => $path };
2648}
2649
2650sub change_dir_prop {
2651        my ($self, $db, $prop, $value) = @_;
2652        $self->{dir_prop}->{$db->{path}} ||= {};
2653        $self->{dir_prop}->{$db->{path}}->{$prop} = $value;
2654        undef;
2655}
2656
2657sub absent_directory {
2658        my ($self, $path, $pb) = @_;
2659        $self->{absent_dir}->{$pb->{path}} ||= [];
2660        push @{$self->{absent_dir}->{$pb->{path}}}, $path;
2661        undef;
2662}
2663
2664sub absent_file {
2665        my ($self, $path, $pb) = @_;
2666        $self->{absent_file}->{$pb->{path}} ||= [];
2667        push @{$self->{absent_file}->{$pb->{path}}}, $path;
2668        undef;
2669}
2670
2671sub change_file_prop {
2672        my ($self, $fb, $prop, $value) = @_;
2673        if ($prop eq 'svn:executable') {
2674                if ($fb->{mode_b} != 120000) {
2675                        $fb->{mode_b} = defined $value ? 100755 : 100644;
2676                }
2677        } elsif ($prop eq 'svn:special') {
2678                $fb->{mode_b} = defined $value ? 120000 : 100644;
2679        } else {
2680                $self->{file_prop}->{$fb->{path}} ||= {};
2681                $self->{file_prop}->{$fb->{path}}->{$prop} = $value;
2682        }
2683        undef;
2684}
2685
2686sub apply_textdelta {
2687        my ($self, $fb, $exp) = @_;
2688        my $fh = IO::File->new_tmpfile;
2689        $fh->autoflush(1);
2690        # $fh gets auto-closed() by SVN::TxDelta::apply(),
2691        # (but $base does not,) so dup() it for reading in close_file
2692        open my $dup, '<&', $fh or croak $!;
2693        my $base = IO::File->new_tmpfile;
2694        $base->autoflush(1);
2695        if ($fb->{blob}) {
2696                defined (my $pid = fork) or croak $!;
2697                if (!$pid) {
2698                        open STDOUT, '>&', $base or croak $!;
2699                        print STDOUT 'link ' if ($fb->{mode_a} == 120000);
2700                        exec qw/git-cat-file blob/, $fb->{blob} or croak $!;
2701                }
2702                waitpid $pid, 0;
2703                croak $? if $?;
2704
2705                if (defined $exp) {
2706                        seek $base, 0, 0 or croak $!;
2707                        my $md5 = Digest::MD5->new;
2708                        $md5->addfile($base);
2709                        my $got = $md5->hexdigest;
2710                        die "Checksum mismatch: $fb->{path} $fb->{blob}\n",
2711                            "expected: $exp\n",
2712                            "     got: $got\n" if ($got ne $exp);
2713                }
2714        }
2715        seek $base, 0, 0 or croak $!;
2716        $fb->{fh} = $dup;
2717        $fb->{base} = $base;
2718        [ SVN::TxDelta::apply($base, $fh, undef, $fb->{path}, $fb->{pool}) ];
2719}
2720
2721sub close_file {
2722        my ($self, $fb, $exp) = @_;
2723        my $hash;
2724        my $path = $fb->{path};
2725        if (my $fh = $fb->{fh}) {
2726                seek($fh, 0, 0) or croak $!;
2727                my $md5 = Digest::MD5->new;
2728                $md5->addfile($fh);
2729                my $got = $md5->hexdigest;
2730                die "Checksum mismatch: $path\n",
2731                    "expected: $exp\n    got: $got\n" if ($got ne $exp);
2732                seek($fh, 0, 0) or croak $!;
2733                if ($fb->{mode_b} == 120000) {
2734                        read($fh, my $buf, 5) == 5 or croak $!;
2735                        $buf eq 'link ' or die "$path has mode 120000",
2736                                               "but is not a link\n";
2737                }
2738                defined(my $pid = open my $out,'-|') or die "Can't fork: $!\n";
2739                if (!$pid) {
2740                        open STDIN, '<&', $fh or croak $!;
2741                        exec qw/git-hash-object -w --stdin/ or croak $!;
2742                }
2743                chomp($hash = do { local $/; <$out> });
2744                close $out or croak $!;
2745                close $fh or croak $!;
2746                $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n";
2747                close $fb->{base} or croak $!;
2748        } else {
2749                $hash = $fb->{blob} or die "no blob information\n";
2750        }
2751        $fb->{pool}->clear;
2752        my $gui = $self->{gui};
2753        print $gui "$fb->{mode_b} $hash\t$path\0" or croak $!;
2754        print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $self->{q};
2755        undef;
2756}
2757
2758sub abort_edit {
2759        my $self = shift;
2760        eval { command_close_pipe($self->{gui}, $self->{ctx}) };
2761        $self->SUPER::abort_edit(@_);
2762}
2763
2764sub close_edit {
2765        my $self = shift;
2766        command_close_pipe($self->{gui}, $self->{ctx});
2767        $self->{git_commit_ok} = 1;
2768        $self->SUPER::close_edit(@_);
2769}
2770
2771package SVN::Git::Editor;
2772use vars qw/@ISA/;
2773use strict;
2774use warnings;
2775use Carp qw/croak/;
2776use IO::File;
2777use Git qw/command command_oneline command_noisy
2778           command_output_pipe command_input_pipe command_close_pipe/;
2779
2780sub new {
2781        my $class = shift;
2782        my $git_svn = shift;
2783        my $self = SVN::Delta::Editor->new(@_);
2784        bless $self, $class;
2785        foreach (qw/svn_path c r ra /) {
2786                die "$_ required!\n" unless (defined $git_svn->{$_});
2787                $self->{$_} = $git_svn->{$_};
2788        }
2789        $self->{pool} = SVN::Pool->new;
2790        $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) };
2791        $self->{rm} = { };
2792        require Digest::MD5;
2793        return $self;
2794}
2795
2796sub split_path {
2797        return ($_[0] =~ m#^(.*?)/?([^/]+)$#);
2798}
2799
2800sub repo_path {
2801        (defined $_[1] && length $_[1]) ? $_[1] : ''
2802}
2803
2804sub url_path {
2805        my ($self, $path) = @_;
2806        $self->{ra}->{url} . '/' . $self->repo_path($path);
2807}
2808
2809sub rmdirs {
2810        my ($self, $q) = @_;
2811        my $rm = $self->{rm};
2812        delete $rm->{''}; # we never delete the url we're tracking
2813        return unless %$rm;
2814
2815        foreach (keys %$rm) {
2816                my @d = split m#/#, $_;
2817                my $c = shift @d;
2818                $rm->{$c} = 1;
2819                while (@d) {
2820                        $c .= '/' . shift @d;
2821                        $rm->{$c} = 1;
2822                }
2823        }
2824        delete $rm->{$self->{svn_path}};
2825        delete $rm->{''}; # we never delete the url we're tracking
2826        return unless %$rm;
2827
2828        my ($fh, $ctx) = command_output_pipe(
2829                                   qw/ls-tree --name-only -r -z/, $self->{c});
2830        local $/ = "\0";
2831        while (<$fh>) {
2832                chomp;
2833                my @dn = split m#/#, $_;
2834                while (pop @dn) {
2835                        delete $rm->{join '/', @dn};
2836                }
2837                unless (%$rm) {
2838                        eval { command_close_pipe($fh) };
2839                        return;
2840                }
2841        }
2842        command_close_pipe($fh, $ctx);
2843
2844        my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat});
2845        foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {
2846                $self->close_directory($bat->{$d}, $p);
2847                my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#);
2848                print "\tD+\t/$d/\n" unless $q;
2849                $self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p);
2850                delete $bat->{$d};
2851        }
2852}
2853
2854sub open_or_add_dir {
2855        my ($self, $full_path, $baton) = @_;
2856        my $p = SVN::Pool->new;
2857        my $t = $self->{ra}->check_path($full_path, $self->{r}, $p);
2858        $p->clear;
2859        if ($t == $SVN::Node::none) {
2860                return $self->add_directory($full_path, $baton,
2861                                                undef, -1, $self->{pool});
2862        } elsif ($t == $SVN::Node::dir) {
2863                return $self->open_directory($full_path, $baton,
2864                                                $self->{r}, $self->{pool});
2865        }
2866        print STDERR "$full_path already exists in repository at ",
2867                "r$self->{r} and it is not a directory (",
2868                ($t == $SVN::Node::file ? 'file' : 'unknown'),"/$t)\n";
2869        exit 1;
2870}
2871
2872sub ensure_path {
2873        my ($self, $path) = @_;
2874        my $bat = $self->{bat};
2875        $path = $self->repo_path($path);
2876        return $bat->{''} unless (length $path);
2877        my @p = split m#/+#, $path;
2878        my $c = shift @p;
2879        $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''});
2880        while (@p) {
2881                my $c0 = $c;
2882                $c .= '/' . shift @p;
2883                $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0});
2884        }
2885        return $bat->{$c};
2886}
2887
2888sub A {
2889        my ($self, $m, $q) = @_;
2890        my ($dir, $file) = split_path($m->{file_b});
2891        my $pbat = $self->ensure_path($dir);
2892        my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
2893                                        undef, -1);
2894        print "\tA\t$m->{file_b}\n" unless $q;
2895        $self->chg_file($fbat, $m);
2896        $self->close_file($fbat,undef,$self->{pool});
2897}
2898
2899sub C {
2900        my ($self, $m, $q) = @_;
2901        my ($dir, $file) = split_path($m->{file_b});
2902        my $pbat = $self->ensure_path($dir);
2903        my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
2904                                $self->url_path($m->{file_a}), $self->{r});
2905        print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $q;
2906        $self->chg_file($fbat, $m);
2907        $self->close_file($fbat,undef,$self->{pool});
2908}
2909
2910sub delete_entry {
2911        my ($self, $path, $pbat) = @_;
2912        my $rpath = $self->repo_path($path);
2913        my ($dir, $file) = split_path($rpath);
2914        $self->{rm}->{$dir} = 1;
2915        $self->SUPER::delete_entry($rpath, $self->{r}, $pbat, $self->{pool});
2916}
2917
2918sub R {
2919        my ($self, $m, $q) = @_;
2920        my ($dir, $file) = split_path($m->{file_b});
2921        my $pbat = $self->ensure_path($dir);
2922        my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
2923                                $self->url_path($m->{file_a}), $self->{r});
2924        print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $q;
2925        $self->chg_file($fbat, $m);
2926        $self->close_file($fbat,undef,$self->{pool});
2927
2928        ($dir, $file) = split_path($m->{file_a});
2929        $pbat = $self->ensure_path($dir);
2930        $self->delete_entry($m->{file_a}, $pbat);
2931}
2932
2933sub M {
2934        my ($self, $m, $q) = @_;
2935        my ($dir, $file) = split_path($m->{file_b});
2936        my $pbat = $self->ensure_path($dir);
2937        my $fbat = $self->open_file($self->repo_path($m->{file_b}),
2938                                $pbat,$self->{r},$self->{pool});
2939        print "\t$m->{chg}\t$m->{file_b}\n" unless $q;
2940        $self->chg_file($fbat, $m);
2941        $self->close_file($fbat,undef,$self->{pool});
2942}
2943
2944sub T { shift->M(@_) }
2945
2946sub change_file_prop {
2947        my ($self, $fbat, $pname, $pval) = @_;
2948        $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool});
2949}
2950
2951sub chg_file {
2952        my ($self, $fbat, $m) = @_;
2953        if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) {
2954                $self->change_file_prop($fbat,'svn:executable','*');
2955        } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
2956                $self->change_file_prop($fbat,'svn:executable',undef);
2957        }
2958        my $fh = IO::File->new_tmpfile or croak $!;
2959        if ($m->{mode_b} =~ /^120/) {
2960                print $fh 'link ' or croak $!;
2961                $self->change_file_prop($fbat,'svn:special','*');
2962        } elsif ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) {
2963                $self->change_file_prop($fbat,'svn:special',undef);
2964        }
2965        defined(my $pid = fork) or croak $!;
2966        if (!$pid) {
2967                open STDOUT, '>&', $fh or croak $!;
2968                exec qw/git-cat-file blob/, $m->{sha1_b} or croak $!;
2969        }
2970        waitpid $pid, 0;
2971        croak $? if $?;
2972        $fh->flush == 0 or croak $!;
2973        seek $fh, 0, 0 or croak $!;
2974
2975        my $md5 = Digest::MD5->new;
2976        $md5->addfile($fh) or croak $!;
2977        seek $fh, 0, 0 or croak $!;
2978
2979        my $exp = $md5->hexdigest;
2980        my $pool = SVN::Pool->new;
2981        my $atd = $self->apply_textdelta($fbat, undef, $pool);
2982        my $got = SVN::TxDelta::send_stream($fh, @$atd, $pool);
2983        die "Checksum mismatch\nexpected: $exp\ngot: $got\n" if ($got ne $exp);
2984        $pool->clear;
2985
2986        close $fh or croak $!;
2987}
2988
2989sub D {
2990        my ($self, $m, $q) = @_;
2991        my ($dir, $file) = split_path($m->{file_b});
2992        my $pbat = $self->ensure_path($dir);
2993        print "\tD\t$m->{file_b}\n" unless $q;
2994        $self->delete_entry($m->{file_b}, $pbat);
2995}
2996
2997sub close_edit {
2998        my ($self) = @_;
2999        my ($p,$bat) = ($self->{pool}, $self->{bat});
3000        foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {
3001                $self->close_directory($bat->{$_}, $p);
3002        }
3003        $self->SUPER::close_edit($p);
3004        $p->clear;
3005}
3006
3007sub abort_edit {
3008        my ($self) = @_;
3009        $self->SUPER::abort_edit($self->{pool});
3010        $self->{pool}->clear;
3011}
3012
3013__END__
3014
3015Data structures:
3016
3017$log_msg hashref as returned by libsvn_log_entry()
3018{
3019        msg => 'whitespace-formatted log entry
3020',                                              # trailing newline is preserved
3021        revision => '8',                        # integer
3022        date => '2004-02-24T17:01:44.108345Z',  # commit date
3023        author => 'committer name'
3024};
3025
3026@mods = array of diff-index line hashes, each element represents one line
3027        of diff-index output
3028
3029diff-index line ($m hash)
3030{
3031        mode_a => first column of diff-index output, no leading ':',
3032        mode_b => second column of diff-index output,
3033        sha1_b => sha1sum of the final blob,
3034        chg => change type [MCRADT],
3035        file_a => original file name of a file (iff chg is 'C' or 'R')
3036        file_b => new/current file name of a file (any chg)
3037}
3038;
3039
3040# retval of read_url_paths{,_all}();
3041$l_map = {
3042        # repository root url
3043        'https://svn.musicpd.org' => {
3044                # repository path               # GIT_SVN_ID
3045                'mpd/trunk'             =>      'trunk',
3046                'mpd/tags/0.11.5'       =>      'tags/0.11.5',
3047        },
3048}
3049
3050Notes:
3051        I don't trust the each() function on unless I created %hash myself
3052        because the internal iterator may not have started at base.