summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar Linus Torvalds <torvalds@g5.osdl.org>2005-07-05 12:03:14 -0700
committerLibravatar Linus Torvalds <torvalds@g5.osdl.org>2005-07-05 12:03:14 -0700
commitbbca20accefe8329f976371d0d8c111c965cdf1a (patch)
treef53618b44d269807c776f79de0e3c5538b78cf99
parentWork around git-http-pull breakage in git-fetch-script (diff)
parentMerge with Linus' current tree (diff)
downloadtgif-bbca20accefe8329f976371d0d8c111c965cdf1a.tar.xz
Merge head 'cvs2git' of http://netz.smurf.noris.de/git/git
-rw-r--r--Documentation/cvs-migration.txt34
-rw-r--r--Documentation/git-cvsimport-script.txt75
-rw-r--r--Makefile3
-rw-r--r--cvs2git.c329
-rwxr-xr-xgit-cvsimport-script757
5 files changed, 831 insertions, 367 deletions
diff --git a/Documentation/cvs-migration.txt b/Documentation/cvs-migration.txt
index 4e2c452034..e39b82938a 100644
--- a/Documentation/cvs-migration.txt
+++ b/Documentation/cvs-migration.txt
@@ -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
diff --git a/Documentation/git-cvsimport-script.txt b/Documentation/git-cvsimport-script.txt
new file mode 100644
index 0000000000..4eb0f43246
--- /dev/null
+++ b/Documentation/git-cvsimport-script.txt
@@ -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
+
diff --git a/Makefile b/Makefile
index a833a85618..233d24afad 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/cvs2git.c b/cvs2git.c
deleted file mode 100644
index 8c50e67d52..0000000000
--- a/cvs2git.c
+++ /dev/null
@@ -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;
-}
diff --git a/git-cvsimport-script b/git-cvsimport-script
index 0ba67461da..4c0333b929 100755
--- a/git-cvsimport-script
+++ b/git-cvsimport-script
@@ -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;
+
+@ARGV <= 1 or usage();
-export CVSROOT="$1"
-export MODULE="$2"
-if [ ! "$CVSROOT" ] || [ ! "$MODULE" ] ; then
- usage
-fi
+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 ||= ".";
-cvsps -h 2>&1 | grep -q "cvsps version 2.1" >& /dev/null || {
- echo "I need cvsps version 2.1"
- exit 1
+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);
+
+
+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;
-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
+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");