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