Sync with 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 );
48
49 sub 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
81 sub 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
129 sub 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
149 sub 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
167 sub 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;
244 The following [svn-remote] sections in your config file ($file) are empty
245 and can be safely removed:
246 EOF
247                 print STDERR "[svn-remote \"$_\"]\n" foreach @emptied;
248         }
249 }
250
251 sub migration_check {
252         migrate_from_v0();
253         migrate_from_v1();
254         migrate_from_v2();
255         minimize_connections() if $_minimize;
256 }
257
258 1;