This commit is contained in:
Linus Torvalds 2005-07-05 12:03:14 -07:00
commit bbca20acce
5 changed files with 831 additions and 367 deletions

View File

@ -63,18 +63,38 @@ Once you've gotten (and installed) cvsps, you may or may not want to get
any more familiar with it, but make sure it is in your path. After that,
the magic command line is
git cvsimport <cvsroot> <module>
git cvsimport -v -d <cvsroot> <module> <destination>
which will do exactly what you'd think it does: it will create a git
archive of the named CVS module. The new archive will be created in a
subdirectory named <module>.
archive of the named CVS module. The new archive will be created in the
subdirectory named <destination>; it'll be created if it doesn't exist.
Default is the local directory.
It can take some time to actually do the conversion for a large archive
since it involves checking out from CVS every revision of every file,
and the conversion script can be reasonably chatty, but on some not very
scientific tests it averaged about eight revisions per second, so a
medium-sized project should not take more than a couple of minutes. For
larger projects or remote repositories, the process may take longer.
and the conversion script is reasonably chatty unless you omit the '-v'
option, but on some not very scientific tests it averaged about twenty
revisions per second, so a medium-sized project should not take more
than a couple of minutes. For larger projects or remote repositories,
the process may take longer.
After the (initial) import is done, the CVS archive's current head
revision will be checked out -- thus, you can start adding your own
changes right away.
The import is incremental, i.e. if you call it again next month it'll
fetch any CVS updates that have been happening in the meantime. The
cut-off is date-based, so don't change the branches that were imported
from CVS.
You can merge those updates (or, in fact, a different CVS branch) into
your main branch:
cg-merge <branch>
The HEAD revision from CVS is named "origin", not "HEAD", because git
already uses "HEAD". (If you don't like 'origin', use cvsimport's
'-o' option to change it.)
Emulating CVS behaviour

View File

@ -0,0 +1,75 @@
git-cvsimport-script(1)
=======================
v0.1, July 2005
NAME
----
git-cvsimport-script - Import a CVS repository into git
SYNOPSIS
--------
'git-cvsimport-script' [ -o <branch-for-HEAD> ] [ -h ] [ -v ]
[ -d <CVSROOT> ] [ -p <options-for-cvsps> ]
[ -C <GIT_repository> ] [ <CVS_module> ]
DESCRIPTION
-----------
Imports a CVS repository into git. It will either create a new
repository, or incrementally import into an existing one.
Splitting the CVS log into patch sets is done by 'cvsps'.
At least version 2.1 is required.
OPTIONS
-------
-d <CVSROOT>::
The root of the CVS archive. May be local (a simple path) or remote;
currently, only the :local:, :ext: and :pserver: access methods
are supported.
-o <branch-for-HEAD>::
The 'HEAD' branch from CVS is imported to the 'origin' branch within
the git repository, as 'HEAD' already has a special meaning for git.
Use this option if you want to import into a different branch.
Use '-o master' for continuing an import that was initially done by
the old cvs2git tool.
-p <options-for-cvsps>::
Additional options for cvsps.
The options '-x' and '-A' are implicit and should not be used here.
If you need to pass multiple options, separate them with a comma.
-v::
Verbosity: let 'cvsimport' report what it is doing.
<CVS_module>::
The CVS module you want to import. Relative to <CVSROOT>.
-h::
Print a short usage message and exit.
OUTPUT
------
If '-v' is specified, the script reports what it is doing.
Otherwise, success is indicated the Unix way, i.e. by simply exiting with
a zero exit status.
Author
------
Written by Matthias Urlichs <smurf@smurf.noris.de>, with help from
various participants of the git-list <git@vger.kernel.org>.
Documentation
--------------
Documentation by Matthias Urlichs <smurf@smurf.noris.de>.
GIT
---
Part of the link:git.html[git] suite

View File

