package Git::SVN::Migration; # these version numbers do NOT correspond to actual version numbers # of git or 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 command_oneline ); use Git::SVN; 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#; my $info_url = command_oneline(qw(rev-parse --git-path), "$id/info/url"); next unless -f $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::SVN::svn_dir(); # 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; my $info_url = command_oneline(qw(rev-parse --git-path), "$x/info/url"); next unless -f $info_url; my $u = eval { ::file_to_s($info_url) }; next unless $u; my $dn = dirname("$svn_dir/$x"); mkpath([$dn]) unless -d $dn; if ($x eq 'svn') { # they used 'svn' as GIT_SVN_ID: mkpath(["$svn_dir/svn"]); print STDERR " - $git_dir/$x/info => ", "$svn_dir/$x/info\n"; rename "$git_dir/$x/info", "$svn_dir/$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", "$svn_dir/$x/$f"; } } else { print STDERR " - $git_dir/$x => $svn_dir/$x\n"; rename "$git_dir/$x", "$svn_dir/$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, $_; } } my $svn_dir = Git::SVN::svn_dir(); foreach (@dir) { my $x = $_; $x =~ s!^\Q$svn_dir\E/!!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, '', Git::SVN::svn_dir()); 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} || command_oneline(qw(rev-parse --git-path 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;