Merge branch 'maint'
[git/mingw.git] / perl / Git / SVN / Migration.pm
blob30daf354655f0b4cc02d08e7ba5af43133a09b95
1 package Git::SVN::Migration;
2 # these version numbers do NOT correspond to actual version numbers
3 # of git nor git-svn. They are just relative.
5 # v0 layout: .git/$id/info/url, refs/heads/$id-HEAD
7 # v1 layout: .git/$id/info/url, refs/remotes/$id
9 # v2 layout: .git/svn/$id/info/url, refs/remotes/$id
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
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)
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/;
41 our $_minimize;
42 use Git qw(
43 command
44 command_noisy
45 command_output_pipe
46 command_close_pipe
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;
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++;
76 command_close_pipe($fh, $ctx);
77 print STDERR "Done migrating from v0 layout...\n" if $migrated;
78 $migrated;
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";
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";
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";
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";
122 $migrated++;
124 command_close_pipe($fh, $ctx);
125 print STDERR "Done migrating from a git-svn v1 layout\n";
126 $migrated;
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, $_;
142 foreach (@dir) {
143 my $x = $_;
144 $x =~ s!^\Q$ENV{GIT_DIR}\E/svn/!!o;
145 read_old_urls($l_map, $x, $_);
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;
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);
162 $migrated++;
164 $migrated;
167 sub minimize_connections {
168 require Git::SVN;
169 require Git::SVN::Ra;
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);
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;
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);
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;
200 next if $@;
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 { });
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 };
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;
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}";
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}
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:
247 print STDERR "[svn-remote \"$_\"]\n" foreach @emptied;
251 sub migration_check {
252 migrate_from_v0();
253 migrate_from_v1();
254 migrate_from_v2();
255 minimize_connections() if $_minimize;