Paper bag fix diff invocation in 'git stash show'
[gitweb.git] / git-svn.perl
index 42906767a10e848031146c4f7eb862c40b67385c..c015ea8580e8e19dc27a0f66af38995335df8c82 100755 (executable)
 use Git;
 
 BEGIN {
-       my $s;
+       # import functions from Git into our packages, en masse
+       no strict 'refs';
        foreach (qw/command command_oneline command_noisy command_output_pipe
                    command_input_pipe command_close_pipe/) {
-               $s .= "*SVN::Git::Editor::$_ = *SVN::Git::Fetcher::$_ = ".
-                     "*Git::SVN::Migration::$_ = ".
-                     "*Git::SVN::Log::$_ = *Git::SVN::$_ = *$_ = *Git::$_; ";
+               for my $package ( qw(SVN::Git::Editor SVN::Git::Fetcher
+                       Git::SVN::Migration Git::SVN::Log Git::SVN),
+                       __PACKAGE__) {
+                       *{"${package}::$_"} = \&{"Git::$_"};
+               }
        }
-       eval $s;
 }
 
 my ($SVN);
@@ -75,11 +77,12 @@ BEGIN
                   \$Git::SVN::_repack_flags,
                %remote_opts );
 
-my ($_trunk, $_tags, $_branches);
+my ($_trunk, $_tags, $_branches, $_stdlayout);
 my %icv;
 my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared,
                   'trunk|T=s' => \$_trunk, 'tags|t=s' => \$_tags,
                   'branches|b=s' => \$_branches, 'prefix=s' => \$_prefix,
+                  'stdlayout|s' => \$_stdlayout,
                   'minimize-url|m' => \$Git::SVN::_minimize_url,
                  'no-metadata' => sub { $icv{noMetadata} = 1 },
                  'use-svm-props' => sub { $icv{useSvmProps} = 1 },