@ -40,7 +40,7 @@ PROG= git-update-cache git-diff-files git-init-db git-write-tree \
git-unpack-file git-export git-diff-cache git-convert-cache \
git-http-pull git-ssh-push git-ssh-pull git-rev-list git-mktag \
git-diff-helper git-tar-tree git-local-pull git-write-blob \
git-get-tar-commit-id git-apply git-stripspace git-cvs2git \
git-get-tar-commit-id git-apply git-stripspace \
git-diff-stages git-rev-parse git-patch-id git-pack-objects \
git-unpack-objects git-verify-pack git-receive-pack git-send-pack \
git-prune-packed git-fetch-pack git-upload-pack
@ -129,7 +129,6 @@ git-diff-helper: diff-helper.c
git-tar-tree: tar-tree.c
git-write-blob: write-blob.c
git-stripspace: stripspace.c
git-cvs2git: cvs2git.c
git-diff-stages: diff-stages.c
git-rev-parse: rev-parse.c
git-patch-id: patch-id.c

329
cvs2git.c
View File

@ -1,329 +0,0 @@
/*
* cvs2git
*
* Copyright (C) Linus Torvalds 2005
*/
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
static int verbose = 0;
/*
* This is a really stupid program that takes cvsps output, and
* generates a a long _shell_script_ that will create the GIT archive
* from it.
*
* You've been warned. I told you it was stupid.
*
* NOTE NOTE NOTE! In order to do branches correctly, this needs
* the fixed cvsps that has the "Ancestor branch" tag output.
* Hopefully David Mansfield will update his distribution soon
* enough (he's the one who wrote the patch, so at least we don't
* have to figt maintainer issues ;)
*
* Usage:
*
* TZ=UTC cvsps -A |
* git-cvs2git --cvsroot=[root] --module=[module] > script
*
* Creates a shell script that will generate the .git archive of
* the names CVS repository.
*
* TZ=UTC cvsps -s 1234- -A |
* git-cvs2git -u --cvsroot=[root] --module=[module] > script
*
* Creates a shell script that will update the .git archive with
* CVS changes from patchset 1234 until the last one.
*
* IMPORTANT NOTE ABOUT "cvsps"! This requires version 2.1 or better,
* and the "TZ=UTC" and the "-A" flag is required for sane results!
*/
enum state {
Header,
Log,
Members
};
static const char *cvsroot;
static const char *cvsmodule;
static char date[100];
static char author[100];
static char branch[100];
static char ancestor[100];
static char tag[100];
static char log[32768];
static int loglen = 0;
static int initial_commit = 1;
static void lookup_author(char *n, char **name, char **email)
{
/*
* FIXME!!! I'm lazy and stupid.
*
* This could be something like
*
* printf("lookup_author '%s'\n", n);
* *name = "$author_name";
* *email = "$author_email";
*
* and that would allow the script to do its own
* lookups at run-time.
*/
*name = n;
*email = n;
}
static void prepare_commit(void)
{
char *author_name, *author_email;
char *src_branch;
lookup_author(author, &author_name, &author_email);
printf("export GIT_COMMITTER_NAME=%s\n", author_name);
printf("export GIT_COMMITTER_EMAIL=%s\n", author_email);
printf("export GIT_COMMITTER_DATE='+0000 %s'\n", date);
printf("export GIT_AUTHOR_NAME=%s\n", author_name);
printf("export GIT_AUTHOR_EMAIL=%s\n", author_email);
printf("export GIT_AUTHOR_DATE='+0000 %s'\n", date);
if (initial_commit)
return;
src_branch = *ancestor ? ancestor : branch;
if (!strcmp(src_branch, "HEAD"))
src_branch = "master";
printf("ln -sf refs/heads/'%s' .git/HEAD\n", src_branch);
/*
* Even if cvsps claims an ancestor, we'll let the new
* branch name take precedence if it already exists
*/
if (*ancestor) {
src_branch = branch;
if (!strcmp(src_branch, "HEAD"))
src_branch = "master";
printf("[ -e .git/refs/heads/'%s' ] && ln -sf refs/heads/'%s' .git/HEAD\n",
src_branch, src_branch);
}
printf("git-read-tree -m HEAD || exit 1\n");
printf("git-checkout-cache -f -u -a\n");
}
static void commit(void)
{
const char *cmit_parent = initial_commit ? "" : "-p HEAD";
const char *dst_branch;
char *space;
int i;
printf("tree=$(git-write-tree)\n");
printf("cat > .cmitmsg <<EOFMSG\n");
/* Escape $ characters, and remove control characters */
for (i = 0; i < loglen; i++) {
unsigned char c = log[i];
switch (c) {
case '$':
case '\\':
case '`':
putchar('\\');
break;
case 0 ... 31:
if (c == '\n' || c == '\t')
break;
case 128 ... 159:
continue;
}
putchar(c);
}
printf("\nEOFMSG\n");
printf("commit=$(cat .cmitmsg | git-commit-tree $tree %s)\n", cmit_parent);
dst_branch = branch;
if (!strcmp(dst_branch, "HEAD"))
dst_branch = "master";
printf("echo $commit > .git/refs/heads/'%s'\n", dst_branch);
space = strchr(tag, ' ');
if (space)
*space = 0;
if (strcmp(tag, "(none)"))
printf("echo $commit > .git/refs/tags/'%s'\n", tag);
printf("echo 'Committed (to %s):' ; cat .cmitmsg; echo\n", dst_branch);
*date = 0;
*author = 0;
*branch = 0;
*ancestor = 0;
*tag = 0;
loglen = 0;
initial_commit = 0;
}
static void update_file(char *line)
{
char *name, *version;
char *dir;
while (isspace(*line))
line++;
name = line;
line = strchr(line, ':');
if (!line)
return;
*line++ = 0;
line = strchr(line, '>');
if (!line)
return;
*line++ = 0;
version = line;
line = strchr(line, '(');
if (line) { /* "(DEAD)" */
printf("git-update-cache --force-remove '%s'\n", name);
return;
}
dir = strrchr(name, '/');
if (dir)
printf("mkdir -p %.*s\n", (int)(dir - name), name);
printf("cvs -q -d %s checkout -d .git-tmp -r%s '%s/%s'\n",
cvsroot, version, cvsmodule, name);
printf("mv -f .git-tmp/%s %s\n", dir ? dir+1 : name, name);
printf("rm -rf .git-tmp\n");
printf("git-update-cache --add -- '%s'\n", name);
}
static struct hdrentry {
const char *name;
char *dest;
} hdrs[] = {
{ "Date:", date },
{ "Author:", author },
{ "Branch:", branch },
{ "Ancestor branch:", ancestor },
{ "Tag:", tag },
{ "Log:", NULL },
{ NULL, NULL }
};
int main(int argc, char **argv)
{
static char line[1000];
enum state state = Header;
int i;
for (i = 1; i < argc; i++) {
const char *arg = argv[i];
if (!memcmp(arg, "--cvsroot=", 10)) {
cvsroot = arg + 10;
continue;
}
if (!memcmp(arg, "--module=", 9)) {
cvsmodule = arg+9;
continue;
}
if (!strcmp(arg, "-v")) {
verbose = 1;
continue;
}
if (!strcmp(arg, "-u")) {
initial_commit = 0;
continue;
}
}
if (!cvsroot)
cvsroot = getenv("CVSROOT");
if (!cvsmodule || !cvsroot) {
fprintf(stderr, "I need a CVSROOT and module name\n");
exit(1);
}
if (initial_commit) {
printf("[ -d .git ] && exit 1\n");
printf("git-init-db\n");
printf("mkdir -p .git/refs/heads\n");
printf("mkdir -p .git/refs/tags\n");
printf("ln -sf refs/heads/master .git/HEAD\n");
}
while (fgets(line, sizeof(line), stdin) != NULL) {
int linelen = strlen(line);
while (linelen && isspace(line[linelen-1]))
line[--linelen] = 0;
switch (state) {
struct hdrentry *entry;
case Header:
if (verbose)
printf("# H: %s\n", line);
for (entry = hdrs ; entry->name ; entry++) {
int len = strlen(entry->name);
char *val;
if (memcmp(entry->name, line, len))
continue;
if (!entry->dest) {
state = Log;
break;
}
val = line + len;
linelen -= len;
while (isspace(*val)) {
val++;
linelen--;
}
memcpy(entry->dest, val, linelen+1);
break;
}
continue;
case Log:
if (verbose)
printf("# L: %s\n", line);
if (!strcmp(line, "Members:")) {
while (loglen && isspace(log[loglen-1]))
log[--loglen] = 0;
prepare_commit();
state = Members;
continue;
}
if (loglen + linelen + 5 > sizeof(log))
continue;
memcpy(log + loglen, line, linelen);
loglen += linelen;
log[loglen++] = '\n';
continue;
case Members:
if (verbose)
printf("# M: %s\n", line);
if (!linelen) {
commit();
state = Header;
continue;
}
update_file(line);
continue;
}
}
return 0;
}

