Code

Merge branch 'maint'
[git.git] / git-svn.perl
index 351e743a902568c8b6f68ffaf4030f087ba8568d..4334b95f70fab5c3cb9e446a9d6c418748b95bca 100755 (executable)
@@ -22,14 +22,13 @@ $Git::SVN::default_ref_id = $ENV{GIT_SVN_ID} || 'git-svn';
 $Git::SVN::Ra::_log_window_size = 100;
 $Git::SVN::_minimize_url = 'unset';
 
-if (! exists $ENV{SVN_SSH}) {
-       if (exists $ENV{GIT_SSH}) {
-               $ENV{SVN_SSH} = $ENV{GIT_SSH};
-               if ($^O eq 'msys') {
-                       $ENV{SVN_SSH} =~ s/\\/\\\\/g;
-                       $ENV{SVN_SSH} =~ s/(.*)/"$1"/;
-               }
-       }
+if (! exists $ENV{SVN_SSH} && exists $ENV{GIT_SSH}) {
+       $ENV{SVN_SSH} = $ENV{GIT_SSH};
+}
+
+if (exists $ENV{SVN_SSH} && $^O eq 'msys') {
+       $ENV{SVN_SSH} =~ s/\\/\\\\/g;
+       $ENV{SVN_SSH} =~ s/(.*)/"$1"/;
 }
 
 $Git::SVN::Log::TZ = $ENV{TZ};
@@ -87,14 +86,15 @@ my ($_stdin, $_help, $_edit,
        $_version, $_fetch_all, $_no_rebase, $_fetch_parent,
        $_merge, $_strategy, $_dry_run, $_local,
        $_prefix, $_no_checkout, $_url, $_verbose,
-       $_git_format, $_commit_url, $_tag, $_merge_info);
+       $_git_format, $_commit_url, $_tag, $_merge_info, $_interactive);
 $Git::SVN::_follow_parent = 1;
 $SVN::Git::Fetcher::_placeholder_filename = ".gitignore";
 $_q ||= 0;
 my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
                     'config-dir=s' => \$Git::SVN::Ra::config_dir,
                     'no-auth-cache' => \$Git::SVN::Prompt::_no_auth_cache,
-                    'ignore-paths=s' => \$SVN::Git::Fetcher::_ignore_regex );
+                    'ignore-paths=s' => \$SVN::Git::Fetcher::_ignore_regex,
+                    'ignore-refs=s' => \$Git::SVN::Ra::_ignore_refs_regex );
 my %fc_opts = ( 'follow-parent|follow!' => \$Git::SVN::_follow_parent,
                'authors-file|A=s' => \$_authors,
                'authors-prog=s' => \$_authors_prog,
@@ -163,6 +163,7 @@ my %cmd = (
                          'revision|r=i' => \$_revision,
                          'no-rebase' => \$_no_rebase,
                          'mergeinfo=s' => \$_merge_info,
+                         'interactive|i' => \$_interactive,
                        %cmt_opts, %fc_opts } ],
        branch => [ \&cmd_branch,
                    'Create a branch in the SVN repository',
@@ -256,6 +257,27 @@ my %cmd = (
                {} ],
 );
 
