08645f7ebb6f2cf1e4825d1be0eaac14e23d4006
   1#!/usr/bin/perl -w
   2
   3# This tool is copyright (c) 2005, Matthias Urlichs.
   4# It is released under the Gnu Public License, version 2.
   5#
   6# The basic idea is to pull and analyze SVN changes.
   7#
   8# Checking out the files is done by a single long-running CVS connection
   9# / server process.
  10#
  11# The head revision is on branch "origin" by default.
  12# You can change that with the '-o' option.
  13
  14require v5.8.0; # for shell-safe open("-|",LIST)
  15use strict;
  16use warnings;
  17use Getopt::Std;
  18use File::Spec;
  19use File::Temp qw(tempfile);
  20use File::Path qw(mkpath);
  21use File::Basename qw(basename dirname);
  22use Time::Local;
  23use IO::Pipe;
  24use POSIX qw(strftime dup2);
  25use IPC::Open2;
  26use SVN::Core;
  27use SVN::Ra;
  28
  29$SIG{'PIPE'}="IGNORE";
  30$ENV{'TZ'}="UTC";
  31
  32our($opt_h,$opt_o,$opt_v,$opt_u,$opt_C,$opt_i,$opt_m,$opt_M,$opt_t,$opt_T,$opt_b);
  33
  34sub usage() {
  35        print STDERR <<END;
  36Usage: ${\basename $0}     # fetch/update GIT from CVS
  37       [-o branch-for-HEAD] [-h] [-v]
  38       [-C GIT_repository] [-t tagname] [-T trunkname] [-b branchname]
  39       [-i] [-u] [-s subst] [-m] [-M regex] [SVN_URL]
  40END
  41        exit(1);
  42}
  43
  44getopts("b:C:hivmM:o:t:T:u") or usage();
  45usage if $opt_h;
  46
  47my $tag_name = $opt_t || "tags";
  48my $trunk_name = $opt_T || "trunk";
  49my $branch_name = $opt_b || "branches";
  50
  51@ARGV <= 1 or usage();
  52
  53$opt_o ||= "origin";
  54my $git_tree = $opt_C;
  55$git_tree ||= ".";
  56
  57my $cvs_tree;
  58if ($#ARGV == 0) {
  59        $cvs_tree = $ARGV[0];
  60} elsif (-f 'CVS/Repository') {
  61        open my $f, '<', 'CVS/Repository' or 
  62            die 'Failed to open CVS/Repository';
  63        $cvs_tree = <$f>;
  64        chomp $cvs_tree;
  65        close $f;
  66} else {
  67        usage();
  68}
  69
  70our @mergerx = ();
  71if ($opt_m) {
  72        @mergerx = ( qr/\W(?:from|of|merge|merging|merged) (\w+)/i );
  73}
  74if ($opt_M) {
  75        push (@mergerx, qr/$opt_M/);
  76}
  77
  78select(STDERR); $|=1; select(STDOUT);
  79
  80
  81package SVNconn;
  82# Basic SVN connection.
  83# We're only interested in connecting and downloading, so ...
  84
  85use File::Spec;
  86use File::Temp qw(tempfile);
  87use POSIX qw(strftime dup2);
  88
  89sub new {
  90        my($what,$repo) = @_;
  91        $what=ref($what) if ref($what);
  92
  93        my $self = {};
  94        $self->{'buffer'} = "";
  95        bless($self,$what);
  96
  97        $repo =~ s#/+$##;
  98        $self->{'fullrep'} = $repo;
  99        $self->conn();
 100
 101        $self->{'lines'} = undef;
 102
 103        return $self;
 104}
 105
 106sub conn {
 107        my $self = shift;
 108        my $repo = $self->{'fullrep'};
 109        my $s = SVN::Ra->new($repo);
 110
 111        die "SVN connection to $repo: $!\n" unless defined $s;
 112        $self->{'svn'} = $s;
 113        $self->{'repo'} = $repo;
 114        $self->{'maxrev'} = $s->get_latest_revnum();
 115}
 116
 117sub file {
 118        my($self,$path,$rev) = @_;
 119        my $res;
 120
 121        my ($fh, $name) = tempfile('gitsvn.XXXXXX', 
 122                    DIR => File::Spec->tmpdir(), UNLINK => 1);
 123
 124        $self->{'svn'}->get_file($path,$rev,$fh) or do {
 125            # retry
 126            $self->conn();
 127                $self->{'svn'}->get_file($path,$rev,$fh)
 128                    or die "$rev: No file $path at $rev\n";
 129        };
 130        close ($fh);
 131
 132        return ($name, $res);
 133}
 134
 135
 136package main;
 137
 138my $svn = SVNconn->new($cvs_tree);
 139
 140
 141sub pdate($) {
 142        my($d) = @_;
 143        $d =~ m#(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)#
 144                or die "Unparseable date: $d\n";
 145        my $y=$1; $y-=1900 if $y>1900;
 146        return timegm($6||0,$5,$4,$3,$2-1,$y);
 147}
 148
 149sub pmode($) {
 150        my($mode) = @_;
 151        my $m = 0;
 152        my $mm = 0;
 153        my $um = 0;
 154        for my $x(split(//,$mode)) {
 155                if($x eq ",") {
 156                        $m |= $mm&$um;
 157                        $mm = 0;
 158                        $um = 0;
 159                } elsif($x eq "u") { $um |= 0700;
 160                } elsif($x eq "g") { $um |= 0070;
 161                } elsif($x eq "o") { $um |= 0007;
 162                } elsif($x eq "r") { $mm |= 0444;
 163                } elsif($x eq "w") { $mm |= 0222;
 164                } elsif($x eq "x") { $mm |= 0111;
 165                } elsif($x eq "=") { # do nothing
 166                } else { die "Unknown mode: $mode\n";
 167                }
 168        }
 169        $m |= $mm&$um;
 170        return $m;
 171}
 172
 173sub getwd() {
 174        my $pwd = `pwd`;
 175        chomp $pwd;
 176        return $pwd;
 177}
 178
 179
 180sub get_headref($$) {
 181    my $name    = shift;
 182    my $git_dir = shift; 
 183    my $sha;
 184    
 185    if (open(C,"$git_dir/refs/heads/$name")) {
 186        chomp($sha = <C>);
 187        close(C);
 188        length($sha) == 40
 189            or die "Cannot get head id for $name ($sha): $!\n";
 190    }
 191    return $sha;
 192}
 193
 194
 195-d $git_tree
 196        or mkdir($git_tree,0777)
 197        or die "Could not create $git_tree: $!";
 198chdir($git_tree);
 199
 200my $orig_branch = "";
 201my $forward_master = 0;
 202my %branches;
 203
 204my $git_dir = $ENV{"GIT_DIR"} || ".git";
 205$git_dir = getwd()."/".$git_dir unless $git_dir =~ m#^/#;
 206$ENV{"GIT_DIR"} = $git_dir;
 207my $orig_git_index;
 208$orig_git_index = $ENV{GIT_INDEX_FILE} if exists $ENV{GIT_INDEX_FILE};
 209my ($git_ih, $git_index) = tempfile('gitXXXXXX', SUFFIX => '.idx',
 210                                    DIR => File::Spec->tmpdir());
 211close ($git_ih);
 212$ENV{GIT_INDEX_FILE} = $git_index;
 213my $maxnum = 0;
 214my $last_rev = "";
 215my $last_branch;
 216my $current_rev = 0;
 217unless(-d $git_dir) {
 218        system("git-init-db");
 219        die "Cannot init the GIT db at $git_tree: $?\n" if $?;
 220        system("git-read-tree");
 221        die "Cannot init an empty tree: $?\n" if $?;
 222
 223        $last_branch = $opt_o;
 224        $orig_branch = "";
 225} else {
 226        -f "$git_dir/refs/heads/$opt_o"
 227                or die "Branch '$opt_o' does not exist.\n".
 228                       "Either use the correct '-o branch' option,\n".
 229                       "or import to a new repository.\n";
 230
 231        -f "$git_dir/svn2git"
 232                or die "'$git_dir/svn2git' does not exist.\n".
 233                       "You need that file for incremental imports.\n";
 234        $last_branch = basename(readlink("$git_dir/HEAD"));
 235        unless($last_branch) {
 236                warn "Cannot read the last branch name: $! -- assuming 'master'\n";
 237                $last_branch = "master";
 238        }
 239        $orig_branch = $last_branch;
 240        $last_rev = get_headref($orig_branch, $git_dir);
 241        if (-f "$git_dir/SVN2GIT_HEAD") {
 242                die <<EOM;
 243SVN2GIT_HEAD exists.
 244Make sure your working directory corresponds to HEAD and remove SVN2GIT_HEAD.
 245You may need to run
 246
 247    git-read-tree -m -u SVN2GIT_HEAD HEAD
 248EOM
 249        }
 250        system('cp', "$git_dir/HEAD", "$git_dir/SVN2GIT_HEAD");
 251
 252        $forward_master =
 253            $opt_o ne 'master' && -f "$git_dir/refs/heads/master" &&
 254            system('cmp', '-s', "$git_dir/refs/heads/master", 
 255                                "$git_dir/refs/heads/$opt_o") == 0;
 256
 257        # populate index
 258        system('git-read-tree', $last_rev);
 259        die "read-tree failed: $?\n" if $?;
 260
 261        # Get the last import timestamps
 262        open my $B,"<", "$git_dir/svn2git";
 263        while(<$B>) {
 264                chomp;
 265                my($num,$branch,$ref) = split;
 266                $branches{$branch}{$num} = $ref;
 267                $branches{$branch}{"LAST"} = $ref;
 268                $current_rev = $num+1 if $current_rev < $num+1;
 269        }
 270        close($B);
 271}
 272-d $git_dir
 273        or die "Could not create git subdir ($git_dir).\n";
 274
 275open BRANCHES,">>", "$git_dir/svn2git";
 276
 277
 278## cvsps output:
 279#---------------------
 280#PatchSet 314
 281#Date: 1999/09/18 13:03:59
 282#Author: wkoch
 283#Branch: STABLE-BRANCH-1-0
 284#Ancestor branch: HEAD
 285#Tag: (none)
 286#Log:
 287#    See ChangeLog: Sat Sep 18 13:03:28 CEST 1999  Werner Koch
 288#Members:
 289#       README:1.57->1.57.2.1
 290#       VERSION:1.96->1.96.2.1
 291#
 292#---------------------
 293
 294my $state = 0;
 295
 296sub get_file($$$) {
 297        my($rev,$branch,$path) = @_;
 298
 299        # revert split_path(), below
 300        my $svnpath;
 301        $path = "" if $path eq "/"; # this should not happen, but ...
 302        if($branch eq "/") {
 303                $svnpath = "/$trunk_name/$path";
 304        } elsif($branch =~ m#^/#) {
 305                $svnpath = "/$tag_name$branch/$path";
 306        } else {
 307                $svnpath = "/$branch_name/$branch/$path";
 308        }
 309
 310        # now get it
 311        my ($name, $res) = $svn->file($svnpath,$rev);
 312
 313        open my $F, '-|', "git-hash-object -w $name"
 314                or die "Cannot create object: $!\n";
 315        my $sha = <$F>;
 316        chomp $sha;
 317        close $F;
 318        # my $mode = pmode($cvs->{'mode'});
 319        my $mode = "0644"; # SV does not seem to store any file modes
 320        return [$mode, $sha, $path];
 321}
 322
 323sub split_path($$) {
 324        my($rev,$path) = @_;
 325        my $branch;
 326
 327        if($path =~ s#^/\Q$tag_name\E/([^/]+)/?##) {
 328                $branch = "/$1";
 329        } elsif($path =~ s#^/\Q$trunk_name\E/?##) {
 330                $branch = "/";
 331        } elsif($path =~ s#^/\Q$branch_name\E/([^/]+)/?##) {
 332                $branch = $1;
 333        } else {
 334                print STDERR "$rev: Unrecognized path: $path\n";
 335                return ()
 336        }
 337        $path = "/" if $path eq "";
 338        return ($branch,$path);
 339}
 340
 341sub commit {
 342        my($branch, $changed_paths, $revision, $author, $date, $message) = @_;
 343        my($author_name,$author_email,$dest);
 344        my(@old,@new);
 345
 346        if ($author =~ /^(.*?)\s+<(.*)>$/) {
 347                ($author_name, $author_email) = ($1, $2);
 348        } else {
 349                $author =~ s/^<(.*)>$/$1/;
 350                $author_name = $author_email = $author;
 351        }
 352        $date = pdate($date);
 353
 354        my $tag;
 355        my $parent;
 356        if($branch eq "/") { # trunk
 357                $parent = $opt_o;
 358        } elsif($branch =~ m#^/(.+)#) { # tag
 359                $tag = 1;
 360                $parent = $1;
 361        } else { # "normal" branch
 362                # nothing to do
 363                $parent = $branch;
 364        }
 365        $dest = $parent;
 366
 367        my $prev = $changed_paths->{"/"};
 368        if($prev and $prev->action eq "A") {
 369                delete $changed_paths->{"/"};
 370                my $oldpath = $prev->copyfrom_path;
 371                my $rev;
 372                if(defined $oldpath) {
 373                        my $p;
 374                        ($parent,$p) = split_path($revision,$oldpath);
 375                        if($parent eq "/") {
 376                                $parent = $opt_o;
 377                        } else {
 378                                $parent =~ s#^/##; # if it's a tag
 379                        }
 380                } else {
 381                        $parent = undef;
 382                }
 383        }
 384
 385        my $rev;
 386        if(defined $parent) {
 387                open(H,"git-rev-parse --verify $parent |");
 388                $rev = <H>;
 389                close(H) or do {
 390                        print STDERR "$revision: cannot find commit '$parent'!\n";
 391                        return;
 392                };
 393                chop $rev;
 394                if(length($rev) != 40) {
 395                        print STDERR "$revision: cannot find commit '$parent'!\n";
 396                        return;
 397                }
 398                $rev = $branches{($parent eq $opt_o) ? "/" : $parent}{"LAST"};
 399                if($revision != 1 and not $rev) {
 400                        print STDERR "$revision: do not know ancestor for '$parent'!\n";
 401                        return;
 402                }
 403        } else {
 404                $rev = undef;
 405        }
 406
 407#       if($prev and $prev->action eq "A") {
 408#               if(not $tag) {
 409#                       unless(open(H,"> $git_dir/refs/heads/$branch")) {
 410#                               print STDERR "$revision: Could not create branch $branch: $!\n";
 411#                               $state=11;
 412#                               next;
 413#                       }
 414#                       print H "$rev\n"
 415#                               or die "Could not write branch $branch: $!";
 416#                       close(H)
 417#                               or die "Could not write branch $branch: $!";
 418#               }
 419#       }
 420        if(not defined $rev) {
 421                unlink($git_index);
 422        } elsif ($rev ne $last_rev) {
 423                print "Switching from $last_rev to $rev ($branch)\n" if $opt_v;
 424                system("git-read-tree", $rev);
 425                die "read-tree failed for $rev: $?\n" if $?;
 426                $last_rev = $rev;
 427        }
 428
 429        while(my($path,$action) = each %$changed_paths) {
 430                if ($action->action eq "A") {
 431                        my $f = get_file($revision,$branch,$path);
 432                        push(@new,$f) if $f;
 433                } elsif ($action->action eq "D") {
 434                        push(@old,$path);
 435                } elsif ($action->action eq "M") {
 436                        my $f = get_file($revision,$branch,$path);
 437                        push(@new,$f) if $f;
 438                } elsif ($action->action eq "R") {
 439                        # refer to a file/tree in an earlier commit
 440                        push(@old,$path); # remove any old stuff
 441
 442                        # ... and add any new stuff
 443                        my($b,$p) = split_path($revision,$action->oldpath);
 444                        open my $F,"-|","git-ls-tree","-r","-z", $branches{$b}{$action->oldrev}, $p;
 445                        local $/ = '\0';
 446                        while(<$F>) {
 447                                chomp;
 448                                my($m,$p) = split(/\t/,$_,2);
 449                                my($mode,$type,$sha1) = split(/ /,$m);
 450                                next if $type ne "blob";
 451                                push(@new,[$mode,$sha1,$p]);
 452                        }
 453                } else {
 454                        die "$revision: unknown action '".$action->action."' for $path\n";
 455                }
 456        }
 457
 458        if(@old) {
 459                open F, "-│", "git-ls-files", "-z", @old or die $!;
 460                @old = ();
 461                local $/ = '\0';
 462                while(<F>) {
 463                        chomp;
 464                        push(@old,$_);
 465                }
 466                close(F);
 467
 468                while(@old) {
 469                        my @o2;
 470                        if(@old > 55) {
 471                                @o2 = splice(@old,0,50);
 472                        } else {
 473                                @o2 = @old;
 474                                @old = ();
 475                        }
 476                        system("git-update-index","--force-remove","--",@o2);
 477                        die "Cannot remove files: $?\n" if $?;
 478                }
 479        }
 480        while(@new) {
 481                my @n2;
 482                if(@new > 12) {
 483                        @n2 = splice(@new,0,10);
 484                } else {
 485                        @n2 = @new;
 486                        @new = ();
 487                }
 488                system("git-update-index","--add",
 489                        (map { ('--cacheinfo', @$_) } @n2));
 490                die "Cannot add files: $?\n" if $?;
 491        }
 492
 493        my $pid = open(C,"-|");
 494        die "Cannot fork: $!" unless defined $pid;
 495        unless($pid) {
 496                exec("git-write-tree");
 497                die "Cannot exec git-write-tree: $!\n";
 498        }
 499        chomp(my $tree = <C>);
 500        length($tree) == 40
 501                or die "Cannot get tree id ($tree): $!\n";
 502        close(C)
 503                or die "Error running git-write-tree: $?\n";
 504        print "Tree ID $tree\n" if $opt_v;
 505
 506        my $pr = IO::Pipe->new() or die "Cannot open pipe: $!\n";
 507        my $pw = IO::Pipe->new() or die "Cannot open pipe: $!\n";
 508        $pid = fork();
 509        die "Fork: $!\n" unless defined $pid;
 510        unless($pid) {
 511                $pr->writer();
 512                $pw->reader();
 513                open(OUT,">&STDOUT");
 514                dup2($pw->fileno(),0);
 515                dup2($pr->fileno(),1);
 516                $pr->close();
 517                $pw->close();
 518
 519                my @par = ();
 520                @par = ("-p",$rev) if defined $rev;
 521
 522                # loose detection of merges
 523                # based on the commit msg
 524                foreach my $rx (@mergerx) {
 525                        if ($message =~ $rx) {
 526                                my $mparent = $1;
 527                                if ($mparent eq 'HEAD') { $mparent = $opt_o };
 528                                if ( -e "$git_dir/refs/heads/$mparent") {
 529                                        $mparent = get_headref($mparent, $git_dir);
 530                                        push @par, '-p', $mparent;
 531                                        print OUT "Merge parent branch: $mparent\n" if $opt_v;
 532                                }
 533                        } 
 534                }
 535
 536                exec("env",
 537                        "GIT_AUTHOR_NAME=$author_name",
 538                        "GIT_AUTHOR_EMAIL=$author_email",
 539                        "GIT_AUTHOR_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)),
 540                        "GIT_COMMITTER_NAME=$author_name",
 541                        "GIT_COMMITTER_EMAIL=$author_email",
 542                        "GIT_COMMITTER_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)),
 543                        "git-commit-tree", $tree,@par);
 544                die "Cannot exec git-commit-tree: $!\n";
 545        }
 546        $pw->writer();
 547        $pr->reader();
 548
 549        $message =~ s/[\s\n]+\z//;
 550
 551        print $pw "$message\n"
 552                or die "Error writing to git-commit-tree: $!\n";
 553        $pw->close();
 554
 555        print "Committed change $revision:$branch ".strftime("%Y-%m-%d %H:%M:%S",gmtime($date)).")\n" if $opt_v;
 556        chomp(my $cid = <$pr>);
 557        length($cid) == 40
 558                or die "Cannot get commit id ($cid): $!\n";
 559        print "Commit ID $cid\n" if $opt_v;
 560        $pr->close();
 561
 562        waitpid($pid,0);
 563        die "Error running git-commit-tree: $?\n" if $?;
 564
 565        if(defined $dest) {
 566                print "Writing to refs/heads/$dest\n" if $opt_v;
 567                open(C,">$git_dir/refs/heads/$dest") and 
 568                print C ("$cid\n") and
 569                close(C)
 570                        or die "Cannot write branch $dest for update: $!\n";
 571        } else {
 572                print "... no known parent\n" if $opt_v;
 573        }
 574        $branches{$branch}{"LAST"} = $cid;
 575        $branches{$branch}{$revision} = $cid;
 576        $last_rev = $cid;
 577        print BRANCHES "$revision $branch $cid\n";
 578        print "DONE: $revision $dest $cid\n" if $opt_v;
 579
 580        if($tag) {
 581                my($in, $out) = ('','');
 582                $last_rev = "-" if %$changed_paths;
 583                # the tag was 'complex', i.e. did not refer to a "real" revision
 584                
 585                $tag =~ tr/_/\./ if $opt_u;
 586
 587                my $pid = open2($in, $out, 'git-mktag');
 588                print $out ("object $cid\n".
 589                    "type commit\n".
 590                    "tag $tag\n".
 591                    "tagger $author_name <$author_email>\n") and
 592                close($out)
 593                    or die "Cannot create tag object $tag: $!\n";
 594
 595                my $tagobj = <$in>;
 596                chomp $tagobj;
 597
 598                if ( !close($in) or waitpid($pid, 0) != $pid or
 599                                $? != 0 or $tagobj !~ /^[0123456789abcdef]{40}$/ ) {
 600                        die "Cannot create tag object $tag: $!\n";
 601                }
 602                
 603
 604                open(C,">$git_dir/refs/tags/$tag")
 605                        or die "Cannot create tag $tag: $!\n";
 606                print C "$tagobj\n"
 607                        or die "Cannot write tag $tag: $!\n";
 608                close(C)
 609                        or die "Cannot write tag $tag: $!\n";
 610
 611                print "Created tag '$tag' on '$branch'\n" if $opt_v;
 612        }
 613}
 614
 615my ($changed_paths, $revision, $author, $date, $message, $pool) = @_;
 616sub _commit_all {
 617        ($changed_paths, $revision, $author, $date, $message, $pool) = @_;
 618}
 619sub commit_all {
 620        my %done;
 621        my @col;
 622        my $pref;
 623        my $branch;
 624
 625        while(my($path,$action) = each %$changed_paths) {
 626                ($branch,$path) = split_path($revision,$path);
 627                next if not defined $branch;
 628                $done{$branch}{$path} = $action;
 629        }
 630        while(($branch,$changed_paths) = each %done) {
 631                commit($branch, $changed_paths, $revision, $author, $date, $message);
 632        }
 633}
 634
 635while(++$current_rev < $svn->{'maxrev'}) {
 636        $svn->{'svn'}->get_log("/",$current_rev,$current_rev,$current_rev,1,1,\&_commit_all,"");
 637        commit_all();
 638}
 639
 640
 641unlink($git_index);
 642
 643if (defined $orig_git_index) {
 644        $ENV{GIT_INDEX_FILE} = $orig_git_index;
 645} else {
 646        delete $ENV{GIT_INDEX_FILE};
 647}
 648
 649# Now switch back to the branch we were in before all of this happened
 650if($orig_branch) {
 651        print "DONE\n" if $opt_v;
 652        system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master")
 653                if $forward_master;
 654        unless ($opt_i) {
 655                system('git-read-tree', '-m', '-u', 'SVN2GIT_HEAD', 'HEAD');
 656                die "read-tree failed: $?\n" if $?;
 657        }
 658} else {
 659        $orig_branch = "master";
 660        print "DONE; creating $orig_branch branch\n" if $opt_v;
 661        system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master")
 662                unless -f "$git_dir/refs/heads/master";
 663        unlink("$git_dir/HEAD");
 664        symlink("refs/heads/$orig_branch","$git_dir/HEAD");
 665        unless ($opt_i) {
 666                system('git checkout');
 667                die "checkout failed: $?\n" if $?;
 668        }
 669}
 670unlink("$git_dir/SVN2GIT_HEAD");
 671close(BRANCHES);