結論

CVS(Concurrent Versions System)とは、1台のサーバーにあるリポジトリでファイルの変更履歴を管理する、集中型のバージョン管理システムです。版番号はファイルごとに付き、複数のファイルを1回でコミットしても「まとめて1つの版」にはなりません。この点が Git・Subversion との一番大きな違いです。

この記事でわかること

  • CVS 1.12.13 で取り込み・コンフリクト・タグ・ブランチを動かした出力の原文
  • Git・Subversion との違いと、コミット時間・保存容量の実測
  • CVS から Git へ移す3つの道具を同じリポジトリで比べた結果

2026年10月29日更新|CVS 1.12.13・Subversion 1.14.3・Git 2.43.0・cvs-fast-export 1.59で実行確認

CVS では、各自が手元に「作業コピー」を取り出し(checkout)、直した内容をサーバーのリポジトリへ戻し(commit)、ほかの人の変更を取り込みます(update)。履歴はリポジトリの中に、ファイル1つにつき1つの ,v ファイルとして残ります。

flowchart TD
    R[(リポジトリ<br>ファイルごとの ,v)]
    A[田中さんの作業コピー] -->|commit| R
    R -->|checkout / update| A
    B[鈴木さんの作業コピー] -->|commit| R
    R -->|checkout / update| B

よく調べられることの答えを先にまとめます。

知りたいこと 答え 詳しい節
CVSとは(IT) 集中型のバージョン管理システム。正式名は Concurrent Versions System CVSとは
CVS の別の意味 流通ではコンビニエンスストア、医療では心血管系など。分野で変わる 場面別の意味の表
CVS の使い方(最小) init → import → checkout → commit → update の5つ 最初の5分
CVS と Git の違い CVS は集中型でファイル単位、Git は分散型でリポジトリ全体を1コミットで記録 Git・Subversionとの違い
CVS と Subversion の違い どちらも集中型。Subversion は全体の版番号・アトミックなコミット・改名に対応 Git・Subversionとの違い
Up-to-date check failed 先にほかの人がコミットした。cvs update で取り込んでからコミットし直す コンフリクト
CVS から Git へ移す cvs-fast-export(または cvs2git)で変換し、diff で照合する Gitへ移す手順
CVS は今も使われている? 新しく始める例は少ないが、OpenBSD のように今も CVS でソースを管理する例がある 今CVSに出会う場面