+use Term::ReadLine;
+package FakeTerm;
+sub new {
+       my ($class, $reason) = @_;
+       return bless \$reason, shift;
+}
+sub readline {
+       my $self = shift;
+       die "Cannot use readline on FakeTerm: $$self";
+}
+package main;
+
+my $term = eval {
+       $ENV{"GIT_SVN_NOTTY"}
+               ? new Term::ReadLine 'git-svn', \*STDIN, \*STDOUT
+               : new Term::ReadLine 'git-svn';
+};
+if ($@) {
+       $term = new FakeTerm "$@: going non-interactive";
+}
+
 my $cmd;
 for (my $i = 0; $i < @ARGV; $i++) {
        if (defined $cmd{$ARGV[$i]}) {
@@ -299,7 +321,7 @@ read_git_config(\%opts);
 if ($cmd && ($cmd eq 'log' || $cmd eq 'blame')) {
        Getopt::Long::Configure('pass_through');
 }
-my $rv = GetOptions(%opts, 'help|H|h' => \$_help, 'version|V' => \$_version,
+my $rv = GetOptions(%opts, '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 {
@@ -366,6 +388,36 @@ sub version {
        exit 0;
 }
 
+sub ask {
+       my ($prompt, %arg) = @_;
+       my $valid_re = $arg{valid_re};
+       my $default = $arg{default};
+       my $resp;
+       my $i = 0;
+
+       if ( !( defined($term->IN)
+            && defined( fileno($term->IN) )
+            && defined( $term->OUT )
+            && defined( fileno($term->OUT) ) ) ){
+               return defined($default) ? $default : undef;
+       }
+
+       while ($i++ < 10) {
+               $resp = $term->readline($prompt);
+               if (!defined $resp) { # EOF
+                       print "\n";
+                       return defined $default ? $default : undef;
+               }
+               if ($resp eq '' and defined $default) {
+                       return $default;
+               }
+               if (!defined $valid_re or $resp =~ /$valid_re/) {
+                       return $resp;
+               }
+       }
+       return undef;
+}
+
 sub do_git_init_db {
        unless (-d $ENV{GIT_DIR}) {
                my @init_db = ('init');
@@ -388,9 +440,12 @@ sub do_git_init_db {
                command_noisy('config', "$pfx.$i", $icv{$i});
                $set = $i;
        }
-       my $ignore_regex = \$SVN::Git::Fetcher::_ignore_regex;
-       command_noisy('config', "$pfx.ignore-paths", $$ignore_regex)
-               if defined $$ignore_regex;
+       my $ignore_paths_regex = \$SVN::Git::Fetcher::_ignore_regex;
+       command_noisy('config', "$pfx.ignore-paths", $$ignore_paths_regex)
+               if defined $$ignore_paths_regex;
+       my $ignore_refs_regex = \$Git::SVN::Ra::_ignore_refs_regex;
+       command_noisy('config', "$pfx.ignore-refs", $$ignore_refs_regex)
+               if defined $$ignore_refs_regex;
 
        if (defined $SVN::Git::Fetcher::_preserve_empty_dirs) {
                my $fname = \$SVN::Git::Fetcher::_placeholder_filename;
@@ -629,7 +684,7 @@ sub populate_merge_info {
                                fatal "merge commit $d has ancestor $parent, but that change "
                      ."does not have git-svn metadata!";
                        }
-                       unless ($branchurl =~ /^$rooturl(.*)/) {
+                       unless ($branchurl =~ /^\Q$rooturl\E(.*)/) {
                                fatal "commit $parent git-svn metadata changed mid-run!";
                        }
                        my $branchpath = $1;
@@ -746,6 +801,27 @@ sub cmd_dcommit {
                     "If these changes depend on each other, re-running ",
                     "without --no-rebase may be required."
        }
+
+       if (defined $_interactive){
+               my $ask_default = "y";
+               foreach my $d (@$linear_refs){
+                       my ($fh, $ctx) = command_output_pipe(qw(show --summary), "$d");
+                       while (<$fh>){
+                               print $_;
+                       }
+                       command_close_pipe($fh, $ctx);
+                       $_ = ask("Commit this patch to SVN? ([y]es (default)|[n]o|[q]uit|[a]ll): ",
+                                valid_re => qr/^(?:yes|y|no|n|quit|q|all|a)/i,
+                                default => $ask_default);
+                       die "Commit this patch reply required" unless defined $_;
+                       if (/^[nq]/i) {
+                               exit(0);
+                       } elsif (/^a/i) {
+                               last;
+                       }
+               }
+       }
+
        my $expect_url = $url;
 
        my $push_merge_info = eval {
@@ -791,7 +867,7 @@ sub cmd_dcommit {
                                                         ."has uuid $uuid!";
                                        }
 
-                                       unless ($branchurl =~ /^$rooturl(.*)/) {
+                                       unless ($branchurl =~ /^\Q$rooturl\E(.*)/) {
                                                # This branch is very strange indeed.
                                                fatal "merge parent $parent for $d is on branch "
                                                         ."$branchurl, which is not under the "
@@ -1802,8 +1878,7 @@ sub cmt_sha2rev_batch {
 
 sub working_head_info {
        my ($head, $refs) = @_;
-       my @args = qw/log --no-color --no-decorate --first-parent
-                     --pretty=medium/;
+       my @args = qw/rev-list --first-parent --pretty=medium/;
        my ($fh, $ctx) = command_output_pipe(@args, $head);
        my $hash;
        my %max;
@@ -1953,6 +2028,7 @@ use Carp qw/croak/;
 use File::Path qw/mkpath/;
 use File::Copy qw/copy/;
 use IPC::Open3;
+use Time::Local;
 use Memoize;  # core since 5.8.0, Jul 2002
 use Memoize::Storable;
 
@@ -2119,6 +2195,8 @@ sub read_all_remotes {
                        $r->{$1}->{url} = $2;
                } elsif (m!^(.+)\.pushurl=\s*(.*)\s*$!) {
                        $r->{$1}->{pushurl} = $2;
+               } elsif (m!^(.+)\.ignore-refs=\s*(.*)\s*$!) {
+                       $r->{$1}->{ignore_refs_regex} = $2;
                } elsif (m!^(.+)\.(branches|tags)=$svn_refspec$!) {
                        my ($remote, $t, $local_ref, $remote_ref) =
                                                             ($1, $2, $3, $4);
@@ -2155,6 +2233,16 @@ sub read_all_remotes {
                }
        } keys %$r;
 
+       foreach my $remote (keys %$r) {
+               foreach ( grep { defined $_ }
+                         map { $r->{$remote}->{$_} } qw(branches tags) ) {
+                       foreach my $rs ( @$_ ) {
+                               $rs->{ignore_refs_regex} =
+                                   $r->{$remote}->{ignore_refs_regex};
+                       }
+               }
+       }
+
        $r;
 }
 
@@ -3199,6 +3287,14 @@ sub get_untracked {
        \@out;
 }
 
+sub get_tz {
+       # some systmes don't handle or mishandle %z, so be creative.
+       my $t = shift || time;
+       my $gm = timelocal(gmtime($t));
+       my $sign = qw( + + - )[ $t <=> $gm ];
+       return sprintf("%s%02d%02d", $sign, (gmtime(abs($t - $gm)))[2,1]);
+}
+
 # parse_svn_date(DATE)
 # --------------------
 # Given a date (in UTC) from Subversion, return a string in the format
@@ -3231,8 +3327,7 @@ sub parse_svn_date {
                        delete $ENV{TZ};
                }
 
-               my $our_TZ =
-                   POSIX::strftime('%Z', $S, $M, $H, $d, $m - 1, $Y - 1900);
+               my $our_TZ = get_tz();
 
                # This converts $epoch_in_UTC into our local timezone.
                my ($sec, $min, $hour, $mday, $mon, $year,
@@ -3832,7 +3927,7 @@ sub rebuild {
        my ($base_rev, $head) = ($partial ? $self->rev_map_max_norebuild(1) :
                (undef, undef));
        my ($log, $ctx) =
-           command_output_pipe(qw/rev-list --pretty=raw --no-color --reverse/,
+           command_output_pipe(qw/rev-list --pretty=raw --reverse/,
                                ($head ? "$head.." : "") . $self->refname,
                                '--');
        my $metadata_url = $self->metadata_url;
@@ -5042,7 +5137,7 @@ sub rmdirs {
 }
 
 sub open_or_add_dir {
-       my ($self, $full_path, $baton) = @_;
+       my ($self, $full_path, $baton, $deletions) = @_;
        my $t = $self->{types}->{$full_path};
        if (!defined $t) {
                die "$full_path not known in r$self->{r} or we have a bug!\n";
@@ -5051,7 +5146,7 @@ sub open_or_add_dir {
                no warnings 'once';
                # SVN::Node::none and SVN::Node::file are used only once,
                # so we're shutting up Perl's warnings about them.
-               if ($t == $SVN::Node::none) {
+               if ($t == $SVN::Node::none || defined($deletions->{$full_path})) {
                        return $self->add_directory($full_path, $baton,
                            undef, -1, $self->{pool});
                } elsif ($t == $SVN::Node::dir) {
@@ -5066,17 +5161,18 @@ sub open_or_add_dir {
 }
 
 sub ensure_path {
-       my ($self, $path) = @_;
+       my ($self, $path, $deletions) = @_;
        my $bat = $self->{bat};
        my $repo_path = $self->repo_path($path);
        return $bat->{''} unless (length $repo_path);
+
        my @p = split m#/+#, $repo_path;
        my $c = shift @p;
-       $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''});
+       $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''}, $deletions);
        while (@p) {
                my $c0 = $c;
                $c .= '/' . shift @p;
-               $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0});
+               $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0}, $deletions);
        }
        return $bat->{$c};
 }
@@ -5133,9 +5229,9 @@ sub apply_autoprops {
 }
 
 sub A {
-       my ($self, $m) = @_;
+       my ($self, $m, $deletions) = @_;
        my ($dir, $file) = split_path($m->{file_b});
-       my $pbat = $self->ensure_path($dir);
+       my $pbat = $self->ensure_path($dir, $deletions);
        my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
                                        undef, -1);
        print "\tA\t$m->{file_b}\n" unless $::_q;
@@ -5145,9 +5241,9 @@ sub A {
 }
 
 sub C {
-       my ($self, $m) = @_;
+       my ($self, $m, $deletions) = @_;
        my ($dir, $file) = split_path($m->{file_b});
-       my $pbat = $self->ensure_path($dir);
+       my $pbat = $self->ensure_path($dir, $deletions);
        my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
                                $self->url_path($m->{file_a}), $self->{r});
        print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
@@ -5164,9 +5260,9 @@ sub delete_entry {
 }
 
 sub R {
-       my ($self, $m) = @_;
+       my ($self, $m, $deletions) = @_;
        my ($dir, $file) = split_path($m->{file_b});
-       my $pbat = $self->ensure_path($dir);
+       my $pbat = $self->ensure_path($dir, $deletions);
        my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
                                $self->url_path($m->{file_a}), $self->{r});
        print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
@@ -5175,14 +5271,14 @@ sub R {
        $self->close_file($fbat,undef,$self->{pool});
 
        ($dir, $file) = split_path($m->{file_a});
-       $pbat = $self->ensure_path($dir);
+       $pbat = $self->ensure_path($dir, $deletions);
        $self->delete_entry($m->{file_a}, $pbat);
 }
 
 sub M {
-       my ($self, $m) = @_;
+       my ($self, $m, $deletions) = @_;
        my ($dir, $file) = split_path($m->{file_b});
-       my $pbat = $self->ensure_path($dir);
+       my $pbat = $self->ensure_path($dir, $deletions);
        my $fbat = $self->open_file($self->repo_path($m->{file_b}),
                                $pbat,$self->{r},$self->{pool});
        print "\t$m->{chg}\t$m->{file_b}\n" unless $::_q;
@@ -5252,9 +5348,9 @@ sub chg_file {
 }
 
 sub D {
-       my ($self, $m) = @_;
+       my ($self, $m, $deletions) = @_;
        my ($dir, $file) = split_path($m->{file_b});
-       my $pbat = $self->ensure_path($dir);
+       my $pbat = $self->ensure_path($dir, $deletions);
        print "\tD\t$m->{file_b}\n" unless $::_q;
        $self->delete_entry($m->{file_b}, $pbat);
 }
@@ -5286,11 +5382,19 @@ sub DESTROY {
 sub apply_diff {
        my ($self) = @_;
        my $mods = $self->{mods};
-       my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
+       my %o = ( D => 0, C => 1, R => 2, A => 3, M => 4, T => 5 );
+       my %deletions;
+
+       foreach my $m (@$mods) {
+               if ($m->{chg} eq "D") {
+                       $deletions{$m->{file_b}} = 1;
+               }
+       }
+
        foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
                my $f = $m->{chg};
                if (defined $o{$f}) {
-                       $self->$f($m);
+                       $self->$f($m, \%deletions);
                } else {
                        fatal("Invalid change type: $f");
                }
@@ -5301,7 +5405,7 @@ sub apply_diff {
                                       $self->{mergeinfo});
        }
        $self->rmdirs if $_rmdir;
-       if (@$mods == 0) {
+       if (@$mods == 0 && !defined($self->{mergeinfo})) {
                $self->abort_edit;
        } else {
                $self->close_edit;
@@ -5310,7 +5414,7 @@ sub apply_diff {
 }
 
 package Git::SVN::Ra;
-use vars qw/@ISA $config_dir $_log_window_size/;
+use vars qw/@ISA $config_dir $_ignore_refs_regex $_log_window_size/;
 use strict;
 use warnings;
 my ($ra_invalid, $can_do_switch, %ignored_err, $RA);
@@ -5768,6 +5872,17 @@ sub get_dir_globbed {
        @finalents;
 }
 
+# return value: 0 -- don't ignore, 1 -- ignore
+sub is_ref_ignored {
+       my ($g, $p) = @_;
+       my $refname = $g->{ref}->full_path($p);
+       return 1 if defined($g->{ignore_refs_regex}) &&
+                   $refname =~ m!$g->{ignore_refs_regex}!;
+       return 0 unless defined($_ignore_refs_regex);
+       return 1 if $refname =~ m!$_ignore_refs_regex!o;
+       return 0;
+}
+
 sub match_globs {
        my ($self, $exists, $paths, $globs, $r) = @_;
 
@@ -5804,6 +5919,7 @@ sub match_globs {
                        next unless /$g->{path}->{regex}/;
                        my $p = $1;
                        my $pathname = $g->{path}->full_path($p);
+                       next if is_ref_ignored($g, $p);
                        next if $exists->{$pathname};
                        next if ($self->check_path($pathname, $r) !=
                                 $SVN::Node::dir);
@@ -5894,7 +6010,6 @@ package Git::SVN::Log;
 use strict;
 use warnings;
 use POSIX qw/strftime/;
-use Time::Local;
 use constant commit_log_separator => ('-' x 72) . "\n";
 use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline
             %rusers $show_commit $incremental/;
@@ -6004,11 +6119,8 @@ sub run_pager {
 }
 
 sub format_svn_date {
-       # some systmes don't handle or mishandle %z, so be creative.
        my $t = shift || time;
-       my $gm = timelocal(gmtime($t));
-       my $sign = qw( + + - )[ $t <=> $gm ];
-       my $gmoff = sprintf("%s%02d%02d", $sign, (gmtime(abs($t - $gm)))[2,1]);
+       my $gmoff = Git::SVN::get_tz($t);
        return strftime("%Y-%m-%d %H:%M:%S $gmoff (%a, %d %b %Y)", localtime($t));
 }