Merge branch 'aj/pack'
[gitweb.git] / git-svn.perl
index 326e89fe037561c1ad72d91c7783bf2a0f603826..e35006142af766080b3bafe0cdcd39bf2ccf3f04 100755 (executable)
@@ -33,7 +33,7 @@
 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 pass_through/;
+use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
 use IPC::Open3;
 use Git;
 
@@ -55,8 +55,8 @@ BEGIN
 my ($_stdin, $_help, $_edit,
        $_message, $_file,
        $_template, $_shared,
-       $_version, $_fetch_all,
-       $_merge, $_strategy, $_dry_run,
+       $_version, $_fetch_all, $_no_rebase,
+       $_merge, $_strategy, $_dry_run, $_local,
        $_prefix, $_no_checkout, $_verbose);
 $Git::SVN::_follow_parent = 1;
 my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
@@ -80,6 +80,7 @@ BEGIN
 my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared,
                   'trunk|T=s' => \$_trunk, 'tags|t=s' => \$_tags,
                   'branches|b=s' => \$_branches, 'prefix=s' => \$_prefix,
+                  'minimize-url|m' => \$Git::SVN::_minimize_url,
                  'no-metadata' => sub { $icv{noMetadata} = 1 },
                  'use-svm-props' => sub { $icv{useSvmProps} = 1 },
                  'use-svnsync-props' => sub { $icv{useSvnsyncProps} = 1 },
@@ -114,6 +115,7 @@ BEGIN
                          'verbose|v' => \$_verbose,
                          'dry-run|n' => \$_dry_run,
                          'fetch-all|all' => \$_fetch_all,
+                         'no-rebase' => \$_no_rebase,
                        %cmt_opts, %fc_opts } ],
        'set-tree' => [ \&cmd_set_tree,
                        "Set an SVN repository to a git tree-ish",
@@ -141,10 +143,13 @@ BEGIN
                          'color' => \$Git::SVN::Log::color,
                          'pager=s' => \$Git::SVN::Log::pager,
                        } ],
+       'find-rev' => [ \&cmd_find_rev, "Translate between SVN revision numbers and tree-ish",
+                       { } ],
        'rebase' => [ \&cmd_rebase, "Fetch and rebase your working directory",
                        { 'merge|m|M' => \$_merge,
                          'verbose|v' => \$_verbose,
                          'strategy|s=s' => \$_strategy,
+                         'local|l' => \$_local,
                          'fetch-all|all' => \$_fetch_all,
                          %fc_opts } ],
        'commit-diff' => [ \&cmd_commit_diff,
@@ -167,13 +172,14 @@ BEGIN
 my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
 
 read_repo_config(\%opts);
+Getopt::Long::Configure('pass_through') if ($cmd && $cmd eq 'log');
 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,
                     'svn-remote|remote|R=s' => sub {
                        $Git::SVN::no_reuse_existing = 1;
                        $Git::SVN::default_repo_id = $_[1] });
-exit 1 if (!$rv && $cmd ne 'log');
+exit 1 if (!$rv && $cmd && $cmd ne 'log');
 
 usage(0) if $_help;
 version() if $_version;
