cvs-fast-export でCVSリポジトリをGitリポジトリに変換するメモ

諸般の事情でCVSリポジトリのインデックスを作成する必要があるので一旦Gitリポジトリを経由するのはどうかと考えた。

... 巨大なCVSリポジトリのルーズな運用に耐えるのは困難っぽいので諦めかな。。なるべく自前のRCSパーサを書くのは避けたいんだけど。。

- https://okuoku.visualstudio.com/_git/gcc-cvs-new
    - 実際にgccのCVSリポジトリを変換してみた

# CVSリポジトリのミラー

普通は `rsync` サーバを併設していることが多いのでそれを使う。例えば gcc のリポジトリであれば、

```sh
rsync -azv --delete rsync://gcc.gnu.org/gcc-cvs gcc-cvs
```

gccは開発を既にSubversionに移行していて、更にGitへの移行も今年を目処に進行中だけど、(EDIT: 完了した https://gcc.gnu.org/git/gitweb.cgi?p=gcc.git )Subversionリポジトリに入っているCVS時代の履歴は破損しているので完全な歴史はCVSに埋まっている。 ... そもそもCVSはそこまでロバストな仕組みではないので完全な履歴は考古学者の如く手で繋ぎ合わせる必要がありそう。

CVSを現役で開発に使用しているものとしてはOpenBSDやNetBSDがある。

```sh
rsync -azv --delete rsync://anoncvs4.usa.openbsd.org/cvs/src openbsd-src
rsync -azv --delete rsync://rsync2.jp.NetBSD.org/netbsd-cvs/src netbsd-src
```

# `cvs-fast-export` のビルド

ESRがメンテナンスしている [cvs-fast-export](https://gitlab.com/esr/cvs-fast-export/) はCVSリポジトリを読み取ってgitのfast-import形式で書き出すことができる。

動作には `cvs` 、ビルドには `asciidoc` 、 `bison` 、 `flex` が必要なのでそれぞれバージョンを確認して、

```sh
$ a2x --version
a2x 8.6.10
$ bison --version
bison (GNU Bison) 3.0.4
$ flex --version
flex 2.6.4
$ cvs --version

Concurrent Versions System (CVS) 1.12.13-MirDebian-25 (client/server)
```

普通に `make` でビルドする。

```sh
make
```

普通に `make install` すると `/usr/local` に入るが、prefixは手で指定できる。今回は `/opt/cvs-fast-export/bin` にインストールした。

```sh
sudo make DESTDIR=/opt prefix=/cvs-fast-export install
```

# 変換

`cvsconvert` コマンドが提供されているので、それにモジュールを1つ渡せばカレントディレクトリにGitリポジトリが作成される。

```sh
cvsconvert -pv gcc-cvs/gcc
```

`cvsconvert` は生成されたリポジトリのブランチやタグを実際にチェックアウトして一致を確認してくれる。このため、変換の完了にはそれなりの時間が掛かる。

# CVS → Git 変換の難しさ (タイムパラドックス)

Git変換の難しさは [cvs-fast-exportのman page](http://www.catb.org/~esr/cvs-fast-export/cvs-fast-export.html) の"RCS/CVS LIMITATIONS" 節に詳しいが、そもそものセマンティクスが大きく異なるのでCVSリポジトリをGitに変換するのはあまり簡単な問題ではない。

最大の違いは、 CVSは **原則的にファイル単位でしかバージョンを管理しない** 点で、CVS → Git 変換をする際には "ファイル毎にバラバラになったコミット処理を1つに纏める" 処理が必要になる。

例えば、CVSにおけるtagは、 **複数のファイルの異なるリビジョンに同じ名前を付けている** だけで、ファイル間の関係性についてはどこにも記録されない。

- https://www.gnu.org/software/trans-coord/manual/cvs/html_node/Tags.html#Tags

```
        file1   file2   file3   file4   file5

        1.1     1.1     1.1     1.1  /--1.1*      <-*-  TAG
        1.2*-   1.2     1.2    -1.2*-
        1.3  \- 1.3*-   1.3   / 1.3
        1.4          \  1.4  /  1.4
                      \-1.5*-   1.5
                        1.6
```

(SCCSやCVSのような伝統的なVCSはファイル単位でリビジョン番号を振り、かつ、管理コストを節約するために連番ではなく枝番を使用している。

例えば、 file 1 のリビジョン `1.3` からブランチを作成した場合、あらたに採番されるブランチ番号 `.2` と ブランチ上のファイルの新規リビジョン `.1` を組み合わせて `1.2.2.1` のようなリビジョン番号が振られる。このため、ファイルのリビジョン番号は常に偶数個の番号の組合せとなる。)

例えば、 [gcc の `x86-64-branch` ブランチ](https://okuoku.visualstudio.com/gcc-cvs-new/_git/gcc-cvs-new?version=GTx86-64-branch) の内容は、

```
$ grep -r x86-64-branch: gcc-cvs/gcc/* | head
gcc-cvs/gcc/Attic/config.if,v:  x86-64-branch:1.7
gcc-cvs/gcc/COPYING,v:  x86-64-branch:1.4
gcc-cvs/gcc/COPYING.LIB,v:      x86-64-branch:1.4
gcc-cvs/gcc/ChangeLog,v:        x86-64-branch:1.467.2.10
gcc-cvs/gcc/INSTALL/README,v:   x86-64-branch:1.7
gcc-cvs/gcc/MAINTAINERS,v:      x86-64-branch:1.205.2.1
gcc-cvs/gcc/Makefile.in,v:      x86-64-branch:1.93.2.2
gcc-cvs/gcc/README,v:   x86-64-branch:1.5
gcc-cvs/gcc/boehm-gc/allchblk.c,v:      x86-64-branch:1.9
gcc-cvs/gcc/boehm-gc/doc/README.environment,v:  x86-64-branch:1.4
(後略)
```

`COPYING` ファイルであればリビジョン `1.4` 、 `ChangeLog` ファイルであればリビジョン `1.467.2.10` の内容を採用すれば良いことがわかる。

対して、git(や、Subversion)はディレクトリツリー全体をバージョン管理の対象としているため、リポジトリを変換するには "ある時刻時点でのリポジトリの状態" を **全てのファイルをスキャンした上で** 再現しなければならない。この制約があるため、cvs-fast-exportはリポジトリに含まれるコミットの情報全てを一旦メモリに読んだ上で処理する方式を取っている。

CVSは当初RCSのフロントエンドとして作られた経緯があるため、ファイルの履歴はRCSの `,v` ファイルに記録される。例えば、 `MAINTAINERS` ファイルの履歴は `MAINTAINERS,v` ファイルに記録されていて、

- https://gist.github.com/okuoku/92031d1a5544854d1cc458896c81869d

```rcs:MAINTAINERS,v
1.406
date    2005.03.30.20.00.27;    author gerald;  state Exp;
branches;
next    1.405;
```

のようにdiffの日付/時刻とauthorの情報が含まれている。このファイルには

```
grep ^date MAINTAINERS,v |wc -l
845
```

845個の変更履歴が含まれているので、これを全ファイルについて時刻順にソートして変更時刻順に並べれば良いように思えるが **そうは問屋が卸さない** 。

例えば、 `gcc/boehm-gc/Attic/Makefile,v` ファイルを見てみると、

- https://gist.github.com/okuoku/8b22752767d3250845ced25ba24cbff3#file-makefile-v-L133

```
1.2
date	99.04.07.08.01.28;	author tromey;	state dead;
branches;
next	1.1;

1.1
date	99.04.07.14.56.06;	author tromey;	state Exp;
branches
	1.1.1.1;
next	;
```

なんと `1.1` (7月14日)よりも `1.2` (7月8日)の方が古い。 **タイムパラドックスだ！** (ついでにこの日付フォーマットには2000年問題もある)

CVSは歴史的事情で時刻はクライアントの自己申告に頼っており、オペミスやその他の要因でタイムパラドックスを含むリポジトリを作成できてしまう。Subversionやより後年のVCSでは、ディレクトリツリー全体を管理する思想によってこのような問題を防いでいる。

一応、最近(2004年)のcvsでは `commitid` という複数ファイルへの編集を一意に識別するためのIDが導入されていて複数ファイルに跨がるコミットを付番できるようになっているが、現役でCVSを使っているような限られたプロジェクトくらいしか恩恵が無い。(特に、gccは2005年にはSubversionに移行してしまったため1つも無い)