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