Teach fsck and prune about the new location of temporary objects
[gitweb.git] / git-svn.perl
index b1510495a7f003991a37d0810c551404c6c1252c..2e0e55242ff089cda3c87ae57f7c776c13508a56 100755 (executable)
@@ -4,7 +4,7 @@
 use warnings;
 use strict;
 use vars qw/   $AUTHOR $VERSION
-               $sha1 $sha1_short $_revision
+               $sha1 $sha1_short $_revision $_repository
                $_q $_authors %users/;
 $AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
 $VERSION = '@@GIT_VERSION@@';
@@ -65,7 +65,8 @@ BEGIN
        $_template, $_shared,
        $_version, $_fetch_all, $_no_rebase,
        $_merge, $_strategy, $_dry_run, $_local,
-       $_prefix, $_no_checkout, $_url, $_verbose);
+       $_prefix, $_no_checkout, $_url, $_verbose,
+       $_git_format);
 $Git::SVN::_follow_parent = 1;
 my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
                     'config-dir=s' => \$Git::SVN::Ra::config_dir,
@@ -82,6 +83,7 @@ BEGIN
                'repack-flags|repack-args|repack-opts=s' =>
                   \$Git::SVN::_repack_flags,
                'use-log-author' => \$Git::SVN::_use_log_author,
+               'add-author-from' => \$Git::SVN::_add_author_from,
                %remote_opts );
 
 my ($_trunk, $_tags, $_branches, $_stdlayout);
@@ -175,6 +177,7 @@ BEGIN
                          'strategy|s=s' => \$_strategy,
                          'local|l' => \$_local,
                          'fetch-all|all' => \$_fetch_all,
+                         'dry-run|n' => \$_dry_run,
                          %fc_opts } ],
        'commit-diff' => [ \&cmd_commit_diff,
                           'Commit a diff between two trees',
@@ -188,7 +191,7 @@ BEGIN
                    { 'url' => \$_url, } ],
        'blame' => [ \&Git::SVN::Log::cmd_blame,
                    "Show what revision and author last modified each line of a file",
-                   {} ],
+                   { 'git-format' => \$_git_format } ],
 );
 
 my $cmd;
@@ -220,12 +223,13 @@ BEGIN
                }
                $ENV{GIT_DIR} = $git_dir;
        }
+       $_repository = Git->repository(Repository => $ENV{GIT_DIR});
 }
 
 my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
 
 read_repo_config(\%opts);
-Getopt::Long::Configure('pass_through') if ($cmd && $cmd eq 'log');
+Getopt::Long::Configure('pass_through') if ($cmd && ($cmd eq 'log' || $cmd eq 'blame'));
 my $rv = GetOptions(%opts, 'help|H|h' => \$_help, 'version|V' => \$_version,
                     'minimize-connections' => \$Git::SVN::Migration::_minimize,
                     'id|i=s' => \$Git::SVN::default_ref_id,
@@ -257,7 +261,7 @@ sub usage {
        my $fd = $exit ? \*STDERR : \*STDOUT;
        print $fd <<"";
 git-svn - bidirectional operations between a single Subversion tree and git
-Usage: $0 <command> [options] [arguments]\n
+Usage: git svn <command> [options] [arguments]\n
 
        print $fd "Available commands:\n" unless $cmd;
 
@@ -301,6 +305,7 @@ sub do_git_init_db {
                        }
                }
                command_noisy(@init_db);
+               $_repository = Git->repository(Repository => ".git");
        }
        my $set;
        my $pfx = "svn-remote.$Git::SVN::default_repo_id";
