git-svn: add --repack and --repack-flags= options
[git.git] / contrib / git-svn / git-svn.perl
index c91160d..a04cf1d 100755 (executable)
@@ -6,7 +6,7 @@ use strict;
 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.1.0-pre';
 
@@ -34,14 +34,18 @@ 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, $_cp_similarity,
+       $_repack, $_repack_nr, $_repack_flags,
        $_version, $_upgrade, $_authors, $_branch_all_refs);
 my (@_branch_from, %tree_map, %users);
 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,
                'branch-all-refs|B' => \$_branch_all_refs,
-               'authors-file|A=s' => \$_authors );
+               'authors-file|A=s' => \$_authors,
+               'repack:i' => \$_repack,
+               'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
 
 # yes, 'native' sets "\n".  Patches to fix this for non-*nix systems welcome:
 my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" );
@@ -76,41 +80,20 @@ for (my $i = 0; $i < @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;
-               }
-       }
-}
-
+read_repo_config(\%opts);
 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";
-
+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 eq 'init';
 $cmd{$cmd}->[0]->(@ARGV);
 exit 0;
 
@@ -129,7 +112,7 @@ Usage: $0 <command> [options] [arguments]\n
                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";
@@ -200,7 +183,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 $?;
@@ -217,9 +200,10 @@ sub rebuild {
                sys(@svn_up,"-r$newest_rev");
                $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
                index_changes();
-               exec('git-write-tree');
+               exec('git-write-tree') or croak $!;
        }
        waitpid $pid, 0;
+       croak $? if $?;
 
        if ($_upgrade) {
                print STDERR <<"";
@@ -241,7 +225,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';
@@ -262,7 +246,14 @@ sub fetch {
        } 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;
@@ -285,6 +276,7 @@ sub fetch {
        unless (-e "$GIT_DIR/refs/heads/master") {
                sys(qw(git-update-ref refs/heads/master),$last_commit);
        }
+       close $svn_log->{fh};
        return $last;
 }
 
@@ -343,7 +335,7 @@ 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"){
@@ -367,7 +359,44 @@ sub read_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)/;
+       return ($url, $path);
 }
 
 sub setup_git_svn {
@@ -375,14 +404,17 @@ 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 {
@@ -681,7 +713,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 $!;
 
@@ -780,7 +812,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 };
 }
@@ -958,7 +990,7 @@ sub index_changes {
                        'remove',
                        $no_text_base);
        do_update_index([qw/git-ls-files -z --others/,
-                             "--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude"],
+                               "--exclude-from=$GIT_SVN_DIR/info/exclude"],
                        'add',
                        $no_text_base);
 }
@@ -1040,7 +1072,7 @@ sub git_commit {
                $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
                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}}) {
@@ -1068,7 +1100,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 $!;
@@ -1084,14 +1116,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;
 }
 
@@ -1140,7 +1176,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 $?;
@@ -1152,7 +1188,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 $?;
@@ -1202,7 +1238,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 $?;
@@ -1220,23 +1256,30 @@ 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
        }
 }
 
@@ -1269,6 +1312,72 @@ sub svn_propget_base {
        return safe_qx(qw/svn propget/, $p, $f);
 }
 
+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 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 ||= '';
+       }
+}
+
 __END__
 
 Data structures: