Documentation: bisect: add some titles to some paragraphs.
[gitweb.git] / git-svn.perl
index 571259fd098d7aa5ea280cdfb3c799f32492087c..e8457893db963ad3bb6be270aba0a81481119b1c 100755 (executable)
@@ -9,6 +9,7 @@
 $AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
 $VERSION = '@@GIT_VERSION@@';
 
+my $git_dir_user_set = 1 if defined $ENV{GIT_DIR};
 $ENV{GIT_DIR} ||= '.git';
 $Git::SVN::default_repo_id = 'svn';
 $Git::SVN::default_ref_id = $ENV{GIT_SVN_ID} || 'git-svn';
@@ -55,7 +56,7 @@ BEGIN
        $_message, $_file,
        $_template, $_shared,
        $_version, $_fetch_all,
-       $_merge, $_strategy, $_dry_run,
+       $_merge, $_strategy, $_dry_run, $_local,
        $_prefix, $_no_checkout, $_verbose);
 $Git::SVN::_follow_parent = 1;
 my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
@@ -66,6 +67,7 @@ BEGIN
                'repack:i' => \$Git::SVN::_repack,
                'noMetadata' => \$Git::SVN::_no_metadata,
                'useSvmProps' => \$Git::SVN::_use_svm_props,
+               'useSvnsyncProps' => \$Git::SVN::_use_svnsync_props,
                'log-window-size=i' => \$Git::SVN::Ra::_log_window_size,
                'no-checkout' => \$_no_checkout,
                'quiet|q' => \$_q,
@@ -74,9 +76,14 @@ BEGIN
                %remote_opts );
 
 my ($_trunk, $_tags, $_branches);
+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,
+                 'no-metadata' => sub { $icv{noMetadata} = 1 },
+                 'use-svm-props' => sub { $icv{useSvmProps} = 1 },
+                 'use-svnsync-props' => sub { $icv{useSvnsyncProps} = 1 },
+                 'rewrite-root=s' => sub { $icv{rewriteRoot} = $_[1] },
                   %remote_opts );
 my %cmt_opts = ( 'edit|e' => \$_edit,
                'rmdir' => \$SVN::Git::Editor::_rmdir,
@@ -90,6 +97,9 @@ BEGIN
                        { 'revision|r=s' => \$_revision,
                          'fetch-all|all' => \$_fetch_all,
                           %fc_opts } ],
+       clone => [ \&cmd_clone, "Initialize and fetch revisions",
+                       { 'revision|r=s' => \$_revision,
+                          %fc_opts, %init_opts } ],
        init => [ \&cmd_init, "Initialize a repo for tracking" .
                          " (requires URL argument)",
                          \%init_opts ],
@@ -135,6 +145,7 @@ BEGIN
                        { '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,
@@ -160,14 +171,38 @@ BEGIN
 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' => \$Git::SVN::default_repo_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');
 
 usage(0) if $_help;
 version() if $_version;
 usage(1) unless defined $cmd;
 load_authors() if $_authors;
-unless ($cmd =~ /^(?:init|multi-init|commit-diff)$/) {
+
+# make sure we're always running
+unless ($cmd =~ /(?:clone|init|multi-init)$/) {
+       unless (-d $ENV{GIT_DIR}) {
+               if ($git_dir_user_set) {
+                       die "GIT_DIR=$ENV{GIT_DIR} explicitly set, ",
+                           "but it is not a directory\n";
+               }
+               my $git_dir = delete $ENV{GIT_DIR};
+               chomp(my $cdup = command_oneline(qw/rev-parse --show-cdup/));
+               unless (length $cdup) {
+                       die "Already at toplevel, but $git_dir ",
+                           "not found '$cdup'\n";
+               }
+               chdir $cdup or die "Unable to chdir up to '$cdup'\n";
+               unless (-d $git_dir) {
+                       die "$git_dir still not found after going to ",
+                           "'$cdup'\n";
+               }
+               $ENV{GIT_DIR} = $git_dir;
+       }
+}
+unless ($cmd =~ /^(?:clone|init|multi-init|commit-diff)$/) {
        Git::SVN::Migration::migration_check();
 }
 Git::SVN::init_vars();
@@ -228,13 +263,33 @@ sub do_git_init_db {
                }
                command_noisy(@init_db);
        }
