1package 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.
35use strict;
36use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : ();
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	command_oneline
48);
49use Git::SVN;
50
51sub 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
85sub 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
134sub 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
155sub 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
173sub 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;
251The following [svn-remote] sections in your config file ($file) are empty
252and can be safely removed:
253EOF
254		print STDERR "[svn-remote \"$_\"]\n" foreach @emptied;
255	}
256}
257
258sub migration_check {
259	migrate_from_v0();
260	migrate_from_v1();
261	migrate_from_v2();
262	minimize_connections() if $_minimize;
263}
264
2651;
266