+
+sub pdate($) {
+ my($d) = @_;
+ m#(\d{2,4})/(\d\d)/(\d\d)\s(\d\d):(\d\d)(?::(\d\d))?#
+ or die "Unparseable date: $d\n";
+ my $y=$1; $y-=1900 if $y>1900;
+ return timegm($6||0,$5,$4,$3,$2-1,$y);
+}
+
+sub pmode($) {
+ my($mode) = @_;
+ my $m = 0;
+ my $mm = 0;
+ my $um = 0;
+ for my $x(split(//,$mode)) {
+ if($x eq ",") {
+ $m |= $mm&$um;
+ $mm = 0;
+ $um = 0;
+ } elsif($x eq "u") { $um |= 0700;
+ } elsif($x eq "g") { $um |= 0070;
+ } elsif($x eq "o") { $um |= 0007;
+ } elsif($x eq "r") { $mm |= 0444;
+ } elsif($x eq "w") { $mm |= 0222;
+ } elsif($x eq "x") { $mm |= 0111;
+ } elsif($x eq "=") { # do nothing
+ } else { die "Unknown mode: $mode\n";
+ }
+ }
+ $m |= $mm&$um;
+ return $m;
+}
+
+sub getwd() {
+ my $pwd = `pwd`;
+ chomp $pwd;
+ return $pwd;
+}
+
+
+sub get_headref($$) {
+ my $name = shift;
+ my $git_dir = shift;
+ my $sha;
+
+ if (open(C,"$git_dir/refs/heads/$name")) {
+ chomp($sha = <C>);
+ close(C);
+ length($sha) == 40
+ or die "Cannot get head id for $name ($sha): $!\n";
+ }
+ return $sha;
+}
+
+
+-d $git_tree
+ or mkdir($git_tree,0777)
+ or die "Could not create $git_tree: $!";
+chdir($git_tree);
+
+my $last_branch = "";
+my $orig_branch = "";
+my $forward_master = 0;
+my %branch_date;
+
+my $git_dir = $ENV{"GIT_DIR"} || ".git";
+$git_dir = getwd()."/".$git_dir unless $git_dir =~ m#^/#;
+$ENV{"GIT_DIR"} = $git_dir;
+my $orig_git_index;
+$orig_git_index = $ENV{GIT_INDEX_FILE} if exists $ENV{GIT_INDEX_FILE};
+my ($git_ih, $git_index) = tempfile('gitXXXXXX', SUFFIX => '.idx',
+ DIR => File::Spec->tmpdir());
+close ($git_ih);
+$ENV{GIT_INDEX_FILE} = $git_index;
+unless(-d $git_dir) {
+ system("git-init-db");
+ die "Cannot init the GIT db at $git_tree: $?\n" if $?;
+ system("git-read-tree");
+ die "Cannot init an empty tree: $?\n" if $?;
+
+ $last_branch = $opt_o;
+ $orig_branch = "";
+} else {
+ -f "$git_dir/refs/heads/$opt_o"
+ or die "Branch '$opt_o' does not exist.\n".
+ "Either use the correct '-o branch' option,\n".
+ "or import to a new repository.\n";
+
+ $last_branch = basename(readlink("$git_dir/HEAD"));
+ unless($last_branch) {
+ warn "Cannot read the last branch name: $! -- assuming 'master'\n";
+ $last_branch = "master";
+ }
+ $orig_branch = $last_branch;
+ if (-f "$git_dir/CVS2GIT_HEAD") {
+ die <<EOM;
+CVS2GIT_HEAD exists.
+Make sure your working directory corresponds to HEAD and remove CVS2GIT_HEAD.
+You may need to run
+
+ git-read-tree -m -u CVS2GIT_HEAD HEAD
+EOM
+ }
+ system('cp', "$git_dir/HEAD", "$git_dir/CVS2GIT_HEAD");
+
+ $forward_master =
+ $opt_o ne 'master' && -f "$git_dir/refs/heads/master" &&
+ system('cmp', '-s', "$git_dir/refs/heads/master",
+ "$git_dir/refs/heads/$opt_o") == 0;
+
+ # populate index
+ system('git-read-tree', $last_branch);
+ die "read-tree failed: $?\n" if $?;
+
+ # Get the last import timestamps
+ opendir(D,"$git_dir/refs/heads");
+ while(defined(my $head = readdir(D))) {
+ next if $head =~ /^\./;
+ open(F,"$git_dir/refs/heads/$head")
+ or die "Bad head branch: $head: $!\n";
+ chomp(my $ftag = <F>);
+ close(F);
+ open(F,"git-cat-file commit $ftag |");
+ while(<F>) {
+ next unless /^author\s.*\s(\d+)\s[-+]\d{4}$/;
+ $branch_date{$head} = $1;
+ last;
+ }
+ close(F);
+ }
+ closedir(D);
+}
+
+-d $git_dir
+ or die "Could not create git subdir ($git_dir).\n";
+
+my $pid = open(CVS,"-|");
+die "Cannot fork: $!\n" unless defined $pid;
+unless($pid) {
+ my @opt;
+ @opt = split(/,/,$opt_p) if defined $opt_p;
+ unshift @opt, '-z', $opt_z if defined $opt_z;
+ exec("cvsps",@opt,"-u","-A","--cvs-direct",'--root',$opt_d,$cvs_tree);
+ die "Could not start cvsps: $!\n";
+}
+
+
+## cvsps output:
+#---------------------
+#PatchSet 314
+#Date: 1999/09/18 13:03:59
+#Author: wkoch
+#Branch: STABLE-BRANCH-1-0
+#Ancestor branch: HEAD
+#Tag: (none)
+#Log:
+# See ChangeLog: Sat Sep 18 13:03:28 CEST 1999 Werner Koch
+#Members:
+# README:1.57->1.57.2.1
+# VERSION:1.96->1.96.2.1
+#
+#---------------------
+
+my $state = 0;
+
+my($patchset,$date,$author,$branch,$ancestor,$tag,$logmsg);
+my(@old,@new);
+my $commit = sub {
+ my $pid;
+ while(@old) {
+ my @o2;
+ if(@old > 55) {
+ @o2 = splice(@old,0,50);
+ } else {
+ @o2 = @old;
+ @old = ();
+ }
+ system("git-update-cache","--force-remove","--",@o2);
+ die "Cannot remove files: $?\n" if $?;
+ }
+ while(@new) {
+ my @n2;
+ if(@new > 12) {
+ @n2 = splice(@new,0,10);
+ } else {
+ @n2 = @new;
+ @new = ();
+ }
+ system("git-update-cache","--add",
+ (map { ('--cacheinfo', @$_) } @n2));
+ die "Cannot add files: $?\n" if $?;
+ }
+
+ $pid = open(C,"-|");
+ die "Cannot fork: $!" unless defined $pid;
+ unless($pid) {
+ exec("git-write-tree");
+ die "Cannot exec git-write-tree: $!\n";
+ }
+ chomp(my $tree = <C>);
+ length($tree) == 40
+ or die "Cannot get tree id ($tree): $!\n";
+ close(C)
+ or die "Error running git-write-tree: $?\n";
+ print "Tree ID $tree\n" if $opt_v;
+
+ my $parent = "";
+ if(open(C,"$git_dir/refs/heads/$last_branch")) {
+ chomp($parent = <C>);
+ close(C);
+ length($parent) == 40
+ or die "Cannot get parent id ($parent): $!\n";
+ print "Parent ID $parent\n" if $opt_v;
+ }
+
+ my $pr = IO::Pipe->new() or die "Cannot open pipe: $!\n";
+ my $pw = IO::Pipe->new() or die "Cannot open pipe: $!\n";
+ $pid = fork();
+ die "Fork: $!\n" unless defined $pid;
+ unless($pid) {
+ $pr->writer();
+ $pw->reader();
+ dup2($pw->fileno(),0);
+ dup2($pr->fileno(),1);
+ $pr->close();
+ $pw->close();
+
+ my @par = ();
+ @par = ("-p",$parent) if $parent;
+
+ # loose detection of merges
+ # based on the commit msg
+ foreach my $rx (@mergerx) {
+ if ($logmsg =~ $rx) {
+ my $mparent = $1;
+ if ($mparent eq 'HEAD') { $mparent = $opt_o };
+ if ( -e "$git_dir/refs/heads/$mparent") {
+ $mparent = get_headref($mparent, $git_dir);
+ push @par, '-p', $mparent;
+ # printing here breaks import #
+ # # print "Merge parent branch: $mparent\n" if $opt_v;
+ }
+ }
+ }
+
+ exec("env",
+ "GIT_AUTHOR_NAME=$author",
+ "GIT_AUTHOR_EMAIL=$author",
+ "GIT_AUTHOR_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)),
+ "GIT_COMMITTER_NAME=$author",
+ "GIT_COMMITTER_EMAIL=$author",
+ "GIT_COMMITTER_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)),
+ "git-commit-tree", $tree,@par);
+ die "Cannot exec git-commit-tree: $!\n";
+ }
+ $pw->writer();
+ $pr->reader();
+
+ # compatibility with git2cvs
+ substr($logmsg,32767) = "" if length($logmsg) > 32767;
+ $logmsg =~ s/[\s\n]+\z//;
+
+ print $pw "$logmsg\n"
+ or die "Error writing to git-commit-tree: $!\n";
+ $pw->close();
+
+ print "Committed patch $patchset ($branch ".strftime("%Y-%m-%d %H:%M:%S",gmtime($date)).")\n" if $opt_v;
+ chomp(my $cid = <$pr>);
+ length($cid) == 40
+ or die "Cannot get commit id ($cid): $!\n";
+ print "Commit ID $cid\n" if $opt_v;
+ $pr->close();
+
+ waitpid($pid,0);
+ die "Error running git-commit-tree: $?\n" if $?;
+
+ open(C,">$git_dir/refs/heads/$branch")
+ or die "Cannot open branch $branch for update: $!\n";
+ print C "$cid\n"
+ or die "Cannot write branch $branch for update: $!\n";
+ close(C)
+ or die "Cannot write branch $branch for update: $!\n";
+
+ if($tag) {
+ open(C,">$git_dir/refs/tags/$tag")
+ or die "Cannot create tag $tag: $!\n";
+ print C "$cid\n"
+ or die "Cannot write tag $branch: $!\n";
+ close(C)
+ or die "Cannot write tag $branch: $!\n";
+ print "Created tag '$tag' on '$branch'\n" if $opt_v;
+ }
+};
+
+while(<CVS>) {
+ chomp;
+ if($state == 0 and /^-+$/) {
+ $state = 1;
+ } elsif($state == 0) {
+ $state = 1;
+ redo;
+ } elsif(($state==0 or $state==1) and s/^PatchSet\s+//) {
+ $patchset = 0+$_;
+ $state=2;
+ } elsif($state == 2 and s/^Date:\s+//) {
+ $date = pdate($_);
+ unless($date) {
+ print STDERR "Could not parse date: $_\n";
+ $state=0;
+ next;
+ }
+ $state=3;
+ } elsif($state == 3 and s/^Author:\s+//) {
+ s/\s+$//;
+ $author = $_;
+ $state = 4;
+ } elsif($state == 4 and s/^Branch:\s+//) {
+ s/\s+$//;
+ s/[\/]/$opt_s/g;
+ $branch = $_;
+ $state = 5;
+ } elsif($state == 5 and s/^Ancestor branch:\s+//) {
+ s/\s+$//;
+ $ancestor = $_;
+ $ancestor = $opt_o if $ancestor eq "HEAD";
+ $state = 6;
+ } elsif($state == 5) {
+ $ancestor = undef;
+ $state = 6;
+ redo;
+ } elsif($state == 6 and s/^Tag:\s+//) {
+ s/\s+$//;
+ if($_ eq "(none)") {
+ $tag = undef;
+ } else {
+ $tag = $_;
+ }
+ $state = 7;
+ } elsif($state == 7 and /^Log:/) {
+ $logmsg = "";
+ $state = 8;
+ } elsif($state == 8 and /^Members:/) {
+ $branch = $opt_o if $branch eq "HEAD";
+ if(defined $branch_date{$branch} and $branch_date{$branch} >= $date) {
+ # skip
+ print "skip patchset $patchset: $date before $branch_date{$branch}\n" if $opt_v;
+ $state = 11;
+ next;
+ }
+ if($ancestor) {
+ if(-f "$git_dir/refs/heads/$branch") {
+ print STDERR "Branch $branch already exists!\n";
+ $state=11;
+ next;
+ }
+ unless(open(H,"$git_dir/refs/heads/$ancestor")) {
+ print STDERR "Branch $ancestor does not exist!\n";
+ $state=11;
+ next;
+ }
+ chomp(my $id = <H>);
+ close(H);
+ unless(open(H,"> $git_dir/refs/heads/$branch")) {
+ print STDERR "Could not create branch $branch: $!\n";
+ $state=11;
+ next;
+ }
+ print H "$id\n"
+ or die "Could not write branch $branch: $!";
+ close(H)
+ or die "Could not write branch $branch: $!";
+ }
+ if(($ancestor || $branch) ne $last_branch) {
+ print "Switching from $last_branch to $branch\n" if $opt_v;
+ system("git-read-tree", $branch);
+ die "read-tree failed: $?\n" if $?;
+ }
+ $last_branch = $branch if $branch ne $last_branch;
+ $state = 9;
+ } elsif($state == 8) {
+ $logmsg .= "$_\n";
+ } elsif($state == 9 and /^\s+(.+?):(INITIAL|\d+(?:\.\d+)+)->(\d+(?:\.\d+)+)\s*$/) {
+# VERSION:1.96->1.96.2.1
+ my $init = ($2 eq "INITIAL");
+ my $fn = $1;
+ my $rev = $3;
+ $fn =~ s#^/+##;
+ my ($tmpname, $size) = $cvs->file($fn,$rev);
+ if($size == -1) {
+ push(@old,$fn);
+ print "Drop $fn\n" if $opt_v;
+ } else {
+ print "".($init ? "New" : "Update")." $fn: $size bytes\n" if $opt_v;
+ open my $F, '-|', "git-hash-object -w $tmpname"
+ or die "Cannot create object: $!\n";
+ my $sha = <$F>;
+ chomp $sha;
+ close $F;
+ my $mode = pmode($cvs->{'mode'});
+ push(@new,[$mode, $sha, $fn]); # may be resurrected!
+ }
+ unlink($tmpname);
+ } elsif($state == 9 and /^\s+(.+?):\d+(?:\.\d+)+->(\d+(?:\.\d+)+)\(DEAD\)\s*$/) {
+ my $fn = $1;
+ $fn =~ s#^/+##;
+ push(@old,$fn);
+ print "Delete $fn\n" if $opt_v;
+ } elsif($state == 9 and /^\s*$/) {
+ $state = 10;
+ } elsif(($state == 9 or $state == 10) and /^-+$/) {
+ &$commit();
+ $state = 1;
+ } elsif($state == 11 and /^-+$/) {
+ $state = 1;
+ } elsif(/^-+$/) { # end of unknown-line processing
+ $state = 1;
+ } elsif($state != 11) { # ignore stuff when skipping
+ print "* UNKNOWN LINE * $_\n";
+ }
+}
+&$commit() if $branch and $state != 11;
+
+unlink($git_index);
+
+if (defined $orig_git_index) {
+ $ENV{GIT_INDEX_FILE} = $orig_git_index;
+} else {
+ delete $ENV{GIT_INDEX_FILE};
+}
+
+# Now switch back to the branch we were in before all of this happened
+if($orig_branch) {
+ print "DONE\n" if $opt_v;
+ system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master")
+ if $forward_master;
+ unless ($opt_i) {
+ system('git-read-tree', '-m', '-u', 'CVS2GIT_HEAD', 'HEAD');
+ die "read-tree failed: $?\n" if $?;
+ }
+} else {
+ $orig_branch = "master";
+ print "DONE; creating $orig_branch branch\n" if $opt_v;
+ system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master")
+ unless -f "$git_dir/refs/heads/master";
+ unlink("$git_dir/HEAD");
+ symlink("refs/heads/$orig_branch","$git_dir/HEAD");
+ unless ($opt_i) {
+ system('git checkout');
+ die "checkout failed: $?\n" if $?;
+ }
+}
+unlink("$git_dir/CVS2GIT_HEAD");