+       my $set;
+       my $pfx = "svn-remote.$Git::SVN::default_repo_id";
+       foreach my $i (keys %icv) {
+               die "'$set' and '$i' cannot both be set\n" if $set;
+               next unless defined $icv{$i};
+               command_noisy('config', "$pfx.$i", $icv{$i});
+               $set = $i;
+       }
 }
 
 sub init_subdir {
        my $repo_path = shift or return;
        mkpath([$repo_path]) unless -d $repo_path;
        chdir $repo_path or die "Couldn't chdir to $repo_path: $!\n";
-       $ENV{GIT_DIR} = $repo_path . "/.git";
+       $ENV{GIT_DIR} = '.git';
+}
+
+sub cmd_clone {
+       my ($url, $path) = @_;
+       if (!defined $path &&
+           (defined $_trunk || defined $_branches || defined $_tags) &&
+           $url !~ m#^[a-z\+]+://#) {
+               $path = $url;
+       }
+       $path = basename($url) if !defined $path || !length $path;
+       cmd_init($url, $path);
+       Git::SVN::fetch_all($Git::SVN::default_repo_id);
 }
 
 sub cmd_init {
@@ -385,14 +440,17 @@ sub cmd_rebase {
                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 $gs = Git::SVN->new;
+       my $url = (::working_head_info('HEAD'))[0];
+       my $gs = Git::SVN->find_by_url($url) || Git::SVN->new;
        my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
-       $gs->traverse_ignore(\*STDOUT, '', $r);
+       $gs->traverse_ignore(\*STDOUT, $gs->{path}, $r);
 }
 
 sub cmd_multi_init {
@@ -400,12 +458,12 @@ sub cmd_multi_init {
        unless (defined $_trunk || defined $_branches || defined $_tags) {
                usage(1);
        }
-       do_git_init_db();
        $_prefix = '' unless defined $_prefix;
        if (defined $url) {
                $url =~ s#/+$##;
                init_subdir(@_);
        }
+       do_git_init_db();
        if (defined $_trunk) {
                my $trunk_ref = $_prefix . 'trunk';
                # try both old-style and new-style lookups:
@@ -728,7 +786,8 @@ package Git::SVN;
 use strict;
 use warnings;
 use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent
-            $_repack $_repack_flags $_use_svm_props $_head/;
+            $_repack $_repack_flags $_use_svm_props $_head
+            $_use_svnsync_props $no_reuse_existing/;
 use Carp qw/croak/;
 use File::Path qw/mkpath/;
 use File::Copy qw/copy/;
@@ -749,7 +808,8 @@ BEGIN
        # 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/) {
+       foreach (qw/follow_parent no_metadata use_svm_props
+                   use_svnsync_props/) {
                my $key = $_;
                $key =~ tr/_//d;
                $e .= "sub $_ {
@@ -922,6 +982,7 @@ sub sanitize_remote_name {
 
 sub find_existing_remote {
        my ($url, $remotes) = @_;
+       return undef if $no_reuse_existing;
        my $existing;
        foreach my $repo_id (keys %$remotes) {
                my $u = $remotes->{$repo_id}->{url} or next;
@@ -996,6 +1057,7 @@ 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;
        my $remotes = read_all_remotes();
        if (defined $full_url && defined $repos_root && !defined $path) {
                $path = $full_url;
@@ -1094,9 +1156,12 @@ sub svm {
                $svm = {
                  source => tmp_config('--get', "$section.svm-source"),
                  uuid => tmp_config('--get', "$section.svm-uuid"),
+                 replace => tmp_config('--get', "$section.svm-replace"),
                }
        };
-       $self->{svm} = $svm if ($svm && $svm->{source} && $svm->{uuid});
+       if ($svm && $svm->{source} && $svm->{uuid} && $svm->{replace}) {
+               $self->{svm} = $svm;
+       }
        $self->{svm};
 }
 
@@ -1105,68 +1170,124 @@ sub _set_svm_vars {
        return $ra if $self->svm;
 
        my @err = ( "useSvmProps set, but failed to read SVM properties\n",
-                   "(svm:source, svm:mirror, svm:mirror) ",
+                   "(svm:source, svm:uuid) ",
                    "from the following URLs:\n" );
        sub read_svm_props {
-               my ($self, $props) = @_;
+               my ($self, $ra, $path, $r) = @_;
+               my $props = ($ra->get_dir($path, $r))[2];
                my $src = $props->{'svm:source'};
-               my $mirror = $props->{'svm:mirror'};
                my $uuid = $props->{'svm:uuid'};
-               return undef if (!$src || !$mirror || !$uuid);
+               return undef if (!$src || !$uuid);
 
-               chomp($src, $mirror, $uuid);
+               chomp($src, $uuid);
 
                $uuid =~ m{^[0-9a-f\-]{30,}$}
                    or die "doesn't look right - svm:uuid is '$uuid'\n";
-               # don't know what a '!' is there for, also the
-               # username is of no interest
-               $src =~ s{/?!$}{$mirror};
+
+               # the '!' is used to mark the repos_root!/relative/path
+               $src =~ s{/?!/?}{/};
                $src =~ s{/+$}{}; # no trailing slashes please
+               # username is of no interest
                $src =~ s{(^[a-z\+]*://)[^/@]*@}{$1};
 
+               my $replace = $ra->{url};
+               $replace .= "/$path" if length $path;
+
                my $section = "svn-remote.$self->{repo_id}";
-               tmp_config('--add', "$section.svm-source", $src);
-               tmp_config('--add', "$section.svm-uuid", $uuid);
-               $self->{svm} = { source => $src , uuid => $uuid };
-               return 1;
+               tmp_config("$section.svm-source", $src);
+               tmp_config("$section.svm-replace", $replace);
+               tmp_config("$section.svm-uuid", $uuid);
+               $self->{svm} = {
+                       source => $src,
+                       uuid => $uuid,
+                       replace => $replace
+               };
        }
 
        my $r = $ra->get_latest_revnum;
        my $path = $self->{path};
-       my @tried_a = ($path);
+       my %tried;
        while (length $path) {
-               if ($self->read_svm_props(($ra->get_dir($path, $r))[2])) {
-                       return $ra;
+               unless ($tried{"$self->{url}/$path"}) {
+                       return $ra if $self->read_svm_props($ra, $path, $r);
+                       $tried{"$self->{url}/$path"} = 1;
                }
-               $path =~ s#/?[^/]+$## && push @tried_a, $path;
-       }
-       if ($self->read_svm_props(($ra->get_dir('', $r))[2])) {
-               return $ra;
+               $path =~ s#/?[^/]+$##;
        }
+       die "Path: '$path' should be ''\n" if $path ne '';
+       return $ra if $self->read_svm_props($ra, $path, $r);
+       $tried{"$self->{url}/$path"} = 1;
 
        if ($ra->{repos_root} eq $self->{url}) {
-               die @err, map { "  $self->{url}/$_\n" } @tried_a, "\n";
+               die @err, (map { "  $_\n" } keys %tried), "\n";
        }
 
        # nope, make sure we're connected to the repository root:
        my $ok;
        my @tried_b;
        $path = $ra->{svn_path};
-       $path =~ s#/?[^/]+$##; # we already tried this one above
        $ra = Git::SVN::Ra->new($ra->{repos_root});
        while (length $path) {
-               $ok = $self->read_svm_props(($ra->get_dir($path, $r))[2]);
-               last if $ok;
-               $path =~ s#/?[^/]+$## && push @tried_b, $path;
+               unless ($tried{"$ra->{url}/$path"}) {
+                       $ok = $self->read_svm_props($ra, $path, $r);
+                       last if $ok;
+                       $tried{"$ra->{url}/$path"} = 1;
+               }
+               $path =~ s#/?[^/]+$##;
        }
-       $ok = $self->read_svm_props(($ra->get_dir('', $r))[2]) unless $ok;
+       die "Path: '$path' should be ''\n" if $path ne '';
+       $ok ||= $self->read_svm_props($ra, $path, $r);
+       $tried{"$ra->{url}/$path"} = 1;
        if (!$ok) {
-               die @err, map { "  $self->{url}/$_\n" } @tried_a, "\n",
-                         map { "  $ra->{url}/$_\n" } @tried_b, "\n"
+               die @err, (map { "  $_\n" } keys %tried), "\n";
        }
        Git::SVN::Ra->new($self->{url});
 }
 
+sub svnsync {
+       my ($self) = @_;
+       return $self->{svnsync} if $self->{svnsync};
+
+       if ($self->no_metadata) {
+               die "Can't have both 'noMetadata' and ",
+                   "'useSvnsyncProps' options set!\n";
+       }
+       if ($self->rewrite_root) {
+               die "Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",
+                   "options set!\n";
+       }
+
+       my $svnsync;
+       # see if we have it in our config, first:
+       eval {
+               my $section = "svn-remote.$self->{repo_id}";
+               $svnsync = {
+                 url => tmp_config('--get', "$section.svnsync-url"),
+                 uuid => tmp_config('--get', "$section.svnsync-uuid"),
+               }
+       };
+       if ($svnsync && $svnsync->{url} && $svnsync->{uuid}) {
+               return $self->{svnsync} = $svnsync;
+       }
+
+       my $err = "useSvnsyncProps set, but failed to read " .
+                 "svnsync property: svn:sync-from-";
+       my $rp = $self->ra->rev_proplist(0);
+
+       my $url = $rp->{'svn:sync-from-url'} or die $err . "url\n";
+       $url =~ m{^[a-z\+]+://} or
+                  die "doesn't look right - svn:sync-from-url is '$url'\n";
+
+       my $uuid = $rp->{'svn:sync-from-uuid'} or die $err . "uuid\n";
+       $uuid =~ m{^[0-9a-f\-]{30,}$} or
+                  die "doesn't look right - svn:sync-from-uuid is '$uuid'\n";
+
+       my $section = "svn-remote.$self->{repo_id}";
+       tmp_config('--add', "$section.svnsync-uuid", $uuid);
+       tmp_config('--add', "$section.svnsync-url", $url);
+       return $self->{svnsync} = { url => $url, uuid => $uuid };
+}
+
 # this allows us to memoize our SVN::Ra UUID locally and avoid a
 # remote lookup (useful for 'git svn log').
 sub ra_uuid {
@@ -1192,6 +1313,9 @@ sub ra {
                if ($self->no_metadata) {
                        die "Can't have both 'noMetadata' and ",
                            "'useSvmProps' options set!\n";
+               } elsif ($self->use_svnsync_props) {
+                       die "Can't have both 'useSvnsyncProps' and ",
+                           "'useSvmProps' options set!\n";
                }
                $ra = $self->_set_svm_vars($ra);
                $self->{-want_revprops} = 1;
@@ -1213,7 +1337,7 @@ sub traverse_ignore {
        my $ra = $self->ra;
        my ($dirent, undef, $props) = $ra->get_dir($path, $r);
        my $p = $path;
-       $p =~ s#^\Q$ra->{svn_path}\E/##;
+       $p =~ s#^\Q$self->{path}\E(/|$)##;
        print $fh length $p ? "\n# $p\n" : "\n# /\n";
        if (my $s = $props->{'svn:ignore'}) {
                $s =~ s/[\r\n]+/\n/g;
@@ -1387,6 +1511,26 @@ sub get_commit_parents {
        @ret;
 }
 
+sub rewrite_root {
+       my ($self) = @_;
+       return $self->{-rewrite_root} if exists $self->{-rewrite_root};
+       my $k = "svn-remote.$self->{repo_id}.rewriteRoot";
+       my $rwr = eval { command_oneline(qw/config --get/, $k) };
+       if ($rwr) {
+               $rwr =~ s#/+$##;
+               if ($rwr !~ m#^[a-z\+]+://#) {
+                       die "$rwr is not a valid URL (key: $k)\n";
+               }
+       }
+       $self->{-rewrite_root} = $rwr;
+}
+
+sub metadata_url {
+       my ($self) = @_;
+       ($self->rewrite_root || $self->{url}) .
+          (length $self->{path} ? '/' . $self->{path} : '');
+}
+
 sub full_url {
        my ($self) = @_;
        $self->{url} . (length $self->{path} ? '/' . $self->{path} : '');
@@ -1534,9 +1678,9 @@ sub find_parent_branch {
        }
        if (defined $r0 && defined $parent) {
                print STDERR "Found branch parent: ($self->{ref_id}) $parent\n";
-               $self->assert_index_clean($parent);
                my $ed;
                if ($self->ra->can_do_switch) {
+                       $self->assert_index_clean($parent);
                        print STDERR "Following parent with do_switch\n";
                        # do_switch works with svn/trunk >= r22312, but that
                        # is not included with SVN 1.4.3 (the latest version
@@ -1685,19 +1829,36 @@ sub make_log_entry {
        my ($name, $email) = defined $::users{$author} ? @{$::users{$author}}
                                                       : ($author, undef);
        if (defined $headrev && $self->use_svm_props) {
+               if ($self->rewrite_root) {
+                       die "Can't have both 'useSvmProps' and 'rewriteRoot' ",
+                           "options set!\n";
+               }
                my ($uuid, $r) = $headrev =~ m{^([a-f\d\-]{30,}):(\d+)$};
-               if ($uuid ne $self->{svm}->{uuid}) {
+               # we don't want "SVM: initializing mirror for junk" ...
+               return undef if $r == 0;
+               my $svm = $self->svm;
+               if ($uuid ne $svm->{uuid}) {
                        die "UUID mismatch on SVM path:\n",
-                           "expected: $self->{svm}->{uuid}\n",
+                           "expected: $svm->{uuid}\n",
                            "     got: $uuid\n";
                }
-               my $full_url = $self->{svm}->{source};
-               $full_url .= "/$self->{path}" if length $self->{path};
+               my $full_url = $self->full_url;
+               $full_url =~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or
+                            die "Failed to replace '$svm->{replace}' with ",
+                                "'$svm->{source}' in $full_url\n";
+               # throw away username for storing in records
+               remove_username($full_url);
                $log_entry{metadata} = "$full_url\@$r $uuid";
                $log_entry{svm_revision} = $r;
                $email ||= "$author\@$uuid"
+       } elsif ($self->use_svnsync_props) {
+               my $full_url = $self->svnsync->{url};
+               $full_url .= "/$self->{path}" if length $self->{path};
+               my $uuid = $self->svnsync->{uuid};
+               $log_entry{metadata} = "$full_url\@$rev $uuid";
+               $email ||= "$author\@$uuid"
        } else {
-               $log_entry{metadata} = $self->full_url . "\@$rev " .
+               $log_entry{metadata} = $self->metadata_url. "\@$rev " .
                                       $self->ra->get_uuid;
                $email ||= "$author\@" . $self->ra->get_uuid;
        }
@@ -1755,12 +1916,14 @@ sub rebuild {
        my ($rev_list, $ctx) = command_output_pipe("rev-list", $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);
+               remove_username($url);
 
                # ignore merges (from set-tree)
                next if (!defined $rev || !$uuid);
@@ -1934,6 +2097,10 @@ sub uri_encode {
        $f
 }
 
+sub remove_username {
+       $_[0] =~ s{^([^:]*://)[^@]+@}{$1};
+}
+
 package Git::SVN::Prompt;
 use strict;
 use warnings;
@@ -2715,6 +2882,7 @@ 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([
@@ -2767,6 +2935,10 @@ sub gs_do_update {
        my $new = ($rev_a == $rev_b);
        my $path = $gs->{path};
 
+       if ($new && -e $gs->{index}) {
+               unlink $gs->{index} or die
+                 "Couldn't unlink index: $gs->{index}: $!\n";
+       }
        my $pool = SVN::Pool->new;
        $editor->set_path_strip($path);
        my (@pc) = split m#/#, $path;