git-svn: add 'log' command, a facsimile of basic `svn log'
[gitweb.git] / contrib / git-svn / git-svn.perl
index de13a96b8a66c97fdc2e5822986ea48285fa9b6d..03416aeec157e1e364b92fde434ea922ccb16e8e 100755 (executable)
@@ -6,14 +6,16 @@
 use vars qw/   $AUTHOR $VERSION
                $SVN_URL $SVN_INFO $SVN_WC $SVN_UUID
                $GIT_SVN_INDEX $GIT_SVN
-               $GIT_DIR $REV_DIR/;
+               $GIT_DIR $REV_DIR $GIT_SVN_DIR/;
 $AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
-$VERSION = '1.0.0';
+$VERSION = '1.1.0-pre';
 
 use Cwd qw/abs_path/;
 $GIT_DIR = abs_path($ENV{GIT_DIR} || '.git');
 $ENV{GIT_DIR} = $GIT_DIR;
 
+my $LC_ALL = $ENV{LC_ALL};
+my $TZ = $ENV{TZ};
 # make sure the svn binary gives consistent output between locales and TZs:
 $ENV{TZ} = 'UTC';
 $ENV{LC_ALL} = 'C';
 use IO::File qw//;
 use File::Basename qw/dirname basename/;
 use File::Path qw/mkpath/;
-use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
+use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/;
 use File::Spec qw//;
 use POSIX qw/strftime/;
 my $sha1 = qr/[a-f\d]{40}/;
 my $sha1_short = qr/[a-f\d]{4,40}/;
 my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
-       $_find_copies_harder, $_l, $_version, $_upgrade, $_authors);
-my (@_branch_from, %tree_map, %users);
-my $_svn_co_url_revs;
+       $_find_copies_harder, $_l, $_cp_similarity,
+       $_repack, $_repack_nr, $_repack_flags,
+       $_template, $_shared, $_no_default_regex, $_no_graft_copy,
+       $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit,
+       $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m);
+my (@_branch_from, %tree_map, %users, %rusers);
+my ($_svn_co_url_revs, $_svn_pg_peg_revs);
+my @repo_path_split_cache;
 
 my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext,
                'branch|b=s' => \@_branch_from,
-               'authors-file|A=s' => \$_authors );
+               'branch-all-refs|B' => \$_branch_all_refs,
+               'authors-file|A=s' => \$_authors,
+               'repack:i' => \$_repack,
+               'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
+
+my ($_trunk, $_tags, $_branches);
+my %multi_opts = ( 'trunk|T=s' => \$_trunk,
+               'tags|t=s' => \$_tags,
+               'branches|b=s' => \$_branches );
+my %init_opts = ( 'template=s' => \$_template, 'shared' => \$_shared );
+
+# yes, 'native' sets "\n".  Patches to fix this for non-*nix systems welcome:
+my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" );
+
 my %cmd = (
        fetch => [ \&fetch, "Download new revisions from SVN",
                        { 'revision|r=s' => \$_revision, %fc_opts } ],
        init => [ \&init, "Initialize a repo for tracking" .
-                         " (requires URL argument)", { } ],
+                         " (requires URL argument)",
+                         \%init_opts ],
        commit => [ \&commit, "Commit git revisions to SVN",
                        {       'stdin|' => \$_stdin,
                                'edit|e' => \$_edit,
                                'rmdir' => \$_rmdir,
                                'find-copies-harder' => \$_find_copies_harder,
                                'l=i' => \$_l,
+                               'copy-similarity|C=i'=> \$_cp_similarity,
                                %fc_opts,
                        } ],
        'show-ignore' => [ \&show_ignore, "Show svn:ignore listings", { } ],
        rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)",
                        { 'no-ignore-externals' => \$_no_ignore_ext,
                          'upgrade' => \$_upgrade } ],
+       'graft-branches' => [ \&graft_branches,
+                       'Detect merges/branches from already imported history',
+                       { 'merge-rx|m' => \@_opt_m,
+                         'no-default-regex' => \$_no_default_regex,
+                         'no-graft-copy' => \$_no_graft_copy } ],
+       'multi-init' => [ \&multi_init,
+                       'Initialize multiple trees (like git-svnimport)',
+                       { %multi_opts, %fc_opts } ],
+       'multi-fetch' => [ \&multi_fetch,
+                       'Fetch multiple trees (like git-svnimport)',
+                       \%fc_opts ],
+       'log' => [ \&show_log, 'Show commit logs',
+                       { 'limit=i' => \$_limit,
+                         'revision|r=s' => \$_revision,
+                         'verbose|v' => \$_verbose,
+                         'incremental' => \$_incremental,
+                         'oneline' => \$_oneline,
+                         'show-commit' => \$_show_commit,
+                         'authors-file|A=s' => \$_authors,
+                       } ],
 );
+
 my $cmd;
 for (my $i = 0; $i < @ARGV; $i++) {
        if (defined $cmd{$ARGV[$i]}) {
 
 my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
 
-# convert GetOpt::Long specs for use by git-repo-config
-foreach my $o (keys %opts) {
-       my $v = $opts{$o};
-       my ($key) = ($o =~ /^([a-z\-]+)/);
-       $key =~ s/-//g;
-       my $arg = 'git-repo-config';
-       $arg .= ' --int' if ($o =~ /=i$/);
-       $arg .= ' --bool' if ($o !~ /=[sfi]$/);
-       if (ref $v eq 'ARRAY') {
-               chomp(my @tmp = `$arg --get-all svn.$key`);
-               @$v = @tmp if @tmp;
-       } else {
-               chomp(my $tmp = `$arg --get svn.$key`);
-               if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) {
-                       $$v = $tmp;
-               }
-       }
-}
-
-GetOptions(%opts, 'help|H|h' => \$_help,
-               'version|V' => \$_version,
-               'id|i=s' => \$GIT_SVN) or exit 1;
-
-$GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
-$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index";
-$SVN_URL = undef;
-$REV_DIR = "$GIT_DIR/$GIT_SVN/revs";
-$SVN_WC = "$GIT_DIR/$GIT_SVN/tree";
+read_repo_config(\%opts);
+my $rv = GetOptions(%opts, 'help|H|h' => \$_help,
+                               'version|V' => \$_version,
+                               'id|i=s' => \$GIT_SVN);
+exit 1 if (!$rv && $cmd ne 'log');
 
+set_default_vals();
 usage(0) if $_help;
 version() if $_version;
 usage(1) unless defined $cmd;
+init_vars();
 load_authors() if $_authors;
+load_all_refs() if $_branch_all_refs;
 svn_compat_check();
+migration_check() unless $cmd =~ /^(?:init|multi-init)$/;
 $cmd{$cmd}->[0]->(@ARGV);
 exit 0;
 
@@ -120,7 +144,7 @@ sub usage {
                print $fd '  ',pack('A13',$_),$cmd{$_}->[1],"\n";
                foreach (keys %{$cmd{$_}->[2]}) {
                        # prints out arguments as they should be passed:
-                       my $x = s#=s$## ? '<arg>' : s#=i$## ? '<num>' : '';
+                       my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
                        print $fd ' ' x 17, join(', ', map { length $_ > 1 ?
                                                        "--$_" : "-$_" }
                                                split /\|/,$_)," $x\n";
@@ -161,18 +185,10 @@ sub rebuild {
                croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o;
                my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`);
                next if (!@commit); # skip merges
