ブランチは何故便利なのでしょう?
ここであのシナリオに戻りましょう。プログラムの新しいバージョンを作ってい る最中の開発者が、ひとつ前のリリースのバグレポートを受けとったところです。 開発者が問題を解決したとしても、顧客にそのバグフィクス版を届ける方法がわ かりません。いえ、プログラムの古いコピーを取ってきて、CVS の知らないとこ ろでそれを直して出荷する、というのは難しいことではありません。でもこれで は、何をしたか記録が残らないのです。CVS は直しに気づかないまま。もしあと でそのパッチによくないところが発見されても、誰にも問題を再生するスタート 地点さえわかりません。
現在の不安定なバージョンのソースのバグを直して顧客に出荷する、というのは もっと悪いアドバイスですね。そりゃ確かにレポートされたバグについては直っ ているかもしれませんが、その他のところは実装途中でテストもされていない状 態です。動きはするでしょうが、but it's certainly not ready for prime time.
最後のリリースバージョンは、そのバグを除いては安定しているのですから、理 想的な解決策は、時間を戻してその古いリリースのバグを直すことです。つまり、 最後のリリースがこのバグフィクスを取り込むような、もうひとつの世界を作るということです。
ここでブランチの登場です。開発者は、開発のメインライン(トランク(幹))の、 最新リビジョンでなく最後のリリースのところに根を下ろすブランチを作ります。 彼女はこのブランチの作業コピーをチェックアウトして、バグフィクスに必要な 変更を加え、それらをブランチにコミットします。こうすればバグフィクスの記 録ができるというわけです。これで、ブランチに基づく暫定リリースをパッケー ジにして、顧客に出荷できます。
**************************************************************** 彼女の変更はトランクにあるコードに何の影響も与えません
同じバグフィクスがトランクのほうにも必要かどうか見つけだす
Her change won't have affected the code on the trunk, nor would she want it to without first finding out (whether the trunk needs the same bug fix or not). ****************************************************************
もし必要なら、ブランチの変更をトランクにマージすることもできます。CVS は 分岐点からブランチの先端(最新の状態のところ)までに加えられた変更を計算し、 その違いをプロジェクトのトランクの先端に適用します。ブランチの根と先端の 相違分のマージはもちろんうまくいって、バグフィクスになります。
マージはアップデートの特別な場合と考えることもできます。マージでの相違分 というのは、作業コピーとリポジトリを比較するかわりに、ブランチの根と先端 を比較することによって算出されるものです。
アップデートの動作というのは、その変更のパッチを作者から直接受け取って、 それを手でパッチするのと同じです。事実アップデートを実行する際、CVS は作 業コピーとリポジトリの相違分を計算して(相違分というのは diff プログラム がやるわけですが)、その diff を作業コピーに適用します、patch プログラム がやるのと同じようにです。これは、開発者が外の世界からの変更を取りいれる やりかた、パッチを作った人からパッチファイルをもらって手でパッチを当てる というやつですが、それを真似ています。
バグフィクスブランチをトランクにマージするというのは、外のコントリビュー タのパッチを当ててバグを直すのと似ています。コントリビュータは最新のリリー スバージョンへのパッチを作ります。ブランチの変更がそのバージョンに対して なされるのと同じです。現在のソースコードのその領域が最後のリリースからそ う変わっていなければ、マージは問題なく成功するでしょう。しかし、コードが とても変わってしまっていたら、マージはコンフリクトを起こして失敗に終わる でしょう(パッチがリジェクトされるでしょうという意味です)、手で直接ゴソゴ ソやる必要があるわけです。これは通常、コンフリクト領域を読み、手で必要な 変更を施し、そしてコミットすればいいのです。Figure 2.3 はブランチとマー ジで何が起こるかを示した図です。
(branch on which bug was fixed) .---------------->---------------. / | / | / | / | / V (<------ point of merge) ====*===================================================================> (main line of development) [Figure 2.3: A branch and then a merge. Time flows left to right.]
**************************************************************** We'll now walk through the steps necessary to make this picture happen. **************************************************************** 図で左から右へ流れているのは実際の時間ではなく、リビジョン履歴です。ブラ ンチはリリースの時点で作られたわけではなく、リリースのリビジョンに根を下 ろすように後で作られるものです。
プロジェクト中のファイルは Release-1999_05_01
とタグ付けされてか
らもたくさんリビジョンを増やして、ファイルも追加された、と仮定しましょう。
古いリリースに関するバグレポートが舞い込んできたときに最初にしたいのは、
例の古いリリース、Release-1999_05_01
というタグをつけたところに根
を下ろすブランチを作ることです。
ひとつの方法として、まずそのタグに基づいた作業コピーをチェックアウトして、 それから -b (ブランチ, branch) オプションで再度タグづけしてブランチを作 ります。
floss$ cd .. floss$ ls myproj/ floss$ cvs -q checkout -d myproj_old_release -r Release-1999_05_01 myproj U myproj_old_release/README.txt U myproj_old_release/hello.c U myproj_old_release/a-subdir/whatever.c U myproj_old_release/a-subdir/subsubdir/fish.c U myproj_old_release/b-subdir/random.c floss$ ls myproj/ myproj_old_release/ floss$ cd myproj_old_release floss$ ls CVS/ README.txt a-subdir/ b-subdir/ hello.c floss$ cvs -q tag -b Release-1999_05_01-bugfixes T README.txt T hello.c T a-subdir/whatever.c T a-subdir/subsubdir/fish.c T b-subdir/random.c floss$
最後のコマンドを良く見てください。ブランチを作るのに使うタグは適当でいい
ように見えるかもしれませんが、これにはちゃんと理由があります: このブラン
チをあとでアクセスするためのラベルとしてこのタグ名は使われます。ブランチ
用のタグはブランチでないタグと変わらないように見えますし、同じネーミング
制限に従っています。ブランチのタグ名には必ず「branch」という単語を居れる
ようにしている人もいます(たとえばRelease-1999_05_01-bugfix-branch
のように)。こうするとブランチタグとほかの種類のタグを区別できます。よく
タグ名をまちがってアクセスしてしまうようなら、そういう風にするのもよいで
しょう。
(あと、最初のコマンドでチェックアウトに -d myproj_old_release というオプ ションがつけてあることに注意してください。これはチェックアウトのときに、 作業コピーを myproj_old_release という名前のディレクトリに置くことを指示 するもので、こうしておくと myproj にある現在のバージョンと混同してしまう こともありません。グローバルオプションの -d や、update の -d と混同しな いようにしてください。)
もちろん、単に tag コマンドを実行したからといってこの作業コピーがブラン チに切り替わってしまったりするわけではありません。タグ付けは作業コピーに は影響ありません。作業コピーのリビジョンにあとでアクセスできるように、リ ポジトリ内にちょっとした情報を記録するだけです(履歴内の1コマとして、ある いはブランチとして、場合によります)。
2つのうちどちらかの方法でアクセスできます(もうこのフレーズはそろそろ耳タ コかなあ)。ブランチ上の新しい作業コピーをチェックアウトしましょう:
floss$ pwd /home/whatever floss$ cvs co -d myproj_branch -r Release-1999_05_01-bugfixes myproj
あるいは、今ある作業コピーをそっちに切り替えますか:
floss$ pwd /home/whatever/myproj floss$ cvs update -r Release-1999_05_01-bugfixes
結果は同じです(新しい作業コピーのトップレベルディレクトリの名前は違いま すが、CVS に関してはあまり重要ではないですね)。現在の作業コピーに未コミッ トの変更があれば、ブランチにアクセスするのに update ではなくて checkout を使いたいだろうと思います。でないと、ブランチに切り替えようとした際、作 業コピーに対して変更をマージしようとしてしまいます。この場合、コンフリク トが起こるかもしれませんし、起こらなくても純粋でないブランチになります。 これでは、作業コピー中のいくつかのファイルは変更されたままの状態なので、 プログラムは指定されたタグをちゃんと反映していないことになります。
とにかく、どちらかの方法でお望みのブランチの作業コピーを取得できたとしま しょう:
floss$ cvs -q status hello.c =================================================================== File: hello.c Status: Up-to-date Working revision: 1.5 Tue Apr 20 06:12:56 1999 Repository revision: 1.5 /usr/local/cvs/myproj/hello.c,v Sticky Tag: Release-1999_05_01-bugfixes (branch: 1.5.2) Sticky Date: (none) Sticky Options: (none) floss$ cvs -q status b-subdir/random.c =================================================================== File: random.c Status: Up-to-date Working revision: 1.2 Mon Apr 19 06:35:27 1999 Repository revision: 1.2 /usr/local/cvs/myproj/b-subdir/random.c,v Sticky Tag: Release-1999_05_01-bugfixes (branch: 1.2.2) Sticky Date: (none) Sticky Options: (none) floss$
(これらの Sticky Tag
行の内容を手短に説明します)hello.c と
random.c を変更したら、コミットをかけます
floss$ cvs -q update M hello.c M b-subdir/random.c floss$ cvs ci -m "fixed old punctuation bugs" cvs commit: Examining . cvs commit: Examining a-subdir cvs commit: Examining a-subdir/subsubdir cvs commit: Examining b-subdir Checking in hello.c; /usr/local/cvs/myproj/hello.c,v <- hello.c new revision: 1.5.2.1; previous revision: 1.5 done Checking in b-subdir/random.c; /usr/local/cvs/myproj/b-subdir/random.c,v <- random.c new revision: 1.2.2.1; previous revision: 1.2 done floss$
リビジョン番号を見ると、なんだかおもしろいことが起こっている様子なのに気 づきませんか:
floss$ cvs -q status hello.c b-subdir/random.c =================================================================== File: hello.c Status: Up-to-date Working revision: 1.5.2.1 Wed May 5 00:13:58 1999 Repository revision: 1.5.2.1 /usr/local/cvs/myproj/hello.c,v Sticky Tag: Release-1999_05_01-bugfixes (branch: 1.5.2) Sticky Date: (none) Sticky Options: (none) =================================================================== File: random.c Status: Up-to-date Working revision: 1.2.2.1 Wed May 5 00:14:25 1999 Repository revision: 1.2.2.1 /usr/local/cvs/myproj/b-subdir/random.c,v Sticky Tag: Release-1999_05_01-bugfixes (branch: 1.2.2) Sticky Date: (none) Sticky Options: (none) floss$
数字が2つではなくて、4つになっています!
よく見ると、各ファイルのリビジョン番号はブランチ時の番号(
Sticky Tag
行に示されています)の最後に余分な数字をつけ加えて
あるようですね:
いま見ているのは CVS の内部作業の片鱗です。ほとんどいつもは、プロジェク
ト全体の分岐をマークするためにブランチを使いますが、CVS は実際はファイル
毎にブランチを記録しています。このプロジェクトには、このブランチの分岐時
に5つのファイルがあったので、5つの個別のブランチが作成され、みな同じタグ
名がつけられました(Release-1999_05_01-bugfixes
)。
CVS の実装のうち、このようなファイルごとのやりかたは、少々エレガントでな いという意見が多いです。これは RCS の遺物なのです、RCS ではファイルをま とめてプロジェクトにしたりできませんでした。CVS は、RCS のブランチを扱う ところのコードを受け継いでいるのでこうなっているのです。
通常は CVS がファイル等を内部的にどのように追跡しているかなんてあまり考 える必要はありませんが、今回の場合は、ブランチ番号とリビジョン番号の関係 について理解する助けになります。hello.c を見ていきましょう、これから hello.c に関して言うことは、ブランチの他のファイルについてもあてはまりま す(リビジョン/ブランチ番号は適当に読み替えてください)
hello.c のリビジョンは、ブランチが根を下ろした点で1.5でした。ブランチを
作成した時、その端には番号がつけられてそれがブランチ番号になります(CVS
は、まだ使われていない非ゼロの偶数整数値をひとつ選んでつけます)。そうす
るとこの場合、ブランチ番号は 1.5.2
となります。ブランチ番号そのも
のはリビジョン番号ではないですが、ブランチ番号はそのブランチ上の hello.c
のリビジョン番号の根(プレフィクス)になります。
しかし、ブランチ後の作業コピーで status コマンドを実行すると、hello.c の
リビジョン番号は単に 1.5
となっていて、1.5.2.0
とかではな
いようです。これは、ブランチ上の最初のリビジョン番号というのは、枝分かれ
した点のトランク上のリビジョン番号とつねに同じだからです。ですから、その
ファイルがブランチ上で変更されずにトランク上のと同じあいだは、CVS は
status の出力にトランクのリビジョン番号を出力します。
hello.c の新しいリビジョンをコミットすれば、hello.c はトランク上とブラン
チ上で違ってしまいます。ブランチ上のファイルは変わってしまいますが、トラ
ンク上のファイルは変わりませんから。従って、ブランチ上の hello.c には最
初のブランチリビジョン番号が割り当てられます。コミットしたあとに status
の出力を見れば、リビジョン番号が 1.5.2.1
になったことがわかります。
random.c ファイルについても同じことが言えます。ブランチ時点でのリビジョ
ン番号は 1.2
ですから最初のブランチは 1.2.2
、random.c をブ
ランチ上に最初にコミットした時には 1.2.2.1
となります。
1.5.2.1
と 1.2.2.1
の間に数字的なつながりはなにもありませ
ん。同じ時にブランチしたものだということすらわかりません。両者に
Release-1999_05_01-bugfixes
というタグがあって、そのタグはそれぞ
れ 1.5.2
と 1.2.2
というブランチ番号についている、というこ
と以外には。従って、プロジェクト全体にわたるものとしてブランチを指定する
ためにはタグ名を使うしかありません。リビジョン番号を直接指定してファイル
をブランチ上のものにすることは可能ですが、
floss$ cvs update -r 1.5.2.1 hello.c U hello.c floss$
これはよくないやりかたです。ブランチリビジョンのファイルを1つ、他のブラ ンチでないリビジョンのファイルと混ぜてしまうことになります。その結果どん な損失があるかわかりません。ブランチにアクセスするときはブランチタグを使 ってすべてのファイルに一度にアクセスすることです。特定のファイルだけ指定 してはいけません。そうすれば、各ファイルの実際のブランチリビジョン番号に ついて知らなくても気にしなくてもかまいません。
ブランチを持つブランチを作ることもでき、不合理なくらいのレベルさえ可能で
す。たとえば、あるファイルがリビジョン番号 1.5.4.37.2.3
の場合、
次の図のような履歴になっていると思います:
1.1 | 1.2 | 1.3 | 1.4 | 1.5 / \ / \ / \ (1.5.2) (1.5.4) <--- (these are branch numbers) / \ 1.5.2.1 1.5.4.1 | | 1.5.2.2 1.5.4.2 | | (etc) (...) <--- (collapsed 34 revisions for brevity) | 1.5.4.37 / / (1.5.4.37.2) <--- (this is also a branch number) / / 1.5.4.37.2.1 | 1.5.4.37.2.2 | 1.5.4.37.2.3 [Figure 2.4: An unusually high degree of branching. Time flows downward.]
こんなに深いブランチを作るような状況になることはめったにありませんが、し
たいと思ったときにはいつでもできるのです。普通のブランチと同じように、ネ
ストしたブランチを作成することもできます。ブランチ N
の作業コピー
をチェックアウトして、その中で cvs tag -b branchname を実行すれば、ブラ
ンチ N.M
がリポジトリの中にできます(N
は各ファイルのブラン
チリビジョン番号で(1.5.2.1
など)、M
はその番号で終わる次の
ブランチを表しています(2
など))。