@@ -317,6 +322,7 @@ sub init_subdir {
        mkpath([$repo_path]) unless -d $repo_path;
        chdir $repo_path or die "Couldn't chdir to $repo_path: $!\n";
        $ENV{GIT_DIR} = '.git';
+       $_repository = Git->repository(Repository => $ENV{GIT_DIR});
 }
 
 sub cmd_clone {
@@ -410,10 +416,12 @@ sub cmd_dcommit {
        $head ||= 'HEAD';
        my @refs;
        my ($url, $rev, $uuid, $gs) = working_head_info($head, \@refs);
-       print "Committing to $url ...\n";
+       if ($url) {
+               print "Committing to $url ...\n";
+       }
        unless ($gs) {
                die "Unable to determine upstream SVN information from ",
-                   "$head history\n";
+                   "$head history.\nPerhaps the repository is empty.";
        }
        my $last_rev;
        my ($linear_refs, $parents) = linearize_history($gs, \@refs);
@@ -529,13 +537,13 @@ sub cmd_find_rev {
                my $head = shift;
                $head ||= 'HEAD';
                my @refs;
-               my (undef, undef, undef, $gs) = working_head_info($head, \@refs);
+               my (undef, undef, $uuid, $gs) = working_head_info($head, \@refs);
                unless ($gs) {
                        die "Unable to determine upstream SVN information from ",
                            "$head history\n";
                }
                my $desired_revision = substr($revision_or_hash, 1);
-               $result = $gs->rev_map_get($desired_revision);
+               $result = $gs->rev_map_get($desired_revision, $uuid);
        } else {
                my (undef, $rev, undef) = cmt_metadata($revision_or_hash);
                $result = $rev;
@@ -550,6 +558,11 @@ sub cmd_rebase {
                die "Unable to determine upstream SVN information from ",
                    "working tree history\n";
        }
+       if ($_dry_run) {
+               print "Remote Branch: " . $gs->refname . "\n";
+               print "SVN URL: " . $url . "\n";
+               return;
+       }
        if (command(qw/diff-index HEAD --/)) {
                print STDERR "Cannot rebase with uncommited changes:\n";
                command_noisy('status');
@@ -612,7 +625,7 @@ sub cmd_create_ignore {
                print GITIGNORE "$s\n";
                close(GITIGNORE)
                  or fatal("Failed to close `$ignore': $!");
-               command_noisy('add', $ignore);
+               command_noisy('add', '-f', $ignore);
        });
 }
 
@@ -630,6 +643,8 @@ sub canonicalize_path {
        $path =~ s#/[^/]+/\.\.##g;
        $path =~ s#/$##g;
        $path =~ s#^\./## if $dot_slash_added;
+       $path =~ s#^/##;
+       $path =~ s#^\.$##;
        return $path;
 }
 
@@ -738,7 +753,7 @@ sub cmd_commit_diff {
        my $usage = "Usage: $0 commit-diff -r<revision> ".
                    "<tree-ish> <tree-ish> [<URL>]";
        fatal($usage) if (!defined $ta || !defined $tb);
-       my $svn_path;
+       my $svn_path = '';
        if (!defined $url) {
                my $gs = eval { Git::SVN->new };
                if (!$gs) {
@@ -762,7 +777,6 @@ sub cmd_commit_diff {
                $_message ||= get_commit_entry($tb)->{log};
        }
        my $ra ||= Git::SVN::Ra->new($url);
-       $svn_path ||= $ra->{svn_path};
        my $r = $_revision;
        if ($r eq 'HEAD') {
                $r = $ra->get_latest_revnum;
@@ -1009,17 +1023,30 @@ sub get_commit_entry {
                my ($msg_fh, $ctx) = command_output_pipe('cat-file',
                                                         $type, $treeish);
                my $in_msg = 0;
+               my $author;
+               my $saw_from = 0;
+               my $msgbuf = "";
                while (<$msg_fh>) {
                        if (!$in_msg) {
                                $in_msg = 1 if (/^\s*$/);
+                               $author = $1 if (/^author (.*>)/);
                        } elsif (/^git-svn-id: /) {
                                # skip this for now, we regenerate the
                                # correct one on re-fetch anyways
                                # TODO: set *:merge properties or like...
                        } else {
-                               print $log_fh $_ or croak $!;
+                               if (/^From:/ || /^Signed-off-by:/) {
+                                       $saw_from = 1;
+                               }
+                               $msgbuf .= $_;
                        }
                }
+               $msgbuf =~ s/\s+$//s;
+               if ($Git::SVN::_add_author_from && defined($author)
+                   && !$saw_from) {
+                       $msgbuf .= "\n\nFrom: $author";
+               }
+               print $log_fh $msgbuf or croak $!;
                command_close_pipe($msg_fh, $ctx);
        }
        close $log_fh or croak $!;
@@ -1135,7 +1162,7 @@ sub working_head_info {
                if (defined $url && defined $rev) {
                        next if $max{$url} and $max{$url} < $rev;
                        if (my $gs = Git::SVN->find_by_url($url)) {
-                               my $c = $gs->rev_map_get($rev);
+                               my $c = $gs->rev_map_get($rev, $uuid);
                                if ($c && $c eq $hash) {
                                        close $fh; # break the pipe
                                        return ($url, $rev, $uuid, $gs);
@@ -1199,7 +1226,7 @@ sub linearize_history {
 
 sub find_file_type_and_diff_status {
        my ($path) = @_;
-       return ('dir', '') if $path eq '.';
+       return ('dir', '') if $path eq '';
 
        my $diff_output =
            command_oneline(qw(diff --cached --name-status --), $path) || "";
@@ -1246,7 +1273,7 @@ package Git::SVN;
 use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent
             $_repack $_repack_flags $_use_svm_props $_head
             $_use_svnsync_props $no_reuse_existing $_minimize_url
-           $_use_log_author/;
+           $_use_log_author $_add_author_from/;
 use Carp qw/croak/;
 use File::Path qw/mkpath/;
 use File::Copy qw/copy/;
@@ -1389,11 +1416,17 @@ sub fetch_all {
 
 sub read_all_remotes {
        my $r = {};
+       my $use_svm_props = eval { command_oneline(qw/config --bool
+           svn.useSvmProps/) };
+       $use_svm_props = $use_svm_props eq 'true' if $use_svm_props;
        foreach (grep { s/^svn-remote\.// } command(qw/config -l/)) {
                if (m!^(.+)\.fetch=\s*(.*)\s*:\s*refs/remotes/(.+)\s*$!) {
                        my ($remote, $local_ref, $remote_ref) = ($1, $2, $3);
                        $local_ref =~ s{^/}{};
                        $r->{$remote}->{fetch}->{$local_ref} = $remote_ref;
+                       $r->{$remote}->{svm} = {} if $use_svm_props;
+               } elsif (m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {
+                       $r->{$1}->{svm} = {};
                } elsif (m!^(.+)\.url=\s*(.*)\s*$!) {
                        $r->{$1}->{url} = $2;
                } elsif (m!^(.+)\.(branches|tags)=
@@ -1410,6 +1443,23 @@ sub read_all_remotes {
                        }
                }
        }
+
+       map {
+               if (defined $r->{$_}->{svm}) {
+                       my $svm;
+                       eval {
+                               my $section = "svn-remote.$_";
+                               $svm = {
+                                       source => tmp_config('--get',
+                                           "$section.svm-source"),
+                                       replace => tmp_config('--get',
+                                           "$section.svm-replace"),
+                               }
+                       };
+                       $r->{$_}->{svm} = $svm;
+               }
+       } keys %$r;
+
        $r;
 }
 
@@ -1437,13 +1487,6 @@ sub verify_remotes_sanity {
        }
 }
 
-# we allow more chars than remotes2config.sh...
-sub sanitize_remote_name {
-       my ($name) = @_;
-       $name =~ tr{A-Za-z0-9:,/+-}{.}c;
-       $name;
-}
-
 sub find_existing_remote {
        my ($url, $remotes) = @_;
        return undef if $no_reuse_existing;
@@ -1543,13 +1586,21 @@ sub find_by_url { # repos_root and, path are optional
                }
                my $p = $path;
                my $rwr = rewrite_root({repo_id => $repo_id});
+               my $svm = $remotes->{$repo_id}->{svm}
+                       if defined $remotes->{$repo_id}->{svm};
                unless (defined $p) {
                        $p = $full_url;
                        my $z = $u;
+                       my $prefix = '';
                        if ($rwr) {
                                $z = $rwr;
+                       } elsif (defined $svm) {
+                               $z = $svm->{source};
+                               $prefix = $svm->{replace};
+                               $prefix =~ s#^\Q$u\E(?:/|$)##;
+                               $prefix =~ s#/$##;
                        }
-                       $p =~ s#^\Q$z\E(?:/|$)## or next;
+                       $p =~ s#^\Q$z\E(?:/|$)#$prefix# or next;
                }
                foreach my $f (keys %$fetch) {
                        next if $f ne $p;
@@ -1900,7 +1951,7 @@ sub prop_walk {
 
        foreach (sort keys %$dirent) {
                next if $dirent->{$_}->{kind} != $SVN::Node::dir;
-               $self->prop_walk($p . $_, $rev, $sub);
+               $self->prop_walk($self->{path} . $p . $_, $rev, $sub);
        }
 }
 
@@ -2426,13 +2477,15 @@ sub make_log_entry {
                        $name_field = $1;
                }
                if (!defined $name_field) {
-                       #
+                       if (!defined $email) {
+                               $email = $name;
+                       }
                } elsif ($name_field =~ /(.*?)\s+<(.*)>/) {
                        ($name, $email) = ($1, $2);
                } elsif ($name_field =~ /(.*)@/) {
                        ($name, $email) = ($1, $name_field);
                } else {
-                       ($name, $email) = ($name_field, 'unknown');
+                       ($name, $email) = ($name_field, $name_field);
                }
        }
        if (defined $headrev && $self->use_svm_props) {
@@ -2550,8 +2603,8 @@ sub rebuild {
        my ($log, $ctx) =
            command_output_pipe(qw/rev-list --pretty=raw --no-color --reverse/,
                                $self->refname, '--');
-       my $full_url = $self->full_url;
-       remove_username($full_url);
+       my $metadata_url = $self->metadata_url;
+       remove_username($metadata_url);
        my $svn_uuid = $self->ra_uuid;
        my $c;
        while (<$log>) {
@@ -2569,7 +2622,7 @@ sub rebuild {
                # if we merged or otherwise started elsewhere, this is
                # how we break out of it
                if (($uuid ne $svn_uuid) ||
-                   ($full_url && $url && ($url ne $full_url))) {
+                   ($metadata_url && $url && ($url ne $metadata_url))) {
                        next;
                }
 
@@ -2826,7 +2879,7 @@ sub _new {
        unless (defined $ref_id && length $ref_id) {
                $_[2] = $ref_id = $Git::SVN::default_ref_id;
        }
-       $_[1] = $repo_id = sanitize_remote_name($repo_id);
+       $_[1] = $repo_id;
        my $dir = "$ENV{GIT_DIR}/svn/$ref_id";
        $_[3] = $path = '' unless (defined $path);
        mkpath(["$ENV{GIT_DIR}/svn"]);
@@ -3013,6 +3066,7 @@ package SVN::Git::Fetcher;
 use strict;
 use warnings;
 use Carp qw/croak/;
+use File::Temp qw/tempfile/;
 use IO::File qw//;
 
 # file baton members: path, mode_a, mode_b, pool, fh, blob, base
@@ -3168,14 +3222,9 @@ sub apply_textdelta {
        my $base = IO::File->new_tmpfile;
        $base->autoflush(1);
        if ($fb->{blob}) {
-               defined (my $pid = fork) or croak $!;
-               if (!$pid) {
-                       open STDOUT, '>&', $base or croak $!;
-                       print STDOUT 'link ' if ($fb->{mode_a} == 120000);
-                       exec qw/git-cat-file blob/, $fb->{blob} or croak $!;
-               }
-               waitpid $pid, 0;
-               croak $? if $?;
+               print $base 'link ' if ($fb->{mode_a} == 120000);
+               my $size = $::_repository->cat_blob($fb->{blob}, $base);
+               die "Failed to read object $fb->{blob}" if ($size < 0);
 
                if (defined $exp) {
                        seek $base, 0, 0 or croak $!;
@@ -3216,14 +3265,21 @@ sub close_file {
                                sysseek($fh, 0, 0) or croak $!;
                        }
                }
-               defined(my $pid = open my $out,'-|') or die "Can't fork: $!\n";
-               if (!$pid) {
-                       open STDIN, '<&', $fh or croak $!;
-                       exec qw/git-hash-object -w --stdin/ or croak $!;
+
+               my ($tmp_fh, $tmp_filename) = File::Temp::tempfile(UNLINK => 1);
+               my $result;
+               while ($result = sysread($fh, my $string, 1024)) {
+                       my $wrote = syswrite($tmp_fh, $string, $result);
+                       defined($wrote) && $wrote == $result
+                               or croak("write $tmp_filename: $!\n");
                }
-               chomp($hash = do { local $/; <$out> });
-               close $out or croak $!;
+               defined $result or croak $!;
+               close $tmp_fh or croak $!;
+
                close $fh or croak $!;
+
+               $hash = $::_repository->hash_and_insert_object($tmp_filename);
+               unlink($tmp_filename);
                $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n";
                close $fb->{base} or croak $!;
        } else {
@@ -3549,13 +3605,8 @@ sub chg_file {
        } elsif ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) {
                $self->change_file_prop($fbat,'svn:special',undef);
        }
-       defined(my $pid = fork) or croak $!;
-       if (!$pid) {
-               open STDOUT, '>&', $fh or croak $!;
-               exec qw/git-cat-file blob/, $m->{sha1_b} or croak $!;
-       }
-       waitpid $pid, 0;
-       croak $? if $?;
+       my $size = $::_repository->cat_blob($m->{sha1_b}, $fh);
+       croak "Failed to read object $m->{sha1_b}" if ($size < 0);
        $fh->flush == 0 or croak $!;
        seek $fh, 0, 0 or croak $!;
 
@@ -3669,7 +3720,7 @@ sub escape_uri_only {
        my ($uri) = @_;
        my @tmp;
        foreach (split m{/}, $uri) {
-               s/([^\w.%-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;
+               s/([^\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;
                push @tmp, $_;
        }
        join('/', @tmp);
@@ -4464,19 +4515,51 @@ sub cmd_show_log {
 }
 
 sub cmd_blame {
-       my $path = shift;
+       my $path = pop;
 
        config_pager();
        run_pager();
 
-       my ($fh, $ctx) = command_output_pipe('blame', @_, $path);
-       while (my $line = <$fh>) {
-               if ($line =~ /^\^?([[:xdigit:]]+)\s/) {
-                       my (undef, $rev, undef) = ::cmt_metadata($1);
-                       $rev = sprintf('%-10s', $rev);
-                       $line =~ s/^\^?[[:xdigit:]]+(\s)/$rev$1/;
+       my ($fh, $ctx, $rev);
+
+       if ($_git_format) {
+               ($fh, $ctx) = command_output_pipe('blame', @_, $path);
+               while (my $line = <$fh>) {
+                       if ($line =~ /^\^?([[:xdigit:]]+)\s/) {
+                               # Uncommitted edits show up as a rev ID of
+                               # all zeros, which we can't look up with
+                               # cmt_metadata
+                               if ($1 !~ /^0+$/) {
+                                       (undef, $rev, undef) =
+                                               ::cmt_metadata($1);
+                                       $rev = '0' if (!$rev);
+                               } else {
+                                       $rev = '0';
+                               }
+                               $rev = sprintf('%-10s', $rev);
+                               $line =~ s/^\^?[[:xdigit:]]+(\s)/$rev$1/;
+                       }
+                       print $line;
+               }
+       } else {
+               ($fh, $ctx) = command_output_pipe('blame', '-p', @_, 'HEAD',
+                                                 '--', $path);
+               my ($sha1);
+               my %authors;
+               while (my $line = <$fh>) {
+                       if ($line =~ /^([[:xdigit:]]{40})\s\d+\s\d+/) {
+                               $sha1 = $1;
+                               (undef, $rev, undef) = ::cmt_metadata($1);
+                               $rev = '0' if (!$rev);
+                       }
+                       elsif ($line =~ /^author (.*)/) {
+                               $authors{$rev} = $1;
+                               $authors{$rev} =~ s/\s/_/g;
+                       }
+                       elsif ($line =~ /^\t(.*)$/) {
+                               printf("%6s %10s %s\n", $rev, $authors{$rev}, $1);
+                       }
                }
-               print $line;
        }
        command_close_pipe($fh, $ctx);
 }
@@ -4567,7 +4650,7 @@ sub migrate_from_v1 {
        mkpath([$svn_dir]);
        print STDERR "Data from a previous version of git-svn exists, but\n\t",
                     "$svn_dir\n\t(required for this version ",
-                    "($::VERSION) of git-svn) does not. exist\n";
+                    "($::VERSION) of git-svn) does not exist.\n";
        my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);
        while (<$fh>) {
                my $x = $_;
@@ -4650,8 +4733,7 @@ sub minimize_connections {
 
                # skip existing cases where we already connect to the root
                if (($ra->{url} eq $ra->{repos_root}) ||
-                   (Git::SVN::sanitize_remote_name($ra->{repos_root}) eq
-                    $repo_id)) {
+                   ($ra->{repos_root} eq $repo_id)) {
                        $root_repos->{$ra->{url}} = $repo_id;
                        next;
                }
@@ -4690,8 +4772,7 @@ sub minimize_connections {
        foreach my $url (keys %$new_urls) {
                # see if we can re-use an existing [svn-remote "repo_id"]
                # instead of creating a(n ugly) new section:
-               my $repo_id = $root_repos->{$url} ||
-                             Git::SVN::sanitize_remote_name($url);
+               my $repo_id = $root_repos->{$url} || $url;
 
                my $fetch = $new_urls->{$url};
                foreach my $path (keys %$fetch) {