@@ -290,7 +293,8 @@ sub init_subdir {
 sub cmd_clone {
        my ($url, $path) = @_;
        if (!defined $path &&
-           (defined $_trunk || defined $_branches || defined $_tags) &&
+           (defined $_trunk || defined $_branches || defined $_tags ||
+            defined $_stdlayout) &&
            $url !~ m#^[a-z\+]+://#) {
                $path = $url;
        }
@@ -300,6 +304,11 @@ sub cmd_clone {
 }
 
 sub cmd_init {
+       if (defined $_stdlayout) {
+               $_trunk = 'trunk' if (!defined $_trunk);
+               $_tags = 'tags' if (!defined $_tags);
+               $_branches = 'branches' if (!defined $_branches);
+       }
        if (defined $_trunk || defined $_branches || defined $_tags) {
                return cmd_multi_init(@_);
        }
@@ -368,12 +377,19 @@ sub cmd_dcommit {
        $head ||= 'HEAD';
        my @refs;
        my ($url, $rev, $uuid, $gs) = working_head_info($head, \@refs);
+       print "Committing to $url ...\n";
        unless ($gs) {
                die "Unable to determine upstream SVN information from ",
                    "$head history\n";
        }
        my $last_rev;
        my ($linear_refs, $parents) = linearize_history($gs, \@refs);
+       if ($_no_rebase && scalar(@$linear_refs) > 1) {
+               warn "Attempting to commit more than one change while ",
+                    "--no-rebase is enabled.\n",
+                    "If these changes depend on each other, re-running ",
+                    "without --no-rebase will be required."
+       }
        foreach my $d (@$linear_refs) {
                unless (defined $last_rev) {
                        (undef, $last_rev, undef) = cmt_metadata("$d~1");
@@ -385,6 +401,7 @@ sub cmd_dcommit {
                if ($_dry_run) {
                        print "diff-tree $d~1 $d\n";
                } else {
+                       my $cmt_rev;
                        my %ed_opts = ( r => $last_rev,
                                        log => get_commit_entry($d)->{log},
                                        ra => Git::SVN::Ra->new($gs->full_url),
@@ -392,42 +409,39 @@ sub cmd_dcommit {
                                        tree_b => $d,
                                        editor_cb => sub {
                                               print "Committed r$_[0]\n";
-                                              $last_rev = $_[0]; },
+                                              $cmt_rev = $_[0];
+                                       },
                                        svn_path => '');
                        if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
                                print "No changes\n$d~1 == $d\n";
                        } elsif ($parents->{$d} && @{$parents->{$d}}) {
-                               $gs->{inject_parents_dcommit}->{$last_rev} =
+                               $gs->{inject_parents_dcommit}->{$cmt_rev} =
                                                               $parents->{$d};
                        }
+                       $_fetch_all ? $gs->fetch_all : $gs->fetch;
+                       next if $_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);
+                       $last_rev = $cmt_rev;
                }
        }
-       return if $_dry_run;
-       unless ($gs) {
-               warn "Could not determine fetch information for $url\n",
-                    "Will not attempt to fetch and rebase commits.\n",
-                    "This probably means you have useSvmProps and should\n",
-                    "now resync your SVN::Mirror repository.\n";
-               return;
-       }
-       $_fetch_all ? $gs->fetch_all : $gs->fetch;
-       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 {
@@ -590,8 +604,7 @@ sub post_fetch_checkout {
        my $index = $ENV{GIT_INDEX_FILE} || "$ENV{GIT_DIR}/index";
        return if -f $index;
 
-       chomp(my $bare = `git config --bool --get core.bare`);
-       return if $bare eq 'true';
+       return if command_oneline(qw/rev-parse --is-inside-work-tree/) eq 'false';
        return if command_oneline(qw/rev-parse --is-inside-git-dir/) eq 'true';
        command_noisy(qw/read-tree -m -u -v HEAD HEAD/);
        print STDERR "Checked out HEAD:\n  ",
@@ -739,7 +752,7 @@ sub load_authors {
        my $log = $cmd eq 'log';
        while (<$authors>) {
                chomp;
-               next unless /^(\S+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
+               next unless /^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
                my ($user, $name, $email) = ($1, $2, $3);
                if ($log) {
                        $Git::SVN::Log::rusers{"$name <$email>"} = $user;
@@ -781,12 +794,12 @@ sub read_repo_config {
 
 sub extract_metadata {
        my $id = shift or return (undef, undef, undef);
-       my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
+       my ($url, $rev, $uuid) = ($id =~ /^\s*git-svn-id:\s+(.*)\@(\d+)
                                                        \s([a-f\d\-]+)$/x);
        if (!defined $rev || !$uuid || !$url) {
                # some of the original repositories I made had
                # identifiers like this:
-               ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
+               ($rev, $uuid) = ($id =~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
        }
        return ($url, $rev, $uuid);
 }
@@ -798,20 +811,29 @@ sub cmt_metadata {
 
 sub working_head_info {
        my ($head, $refs) = @_;
-       my ($fh, $ctx) = command_output_pipe('rev-list', $head);
-       while (my $hash = <$fh>) {
-               chomp($hash);
-               my ($url, $rev, $uuid) = cmt_metadata($hash);
+       my ($fh, $ctx) = command_output_pipe('log', '--no-color', $head);
+       my $hash;
+       my %max;
+       while (<$fh>) {
+               if ( m{^commit ($::sha1)$} ) {
+                       unshift @$refs, $hash if $hash and $refs;
+                       $hash = $1;
+                       next;
+               }
+               next unless s{^\s*(git-svn-id:)}{$1};
+               my ($url, $rev, $uuid) = extract_metadata($_);
                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_db_get($rev);
                                if ($c && $c eq $hash) {
                                        close $fh; # break the pipe
                                        return ($url, $rev, $uuid, $gs);
+                               } else {
+                                       $max{$url} ||= $gs->rev_db_max;
                                }
                        }
                }
-               unshift @$refs, $hash if $refs;
        }
        command_close_pipe($fh, $ctx);
        (undef, undef, undef, undef);
@@ -819,14 +841,9 @@ sub working_head_info {
 
 sub read_commit_parents {
        my ($parents, $c) = @_;
-       my ($fh, $ctx) = command_output_pipe(qw/cat-file commit/, $c);
-       while (<$fh>) {
-               chomp;
-               last if '';
-               /^parent ($sha1)/ or next;
-               push @{$parents->{$c}}, $1;
-       }
-       close $fh; # break the pipe
+       chomp(my $p = command_oneline(qw/rev-list --parents -1/, $c));
+       $p =~ s/^($c)\s*// or die "rev-list --parents -1 $c failed!\n";
+       @{$parents->{$c}} = split(/ /, $p);
 }
 
 sub linearize_history {
@@ -895,26 +912,26 @@ BEGIN
        # some options are read globally, but can be overridden locally
        # per [svn-remote "..."] section.  Command-line options will *NOT*
        # override options set in an [svn-remote "..."] section
-       my $e;
-       foreach (qw/follow_parent no_metadata use_svm_props
-                   use_svnsync_props/) {
-               my $key = $_;
+       no strict 'refs';
+       for my $option (qw/follow_parent no_metadata use_svm_props
+                          use_svnsync_props/) {
+               my $key = $option;
                $key =~ tr/_//d;
-               $e .= "sub $_ {
-                       my (\$self) = \@_;
-                       return \$self->{-$_} if exists \$self->{-$_};
-                       my \$k = \"svn-remote.\$self->{repo_id}\.$key\";
-                       eval { command_oneline(qw/config --get/, \$k) };
-                       if (\$@) {
-                               \$self->{-$_} = \$Git::SVN::_$_;
+               my $prop = "-$option";
+               *$option = sub {
+                       my ($self) = @_;
+                       return $self->{$prop} if exists $self->{$prop};
+                       my $k = "svn-remote.$self->{repo_id}.$key";
+                       eval { command_oneline(qw/config --get/, $k) };
+                       if ($@) {
+                               $self->{$prop} = ${"Git::SVN::_$option"};
                        } else {
-                               my \$v = command_oneline(qw/config --bool/,\$k);
-                               \$self->{-$_} = \$v eq 'false' ? 0 : 1;
+                               my $v = command_oneline(qw/config --bool/,$k);
+                               $self->{$prop} = $v eq 'false' ? 0 : 1;
                        }
-                       return \$self->{-$_} }\n";
+                       return $self->{$prop};
+               }
        }
-       $e .= "1;\n";
-       eval $e or die $@;
 }
 
 my %LOCKFILES;
@@ -928,8 +945,8 @@ sub resolve_local_globs {
        foreach (command(qw#for-each-ref --format=%(refname) refs/remotes#)) {
                next unless m#^refs/remotes/$ref->{regex}$#;
                my $p = $1;
-               my $pathname = $path->full_path($p);
-               my $refname = $ref->full_path($p);
+               my $pathname = desanitize_refname($path->full_path($p));
+               my $refname = desanitize_refname($ref->full_path($p));
                if (my $existing = $fetch->{$pathname}) {
                        if ($existing ne $refname) {
                                die "Refspec conflict:\n",
@@ -1016,7 +1033,9 @@ sub read_all_remotes {
        my $r = {};
        foreach (grep { s/^svn-remote\.// } command(qw/config -l/)) {
                if (m!^(.+)\.fetch=\s*(.*)\s*:\s*refs/remotes/(.+)\s*$!) {
-                       $r->{$1}->{fetch}->{$2} = $3;
+                       my ($remote, $local_ref, $remote_ref) = ($1, $2, $3);
+                       $local_ref =~ s{^/}{};
+                       $r->{$remote}->{fetch}->{$local_ref} = $remote_ref;
                } elsif (m!^(.+)\.url=\s*(.*)\s*$!) {
                        $r->{$1}->{url} = $2;
                } elsif (m!^(.+)\.(branches|tags)=
@@ -1136,6 +1155,7 @@ sub init_remote_config {
        unless ($no_write) {
                command_noisy('config',
                              "svn-remote.$self->{repo_id}.url", $url);
+               $self->{path} =~ s{^/}{};
                command_noisy('config', '--add',
                              "svn-remote.$self->{repo_id}.fetch",
                              "$self->{path}:".$self->refname);
@@ -1226,7 +1246,40 @@ sub new {
        $self;
 }
 
-sub refname { "refs/remotes/$_[0]->{ref_id}" }
+sub refname {
+       my ($refname) = "refs/remotes/$_[0]->{ref_id}" ;
+
+       # It cannot end with a slash /, we'll throw up on this because
+       # SVN can't have directories with a slash in their name, either:
+       if ($refname =~ m{/$}) {
+               die "ref: '$refname' ends with a trailing slash, this is ",
+                   "not permitted by git nor Subversion\n";
+       }
+
+       # It cannot have ASCII control character space, tilde ~, caret ^,
+       # colon :, question-mark ?, asterisk *, space, or open bracket [
+       # anywhere.
+       #
+       # Additionally, % must be escaped because it is used for escaping
+       # and we want our escaped refname to be reversible
+       $refname =~ s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;
+
+       # no slash-separated component can begin with a dot .
+       # /.* becomes /%2E*
+       $refname =~ s{/\.}{/%2E}g;
+
+       # It cannot have two consecutive dots .. anywhere
+       # .. becomes %2E%2E
+       $refname =~ s{\.\.}{%2E%2E}g;
+
+       return $refname;
+}
+
+sub desanitize_refname {
+       my ($refname) = @_;
+       $refname =~ s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;
+       return $refname;
+}
 
 sub svm_uuid {
        my ($self) = @_;
@@ -2018,16 +2071,19 @@ sub rebuild {
                return;
        }
        print "Rebuilding $db_path ...\n";
-       my ($rev_list, $ctx) = command_output_pipe("rev-list", $self->refname);
+       my ($log, $ctx) = command_output_pipe("log", '--no-color', $self->refname);
        my $latest;
        my $full_url = $self->full_url;
        remove_username($full_url);
        my $svn_uuid;
-       while (<$rev_list>) {
-               chomp;
-               my $c = $_;
-               die "Non-SHA1: $c\n" unless $c =~ /^$::sha1$/o;
-               my ($url, $rev, $uuid) = ::cmt_metadata($c);
+       my $c;
+       while (<$log>) {
+               if ( m{^commit ($::sha1)$} ) {
+                       $c = $1;
+                       next;
+               }
+               next unless s{^\s*(git-svn-id:)}{$1};
+               my ($url, $rev, $uuid) = ::extract_metadata($_);
                remove_username($url);
 
                # ignore merges (from set-tree)
@@ -2045,7 +2101,7 @@ sub rebuild {
                $self->rev_db_set($rev, $c);
                print "r$rev = $c\n";
        }
-       command_close_pipe($rev_list, $ctx);
+       command_close_pipe($log, $ctx);
        print "Done rebuilding $db_path\n";
 }
 
@@ -2708,6 +2764,9 @@ sub repo_path {
 
 sub url_path {
        my ($self, $path) = @_;
+       if ($self->{url} =~ m#^https?://#) {
+               $path =~ s/([^a-zA-Z0-9_.-])/uc sprintf("%%%02x",ord($1))/eg;
+       }
        $self->{url} . '/' . $self->repo_path($path);
 }
 
@@ -2949,21 +3008,21 @@ package Git::SVN::Ra;
 use vars qw/@ISA $config_dir $_log_window_size/;
 use strict;
 use warnings;
-my ($can_do_switch, %ignored_err, $RA);
+my ($ra_invalid, $can_do_switch, %ignored_err, $RA);
 
 BEGIN {
        # enforce temporary pool usage for some simple functions
-       my $e;
-       foreach (qw/rev_proplist get_latest_revnum get_uuid get_repos_root/) {
-               $e .= "sub $_ {
-                       my \$self = shift;
-                       my \$pool = SVN::Pool->new;
-                       my \@ret = \$self->SUPER::$_(\@_,\$pool);
-                       \$pool->clear;
-                       wantarray ? \@ret : \$ret[0]; }\n";
+       no strict 'refs';
+       for my $f (qw/rev_proplist get_latest_revnum get_uuid get_repos_root/) {
+               my $SUPER = "SUPER::$f";
+               *$f = sub {
+                       my $self = shift;
+                       my $pool = SVN::Pool->new;
+                       my @ret = $self->$SUPER(@_,$pool);
+                       $pool->clear;
+                       wantarray ? @ret : $ret[0];
+               };
        }
-
-       eval "$e; 1;" or die $@;
 }
 
 sub new {
@@ -2977,6 +3036,7 @@ sub new {
            SVN::Client::get_ssl_server_trust_file_provider(),
            SVN::Client::get_simple_prompt_provider(
              \&Git::SVN::Prompt::simple, 2),
+           SVN::Client::get_ssl_client_cert_file_provider(),
            SVN::Client::get_ssl_client_cert_prompt_provider(
              \&Git::SVN::Prompt::ssl_client_cert, 2),
            SVN::Client::get_ssl_client_cert_pw_prompt_provider(
@@ -2988,6 +3048,7 @@ sub new {
              \&Git::SVN::Prompt::username, 2),
          ]);
        my $config = SVN::Core::config_get_config($config_dir);
+       $RA = undef;
        my $self = SVN::Ra->new(url => $url, auth => $baton,
                              config => $config,
                              pool => SVN::Pool->new,
@@ -3108,7 +3169,11 @@ sub gs_do_switch {
                        $self->{url} = $full_url;
                        $reparented = 1;
                } else {
+                       $_[0] = undef;
+                       $self = undef;
+                       $RA = undef;
                        $ra = Git::SVN::Ra->new($full_url);
+                       $ra_invalid = 1;
                }
        }
        $ra ||= $self;
@@ -3168,6 +3233,7 @@ sub gs_fetch_loop_common {
        my $inc = $_log_window_size;
        my ($min, $max) = ($base, $head < $base + $inc ? $head : $base + $inc);
        my $longest_path = longest_common_path($gsv, $globs);
+       my $ra_url = $self->{url};
        while (1) {
                my %revs;
                my $err;
@@ -3229,6 +3295,13 @@ sub gs_fetch_loop_common {
                                        "$g->{t}-maxRev";
                                Git::SVN::tmp_config($k, $r);
                        }
+                       if ($ra_invalid) {
+                               $_[0] = undef;
+                               $self = undef;
+                               $RA = undef;
+                               $self = Git::SVN::Ra->new($ra_url);
+                               $ra_invalid = undef;
+                       }
                }
                # pre-fill the .rev_db since it'll eventually get filled in
                # with '0' x40 if something new gets committed
@@ -3447,11 +3520,17 @@ sub log_use_color {
 sub git_svn_log_cmd {
        my ($r_min, $r_max, @args) = @_;
        my $head = 'HEAD';
+       my (@files, @log_opts);
        foreach my $x (@args) {
-               last if $x eq '--';
-               next unless ::verify_ref("$x^0");
-               $head = $x;
-               last;
+               if ($x eq '--' || @files) {
+                       push @files, $x;
+               } else {
+                       if (::verify_ref("$x^0")) {
+                               $head = $x;
+                       } else {
+                               push @log_opts, $x;
+                       }
+               }
        }
 
        my ($url, $rev, $uuid, $gs) = ::working_head_info($head);
@@ -3461,13 +3540,13 @@ sub git_svn_log_cmd {
        push @cmd, '-r' unless $non_recursive;
        push @cmd, qw/--raw --name-status/ if $verbose;
        push @cmd, '--color' if log_use_color();
-       return @cmd unless defined $r_max;
-       if ($r_max == $r_min) {
+       push @cmd, @log_opts;
+       if (defined $r_max && $r_max == $r_min) {
                push @cmd, '--max-count=1';
                if (my $c = $gs->rev_db_get($r_max)) {
                        push @cmd, $c;
                }
-       } else {
+       } elsif (defined $r_max) {
                my ($c_min, $c_max);
                $c_max = $gs->rev_db_get($r_max);
                $c_min = $gs->rev_db_get($r_min);
@@ -3483,7 +3562,7 @@ sub git_svn_log_cmd {
                        push @cmd, $c_min;
                }
        }
-       return @cmd;
+       return (@cmd, @files);
 }
 
 # adapted from pager.c
@@ -3497,7 +3576,7 @@ sub config_pager {
 }
 
 sub run_pager {
-       return unless -t *STDOUT;
+       return unless -t *STDOUT && defined $pager;
        pipe my $rfd, my $wfd or return;
        defined(my $pid = fork) or ::fatal "Can't fork: $!\n";
        if (!$pid) {
@@ -3648,7 +3727,7 @@ sub cmd_show_log {
        }
 
        config_pager();
-       @args = (git_svn_log_cmd($r_min, $r_max, @args), @args);
+       @args = git_svn_log_cmd($r_min, $r_max, @args);
        my $log = command_output_pipe(@args);
        run_pager();
        my (@k, $c, $d, $stat);