Code

grep: simplify -p output
[git.git] / git-svn.perl
index 33017974d0a098bdaff707ccf0e8660aa5009079..d1af1a3d2f1e24c068e227f847ccceb7e156ad0d 100755 (executable)
@@ -63,7 +63,7 @@ my ($SVN);
 $sha1 = qr/[a-f\d]{40}/;
 $sha1_short = qr/[a-f\d]{4,40}/;
 my ($_stdin, $_help, $_edit,
-       $_message, $_file,
+       $_message, $_file, $_branch_dest,
        $_template, $_shared,
        $_version, $_fetch_all, $_no_rebase, $_fetch_parent,
        $_merge, $_strategy, $_dry_run, $_local,
@@ -92,11 +92,11 @@ my %fc_opts = ( 'follow-parent|follow!' => \$Git::SVN::_follow_parent,
                'localtime' => \$Git::SVN::_localtime,
                %remote_opts );
 
-my ($_trunk, $_tags, $_branches, $_stdlayout);
+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,
+                  '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 },
@@ -141,11 +141,13 @@ my %cmd = (
        branch => [ \&cmd_branch,
                    'Create a branch in the SVN repository',
                    { 'message|m=s' => \$_message,
+                     'destination|d=s' => \$_branch_dest,
                      'dry-run|n' => \$_dry_run,
                      'tag|t' => \$_tag } ],
        tag => [ sub { $_tag = 1; cmd_branch(@_) },
                 'Create a tag in the SVN repository',
                 { 'message|m=s' => \$_message,
+                  'destination|d=s' => \$_branch_dest,
                   'dry-run|n' => \$_dry_run } ],
        'set-tree' => [ \&cmd_set_tree,
                        "Set an SVN repository to a git tree-ish",
@@ -211,6 +213,10 @@ my %cmd = (
        'blame' => [ \&Git::SVN::Log::cmd_blame,
                    "Show what revision and author last modified each line of a file",
                    { 'git-format' => \$_git_format } ],
+       'reset' => [ \&cmd_reset,
+                    "Undo fetches back to the specified SVN revision",
+                    { 'revision|r=s' => \$_revision,
+                      'parent|p' => \$_fetch_parent } ],
 );
 
 my $cmd;
@@ -219,6 +225,9 @@ for (my $i = 0; $i < @ARGV; $i++) {
                $cmd = $ARGV[$i];
                splice @ARGV, $i, 1;
                last;
+       } elsif ($ARGV[$i] eq 'help') {
+               $cmd = $ARGV[$i+1];
+               usage(0);
        }
 };
 
@@ -358,7 +367,7 @@ sub init_subdir {
 sub cmd_clone {
        my ($url, $path) = @_;
        if (!defined $path &&
-           (defined $_trunk || defined $_branches || defined $_tags ||
+           (defined $_trunk || @_branches || @_tags ||
             defined $_stdlayout) &&
            $url !~ m#^[a-z\+]+://#) {
                $path = $url;
@@ -372,14 +381,15 @@ sub cmd_clone {
 sub cmd_init {
        if (defined $_stdlayout) {
                $_trunk = 'trunk' if (!defined $_trunk);
-               $_tags = 'tags' if (!defined $_tags);
-               $_branches = 'branches' if (!defined $_branches);
+               @_tags = 'tags' if (! @_tags);
+               @_branches = 'branches' if (! @_branches);
        }
-       if (defined $_trunk || defined $_branches || defined $_tags) {
+       if (defined $_trunk || @_branches || @_tags) {
                return cmd_multi_init(@_);
        }
        my $url = shift or die "SVN repository location required ",
                               "as a command-line argument\n";
+       $url = canonicalize_url($url);
        init_subdir(@_);
        do_git_init_db();
 
@@ -454,8 +464,22 @@ sub cmd_dcommit {
                'Cannot dcommit with a dirty index.  Commit your changes first, '
                . "or stash them with `git stash'.\n";
        $head ||= 'HEAD';
+
+       my $old_head;
+       if ($head ne 'HEAD') {
+               $old_head = eval {
+                       command_oneline([qw/symbolic-ref -q HEAD/])
+               };
+               if ($old_head) {
+                       $old_head =~ s{^refs/heads/}{};
+               } else {
+                       $old_head = eval { command_oneline(qw/rev-parse HEAD/) };
+               }
+               command(['checkout', $head], STDERR => 0);
+       }
+
        my @refs;
-       my ($url, $rev, $uuid, $gs) = working_head_info($head, \@refs);
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD', \@refs);
        unless ($gs) {
                die "Unable to determine upstream SVN information from ",
                    "$head history.\nPerhaps the repository is empty.";
@@ -541,7 +565,7 @@ sub cmd_dcommit {
                        if (@diff) {
                                @refs = ();
                                my ($url_, $rev_, $uuid_, $gs_) =
-                                             working_head_info($head, \@refs);
+                                             working_head_info('HEAD', \@refs);
                                my ($linear_refs_, $parents_) =
                                              linearize_history($gs_, \@refs);
                                if (scalar(@$linear_refs) !=
@@ -579,6 +603,22 @@ sub cmd_dcommit {
                        }
                }
        }
+
+       if ($old_head) {
+               my $new_head = command_oneline(qw/rev-parse HEAD/);
+               my $new_is_symbolic = eval {
+                       command_oneline(qw/symbolic-ref -q HEAD/);
+               };
+               if ($new_is_symbolic) {
+                       print "dcommitted the branch ", $head, "\n";
+               } else {
+                       print "dcommitted on a detached HEAD because you gave ",
+                             "a revision argument.\n",
+                             "The rewritten commit is: ", $new_head, "\n";
+               }
+               command(['checkout', $old_head], STDERR => 0);
+       }
+
        unlink $gs->{index};
 }
 
@@ -593,7 +633,33 @@ sub cmd_branch {
        my ($src, $rev, undef, $gs) = working_head_info($head);
 
        my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}};
-       my $glob = $remote->{ $_tag ? 'tags' : 'branches' };
+       my $allglobs = $remote->{ $_tag ? 'tags' : 'branches' };
+       my $glob;
+       if ($#{$allglobs} == 0) {
+               $glob = $allglobs->[0];
+       } else {
+               unless(defined $_branch_dest) {
+                       die "Multiple ",
+                           $_tag ? "tag" : "branch",
+                           " paths defined for Subversion repository.\n",
+                           "You must specify where you want to create the ",
+                           $_tag ? "tag" : "branch",
+                           " with the --destination argument.\n";
+               }
+               foreach my $g (@{$allglobs}) {
+                       # SVN::Git::Editor could probably be moved to Git.pm..
+                       my $re = SVN::Git::Editor::glob2pat($g->{path}->{left});
+                       if ($_branch_dest =~ /$re/) {
+                               $glob = $g;
+                               last;
+                       }
+               }
+               unless (defined $glob) {
+                       die "Unknown ",
+                           $_tag ? "tag" : "branch",
+                           " destination $_branch_dest\n";
+               }
+       }
        my ($lft, $rgt) = @{ $glob->{path} }{qw/left right/};
        my $dst = join '/', $remote->{url}, $lft, $branch_name, ($rgt || ());
 
@@ -741,6 +807,12 @@ sub canonicalize_path {
        return $path;
 }
 
+sub canonicalize_url {
+       my ($url) = @_;
+       $url =~ s#^([^:]+://[^/]*/)(.*)$#$1 . canonicalize_path($2)#e;
+       return $url;
+}
+
 # get_svnprops(PATH)
 # ------------------
 # Helper for cmd_propget and cmd_proplist below.
@@ -800,7 +872,7 @@ sub cmd_proplist {
 
 sub cmd_multi_init {
        my $url = shift;
-       unless (defined $_trunk || defined $_branches || defined $_tags) {
+       unless (defined $_trunk || @_branches || @_tags) {
                usage(1);
        }
 
@@ -810,7 +882,7 @@ sub cmd_multi_init {
 
        $_prefix = '' unless defined $_prefix;
        if (defined $url) {
-               $url =~ s#/+$##;
+               $url = canonicalize_url($url);
                init_subdir(@_);
        }
        do_git_init_db();
@@ -825,10 +897,14 @@ sub cmd_multi_init {
                                                   undef, $trunk_ref);
                }
        }
-       return unless defined $_branches || defined $_tags;
+       return unless @_branches || @_tags;
        my $ra = $url ? Git::SVN::Ra->new($url) : undef;
-       complete_url_ls_init($ra, $_branches, '--branches/-b', $_prefix);
-       complete_url_ls_init($ra, $_tags, '--tags/-t', $_prefix . 'tags/');
+       foreach my $path (@_branches) {
+               complete_url_ls_init($ra, $path, '--branches/-b', $_prefix);
+       }
+       foreach my $path (@_tags) {
+               complete_url_ls_init($ra, $path, '--tags/-t', $_prefix.'tags/');
+       }
 }
 
 sub cmd_multi_fetch {
@@ -1021,6 +1097,20 @@ sub cmd_info {
        print $result, "\n";
 }
 
+sub cmd_reset {
+       my $target = shift || $_revision or die "SVN revision required\n";
+       $target = $1 if $target =~ /^r(\d+)$/;
+       $target =~ /^\d+$/ or die "Numeric SVN revision expected\n";
+       my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
+       unless ($gs) {
+               die "Unable to determine upstream SVN information from ".
+                   "history\n";
+       }
+       my ($r, $c) = $gs->find_rev_before($target, not $_fetch_parent);
+       $gs->rev_map_set($r, $c, 'reset', $uuid);
+       print "r$r = $c ($gs->{ref_id})\n";
+}
+
 ########################### utility functions #########################
 
 sub rebase_cmd {
@@ -1099,6 +1189,7 @@ sub complete_url_ls_init {
                die "--prefix='$pfx' must have a trailing slash '/'\n";
        }
        command_noisy('config',
+                     '--add',
                      "svn-remote.$gs->{repo_id}.$n",
                      "$remote_path:refs/remotes/$pfx*" .
                        ('/*' x (($remote_path =~ tr/*/*/) - 1)) );
@@ -1565,7 +1656,8 @@ sub fetch_all {
        # read the max revs for wildcard expansion (branches/*, tags/*)
        foreach my $t (qw/branches tags/) {
                defined $remote->{$t} or next;
-               push @globs, $remote->{$t};
+               push @globs, @{$remote->{$t}};
+
                my $max_rev = eval { tmp_config(qw/--int --get/,
                                         "svn-remote.$repo_id.${t}-maxRev") };
                if (defined $max_rev && ($max_rev < $base)) {
@@ -1612,15 +1704,16 @@ sub read_all_remotes {
                } elsif (m!^(.+)\.(branches|tags)=
                           (.*):refs/remotes/(.+)\s*$/!x) {
                        my ($p, $g) = ($3, $4);
-                       my $rs = $r->{$1}->{$2} = {
-                                         t => $2,
-                                         remote => $1,
-                                         path => Git::SVN::GlobSpec->new($p),
-                                         ref => Git::SVN::GlobSpec->new($g) };
+                       my $rs = {
+                           t => $2,
+                           remote => $1,
+                           path => Git::SVN::GlobSpec->new($p),
+                           ref => Git::SVN::GlobSpec->new($g) };
                        if (length($rs->{ref}->{right}) != 0) {
                                die "The '*' glob character must be the last ",
                                    "character of '$g'\n";
                        }
+                       push @{ $r->{$1}->{$2} }, $rs;
                }
        }
 
@@ -1760,9 +1853,10 @@ sub find_by_url { # repos_root and, path are optional
                next if defined $repos_root && $repos_root ne $u;
 
                my $fetch = $remotes->{$repo_id}->{fetch} || {};
-               foreach (qw/branches tags/) {
-                       resolve_local_globs($u, $fetch,
-                                           $remotes->{$repo_id}->{$_});
+               foreach my $t (qw/branches tags/) {
+                       foreach my $globspec (@{$remotes->{$repo_id}->{$t}}) {
+                               resolve_local_globs($u, $fetch, $globspec);
+                       }
                }
                my $p = $path;
                my $rwr = rewrite_root({repo_id => $repo_id});
@@ -2990,6 +3084,14 @@ sub _rev_map_set {
          croak "write: $!";
 }
 
+sub _rev_map_reset {
+       my ($fh, $rev, $commit) = @_;
+       my $c = _rev_map_get($fh, $rev);
+       $c eq $commit or die "_rev_map_reset(@_) commit $c does not match!\n";
+       my $offset = sysseek($fh, 0, SEEK_CUR) or croak "seek: $!";
+       truncate $fh, $offset or croak "truncate: $!";
+}
+
 sub mkfile {
        my ($path) = @_;
        unless (-e $path) {
@@ -3006,6 +3108,7 @@ sub rev_map_set {
        my $db = $self->map_path($uuid);
        my $db_lock = "$db.lock";
        my $sig;
+       $update_ref ||= 0;
        if ($update_ref) {
                $SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} =
                            $SIG{USR1} = $SIG{USR2} = sub { $sig = $_[0] };
@@ -3029,7 +3132,8 @@ sub rev_map_set {
 
        sysopen(my $fh, $db_lock, O_RDWR | O_CREAT)
             or croak "Couldn't open $db_lock: $!\n";
-       _rev_map_set($fh, $rev, $commit);
+       $update_ref eq 'reset' ? _rev_map_reset($fh, $rev, $commit) :
+                                _rev_map_set($fh, $rev, $commit);
        if ($sync) {
                $fh->flush or die "Couldn't flush $db_lock: $!\n";
                $fh->sync or die "Couldn't sync $db_lock: $!\n";
@@ -3037,7 +3141,9 @@ sub rev_map_set {
        close $fh or croak $!;
        if ($update_ref) {
                $_head = $self;
-               command_noisy('update-ref', '-m', "r$rev",
+               my $note = "";
+               $note = " ($update_ref)" if ($update_ref !~ /^\d*$/);
+               command_noisy('update-ref', '-m', "r$rev$note",
                              $self->refname, $commit);
        }
        rename $db_lock, $db or die "rev_map_set(@_): ", "Failed to rename: ",
@@ -3099,12 +3205,19 @@ sub rev_map_get {
        return undef unless -e $map_path;
 
        sysopen(my $fh, $map_path, O_RDONLY) or croak "open: $!";
+       my $c = _rev_map_get($fh, $rev);
+       close($fh) or croak "close: $!";
+       $c
+}
+
+sub _rev_map_get {
+       my ($fh, $rev) = @_;
+
        binmode $fh or croak "binmode: $!";
        my $size = (stat($fh))[7];
        ($size % 24) == 0 or croak "inconsistent size: $size";
 
        if ($size == 0) {
-               close $fh or croak "close: $fh";
                return undef;
        }
 
@@ -3122,11 +3235,9 @@ sub rev_map_get {
                } elsif ($r > $rev) {
                        $u = $i - 24;
                } else { # $r == $rev
-                       close($fh) or croak "close: $!";
                        return $c eq ('0' x 40) ? undef : $c;
                }
        }
-       close($fh) or croak "close: $!";
        undef;
 }
 
@@ -3138,6 +3249,8 @@ sub find_rev_before {
        my ($self, $rev, $eq_ok, $min_rev) = @_;
        --$rev unless $eq_ok;
        $min_rev ||= 1;
+       my $max_rev = $self->rev_map_max;
+       $rev = $max_rev if ($rev > $max_rev);
        while ($rev >= $min_rev) {
                if (my $c = $self->rev_map_get($rev)) {
                        return ($rev, $c);