perl / Git / SVN / Migration.pmon commit Merge branch 'sb/status-doc-fix' (7676b86)
   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;
  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;