-               my $id = $commit[$#commit];
-               my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
-                                               \s([a-f\d\-]+)$/x);
-               if (!$rev || !$uuid || !$url) {
-                       # some of the original repositories I made had
-                       # indentifiers like this:
-                       ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)
-                                                       \@([a-f\d\-]+)/x);
-                       if (!$rev || !$uuid) {
-                               croak "Unable to extract revision or UUID from ",
-                                       "$c, $id\n";
-                       }
+               my ($url, $rev, $uuid) = extract_metadata($commit[$#commit]);
+               if (!$rev || !$uuid) {
+                       croak "Unable to extract revision or UUID from ",
+                               "$c, $commit[$#commit]\n";
                }
 
                # if we merged or otherwise started elsewhere, this is
@@ -191,7 +207,7 @@ sub rebuild {
                        $latest = $rev;
                }
                assert_revision_eq_or_unknown($rev, $c);
-               sys('git-update-ref',"$GIT_SVN/revs/$rev",$c);
+               sys('git-update-ref',"svn/$GIT_SVN/revs/$rev",$c);
                $newest_rev = $rev if ($rev > $newest_rev);
        }
        close $rev_list or croak $?;
@@ -207,10 +223,11 @@ sub rebuild {
                push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
                sys(@svn_up,"-r$newest_rev");
                $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
-               git_addremove();
-               exec('git-write-tree');
+               index_changes();
+               exec('git-write-tree') or croak $!;
        }
        waitpid $pid, 0;
+       croak $? if $?;
 
        if ($_upgrade) {
                print STDERR <<"";
@@ -223,8 +240,12 @@ sub rebuild {
 sub init {
        $SVN_URL = shift or die "SVN repository location required " .
                                "as a command-line argument\n";
+       $SVN_URL =~ s!/+$!!; # strip trailing slash
        unless (-d $GIT_DIR) {
-               sys('git-init-db');
+               my @init_db = ('git-init-db');
+               push @init_db, "--template=$_template" if defined $_template;
+               push @init_db, "--shared" if defined $_shared;
+               sys(@init_db);
        }
        setup_git_svn();
 }
@@ -232,7 +253,7 @@ sub init {
 sub fetch {
        my (@parents) = @_;
        check_upgrade_needed();
-       $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
+       $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
        my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL);
        unless ($_revision) {
                $_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD';
@@ -249,29 +270,41 @@ sub fetch {
                chdir $SVN_WC or croak $!;
                read_uuid();
                $last_commit = git_commit($base, @parents);
-               assert_svn_wc_clean($base->{revision}, $last_commit);
+               assert_tree($last_commit);
        } else {
                chdir $SVN_WC or croak $!;
                read_uuid();
-               $last_commit = file_to_s("$REV_DIR/$base->{revision}");
+               eval { $last_commit = file_to_s("$REV_DIR/$base->{revision}") };
+               # looks like a user manually cp'd and svn switch'ed
+               unless ($last_commit) {
+                       sys(qw/svn revert -R ./);
+                       assert_svn_wc_clean($base->{revision});
+                       $last_commit = git_commit($base, @parents);
+                       assert_tree($last_commit);
+               }
        }
        my @svn_up = qw(svn up);
        push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
        my $last = $base;
        while (my $log_msg = next_log_entry($svn_log)) {
-               assert_svn_wc_clean($last->{revision}, $last_commit);
+               assert_tree($last_commit);
                if ($last->{revision} >= $log_msg->{revision}) {
                        croak "Out of order: last >= current: ",
                                "$last->{revision} >= $log_msg->{revision}\n";
                }
+               # Revert is needed for cases like:
+               # https://svn.musicpd.org/Jamming/trunk (r166:167), but
+               # I can't seem to reproduce something like that on a test...
+               sys(qw/svn revert -R ./);
+               assert_svn_wc_clean($last->{revision});
                sys(@svn_up,"-r$log_msg->{revision}");
                $last_commit = git_commit($log_msg, $last_commit, @parents);
                $last = $log_msg;
        }
-       assert_svn_wc_clean($last->{revision}, $last_commit);
        unless (-e "$GIT_DIR/refs/heads/master") {
                sys(qw(git-update-ref refs/heads/master),$last_commit);
        }
+       close $svn_log->{fh};
        return $last;
 }
 
@@ -300,9 +333,16 @@ sub commit {
        }
        chomp @revs;
 
-       fetch();
-       chdir $SVN_WC or croak $!;
+       chdir $SVN_WC or croak "Unable to chdir $SVN_WC: $!\n";
        my $info = svn_info('.');
+       my $fetched = fetch();
+       if ($info->{Revision} != $fetched->{revision}) {
+               print STDERR "There are new revisions that were fetched ",
+                               "and need to be merged (or acknowledged) ",
+                               "before committing.\n";
+               exit 1;
+       }
+       $info = svn_info('.');
        read_uuid($info);
        my $svn_current_rev =  $info->{'Last Changed Rev'};
        foreach my $c (@revs) {
@@ -314,7 +354,6 @@ sub commit {
                $svn_current_rev = svn_commit_tree($svn_current_rev, $c);
        }
        print "Done committing ",scalar @revs," revisions to SVN\n";
-
 }
 
 sub show_ignore {
@@ -324,12 +363,12 @@ sub show_ignore {
        chomp(my @excludes = (<$fh>));
        close $fh or croak $!;
 
-       $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
+       $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
        chdir $SVN_WC or croak $!;
        my %ign;
        File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){
                s#^\./##;
-               @{$ign{$_}} = safe_qx(qw(svn propget svn:ignore),$_);
+               @{$ign{$_}} = svn_propget_base('svn:ignore', $_);
                }}, no_chdir=>1},'.');
 
        print "\n# /\n";
@@ -341,14 +380,402 @@ sub show_ignore {
        }
 }
 
+sub graft_branches {
+       my $gr_file = "$GIT_DIR/info/grafts";
+       my ($grafts, $comments) = read_grafts($gr_file);
+       my $gr_sha1;
+
+       if (%$grafts) {
+               # temporarily disable our grafts file to make this idempotent
+               chomp($gr_sha1 = safe_qx(qw/git-hash-object -w/,$gr_file));
+               rename $gr_file, "$gr_file~$gr_sha1" or croak $!;
+       }
+
+       my $l_map = read_url_paths();
+       my @re = map { qr/$_/is } @_opt_m if @_opt_m;
+       unless ($_no_default_regex) {
+               push @re, (     qr/\b(?:merge|merging|merged)\s+(\S.+)/is,
+                               qr/\b(?:from|of)\s+(\S.+)/is );
+       }
+       foreach my $u (keys %$l_map) {
+               if (@re) {
+                       foreach my $p (keys %{$l_map->{$u}}) {
+                               graft_merge_msg($grafts,$l_map,$u,$p);
+                       }
+               }
+               graft_file_copy($grafts,$l_map,$u) unless $_no_graft_copy;
+       }
+
+       write_grafts($grafts, $comments, $gr_file);
+       unlink "$gr_file~$gr_sha1" if $gr_sha1;
+}
+
+sub multi_init {
+       my $url = shift;
+       $_trunk ||= 'trunk';
+       $_trunk =~ s#/+$##;
+       $url =~ s#/+$## if $url;
+       if ($_trunk !~ m#^[a-z\+]+://#) {
+               $_trunk = '/' . $_trunk if ($_trunk !~ m#^/#);
+               unless ($url) {
+                       print STDERR "E: '$_trunk' is not a complete URL ",
+                               "and a separate URL is not specified\n";
+                       exit 1;
+               }
+               $_trunk = $url . $_trunk;
+       }
+       if ($GIT_SVN eq 'git-svn') {
+               print "GIT_SVN_ID set to 'trunk' for $_trunk\n";
+               $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
+       }
+       init_vars();
+       init($_trunk);
+       complete_url_ls_init($url, $_branches, '--branches/-b', '');
+       complete_url_ls_init($url, $_tags, '--tags/-t', 'tags/');
+}
+
+sub multi_fetch {
+       # try to do trunk first, since branches/tags
+       # may be descended from it.
+       if (-d "$GIT_DIR/svn/trunk") {
+               print "Fetching trunk\n";
+               defined(my $pid = fork) or croak $!;
+               if (!$pid) {
+                       $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
+                       init_vars();
+                       fetch(@_);
+                       exit 0;
+               }
+               waitpid $pid, 0;
+               croak $? if $?;
+       }
+       rec_fetch('', "$GIT_DIR/svn", @_);
+}
+
+sub show_log {
+       my (@args) = @_;
+       my ($r_min, $r_max);
+       my $r_last = -1; # prevent dupes
+       rload_authors() if $_authors;
+       if (defined $TZ) {
+               $ENV{TZ} = $TZ;
+       } else {
+               delete $ENV{TZ};
+       }
+       if (defined $_revision) {
+               if ($_revision =~ /^(\d+):(\d+)$/) {
+                       ($r_min, $r_max) = ($1, $2);
+               } elsif ($_revision =~ /^\d+$/) {
+                       $r_min = $r_max = $_revision;
+               } else {
+                       print STDERR "-r$_revision is not supported, use ",
+                               "standard \'git log\' arguments instead\n";
+                       exit 1;
+               }
+       }
+
+       my $pid = open(my $log,'-|');
+       defined $pid or croak $!;
+       if (!$pid) {
+               my @rl = (qw/git-log --abbrev-commit --pretty=raw
+                               --default/, "remotes/$GIT_SVN");
+               push @rl, '--raw' if $_verbose;
+               exec(@rl, @args) or croak $!;
+       }
+       setup_pager();
+       my (@k, $c, $d);
+       while (<$log>) {
+               if (/^commit ($sha1_short)/o) {
+                       my $cmt = $1;
+                       if ($c && defined $c->{r} && $c->{r} != $r_last) {
+                               $r_last = $c->{r};
+                               process_commit($c, $r_min, $r_max, \@k) or
+                                                               goto out;
+                       }
+                       $d = undef;
+                       $c = { c => $cmt };
+               } elsif (/^author (.+) (\d+) ([\-\+]?\d+)$/) {
+                       get_author_info($c, $1, $2, $3);
+               } elsif (/^(?:tree|parent|committer) /) {
+                       # ignore
+               } elsif (/^:\d{6} \d{6} $sha1_short/o) {
+                       push @{$c->{raw}}, $_;
+               } elsif (/^diff /) {
+                       $d = 1;
+                       push @{$c->{diff}}, $_;
+               } elsif ($d) {
+                       push @{$c->{diff}}, $_;
+               } elsif (/^    (git-svn-id:.+)$/) {
+                       my ($url, $rev, $uuid) = extract_metadata($1);
+                       $c->{r} = $rev;
+               } elsif (s/^    //) {
+                       push @{$c->{l}}, $_;
+               }
+       }
+       if ($c && defined $c->{r} && $c->{r} != $r_last) {
+               $r_last = $c->{r};
+               process_commit($c, $r_min, $r_max, \@k);
+       }
+       if (@k) {
+               my $swap = $r_max;
+               $r_max = $r_min;
+               $r_min = $swap;
+               process_commit($_, $r_min, $r_max) foreach reverse @k;
+       }
+out:
+       close $log;
+       print '-' x72,"\n" unless $_incremental || $_oneline;
+}
+
 ########################### utility functions #########################
 
+sub rec_fetch {
+       my ($pfx, $p, @args) = @_;
+       my @dir;
+       foreach (sort <$p/*>) {
+               if (-r "$_/info/url") {
+                       $pfx .= '/' if $pfx && $pfx !~ m!/$!;
+                       my $id = $pfx . basename $_;
+                       next if $id eq 'trunk';
+                       print "Fetching $id\n";
+                       defined(my $pid = fork) or croak $!;
+                       if (!$pid) {
+                               $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
+                               init_vars();
+                               fetch(@args);
+                               exit 0;
+                       }
+                       waitpid $pid, 0;
+                       croak $? if $?;
+               } elsif (-d $_) {
+                       push @dir, $_;
+               }
+       }
+       foreach (@dir) {
+               my $x = $_;
+               $x =~ s!^\Q$GIT_DIR\E/svn/!!;
+               rec_fetch($x, $_);
+       }
+}
+
+sub complete_url_ls_init {
+       my ($url, $var, $switch, $pfx) = @_;
+       unless ($var) {
+               print STDERR "W: $switch not specified\n";
+               return;
+       }
+       $var =~ s#/+$##;
+       if ($var !~ m#^[a-z\+]+://#) {
+               $var = '/' . $var if ($var !~ m#^/#);
+               unless ($url) {
+                       print STDERR "E: '$var' is not a complete URL ",
+                               "and a separate URL is not specified\n";
+                       exit 1;
+               }
+               $var = $url . $var;
+       }
+       chomp(my @ls = safe_qx(qw/svn ls --non-interactive/, $var));
+       my $old = $GIT_SVN;
+       defined(my $pid = fork) or croak $!;
+       if (!$pid) {
+               foreach my $u (map { "$var/$_" } (grep m!/$!, @ls)) {
+                       $u =~ s#/+$##;
+                       if ($u !~ m!\Q$var\E/(.+)$!) {
+                               print STDERR "W: Unrecognized URL: $u\n";
+                               die "This should never happen\n";
+                       }
+                       my $id = $pfx.$1;
+                       print "init $u => $id\n";
+                       $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
+                       init_vars();
+                       init($u);
+               }
+               exit 0;
+       }
+       waitpid $pid, 0;
+       croak $? if $?;
+}
+
+sub common_prefix {
+       my $paths = shift;
+       my %common;
+       foreach (@$paths) {
+               my @tmp = split m#/#, $_;
+               my $p = '';
+               while (my $x = shift @tmp) {
+                       $p .= "/$x";
+                       $common{$p} ||= 0;
+                       $common{$p}++;
+               }
+       }
+       foreach (sort {length $b <=> length $a} keys %common) {
+               if ($common{$_} == @$paths) {
+                       return $_;
+               }
+       }
+       return '';
+}
+
+# this isn't funky-filename safe, but good enough for now...
+sub graft_file_copy {
+       my ($grafts, $l_map, $u) = @_;
+       my $paths = $l_map->{$u};
+       my $pfx = common_prefix([keys %$paths]);
+
+       my $pid = open my $fh, '-|';
+       defined $pid or croak $!;
+       unless ($pid) {
+               exec(qw/svn log -v/, $u.$pfx) or croak $!;
+       }
+       my ($r, $mp) = (undef, undef);
+       while (<$fh>) {
+               chomp;
+               if (/^\-{72}$/) {
+                       $mp = $r = undef;
+               } elsif (/^r(\d+) \| /) {
+                       $r = $1 unless defined $r;
+               } elsif (/^Changed paths:/) {
+                       $mp = 1;
+               } elsif ($mp && m#^   [AR] /(\S.*?) \(from /(\S+?):(\d+)\)$#) {
+                       my $dbg = "r$r | $_";
+                       my ($p1, $p0, $r0) = ($1, $2, $3);
+                       my $c;
+                       foreach my $x (keys %$paths) {
+                               next unless ($p1 =~ /^\Q$x\E/);
+                               my $i = $paths->{$x};
+                               my $f = "$GIT_DIR/svn/$i/revs/$r";
+                               unless (-r $f) {
+                                       print STDERR "r$r of $i not imported,",
+                                                               " $dbg\n";
+                                       next;
+                               }
+                               $c = file_to_s($f);
+                       }
+                       next unless $c;
+                       foreach my $x (keys %$paths) {
+                               next unless ($p0 =~ /^\Q$x\E/);
+                               my $i = $paths->{$x};
+                               my $f = "$GIT_DIR/svn/$i/revs/$r0";
+                               while ($r0 && !-r $f) {
+                                       # could be an older revision, too...
+                                       $r0--;
+                                       $f = "$GIT_DIR/svn/$i/revs/$r0";
+                               }
+                               unless (-r $f) {
+                                       print STDERR "r$r0 of $i not imported,",
+                                                               " $dbg\n";
+                                       next;
+                               }
+                               my $r1 = file_to_s($f);
+                               $grafts->{$c}->{$r1} = 1;
+                       }
+               }
+       }
+}
+
+sub process_merge_msg_matches {
+       my ($grafts, $l_map, $u, $p, $c, @matches) = @_;
+       my (@strong, @weak);
+       foreach (@matches) {
+               # merging with ourselves is not interesting
+               next if $_ eq $p;
+               if ($l_map->{$u}->{$_}) {
+                       push @strong, $_;
+               } else {
+                       push @weak, $_;
+               }
+       }
+       foreach my $w (@weak) {
+               last if @strong;
+               # no exact match, use branch name as regexp.
+               my $re = qr/\Q$w\E/i;
+               foreach (keys %{$l_map->{$u}}) {
+                       if (/$re/) {
+                               push @strong, $_;
+                               last;
+                       }
+               }
+               last if @strong;
+               $w = basename($w);
+               $re = qr/\Q$w\E/i;
+               foreach (keys %{$l_map->{$u}}) {
+                       if (/$re/) {
+                               push @strong, $_;
+                               last;
+                       }
+               }
+       }
+       my ($rev) = ($c->{m} =~ /^git-svn-id:\s(?:\S+?)\@(\d+)
+                                       \s(?:[a-f\d\-]+)$/xsm);
+       unless (defined $rev) {
+               ($rev) = ($c->{m} =~/^git-svn-id:\s(\d+)
+                                       \@(?:[a-f\d\-]+)/xsm);
+               return unless defined $rev;
+       }
+       foreach my $m (@strong) {
+               my ($r0, $s0) = find_rev_before($rev, $m);
+               $grafts->{$c->{c}}->{$s0} = 1 if defined $s0;
+       }
+}
+
+sub graft_merge_msg {
+       my ($grafts, $l_map, $u, $p, @re) = @_;
+
+       my $x = $l_map->{$u}->{$p};
+       my $rl = rev_list_raw($x);
+       while (my $c = next_rev_list_entry($rl)) {
+               foreach my $re (@re) {
+                       my (@br) = ($c->{m} =~ /$re/g);
+                       next unless @br;
+                       process_merge_msg_matches($grafts,$l_map,$u,$p,$c,@br);
+               }
+       }
+}
+
 sub read_uuid {
        return if $SVN_UUID;
        my $info = shift || svn_info('.');
        $SVN_UUID = $info->{'Repository UUID'} or
                                        croak "Repository UUID unreadable\n";
-       s_to_file($SVN_UUID,"$GIT_DIR/$GIT_SVN/info/uuid");
+       s_to_file($SVN_UUID,"$GIT_SVN_DIR/info/uuid");
+}
+
+sub quiet_run {
+       my $pid = fork;
+       defined $pid or croak $!;
+       if (!$pid) {
+               open my $null, '>', '/dev/null' or croak $!;
+               open STDERR, '>&', $null or croak $!;
+               open STDOUT, '>&', $null or croak $!;
+               exec @_ or croak $!;
+       }
+       waitpid $pid, 0;
+       return $?;
+}
+
+sub repo_path_split {
+       my $full_url = shift;
+       $full_url =~ s#/+$##;
+
+       foreach (@repo_path_split_cache) {
+               if ($full_url =~ s#$_##) {
+                       my $u = $1;
+                       $full_url =~ s#^/+##;
+                       return ($u, $full_url);
+               }
+       }
+
+       my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i);
+       $path =~ s#^/+##;
+       my @paths = split(m#/+#, $path);
+
+       while (quiet_run(qw/svn ls --non-interactive/, $url)) {
+               my $n = shift @paths || last;
+               $url .= "/$n";
+       }
+       push @repo_path_split_cache, qr/^(\Q$url\E)/;
+       $path = join('/',@paths);
+       return ($url, $path);
 }
 
 sub setup_git_svn {
@@ -356,24 +783,25 @@ sub setup_git_svn {
        unless (-d $GIT_DIR) {
                croak "GIT_DIR=$GIT_DIR does not exist!\n";
        }
-       mkpath(["$GIT_DIR/$GIT_SVN"]);
-       mkpath(["$GIT_DIR/$GIT_SVN/info"]);
+       mkpath([$GIT_SVN_DIR]);
+       mkpath(["$GIT_SVN_DIR/info"]);
        mkpath([$REV_DIR]);
-       s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url");
+       s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url");
 
-       open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!;
+       open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!;
        print $fd '.svn',"\n";
        close $fd or croak $!;
+       my ($url, $path) = repo_path_split($SVN_URL);
+       s_to_file($url, "$GIT_SVN_DIR/info/repo_url");
+       s_to_file($path, "$GIT_SVN_DIR/info/repo_path");
 }
 
 sub assert_svn_wc_clean {
-       my ($svn_rev, $treeish) = @_;
+       my ($svn_rev) = @_;
        croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
-       croak "$treeish is not a sha1!\n" unless ($treeish =~ /^$sha1$/o);
        my $lcr = svn_info('.')->{'Last Changed Rev'};
        if ($svn_rev != $lcr) {
                print STDERR "Checking for copy-tree ... ";
-               # use
                my @diff = grep(/^Index: /,(safe_qx(qw(svn diff),
                                                "-r$lcr:$svn_rev")));
                if (@diff) {
@@ -389,7 +817,6 @@ sub assert_svn_wc_clean {
                print STDERR $_ foreach @status;
                croak;
        }
-       assert_tree($treeish);
 }
 
 sub assert_tree {
@@ -416,7 +843,7 @@ sub assert_tree {
                unlink $tmpindex or croak $!;
        }
        $ENV{GIT_INDEX_FILE} = $tmpindex;
-       git_addremove();
+       index_changes(1);
        chomp(my $tree = `git-write-tree`);
        if ($old_index) {
                $ENV{GIT_INDEX_FILE} = $old_index;
@@ -426,6 +853,7 @@ sub assert_tree {
        if ($tree ne $expected) {
                croak "Tree mismatch, Got: $tree, Expected: $expected\n";
        }
+       unlink $tmpindex;
 }
 
 sub parse_diff_tree {
@@ -562,12 +990,17 @@ sub precommit_check {
 sub svn_checkout_tree {
        my ($svn_rev, $treeish) = @_;
        my $from = file_to_s("$REV_DIR/$svn_rev");
-       assert_svn_wc_clean($svn_rev,$from);
+       assert_tree($from);
        print "diff-tree $from $treeish\n";
        my $pid = open my $diff_fh, '-|';
        defined $pid or croak $!;
        if ($pid == 0) {
-               my @diff_tree = qw(git-diff-tree -z -r -C);
+               my @diff_tree = qw(git-diff-tree -z -r);
+               if ($_cp_similarity) {
+                       push @diff_tree, "-C$_cp_similarity";
+               } else {
+                       push @diff_tree, '-C';
+               }
                push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
                push @diff_tree, "-l$_l" if defined $_l;
                exec(@diff_tree, $from, $treeish) or croak $!;
@@ -659,7 +1092,7 @@ sub handle_rmdir {
 
 sub svn_commit_tree {
        my ($svn_rev, $commit) = @_;
-       my $commit_msg = "$GIT_DIR/$GIT_SVN/.svn-commit.tmp.$$";
+       my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
        my %log_msg = ( msg => '' );
        open my $msg, '>', $commit_msg or croak $!;
 
@@ -699,23 +1132,34 @@ sub svn_commit_tree {
        my ($oneline) = ($log_msg{msg} =~ /([^\n\r]+)/);
        print "Committing $commit: $oneline\n";
 
+       if (defined $LC_ALL) {
+               $ENV{LC_ALL} = $LC_ALL;
+       } else {
+               delete $ENV{LC_ALL};
+       }
        my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
-       my ($committed) = grep(/^Committed revision \d+\./,@ci_output);
+       $ENV{LC_ALL} = 'C';
        unlink $commit_msg;
-       defined $committed or croak
+       my ($committed) = ($ci_output[$#ci_output] =~ /(\d+)/);
+       if (!defined $committed) {
+               my $out = join("\n",@ci_output);
+               print STDERR "W: Trouble parsing \`svn commit' output:\n\n",
+                               $out, "\n\nAssuming English locale...";
+               ($committed) = ($out =~ /^Committed revision \d+\./sm);
+               defined $committed or die " FAILED!\n",
                        "Commit output failed to parse committed revision!\n",
-                       join("\n",@ci_output),"\n";
-       my ($rev_committed) = ($committed =~ /^Committed revision (\d+)\./);
+               print STDERR " OK\n";
+       }
 
        my @svn_up = qw(svn up);
        push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
-       if ($rev_committed == ($svn_rev + 1)) {
-               push @svn_up, "-r$rev_committed";
+       if ($committed == ($svn_rev + 1)) {
+               push @svn_up, "-r$committed";
                sys(@svn_up);
                my $info = svn_info('.');
                my $date = $info->{'Last Changed Date'} or die "Missing date\n";
-               if ($info->{'Last Changed Rev'} != $rev_committed) {
-                       croak "$info->{'Last Changed Rev'} != $rev_committed\n"
+               if ($info->{'Last Changed Rev'} != $committed) {
+                       croak "$info->{'Last Changed Rev'} != $committed\n"
                }
                my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
                                        /(\d{4})\-(\d\d)\-(\d\d)\s
@@ -723,16 +1167,48 @@ sub svn_commit_tree {
                                         or croak "Failed to parse date: $date\n";
                $log_msg{date} = "$tz $Y-$m-$d $H:$M:$S";
                $log_msg{author} = $info->{'Last Changed Author'};
-               $log_msg{revision} = $rev_committed;
+               $log_msg{revision} = $committed;
                $log_msg{msg} .= "\n";
                my $parent = file_to_s("$REV_DIR/$svn_rev");
                git_commit(\%log_msg, $parent, $commit);
-               return $rev_committed;
+               return $committed;
        }
        # resync immediately
        push @svn_up, "-r$svn_rev";
        sys(@svn_up);
-       return fetch("$rev_committed=$commit")->{revision};
+       return fetch("$committed=$commit")->{revision};
+}
+
+sub rev_list_raw {
+       my (@args) = @_;
+       my $pid = open my $fh, '-|';
+       defined $pid or croak $!;
+       if (!$pid) {
+               exec(qw/git-rev-list --pretty=raw/, @args) or croak $!;
+       }
+       return { fh => $fh, t => { } };
+}
+
+sub next_rev_list_entry {
+       my $rl = shift;
+       my $fh = $rl->{fh};
+       my $x = $rl->{t};
+       while (<$fh>) {
+               if (/^commit ($sha1)$/o) {
+                       if ($x->{c}) {
+                               $rl->{t} = { c => $1 };
+                               return $x;
+                       } else {
+                               $x->{c} = $1;
+                       }
+               } elsif (/^parent ($sha1)$/o) {
+                       $x->{p}->{$1} = 1;
+               } elsif (s/^    //) {
+                       $x->{m} ||= '';
+                       $x->{m} .= $_;
+               }
+       }
+       return ($x != $rl->{t}) ? $x : undef;
 }
 
 # read the entire log into a temporary file (which is removed ASAP)
@@ -747,7 +1223,7 @@ sub svn_log_raw {
                exec (qw(svn log), @log_args) or croak $!
        }
        waitpid $pid, 0;
-       croak if $?;
+       croak $? if $?;
        seek $log_fh, 0, 0 or croak $!;
        return { state => 'sep', fh => $log_fh };
 }
@@ -852,13 +1328,82 @@ sub svn_info {
 
 sub sys { system(@_) == 0 or croak $? }
 
-sub git_addremove {
-       system( "git-diff-files --name-only -z ".
-                               " | git-update-index --remove -z --stdin && ".
-               "git-ls-files -z --others ".
-                       "'--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude'".
-                               " | git-update-index --add -z --stdin"
-               ) == 0 or croak $?
+sub eol_cp {
+       my ($from, $to) = @_;
+       my $es = svn_propget_base('svn:eol-style', $to);
+       open my $rfd, '<', $from or croak $!;
+       binmode $rfd or croak $!;
+       open my $wfd, '>', $to or croak $!;
+       binmode $wfd or croak $!;
+
+       my $eol = $EOL{$es} or undef;
+       my $buf;
+       use bytes;
+       while (1) {
+               my ($r, $w, $t);
+               defined($r = sysread($rfd, $buf, 4096)) or croak $!;
+               return unless $r;
+               if ($eol) {
+                       if ($buf =~ /\015$/) {
+                               my $c;
+                               defined($r = sysread($rfd,$c,1)) or croak $!;
+                               $buf .= $c if $r > 0;
+                       }
+                       $buf =~ s/(?:\015\012|\015|\012)/$eol/gs;
+                       $r = length($buf);
+               }
+               for ($w = 0; $w < $r; $w += $t) {
+                       $t = syswrite($wfd, $buf, $r - $w, $w) or croak $!;
+               }
+       }
+       no bytes;
+}
+
+sub do_update_index {
+       my ($z_cmd, $cmd, $no_text_base) = @_;
+
+       my $z = open my $p, '-|';
+       defined $z or croak $!;
+       unless ($z) { exec @$z_cmd or croak $! }
+
+       my $pid = open my $ui, '|-';
+       defined $pid or croak $!;
+       unless ($pid) {
+               exec('git-update-index',"--$cmd",'-z','--stdin') or croak $!;
+       }
+       local $/ = "\0";
+       while (my $x = <$p>) {
+               chomp $x;
+               if (!$no_text_base && lstat $x && ! -l _ &&
+                               svn_propget_base('svn:keywords', $x)) {
+                       my $mode = -x _ ? 0755 : 0644;
+                       my ($v,$d,$f) = File::Spec->splitpath($x);
+                       my $tb = File::Spec->catfile($d, '.svn', 'tmp',
+                                               'text-base',"$f.svn-base");
+                       $tb =~ s#^/##;
+                       unless (-f $tb) {
+                               $tb = File::Spec->catfile($d, '.svn',
+                                               'text-base',"$f.svn-base");
+                               $tb =~ s#^/##;
+                       }
+                       unlink $x or croak $!;
+                       eol_cp($tb, $x);
+                       chmod(($mode &~ umask), $x) or croak $!;
+               }
+               print $ui $x,"\0";
+       }
+       close $ui or croak $!;
+}
+
+sub index_changes {
+       my $no_text_base = shift;
+       do_update_index([qw/git-diff-files --name-only -z/],
+                       'remove',
+                       $no_text_base);
+       do_update_index([qw/git-ls-files -z --others/,
+                               "--exclude-from=$GIT_SVN_DIR/info/exclude"],
+                       'add',
+                       $no_text_base);
 }
 
 sub s_to_file {
@@ -936,9 +1481,9 @@ sub git_commit {
        defined $pid or croak $!;
        if ($pid == 0) {
                $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
-               git_addremove();
+               index_changes();
                chomp(my $tree = `git-write-tree`);
-               croak if $?;
+               croak $? if $?;
                if (exists $tree_map{$tree}) {
                        my %seen_parent = map { $_ => 1 } @exec_parents;
                        foreach (@{$tree_map{$tree}}) {
@@ -966,7 +1511,7 @@ sub git_commit {
                exec @exec or croak $!;
        }
        waitpid($pid,0);
-       croak if $?;
+       croak $? if $?;
 
        $out_fh->flush == 0 or croak $!;
        seek $out_fh, 0, 0 or croak $!;
@@ -982,14 +1527,18 @@ sub git_commit {
                        close STDERR;
                        close STDOUT;
                        exec 'git-rev-parse','--verify',
-                                               "refs/remotes/$GIT_SVN^0";
+                                       "refs/remotes/$GIT_SVN^0" or croak $!;
                }
                waitpid $pid, 0;
                push @update_ref, $primary_parent unless $?;
        }
        sys(@update_ref);
-       sys('git-update-ref',"$GIT_SVN/revs/$log_msg->{revision}",$commit);
+       sys('git-update-ref',"svn/$GIT_SVN/revs/$log_msg->{revision}",$commit);
        print "r$log_msg->{revision} = $commit\n";
+       if ($_repack && (--$_repack_nr == 0)) {
+               $_repack_nr = $_repack;
+               sys("git repack $_repack_flags");
+       }
        return $commit;
 }
 
@@ -1038,7 +1587,7 @@ sub blob_to_file {
 
        if ($pid == 0) {
                open STDOUT, '>&', $blob_fh or croak $!;
-               exec('git-cat-file','blob',$blob);
+               exec('git-cat-file','blob',$blob) or croak $!;
        }
        waitpid $pid, 0;
        croak $? if $?;
@@ -1050,7 +1599,7 @@ sub safe_qx {
        my $pid = open my $child, '-|';
        defined $pid or croak $!;
        if ($pid == 0) {
-               exec(@_) or croak $?;
+               exec(@_) or croak $!;
        }
        my @ret = (<$child>);
        close $child or croak $?;
@@ -1068,6 +1617,9 @@ sub svn_compat_check {
        if (grep /usage: checkout URL\[\@REV\]/,@co_help) {
                $_svn_co_url_revs = 1;
        }
+       if (grep /\[TARGET\[\@REV\]\.\.\.\]/, `svn propget -h`) {
+               $_svn_pg_peg_revs = 1;
+       }
 
        # I really, really hope nobody hits this...
        unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) {
@@ -1097,7 +1649,7 @@ sub check_upgrade_needed {
                defined $pid or croak $!;
                if ($pid == 0) {
                        close STDERR;
-                       exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $?;
+                       exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $!;
                }
                my @ret = (<$child>);
                close $child or croak $?;
@@ -1115,24 +1667,42 @@ sub check_upgrade_needed {
 # fills %tree_map with a reverse mapping of trees to commits.  Useful
 # for finding parents to commit on.
 sub map_tree_joins {
+       my %seen;
        foreach my $br (@_branch_from) {
                my $pid = open my $pipe, '-|';
                defined $pid or croak $!;
                if ($pid == 0) {
-                       exec(qw(git-rev-list --pretty=raw), $br) or croak $?;
+                       exec(qw(git-rev-list --topo-order --pretty=raw), $br)
+                                                               or croak $!;
                }
                while (<$pipe>) {
                        if (/^commit ($sha1)$/o) {
                                my $commit = $1;
+
+                               # if we've seen a commit,
+                               # we've seen its parents
+                               last if $seen{$commit};
                                my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o);
                                unless (defined $tree) {
                                        die "Failed to parse commit $commit\n";
                                }
                                push @{$tree_map{$tree}}, $commit;
+                               $seen{$commit} = 1;
                        }
                }
-               close $pipe or croak $?;
+               close $pipe; # we could be breaking the pipe early
+       }
+}
+
+sub load_all_refs {
+       if (@_branch_from) {
+               print STDERR '--branch|-b parameters are ignored when ',
+                       "--branch-all-refs|-B is passed\n";
        }
+
+       # don't worry about rev-list on non-commit objects/tags,
+       # it shouldn't blow up if a ref is a blob or tree...
+       chomp(@_branch_from = `git-rev-parse --symbolic --all`);
 }
 
 # '<svn username> = real-name <email address>' mapping based on git-svnimport:
@@ -1147,6 +1717,318 @@ sub load_authors {
        close $authors or croak $!;
 }
 
+sub rload_authors {
+       open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
+       while (<$authors>) {
+               chomp;
+               next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
+               my ($user, $name, $email) = ($1, $2, $3);
+               $rusers{"$name <$email>"} = $user;
+       }
+       close $authors or croak $!;
+}
+
+sub svn_propget_base {
+       my ($p, $f) = @_;
+       $f .= '@BASE' if $_svn_pg_peg_revs;
+       return safe_qx(qw/svn propget/, $p, $f);
+}
+
+sub git_svn_each {
+       my $sub = shift;
+       foreach (`git-rev-parse --symbolic --all`) {
+               next unless s#^refs/remotes/##;
+               chomp $_;
+               next unless -f "$GIT_DIR/svn/$_/info/url";
+               &$sub($_);
+       }
+}
+
+sub migration_check {
+       return if (-d "$GIT_DIR/svn" || !-d $GIT_DIR);
+       print "Upgrading repository...\n";
+       unless (-d "$GIT_DIR/svn") {
+               mkdir "$GIT_DIR/svn" or croak $!;
+       }
+       print "Data from a previous version of git-svn exists, but\n\t",
+                               "$GIT_SVN_DIR\n\t(required for this version ",
+                               "($VERSION) of git-svn) does not.\n";
+
+       foreach my $x (`git-rev-parse --symbolic --all`) {
+               next unless $x =~ s#^refs/remotes/##;
+               chomp $x;
+               next unless -f "$GIT_DIR/$x/info/url";
+               my $u = eval { file_to_s("$GIT_DIR/$x/info/url") };
+               next unless $u;
+               my $dn = dirname("$GIT_DIR/svn/$x");
+               mkpath([$dn]) unless -d $dn;
+               rename "$GIT_DIR/$x", "$GIT_DIR/svn/$x" or croak "$!: $x";
+               my ($url, $path) = repo_path_split($u);
+               s_to_file($url, "$GIT_DIR/svn/$x/info/repo_url");
+               s_to_file($path, "$GIT_DIR/svn/$x/info/repo_path");
+       }
+       print "Done upgrading.\n";
+}
+
+sub find_rev_before {
+       my ($r, $git_svn_id) = @_;
+       my @revs = map { basename $_ } <$GIT_DIR/svn/$git_svn_id/revs/*>;
+       foreach my $r0 (sort { $b <=> $a } @revs) {
+               next if $r0 >= $r;
+               return ($r0, file_to_s("$GIT_DIR/svn/$git_svn_id/revs/$r0"));
+       }
+       return (undef, undef);
+}
+
+sub init_vars {
+       $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
+       $GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN";
+       $GIT_SVN_INDEX = "$GIT_SVN_DIR/index";
+       $SVN_URL = undef;
+       $REV_DIR = "$GIT_SVN_DIR/revs";
+       $SVN_WC = "$GIT_SVN_DIR/tree";
+}
+
+# convert GetOpt::Long specs for use by git-repo-config
+sub read_repo_config {
+       return unless -d $GIT_DIR;
+       my $opts = shift;
+       foreach my $o (keys %$opts) {
+               my $v = $opts->{$o};
+               my ($key) = ($o =~ /^([a-z\-]+)/);
+               $key =~ s/-//g;
+               my $arg = 'git-repo-config';
+               $arg .= ' --int' if ($o =~ /[:=]i$/);
+               $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
+               if (ref $v eq 'ARRAY') {
+                       chomp(my @tmp = `$arg --get-all svn.$key`);
+                       @$v = @tmp if @tmp;
+               } else {
+                       chomp(my $tmp = `$arg --get svn.$key`);
+                       if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) {
+                               $$v = $tmp;
+                       }
+               }
+       }
+}
+
+sub set_default_vals {
+       if (defined $_repack) {
+               $_repack = 1000 if ($_repack <= 0);
+               $_repack_nr = $_repack;
+               $_repack_flags ||= '';
+       }
+}
+
+sub read_grafts {
+       my $gr_file = shift;
+       my ($grafts, $comments) = ({}, {});
+       if (open my $fh, '<', $gr_file) {
+               my @tmp;
+               while (<$fh>) {
+                       if (/^($sha1)\s+/) {
+                               my $c = $1;
+                               if (@tmp) {
+                                       @{$comments->{$c}} = @tmp;
+                                       @tmp = ();
+                               }
+                               foreach my $p (split /\s+/, $_) {
+                                       $grafts->{$c}->{$p} = 1;
+                               }
+                       } else {
+                               push @tmp, $_;
+                       }
+               }
+               close $fh or croak $!;
+               @{$comments->{'END'}} = @tmp if @tmp;
+       }
+       return ($grafts, $comments);
+}
+
+sub write_grafts {
+       my ($grafts, $comments, $gr_file) = @_;
+
+       open my $fh, '>', $gr_file or croak $!;
+       foreach my $c (sort keys %$grafts) {
+               if ($comments->{$c}) {
+                       print $fh $_ foreach @{$comments->{$c}};
+               }
+               my $p = $grafts->{$c};
+               delete $p->{$c}; # commits are not self-reproducing...
+               my $pid = open my $ch, '-|';
+               defined $pid or croak $!;
+               if (!$pid) {
+                       exec(qw/git-cat-file commit/, $c) or croak $!;
+               }
+               while (<$ch>) {
+                       if (/^parent ([a-f\d]{40})/) {
+                               $p->{$1} = 1;
+                       } else {
+                               last unless /^\S/i;
+                       }
+               }
+               close $ch; # breaking the pipe
+               print $fh $c, ' ', join(' ', sort keys %$p),"\n";
+       }
+       if ($comments->{'END'}) {
+               print $fh $_ foreach @{$comments->{'END'}};
+       }
+       close $fh or croak $!;
+}
+
+sub read_url_paths {
+       my $l_map = {};
+       git_svn_each(sub { my $x = shift;
+                       my $u = file_to_s("$GIT_DIR/svn/$x/info/repo_url");
+                       my $p = file_to_s("$GIT_DIR/svn/$x/info/repo_path");
+                       # we hate trailing slashes
+                       if ($u =~ s#(?:^\/+|\/+$)##g) {
+                               s_to_file($u,"$GIT_DIR/svn/$x/info/repo_url");
+                       }
+                       if ($p =~ s#(?:^\/+|\/+$)##g) {
+                               s_to_file($p,"$GIT_DIR/svn/$x/info/repo_path");
+                       }
+                       $l_map->{$u}->{$p} = $x;
+                       });
+       return $l_map;
+}
+
+sub extract_metadata {
+       my $id = shift;
+       my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
+                                                       \s([a-f\d\-]+)$/x);
+       if (!$rev || !$uuid || !$url) {
+               # some of the original repositories I made had
+               # indentifiers like this:
+               ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
+       }
+       return ($url, $rev, $uuid);
+}
+
+sub tz_to_s_offset {
+       my ($tz) = @_;
+       $tz =~ s/(\d\d)$//;
+       return ($1 * 60) + ($tz * 3600);
+}
+
+sub setup_pager { # translated to Perl from pager.c
+       return unless (-t *STDOUT);
+       my $pager = $ENV{PAGER};
+       if (!defined $pager) {
+               $pager = 'less';
+       } elsif (length $pager == 0 || $pager eq 'cat') {
+               return;
+       }
+       pipe my $rfd, my $wfd or return;
+       defined(my $pid = fork) or croak $!;
+       if (!$pid) {
+               open STDOUT, '>&', $wfd or croak $!;
+               return;
+       }
+       open STDIN, '<&', $rfd or croak $!;
+       $ENV{LESS} ||= '-S';
+       exec $pager or croak "Can't run pager: $!\n";;
+}
+
+sub get_author_info {
+       my ($dest, $author, $t, $tz) = @_;
+       $author =~ s/(?:^\s*|\s*$)//g;
+       my $_a;
+       if ($_authors) {
+               $_a = $rusers{$author} || undef;
+       }
+       if (!$_a) {
+               ($_a) = ($author =~ /<([^>]+)\@[^>]+>$/);
+       }
+       $dest->{t} = $t;
+       $dest->{tz} = $tz;
+       $dest->{a} = $_a;
+       # Date::Parse isn't in the standard Perl distro :(
+       if ($tz =~ s/^\+//) {
+               $t += tz_to_s_offset($tz);
+       } elsif ($tz =~ s/^\-//) {
+               $t -= tz_to_s_offset($tz);
+       }
+       $dest->{t_utc} = $t;
+}
+
+sub process_commit {
+       my ($c, $r_min, $r_max, $defer) = @_;
+       if (defined $r_min && defined $r_max) {
+               if ($r_min == $c->{r} && $r_min == $r_max) {
+                       show_commit($c);
+                       return 0;
+               }
+               return 1 if $r_min == $r_max;
+               if ($r_min < $r_max) {
+                       # we need to reverse the print order
+                       return 0 if (defined $_limit && --$_limit < 0);
+                       push @$defer, $c;
+                       return 1;
+               }
+               if ($r_min != $r_max) {
+                       return 1 if ($r_min < $c->{r});
+                       return 1 if ($r_max > $c->{r});
+               }
+       }
+       return 0 if (defined $_limit && --$_limit < 0);
+       show_commit($c);
+       return 1;
+}
+
+sub show_commit {
+       my $c = shift;
+       if ($_oneline) {
+               my $x = "\n";
+               if (my $l = $c->{l}) {
+                       while ($l->[0] =~ /^\s*$/) { shift @$l }
+                       $x = $l->[0];
+               }
+               $_l_fmt ||= 'A' . length($c->{r});
+               print 'r',pack($_l_fmt, $c->{r}),' | ';
+               print "$c->{c} | " if $_show_commit;
+               print $x;
+       } else {
+               show_commit_normal($c);
+       }
+}
+
+sub show_commit_normal {
+       my ($c) = @_;
+       print '-' x72, "\nr$c->{r} | ";
+       print "$c->{c} | " if $_show_commit;
+       print "$c->{a} | ", strftime("%Y-%m-%d %H:%M:%S %z (%a, %d %b %Y)",
+                                localtime($c->{t_utc})), ' | ';
+       my $nr_line = 0;
+
+       if (my $l = $c->{l}) {
+               while ($l->[$#$l] eq "\n" && $l->[($#$l - 1)] eq "\n") {
+                       pop @$l;
+               }
+               $nr_line = scalar @$l;
+               if (!$nr_line) {
+                       print "1 line\n\n\n";
+               } else {
+                       if ($nr_line == 1) {
+                               $nr_line = '1 line';
+                       } else {
+                               $nr_line .= ' lines';
+                       }
+                       print $nr_line, "\n\n";
+                       print $_ foreach @$l;
+               }
+       } else {
+               print "1 line\n\n";
+
+       }
+       foreach my $x (qw/raw diff/) {
+               if ($c->{$x}) {
+                       print "\n";
+                       print $_ foreach @{$c->{$x}}
+               }
+       }
+}
+
 __END__
 
 Data structures: