Merge branch 'km/test-mailinfo-b-failure' into maint
[git] / perl / Git / SVN / Migration.pm
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;
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;