@@ -228,6 +234,8 @@ sub usage {
                next if /^multi-/; # don't show deprecated commands
                print $fd '  ',pack('A17',$_),$cmd{$_}->[1],"\n";
                foreach (keys %{$cmd{$_}->[2]}) {
+                       # mixed-case options are for .git/config only
+                       next if /[A-Z]/ && /^[a-z]+$/i;
                        # prints out arguments as they should be passed:
                        my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
                        print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
@@ -359,13 +367,12 @@ sub cmd_dcommit {
        my $head = shift;
        $head ||= 'HEAD';
        my @refs;
-       my ($url, $rev, $uuid) = working_head_info($head, \@refs);
-       my $c = $refs[-1];
-       unless (defined $url && defined $rev && defined $uuid) {
+       my ($url, $rev, $uuid, $gs) = working_head_info($head, \@refs);
+       unless ($gs) {
                die "Unable to determine upstream SVN information from ",
                    "$head history\n";
        }
-       my $gs = Git::SVN->find_by_url($url);
+       my $c = $refs[-1];
        my $last_rev;
        foreach my $d (@refs) {
                if (!verify_ref("$d~1")) {
@@ -387,7 +394,7 @@ sub cmd_dcommit {
                } else {
                        my %ed_opts = ( r => $last_rev,
                                        log => get_commit_entry($d)->{log},
-                                       ra => Git::SVN::Ra->new($url),
+                                       ra => Git::SVN::Ra->new($gs->full_url),
                                        tree_a => "$d~1",
                                        tree_b => $d,
                                        editor_cb => sub {
@@ -408,44 +415,67 @@ sub cmd_dcommit {
                return;
        }
        $_fetch_all ? $gs->fetch_all : $gs->fetch;
-       # we always want to rebase against the current HEAD, not any
-       # head that was passed to us
-       my @diff = command('diff-tree', 'HEAD', $gs->refname, '--');
-       my @finish;
-       if (@diff) {
-               @finish = rebase_cmd();
-               print STDERR "W: HEAD and ", $gs->refname, " differ, ",
-                            "using @finish:\n", "@diff";
+       unless ($_no_rebase) {
+               # we always want to rebase against the current HEAD, not any
+               # head that was passed to us
+               my @diff = command('diff-tree', 'HEAD', $gs->refname, '--');
+               my @finish;
+               if (@diff) {
+                       @finish = rebase_cmd();
+                       print STDERR "W: HEAD and ", $gs->refname, " differ, ",
+                                    "using @finish:\n", "@diff";
+               } else {
+                       print "No changes between current HEAD and ",
+                             $gs->refname, "\nResetting to the latest ",
+                             $gs->refname, "\n";
+                       @finish = qw/reset --mixed/;
+               }
+               command_noisy(@finish, $gs->refname);
+       }
+}
+
+sub cmd_find_rev {
+       my $revision_or_hash = shift;
+       my $result;
+       if ($revision_or_hash =~ /^r\d+$/) {
+               my $head = shift;
+               $head ||= 'HEAD';
+               my @refs;
+               my (undef, undef, undef, $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_db_get($desired_revision);
        } else {
-               print "No changes between current HEAD and ",
-                     $gs->refname, "\nResetting to the latest ",
-                     $gs->refname, "\n";
-               @finish = qw/reset --mixed/;
+               my (undef, $rev, undef) = cmt_metadata($revision_or_hash);
+               $result = $rev;
        }
-       command_noisy(@finish, $gs->refname);
+       print "$result\n" if $result;
 }
 
 sub cmd_rebase {
        command_noisy(qw/update-index --refresh/);
-       my $url = (working_head_info('HEAD'))[0];
-       if (!defined $url) {
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       unless ($gs) {
                die "Unable to determine upstream SVN information from ",
                    "working tree history\n";
        }
-
-       my $gs = Git::SVN->find_by_url($url);
        if (command(qw/diff-index HEAD --/)) {
                print STDERR "Cannot rebase with uncommited changes:\n";
                command_noisy('status');
                exit 1;
        }
-       $_fetch_all ? $gs->fetch_all : $gs->fetch;
+       unless ($_local) {
+               $_fetch_all ? $gs->fetch_all : $gs->fetch;
+       }
        command_noisy(rebase_cmd(), $gs->refname);
 }
 
 sub cmd_show_ignore {
-       my $url = (::working_head_info('HEAD'))[0];
-       my $gs = Git::SVN->find_by_url($url) || Git::SVN->new;
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       $gs ||= Git::SVN->new;
        my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
        $gs->traverse_ignore(\*STDOUT, $gs->{path}, $r);
 }
@@ -455,6 +485,11 @@ sub cmd_multi_init {
        unless (defined $_trunk || defined $_branches || defined $_tags) {
                usage(1);
        }
+
+       # there are currently some bugs that prevent multi-init/multi-fetch
+       # setups from working well without this.
+       $Git::SVN::_minimize_url = 1;
+
        $_prefix = '' unless defined $_prefix;
        if (defined $url) {
                $url =~ s#/+$##;
@@ -767,16 +802,23 @@ sub cmt_metadata {
 
 sub working_head_info {
        my ($head, $refs) = @_;
-       my ($url, $rev, $uuid);
        my ($fh, $ctx) = command_output_pipe('rev-list', $head);
-       while (<$fh>) {
-               chomp;
-               ($url, $rev, $uuid) = cmt_metadata($_);
-               last if (defined $url && defined $rev && defined $uuid);
-               unshift @$refs, $_ if $refs;
+       while (my $hash = <$fh>) {
+               chomp($hash);
+               my ($url, $rev, $uuid) = cmt_metadata($hash);
+               if (defined $url && defined $rev) {
+                       if (my $gs = Git::SVN->find_by_url($url)) {
+                               my $c = $gs->rev_db_get($rev);
+                               if ($c && $c eq $hash) {
+                                       close $fh; # break the pipe
+                                       return ($url, $rev, $uuid, $gs);
+                               }
+                       }
+               }
+               unshift @$refs, $hash if $refs;
        }
-       close $fh; # break the pipe
-       ($url, $rev, $uuid);
+       command_close_pipe($fh, $ctx);
+       (undef, undef, undef, undef);
 }
 
 package Git::SVN;
@@ -784,7 +826,7 @@ package Git::SVN;
 use warnings;
 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/;
+            $_use_svnsync_props $no_reuse_existing $_minimize_url/;
 use Carp qw/croak/;
 use File::Path qw/mkpath/;
 use File::Copy qw/copy/;
@@ -1001,7 +1043,7 @@ sub init_remote_config {
                                     "[svn-remote \"$existing\"]\n";
                }
                $self->{repo_id} = $existing;
-       } else {
+       } elsif ($_minimize_url) {
                my $min_url = Git::SVN::Ra->new($url)->minimize_url;
                $existing = find_existing_remote($min_url, $r);
                if ($existing) {
@@ -1054,7 +1096,10 @@ sub init_remote_config {
 
 sub find_by_url { # repos_root and, path are optional
        my ($class, $full_url, $repos_root, $path) = @_;
+
        return undef unless defined $full_url;
+       remove_username($full_url);
+       remove_username($repos_root) if defined $repos_root;
        my $remotes = read_all_remotes();
        if (defined $full_url && defined $repos_root && !defined $path) {
                $path = $full_url;
@@ -1062,6 +1107,7 @@ sub find_by_url { # repos_root and, path are optional
        }
        foreach my $repo_id (keys %$remotes) {
                my $u = $remotes->{$repo_id}->{url} or next;
+               remove_username($u);
                next if defined $repos_root && $repos_root ne $u;
 
                my $fetch = $remotes->{$repo_id}->{fetch} || {};
@@ -1324,8 +1370,10 @@ sub rel_path {
        my ($self) = @_;
        my $repos_root = $self->ra->{repos_root};
        return $self->{path} if ($self->{url} eq $repos_root);
-       die "BUG: rel_path failed! repos_root: $repos_root, Ra URL: ",
-           $self->ra->{url}, " path: $self->{path},  URL: $self->{url}\n";
+       my $url = $self->{url} .
+                 (length $self->{path} ? "/$self->{path}" : $self->{path});
+       $url =~ s!^\Q$repos_root\E(?:/+|$)!!g;
+       $url;
 }
 
 sub traverse_ignore {
@@ -1348,7 +1396,7 @@ sub traverse_ignore {
                }
        }
        foreach (sort keys %$dirent) {
-               next if $dirent->{$_}->kind != $SVN::Node::dir;
+               next if $dirent->{$_}->{kind} != $SVN::Node::dir;
                $self->traverse_ignore($fh, "$path/$_", $r);
        }
 }
@@ -1670,7 +1718,10 @@ sub find_parent_branch {
        }
        my ($r0, $parent) = $gs->find_rev_before($r, 1);
        if (!defined $r0 || !defined $parent) {
-               $gs->fetch(0, $r);
+               my ($base, $head) = parse_revision_argument(0, $r);
+               if ($base <= $r) {
+                       $gs->fetch($base, $r);
+               }
                ($r0, $parent) = $gs->last_rev_commit;
        }
        if (defined $r0 && defined $parent) {
@@ -1851,11 +1902,14 @@ sub make_log_entry {
        } elsif ($self->use_svnsync_props) {
                my $full_url = $self->svnsync->{url};
                $full_url .= "/$self->{path}" if length $self->{path};
+               remove_username($full_url);
                my $uuid = $self->svnsync->{uuid};
                $log_entry{metadata} = "$full_url\@$rev $uuid";
                $email ||= "$author\@$uuid"
        } else {
-               $log_entry{metadata} = $self->metadata_url. "\@$rev " .
+               my $url = $self->metadata_url;
+               remove_username($url);
+               $log_entry{metadata} = "$url\@$rev " .
                                       $self->ra->get_uuid;
                $email ||= "$author\@" . $self->ra->get_uuid;
        }
@@ -2418,15 +2472,19 @@ sub close_file {
        my $hash;
        my $path = $self->git_path($fb->{path});
        if (my $fh = $fb->{fh}) {
-               seek($fh, 0, 0) or croak $!;
-               my $md5 = Digest::MD5->new;
-               $md5->addfile($fh);
-               my $got = $md5->hexdigest;
-               die "Checksum mismatch: $path\n",
-                   "expected: $exp\n    got: $got\n" if ($got ne $exp);
-               seek($fh, 0, 0) or croak $!;
+               if (defined $exp) {
+                       seek($fh, 0, 0) or croak $!;
+                       my $md5 = Digest::MD5->new;
+                       $md5->addfile($fh);
+                       my $got = $md5->hexdigest;
+                       if ($got ne $exp) {
+                               die "Checksum mismatch: $path\n",
+                                   "expected: $exp\n    got: $got\n";
+                       }
+               }
+               sysseek($fh, 0, 0) or croak $!;
                if ($fb->{mode_b} == 120000) {
-                       read($fh, my $buf, 5) == 5 or croak $!;
+                       sysread($fh, my $buf, 5) == 5 or croak $!;
                        $buf eq 'link ' or die "$path has mode 120000",
                                               "but is not a link\n";
                }
@@ -2792,8 +2850,10 @@ sub close_edit {
        my ($self) = @_;
        my ($p,$bat) = ($self->{pool}, $self->{bat});
        foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {
+               next if $_ eq '';
                $self->close_directory($bat->{$_}, $p);
        }
+       $self->close_directory($bat->{''}, $p);
        $self->SUPER::close_edit($p);
        $p->clear;
 }
@@ -2835,13 +2895,12 @@ package Git::SVN::Ra;
 use vars qw/@ISA $config_dir $_log_window_size/;
 use strict;
 use warnings;
-my ($can_do_switch);
-my $RA;
+my ($can_do_switch, %ignored_err, $RA);
 
 BEGIN {
        # enforce temporary pool usage for some simple functions
        my $e;
-       foreach (qw/get_latest_revnum get_uuid get_repos_root/) {
+       foreach (qw/rev_proplist get_latest_revnum get_uuid get_repos_root/) {
                $e .= "sub $_ {
                        my \$self = shift;
                        my \$pool = SVN::Pool->new;
@@ -2850,36 +2909,13 @@ BEGIN
                        wantarray ? \@ret : \$ret[0]; }\n";
        }
 
-       # get_dir needs $pool held in cache for dirents to work,
-       # check_path is cacheable and rev_proplist is close enough
-       # for our purposes.
-       foreach (qw/check_path get_dir rev_proplist/) {
-               $e .= "my \%${_}_cache; my \$${_}_rev = 0; sub $_ {
-                       my \$self = shift;
-                       my \$r = pop;
-                       my \$k = join(\"\\0\", \@_);
-                       if (my \$x = \$${_}_cache{\$r}->{\$k}) {
-                               return wantarray ? \@\$x : \$x->[0];
-                       }
-                       my \$pool = SVN::Pool->new;
-                       my \@ret = \$self->SUPER::$_(\@_, \$r, \$pool);
-                       if (\$r != \$${_}_rev) {
-                               \%${_}_cache = ( pool => [] );
-                               \$${_}_rev = \$r;
-                       }
-                       \$${_}_cache{\$r}->{\$k} = \\\@ret;
-                       push \@{\$${_}_cache{pool}}, \$pool;
-                       wantarray ? \@ret : \$ret[0]; }\n";
-       }
-       $e .= "\n1;";
-       eval $e or die $@;
+       eval "$e; 1;" or die $@;
 }
 
 sub new {
        my ($class, $url) = @_;
        $url =~ s!/+$!!;
        return $RA if ($RA && $RA->{url} eq $url);
-       $RA->{pool}->clear if $RA;
 
        SVN::_Core::svn_config_ensure($config_dir, undef);
        my ($baton, $callbacks) = SVN::Core::auth_open_helper([
@@ -2905,9 +2941,47 @@ sub new {
        $self->{svn_path} = $url;
        $self->{repos_root} = $self->get_repos_root;
        $self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;
+       $self->{cache} = { check_path => { r => 0, data => {} },
+                          get_dir => { r => 0, data => {} } };
        $RA = bless $self, $class;
 }
 
+sub check_path {
+       my ($self, $path, $r) = @_;
+       my $cache = $self->{cache}->{check_path};
+       if ($r == $cache->{r} && exists $cache->{data}->{$path}) {
+               return $cache->{data}->{$path};
+       }
+       my $pool = SVN::Pool->new;
+       my $t = $self->SUPER::check_path($path, $r, $pool);
+       $pool->clear;
+       if ($r != $cache->{r}) {
+               %{$cache->{data}} = ();
+               $cache->{r} = $r;
+       }
+       $cache->{data}->{$path} = $t;
+}
+
+sub get_dir {
+       my ($self, $dir, $r) = @_;
+       my $cache = $self->{cache}->{get_dir};
+       if ($r == $cache->{r}) {
+               if (my $x = $cache->{data}->{$dir}) {
+                       return wantarray ? @$x : $x->[0];
+               }
+       }
+       my $pool = SVN::Pool->new;
+       my ($d, undef, $props) = $self->SUPER::get_dir($dir, $r, $pool);
+       my %dirents = map { $_ => { kind => $d->{$_}->kind } } keys %$d;
+       $pool->clear;
+       if ($r != $cache->{r}) {
+               %{$cache->{data}} = ();
+               $cache->{r} = $r;
+       }
+       $cache->{data}->{$dir} = [ \%dirents, $r, $props ];
+       wantarray ? (\%dirents, $r, $props) : \%dirents;
+}
+
 sub DESTROY {
        # do not call the real DESTROY since we store ourselves in $RA
 }
@@ -3122,7 +3196,7 @@ sub match_globs {
                return unless scalar @x == 3;
                my $dirents = $x[0];
                foreach my $de (keys %$dirents) {
-                       next if $dirents->{$de}->kind != $SVN::Node::dir;
+                       next if $dirents->{$de}->{kind} != $SVN::Node::dir;
                        my $p = $g->{path}->full_path($de);
                        next if $exists->{$p};
                        next if (length $g->{path}->{right} &&
@@ -3148,6 +3222,8 @@ sub match_globs {
                        my $p = $1;
                        my $pathname = $g->{path}->full_path($p);
                        next if $exists->{$pathname};
+                       next if ($self->check_path($pathname, $r) !=
+                                $SVN::Node::dir);
                        $exists->{$pathname} = Git::SVN->init(
                                              $self->{url}, $pathname, undef,
                                              $g->{ref}->full_path($p), 1);
@@ -3208,9 +3284,16 @@ sub skip_unknown_revs {
        # 175007 - http(s):// (this repo required authorization, too...)
        #   More codes may be discovered later...
        if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
-               warn "W: Ignoring error from SVN, path probably ",
-                    "does not exist: ($errno): ",
-                    $err->expanded_message,"\n";
+               my $err_key = $err->expanded_message;
+               # revision numbers change every time, filter them out
+               $err_key =~ s/\d+/\0/g;
+               $err_key = "$errno\0$err_key";
+               unless ($ignored_err{$err_key}) {
+                       warn "W: Ignoring error from SVN, path probably ",
+                            "does not exist: ($errno): ",
+                            $err->expanded_message,"\n";
+                       $ignored_err{$err_key} = 1;
+               }
                return;
        }
        die "Error from SVN, ($errno): ", $err->expanded_message,"\n";
@@ -3245,12 +3328,19 @@ package Git::SVN::Log;
 sub cmt_showable {
        my ($c) = @_;
        return 1 if defined $c->{r};
+
+       # big commit message got truncated by the 16k pretty buffer in rev-list
        if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
                                $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
+               @{$c->{l}} = ();
                my @log = command(qw/cat-file commit/, $c->{c});
-               shift @log while ($log[0] ne "\n");
+
+               # shift off the headers
+               shift @log while ($log[0] ne '');
                shift @log;
-               @{$c->{l}} = grep !/^git-svn-id: /, @log;
+
+               # TODO: make $c->{l} not have a trailing newline in the future
+               @{$c->{l}} = map { "$_\n" } grep !/^git-svn-id: /, @log;
 
                (undef, $c->{r}, undef) = ::extract_metadata(
                                (grep(/^git-svn-id: /, @log))[-1]);
@@ -3304,8 +3394,8 @@ sub git_svn_log_cmd {
                last;
        }
 
-       my $url = (::working_head_info($head))[0];
-       my $gs = Git::SVN->find_by_url($url) || Git::SVN->_new;
+       my ($url, $rev, $uuid, $gs) = ::working_head_info($head);
+       $gs ||= Git::SVN->_new;
        my @cmd = (qw/log --abbrev-commit --pretty=raw --default/,
                   $gs->refname);
        push @cmd, '-r' unless $non_recursive;