確認環境:Ubuntu 24.04.5 LTS(2 CPU)の上で、Ubuntu の公式パッケージ cvs 2:1.12.13+real-30build1(cvs --version の表示は 1.12.13-MirDebian-30)、subversion 1.14.3、cvsps 2.1、git-cvs 2.43.0、cvs-fast-export 1.59、rcs 5.10.1 を apt-get download で取得し、作業用のフォルダーに展開して使いました(システムにはインストールしていません)。Git は 2.43.0、計測の補助に Python 3.13.18 を使っています。リポジトリはすべて作業用フォルダーの中に作り、接続方式は :local: と :fork:(同じ機械の中でクライアントとサーバーの手順を通す方式)で確かめました。:pserver:・:ext: のサーバー設定と cvs2git は動かしておらず、CVS の公式マニュアル(Cederqvist、パッケージ同梱の cvs.txt)と cvs2svn プロジェクトの公式ドキュメントで確かめた範囲で書いています。版の日付は GNU の配布サーバー(ftp.gnu.org/non-gnu/cvs)のファイル一覧で確かめました。作者名が root と出るのは、この環境の OS のユーザー名がそのまま記録されるためです(確認日 2026年10月29日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「CVSとは?使い方・Gitとの違い・移行を実機で検証|バージョン管理」

CVSとは(場面別の意味の表とバージョン管理のCVS)

この節では、「CVS」が分野ごとに何を指すかと、IT の CVS(バージョン管理システム)の位置づけが分かります。

「CVS」は分野によって指すものが変わる略語です。プログラムやソースコードの話で出てくる CVS は、ほぼ必ずバージョン管理システムの Concurrent Versions System(シーブイエス)です。

場面 CVS が指すもの 英語の正式名 例
IT・プログラミング バージョン管理システム Concurrent Versions System cvs commit・CVSROOT
流通・小売・決済 コンビニエンスストア Convenience Store 「CVS 決済」「CVS 払込票」
医療 心血管系、または絨毛検査 Cardiovascular System/Chorionic Villus Sampling 論文・カルテの略語
米国の企業 薬局チェーン・ヘルスケア企業 CVS Pharmacy/CVS Health 株式の銘柄記号

ここから先は IT の CVS の話です。CVS は、ファイルの「いつ・誰が・何を変えたか」を記録し、前の状態に戻したり、ほかの人の変更とまとめたりするための道具です。ソースコードだけでなく、設定ファイルや文書の管理にも使われてきました。

1集中型

リポジトリは1か所(サーバー)だけ。履歴を見る・コミットするにはリポジトリにつながる必要がある。

2ファイル単位の版番号

README.txt は 1.4、main.c は 1.2 のように、ファイルごとに別々の番号が進む。

3ロックしない同時編集

同じファイルを何人でも同時に直せる。ぶつかったら update のときに統合(マージ)する。

CVS の歴史(年表)

CVS は、1ファイルずつ履歴を残す RCS という道具の上に作られました。今もリポジトリの中身は RCS の形式(,v ファイル)です。

年 できごと
1986年 Dick Grune が、RCS を使うシェルスクリプトとして公開
1989年 Brian Berliner が C 言語で書き直しを始める(今の CVS の元)
1990年 CVS 1.0 を Free Software Foundation に提出。以後、ネットワーク越しに使える版管理として広まる
2005年10月 機能追加の系列 1.12.13 を配布(配布サーバーのファイル日付)。Ubuntu・Debian の cvs パッケージはこの系列
2008年5月 安定版の系列 1.11.23 を配布。これを最後に新しい版は出ていない

2000年代に入ると、CVS の弱点(改名できない・アトミックでないコミットなど)を直した Subversion が広まり、その後、分散型の Git が主流になりました。

最初の5分:CVSを入れてリポジトリを作る最小手順

この節では、CVS を入れてから、リポジトリを作り、最初のファイルを取り込んで取り出すまでの最小の手順と出力が分かります。

CVS の最小の流れは「init で器を作る → import で取り込む → checkout で取り出す」の3コマンドです。Linux なら sudo apt install cvs(Ubuntu・Debian)のようにディストリビューションのパッケージで入ります。以下はこの確認環境で実行した出力です。

$ cvs --version | head -1
Concurrent Versions System (CVS) 1.12.13-MirDebian-30 (client/server)

$ cvs -d $W/repo init          # リポジトリを作る
$ ls $W/repo
CVSROOT
$ ls $W/repo/CVSROOT | head -6
Emptydir
checkoutlist
checkoutlist,v
commitinfo
commitinfo,v
config

次に、手元のフォルダー proj を、モジュール名 myapp としてリポジトリへ取り込みます。vendor と start は取り込み元と初回を表す名前(タグ)で、必ず指定します。

$ cd proj
$ cvs -d $W/repo import -m "初回取り込み" myapp vendor start
N myapp/main.c
N myapp/README.txt
cvs import: Importing /dev/shm/t186/w/repo/myapp/src
N myapp/src/util.c

No conflicts created by this import

$ cd ..
$ cvs -d $W/repo checkout -d tanaka myapp
cvs checkout: Updating tanaka
U tanaka/README.txt
U tanaka/main.c
cvs checkout: Updating tanaka/src
U tanaka/src/util.c

import したフォルダーそのものは作業コピーになりません。作業は checkout したフォルダー(ここでは tanaka)で行います。毎回 -d を書く代わりに、環境変数 CVSROOT にリポジトリの場所を入れておく方法もあります。

1

リポジトリを作る

cvs -d /path/repo init。中に管理用の CVSROOT フォルダーができる。

1回だけ

2

既存のファイルを取り込む

取り込みたいフォルダーで cvs import -m "説明" モジュール名 vendorタグ releaseタグ。

1回だけ

3

作業コピーを取り出す

cvs checkout モジュール名。各フォルダーに CVS という管理用フォルダーができる。

人ごとに1回

4

直してコミットする

cvs diff -u で差分を見て、cvs commit -m "説明"。

毎日

5

ほかの人の変更を取り込む

cvs update。コミットの前にも実行する習慣にする。

毎日

CVSの仕組み:作業コピーのCVSフォルダーとリポジトリの,vファイル

この節では、CVS が履歴をどこに・どんな形で持っているかを、実際のファイルの中身で確かめられます。

作業コピーの各フォルダーには「CVS」という小さな管理フォルダーがあり、リポジトリにはファイルごとの ,v ファイルがあります。Git の .git のように履歴を手元に持つわけではありません。

$ ls -a tanaka/CVS
.  ..  Entries  Repository  Root
$ cat tanaka/CVS/Root tanaka/CVS/Repository tanaka/CVS/Entries
/dev/shm/t186/w/repo
myapp
/README.txt/1.1.1.1/Fri Oct 30 23:49:19 2026//
/main.c/1.1.1.1/Fri Oct 30 23:49:19 2026//
D/src////

Root はリポジトリの場所、Repository はモジュール名、Entries は「どのファイルを、どの版で取り出したか」の一覧です。履歴の本体はリポジトリ側の README.txt,v にあります。中を開くと、最新版(head)だけが全文で、それより前の版は「1行目を消して書き足す」のような差分で入っていました。

head	1.4;
access;
symbols
	FIX_1_0:1.3.0.2
	REL_1_0:1.3
	start:1.1.1.1
	vendor:1.1.1;
locks; strict;
(中略)
1.4
log
@FIX_1_0 を幹へマージ
@
text
@Hello CVS
line2 by tanaka
hotfix
@

1.3
log
@衝突を解消
@
text
@d1 1
a1 1
Hello CVS!
d3 1
@

d1 1 は「1行目から1行消す」、a1 1 は「1行目の後に1行足す」という RCS の差分の書き方です。最新版を取り出すのが速く、前の版ほど差分を順にさかのぼって組み立てます。タグ(symbols)も、ファイルごとに「この名前はこの版」と書かれています。

作業コピー側(各自の PC)

フォルダーごとの CVS/Root・Repository・Entries

履歴は持たない。log や diff はリポジトリに問い合わせる

ブランチで取り出すと CVS/Tag ができる

リポジトリ側(サーバー)

ファイル1つにつき1つの ,v(RCS 形式)

消したファイルは Attic フォルダーへ移る

設定は CVSROOT フォルダー(modules・loginfo など)

CVSの基本コマンド一覧(早見表)

この節では、CVS の主なコマンドと、Git・Subversion で同じことをするコマンドの対応が一覧で分かります。

CVS のコマンドは cvs [全体のオプション] サブコマンド [オプション] [ファイル] の形です。多くのサブコマンドは、ファイルを省くと今のフォルダーから下を全部対象にします。

やりたいこと CVS Subversion Git
リポジトリを作るcvs initsvnadmin creategit init
既存ファイルを登録cvs importsvn importgit add . と git commit
取り出すcvs checkoutsvn checkoutgit clone
最新を取り込むcvs updatesvn updategit pull
ファイルを追加cvs addsvn addgit add
ファイルを削除cvs removesvn deletegit rm
改名無い(remove と add)svn movegit mv
変更を記録cvs commitsvn commitgit commit と git push
差分を見るcvs diff -usvn diffgit diff
状態を見るcvs status・cvs -n updatesvn statusgit status
履歴を見るcvs logsvn loggit log
行ごとの変更者cvs annotatesvn blamegit blame
タグを付けるcvs tagsvn copy(tags へ)git tag
ブランチを作るcvs tag -bsvn copy(branches へ)git branch
ブランチを統合cvs update -jsvn mergegit merge

状態の確認によく使うのが cvs -n update です。-n は「何も書き換えない」という全体のオプションで、各ファイルの状態を1文字で表示します。

記号 意味 次にすること
Uリポジトリの新しい版で更新された(する)そのまま
PU と同じ。差分(パッチ)で送られたそのまま
M手元で変更している(またはマージ済み)commit する
Aadd 済みで、まだコミットしていないcommit する
Rremove 済みで、まだコミットしていないcommit する
Cコンフリクト(衝突)した手で直して commit
?CVS が管理していないファイルadd するか、.cvsignore に書く

Git でのやりたいこと別のコマンドは、関連記事「Gitコマンド一覧|やりたいこと別の早見表と実行結果・エラー対処」で扱っています。

2人で同じファイルを直す:cvs updateとコンフリクトの直し方

この節では、CVS の「ロックしない同時編集」が実際にどう動くか、コンフリクトをどう直すかが、2つの作業コピーで再現した出力で分かります。

CVS では、先にコミットした人が勝ち、後の人のコミットは「Up-to-date check failed」で止まります。後の人は cvs update で先の変更を取り込み、ぶつかった箇所を直してからコミットします。

田中さんの作業コピー(tanaka)と鈴木さんの作業コピー(suzuki)で、同じ README.txt の1行目を別々に直しました。先に田中さんがコミットします。

$ cd tanaka
$ cvs -n update
cvs update: Updating .
M README.txt
cvs update: Updating src
$ cvs diff -u README.txt
Index: README.txt
===================================================================
RCS file: /dev/shm/t186/w/repo/myapp/README.txt,v
retrieving revision 1.1.1.1
diff -u -r1.1.1.1 README.txt
(日付の行は略)
@@ -1 +1,2 @@
 Hello CVS
+line2 by tanaka
$ cvs commit -m "README に2行目" README.txt
/dev/shm/t186/w/repo/myapp/README.txt,v  <--  README.txt
new revision: 1.2; previous revision: 1.1

続いて鈴木さんがコミットすると、止まります。

$ cd ../suzuki
$ cvs commit -m "suzuki edit" README.txt
cvs commit: Up-to-date check failed for `README.txt'
cvs [commit aborted]: correct above errors first!

$ cvs update README.txt
RCS file: /dev/shm/t186/w/repo/myapp/README.txt,v
retrieving revision 1.1.1.1
retrieving revision 1.2
Merging differences between 1.1.1.1 and 1.2 into README.txt
rcsmerge: warning: conflicts during merge
cvs update: conflicts found in README.txt
C README.txt

$ cat README.txt
<<<<<<< README.txt
Hello CVS!
=======
Hello CVS
line2 by tanaka
>>>>>>> 1.2

<<<<<<< から ======= までが自分の変更、>>>>>>> 1.2 までがリポジトリの版 1.2 です。CVS は直す前のファイルを .#README.txt.1.1.1.1 という名前で残していました。cvs status では「Unresolved Conflict」と出ます。

$ ls -a
.  .#README.txt.1.1.1.1  ..  CVS  README.txt  main.c  src
$ cvs status README.txt
===================================================================
File: README.txt       	Status: Unresolved Conflict

   Working revision:	1.2
   Repository revision:	1.2	/dev/shm/t186/w/repo/myapp/README.txt,v
   Commit Identifier:	1006AE52D0C1912296B
   Sticky Tag:		(none)
   Sticky Date:		(none)
   Sticky Options:	(none)

$ printf 'Hello CVS!\nline2 by tanaka\n' > README.txt   # 両方を生かして手で直す
$ cvs commit -m "衝突を解消" README.txt
new revision: 1.3; previous revision: 1.2

CVS のマーカーの見張りは弱いので注意が要ります。別の作業コピーで、コンフリクトしたファイルをそのままコミットすると止まりましたが、末尾に空行を1つ足しただけでコミットは通り、マーカーごと版 1.6 になりました。

$ cvs commit -m "marker" README.txt
cvs commit: file `README.txt' had a conflict and has not been modified
cvs [commit aborted]: correct above errors first!
$ echo >> README.txt; cvs commit -m "marker2" README.txt
cvs commit: warning: file `README.txt' seems to still contain conflict indicators
/dev/shm/t186/w/repo/myapp/README.txt,v  <--  README.txt
new revision: 1.6; previous revision: 1.5

直した後は cvs log と cvs annotate で、誰がどの行をどの版で変えたかを確かめられます。

$ cvs annotate README.txt
Annotations for README.txt
***************
1.3          (root     30-Oct-26): Hello CVS!
1.2          (root     30-Oct-26): line2 by tanaka

マーカーの読み方や直し方の考え方は Git と同じです。詳しくは関連記事「diffコマンドの使い方|-uの読み方・patch適用・オプション一覧」で -u 形式の差分の読み方を扱っています。

  • 1コミットの前に cvs updateUp-to-date check failed を避ける一番の方法。C が出たら必ず中身を見る
  • 2マーカーを消し忘れないファイルを少しでも書き換えると、マーカーが残っていても警告だけでコミットされた。commit 前に grep で確かめる
  • 3.#ファイル名.版 は控え直し終わったら消してよい。管理対象ではない
  • 4バイナリはマージできない画像などは cvs edit・watch で「誰が触っているか」を知らせ合う運用にする

CVSのタグとブランチ:tag・tag -b・update -j

この節では、CVS でタグとブランチを作り、ブランチの修正を幹(trunk)へ戻すまでの流れと、CVS 特有の「スティッキータグ」が分かります。

CVS のブランチは「-b を付けたタグ」で、ブランチで取り出した作業コピーにはブランチ名が貼り付いたまま(スティッキー)になります。幹に戻るには cvs update -A が要ります。

$ cvs tag REL_1_0            # 今の版に名前を付ける
cvs tag: Tagging .
T README.txt
T main.c
cvs tag: Tagging src
T src/util.c
$ cvs tag -b FIX_1_0         # ブランチを作る
$ cvs update -r FIX_1_0      # ブランチへ切り替える
$ cvs status main.c | grep -E 'Sticky Tag|Working'
   Working revision:	1.1.1.1	2026-10-31 08:49:19 +0900
   Sticky Tag:		FIX_1_0 (branch: 1.1.1.1.2)
$ cat CVS/Tag
TFIX_1_0
$ cvs commit -m "修正を枝に" README.txt
new revision: 1.3.2.1; previous revision: 1.3

ブランチ上の版番号は 1.3.2.1 のように4つ組になりました。「版 1.3 から分かれた2番目の枝の1つ目」という意味です。幹へ戻して、ブランチの変更を取り込みます。

$ cvs update -A                      # スティッキータグを外して幹へ
$ cvs update -j FIX_1_0 README.txt   # ブランチの変更を取り込む
RCS file: /dev/shm/t186/w/repo/myapp/README.txt,v
retrieving revision 1.3
retrieving revision 1.3.2.1
Merging differences between 1.3 and 1.3.2.1 into README.txt
$ cvs commit -m "FIX_1_0 を幹へマージ" README.txt
new revision: 1.4; previous revision: 1.3
$ cvs log -h README.txt | sed -n '/symbolic/,/keyword/p'
symbolic names:
	FIX_1_0: 1.3.0.2
	REL_1_0: 1.3
	start: 1.1.1.1
	vendor: 1.1.1
keyword substitution: kv

CVS はどこまでマージしたかを覚えていません。同じブランチから2回目を取り込むときは、-j 前回のタグ -j FIX_1_0 のように範囲を自分で指定します。マージのたびにブランチ側へ目印のタグを付けるのが CVS の定番の運用でした。

タグを付け直したときの出力も、CVS らしさがよく出ます。後から追加したファイルがある状態で同じ名前のタグを付けると、既存のファイルは動かさず、新しいファイルにだけタグが付きました。

$ cvs tag REL_1_0
cvs tag: Tagging .
W README.txt : REL_1_0 already exists on version 1.3 : NOT MOVING tag to version 1.4
T app.c
T data.bin
T logo.png

この結果、REL_1_0 は「README.txt は前の版、app.c は新しい版」という、ある時点のどの状態とも一致しないタグになります。この混ざったタグは、後の Git への移行で差が出る原因になりました(移行の節で扱います)。タグの名前の付け方は、関連記事「git tagとセマンティックバージョニング|命名規則・コマンド早見表」が参考になります。

  1. 1cvs tag 名前 → 今の作業コピーの版に名前ファイルごとに「その名前はこの版」と記録される
  2. 2cvs tag -b 名前 → ブランチを作る作業コピーはまだ幹のまま
  3. 3cvs update -r 名前 → そのブランチへ切り替えるCVS/Tag ができ、以後のコミットはブランチへ
  4. 4cvs update -A → 幹へ戻るスティッキータグ・日付・-k の指定を外す
  5. 5cvs update -j 名前 → ブランチの変更を取り込む2回目以降は -j を2つ書いて範囲を指定

CVSが苦手なこと:改名・削除・アトミックでないコミット

この節では、CVS が Subversion や Git に置き換えられた理由になった弱点を、実際の動きで確かめられます。

改名すると履歴が切れる

CVS には改名のコマンドがありません。公式マニュアルの方法も「新しい名前で add し、前の名前を remove する」です。main.c を app.c に変えると、app.c の履歴は版 1.1 から始まりました。

$ mv main.c app.c
$ cvs remove main.c
cvs remove: scheduling `main.c' for removal
$ cvs add app.c
cvs add: scheduling file `app.c' for addition
$ cvs commit -m "main.c を app.c に改名"
/dev/shm/t186/w/repo/myapp/app.c,v  <--  app.c
initial revision: 1.1
/dev/shm/t186/w/repo/myapp/main.c,v  <--  main.c
new revision: delete; previous revision: 1.2
$ cvs log app.c | grep -E '^revision|total'
total revisions: 1;	selected revisions: 1
revision 1.1

同じことを Subversion(svn move)と Git(git mv と git log --follow)で行うと、改名前の履歴まで続けて表示されました。

削除したファイルは Attic へ

CVS は、手元にファイルが残っていると remove を受け付けません。消したファイルの ,v はリポジトリの Attic フォルダーへ移り、履歴は残ります。

$ cvs remove src/util.c
cvs remove: file `src/util.c' still in working directory
cvs remove: 1 file exists; remove it first
$ rm src/util.c; cvs remove src/util.c
cvs remove: scheduling `src/util.c' for removal
$ cvs commit -m "util.c を削除" src/util.c
new revision: delete; previous revision: 1.2
$ ls repo/myapp/src/Attic
util.c,v

フォルダーは消せません。空になったフォルダーは、cvs update -P(または checkout -P)で作業コピーから取り除く運用です。逆に、空のフォルダーを cvs add すると、すぐにリポジトリへ作られました(コミットを待たない)。

複数ファイルのコミットが1つにまとまらない

CVS のコミットは1ファイルずつ順に記録され、途中で失敗すると一部だけ入ることがあります(アトミックではない)。2ファイルを1回でコミットすると、それぞれの ,v が別々に版を進めました。

$ cvs commit -m "2ファイル同時"
/dev/shm/t186/w/repo/myapp/main.c,v  <--  main.c
new revision: 1.2; previous revision: 1.1
/dev/shm/t186/w/repo/myapp/src/util.c,v  <--  src/util.c
new revision: 1.2; previous revision: 1.1
$ cvs log -N -r main.c src/util.c | grep -E 'Working file|^revision|commitid'
Working file: main.c
revision 1.2
date: 2026-10-31 08:49:46 +0900;  author: root;  state: Exp;  lines: +2 -1;  commitid: 1006AE52D1A19C9133F;
Working file: src/util.c
revision 1.2
date: 2026-10-31 08:49:46 +0900;  author: root;  state: Exp;  lines: +1 -0;  commitid: 1006AE52D1A19C9133F;

CVS 1.12 系では、同じコミットのファイルに同じ commitid が付きます。これで「同時にコミットしたもの」を後からたどれますが、リポジトリ全体の版番号はありません。「版 1.2 の状態に戻して」と言っても、ファイルごとに意味が違います。全体の状態を指すには、タグか日付(cvs update -D "2026-10-01")を使います。

「この時点の状態」を人に伝えるとき

避ける「版 1.4 で動いていた」と版番号で伝える。README.txt の 1.4 と app.c の 1.4 は別の時点を指す
→
代わりにリリースのたびに cvs tag を付け、「REL_1_0 で動いていた」とタグで伝える。日付で指す場合は時刻とタイムゾーンまで書く

Subversion の「r123」、Git のコミット ID にあたるものは CVS には無い

CVSでバイナリファイルを扱う:-kbとキーワード置換の実測

この節では、CVS で画像などのバイナリファイルを登録するときに -kb を付けないと何が起きるか、保存容量がどう増えるかが実測で分かります。

CVS は、テキストの中の $Id$ などのキーワードを自動で書き換えます。バイナリファイルに同じ並びの文字があると、取り出したときに中身が変わります。Windows のクライアントでは改行コードの変換も起きます(公式マニュアル「How to store binary files」)。

$ printf 'BIN\x00\x01$Id$\x00\xff\r\nEND' > data.bin
$ sha256sum data.bin | cut -c1-16; wc -c data.bin
174f2031ccae1163
16 data.bin
$ cvs -Q add data.bin; cvs -Q commit -m "kbなし" data.bin
$ rm data.bin; cvs -Q update data.bin
$ sha256sum data.bin | cut -c1-16; wc -c data.bin
7e7c7a211d74b1ad
62 data.bin
$ od -c data.bin | head -2
0000000   B   I   N  \0 001   $   I   d   :       d   a   t   a   .   b
0000020   i   n   ,   v       1   .   1       2   0   2   6   /   1   0

16バイトのファイルが、取り出すと62バイトになり、ハッシュ値も変わりました。cvs admin -kb で後から指定し直し、作業コピーを取り直すと、元のハッシュ値(174f2031…)に戻りました。最初から cvs add -kb で登録するのが基本です。拡張子ごとに自動で -kb にするには、リポジトリの CVSROOT/cvswrappers に *.png -k 'b' のように書きます(この確認環境の既定では空でした)。

保存容量:改行の無いバイナリは毎回まるごと増える

1MB のファイルの1バイトだけを変えるコミットを20回くり返し、リポジトリの大きさを比べました。ファイルは乱数で作り、改行の文字(0x0A)を含むものと含まないものの2通りです。

1MB×21版 CVS(-kb) Subversion Git(gc 前→gc 後)
改行を含む乱数1,040KB1,244KB21,756KB → 1,048KB
改行を含まない乱数21,592KB1,244KB21,756KB → 1,048KB

CVS は RCS の行単位の差分で版を保存するため、改行の無いバイナリでは1バイト変えただけでも毎回まるごと1MB 増えました(約21倍)。改行を含むファイルなら差分で小さく収まります。Subversion はバイナリでも差分を取るため、どちらも約1.2MB でした。Git は1版ずつ丸ごと保存し、git gc でまとめたときに差分へ圧縮されます。「CVS はバイナリを全部の版まるごと保存する」と説明されることがありますが、実際は「行の区切りが少ないファイルほど差分が効かない」というのが正確です。画像・圧縮ファイル・Office 文書は改行がほとんど無いため、CVS に入れると大きくなりやすいです。

21倍改行の無いバイナリの CVS の容量改行ありとの比較。1MB×21版で 21,592KB
16→62B-kb なしで $Id$ が置換されたハッシュ値も変わった
1.2MBSubversion の容量改行の有無に関係なく同じ

日本語のファイル名

UTF-8 の Linux 同士では、仕様書.txt を add・commit・checkout でき、リポジトリにも 仕様書.txt,v として保存されました。ただし CVS はファイル名の文字コードを変換しません。Shift_JIS の環境と UTF-8 の環境が同じリポジトリを使うと、名前が正しく表示されない原因になります。日本語版 Wikipedia も、CVS は JIS・SJIS・EUC の違いに対応していないと説明しています。

CVSのリポジトリへの接続方式::local:・:ext:・:pserver:・:fork:

この節では、CVSROOT の書き方と、ネットワーク越しにリポジトリへつなぐ方式の違いが分かります。

CVSROOT は :方式:ユーザー@ホスト:パス の形で書きます。公式マニュアルによると、方式は ext・fork・gserver・kserver・local・pserver(環境によって server)があり、/ から始まるパスで方式を省くと local になります。

方式 書き方の例 使う場面
:local:/srv/cvsroot同じ機械のフォルダー(この記事の多くの例)
:ext::ext:user@host:/cvsssh などで接続。CVS_RSH=ssh を設定して使うのが一般的
:pserver::pserver:user@host:/cvsCVS 専用のパスワード認証(ポート 2401)。読み取り専用の公開でよく使われた
:gserver:・:kserver::gserver:host:/cvsGSSAPI・Kerberos の認証
:fork::fork:/srv/cvsroot同じ機械でクライアント・サーバーの手順を通す(試験・問題の切り分け用)

:fork: で取り出し、環境変数 CVS_CLIENT_LOG でクライアントとサーバーのやり取りを記録すると、CVS が1行ずつの文字の命令で話していることが見えました。

$ cvs -d :fork:$W/repo checkout -d forkwc myapp
$ cat forkwc/CVS/Root
:fork:/dev/shm/t186/w/repo
$ cd forkwc; CVS_CLIENT_LOG=$W/clog cvs -Q update
$ head $W/clog.in        # クライアント → サーバー(抜粋)
Root /dev/shm/t186/w/repo
valid-requests
UseUnchanged
Global_option -q
Global_option -Q
Argument -u
Directory .
myapp
Entry /README.txt/1.4///
Unchanged README.txt

作業コピーの各ファイルの版(Entry)と「変わっていない(Unchanged)」をサーバーに伝え、サーバーが必要な差分だけを返します。CVS が差分だけを送るので軽い、と言われるのはこの仕組みです。

:pserver: の注意:公式マニュアルは、pserver のパスワードはクライアント側に「平文をごく簡単に符号化しただけ」の形で保存・送信されると説明しています。インターネットを通すなら :ext:(ssh)を使います。手元で pserver を立てずに接続を試すと、次のように拒否されました。

$ cvs -d :pserver:anon@127.0.0.1:2401/dev/shm/t186/w/repo checkout myapp
cvs [checkout aborted]: connect to 127.0.0.1(127.0.0.1):2401 failed: Connection refused

CVSとGit・Subversionの違い(対応表と実測)

この節では、CVS・Subversion・Git の設計の違いと、同じ操作にかかる時間の差が分かります。

Subversion は「集中型のまま CVS の弱点を直したもの」、Git は「履歴を各自が丸ごと持つ分散型」です。

項目 CVS Subversion Git
型集中型集中型分散型
版番号ファイルごと(1.4 など)リポジトリ全体(r123)コミットごとのハッシュ値
コミット1ファイルずつ(アトミックでない)アトミックアトミック
改名・移動できない(remove と add)できる(履歴を引き継ぐ)できる(--follow でたどる)
フォルダーの削除できない(-P で隠す)できる空フォルダーはそもそも記録しない
ブランチ・タグファイルごとに名前を付けるフォルダーのコピーコミットを指す名前
マージの記録覚えない(タグで目印)覚える(svn:mergeinfo)覚える(親が2つのコミット)
オフラインで履歴見られない見られない見られる
保存形式RCS の ,v ファイル独自の DB(FSFS)オブジェクト(zlib 圧縮・pack)

実測:1,000ファイルをコミット・タグ付けする時間

50行のテキストファイル1,000個を登録し、全部に1行足してコミット、タグを付け、状態を確かめるまでの時間を、3回の中央値で比べました(リポジトリはすべて同じ機械の中)。

操作 CVS Subversion Git
1,000ファイルの変更をコミット1,508ms696ms37ms
タグを付ける178ms20ms(svn copy)4ms
状態を見る46ms(-n update)21ms(status)8ms(status)
ディスク使用量(2版)4,000KB656KB196KB

CVS はタグを付けるだけでも1,000個の ,v を1つずつ書き換えるため、ファイル数に比例して遅くなります。Subversion のタグはフォルダーの安いコピー、Git のタグはコミットを指す1つの名前なので、ファイル数にほぼ関係ありません。ディスク使用量の差は、CVS が小さなファイル1つでも ,v を1つ作る(ファイルシステムの最小単位 4KB を使う)ことが大きく効いています。

CVS から Subversion へ(集中型のまま)

サーバー中心の運用・権限管理をそのまま移しやすい

チェックアウト・コミットの考え方がほぼ同じ

全体の版番号・改名・アトミックなコミットが手に入る

CVS から Git へ(分散型)

ブランチ・マージが軽く、プルリクエストの運用に乗れる

GitHub・GitLab などのサービスや CI とつながる

push・pull・ステージングなど新しく覚えることが増える

Git のブランチの運用の決め方は、関連記事「Gitのブランチ戦略|Git Flow・GitHub Flowの違いと選び方」で扱っています。

CVSからGitへ移す手順(cvs-fast-export・git cvsimport・cvs2git)

この節では、CVS の履歴を Git へ移す道具の選び方と、同じリポジトリを2つの道具で変換して照合した結果が分かります。

一度きりの移行なら cvs-fast-export か cvs2git を使います。Git の公式ドキュメントも、git cvsimport の説明の冒頭で、一度きりの取り込みには cvs2git か cvs-fast-export を検討するよう書いています。どの道具も、リポジトリの ,v ファイルを直接読むので、サーバーにあるリポジトリのフォルダーを丸ごと複製して手元に置くところから始めます。

道具 動かし方 この確認での結果
cvs-fast-export 1.59,v の一覧を渡し、出力を git fast-import へタグ REL_1_0 の中身が CVS と完全に一致。0.03秒
cvsconvert(同梱)cvs-fast-export の変換と、全ブランチ・タグの照合を一度に差は $Id$ の行だけ(5か所)と報告
git cvsimportcvsps 2.x を使い、CVS から順に取り込むREL_1_0 が最新のコミットに付き、中身がずれた
cvs2git(cvs2svn)blob と dump の2ファイルを作り git fast-import へ動かしていない。公式に Python 2 が必要とある

cvs-fast-export で変換する

$ mkdir mig2 && cd mig2 && git init -q
$ printf 'root = Taro Tanaka <tanaka@example.test> Asia/Tokyo\n' > ../authors-fe.txt
$ find $W/repo/myapp -name '*,v' | cvs-fast-export -A ../authors-fe.txt | git fast-import --quiet
cvs-fast-export: warning - branch point FIX_1_0 -> import-1.1.1 matched by date
$ git branch -a
  FIX_1_0
  REL_1_0
  import-1.1.1
  master
$ git tag
start
$ git log --oneline --graph --all | tail -6
* fdacf89 README に2行目
| * 5085ca0 修正を枝に
| * 1376a15 初回取り込み
|/
* 8964858 *** empty log message ***
$ git log -1 REL_1_0 --format=%s
Synthetic commit for incomplete tag REL_1_0

作者の対応表(-A)で、CVS のユーザー名 root が「Taro Tanaka <tanaka@example.test>」になりました。前の節で作った「混ざったタグ」REL_1_0 は、タグではなく「Synthetic commit(合成したコミット)」を持つブランチとして再現されています。import のときの vendor ブランチは import-1.1.1 というブランチになり、FIX_1_0 の分岐点は日付で推定したという警告が出ました。cvs-fast-export のマニュアルも、vendor ブランチは変換の問題の元になりやすいと書いています。

変換結果を CVS と照合する

移行の成否は、CVS から取り出したものと Git から取り出したものを diff -r で比べて確かめます。タグ REL_1_0 で比べた結果です。

$ cvs -Q -d $W/repo co -r REL_1_0 -d chk myapp
$ for m in mig1 mig2; do
    mkdir x_$m; git -C $m archive REL_1_0 | tar -x -C x_$m
    echo "== $m"; diff -r -x CVS -x .gitignore chk x_$m && echo 一致
  done
== mig1
diff -r -x CVS -x .gitignore chk/README.txt x_mig1/README.txt
1c1
< Hello CVS!
---
> Hello CVS
2a3
> hotfix
(app.c の $Id$ の差は略)
Only in chk: main.c
Only in chk: src
== mig2
一致

mig1 は git cvsimport(-k -a -o main -A を指定)で変換したものです。git cvsimport は REL_1_0 を最新のコミットに付けたため、README.txt の中身が違い、タグの時点にあった main.c と src/util.c が抜けていました。Git の公式ドキュメントの ISSUES 節にも、同じ版に複数のタグがある場合などに正しく取り込めないことがあると書かれています。cvs-fast-export(mig2)は完全に一致しました。

cvs-fast-export に同梱の cvsconvert は、変換と、全ブランチ・タグの照合をまとめて行います。この確認環境では Git の既定のブランチ名を main にしていたため、最初は次のエラーで止まりました。

$ cvsconvert -v repo/myapp
fatal: You are on a branch yet to be born
cvsconvert: cat repo-myapp.git.fi | (cd repo-myapp-git >/dev/null; git fast-import --quiet --done && git checkout) returned 128.

cvs-fast-export は幹を master という名前で出力するためです。git config --global init.defaultBranch master にしてから実行すると最後まで進み、差は5か所、すべて $Id$ の行だけと報告されました(終わったら設定を main に戻しました)。

repo branch REL_1_0: repo-myapp.checkout/app.c and repo-myapp-git/app.c are different.
-/* $Id: main.c,v 1.2 2026/10/30 23:49:46 root Exp $ */
+/* $Id: app.c,v 1.1 2026/10/30 23:49:55 root Exp $ */
cvsconvert: conversion is in repo-myapp-git

cvs-fast-export のマニュアルは、$ で囲んだキーワードの中身は変換後には当てにならないため、移行後の片付けとして消すのがよいと書いています。

1

CVS を止めて複製する

コミットを止める日を決め、リポジトリのフォルダー(,v ファイル)を丸ごと複製する。変換は複製に対して行う。

準備

2

作者の対応表を作る

cvs log の author を集め、「ユーザー名 = 名前 <メール> タイムゾーン」の表を作る。

準備

3

変換する

cvs-fast-export(または cvsconvert)で変換。警告の文言は全部控える。

変換

4

全部のタグとブランチを照合する

CVS の co -r タグ と Git の archive タグ を diff -r で比べる。差が $Id$・空フォルダー・.cvsignore だけかを見る。

検証

5

後片付けをする

.cvsignore を .gitignore へ、$Id$ を削除、改行コードと -kb のファイルを .gitattributes へ。CVS は読み取り専用で残す。

仕上げ

cvs2git を使う場合の公式の手順は、cvs2git --blobfile=… --dumpfile=… --username=cvs2git /path/to/cvs/repo で2つのファイルを作り、git init --bare したリポジトリへ git fast-import で2回に分けて読み込み、TAG.FIXUP ブランチを消して git gc --prune=now する流れです。Windows の作業者が混ざる場合の改行コードの決め方は、関連記事「Git改行コード設定|autocrlfと.gitattributesの違い」で扱っています。

今CVSに出会う場面と判断表

この節では、2026年10月29日時点で CVS に出会うのはどんな場面か、そのとき移行するか・そのまま使うかの判断の目安が分かります。

新しく CVS で始める例は少ない一方、長く続くプロジェクトや社内の設定管理には今も残っています。たとえば OpenBSD の公式サイトは、OpenBSD のソースツリーの管理に CVS を使っていると説明し、anoncvs(読み取り専用の公開サーバー)からの取り出し方を載せています。

  • 長く運用している社内システムのソースや、サーバーの設定ファイルが CVS のまま残っている
  • 開発ツールや IDE の設定・メニューに CVS の項目がある(ここでの CVS はコンビニではない)
  • 引き継いだ資料に CVSROOT・$Id$・1.23 のような版番号が書いてある
  • OpenBSD などのソースを anoncvs で取り出す
状況 判断の目安 理由
今も毎日コミットがあるGit(または Subversion)へ移す計画を立てる改名・アトミックなコミット・ブランチの運用で差が大きい
ほとんど更新が無く、参照だけCVS を読み取り専用で残すか、Git へ変換して保管動いているものを急いで変える利点が小さい
バイナリが多い早めに移す。容量を先に測る改行の無いファイルは版ごとにまるごと増える
vendor ブランチ・混ざったタグが多いcvs-fast-export で変換し、全タグを照合git cvsimport ではずれが出た
外部の公開ソースを取り出すだけ公式の手順どおり cvs checkout移行は不要
pserver をインターネットに出しているssh(:ext:)に切り替える公式マニュアルがパスワードの扱いの弱さを明記

移行するかどうかは「更新の頻度」「CVS を知る人がいるか」「ほかのツール(CI・課題管理)とつなぎたいか」の3つで考えると決めやすいです。

CVSのよくあるエラーとつまずき(原文つき)

この節では、CVS を触ったときに出やすいエラーの原文と、原因・直し方が一覧で分かります。すべてこの確認環境で出した文言です。

エラーの原文 原因 直し方
cvs checkout: No CVSROOT specified! Please use the `-d' optionリポジトリの場所が分からない-d を付けるか、環境変数 CVSROOT を設定
cvs [checkout aborted]: /nonexist/CVSROOT: No such file or directoryパスが違う、または init していないパスを見直す。新規なら cvs init
cvs checkout: cannot find module `nosuchmodule' - ignoredモジュール名が違うリポジトリ直下のフォルダー名を確かめる
cvs commit: Up-to-date check failed for `README.txt'先にほかの人がコミットしたcvs update してからコミット
cvs update: conflicts found in README.txt同じ行を2人が変えたマーカーの箇所を直してコミット
cvs commit: file `README.txt' had a conflict and has not been modifiedコンフリクトを直さずにコミットしたマーカーの箇所を直してからコミット
warning: file `README.txt' seems to still contain conflict indicatorsマーカーが残ったまま(コミットは通る)すぐ直して次の版をコミット
cvs remove: file `src/util.c' still in working directoryファイルを消す前に remove した先に rm してから cvs remove(または remove -f)
cvs commit: nothing known about `nosuch.txt'add していない・名前違いcvs add してからコミット
cvs add: `README.txt' already exists, with version number 1.4すでに管理対象add は不要。そのまま commit
cvs [update aborted]: no such tag `NOSUCH'タグ名・ブランチ名が違うcvs log -h ファイル の symbolic names を見る
W README.txt : REL_1_0 already exists on version 1.3 : NOT MOVING tag同じ名前のタグがすでにある動かすなら cvs tag -F。混ざったタグに注意
connect to 127.0.0.1(127.0.0.1):2401 failed: Connection refusedpserver が動いていない・ポートが閉じているサーバー側の設定を確かめる。:ext: も検討

エラーにならない「つまずき」もあります。

1ブランチのまま気づかない

update -r で切り替えた後は、何をコミットしてもブランチへ入る。CVS/Tag があるか、status の Sticky Tag を見る。

2新しいフォルダーが取り込まれない

ほかの人が作ったフォルダーは、cvs update -d を付けないと手元に作られない。

3-kb の付け忘れ

画像に $Id$ と同じ並びがあると中身が変わる。cvs status の Sticky Options で -kb を確かめる。

よくある質問

この節では、CVS について検索されやすい疑問に短く答えます。

Q. CVS は何の略ですか。読み方は。
IT では Concurrent Versions System の略で、「シーブイエス」と読みます。「並行して版を管理する仕組み」という意味です。流通ではコンビニエンスストアの略として使われます。

Q. CVS と Git の一番の違いは何ですか。
CVS はサーバーにある1つのリポジトリを全員で使う集中型で、版番号がファイルごとに付きます。Git は各自が履歴を丸ごと持つ分散型で、1回のコミットがリポジトリ全体の状態を表します。

Q. CVS と Subversion はどちらが新しいですか。
Subversion です。CVS の使い勝手を保ちつつ、改名・アトミックなコミット・全体の版番号に対応するように作られました。どちらも集中型です。

Q. CVS は今もダウンロードできますか。
GNU の配布サーバー(ftp.gnu.org/non-gnu/cvs)にソースがあり、Ubuntu・Debian などでは公式パッケージ(cvs)で入ります。新しい版は 2008年の 1.11.23 以降出ていません。

Q. CVS で改名したいときはどうしますか。
新しい名前でファイルを作って cvs add、前の名前を cvs remove し、まとめてコミットします。履歴は新しい名前で版 1.1 から始まるので、コミットのメッセージに前の名前を書いておくと後で追えます。

Q. 「Up-to-date check failed」と出てコミットできません。
ほかの人が同じファイルを先にコミットしています。cvs update で取り込み、C(コンフリクト)が出たら直してから、もう一度コミットします。

Q. CVS の $Id$ とは何ですか。
キーワード置換の目印です。取り出すときに $Id: main.c,v 1.1.1.1 2026/10/30 23:49:19 root Exp $ のように、ファイル名・版・日時・作者へ書き換えられます。バイナリでは中身を壊す原因になるため、-kb を付けます。

Q. CVS の履歴を Git に移すと、何が失われますか。
この確認では、cvs-fast-export で変換すると中身はすべて一致し、差は $Id$ の行だけでした。空のフォルダーは Git が記録しないため消え、混ざったタグは「合成したコミット」を持つブランチになります。

Q. git cvsimport は使えますか。
使えますが、cvsps 2.x が必要で、cvsps 3 以降では動きません。この確認ではタグの位置がずれたため、一度きりの移行なら cvs-fast-export か cvs2git が向いています。

Q. Windows や Mac で CVS を使うには。
CVS のクライアントは Windows・macOS 向けのものもあり、IDE に組み込まれていることもあります。製品ごとに提供状況が変わるため、使う道具の公式サイトで確かめます。この記事では Linux の cvs コマンドだけを動かしています。

まとめ

この節では、記事の要点を5つに絞って振り返ります。

  • CVS(Concurrent Versions System)は、1990年ごろから広く使われた集中型のバージョン管理システム。版番号はファイルごとに付き、履歴は RCS 形式の ,v ファイルに残る。
  • 使い方の芯は init・import・checkout・commit・update。同時編集はロックせず、先にコミットした人が勝ち、後の人は update で統合する。
  • 改名できない・コミットがアトミックでない・タグがファイルごと、の3点が Subversion・Git との大きな違い。1,000ファイルのコミットは CVS 1,508ms、Subversion 696ms、Git 37ms だった。
  • バイナリは -kb で登録する。付けないと $Id$ が置換され、16バイトが62バイトになった。改行の無いバイナリは版ごとにまるごと増え、容量は約21倍になった。
  • Git へ移すなら cvs-fast-export(または cvs2git)で変換し、全タグを diff -r で照合する。git cvsimport ではタグの中身がずれた。

参考・出典

確認日はいずれも 2026年10月29日です。

  • GNU「Concurrent Versions System(CVS)」:https://www.nongnu.org/cvs/
  • GNU 配布サーバー「cvs 1.12.13」:https://ftp.gnu.org/non-gnu/cvs/source/feature/1.12.13/
  • GNU 配布サーバー「cvs 1.11.23」:https://ftp.gnu.org/non-gnu/cvs/source/stable/1.11.23/
  • Per Cederqvist ほか「Version Management with CVS」(cvs 1.12.13 のパッケージ同梱 cvs.txt。2.9 Remote repositories・9.2 How to store binary files・10.7 Choosing between reserved or unreserved checkouts)
  • Git「git-cvsimport Documentation」:https://git-scm.com/docs/git-cvsimport
  • cvs2svn プロジェクト「cvs2git Documentation」:https://github.com/mhagger/cvs2svn/blob/master/cvs2git.html
  • cvs-fast-export 1.59 マニュアル(cvs-fast-export(1)・cvsconvert(1)):https://gitlab.com/esr/cvs-fast-export
  • OpenBSD「Anonymous CVS」:https://www.openbsd.org/anoncvs.html
  • Wikipedia「Concurrent Versions System」(英語版・日本語版):https://en.wikipedia.org/wiki/Concurrent_Versions_System
  • Apache Subversion「Version Control with Subversion」:https://svnbook.red-bean.com/

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「CVSとは?使い方・Gitとの違い・移行を実機で検証|バージョン管理」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

成長のヒミツ、 ぜんぶ丸わかり