View File

@ -1,38 +1,737 @@
#!/bin/sh
#!/usr/bin/perl -w
usage () {
echo "Usage: git cvsimport [-v] [-z fuzz] <cvsroot> <module>"
exit 1
# This tool is copyright (c) 2005, Matthias Urlichs.
# It is released under the Gnu Public License, version 2.
#
# The basic idea is to aggregate CVS check-ins into related changes.
# Fortunately, "cvsps" does that for us; all we have to do is to parse
# its output.
#
# Checking out the files is done by a single long-running CVS connection
# / server process.
#
# The head revision is on branch "origin" by default.
# You can change that with the '-o' option.
use strict;
use warnings;
use Getopt::Std;
use File::Spec;
use File::Temp qw(tempfile);
use File::Path qw(mkpath);
use File::Basename qw(basename dirname);
use Time::Local;
use IO::Socket;
use IO::Pipe;
use POSIX qw(strftime dup2);
$SIG{'PIPE'}="IGNORE";
$ENV{'TZ'}="UTC";
our($opt_h,$opt_o,$opt_v,$opt_d,$opt_p,$opt_C,$opt_z);
sub usage() {
print STDERR <<END;
Usage: ${\basename $0} # fetch/update GIT from CVS
[ -o branch-for-HEAD ] [ -h ] [ -v ] [ -d CVSROOT ]
[ -p opts-for-cvsps ] [ -C GIT_repository ] [ -z fuzz ]
[ CVS_module ]
END
exit(1);
}
CVS2GIT=""
CVSPS="--cvs-direct -x -A"
while true; do
case "$1" in
-v) CVS2GIT="$1" ;;
-z) shift; CVSPS="$CVSPS -z $1" ;;
-*) usage ;;
*) break ;;
esac
shift
done
getopts("hvo:d:p:C:z:") or usage();
usage if $opt_h;
export CVSROOT="$1"
export MODULE="$2"
if [ ! "$CVSROOT" ] || [ ! "$MODULE" ] ; then
usage
fi
@ARGV <= 1 or usage();
cvsps -h 2>&1 | grep -q "cvsps version 2.1" >& /dev/null || {
echo "I need cvsps version 2.1"
exit 1
if($opt_d) {
$ENV{"CVSROOT"} = $opt_d;
} elsif(-f 'CVS/Root') {
open my $f, '<', 'CVS/Root' or die 'Failed to open CVS/Root';
$opt_d = <$f>;
chomp $opt_d;
close $f;
$ENV{"CVSROOT"} = $opt_d;
} elsif($ENV{"CVSROOT"}) {
$opt_d = $ENV{"CVSROOT"};
} else {
die "CVSROOT needs to be set";
}
$opt_o ||= "origin";
my $git_tree = $opt_C;
$git_tree ||= ".";
my $cvs_tree;
if ($#ARGV == 0) {
$cvs_tree = $ARGV[0];
} elsif (-f 'CVS/Repository') {
open my $f, '<', 'CVS/Repository' or
die 'Failed to open CVS/Repository';
$cvs_tree = <$f>;
chomp $cvs_tree;
close $f
} else {
usage();
}
mkdir "$MODULE" || exit 1
cd "$MODULE"
select(STDERR); $|=1; select(STDOUT);
TZ=UTC cvsps $CVSPS $MODULE > .git-cvsps-result
[ -s .git-cvsps-result ] || exit 1
git-cvs2git $CVS2GIT --cvsroot="$CVSROOT" --module="$MODULE" < .git-cvsps-result > .git-create-script || exit 1
sh .git-create-script
package CVSconn;
# Basic CVS dialog.
# We're only interested in connecting and downloading, so ...
use File::Spec;
use File::Temp qw(tempfile);
use POSIX qw(strftime dup2);
sub new {
my($what,$repo,$subdir) = @_;
$what=ref($what) if ref($what);
my $self = {};
$self->{'buffer'} = "";
bless($self,$what);
$repo =~ s#/+$##;
$self->{'fullrep'} = $repo;
$self->conn();
$self->{'subdir'} = $subdir;
$self->{'lines'} = undef;
return $self;
}
sub conn {
my $self = shift;
my $repo = $self->{'fullrep'};
if($repo =~ s/^:pserver:(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?//) {
my($user,$pass,$serv,$port) = ($1,$2,$3,$4);
$user="anonymous" unless defined $user;
my $rr2 = "-";
unless($port) {
$rr2 = ":pserver:$user\@$serv:$repo";
$port=2401;
}
my $rr = ":pserver:$user\@$serv:$port$repo";
unless($pass) {
open(H,$ENV{'HOME'}."/.cvspass") and do {
# :pserver:cvs@mea.tmt.tele.fi:/cvsroot/zmailer Ah<Z
while(<H>) {
chomp;
s/^\/\d+\s+//;
my ($w,$p) = split(/\s/,$_,2);
if($w eq $rr or $w eq $rr2) {
$pass = $p;
last;
}
}
};
}
$pass="A" unless $pass;
my $s = IO::Socket::INET->new(PeerHost => $serv, PeerPort => $port);
die "Socket to $serv: $!\n" unless defined $s;
$s->write("BEGIN AUTH REQUEST\n$repo\n$user\n$pass\nEND AUTH REQUEST\n")
or die "Write to $serv: $!\n";
$s->flush();
my $rep = <$s>;
if($rep ne "I LOVE YOU\n") {
$rep="<unknown>" unless $rep;
die "AuthReply: $rep\n";
}
$self->{'socketo'} = $s;
$self->{'socketi'} = $s;
} else { # local or ext: Fork off our own cvs server.
my $pr = IO::Pipe->new();
my $pw = IO::Pipe->new();
my $pid = fork();
die "Fork: $!\n" unless defined $pid;
my $cvs = 'cvs';
$cvs = $ENV{CVS_SERVER} if exists $ENV{CVS_SERVER};
my $rsh = 'rsh';
$rsh = $ENV{CVS_RSH} if exists $ENV{CVS_RSH};
my @cvs = ($cvs, 'server');
my ($local, $user, $host);
$local = $repo =~ s/:local://;
if (!$local) {
$repo =~ s/:ext://;
$local = !($repo =~ s/^(?:([^\@:]+)\@)?([^:]+)://);
($user, $host) = ($1, $2);
}
if (!$local) {
if ($user) {
unshift @cvs, $rsh, '-l', $user, $host;
} else {
unshift @cvs, $rsh, $host;
}
}
unless($pid) {
$pr->writer();
$pw->reader();
dup2($pw->fileno(),0);
dup2($pr->fileno(),1);
$pr->close();
$pw->close();
exec(@cvs);
}
$pw->writer();
$pr->reader();
$self->{'socketo'} = $pw;
$self->{'socketi'} = $pr;
}
$self->{'socketo'}->write("Root $repo\n");
# Trial and error says that this probably is the minimum set
$self->{'socketo'}->write("Valid-responses ok error Valid-requests Mode M Mbinary E F Checked-in Created Updated Merged Removed\n");
$self->{'socketo'}->write("valid-requests\n");
$self->{'socketo'}->flush();
chomp(my $rep=$self->readline());
if($rep !~ s/^Valid-requests\s*//) {
$rep="<unknown>" unless $rep;
die "Expected Valid-requests from server, but got: $rep\n";
}
chomp(my $res=$self->readline());
die "validReply: $res\n" if $res ne "ok";
$self->{'socketo'}->write("UseUnchanged\n") if $rep =~ /\bUseUnchanged\b/;
$self->{'repo'} = $repo;
}
sub readline {
my($self) = @_;
return $self->{'socketi'}->getline();
}
sub _file {
# Request a file with a given revision.
# Trial and error says this is a good way to do it. :-/
my($self,$fn,$rev) = @_;
$self->{'socketo'}->write("Argument -N\n") or return undef;
$self->{'socketo'}->write("Argument -P\n") or return undef;
# $self->{'socketo'}->write("Argument -ko\n") or return undef;
# -ko: Linus' version doesn't use it
$self->{'socketo'}->write("Argument -r\n") or return undef;
$self->{'socketo'}->write("Argument $rev\n") or return undef;
$self->{'socketo'}->write("Argument --\n") or return undef;
$self->{'socketo'}->write("Argument $self->{'subdir'}/$fn\n") or return undef;
$self->{'socketo'}->write("Directory .\n") or return undef;
$self->{'socketo'}->write("$self->{'repo'}\n") or return undef;
# $self->{'socketo'}->write("Sticky T1.0\n") or return undef;
$self->{'socketo'}->write("co\n") or return undef;
$self->{'socketo'}->flush() or return undef;
$self->{'lines'} = 0;
return 1;
}
sub _line {
# Read a line from the server.
# ... except that 'line' may be an entire file. ;-)
my($self, $fh) = @_;
die "Not in lines" unless defined $self->{'lines'};
my $line;
my $res=0;
while(defined($line = $self->readline())) {
# M U gnupg-cvs-rep/AUTHORS
# Updated gnupg-cvs-rep/
# /daten/src/rsync/gnupg-cvs-rep/AUTHORS
# /AUTHORS/1.1///T1.1
# u=rw,g=rw,o=rw
# 0
# ok
if($line =~ s/^(?:Created|Updated) //) {
$line = $self->readline(); # path
$line = $self->readline(); # Entries line
my $mode = $self->readline(); chomp $mode;
$self->{'mode'} = $mode;
defined (my $cnt = $self->readline())
or die "EOF from server after 'Changed'\n";
chomp $cnt;
die "Duh: Filesize $cnt" if $cnt !~ /^\d+$/;
$line="";
$res=0;
while($cnt) {
my $buf;
my $num = $self->{'socketi'}->read($buf,$cnt);
die "Server: Filesize $cnt: $num: $!\n" if not defined $num or $num<=0;
print $fh $buf;
$res += $num;
$cnt -= $num;
}
} elsif($line =~ s/^ //) {
print $fh $line;
$res += length($line);
} elsif($line =~ /^M\b/) {
# output, do nothing
} elsif($line =~ /^Mbinary\b/) {
my $cnt;
die "EOF from server after 'Mbinary'" unless defined ($cnt = $self->readline());
chomp $cnt;
die "Duh: Mbinary $cnt" if $cnt !~ /^\d+$/ or $cnt<1;
$line="";
while($cnt) {
my $buf;
my $num = $self->{'socketi'}->read($buf,$cnt);
die "S: Mbinary $cnt: $num: $!\n" if not defined $num or $num<=0;
print $fh $buf;
$res += $num;
$cnt -= $num;
}
} else {
chomp $line;
if($line eq "ok") {
# print STDERR "S: ok (".length($res).")\n";
return $res;
} elsif($line =~ s/^E //) {
# print STDERR "S: $line\n";
} else {
die "Unknown: $line\n";
}
}
}
}
sub file {
my($self,$fn,$rev) = @_;
my $res;
my ($fh, $name) = tempfile('gitcvs.XXXXXX',
DIR => File::Spec->tmpdir(), UNLINK => 1);
$self->_file($fn,$rev) and $res = $self->_line($fh);
if (!defined $res) {
# retry
$self->conn();
$self->_file($fn,$rev)
or die "No file command send\n";
$res = $self->_line($fh);
die "No input: $fn $rev\n" unless defined $res;
}
return ($name, $res);
}
package main;
my $cvs = CVSconn->new($opt_d, $cvs_tree);
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;
}
-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;
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)\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+$//;
$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+(\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);
print "".($init ? "New" : "Update")." $fn: $size bytes.\n" if $opt_v;
open my $F, '-|', "git-write-blob $tmpname"
or die "Cannot create object: $!\n";
my $sha = <$F>;
chomp $sha;
close $F;
unlink($tmpname);
my $mode = pmode($cvs->{'mode'});
push(@new,[$mode, $sha, $fn]); # may be resurrected!
} elsif($state == 9 and /^\s+(\S+):\d(?:\.\d+)+->(\d(?:\.\d+)+)\(DEAD\)\s*$/) {
my $fn = $1;
$fn =~ s#^/+##;
push(@old,$fn);
} 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;
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");
system('git checkout');
die "checkout failed: $?\n" if $?;
}
unlink("$git_dir/CVS2GIT_HEAD");