git-completion: add "git stash"
[gitweb.git] / git-svn.perl
index 50128d72850714b80a765ad9c5ffa9588b8da505..ee7ef693fa9300082ca090ebf97f7e242043fb72 100755 (executable)
@@ -374,16 +374,9 @@ sub cmd_dcommit {
                die "Unable to determine upstream SVN information from ",
                    "$head history\n";
        }
-       my $c = $refs[-1];
        my $last_rev;
-       foreach my $d (@refs) {
-               if (!verify_ref("$d~1")) {
-                       fatal "Commit $d\n",
-                             "has no parent commit, and therefore ",
-                             "nothing to diff against.\n",
-                             "You should be working from a repository ",
-                             "originally created by git-svn\n";
-               }
+       my ($linear_refs, $parents) = linearize_history($gs, \@refs);
+       foreach my $d (@$linear_refs) {
                unless (defined $last_rev) {
                        (undef, $last_rev, undef) = cmt_metadata("$d~1");
                        unless (defined $last_rev) {
@@ -405,6 +398,9 @@ sub cmd_dcommit {
                                        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} =
+                                                              $parents->{$d};
                        }
                }
        }
@@ -596,8 +592,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  ",
@@ -745,7 +740,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;
@@ -787,12 +782,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);
 }
@@ -804,25 +799,87 @@ 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', $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);
 }
 
+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
+}
+
+sub linearize_history {
+       my ($gs, $refs) = @_;
+       my %parents;
+       foreach my $c (@$refs) {
+               read_commit_parents(\%parents, $c);
+       }
+
+       my @linear_refs;
+       my %skip = ();
+       my $last_svn_commit = $gs->last_commit;
+       foreach my $c (reverse @$refs) {
+               next if $c eq $last_svn_commit;
+               last if $skip{$c};
+
+               unshift @linear_refs, $c;
+               $skip{$c} = 1;
+
+               # we only want the first parent to diff against for linear
+               # history, we save the rest to inject when we finalize the
+               # svn commit
+               my $fp_a = verify_ref("$c~1");
+               my $fp_b = shift @{$parents{$c}} if $parents{$c};
+               if (!$fp_a || !$fp_b) {
+                       die "Commit $c\n",
+                           "has no parent commit, and therefore ",
+                           "nothing to diff against.\n",
+                           "You should be working from a repository ",
+                           "originally created by git-svn\n";
+               }
+               if ($fp_a ne $fp_b) {
+                       die "$c~1 = $fp_a, however parsing commit $c ",
+                           "revealed that:\n$c~1 = $fp_b\nBUG!\n";
+               }
+
+               foreach my $p (@{$parents{$c}}) {
+                       $skip{$p} = 1;
+               }
+       }
+       (\@linear_refs, \%parents);
+}
+
 package Git::SVN;
 use strict;
 use warnings;
@@ -881,8 +938,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",
@@ -969,7 +1026,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)=
@@ -1089,6 +1148,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);
@@ -1179,7 +1239,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) = @_;
@@ -1543,6 +1636,11 @@ sub get_commit_parents {
        if (my $cur = ::verify_ref($self->refname.'^0')) {
                push @tmp, $cur;
        }
+       if (my $ipd = $self->{inject_parents_dcommit}) {
+               if (my $commit = delete $ipd->{$log_entry->{revision}}) {
+                       push @tmp, @$commit;
+               }
+       }
        push @tmp, $_ foreach (@{$log_entry->{parents}}, @tmp);
        while (my $p = shift @tmp) {
                next if $seen{$p};
@@ -1966,16 +2064,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", $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)
@@ -1993,7 +2094,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";
 }
 
@@ -2656,6 +2757,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);
 }
 
@@ -2925,6 +3029,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(
@@ -2936,6 +3041,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,