X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=git-svn.perl;h=4c779b6c6d1c53030c0ed984155c885a25604e57;hb=8391c60b6e08604e7023a8fff74350d6b35974af;hp=51979f9639d2da8a7b9a38527051ccf62ebcd7b6;hpb=1fd81efad0d65afba91f8fb27b40a6345bfb50de;p=git.git diff --git a/git-svn.perl b/git-svn.perl index 51979f963..4c779b6c6 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -77,11 +77,12 @@ my %fc_opts = ( 'follow-parent|follow!' => \$Git::SVN::_follow_parent, \$Git::SVN::_repack_flags, %remote_opts ); -my ($_trunk, $_tags, $_branches); +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, + 'stdlayout|s' => \$_stdlayout, 'minimize-url|m' => \$Git::SVN::_minimize_url, 'no-metadata' => sub { $icv{noMetadata} = 1 }, 'use-svm-props' => sub { $icv{useSvmProps} = 1 }, @@ -292,7 +293,8 @@ sub init_subdir { sub cmd_clone { my ($url, $path) = @_; if (!defined $path && - (defined $_trunk || defined $_branches || defined $_tags) && + (defined $_trunk || defined $_branches || defined $_tags || + defined $_stdlayout) && $url !~ m#^[a-z\+]+://#) { $path = $url; } @@ -302,6 +304,11 @@ sub cmd_clone { } sub cmd_init { + if (defined $_stdlayout) { + $_trunk = 'trunk' if (!defined $_trunk); + $_tags = 'tags' if (!defined $_tags); + $_branches = 'branches' if (!defined $_branches); + } if (defined $_trunk || defined $_branches || defined $_tags) { return cmd_multi_init(@_); } @@ -367,16 +374,27 @@ sub cmd_set_tree { sub cmd_dcommit { my $head = shift; + 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"; $head ||= 'HEAD'; my @refs; my ($url, $rev, $uuid, $gs) = working_head_info($head, \@refs); + print "Committing to $url ...\n"; unless ($gs) { die "Unable to determine upstream SVN information from ", "$head history\n"; } my $last_rev; my ($linear_refs, $parents) = linearize_history($gs, \@refs); - foreach my $d (@$linear_refs) { + if ($_no_rebase && scalar(@$linear_refs) > 1) { + warn "Attempting to commit more than one change while ", + "--no-rebase is enabled.\n", + "If these changes depend on each other, re-running ", + "without --no-rebase will be required." + } + while (1) { + my $d = shift @$linear_refs or last; unless (defined $last_rev) { (undef, $last_rev, undef) = cmt_metadata("$d~1"); unless (defined $last_rev) { @@ -387,6 +405,7 @@ sub cmd_dcommit { if ($_dry_run) { print "diff-tree $d~1 $d\n"; } else { + my $cmt_rev; my %ed_opts = ( r => $last_rev, log => get_commit_entry($d)->{log}, ra => Git::SVN::Ra->new($gs->full_url), @@ -394,42 +413,78 @@ sub cmd_dcommit { tree_b => $d, editor_cb => sub { print "Committed r$_[0]\n"; - $last_rev = $_[0]; }, + $cmt_rev = $_[0]; + }, 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} = + $gs->{inject_parents_dcommit}->{$cmt_rev} = $parents->{$d}; } + $_fetch_all ? $gs->fetch_all : $gs->fetch; + next if $_no_rebase; + + # we always want to rebase against the current HEAD, + # not any head that was passed to us + my @diff = command('diff-tree', $d, + $gs->refname, '--'); + my @finish; + if (@diff) { + @finish = rebase_cmd(); + print STDERR "W: $d and ", $gs->refname, + " differ, using @finish:\n", + join("\n", @diff), "\n"; + } else { + print "No changes between current HEAD and ", + $gs->refname, + "\nResetting to the latest ", + $gs->refname, "\n"; + @finish = qw/reset --mixed/; + } + command_noisy(@finish, $gs->refname); + if (@diff) { + @refs = (); + my ($url_, $rev_, $uuid_, $gs_) = + working_head_info($head, \@refs); + my ($linear_refs_, $parents_) = + linearize_history($gs_, \@refs); + if (scalar(@$linear_refs) != + scalar(@$linear_refs_)) { + fatal "# of revisions changed ", + "\nbefore:\n", + join("\n", @$linear_refs), + "\n\nafter:\n", + join("\n", @$linear_refs_), "\n", + 'If you are attempting to commit ', + "merges, try running:\n\t", + 'git rebase --interactive', + '--preserve-merges ', + $gs->refname, + "\nBefore dcommitting"; + } + if ($url_ ne $url) { + fatal "URL mismatch after rebase: ", + "$url_ != $url"; + } + if ($uuid_ ne $uuid) { + fatal "uuid mismatch after rebase: ", + "$uuid_ != $uuid"; + } + # remap parents + my (%p, @l, $i); + for ($i = 0; $i < scalar @$linear_refs; $i++) { + my $new = $linear_refs_->[$i] or next; + $p{$new} = + $parents->{$linear_refs->[$i]}; + push @l, $new; + } + $parents = \%p; + $linear_refs = \@l; + } + $last_rev = $cmt_rev; } } - return if $_dry_run; - unless ($gs) { - warn "Could not determine fetch information for $url\n", - "Will not attempt to fetch and rebase commits.\n", - "This probably means you have useSvmProps and should\n", - "now resync your SVN::Mirror repository.\n"; - return; - } - $_fetch_all ? $gs->fetch_all : $gs->fetch; - unless ($_no_rebase) { - # we always want to rebase against the current HEAD, not any - # head that was passed to us - my @diff = command('diff-tree', 'HEAD', $gs->refname, '--'); - my @finish; - if (@diff) { - @finish = rebase_cmd(); - print STDERR "W: HEAD and ", $gs->refname, " differ, ", - "using @finish:\n", "@diff"; - } else { - print "No changes between current HEAD and ", - $gs->refname, "\nResetting to the latest ", - $gs->refname, "\n"; - @finish = qw/reset --mixed/; - } - command_noisy(@finish, $gs->refname); - } } sub cmd_find_rev { @@ -740,7 +795,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; @@ -799,7 +854,7 @@ sub cmt_metadata { sub working_head_info { my ($head, $refs) = @_; - my ($fh, $ctx) = command_output_pipe('log', $head); + my ($fh, $ctx) = command_output_pipe('log', '--no-color', $head); my $hash; my %max; while (<$fh>) { @@ -829,14 +884,9 @@ sub working_head_info { 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 + chomp(my $p = command_oneline(qw/rev-list --parents -1/, $c)); + $p =~ s/^($c)\s*// or die "rev-list --parents -1 $c failed!\n"; + @{$parents->{$c}} = split(/ /, $p); } sub linearize_history { @@ -938,8 +988,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", @@ -1026,7 +1076,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)= @@ -1146,6 +1198,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); @@ -1236,7 +1289,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) = @_; @@ -2028,7 +2114,7 @@ sub rebuild { return; } print "Rebuilding $db_path ...\n"; - my ($log, $ctx) = command_output_pipe("log", $self->refname); + my ($log, $ctx) = command_output_pipe("log", '--no-color', $self->refname); my $latest; my $full_url = $self->full_url; remove_username($full_url); @@ -2721,6 +2807,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); } @@ -2962,7 +3051,7 @@ package Git::SVN::Ra; use vars qw/@ISA $config_dir $_log_window_size/; use strict; use warnings; -my ($can_do_switch, %ignored_err, $RA); +my ($ra_invalid, $can_do_switch, %ignored_err, $RA); BEGIN { # enforce temporary pool usage for some simple functions @@ -3002,6 +3091,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, @@ -3122,7 +3212,11 @@ sub gs_do_switch { $self->{url} = $full_url; $reparented = 1; } else { + $_[0] = undef; + $self = undef; + $RA = undef; $ra = Git::SVN::Ra->new($full_url); + $ra_invalid = 1; } } $ra ||= $self; @@ -3182,6 +3276,7 @@ sub gs_fetch_loop_common { my $inc = $_log_window_size; my ($min, $max) = ($base, $head < $base + $inc ? $head : $base + $inc); my $longest_path = longest_common_path($gsv, $globs); + my $ra_url = $self->{url}; while (1) { my %revs; my $err; @@ -3243,6 +3338,13 @@ sub gs_fetch_loop_common { "$g->{t}-maxRev"; Git::SVN::tmp_config($k, $r); } + if ($ra_invalid) { + $_[0] = undef; + $self = undef; + $RA = undef; + $self = Git::SVN::Ra->new($ra_url); + $ra_invalid = undef; + } } # pre-fill the .rev_db since it'll eventually get filled in # with '0' x40 if something new gets committed @@ -3461,11 +3563,17 @@ sub log_use_color { sub git_svn_log_cmd { my ($r_min, $r_max, @args) = @_; my $head = 'HEAD'; + my (@files, @log_opts); foreach my $x (@args) { - last if $x eq '--'; - next unless ::verify_ref("$x^0"); - $head = $x; - last; + if ($x eq '--' || @files) { + push @files, $x; + } else { + if (::verify_ref("$x^0")) { + $head = $x; + } else { + push @log_opts, $x; + } + } } my ($url, $rev, $uuid, $gs) = ::working_head_info($head); @@ -3475,13 +3583,13 @@ sub git_svn_log_cmd { push @cmd, '-r' unless $non_recursive; push @cmd, qw/--raw --name-status/ if $verbose; push @cmd, '--color' if log_use_color(); - return @cmd unless defined $r_max; - if ($r_max == $r_min) { + push @cmd, @log_opts; + if (defined $r_max && $r_max == $r_min) { push @cmd, '--max-count=1'; if (my $c = $gs->rev_db_get($r_max)) { push @cmd, $c; } - } else { + } elsif (defined $r_max) { my ($c_min, $c_max); $c_max = $gs->rev_db_get($r_max); $c_min = $gs->rev_db_get($r_min); @@ -3497,7 +3605,7 @@ sub git_svn_log_cmd { push @cmd, $c_min; } } - return @cmd; + return (@cmd, @files); } # adapted from pager.c @@ -3511,7 +3619,7 @@ sub config_pager { } sub run_pager { - return unless -t *STDOUT; + return unless -t *STDOUT && defined $pager; pipe my $rfd, my $wfd or return; defined(my $pid = fork) or ::fatal "Can't fork: $!\n"; if (!$pid) { @@ -3662,7 +3770,7 @@ sub cmd_show_log { } config_pager(); - @args = (git_svn_log_cmd($r_min, $r_max, @args), @args); + @args = git_svn_log_cmd($r_min, $r_max, @args); my $log = command_output_pipe(@args); run_pager(); my (@k, $c, $d, $stat);