1package Git::SVN::Migration; 2# these version numbers do NOT correspond to actual version numbers 3# of git nor git-svn. They are just relative. 4# 5# v0 layout: .git/$id/info/url, refs/heads/$id-HEAD 6# 7# v1 layout: .git/$id/info/url, refs/remotes/$id 8# 9# v2 layout: .git/svn/$id/info/url, refs/remotes/$id 10# 11# v3 layout: .git/svn/$id, refs/remotes/$id 12# - info/url may remain for backwards compatibility 13# - this is what we migrate up to this layout automatically, 14# - this will be used by git svn init on single branches 15# v3.1 layout (auto migrated): 16# - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink 17# for backwards compatibility 18# 19# v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id 20# - this is only created for newly multi-init-ed 21# repositories. Similar in spirit to the 22# --use-separate-remotes option in git-clone (now default) 23# - we do not automatically migrate to this (following 24# the example set by core git) 25# 26# v5 layout: .rev_db.$UUID => .rev_map.$UUID 27# - newer, more-efficient format that uses 24-bytes per record 28# with no filler space. 29# - use xxd -c24 < .rev_map.$UUID to view and debug 30# - This is a one-way migration, repositories updated to the 31# new format will not be able to use old git-svn without 32# rebuilding the .rev_db. Rebuilding the rev_db is not 33# possible if noMetadata or useSvmProps are set; but should 34# be no problem for users that use the (sensible) defaults. 35use strict; 36use warnings; 37use Carp qw/croak/; 38use File::Path qw/mkpath/; 39use File::Basename qw/dirname basename/; 40 41our $_minimize; 42use Git qw( 43 command 44 command_noisy 45 command_output_pipe 46 command_close_pipe 47); 48 49sub migrate_from_v0 { 50 my $git_dir = $ENV{GIT_DIR}; 51 return undef unless -d $git_dir; 52 my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/); 53 my $migrated = 0; 54 while (<$fh>) { 55 chomp; 56 my ($id, $orig_ref) = ($_, $_); 57 next unless $id =~ s#^refs/heads/(.+)-HEAD$#$1#; 58 next unless -f "$git_dir/$id/info/url"; 59 my $new_ref = "refs/remotes/$id"; 60 if (::verify_ref("$new_ref^0")) { 61 print STDERR "W: $orig_ref is probably an old ", 62 "branch used by an ancient version of ", 63 "git-svn.\n", 64 "However, $new_ref also exists.\n", 65 "We will not be able ", 66 "to use this branch until this ", 67 "ambiguity is resolved.\n"; 68 next; 69 } 70 print STDERR "Migrating from v0 layout...\n" if !$migrated; 71 print STDERR "Renaming ref: $orig_ref => $new_ref\n"; 72 command_noisy('update-ref', $new_ref, $orig_ref); 73 command_noisy('update-ref', '-d', $orig_ref, $orig_ref); 74 $migrated++; 75 } 76 command_close_pipe($fh, $ctx); 77 print STDERR "Done migrating from v0 layout...\n" if $migrated; 78 $migrated; 79} 80 81sub migrate_from_v1 { 82 my $git_dir = $ENV{GIT_DIR}; 83 my $migrated = 0; 84 return $migrated unless -d $git_dir; 85 my $svn_dir = "$git_dir/svn"; 86 87 # just in case somebody used 'svn' as their $id at some point... 88 return $migrated if -d $svn_dir && ! -f "$svn_dir/info/url"; 89 90 print STDERR "Migrating from a git-svn v1 layout...\n"; 91 mkpath([$svn_dir]); 92 print STDERR "Data from a previous version of git-svn exists, but\n\t", 93 "$svn_dir\n\t(required for this version ", 94 "($::VERSION) of git-svn) does not exist.\n"; 95 my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/); 96 while (<$fh>) { 97 my $x = $_; 98 next unless $x =~ s#^refs/remotes/##; 99 chomp $x; 100 next unless -f "$git_dir/$x/info/url"; 101 my $u = eval { ::file_to_s("$git_dir/$x/info/url") }; 102 next unless $u; 103 my $dn = dirname("$git_dir/svn/$x"); 104 mkpath([$dn]) unless -d $dn; 105 if ($x eq 'svn') { # they used 'svn' as GIT_SVN_ID: 106 mkpath(["$git_dir/svn/svn"]); 107 print STDERR " - $git_dir/$x/info => ", 108 "$git_dir/svn/$x/info\n"; 109 rename "$git_dir/$x/info", "$git_dir/svn/$x/info" or 110 croak "$!: $x"; 111 # don't worry too much about these, they probably 112 # don't exist with repos this old (save for index, 113 # and we can easily regenerate that) 114 foreach my $f (qw/unhandled.log index .rev_db/) { 115 rename "$git_dir/$x/$f", "$git_dir/svn/$x/$f"; 116 } 117 } else { 118 print STDERR " - $git_dir/$x => $git_dir/svn/$x\n"; 119 rename "$git_dir/$x", "$git_dir/svn/$x" or 120 croak "$!: $x"; 121 } 122 $migrated++; 123 } 124 command_close_pipe($fh, $ctx); 125 print STDERR "Done migrating from a git-svn v1 layout\n"; 126 $migrated; 127} 128 129sub read_old_urls { 130 my ($l_map, $pfx, $path) = @_; 131 my @dir; 132 foreach (<$path/*>) { 133 if (-r "$_/info/url") { 134 $pfx .= '/' if $pfx && $pfx !~ m!/$!; 135 my $ref_id = $pfx . basename $_; 136 my $url = ::file_to_s("$_/info/url"); 137 $l_map->{$ref_id} = $url; 138 } elsif (-d $_) { 139 push @dir, $_; 140 } 141 } 142 foreach (@dir) { 143 my $x = $_; 144 $x =~ s!^\Q$ENV{GIT_DIR}\E/svn/!!o; 145 read_old_urls($l_map, $x, $_); 146 } 147} 148 149sub migrate_from_v2 { 150 my @cfg = command(qw/config -l/); 151 return if grep /^svn-remote\..+\.url=/, @cfg; 152 my %l_map; 153 read_old_urls(\%l_map, '', "$ENV{GIT_DIR}/svn"); 154 my $migrated = 0; 155 156 require Git::SVN; 157 foreach my $ref_id (sort keys %l_map) { 158 eval { Git::SVN->init($l_map{$ref_id}, '', undef, $ref_id) }; 159 if ($@) { 160 Git::SVN->init($l_map{$ref_id}, '', $ref_id, $ref_id); 161 } 162 $migrated++; 163 } 164 $migrated; 165} 166 167sub minimize_connections { 168 require Git::SVN; 169 require Git::SVN::Ra; 170 171 my $r = Git::SVN::read_all_remotes(); 172 my $new_urls = {}; 173 my $root_repos = {}; 174 foreach my $repo_id (keys %$r) { 175 my $url = $r->{$repo_id}->{url} or next; 176 my $fetch = $r->{$repo_id}->{fetch} or next; 177 my $ra = Git::SVN::Ra->new($url); 178 179 # skip existing cases where we already connect to the root 180 if (($ra->url eq $ra->{repos_root}) || 181 ($ra->{repos_root} eq $repo_id)) { 182 $root_repos->{$ra->url} = $repo_id; 183 next; 184 } 185 186 my $root_ra = Git::SVN::Ra->new($ra->{repos_root}); 187 my $root_path = $ra->url; 188 $root_path =~ s#^\Q$ra->{repos_root}\E(/|$)##; 189 foreach my $path (keys %$fetch) { 190 my $ref_id = $fetch->{$path}; 191 my $gs = Git::SVN->new($ref_id, $repo_id, $path); 192 193 # make sure we can read when connecting to 194 # a higher level of a repository 195 my ($last_rev, undef) = $gs->last_rev_commit; 196 if (!defined $last_rev) { 197 $last_rev = eval { 198 $root_ra->get_latest_revnum; 199 }; 200 next if $@; 201 } 202 my $new = $root_path; 203 $new .= length $path ? "/$path" : ''; 204 eval { 205 $root_ra->get_log([$new], $last_rev, $last_rev, 206 0, 0, 1, sub { }); 207 }; 208 next if $@; 209 $new_urls->{$ra->{repos_root}}->{$new} = 210 { ref_id => $ref_id, 211 old_repo_id => $repo_id, 212 old_path => $path }; 213 } 214 } 215 216 my @emptied; 217 foreach my $url (keys %$new_urls) { 218 # see if we can re-use an existing [svn-remote "repo_id"] 219 # instead of creating a(n ugly) new section: 220 my $repo_id = $root_repos->{$url} || $url; 221 222 my $fetch = $new_urls->{$url}; 223 foreach my $path (keys %$fetch) { 224 my $x = $fetch->{$path}; 225 Git::SVN->init($url, $path, $repo_id, $x->{ref_id}); 226 my $pfx = "svn-remote.$x->{old_repo_id}"; 227 228 my $old_fetch = quotemeta("$x->{old_path}:". 229 "$x->{ref_id}"); 230 command_noisy(qw/config --unset/, 231 "$pfx.fetch", '^'. $old_fetch . '$'); 232 delete $r->{$x->{old_repo_id}}-> 233 {fetch}->{$x->{old_path}}; 234 if (!keys %{$r->{$x->{old_repo_id}}->{fetch}}) { 235 command_noisy(qw/config --unset/, 236 "$pfx.url"); 237 push @emptied, $x->{old_repo_id} 238 } 239 } 240 } 241 if (@emptied) { 242 my $file = $ENV{GIT_CONFIG} || "$ENV{GIT_DIR}/config"; 243 print STDERR <<EOF; 244The following [svn-remote] sections in your config file ($file) are empty 245and can be safely removed: 246EOF 247 print STDERR "[svn-remote \"$_\"]\n" foreach @emptied; 248 } 249} 250 251sub migration_check { 252 migrate_from_v0(); 253 migrate_from_v1(); 254 migrate_from_v2(); 255 minimize_connections() if $_minimize; 256} 257 2581;