diff options
Diffstat (limited to 'git-svn.perl')
-rwxr-xr-x | git-svn.perl | 2183 |
1 files changed, 576 insertions, 1607 deletions
diff --git a/git-svn.perl b/git-svn.perl index 473a0b9d55..0b074c4c63 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -1,6 +1,7 @@ -#!/usr/bin/env perl +#!/usr/bin/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 @@ -21,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}; @@ -36,16 +36,37 @@ $ENV{TZ} = 'UTC'; $| = 1; # unbuffer STDOUT sub fatal (@) { print STDERR "@_\n"; exit 1 } -require SVN::Core; # use()-ing this causes segfaults for me... *shrug* -require SVN::Ra; -require SVN::Delta; -if ($SVN::Core::VERSION lt '1.1.0') { - fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)"; + +# All SVN commands do it. Otherwise we may die on SIGPIPE when the remote +# repository decides to close the connection which we expect to be kept alive. +$SIG{PIPE} = 'IGNORE'; + +# Given a dot separated version number, "subtract" it from +# the SVN::Core::VERSION; non-negaitive return means the SVN::Core +# is at least at the version the caller asked for. +sub compare_svn_version { + my (@ours) = split(/\./, $SVN::Core::VERSION); + my (@theirs) = split(/\./, $_[0]); + my ($i, $diff); + + for ($i = 0; $i < @ours && $i < @theirs; $i++) { + $diff = $ours[$i] - $theirs[$i]; + return $diff if ($diff); + } + return 1 if ($i < @ours); + return -1 if ($i < @theirs); + return 0; +} + +sub _req_svn { + require SVN::Core; # use()-ing this causes segfaults for me... *shrug* + require SVN::Ra; + require SVN::Delta; + if (::compare_svn_version('1.1.0') < 0) { + fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)"; + } } my $can_compress = eval { require Compress::Zlib; 1}; -push @Git::SVN::Ra::ISA, 'SVN::Ra'; -push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor'; -push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor'; use Carp qw/croak/; use Digest::MD5; use IO::File qw//; @@ -56,6 +77,11 @@ use File::Find; use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; use IPC::Open3; use Git; +use Git::SVN::Editor qw//; +use Git::SVN::Fetcher qw//; +use Git::SVN::Ra qw//; +use Git::SVN::Prompt qw//; +use Memoize; # core since 5.8.0, Jul 2002 BEGIN { # import functions from Git into our packages, en masse @@ -63,12 +89,13 @@ BEGIN { foreach (qw/command command_oneline command_noisy command_output_pipe command_input_pipe command_close_pipe command_bidi_pipe command_close_bidi_pipe/) { - for my $package ( qw(SVN::Git::Editor SVN::Git::Fetcher - Git::SVN::Migration Git::SVN::Log Git::SVN), + for my $package ( qw(Git::SVN::Migration Git::SVN::Log Git::SVN), __PACKAGE__) { *{"${package}::$_"} = \&{"Git::$_"}; } } + Memoize::memoize 'Git::config'; + Memoize::memoize 'Git::config_bool'; } my ($SVN); @@ -79,15 +106,17 @@ my ($_stdin, $_help, $_edit, $_message, $_file, $_branch_dest, $_template, $_shared, $_version, $_fetch_all, $_no_rebase, $_fetch_parent, - $_merge, $_strategy, $_dry_run, $_local, + $_merge, $_strategy, $_preserve_merges, $_dry_run, $_local, $_prefix, $_no_checkout, $_url, $_verbose, - $_git_format, $_commit_url, $_tag); + $_git_format, $_commit_url, $_tag, $_merge_info, $_interactive); $Git::SVN::_follow_parent = 1; +$Git::SVN::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' => \$Git::SVN::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, @@ -119,10 +148,10 @@ my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared, 'rewrite-uuid=s' => sub { $icv{rewriteUUID} = $_[1] }, %remote_opts ); my %cmt_opts = ( 'edit|e' => \$_edit, - 'rmdir' => \$SVN::Git::Editor::_rmdir, - 'find-copies-harder' => \$SVN::Git::Editor::_find_copies_harder, - 'l=i' => \$SVN::Git::Editor::_rename_limit, - 'copy-similarity|C=i'=> \$SVN::Git::Editor::_cp_similarity + 'rmdir' => \$Git::SVN::Editor::_rmdir, + 'find-copies-harder' => \$Git::SVN::Editor::_find_copies_harder, + 'l=i' => \$Git::SVN::Editor::_rename_limit, + 'copy-similarity|C=i'=> \$Git::SVN::Editor::_cp_similarity ); my %cmd = ( @@ -133,6 +162,10 @@ my %cmd = ( %fc_opts } ], clone => [ \&cmd_clone, "Initialize and fetch revisions", { 'revision|r=s' => \$_revision, + 'preserve-empty-dirs' => + \$Git::SVN::Fetcher::_preserve_empty_dirs, + 'placeholder-filename=s' => + \$Git::SVN::Fetcher::_placeholder_filename, %fc_opts, %init_opts } ], init => [ \&cmd_init, "Initialize a repo for tracking" . " (requires URL argument)", @@ -151,6 +184,8 @@ my %cmd = ( 'commit-url=s' => \$_commit_url, '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', @@ -220,6 +255,7 @@ my %cmd = ( 'local|l' => \$_local, 'fetch-all|all' => \$_fetch_all, 'dry-run|n' => \$_dry_run, + 'preserve-merges|p' => \$_preserve_merges, %fc_opts } ], 'commit-diff' => [ \&cmd_commit_diff, 'Commit a diff between two trees', @@ -244,6 +280,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]}) { @@ -287,7 +344,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 { @@ -349,10 +406,41 @@ information. } sub version { + ::_req_svn(); print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n"; 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'); @@ -367,7 +455,6 @@ sub do_git_init_db { command_noisy(@init_db); $_repository = Git->repository(Repository => ".git"); } - command_noisy('config', 'core.autocrlf', 'false'); my $set; my $pfx = "svn-remote.$Git::SVN::default_repo_id"; foreach my $i (keys %icv) { @@ -376,9 +463,18 @@ 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 = \$Git::SVN::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 $Git::SVN::Fetcher::_preserve_empty_dirs) { + my $fname = \$Git::SVN::Fetcher::_placeholder_filename; + command_noisy('config', "$pfx.preserve-empty-dirs", 'true'); + command_noisy('config', "$pfx.placeholder-filename", $$fname); + } } sub init_subdir { @@ -490,8 +586,198 @@ sub cmd_set_tree { unlink $gs->{index}; } +sub split_merge_info_range { + my ($range) = @_; + if ($range =~ /(\d+)-(\d+)/) { + return (int($1), int($2)); + } else { + return (int($range), int($range)); + } +} + +sub combine_ranges { + my ($in) = @_; + + my @fnums = (); + my @arr = split(/,/, $in); + for my $element (@arr) { + my ($start, $end) = split_merge_info_range($element); + push @fnums, $start; + } + + my @sorted = @arr [ sort { + $fnums[$a] <=> $fnums[$b] + } 0..$#arr ]; + + my @return = (); + my $last = -1; + my $first = -1; + for my $element (@sorted) { + my ($start, $end) = split_merge_info_range($element); + + if ($last == -1) { + $first = $start; + $last = $end; + next; + } + if ($start <= $last+1) { + if ($end > $last) { + $last = $end; + } + next; + } + if ($first == $last) { + push @return, "$first"; + } else { + push @return, "$first-$last"; + } + $first = $start; + $last = $end; + } + + if ($first != -1) { + if ($first == $last) { + push @return, "$first"; + } else { + push @return, "$first-$last"; + } + } + + return join(',', @return); +} + +sub merge_revs_into_hash { + my ($hash, $minfo) = @_; + my @lines = split(' ', $minfo); + + for my $line (@lines) { + my ($branchpath, $revs) = split(/:/, $line); + + if (exists($hash->{$branchpath})) { + # Merge the two revision sets + my $combined = "$hash->{$branchpath},$revs"; + $hash->{$branchpath} = combine_ranges($combined); + } else { + # Just do range combining for consolidation + $hash->{$branchpath} = combine_ranges($revs); + } + } +} + +sub merge_merge_info { + my ($mergeinfo_one, $mergeinfo_two) = @_; + my %result_hash = (); + + merge_revs_into_hash(\%result_hash, $mergeinfo_one); + merge_revs_into_hash(\%result_hash, $mergeinfo_two); + + my $result = ''; + # Sort below is for consistency's sake + for my $branchname (sort keys(%result_hash)) { + my $revlist = $result_hash{$branchname}; + $result .= "$branchname:$revlist\n" + } + return $result; +} + +sub populate_merge_info { + my ($d, $gs, $uuid, $linear_refs, $rewritten_parent) = @_; + + my %parentshash; + read_commit_parents(\%parentshash, $d); + my @parents = @{$parentshash{$d}}; + if ($#parents > 0) { + # Merge commit + my $all_parents_ok = 1; + my $aggregate_mergeinfo = ''; + my $rooturl = $gs->repos_root; + + if (defined($rewritten_parent)) { + # Replace first parent with newly-rewritten version + shift @parents; + unshift @parents, $rewritten_parent; + } + + foreach my $parent (@parents) { + my ($branchurl, $svnrev, $paruuid) = + cmt_metadata($parent); + + unless (defined($svnrev)) { + # Should have been caught be preflight check + fatal "merge commit $d has ancestor $parent, but that change " + ."does not have git-svn metadata!"; + } + unless ($branchurl =~ /^\Q$rooturl\E(.*)/) { + fatal "commit $parent git-svn metadata changed mid-run!"; + } + my $branchpath = $1; + + my $ra = Git::SVN::Ra->new($branchurl); + my (undef, undef, $props) = + $ra->get_dir(canonicalize_path("."), $svnrev); + my $par_mergeinfo = $props->{'svn:mergeinfo'}; + unless (defined $par_mergeinfo) { + $par_mergeinfo = ''; + } + # Merge previous mergeinfo values + $aggregate_mergeinfo = + merge_merge_info($aggregate_mergeinfo, + $par_mergeinfo, 0); + + next if $parent eq $parents[0]; # Skip first parent + # Add new changes being placed in tree by merge + my @cmd = (qw/rev-list --reverse/, + $parent, qw/--not/); + foreach my $par (@parents) { + unless ($par eq $parent) { + push @cmd, $par; + } + } + my @revsin = (); + my ($revlist, $ctx) = command_output_pipe(@cmd); + while (<$revlist>) { + my $irev = $_; + chomp $irev; + my (undef, $csvnrev, undef) = + cmt_metadata($irev); + unless (defined $csvnrev) { + # A child is missing SVN annotations... + # this might be OK, or might not be. + warn "W:child $irev is merged into revision " + ."$d but does not have git-svn metadata. " + ."This means git-svn cannot determine the " + ."svn revision numbers to place into the " + ."svn:mergeinfo property. You must ensure " + ."a branch is entirely committed to " + ."SVN before merging it in order for " + ."svn:mergeinfo population to function " + ."properly"; + } + push @revsin, $csvnrev; + } + command_close_pipe($revlist, $ctx); + + last unless $all_parents_ok; + + # We now have a list of all SVN revnos which are + # merged by this particular parent. Integrate them. + next if $#revsin == -1; + my $newmergeinfo = "$branchpath:" . join(',', @revsin); + $aggregate_mergeinfo = + merge_merge_info($aggregate_mergeinfo, + $newmergeinfo, 1); + } + if ($all_parents_ok and $aggregate_mergeinfo) { + return $aggregate_mergeinfo; + } + } + + return undef; +} + 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"; @@ -523,7 +809,7 @@ sub cmd_dcommit { $url = eval { command_oneline('config', '--get', "svn-remote.$gs->{repo_id}.commiturl") }; if (!$url) { - $url = $gs->full_url + $url = $gs->full_pushurl } } @@ -538,8 +824,88 @@ 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 { + command_oneline(qw/config --get svn.pushmergeinfo/) + }; + if (not defined($push_merge_info) + or $push_merge_info eq "false" + or $push_merge_info eq "no" + or $push_merge_info eq "never") { + $push_merge_info = 0; + } + + unless (defined($_merge_info) || ! $push_merge_info) { + # Preflight check of changes to ensure no issues with mergeinfo + # This includes check for uncommitted-to-SVN parents + # (other than the first parent, which we will handle), + # information from different SVN repos, and paths + # which are not underneath this repository root. + my $rooturl = $gs->repos_root; + foreach my $d (@$linear_refs) { + my %parentshash; + read_commit_parents(\%parentshash, $d); + my @realparents = @{$parentshash{$d}}; + if ($#realparents > 0) { + # Merge commit + shift @realparents; # Remove/ignore first parent + foreach my $parent (@realparents) { + my ($branchurl, $svnrev, $paruuid) = cmt_metadata($parent); + unless (defined $paruuid) { + # A parent is missing SVN annotations... + # abort the whole operation. + fatal "$parent is merged into revision $d, " + ."but does not have git-svn metadata. " + ."Either dcommit the branch or use a " + ."local cherry-pick, FF merge, or rebase " + ."instead of an explicit merge commit."; + } + + unless ($paruuid eq $uuid) { + # Parent has SVN metadata from different repository + fatal "merge parent $parent for change $d has " + ."git-svn uuid $paruuid, while current change " + ."has uuid $uuid!"; + } + + 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 " + ."git-svn root $rooturl!"; + } + } + } + } + } + + my $rewritten_parent; Git::SVN::remove_username($expect_url); + if (defined($_merge_info)) { + $_merge_info =~ tr{ }{\n}; + } while (1) { my $d = shift @$linear_refs or last; unless (defined $last_rev) { @@ -553,6 +919,14 @@ sub cmd_dcommit { print "diff-tree $d~1 $d\n"; } else { my $cmt_rev; + + unless (defined($_merge_info) || ! $push_merge_info) { + $_merge_info = populate_merge_info($d, $gs, + $uuid, + $linear_refs, + $rewritten_parent); + } + my %ed_opts = ( r => $last_rev, log => get_commit_entry($d)->{log}, ra => Git::SVN::Ra->new($url), @@ -565,8 +939,9 @@ 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) { + if (!Git::SVN::Editor->new(\%ed_opts)->apply_diff) { print "No changes\n$d~1 == $d\n"; } elsif ($parents->{$d} && @{$parents->{$d}}) { $gs->{inject_parents_dcommit}->{$cmt_rev} = @@ -594,6 +969,9 @@ sub cmd_dcommit { @finish = qw/reset --mixed/; } command_noisy(@finish, $gs->refname); + + $rewritten_parent = command_oneline(qw/rev-parse HEAD/); + if (@diff) { @refs = (); my ($url_, $rev_, $uuid_, $gs_) = @@ -670,7 +1048,7 @@ sub cmd_branch { $head ||= 'HEAD'; my (undef, $rev, undef, $gs) = working_head_info($head); - my $src = $gs->full_url; + my $src = $gs->full_pushurl; my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}}; my $allglobs = $remote->{ $_tag ? 'tags' : 'branches' }; @@ -687,8 +1065,7 @@ sub cmd_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}); + my $re = Git::SVN::Editor::glob2pat($g->{path}->{left}); if ($_branch_dest =~ /$re/) { $glob = $g; last; @@ -721,7 +1098,7 @@ sub cmd_branch { $url = eval { command_oneline('config', '--get', "svn-remote.$gs->{repo_id}.commiturl") }; if (!$url) { - $url = $remote->{url}; + $url = $remote->{pushurl} || $remote->{url}; } } my $dst = join '/', $url, $lft, $branch_name, ($rgt || ()); @@ -730,6 +1107,8 @@ sub cmd_branch { $src=~s/^http:/https:/; } + ::_req_svn(); + my $ctx = SVN::Client->new( auth => Git::SVN::Ra::_auth_providers(), log_msg => sub { @@ -773,6 +1152,15 @@ sub cmd_find_rev { print "$result\n" if $result; } +sub auto_create_empty_directories { + my ($gs) = @_; + my $var = eval { command_oneline('config', '--get', '--bool', + "svn-remote.$gs->{repo_id}.automkdirs") }; + # By default, create empty directories by consulting the unhandled log, + # but allow setting it to 'false' to skip it. + return !($var && $var eq 'false'); +} + sub cmd_rebase { command_noisy(qw/update-index --refresh/); my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); @@ -796,7 +1184,9 @@ sub cmd_rebase { $_fetch_all ? $gs->fetch_all : $gs->fetch; } command_noisy(rebase_cmd(), $gs->refname); - $gs->mkemptydirs; + if (auto_create_empty_directories($gs)) { + $gs->mkemptydirs; + } } sub cmd_show_ignore { @@ -959,6 +1349,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) }; @@ -1032,7 +1423,7 @@ sub cmd_commit_diff { tree_b => $tb, editor_cb => sub { print "Committed r$_[0]\n" }, svn_path => $svn_path ); - if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) { + if (!Git::SVN::Editor->new(\%ed_opts)->apply_diff) { print "No changes\n$ta == $tb\n"; } } @@ -1098,8 +1489,9 @@ sub cmd_info { if ($@) { $result .= "Repository Root: (offline)\n"; } + ::_req_svn(); $result .= "Repository UUID: $uuid\n" unless $diff_status eq "A" && - ($SVN::Core::VERSION le '1.5.4' || $file_type ne "dir"); + (::compare_svn_version('1.5.4') <= 0 || $file_type ne "dir"); $result .= "Revision: " . ($diff_status eq "A" ? 0 : $rev) . "\n"; $result .= "Node Kind: " . @@ -1180,6 +1572,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"; } @@ -1199,6 +1592,7 @@ sub rebase_cmd { push @cmd, '-v' if $_verbose; push @cmd, qw/--merge/ if $_merge; push @cmd, "--strategy=$_strategy" if $_strategy; + push @cmd, "--preserve-merges" if $_preserve_merges; @cmd; } @@ -1231,7 +1625,9 @@ sub post_fetch_checkout { command_noisy(qw/read-tree -m -u -v HEAD HEAD/); print STDERR "Checked out HEAD:\n ", $gs->full_url, " r", $gs->last_rev, "\n"; - $gs->mkemptydirs($gs->last_rev); + if (auto_create_empty_directories($gs)) { + $gs->mkemptydirs($gs->last_rev); + } } sub complete_svn_url { @@ -1505,7 +1901,7 @@ sub cmt_sha2rev_batch { sub working_head_info { my ($head, $refs) = @_; - my @args = ('log', '--no-color', '--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; @@ -1655,8 +2051,14 @@ 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; +use POSIX qw(:signal_h); +my $can_use_yaml; +BEGIN { + $can_use_yaml = eval { require Git::SVN::Memoize::YAML; 1}; +} my ($_gc_nr, $_gc_period); @@ -1812,18 +2214,24 @@ 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*$!) { $r->{$1}->{svm} = {}; } elsif (m!^(.+)\.url=\s*(.*)\s*$!) { $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); 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, @@ -1853,6 +2261,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; } @@ -2048,9 +2466,14 @@ 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"; + $self->{pushurl} = eval { command_oneline('config', '--get', + "svn-remote.$repo_id.pushurl") }; $self->rebuild; $self; } @@ -2081,6 +2504,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; } @@ -2520,6 +2951,15 @@ sub full_url { $self->{url} . (length $self->{path} ? '/' . $self->{path} : ''); } +sub full_pushurl { + my ($self) = @_; + if ($self->{pushurl}) { + return $self->{pushurl} . (length $self->{path} ? '/' . + $self->{path} : ''); + } else { + return $self->full_url; + } +} sub set_commit_header_env { my ($log_entry) = @_; @@ -2719,7 +3159,7 @@ sub find_parent_branch { # at the moment), so we can't rely on it $self->{last_rev} = $r0; $self->{last_commit} = $parent; - $ed = SVN::Git::Fetcher->new($self, $gs->{path}); + $ed = Git::SVN::Fetcher->new($self, $gs->{path}); $gs->ra->gs_do_switch($r0, $rev, $gs, $self->full_url, $ed) or die "SVN connection failed somewhere...\n"; @@ -2737,7 +3177,7 @@ sub find_parent_branch { } else { print STDERR "Following parent with do_update\n" unless $::_q > 1; - $ed = SVN::Git::Fetcher->new($self); + $ed = Git::SVN::Fetcher->new($self); $self->ra->gs_do_update($rev, $rev, $self, $ed) or die "SVN connection failed somewhere...\n"; } @@ -2760,7 +3200,7 @@ sub do_fetch { push @{$log_entry->{parents}}, $lc; return $log_entry; } - $ed = SVN::Git::Fetcher->new($self); + $ed = Git::SVN::Fetcher->new($self); $last_rev = $self->{last_rev}; $ed->{c} = $lc; @parents = ($lc); @@ -2769,7 +3209,7 @@ sub do_fetch { if (my $log_entry = $self->find_parent_branch($paths, $rev)) { return $log_entry; } - $ed = SVN::Git::Fetcher->new($self); + $ed = Git::SVN::Fetcher->new($self); } unless ($self->ra->gs_do_update($last_rev, $rev, $self, $ed)) { die "SVN connection failed somewhere...\n"; @@ -2822,8 +3262,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"; @@ -2874,6 +3315,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 @@ -2906,8 +3355,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, @@ -2937,18 +3385,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->metadata_url); + $ref_id .= '-'; + } + print STDERR "Initializing parent: $ref_id\n" unless $::_q > 1; } $gs } @@ -2993,7 +3452,7 @@ sub find_extra_svk_parents { for my $ticket ( @tickets ) { my ($uuid, $path, $rev) = split /:/, $ticket; if ( $uuid eq $self->ra_uuid ) { - my $url = $self->rewrite_root || $self->{url}; + my $url = $self->{url}; my $repos_root = $url; my $branch_from = $path; $branch_from =~ s{^/}{}; @@ -3058,8 +3517,12 @@ sub lookup_svn_merge { next; } - push @merged_commit_ranges, - "$bottom_commit^..$top_commit"; + if (scalar(command('rev-parse', "$bottom_commit^@"))) { + push @merged_commit_ranges, + "$bottom_commit^..$top_commit"; + } else { + push @merged_commit_ranges, "$top_commit"; + } if ( !defined $tip or $top > $tip ) { $tip = $top; @@ -3085,11 +3548,12 @@ 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)}; + delete @commits{_rev_list($range, "--")}; } for my $commit (keys %commits) { if (has_no_changes($commit)) { @@ -3117,6 +3581,17 @@ sub has_no_changes { command_oneline("rev-parse", "$commit~1^{tree}")); } +sub tie_for_persistent_memoization { + my $hash = shift; + my $path = shift; + + if ($can_use_yaml) { + tie %$hash => 'Git::SVN::Memoize::YAML', "$path.yaml"; + } else { + tie %$hash => 'Memoize::Storable', "$path.db", 'nstore'; + } +} + # The GIT_DIR environment variable is not always set until after the command # line arguments are processed, so we can't memoize in a BEGIN block. { @@ -3129,27 +3604,49 @@ sub has_no_changes { my $cache_path = "$ENV{GIT_DIR}/svn/.caches/"; mkpath([$cache_path]) unless -d $cache_path; - tie my %lookup_svn_merge_cache => 'Memoize::Storable', - "$cache_path/lookup_svn_merge.db", 'nstore'; + my %lookup_svn_merge_cache; + my %check_cherry_pick_cache; + my %has_no_changes_cache; + + tie_for_persistent_memoization(\%lookup_svn_merge_cache, + "$cache_path/lookup_svn_merge"); memoize 'lookup_svn_merge', SCALAR_CACHE => 'FAULT', LIST_CACHE => ['HASH' => \%lookup_svn_merge_cache], ; - tie my %check_cherry_pick_cache => 'Memoize::Storable', - "$cache_path/check_cherry_pick.db", 'nstore'; + tie_for_persistent_memoization(\%check_cherry_pick_cache, + "$cache_path/check_cherry_pick"); memoize 'check_cherry_pick', SCALAR_CACHE => 'FAULT', LIST_CACHE => ['HASH' => \%check_cherry_pick_cache], ; - tie my %has_no_changes_cache => 'Memoize::Storable', - "$cache_path/has_no_changes.db", 'nstore'; + tie_for_persistent_memoization(\%has_no_changes_cache, + "$cache_path/has_no_changes"); memoize 'has_no_changes', SCALAR_CACHE => ['HASH' => \%has_no_changes_cache], 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'; + } + + Memoize::memoize 'Git::SVN::repos_root'; +} + +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 { @@ -3201,7 +3698,7 @@ sub find_extra_svn_parents { # are now marked as merge, we can add the tip as a parent. my @merges = split "\n", $mergeinfo; my @merge_tips; - my $url = $self->rewrite_root || $self->{url}; + my $url = $self->{url}; my $uuid = $self->ra_uuid; my %ranges; for my $merge ( @merges ) { @@ -3247,6 +3744,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, ); @@ -3273,7 +3771,7 @@ sub find_extra_svn_parents { "$new_parents[$i]..$new_parents[$j]", ); if ( !$revs ) { - undef($new_parents[$i]); + undef($new_parents[$j]); } } } @@ -3431,7 +3929,7 @@ sub set_tree { editor_cb => sub { $self->set_tree_cb($log_entry, $tree, @_) }, svn_path => $self->{path} ); - if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) { + if (!Git::SVN::Editor->new(\%ed_opts)->apply_diff) { print "No changes\nr$self->{last_rev} = $tree\n"; } } @@ -3472,7 +3970,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; @@ -3600,14 +4098,18 @@ 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"; - my $sig; + my $sigmask; $update_ref ||= 0; if ($update_ref) { - $SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} = - $SIG{USR1} = $SIG{USR2} = sub { $sig = $_[0] }; + $sigmask = POSIX::SigSet->new(); + my $signew = POSIX::SigSet->new(SIGINT, SIGHUP, SIGTERM, + SIGALRM, SIGUSR1, SIGUSR2); + sigprocmask(SIG_BLOCK, $signew, $sigmask) or + croak "Can't block signals: $!"; } mkfile($db); @@ -3646,9 +4148,8 @@ sub rev_map_set { "$db_lock => $db ($!)\n"; delete $LOCKFILES{$db_lock}; if ($update_ref) { - $SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} = - $SIG{USR1} = $SIG{USR2} = 'DEFAULT'; - kill $sig, $$ if defined $sig; + sigprocmask(SIG_SETMASK, $sigmask) or + croak "Can't restore signal mask: $!"; } } @@ -3844,1539 +4345,10 @@ sub remove_username { $_[0] =~ s{^([^:]*://)[^@]+@}{$1}; } -package Git::SVN::Prompt; -use strict; -use warnings; -require SVN::Core; -use vars qw/$_no_auth_cache $_username/; - -sub simple { - my ($cred, $realm, $default_username, $may_save, $pool) = @_; - $may_save = undef if $_no_auth_cache; - $default_username = $_username if defined $_username; - if (defined $default_username && length $default_username) { - if (defined $realm && length $realm) { - print STDERR "Authentication realm: $realm\n"; - STDERR->flush; - } - $cred->username($default_username); - } else { - username($cred, $realm, $may_save, $pool); - } - $cred->password(_read_password("Password for '" . - $cred->username . "': ", $realm)); - $cred->may_save($may_save); - $SVN::_Core::SVN_NO_ERROR; -} - -sub ssl_server_trust { - my ($cred, $realm, $failures, $cert_info, $may_save, $pool) = @_; - $may_save = undef if $_no_auth_cache; - print STDERR "Error validating server certificate for '$realm':\n"; - { - no warnings 'once'; - # All variables SVN::Auth::SSL::* are used only once, - # so we're shutting up Perl warnings about this. - if ($failures & $SVN::Auth::SSL::UNKNOWNCA) { - print STDERR " - The certificate is not issued ", - "by a trusted authority. Use the\n", - " fingerprint to validate ", - "the certificate manually!\n"; - } - if ($failures & $SVN::Auth::SSL::CNMISMATCH) { - print STDERR " - The certificate hostname ", - "does not match.\n"; - } - if ($failures & $SVN::Auth::SSL::NOTYETVALID) { - print STDERR " - The certificate is not yet valid.\n"; - } - if ($failures & $SVN::Auth::SSL::EXPIRED) { - print STDERR " - The certificate has expired.\n"; - } - if ($failures & $SVN::Auth::SSL::OTHER) { - print STDERR " - The certificate has ", - "an unknown error.\n"; - } - } # no warnings 'once' - printf STDERR - "Certificate information:\n". - " - Hostname: %s\n". - " - Valid: from %s until %s\n". - " - Issuer: %s\n". - " - Fingerprint: %s\n", - map $cert_info->$_, qw(hostname valid_from valid_until - issuer_dname fingerprint); - my $choice; -prompt: - print STDERR $may_save ? - "(R)eject, accept (t)emporarily or accept (p)ermanently? " : - "(R)eject or accept (t)emporarily? "; - STDERR->flush; - $choice = lc(substr(<STDIN> || 'R', 0, 1)); - if ($choice =~ /^t$/i) { - $cred->may_save(undef); - } elsif ($choice =~ /^r$/i) { - return -1; - } elsif ($may_save && $choice =~ /^p$/i) { - $cred->may_save($may_save); - } else { - goto prompt; - } - $cred->accepted_failures($failures); - $SVN::_Core::SVN_NO_ERROR; -} - -sub ssl_client_cert { - my ($cred, $realm, $may_save, $pool) = @_; - $may_save = undef if $_no_auth_cache; - print STDERR "Client certificate filename: "; - STDERR->flush; - chomp(my $filename = <STDIN>); - $cred->cert_file($filename); - $cred->may_save($may_save); - $SVN::_Core::SVN_NO_ERROR; -} - -sub ssl_client_cert_pw { - my ($cred, $realm, $may_save, $pool) = @_; - $may_save = undef if $_no_auth_cache; - $cred->password(_read_password("Password: ", $realm)); - $cred->may_save($may_save); - $SVN::_Core::SVN_NO_ERROR; -} - -sub username { - my ($cred, $realm, $may_save, $pool) = @_; - $may_save = undef if $_no_auth_cache; - if (defined $realm && length $realm) { - print STDERR "Authentication realm: $realm\n"; - } - my $username; - if (defined $_username) { - $username = $_username; - } else { - print STDERR "Username: "; - STDERR->flush; - chomp($username = <STDIN>); - } - $cred->username($username); - $cred->may_save($may_save); - $SVN::_Core::SVN_NO_ERROR; -} - -sub _read_password { - my ($prompt, $realm) = @_; - print STDERR $prompt; - STDERR->flush; - require Term::ReadKey; - Term::ReadKey::ReadMode('noecho'); - my $password = ''; - while (defined(my $key = Term::ReadKey::ReadKey(0))) { - last if $key =~ /[\012\015]/; # \n\r - $password .= $key; - } - Term::ReadKey::ReadMode('restore'); - print STDERR "\n"; - STDERR->flush; - $password; -} - -package SVN::Git::Fetcher; -use vars qw/@ISA/; -use strict; -use warnings; -use Carp qw/croak/; -use File::Temp qw/tempfile/; -use IO::File qw//; -use vars qw/$_ignore_regex/; - -# file baton members: path, mode_a, mode_b, pool, fh, blob, base -sub new { - my ($class, $git_svn, $switch_path) = @_; - my $self = SVN::Delta::Editor->new; - bless $self, $class; - if (exists $git_svn->{last_commit}) { - $self->{c} = $git_svn->{last_commit}; - $self->{empty_symlinks} = - _mark_empty_symlinks($git_svn, $switch_path); - } - $self->{ignore_regex} = eval { command_oneline('config', '--get', - "svn-remote.$git_svn->{repo_id}.ignore-paths") }; - $self->{empty} = {}; - $self->{dir_prop} = {}; - $self->{file_prop} = {}; - $self->{absent_dir} = {}; - $self->{absent_file} = {}; - $self->{gii} = $git_svn->tmp_index_do(sub { Git::IndexInfo->new }); - $self; -} - -# this uses the Ra object, so it must be called before do_{switch,update}, -# not inside them (when the Git::SVN::Fetcher object is passed) to -# do_{switch,update} -sub _mark_empty_symlinks { - my ($git_svn, $switch_path) = @_; - my $bool = Git::config_bool('svn.brokenSymlinkWorkaround'); - return {} if (!defined($bool)) || (defined($bool) && ! $bool); - - my %ret; - my ($rev, $cmt) = $git_svn->last_rev_commit; - return {} unless ($rev && $cmt); - - # allow the warning to be printed for each revision we fetch to - # ensure the user sees it. The user can also disable the workaround - # on the repository even while git svn is running and the next - # revision fetched will skip this expensive function. - my $printed_warning; - chomp(my $empty_blob = `git hash-object -t blob --stdin < /dev/null`); - my ($ls, $ctx) = command_output_pipe(qw/ls-tree -r -z/, $cmt); - local $/ = "\0"; - my $pfx = defined($switch_path) ? $switch_path : $git_svn->{path}; - $pfx .= '/' if length($pfx); - while (<$ls>) { - chomp; - s/\A100644 blob $empty_blob\t//o or next; - unless ($printed_warning) { - print STDERR "Scanning for empty symlinks, ", - "this may take a while if you have ", - "many empty files\n", - "You may disable this with `", - "git config svn.brokenSymlinkWorkaround ", - "false'.\n", - "This may be done in a different ", - "terminal without restarting ", - "git svn\n"; - $printed_warning = 1; - } - my $path = $_; - my (undef, $props) = - $git_svn->ra->get_file($pfx.$path, $rev, undef); - if ($props->{'svn:special'}) { - $ret{$path} = 1; - } - } - command_close_pipe($ls, $ctx); - \%ret; -} - -# returns true if a given path is inside a ".git" directory -sub in_dot_git { - $_[0] =~ m{(?:^|/)\.git(?:/|$)}; -} - -# return value: 0 -- don't ignore, 1 -- ignore -sub is_path_ignored { - my ($self, $path) = @_; - return 1 if in_dot_git($path); - return 1 if defined($self->{ignore_regex}) && - $path =~ m!$self->{ignore_regex}!; - return 0 unless defined($_ignore_regex); - return 1 if $path =~ m!$_ignore_regex!o; - return 0; -} - -sub set_path_strip { - my ($self, $path) = @_; - $self->{path_strip} = qr/^\Q$path\E(\/|$)/ if length $path; -} - -sub open_root { - { path => '' }; -} - -sub open_directory { - my ($self, $path, $pb, $rev) = @_; - { path => $path }; -} - -sub git_path { - my ($self, $path) = @_; - if ($self->{path_strip}) { - $path =~ s!$self->{path_strip}!! or - die "Failed to strip path '$path' ($self->{path_strip})\n"; - } - $path; -} - -sub delete_entry { - my ($self, $path, $rev, $pb) = @_; - return undef if $self->is_path_ignored($path); - - my $gpath = $self->git_path($path); - return undef if ($gpath eq ''); - - # remove entire directories. - my ($tree) = (command('ls-tree', '-z', $self->{c}, "./$gpath") - =~ /\A040000 tree ([a-f\d]{40})\t\Q$gpath\E\0/); - if ($tree) { - my ($ls, $ctx) = command_output_pipe(qw/ls-tree - -r --name-only -z/, - $tree); - local $/ = "\0"; - while (<$ls>) { - chomp; - my $rmpath = "$gpath/$_"; - $self->{gii}->remove($rmpath); - print "\tD\t$rmpath\n" unless $::_q; - } - print "\tD\t$gpath/\n" unless $::_q; - command_close_pipe($ls, $ctx); - } else { - $self->{gii}->remove($gpath); - print "\tD\t$gpath\n" unless $::_q; - } - $self->{empty}->{$path} = 0; - undef; -} - -sub open_file { - my ($self, $path, $pb, $rev) = @_; - my ($mode, $blob); - - goto out if $self->is_path_ignored($path); - - my $gpath = $self->git_path($path); - ($mode, $blob) = (command('ls-tree', '-z', $self->{c}, "./$gpath") - =~ /\A(\d{6}) blob ([a-f\d]{40})\t\Q$gpath\E\0/); - unless (defined $mode && defined $blob) { - die "$path was not found in commit $self->{c} (r$rev)\n"; - } - if ($mode eq '100644' && $self->{empty_symlinks}->{$path}) { - $mode = '120000'; - } -out: - { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob, - pool => SVN::Pool->new, action => 'M' }; -} - -sub add_file { - my ($self, $path, $pb, $cp_path, $cp_rev) = @_; - my $mode; - - if (!$self->is_path_ignored($path)) { - my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#); - delete $self->{empty}->{$dir}; - $mode = '100644'; - } - { path => $path, mode_a => $mode, mode_b => $mode, - pool => SVN::Pool->new, action => 'A' }; -} - -sub add_directory { - my ($self, $path, $cp_path, $cp_rev) = @_; - goto out if $self->is_path_ignored($path); - my $gpath = $self->git_path($path); - if ($gpath eq '') { - my ($ls, $ctx) = command_output_pipe(qw/ls-tree - -r --name-only -z/, - $self->{c}); - local $/ = "\0"; - while (<$ls>) { - chomp; - $self->{gii}->remove($_); - print "\tD\t$_\n" unless $::_q; - } - command_close_pipe($ls, $ctx); - $self->{empty}->{$path} = 0; - } - my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#); - delete $self->{empty}->{$dir}; - $self->{empty}->{$path} = 1; -out: - { path => $path }; -} - -sub change_dir_prop { - my ($self, $db, $prop, $value) = @_; - return undef if $self->is_path_ignored($db->{path}); - $self->{dir_prop}->{$db->{path}} ||= {}; - $self->{dir_prop}->{$db->{path}}->{$prop} = $value; - undef; -} - -sub absent_directory { - my ($self, $path, $pb) = @_; - return undef if $self->is_path_ignored($path); - $self->{absent_dir}->{$pb->{path}} ||= []; - push @{$self->{absent_dir}->{$pb->{path}}}, $path; - undef; -} - -sub absent_file { - my ($self, $path, $pb) = @_; - return undef if $self->is_path_ignored($path); - $self->{absent_file}->{$pb->{path}} ||= []; - push @{$self->{absent_file}->{$pb->{path}}}, $path; - undef; -} - -sub change_file_prop { - my ($self, $fb, $prop, $value) = @_; - return undef if $self->is_path_ignored($fb->{path}); - if ($prop eq 'svn:executable') { - if ($fb->{mode_b} != 120000) { - $fb->{mode_b} = defined $value ? 100755 : 100644; - } - } elsif ($prop eq 'svn:special') { - $fb->{mode_b} = defined $value ? 120000 : 100644; - } else { - $self->{file_prop}->{$fb->{path}} ||= {}; - $self->{file_prop}->{$fb->{path}}->{$prop} = $value; - } - undef; -} - -sub apply_textdelta { - my ($self, $fb, $exp) = @_; - return undef if $self->is_path_ignored($fb->{path}); - my $fh = $::_repository->temp_acquire('svn_delta'); - # $fh gets auto-closed() by SVN::TxDelta::apply(), - # (but $base does not,) so dup() it for reading in close_file - open my $dup, '<&', $fh or croak $!; - my $base = $::_repository->temp_acquire('git_blob'); - - if ($fb->{blob}) { - my ($base_is_link, $size); - - if ($fb->{mode_a} eq '120000' && - ! $self->{empty_symlinks}->{$fb->{path}}) { - print $base 'link ' or die "print $!\n"; - $base_is_link = 1; - } - retry: - $size = $::_repository->cat_blob($fb->{blob}, $base); - die "Failed to read object $fb->{blob}" if ($size < 0); - - if (defined $exp) { - seek $base, 0, 0 or croak $!; - my $got = ::md5sum($base); - if ($got ne $exp) { - my $err = "Checksum mismatch: ". - "$fb->{path} $fb->{blob}\n" . - "expected: $exp\n" . - " got: $got\n"; - if ($base_is_link) { - warn $err, - "Retrying... (possibly ", - "a bad symlink from SVN)\n"; - $::_repository->temp_reset($base); - $base_is_link = 0; - goto retry; - } - die $err; - } - } - } - seek $base, 0, 0 or croak $!; - $fb->{fh} = $fh; - $fb->{base} = $base; - [ SVN::TxDelta::apply($base, $dup, undef, $fb->{path}, $fb->{pool}) ]; -} - -sub close_file { - my ($self, $fb, $exp) = @_; - return undef if $self->is_path_ignored($fb->{path}); - - my $hash; - my $path = $self->git_path($fb->{path}); - if (my $fh = $fb->{fh}) { - if (defined $exp) { - seek($fh, 0, 0) or croak $!; - my $got = ::md5sum($fh); - if ($got ne $exp) { - die "Checksum mismatch: $path\n", - "expected: $exp\n got: $got\n"; - } - } - if ($fb->{mode_b} == 120000) { - sysseek($fh, 0, 0) or croak $!; - my $rd = sysread($fh, my $buf, 5); - - if (!defined $rd) { - croak "sysread: $!\n"; - } elsif ($rd == 0) { - warn "$path has mode 120000", - " but it points to nothing\n", - "converting to an empty file with mode", - " 100644\n"; - $fb->{mode_b} = '100644'; - } elsif ($buf ne 'link ') { - warn "$path has mode 120000", - " but is not a link\n"; - } else { - my $tmp_fh = $::_repository->temp_acquire( - 'svn_hash'); - my $res; - while ($res = sysread($fh, my $str, 1024)) { - my $out = syswrite($tmp_fh, $str, $res); - defined($out) && $out == $res - or croak("write ", - Git::temp_path($tmp_fh), - ": $!\n"); - } - defined $res or croak $!; - - ($fh, $tmp_fh) = ($tmp_fh, $fh); - Git::temp_release($tmp_fh, 1); - } - } - - $hash = $::_repository->hash_and_insert_object( - Git::temp_path($fh)); - $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n"; - - Git::temp_release($fb->{base}, 1); - Git::temp_release($fh, 1); - } else { - $hash = $fb->{blob} or die "no blob information\n"; - } - $fb->{pool}->clear; - $self->{gii}->update($fb->{mode_b}, $hash, $path) or croak $!; - print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $::_q; - undef; -} - -sub abort_edit { - my $self = shift; - $self->{nr} = $self->{gii}->{nr}; - delete $self->{gii}; - $self->SUPER::abort_edit(@_); -} - -sub close_edit { - my $self = shift; - $self->{git_commit_ok} = 1; - $self->{nr} = $self->{gii}->{nr}; - delete $self->{gii}; - $self->SUPER::close_edit(@_); -} - -package SVN::Git::Editor; -use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/; -use strict; -use warnings; -use Carp qw/croak/; -use IO::File; - -sub new { - my ($class, $opts) = @_; - foreach (qw/svn_path r ra tree_a tree_b log editor_cb/) { - die "$_ required!\n" unless (defined $opts->{$_}); - } - - my $pool = SVN::Pool->new; - my $mods = generate_diff($opts->{tree_a}, $opts->{tree_b}); - my $types = check_diff_paths($opts->{ra}, $opts->{svn_path}, - $opts->{r}, $mods); - - # $opts->{ra} functions should not be used after this: - my @ce = $opts->{ra}->get_commit_editor($opts->{log}, - $opts->{editor_cb}, $pool); - my $self = SVN::Delta::Editor->new(@ce, $pool); - bless $self, $class; - foreach (qw/svn_path r tree_a tree_b/) { - $self->{$_} = $opts->{$_}; - } - $self->{url} = $opts->{ra}->{url}; - $self->{mods} = $mods; - $self->{types} = $types; - $self->{pool} = $pool; - $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) }; - $self->{rm} = { }; - $self->{path_prefix} = length $self->{svn_path} ? - "$self->{svn_path}/" : ''; - $self->{config} = $opts->{config}; - return $self; -} - -sub generate_diff { - my ($tree_a, $tree_b) = @_; - my @diff_tree = qw(diff-tree -z -r); - if ($_cp_similarity) { - push @diff_tree, "-C$_cp_similarity"; - } else { - push @diff_tree, '-C'; - } - push @diff_tree, '--find-copies-harder' if $_find_copies_harder; - push @diff_tree, "-l$_rename_limit" if defined $_rename_limit; - push @diff_tree, $tree_a, $tree_b; - my ($diff_fh, $ctx) = command_output_pipe(@diff_tree); - local $/ = "\0"; - my $state = 'meta'; - my @mods; - while (<$diff_fh>) { - chomp $_; # this gets rid of the trailing "\0" - if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s - ($::sha1)\s($::sha1)\s - ([MTCRAD])\d*$/xo) { - push @mods, { mode_a => $1, mode_b => $2, - sha1_a => $3, sha1_b => $4, - chg => $5 }; - if ($5 =~ /^(?:C|R)$/) { - $state = 'file_a'; - } else { - $state = 'file_b'; - } - } elsif ($state eq 'file_a') { - my $x = $mods[$#mods] or croak "Empty array\n"; - if ($x->{chg} !~ /^(?:C|R)$/) { - croak "Error parsing $_, $x->{chg}\n"; - } - $x->{file_a} = $_; - $state = 'file_b'; - } elsif ($state eq 'file_b') { - my $x = $mods[$#mods] or croak "Empty array\n"; - if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) { - croak "Error parsing $_, $x->{chg}\n"; - } - if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) { - croak "Error parsing $_, $x->{chg}\n"; - } - $x->{file_b} = $_; - $state = 'meta'; - } else { - croak "Error parsing $_\n"; - } - } - command_close_pipe($diff_fh, $ctx); - \@mods; -} - -sub check_diff_paths { - my ($ra, $pfx, $rev, $mods) = @_; - my %types; - $pfx .= '/' if length $pfx; - - sub type_diff_paths { - my ($ra, $types, $path, $rev) = @_; - my @p = split m#/+#, $path; - my $c = shift @p; - unless (defined $types->{$c}) { - $types->{$c} = $ra->check_path($c, $rev); - } - while (@p) { - $c .= '/' . shift @p; - next if defined $types->{$c}; - $types->{$c} = $ra->check_path($c, $rev); - } - } - - foreach my $m (@$mods) { - foreach my $f (qw/file_a file_b/) { - next unless defined $m->{$f}; - my ($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#); - if (length $pfx.$dir && ! defined $types{$dir}) { - type_diff_paths($ra, \%types, $pfx.$dir, $rev); - } - } - } - \%types; -} - -sub split_path { - return ($_[0] =~ m#^(.*?)/?([^/]+)$#); -} - -sub repo_path { - my ($self, $path) = @_; - $self->{path_prefix}.(defined $path ? $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); -} - -sub rmdirs { - my ($self) = @_; - my $rm = $self->{rm}; - delete $rm->{''}; # we never delete the url we're tracking - return unless %$rm; - - foreach (keys %$rm) { - my @d = split m#/#, $_; - my $c = shift @d; - $rm->{$c} = 1; - while (@d) { - $c .= '/' . shift @d; - $rm->{$c} = 1; - } - } - delete $rm->{$self->{svn_path}}; - delete $rm->{''}; # we never delete the url we're tracking - return unless %$rm; - - my ($fh, $ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/, - $self->{tree_b}); - local $/ = "\0"; - while (<$fh>) { - chomp; - my @dn = split m#/#, $_; - while (pop @dn) { - delete $rm->{join '/', @dn}; - } - unless (%$rm) { - close $fh; - return; - } - } - command_close_pipe($fh, $ctx); - - my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat}); - foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) { - $self->close_directory($bat->{$d}, $p); - my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#); - print "\tD+\t$d/\n" unless $::_q; - $self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p); - delete $bat->{$d}; - } -} - -sub open_or_add_dir { - my ($self, $full_path, $baton) = @_; - my $t = $self->{types}->{$full_path}; - if (!defined $t) { - die "$full_path not known in r$self->{r} or we have a bug!\n"; - } - { - 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) { - return $self->add_directory($full_path, $baton, - undef, -1, $self->{pool}); - } elsif ($t == $SVN::Node::dir) { - return $self->open_directory($full_path, $baton, - $self->{r}, $self->{pool}); - } # no warnings 'once' - print STDERR "$full_path already exists in repository at ", - "r$self->{r} and it is not a directory (", - ($t == $SVN::Node::file ? 'file' : 'unknown'),"/$t)\n"; - } # no warnings 'once' - exit 1; -} - -sub ensure_path { - my ($self, $path) = @_; - 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->{''}); - while (@p) { - my $c0 = $c; - $c .= '/' . shift @p; - $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0}); - } - return $bat->{$c}; -} - -# Subroutine to convert a globbing pattern to a regular expression. -# From perl cookbook. -sub glob2pat { - my $globstr = shift; - my %patmap = ('*' => '.*', '?' => '.', '[' => '[', ']' => ']'); - $globstr =~ s{(.)} { $patmap{$1} || "\Q$1" }ge; - return '^' . $globstr . '$'; -} - -sub check_autoprop { - my ($self, $pattern, $properties, $file, $fbat) = @_; - # Convert the globbing pattern to a regular expression. - my $regex = glob2pat($pattern); - # Check if the pattern matches the file name. - if($file =~ m/($regex)/) { - # Parse the list of properties to set. - my @props = split(/;/, $properties); - foreach my $prop (@props) { - # Parse 'name=value' syntax and set the property. - if ($prop =~ /([^=]+)=(.*)/) { - my ($n,$v) = ($1,$2); - for ($n, $v) { - s/^\s+//; s/\s+$//; - } - $self->change_file_prop($fbat, $n, $v); - } - } - } -} - -sub apply_autoprops { - my ($self, $file, $fbat) = @_; - my $conf_t = ${$self->{config}}{'config'}; - no warnings 'once'; - # Check [miscellany]/enable-auto-props in svn configuration. - if (SVN::_Core::svn_config_get_bool( - $conf_t, - $SVN::_Core::SVN_CONFIG_SECTION_MISCELLANY, - $SVN::_Core::SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS, - 0)) { - # Auto-props are enabled. Enumerate them to look for matches. - my $callback = sub { - $self->check_autoprop($_[0], $_[1], $file, $fbat); - }; - SVN::_Core::svn_config_enumerate( - $conf_t, - $SVN::_Core::SVN_CONFIG_SECTION_AUTO_PROPS, - $callback); - } -} - -sub A { - my ($self, $m) = @_; - my ($dir, $file) = split_path($m->{file_b}); - my $pbat = $self->ensure_path($dir); - my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat, - undef, -1); - print "\tA\t$m->{file_b}\n" unless $::_q; - $self->apply_autoprops($file, $fbat); - $self->chg_file($fbat, $m); - $self->close_file($fbat,undef,$self->{pool}); -} - -sub C { - my ($self, $m) = @_; - my ($dir, $file) = split_path($m->{file_b}); - my $pbat = $self->ensure_path($dir); - 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; - $self->chg_file($fbat, $m); - $self->close_file($fbat,undef,$self->{pool}); -} - -sub delete_entry { - my ($self, $path, $pbat) = @_; - my $rpath = $self->repo_path($path); - my ($dir, $file) = split_path($rpath); - $self->{rm}->{$dir} = 1; - $self->SUPER::delete_entry($rpath, $self->{r}, $pbat, $self->{pool}); -} - -sub R { - my ($self, $m) = @_; - my ($dir, $file) = split_path($m->{file_b}); - my $pbat = $self->ensure_path($dir); - 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; - $self->apply_autoprops($file, $fbat); - $self->chg_file($fbat, $m); - $self->close_file($fbat,undef,$self->{pool}); - - ($dir, $file) = split_path($m->{file_a}); - $pbat = $self->ensure_path($dir); - $self->delete_entry($m->{file_a}, $pbat); -} - -sub M { - my ($self, $m) = @_; - my ($dir, $file) = split_path($m->{file_b}); - my $pbat = $self->ensure_path($dir); - 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; - $self->chg_file($fbat, $m); - $self->close_file($fbat,undef,$self->{pool}); -} - -sub T { shift->M(@_) } - -sub change_file_prop { - my ($self, $fbat, $pname, $pval) = @_; - $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool}); -} - -sub _chg_file_get_blob ($$$$) { - my ($self, $fbat, $m, $which) = @_; - my $fh = $::_repository->temp_acquire("git_blob_$which"); - if ($m->{"mode_$which"} =~ /^120/) { - print $fh 'link ' or croak $!; - $self->change_file_prop($fbat,'svn:special','*'); - } elsif ($m->{mode_a} =~ /^120/ && $m->{"mode_$which"} !~ /^120/) { - $self->change_file_prop($fbat,'svn:special',undef); - } - my $blob = $m->{"sha1_$which"}; - return ($fh,) if ($blob =~ /^0{40}$/); - my $size = $::_repository->cat_blob($blob, $fh); - croak "Failed to read object $blob" if ($size < 0); - $fh->flush == 0 or croak $!; - seek $fh, 0, 0 or croak $!; - - my $exp = ::md5sum($fh); - seek $fh, 0, 0 or croak $!; - return ($fh, $exp); -} - -sub chg_file { - my ($self, $fbat, $m) = @_; - if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) { - $self->change_file_prop($fbat,'svn:executable','*'); - } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) { - $self->change_file_prop($fbat,'svn:executable',undef); - } - my ($fh_a, $exp_a) = _chg_file_get_blob $self, $fbat, $m, 'a'; - my ($fh_b, $exp_b) = _chg_file_get_blob $self, $fbat, $m, 'b'; - my $pool = SVN::Pool->new; - my $atd = $self->apply_textdelta($fbat, $exp_a, $pool); - if (-s $fh_a) { - my $txstream = SVN::TxDelta::new ($fh_a, $fh_b, $pool); - my $res = SVN::TxDelta::send_txstream($txstream, @$atd, $pool); - if (defined $res) { - die "Unexpected result from send_txstream: $res\n", - "(SVN::Core::VERSION: $SVN::Core::VERSION)\n"; - } - } else { - my $got = SVN::TxDelta::send_stream($fh_b, @$atd, $pool); - die "Checksum mismatch\nexpected: $exp_b\ngot: $got\n" - if ($got ne $exp_b); - } - Git::temp_release($fh_b, 1); - Git::temp_release($fh_a, 1); - $pool->clear; -} - -sub D { - my ($self, $m) = @_; - my ($dir, $file) = split_path($m->{file_b}); - my $pbat = $self->ensure_path($dir); - print "\tD\t$m->{file_b}\n" unless $::_q; - $self->delete_entry($m->{file_b}, $pbat); -} - -sub close_edit { - my ($self) = @_; - my ($p,$bat) = ($self->{pool}, $self->{bat}); - foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) { - next if $_ eq ''; - $self->close_directory($bat->{$_}, $p); - } - $self->close_directory($bat->{''}, $p); - $self->SUPER::close_edit($p); - $p->clear; -} - -sub abort_edit { - my ($self) = @_; - $self->SUPER::abort_edit($self->{pool}); -} - -sub DESTROY { - my $self = shift; - $self->SUPER::DESTROY(@_); - $self->{pool}->clear; -} - -# this drives the editor -sub apply_diff { - my ($self) = @_; - my $mods = $self->{mods}; - my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 ); - foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) { - my $f = $m->{chg}; - if (defined $o{$f}) { - $self->$f($m); - } else { - fatal("Invalid change type: $f"); - } - } - $self->rmdirs if $_rmdir; - if (@$mods == 0) { - $self->abort_edit; - } else { - $self->close_edit; - } - return scalar @$mods; -} - -package Git::SVN::Ra; -use vars qw/@ISA $config_dir $_log_window_size/; -use strict; -use warnings; -my ($ra_invalid, $can_do_switch, %ignored_err, $RA); - -BEGIN { - # enforce temporary pool usage for some simple functions - no strict 'refs'; - for my $f (qw/rev_proplist get_latest_revnum get_uuid get_repos_root - get_file/) { - my $SUPER = "SUPER::$f"; - *$f = sub { - my $self = shift; - my $pool = SVN::Pool->new; - my @ret = $self->$SUPER(@_,$pool); - $pool->clear; - wantarray ? @ret : $ret[0]; - }; - } -} - -sub _auth_providers () { - [ - SVN::Client::get_simple_provider(), - SVN::Client::get_ssl_server_trust_file_provider(), - SVN::Client::get_simple_prompt_provider( - \&Git::SVN::Prompt::simple, 2), - SVN::Client::get_ssl_client_cert_file_provider(), - SVN::Client::get_ssl_client_cert_prompt_provider( - \&Git::SVN::Prompt::ssl_client_cert, 2), - SVN::Client::get_ssl_client_cert_pw_file_provider(), - SVN::Client::get_ssl_client_cert_pw_prompt_provider( - \&Git::SVN::Prompt::ssl_client_cert_pw, 2), - SVN::Client::get_username_provider(), - SVN::Client::get_ssl_server_trust_prompt_provider( - \&Git::SVN::Prompt::ssl_server_trust), - SVN::Client::get_username_prompt_provider( - \&Git::SVN::Prompt::username, 2) - ] -} - -sub escape_uri_only { - my ($uri) = @_; - my @tmp; - foreach (split m{/}, $uri) { - s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg; - push @tmp, $_; - } - join('/', @tmp); -} - -sub escape_url { - my ($url) = @_; - if ($url =~ m#^(https?)://([^/]+)(.*)$#) { - my ($scheme, $domain, $uri) = ($1, $2, escape_uri_only($3)); - $url = "$scheme://$domain$uri"; - } - $url; -} - -sub new { - my ($class, $url) = @_; - $url =~ s!/+$!!; - return $RA if ($RA && $RA->{url} eq $url); - - SVN::_Core::svn_config_ensure($config_dir, undef); - my ($baton, $callbacks) = SVN::Core::auth_open_helper(_auth_providers); - my $config = SVN::Core::config_get_config($config_dir); - $RA = undef; - my $dont_store_passwords = 1; - my $conf_t = ${$config}{'config'}; - { - no warnings 'once'; - # The usage of $SVN::_Core::SVN_CONFIG_* variables - # produces warnings that variables are used only once. - # I had not found the better way to shut them up, so - # the warnings of type 'once' are disabled in this block. - if (SVN::_Core::svn_config_get_bool($conf_t, - $SVN::_Core::SVN_CONFIG_SECTION_AUTH, - $SVN::_Core::SVN_CONFIG_OPTION_STORE_PASSWORDS, - 1) == 0) { - SVN::_Core::svn_auth_set_parameter($baton, - $SVN::_Core::SVN_AUTH_PARAM_DONT_STORE_PASSWORDS, - bless (\$dont_store_passwords, "_p_void")); - } - if (SVN::_Core::svn_config_get_bool($conf_t, - $SVN::_Core::SVN_CONFIG_SECTION_AUTH, - $SVN::_Core::SVN_CONFIG_OPTION_STORE_AUTH_CREDS, - 1) == 0) { - $Git::SVN::Prompt::_no_auth_cache = 1; - } - } # no warnings 'once' - my $self = SVN::Ra->new(url => escape_url($url), auth => $baton, - config => $config, - pool => SVN::Pool->new, - auth_provider_callbacks => $callbacks); - $self->{url} = $url; - $self->{svn_path} = $url; - $self->{repos_root} = $self->get_repos_root; - $self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##; - $self->{cache} = { check_path => { r => 0, data => {} }, - get_dir => { r => 0, data => {} } }; - $RA = bless $self, $class; -} - -sub check_path { - my ($self, $path, $r) = @_; - my $cache = $self->{cache}->{check_path}; - if ($r == $cache->{r} && exists $cache->{data}->{$path}) { - return $cache->{data}->{$path}; - } - my $pool = SVN::Pool->new; - my $t = $self->SUPER::check_path($path, $r, $pool); - $pool->clear; - if ($r != $cache->{r}) { - %{$cache->{data}} = (); - $cache->{r} = $r; - } - $cache->{data}->{$path} = $t; -} - -sub get_dir { - my ($self, $dir, $r) = @_; - my $cache = $self->{cache}->{get_dir}; - if ($r == $cache->{r}) { - if (my $x = $cache->{data}->{$dir}) { - return wantarray ? @$x : $x->[0]; - } - } - my $pool = SVN::Pool->new; - my ($d, undef, $props) = $self->SUPER::get_dir($dir, $r, $pool); - my %dirents = map { $_ => { kind => $d->{$_}->kind } } keys %$d; - $pool->clear; - if ($r != $cache->{r}) { - %{$cache->{data}} = (); - $cache->{r} = $r; - } - $cache->{data}->{$dir} = [ \%dirents, $r, $props ]; - wantarray ? (\%dirents, $r, $props) : \%dirents; -} - -sub DESTROY { - # do not call the real DESTROY since we store ourselves in $RA -} - -# get_log(paths, start, end, limit, -# discover_changed_paths, strict_node_history, receiver) -sub get_log { - my ($self, @args) = @_; - my $pool = SVN::Pool->new; - - # svn_log_changed_path_t objects passed to get_log are likely to be - # overwritten even if only the refs are copied to an external variable, - # so we should dup the structures in their entirety. Using an - # externally passed pool (instead of our temporary and quickly cleared - # pool in Git::SVN::Ra) does not help matters at all... - my $receiver = pop @args; - my $prefix = "/".$self->{svn_path}; - $prefix =~ s#/+($)##; - my $prefix_regex = qr#^\Q$prefix\E#; - push(@args, sub { - my ($paths) = $_[0]; - return &$receiver(@_) unless $paths; - $_[0] = (); - foreach my $p (keys %$paths) { - my $i = $paths->{$p}; - # Make path relative to our url, not repos_root - $p =~ s/$prefix_regex//; - my %s = map { $_ => $i->$_; } - qw/copyfrom_path copyfrom_rev action/; - if ($s{'copyfrom_path'}) { - $s{'copyfrom_path'} =~ s/$prefix_regex//; - } - $_[0]{$p} = \%s; - } - &$receiver(@_); - }); - - - # the limit parameter was not supported in SVN 1.1.x, so we - # drop it. Therefore, the receiver callback passed to it - # is made aware of this limitation by being wrapped if - # the limit passed to is being wrapped. - if ($SVN::Core::VERSION le '1.2.0') { - my $limit = splice(@args, 3, 1); - if ($limit > 0) { - my $receiver = pop @args; - push(@args, sub { &$receiver(@_) if (--$limit >= 0) }); - } - } - my $ret = $self->SUPER::get_log(@args, $pool); - $pool->clear; - $ret; -} - -sub trees_match { - my ($self, $url1, $rev1, $url2, $rev2) = @_; - my $ctx = SVN::Client->new(auth => _auth_providers); - my $out = IO::File->new_tmpfile; - - # older SVN (1.1.x) doesn't take $pool as the last parameter for - # $ctx->diff(), so we'll create a default one - my $pool = SVN::Pool->new_default_sub; - - $ra_invalid = 1; # this will open a new SVN::Ra connection to $url1 - $ctx->diff([], $url1, $rev1, $url2, $rev2, 1, 1, 0, $out, $out); - $out->flush; - my $ret = (($out->stat)[7] == 0); - close $out or croak $!; - - $ret; -} - -sub get_commit_editor { - my ($self, $log, $cb, $pool) = @_; - my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : (); - $self->SUPER::get_commit_editor($log, $cb, @lock, $pool); -} - -sub gs_do_update { - my ($self, $rev_a, $rev_b, $gs, $editor) = @_; - 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; - my $reporter = $self->do_update($rev_b, (@pc ? shift @pc : ''), - 1, $editor, $pool); - my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : (); - - # Since we can't rely on svn_ra_reparent being available, we'll - # just have to do some magic with set_path to make it so - # we only want a partial path. - my $sp = ''; - my $final = join('/', @pc); - while (@pc) { - $reporter->set_path($sp, $rev_b, 0, @lock, $pool); - $sp .= '/' if length $sp; - $sp .= shift @pc; - } - die "BUG: '$sp' != '$final'\n" if ($sp ne $final); - - $reporter->set_path($sp, $rev_a, $new, @lock, $pool); - - $reporter->finish_report($pool); - $pool->clear; - $editor->{git_commit_ok}; -} - -# this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and -# svn_ra_reparent didn't work before 1.4) -sub gs_do_switch { - my ($self, $rev_a, $rev_b, $gs, $url_b, $editor) = @_; - my $path = $gs->{path}; - my $pool = SVN::Pool->new; - - my $full_url = $self->{url}; - my $old_url = $full_url; - $full_url .= '/' . $path if length $path; - my ($ra, $reparented); - - if ($old_url =~ m#^svn(\+ssh)?://# || - ($full_url =~ m#^https?://# && - escape_url($full_url) ne $full_url)) { - $_[0] = undef; - $self = undef; - $RA = undef; - $ra = Git::SVN::Ra->new($full_url); - $ra_invalid = 1; - } elsif ($old_url ne $full_url) { - SVN::_Ra::svn_ra_reparent($self->{session}, $full_url, $pool); - $self->{url} = $full_url; - $reparented = 1; - } - - $ra ||= $self; - $url_b = escape_url($url_b); - my $reporter = $ra->do_switch($rev_b, '', 1, $url_b, $editor, $pool); - my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : (); - $reporter->set_path('', $rev_a, 0, @lock, $pool); - $reporter->finish_report($pool); - - if ($reparented) { - SVN::_Ra::svn_ra_reparent($self->{session}, $old_url, $pool); - $self->{url} = $old_url; - } - - $pool->clear; - $editor->{git_commit_ok}; -} - -sub longest_common_path { - my ($gsv, $globs) = @_; - my %common; - my $common_max = scalar @$gsv; - - foreach my $gs (@$gsv) { - my @tmp = split m#/#, $gs->{path}; - my $p = ''; - foreach (@tmp) { - $p .= length($p) ? "/$_" : $_; - $common{$p} ||= 0; - $common{$p}++; - } - } - $globs ||= []; - $common_max += scalar @$globs; - foreach my $glob (@$globs) { - my @tmp = split m#/#, $glob->{path}->{left}; - my $p = ''; - foreach (@tmp) { - $p .= length($p) ? "/$_" : $_; - $common{$p} ||= 0; - $common{$p}++; - } - } - - my $longest_path = ''; - foreach (sort {length $b <=> length $a} keys %common) { - if ($common{$_} == $common_max) { - $longest_path = $_; - last; - } - } - $longest_path; -} - -sub gs_fetch_loop_common { - my ($self, $base, $head, $gsv, $globs) = @_; - return if ($base > $head); - 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}; - my $find_trailing_edge; - while (1) { - my %revs; - my $err; - my $err_handler = $SVN::Error::handler; - $SVN::Error::handler = sub { - ($err) = @_; - skip_unknown_revs($err); - }; - sub _cb { - my ($paths, $r, $author, $date, $log) = @_; - [ $paths, - { author => $author, date => $date, log => $log } ]; - } - $self->get_log([$longest_path], $min, $max, 0, 1, 1, - sub { $revs{$_[1]} = _cb(@_) }); - if ($err) { - print "Checked through r$max\r"; - } else { - $find_trailing_edge = 1; - } - if ($err and $find_trailing_edge) { - print STDERR "Path '$longest_path' ", - "was probably deleted:\n", - $err->expanded_message, - "\nWill attempt to follow ", - "revisions r$min .. r$max ", - "committed before the deletion\n"; - my $hi = $max; - while (--$hi >= $min) { - my $ok; - $self->get_log([$longest_path], $min, $hi, - 0, 1, 1, sub { - $ok = $_[1]; - $revs{$_[1]} = _cb(@_) }); - if ($ok) { - print STDERR "r$min .. r$ok OK\n"; - last; - } - } - $find_trailing_edge = 0; - } - $SVN::Error::handler = $err_handler; - - my %exists = map { $_->{path} => $_ } @$gsv; - foreach my $r (sort {$a <=> $b} keys %revs) { - my ($paths, $logged) = @{$revs{$r}}; - - foreach my $gs ($self->match_globs(\%exists, $paths, - $globs, $r)) { - if ($gs->rev_map_max >= $r) { - next; - } - next unless $gs->match_paths($paths, $r); - $gs->{logged_rev_props} = $logged; - if (my $last_commit = $gs->last_commit) { - $gs->assert_index_clean($last_commit); - } - my $log_entry = $gs->do_fetch($paths, $r); - if ($log_entry) { - $gs->do_git_commit($log_entry); - } - $INDEX_FILES{$gs->{index}} = 1; - } - foreach my $g (@$globs) { - my $k = "svn-remote.$g->{remote}." . - "$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 - foreach my $gs (@$gsv) { - next if $gs->rev_map_max >= $max; - next if defined $gs->rev_map_get($max); - $gs->rev_map_set($max, 0 x40); - } - foreach my $g (@$globs) { - my $k = "svn-remote.$g->{remote}.$g->{t}-maxRev"; - Git::SVN::tmp_config($k, $max); - } - last if $max >= $head; - $min = $max + 1; - $max += $inc; - $max = $head if ($max > $head); - } - Git::SVN::gc(); -} - -sub get_dir_globbed { - my ($self, $left, $depth, $r) = @_; - - my @x = eval { $self->get_dir($left, $r) }; - return unless scalar @x == 3; - my $dirents = $x[0]; - my @finalents; - foreach my $de (keys %$dirents) { - next if $dirents->{$de}->{kind} != $SVN::Node::dir; - if ($depth > 1) { - my @args = ("$left/$de", $depth - 1, $r); - foreach my $dir ($self->get_dir_globbed(@args)) { - push @finalents, "$de/$dir"; - } - } else { - push @finalents, $de; - } - } - @finalents; -} - -sub match_globs { - my ($self, $exists, $paths, $globs, $r) = @_; - - sub get_dir_check { - my ($self, $exists, $g, $r) = @_; - - my @dirs = $self->get_dir_globbed($g->{path}->{left}, - $g->{path}->{depth}, - $r); - - foreach my $de (@dirs) { - my $p = $g->{path}->full_path($de); - next if $exists->{$p}; - next if (length $g->{path}->{right} && - ($self->check_path($p, $r) != - $SVN::Node::dir)); - next unless $p =~ /$g->{path}->{regex}/; - $exists->{$p} = Git::SVN->init($self->{url}, $p, undef, - $g->{ref}->full_path($de), 1); - } - } - foreach my $g (@$globs) { - if (my $path = $paths->{"/$g->{path}->{left}"}) { - if ($path->{action} =~ /^[AR]$/) { - get_dir_check($self, $exists, $g, $r); - } - } - foreach (keys %$paths) { - if (/$g->{path}->{left_regex}/ && - !/$g->{path}->{regex}/) { - next if $paths->{$_}->{action} !~ /^[AR]$/; - get_dir_check($self, $exists, $g, $r); - } - next unless /$g->{path}->{regex}/; - my $p = $1; - my $pathname = $g->{path}->full_path($p); - next if $exists->{$pathname}; - next if ($self->check_path($pathname, $r) != - $SVN::Node::dir); - $exists->{$pathname} = Git::SVN->init( - $self->{url}, $pathname, undef, - $g->{ref}->full_path($p), 1); - } - my $c = ''; - foreach (split m#/#, $g->{path}->{left}) { - $c .= "/$_"; - next unless ($paths->{$c} && - ($paths->{$c}->{action} =~ /^[AR]$/)); - get_dir_check($self, $exists, $g, $r); - } - } - values %$exists; -} - -sub minimize_url { - my ($self) = @_; - return $self->{url} if ($self->{url} eq $self->{repos_root}); - my $url = $self->{repos_root}; - my @components = split(m!/!, $self->{svn_path}); - my $c = ''; - do { - $url .= "/$c" if length $c; - eval { - my $ra = (ref $self)->new($url); - my $latest = $ra->get_latest_revnum; - $ra->get_log("", $latest, 0, 1, 0, 1, sub {}); - }; - } while ($@ && ($c = shift @components)); - $url; -} - -sub can_do_switch { - my $self = shift; - unless (defined $can_do_switch) { - my $pool = SVN::Pool->new; - my $rep = eval { - $self->do_switch(1, '', 0, $self->{url}, - SVN::Delta::Editor->new, $pool); - }; - if ($@) { - $can_do_switch = 0; - } else { - $rep->abort_report($pool); - $can_do_switch = 1; - } - $pool->clear; - } - $can_do_switch; -} - -sub skip_unknown_revs { - my ($err) = @_; - my $errno = $err->apr_err(); - # Maybe the branch we're tracking didn't - # exist when the repo started, so it's - # not an error if it doesn't, just continue - # - # Wonderfully consistent library, eh? - # 160013 - svn:// and file:// - # 175002 - http(s):// - # 175007 - http(s):// (this repo required authorization, too...) - # More codes may be discovered later... - if ($errno == 175007 || $errno == 175002 || $errno == 160013) { - my $err_key = $err->expanded_message; - # revision numbers change every time, filter them out - $err_key =~ s/\d+/\0/g; - $err_key = "$errno\0$err_key"; - unless ($ignored_err{$err_key}) { - warn "W: Ignoring error from SVN, path probably ", - "does not exist: ($errno): ", - $err->expanded_message,"\n"; - warn "W: Do not be alarmed at the above message ", - "git-svn is just searching aggressively for ", - "old history.\n", - "This may take a while on large repositories\n"; - $ignored_err{$err_key} = 1; - } - return; - } - die "Error from SVN, ($errno): ", $err->expanded_message,"\n"; -} - 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/; @@ -5486,11 +4458,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)); } @@ -5650,7 +4619,7 @@ sub cmd_show_log { my (@k, $c, $d, $stat); my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/; while (<$log>) { - if (/^${esc_color}commit -?($::sha1_short)/o) { + if (/^${esc_color}commit (?:- )?($::sha1_short)/o) { my $cmt = $1; if ($c && cmt_showable($c) && $c->{r} != $r_last) { $r_last = $c->{r}; |