rebase: extract merge code to new source file
[gitweb.git] / git-svn.perl
index 2c86ea2e384e4b3ecf9f6c2a856c52a0203ce489..177dd259cd53cde017d73db5ccbecc7ed7dfa573 100755 (executable)
@@ -1,6 +1,7 @@
 #!/usr/bin/env perl
 # Copyright (C) 2006, Eric Wong <normalperson@yhbt.net>
 # License: GPL v2 or later
+use 5.008;
 use warnings;
 use strict;
 use vars qw/   $AUTHOR $VERSION
@@ -83,7 +84,7 @@ BEGIN
        $_version, $_fetch_all, $_no_rebase, $_fetch_parent,
        $_merge, $_strategy, $_dry_run, $_local,
        $_prefix, $_no_checkout, $_url, $_verbose,
-       $_git_format, $_commit_url, $_tag);
+       $_git_format, $_commit_url, $_tag, $_merge_info);
 $Git::SVN::_follow_parent = 1;
 $_q ||= 0;
 my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
@@ -153,6 +154,7 @@ BEGIN
                          'commit-url=s' => \$_commit_url,
                          'revision|r=i' => \$_revision,
                          'no-rebase' => \$_no_rebase,
+                         'mergeinfo=s' => \$_merge_info,
                        %cmt_opts, %fc_opts } ],
        branch => [ \&cmd_branch,
                    'Create a branch in the SVN repository',
@@ -494,6 +496,7 @@ sub cmd_set_tree {
 
 sub cmd_dcommit {
        my $head = shift;
+       command_noisy(qw/update-index --refresh/);
        git_cmd_try { command_oneline(qw/diff-index --quiet HEAD/) }
                'Cannot dcommit with a dirty index.  Commit your changes first, '
                . "or stash them with `git stash'.\n";
@@ -567,6 +570,7 @@ sub cmd_dcommit {
                                               print "Committed r$_[0]\n";
                                               $cmt_rev = $_[0];
                                        },
+                                       mergeinfo => $_merge_info,
                                        svn_path => '');
                        if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
                                print "No changes\n$d~1 == $d\n";
@@ -963,6 +967,7 @@ sub cmd_multi_init {
        }
        do_git_init_db();
        if (defined $_trunk) {
+               $_trunk =~ s#^/+##;
                my $trunk_ref = 'refs/remotes/' . $_prefix . 'trunk';
                # try both old-style and new-style lookups:
                my $gs_trunk = eval { Git::SVN->new($trunk_ref) };
@@ -1185,6 +1190,7 @@ sub cmd_reset {
                    "history\n";
        }
        my ($r, $c) = $gs->find_rev_before($target, not $_fetch_parent);
+       die "Cannot find SVN revision $target\n" unless defined($c);
        $gs->rev_map_set($r, $c, 'reset', $uuid);
        print "r$r = $c ($gs->{ref_id})\n";
 }
@@ -1510,7 +1516,8 @@ sub cmt_sha2rev_batch {
 
 sub working_head_info {
        my ($head, $refs) = @_;
-       my @args = ('log', '--no-color', '--first-parent', '--pretty=medium');
+       my @args = qw/log --no-color --no-decorate --first-parent
+                     --pretty=medium/;
        my ($fh, $ctx) = command_output_pipe(@args, $head);
        my $hash;
        my %max;
@@ -1817,6 +1824,7 @@ sub read_all_remotes {
                        die("svn-remote.$remote: remote ref '$remote_ref' "
                            . "must start with 'refs/'\n")
                                unless $remote_ref =~ m{^refs/};
+                       $local_ref = uri_decode($local_ref);
                        $r->{$remote}->{fetch}->{$local_ref} = $remote_ref;
                        $r->{$remote}->{svm} = {} if $use_svm_props;
                } elsif (m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {
@@ -1829,6 +1837,7 @@ sub read_all_remotes {
                        die("svn-remote.$remote: remote ref '$remote_ref' ($t) "
                            . "must start with 'refs/'\n")
                                unless $remote_ref =~ m{^refs/};
+                       $local_ref = uri_decode($local_ref);
                        my $rs = {
                            t => $t,
                            remote => $remote,
@@ -2053,6 +2062,9 @@ sub new {
                         "\":$ref_id\$\" in config\n";
                ($self->{path}, undef) = split(/\s*:\s*/, $fetch);
        }
+       $self->{path} =~ s{/+}{/}g;
+       $self->{path} =~ s{\A/}{};
+       $self->{path} =~ s{/\z}{};
        $self->{url} = command_oneline('config', '--get',
                                       "svn-remote.$repo_id.url") or
                   die "Failed to read \"svn-remote.$repo_id.url\" in config\n";
@@ -2086,6 +2098,14 @@ sub refname {
        # .. becomes %2E%2E
        $refname =~ s{\.\.}{%2E%2E}g;
 
+       # trailing dots and .lock are not allowed
+       # .$ becomes %2E and .lock becomes %2Elock
+       $refname =~ s{\.(?=$|lock$)}{%2E};
+
+       # the sequence @{ is used to access the reflog
+       # @{ becomes %40{
+       $refname =~ s{\@\{}{%40\{}g;
+
        return $refname;
 }
 
@@ -2827,8 +2847,9 @@ sub mkemptydirs {
        foreach my $d (sort keys %empty_dirs) {
                $d = uri_decode($d);
                $d =~ s/$strip//;
+               next unless length($d);
                next if -d $d;
-               if (-e _) {
+               if (-e $d) {
                        warn "$d exists but is not a directory\n";
                } else {
                        print "creating empty directory: $d\n";
@@ -2942,18 +2963,29 @@ sub other_gs {
        my $gs = Git::SVN->find_by_url($new_url, $url, $branch_from);
        unless ($gs) {
                my $ref_id = $old_ref_id;
-               $ref_id =~ s/\@\d+$//;
+               $ref_id =~ s/\@\d+-*$//;
                $ref_id .= "\@$r";
                # just grow a tail if we're not unique enough :x
                $ref_id .= '-' while find_ref($ref_id);
-               print STDERR "Initializing parent: $ref_id\n" unless $::_q > 1;
                my ($u, $p, $repo_id) = ($new_url, '', $ref_id);
                if ($u =~ s#^\Q$url\E(/|$)##) {
                        $p = $u;
                        $u = $url;
                        $repo_id = $self->{repo_id};
                }
-               $gs = Git::SVN->init($u, $p, $repo_id, $ref_id, 1);
+               while (1) {
+                       # It is possible to tag two different subdirectories at
+                       # the same revision.  If the url for an existing ref
+                       # does not match, we must either find a ref with a
+                       # matching url or create a new ref by growing a tail.
+                       $gs = Git::SVN->init($u, $p, $repo_id, $ref_id, 1);
+                       my (undef, $max_commit) = $gs->rev_map_max(1);
+                       last if (!$max_commit);
+                       my ($url) = ::cmt_metadata($max_commit);
+                       last if ($url eq $gs->full_url);
+                       $ref_id .= '-';
+               }
+               print STDERR "Initializing parent: $ref_id\n" unless $::_q > 1;
        }
        $gs
 }
@@ -3090,9 +3122,10 @@ sub _rev_list {
 sub check_cherry_pick {
        my $base = shift;
        my $tip = shift;
+       my $parents = shift;
        my @ranges = @_;
        my %commits = map { $_ => 1 }
-               _rev_list("--no-merges", $tip, "--not", $base);
+               _rev_list("--no-merges", $tip, "--not", $base, @$parents);
        for my $range ( @ranges ) {
                delete @commits{_rev_list($range)};
        }
@@ -3155,6 +3188,22 @@ sub has_no_changes {
                        LIST_CACHE => 'FAULT',
                ;
        }
+
+       sub unmemoize_svn_mergeinfo_functions {
+               return if not $memoized;
+               $memoized = 0;
+
+               Memoize::unmemoize 'lookup_svn_merge';
+               Memoize::unmemoize 'check_cherry_pick';
+               Memoize::unmemoize 'has_no_changes';
+       }
+}
+
+END {
+       # Force cache writeout explicitly instead of waiting for
+       # global destruction to avoid segfault in Storable:
+       # http://rt.cpan.org/Public/Bug/Display.html?id=36087
+       unmemoize_svn_mergeinfo_functions();
 }
 
 sub parents_exclude {
@@ -3252,6 +3301,7 @@ sub find_extra_svn_parents {
                # double check that there are no missing non-merge commits
                my (@incomplete) = check_cherry_pick(
                        $merge_base, $merge_tip,
+                       $parents,
                        @$ranges,
                       );
 
@@ -3605,6 +3655,7 @@ sub mkfile {
 
 sub rev_map_set {
        my ($self, $rev, $commit, $update_ref, $uuid) = @_;
+       defined $commit or die "missing arg3\n";
        length $commit == 40 or die "arg3 must be a full SHA1 hexsum\n";
        my $db = $self->map_path($uuid);
        my $db_lock = "$db.lock";
@@ -3998,7 +4049,6 @@ package SVN::Git::Fetcher;
 use strict;
 use warnings;
 use Carp qw/croak/;
-use File::Temp qw/tempfile/;
 use IO::File qw//;
 use vars qw/$_ignore_regex/;
 
@@ -4020,6 +4070,7 @@ sub new {
        $self->{absent_dir} = {};
        $self->{absent_file} = {};
        $self->{gii} = $git_svn->tmp_index_do(sub { Git::IndexInfo->new });
+       $self->{pathnameencoding} = Git::config('svn.pathnameencoding');
        $self;
 }
 
@@ -4103,6 +4154,10 @@ sub open_directory {
 
 sub git_path {
        my ($self, $path) = @_;
+       if (my $enc = $self->{pathnameencoding}) {
+               require Encode;
+               Encode::from_to($path, 'UTF-8', $enc);
+       }
        if ($self->{path_strip}) {
                $path =~ s!$self->{path_strip}!! or
                  die "Failed to strip path '$path' ($self->{path_strip})\n";
@@ -4398,6 +4453,7 @@ sub new {
        $self->{path_prefix} = length $self->{svn_path} ?
                               "$self->{svn_path}/" : '';
        $self->{config} = $opts->{config};
+       $self->{mergeinfo} = $opts->{mergeinfo};
        return $self;
 }
 
@@ -4491,6 +4547,10 @@ sub split_path {
 
 sub repo_path {
        my ($self, $path) = @_;
+       if (my $enc = $self->{pathnameencoding}) {
+               require Encode;
+               Encode::from_to($path, $enc, 'UTF-8');
+       }
        $self->{path_prefix}.(defined $path ? $path : '');
 }
 
@@ -4703,6 +4763,11 @@ sub change_file_prop {
        $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool});
 }
 
+sub change_dir_prop {
+       my ($self, $pbat, $pname, $pval) = @_;
+       $self->SUPER::change_dir_prop($pbat, $pname, $pval, $self->{pool});
+}
+
 sub _chg_file_get_blob ($$$$) {
        my ($self, $fbat, $m, $which) = @_;
        my $fh = $::_repository->temp_acquire("git_blob_$which");
@@ -4796,6 +4861,11 @@ sub apply_diff {
                        fatal("Invalid change type: $f");
                }
        }
+
+       if (defined($self->{mergeinfo})) {
+               $self->change_dir_prop($self->{bat}{''}, "svn:mergeinfo",
+                                      $self->{mergeinfo});
+       }
        $self->rmdirs if $_rmdir;
        if (@$mods == 0) {
                $self->abort_edit;