|  | package Git::SVN::Migration; | 
|  | # these version numbers do NOT correspond to actual version numbers | 
|  | # of git nor git-svn.  They are just relative. | 
|  | # | 
|  | # v0 layout: .git/$id/info/url, refs/heads/$id-HEAD | 
|  | # | 
|  | # v1 layout: .git/$id/info/url, refs/remotes/$id | 
|  | # | 
|  | # v2 layout: .git/svn/$id/info/url, refs/remotes/$id | 
|  | # | 
|  | # v3 layout: .git/svn/$id, refs/remotes/$id | 
|  | #            - info/url may remain for backwards compatibility | 
|  | #            - this is what we migrate up to this layout automatically, | 
|  | #            - this will be used by git svn init on single branches | 
|  | # v3.1 layout (auto migrated): | 
|  | #            - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink | 
|  | #              for backwards compatibility | 
|  | # | 
|  | # v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id | 
|  | #            - this is only created for newly multi-init-ed | 
|  | #              repositories.  Similar in spirit to the | 
|  | #              --use-separate-remotes option in git-clone (now default) | 
|  | #            - we do not automatically migrate to this (following | 
|  | #              the example set by core git) | 
|  | # | 
|  | # v5 layout: .rev_db.$UUID => .rev_map.$UUID | 
|  | #            - newer, more-efficient format that uses 24-bytes per record | 
|  | #              with no filler space. | 
|  | #            - use xxd -c24 < .rev_map.$UUID to view and debug | 
|  | #            - This is a one-way migration, repositories updated to the | 
|  | #              new format will not be able to use old git-svn without | 
|  | #              rebuilding the .rev_db.  Rebuilding the rev_db is not | 
|  | #              possible if noMetadata or useSvmProps are set; but should | 
|  | #              be no problem for users that use the (sensible) defaults. | 
|  | use strict; | 
|  | use warnings; | 
|  | use Carp qw/croak/; | 
|  | use File::Path qw/mkpath/; | 
|  | use File::Basename qw/dirname basename/; | 
|  |  | 
|  | our $_minimize; | 
|  | use Git qw( | 
|  | command | 
|  | command_noisy | 
|  | command_output_pipe | 
|  | command_close_pipe | 
|  | ); | 
|  |  | 
|  | sub migrate_from_v0 { | 
|  | my $git_dir = $ENV{GIT_DIR}; | 
|  | return undef unless -d $git_dir; | 
|  | my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/); | 
|  | my $migrated = 0; | 
|  | while (<$fh>) { | 
|  | chomp; | 
|  | my ($id, $orig_ref) = ($_, $_); | 
|  | next unless $id =~ s#^refs/heads/(.+)-HEAD$#$1#; | 
|  | next unless -f "$git_dir/$id/info/url"; | 
|  | my $new_ref = "refs/remotes/$id"; | 
|  | if (::verify_ref("$new_ref^0")) { | 
|  | print STDERR "W: $orig_ref is probably an old ", | 
|  | "branch used by an ancient version of ", | 
|  | "git-svn.\n", | 
|  | "However, $new_ref also exists.\n", | 
|  | "We will not be able ", | 
|  | "to use this branch until this ", | 
|  | "ambiguity is resolved.\n"; | 
|  | next; | 
|  | } | 
|  | print STDERR "Migrating from v0 layout...\n" if !$migrated; | 
|  | print STDERR "Renaming ref: $orig_ref => $new_ref\n"; | 
|  | command_noisy('update-ref', $new_ref, $orig_ref); | 
|  | command_noisy('update-ref', '-d', $orig_ref, $orig_ref); | 
|  | $migrated++; | 
|  | } | 
|  | command_close_pipe($fh, $ctx); | 
|  | print STDERR "Done migrating from v0 layout...\n" if $migrated; | 
|  | $migrated; | 
|  | } | 
|  |  | 
|  | sub migrate_from_v1 { | 
|  | my $git_dir = $ENV{GIT_DIR}; | 
|  | my $migrated = 0; | 
|  | return $migrated unless -d $git_dir; | 
|  | my $svn_dir = "$git_dir/svn"; | 
|  |  | 
|  | # just in case somebody used 'svn' as their $id at some point... | 
|  | return $migrated if -d $svn_dir && ! -f "$svn_dir/info/url"; | 
|  |  | 
|  | print STDERR "Migrating from a git-svn v1 layout...\n"; | 
|  | mkpath([$svn_dir]); | 
|  | print STDERR "Data from a previous version of git-svn exists, but\n\t", | 
|  | "$svn_dir\n\t(required for this version ", | 
|  | "($::VERSION) of git-svn) does not exist.\n"; | 
|  | my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/); | 
|  | while (<$fh>) { | 
|  | my $x = $_; | 
|  | next unless $x =~ s#^refs/remotes/##; | 
|  | chomp $x; | 
|  | next unless -f "$git_dir/$x/info/url"; | 
|  | my $u = eval { ::file_to_s("$git_dir/$x/info/url") }; | 
|  | next unless $u; | 
|  | my $dn = dirname("$git_dir/svn/$x"); | 
|  | mkpath([$dn]) unless -d $dn; | 
|  | if ($x eq 'svn') { # they used 'svn' as GIT_SVN_ID: | 
|  | mkpath(["$git_dir/svn/svn"]); | 
|  | print STDERR " - $git_dir/$x/info => ", | 
|  | "$git_dir/svn/$x/info\n"; | 
|  | rename "$git_dir/$x/info", "$git_dir/svn/$x/info" or | 
|  | croak "$!: $x"; | 
|  | # don't worry too much about these, they probably | 
|  | # don't exist with repos this old (save for index, | 
|  | # and we can easily regenerate that) | 
|  | foreach my $f (qw/unhandled.log index .rev_db/) { | 
|  | rename "$git_dir/$x/$f", "$git_dir/svn/$x/$f"; | 
|  | } | 
|  | } else { | 
|  | print STDERR " - $git_dir/$x => $git_dir/svn/$x\n"; | 
|  | rename "$git_dir/$x", "$git_dir/svn/$x" or | 
|  | croak "$!: $x"; | 
|  | } | 
|  | $migrated++; | 
|  | } | 
|  | command_close_pipe($fh, $ctx); | 
|  | print STDERR "Done migrating from a git-svn v1 layout\n"; | 
|  | $migrated; | 
|  | } | 
|  |  | 
|  | sub read_old_urls { | 
|  | my ($l_map, $pfx, $path) = @_; | 
|  | my @dir; | 
|  | foreach (<$path/*>) { | 
|  | if (-r "$_/info/url") { | 
|  | $pfx .= '/' if $pfx && $pfx !~ m!/$!; | 
|  | my $ref_id = $pfx . basename $_; | 
|  | my $url = ::file_to_s("$_/info/url"); | 
|  | $l_map->{$ref_id} = $url; | 
|  | } elsif (-d $_) { | 
|  | push @dir, $_; | 
|  | } | 
|  | } | 
|  | foreach (@dir) { | 
|  | my $x = $_; | 
|  | $x =~ s!^\Q$ENV{GIT_DIR}\E/svn/!!o; | 
|  | read_old_urls($l_map, $x, $_); | 
|  | } | 
|  | } | 
|  |  | 
|  | sub migrate_from_v2 { | 
|  | my @cfg = command(qw/config -l/); | 
|  | return if grep /^svn-remote\..+\.url=/, @cfg; | 
|  | my %l_map; | 
|  | read_old_urls(\%l_map, '', "$ENV{GIT_DIR}/svn"); | 
|  | my $migrated = 0; | 
|  |  | 
|  | require Git::SVN; | 
|  | foreach my $ref_id (sort keys %l_map) { | 
|  | eval { Git::SVN->init($l_map{$ref_id}, '', undef, $ref_id) }; | 
|  | if ($@) { | 
|  | Git::SVN->init($l_map{$ref_id}, '', $ref_id, $ref_id); | 
|  | } | 
|  | $migrated++; | 
|  | } | 
|  | $migrated; | 
|  | } | 
|  |  | 
|  | sub minimize_connections { | 
|  | require Git::SVN; | 
|  | require Git::SVN::Ra; | 
|  |  | 
|  | my $r = Git::SVN::read_all_remotes(); | 
|  | my $new_urls = {}; | 
|  | my $root_repos = {}; | 
|  | foreach my $repo_id (keys %$r) { | 
|  | my $url = $r->{$repo_id}->{url} or next; | 
|  | my $fetch = $r->{$repo_id}->{fetch} or next; | 
|  | my $ra = Git::SVN::Ra->new($url); | 
|  |  | 
|  | # skip existing cases where we already connect to the root | 
|  | if (($ra->url eq $ra->{repos_root}) || | 
|  | ($ra->{repos_root} eq $repo_id)) { | 
|  | $root_repos->{$ra->url} = $repo_id; | 
|  | next; | 
|  | } | 
|  |  | 
|  | my $root_ra = Git::SVN::Ra->new($ra->{repos_root}); | 
|  | my $root_path = $ra->url; | 
|  | $root_path =~ s#^\Q$ra->{repos_root}\E(/|$)##; | 
|  | foreach my $path (keys %$fetch) { | 
|  | my $ref_id = $fetch->{$path}; | 
|  | my $gs = Git::SVN->new($ref_id, $repo_id, $path); | 
|  |  | 
|  | # make sure we can read when connecting to | 
|  | # a higher level of a repository | 
|  | my ($last_rev, undef) = $gs->last_rev_commit; | 
|  | if (!defined $last_rev) { | 
|  | $last_rev = eval { | 
|  | $root_ra->get_latest_revnum; | 
|  | }; | 
|  | next if $@; | 
|  | } | 
|  | my $new = $root_path; | 
|  | $new .= length $path ? "/$path" : ''; | 
|  | eval { | 
|  | $root_ra->get_log([$new], $last_rev, $last_rev, | 
|  | 0, 0, 1, sub { }); | 
|  | }; | 
|  | next if $@; | 
|  | $new_urls->{$ra->{repos_root}}->{$new} = | 
|  | { ref_id => $ref_id, | 
|  | old_repo_id => $repo_id, | 
|  | old_path => $path }; | 
|  | } | 
|  | } | 
|  |  | 
|  | my @emptied; | 
|  | foreach my $url (keys %$new_urls) { | 
|  | # see if we can re-use an existing [svn-remote "repo_id"] | 
|  | # instead of creating a(n ugly) new section: | 
|  | my $repo_id = $root_repos->{$url} || $url; | 
|  |  | 
|  | my $fetch = $new_urls->{$url}; | 
|  | foreach my $path (keys %$fetch) { | 
|  | my $x = $fetch->{$path}; | 
|  | Git::SVN->init($url, $path, $repo_id, $x->{ref_id}); | 
|  | my $pfx = "svn-remote.$x->{old_repo_id}"; | 
|  |  | 
|  | my $old_fetch = quotemeta("$x->{old_path}:". | 
|  | "$x->{ref_id}"); | 
|  | command_noisy(qw/config --unset/, | 
|  | "$pfx.fetch", '^'. $old_fetch . '$'); | 
|  | delete $r->{$x->{old_repo_id}}-> | 
|  | {fetch}->{$x->{old_path}}; | 
|  | if (!keys %{$r->{$x->{old_repo_id}}->{fetch}}) { | 
|  | command_noisy(qw/config --unset/, | 
|  | "$pfx.url"); | 
|  | push @emptied, $x->{old_repo_id} | 
|  | } | 
|  | } | 
|  | } | 
|  | if (@emptied) { | 
|  | my $file = $ENV{GIT_CONFIG} || "$ENV{GIT_DIR}/config"; | 
|  | print STDERR <<EOF; | 
|  | The following [svn-remote] sections in your config file ($file) are empty | 
|  | and can be safely removed: | 
|  | EOF | 
|  | print STDERR "[svn-remote \"$_\"]\n" foreach @emptied; | 
|  | } | 
|  | } | 
|  |  | 
|  | sub migration_check { | 
|  | migrate_from_v0(); | 
|  | migrate_from_v1(); | 
|  | migrate_from_v2(); | 
|  | minimize_connections() if $_minimize; | 
|  | } | 
|  |  | 
|  | 1; |