Mercurialで快適変更管理

Mercutialの勧め

Mercurialはバージョン管理システムである。

ソフトウェアをチームで開発する場合は必須のツールで、かなり長い間CVS(Concurrent Version System)が採用されることが多かった。CVSはとても便利で、今も私が参加しているチームでは主役だ。しかし、CVSには欠点が多く、決定的な事はチェンジセットをサポートしていない事だと思う。チェンジセットとは複数のファイルをひとつの変更単位として扱うことである。CVSはファイル単位でしか管理することができない。(CVSの成り立ちからそうならざるを得なかった。)だから、CVSでは案件別に変更を管理しようとすると運用が難しくなってしまうのだ

Mercurialはこのチェンジセットという更新単位をサポートする。以下はバージョン管理システムの基本的な動作をチェンジセットでどのように管理されるのかを具体例で説明する。

Mercurialでチェンジセットの恩恵にあずかろう!

始めの一歩は、リポジトリ作成から

リポジトリを作成してみる。mercurialはhgコマンドの後にサブコマンドを付けることで動作を指示する。リポジトリ作成のサブコマンドは”init”だ。

$ hg init
$

実行したディレクトリに”".hg”というフォルダができる。これがリポジトリのフォルダだ。リポジトリの管理範囲は、”.hg”ディレクトリのあるディレクトリとなる。

リポジトリ作成後でhello.cというC言語のソースコードファイルをサンプルとして作成した。このファイルは出来たてほやほやで、リポジトリにはまだ登録されていない。

$ ls -la
合計 32
drwxrwxr-x  3 t-randt t-randt 4096 10月  5 01:28 .
drwxrwxr-x 29 t-randt t-randt 4096 10月  5 01:19 ..
drwxrwxr-x  3 t-randt t-randt 4096 10月  5 01:28 .hg
-rw-rw-r--  1 t-randt t-randt   96 10月  5 01:22 hello.c

ファイルを追加してみる

hello.cをリポジトリに追加する。hg addコマンドを実行したあと、hg statusで確認。”A”とマーキングされている。追加対象のファイルであることを教えてくれる。

$ hg add hello.c
$ hg status
A hello.c

hg commtコマンドで追加を確定させる。これでリポジトリにhello.cが追加された。hg statusで確認。追加されてしまったので今度は何も表示されない。

$ hg commit
$ hg status
$

hg commit実行後チェンジセットID=0:a85eea9d6407が付けられる。今回の変更分はこのIDで識別できる。チェンジセットIDを確認するにはhg logコマンドを実行。

$ hg log
changeset:   0:a85eea9d6407
tag:         tip
user:        XXXXXXX
date:        Fri Oct 05 01:40:47 2007 +0900
summary:     initial

$

ファイルを変更してみる

hello.cを変更した後hg statusコマンドを実行すると、hello.cが変更対象として”M”マーキングされていることがわかる。

$ hg status
M hello.c
$

この状態で、hg diffコマンドを実行すると、変更の内容が表示される。printf(”Hello Tohru!!\n”);の行がhello.cに追加されていることが分かる。hg diffコマンドはdiff形式で変更内容を表示してくれる。

$ hg diff
diff -r a85eea9d6407 hello.c
--- a/hello.c   Fri Oct 05 01:40:47 2007 +0900
+++ b/hello.c   Fri Oct 05 01:43:54 2007 +0900
@@ -4,5 +4,6 @@ int main(int argc, char ** argv)
 {
        printf("Hello World!!\n");
        printf("Hello Mercurial\n");
+       printf("Hello Tohru!!\n");
        return 0;
 }

変更をコミットしてhg logで確認。今回の変更にチェンジセットID=1:c3528466158aが付けられた。

$ hg commit
$ hg log
changeset:   1:c3528466158a
tag:         tip
user:        XXXXXXXX
date:        Fri Oct 05 01:45:56 2007 +0900
summary:     modify
changeset:   0:a85eea9d6407
user:        XXXXXXXX
date:        Fri Oct 05 01:40:47 2007 +0900
summary:     initial

サブディレクトリにあるファイルを追加してみる

当然ではあるが、サブディレクトリにあるファイルも同様に追加できる。

$ pwd
/home/t-randt/develop/sample/mercurial/include
$ ls
commonfunc.h  commonfunc.h~
$ hg add commonfunc.h
$ hg status
A include/commonfunc.h
? include/commonfunc.h~
$ hg commit
No username found, using 'XXXX' instead
$ hg status
? include/commonfunc.h~
$ hg log
changeset:   2:f5c9ddb26835
tag:         tip
user:        XXXXXX
date:        Sat Oct 13 00:44:05 2007 +0900
summary:     add file under subdirectory
changeset:   1:c3528466158a
user:        XXXXXX
date:        Fri Oct 05 01:45:56 2007 +0900
summary:     modify
changeset:   0:a85eea9d6407
user:        XXXXXX
date:        Fri Oct 05 01:40:47 2007 +0900
summary:     initial

ファイルの削除をしてみる

チェックイン済のファイルを削除するというのは、リポジトリに「削除」という変更をすることになりる。バージョン管理システムは「削除」前のバージョンに戻る事ができなければいけないから、完全にファイルが「削除」されることはない。あるバージョンからは「削除」されたファイルの管理をしないという変更を行うことになる。

先ほど追加したcommonfunc.hファイル削除してみる。commonfunc.h自体をrmコマンドで削除すると、hg stausは”!”マーキングで管理対象ファイルが無いこと教えてくれる。この状態は単に管理されているファイルが無いだけで、実際に「削除」されてはいるわけではない。

$ ls
commonfunc.h  commonfunc.h~
$ rm *
$ ls
$ hg status
! include/commonfunc.h
$

hg remove [ファイル名]コマンドで「削除」を実行する。実行後、hg statusで確認すると対象ファイルに”R”マークが付く。

$ hg remove
abort: no files specified
$ hg remove commonfunc.h
$ hg status
R include/commonfunc.h
$

削除をチェックインしてhg logで確認。削除にもチェンジセットID=3:3d55316ca048が付けられた。

$ hg commit
No username found, using 'XXXXXX' instead
$ hg status
$ hg log
changeset:   3:3d55316ca048
tag:         tip
user:        XXXXXXX
date:        Sat Oct 13 00:56:01 2007 +0900
summary:     remove the file
changeset:   2:f5c9ddb26835
user:        XXXXXXX
date:        Sat Oct 13 00:44:05 2007 +0900
summary:     add file under subdirectory
changeset:   1:c3528466158a
user:        XXXXXXX
date:        Fri Oct 05 01:45:56 2007 +0900
summary:     modify
changeset:   0:a85eea9d6407
user:        XXXXXXX
date:        Fri Oct 05 01:40:47 2007 +0900
summary:     initial

コミット前の状態を元に戻す

コミットする前の状態であれば変更内容を無かったことにすることができる。例えば、誤ってremove操作したファイルは”R”対象からはずさなければいけない。そんな時はhg revertコマンドを実行する。もちろん編集”M”や、追加”A”も取り消せる。

$ hg remove include/commonfunc.h
$ hg status
R include/commonfunc.h
$ hg revert --all
undeleting include/commonfunc.h
$ hg status
$ hg commit
nothing changed
$

TAGを打ってみる

TAGはチェンジセットに対する名前である。チェンジセットは通常英数文字の羅列であるIDで管理されているので識別しずらいという方もいるだろう。TAGを打つことでわかりやすくすることができるし、独自のバージョンNoで管理するという運用も可能となる。TAGを打ってあると、TAGでその時点のソースコードを取り出すことができる。

TAGを表示するコマンドはhg tagsだ。まだTAGを打っていない状態だが、”tip”というTAGが表示される。”tip”というのは特別で、常に最新のチェンジセットに付けられる名前だ。だから”tip”というTAGを打つことはできない。TAGを打つコマンドはhg tag [TAG名]だ。

$ hg tags
tip                                4:1267ec260381
$ hg tag tip
abort: the name 'tip' is reserved
$

TAG1というTAGを打ってみる。4:1267ec260381チェンジセットに”TAG1″という名前を付けることができた。TAGを打つと新たにチェンジセットIDが付けられる。hg tagsを見ると、”tip”タグが新しいチェンジセットIDになっているのが分かる。TAGを管理するために”.hgtags”というファイルがリポジトリに追加されるので、チェンジセットがひとつ増えることになるのだ。次からTAGを作成する度にこのファイルが更新され、チェンジセットもその都度増えていく。(しかたがないが、これは運用上あまり嬉しくないことだ。)

$ hg tag TAG1
$ hg tags
tip                                5:8cedd42fdd57
TAG1                               4:1267ec260381
$

ブランチ(コピー)してみる

開発のために現在のソースコードはそのままにしておいて、別の場所で更新内容を管理したいことはよくある。たとえば、「安定版」系、「開発版」系などに役割を分担し、それぞれの更新内容を管理したい場合だ。

CVSではブランチを作成することで実現している。ブランチとは更新対象となる系列から分岐して新たに設けた系列のこと。同じファイルを2系列で更新することが可能となる。もちろん、相互に更新内容をマージすることも可能である。

Mercurialでも同様の事ができる。ただ、cvsに慣れている方は理解するのに少し時間がかかるかもしれない。

cvsではファイル単位の管理となっていて、ブランチもファイル単位のリビジョン番号で管理されている。ファイル単位で全ての更新を串刺しして表現することが可能なのだ。1.0のブランチは1.0.0、1.1のブランチは1.1.0という具合だ。これって変更管理の立場からはとってもわかりやすい。

さてさて、Mercurialの分岐はリポジトリのコピーである。リポジトリが2つになるので、それぞれに更新管理が可能という事だ。これってあとで難しいことにならないだろうか?

CVSの恩恵にどっぷり浸っていた方は心配になると思う。かくいう私も心配だった、うまく管理できるのだろうかと。しかし、Mercurialはリポジトリ関でマージができるので、良く考えれば何の問題もない。実際の現場では、おそらく「マスタ」リポジトリと、それから分岐した「開発用」リポジトリという具合で運用となるのではないだろうか。

分岐してみる

では、分岐してみる。分岐にはhg clone [元] [先]コマンドを実行する。例ではmercurialディレクトリをmercurila_cloneディレクトリにコピーしている。

$ hg log
changeset:   5:8cedd42fdd57
branch:      test
tag:         tip
user:        XXXXXXX
date:        Fri Nov 02 18:51:24 2007 +0900
summary:     Added tag TAG1 for changeset 1267ec260381
 :
$ cd ..
$ hg clone mercurial mercurial_clone
3 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd mercurial_clone/
$ ls -la
合計 28
drwxrwxr-x  4 t-randt t-randt 4096 12月 29 02:48 .
drwxrwxr-x 36 t-randt t-randt 4096 12月 29 02:48 ..
drwxrwxr-x  3 t-randt t-randt 4096 12月 29 02:48 .hg
-rw-rw-r--  1 t-randt t-randt   46 12月 29 02:48 .hgtags
-rw-rw-r--  1 t-randt t-randt  154 12月 29 02:48 hello.c
drwxrwxr-x  2 t-randt t-randt 4096 12月 29 02:48 include

hg logを確認すると、コピー元と全く同じ表示結果となる。

$ hg log
changeset:   5:8cedd42fdd57
branch:      test
tag:         tip
user:        XXXXXXX
date:        Fri Nov 02 18:51:24 2007 +0900
 :

分岐先リポジトリでファイルを更新する

分岐先リポジトリで、hello.cファイルの更新を行います。

$ pwd
/home/t-randt/develop/sample/mercurial_clone
$ vi hello.c
$ hg status -m
M hello.c
$ hg ci
No username found, using 'XXXXXXX' instead
$ hg log -r 6
changeset:   6:79715a23ef0f
branch:      test
user:        XXXXXXX
date:        Sat Dec 29 03:17:31 2007 +0900
summary:     hello.c modify after clone

今回の変更内容の親チェンジセットを確認しておく。hg parentsコマンドはチェンジセットの親チェンジセットを確認できる。

$ hg parents -r 6
changeset:   5:8cedd42fdd57
branch:      test
user:        XXXXXXX
date:        Fri Nov 02 18:51:24 2007 +0900
summary:     Added tag TAG1 for changeset 1267ec260381

分岐元リポジトリでファイルを更新する

分岐元でも同じhello.cファイルを更新する。

$ cd ..
$ cd mercurial
$ pwd
/home/t-randt/develop/sample/mercurial
$ vi hello.c
$ hg status -m
M hello.c
$ hg ci
No username found, using 'XXXXXXX' instead
$ hg log -r 6
changeset:   6:18a85a40a048
branch:      test
user:        XXXXXXX
date:        Sat Dec 29 03:32:37 2007 +0900
summary:     hello.c modify after clone

最新のチェンジセットIDは分岐先と元で異なる。けれど、分岐するまえのチェンジセット、つまり、親チェンジセットは両リポジトリとも同じ物が表示されている。つまり、元は同じ物だ。

$ hg parents -r 6
changeset:   5:8cedd42fdd57
branch:      test
user:        XXXXXXX
date:        Fri Nov 02 18:51:24 2007 +0900
summary:     Added tag TAG1 for changeset 1267ec260381

分岐元の変更内容を、分岐先に反映する

分岐先リポジトリでの更新内容を分岐元に反映するためには、まず、分岐先の変更内容を分岐元に反映して変更内容の同期をとることが必要だ。想定シチュエーションは開発が済んだのでリリース用のマスタリポジトリにマージ作業が必要な状態だ。

分岐元のリポジトリで、分岐先の変更内容を取得する。hg pull [リポジトリ]コマンドでリポジトリの変更内容を持ってくる事ができる。

$ cd ..
$ cd mercurial_clone/
$ pwd
/home/t-randt/develop/sample/mercurial_clone
$ hg pull ../mercurial
pulling from ../mercurial
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
(run 'hg heads' to see heads, 'hg merge' to merge)

pullコマンドを実行すると分岐元の更新の情報が得られるが、更新情報は分岐先で新しいチェンジセットとなる.
hg statusで確認するとparentは分岐時のチェンジセットになっている。

$ hg status
$ hg log
changeset:   7:18a85a40a048
branch:      test
tag:         tip
parent:      5:8cedd42fdd57
user:        XXXXXXX
date:        Sat Dec 29 03:32:37 2007 +0900
summary:     hello.c modify after clone
 
changeset:   6:79715a23ef0f
branch:      test
user:        XXXXXXX
date:        Sat Dec 29 03:17:31 2007 +0900
summary:     hello.c modify after clone
 
changeset:   5:8cedd42fdd57
branch:      test
user:        XXXXXXX
date:        Fri Nov 02 18:51:24 2007 +0900
summary:     Added tag TAG1 for changeset 1267ec260381
 

この状態はまだ分岐元の更新内容がhello.cファイルに反映されていない。単に更新情報があるだけだ。hello.cに対してhg mergeコマンドでマージ作業を行う。

$ hg update
abort: update spans branches, use 'hg merge' or 'hg update -C' to lose changes
$ hg merge
merging hello.c
conflicts detected in /home/t-randt/develop/sample/mercurial_clone/hello.c
0 files updated, 1 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)
$

マージを行うと、hello.cファイルは”M”マーキングされる。チェックインすれば、分岐元の変更が分岐先に反映する。

$ hg status
M hello.c
$ hg ci
No username found, using 'XXXXXXX' instead
$ hg status

マージしたファイルは新たなチェンジセットとしてコミットされる。マージ元のチェンジセットが2つあるので、parentが2つ表示される。例は、チェンジセット8:06846c7cc667が、分岐先の更新内容6:79715a23ef0fと、分岐元の更新内容7:18a85a40a048のマージ結果であることを表示している。

$ hg log -r tip -r 7 -r 6
changeset:   8:06846c7cc667
branch:      test
tag:         tip
parent:      6:79715a23ef0f
parent:      7:18a85a40a048
user:        XXXXXXX
date:        Sat Dec 29 04:15:40 2007 +0900
summary:     merge from parent repo
 
changeset:   7:18a85a40a048
branch:      test
parent:      5:8cedd42fdd57
user:        XXXXXXX
date:        Sat Dec 29 03:32:37 2007 +0900
summary:     hello.c modify after clone
 
changeset:   6:79715a23ef0f
branch:      test
user:        XXXXXXX
date:        Sat Dec 29 03:17:31 2007 +0900
summary:     hello.c modify after clone
 

分岐先の変更内容を、分岐元に反映する

同期をとった後、分岐先リポジトリの変更内容を、分岐元リポジトリにも反映する。hg push [リポジトリ]コマンドを実行する。

$ hg push ../mercurial
pushing to ../mercurial
searching for changes
adding changesets
adding manifests
adding file changes
added 2 changesets with 2 changes to 1 files

では、分岐元に行って確認してる。分岐先の更新内容がチェンジセット: 7:79715a23ef0fとして登録され、マージの内容がチェンジセット: 8:06846c7cc667として登録されている。これで、両リポジトリ間の同期は完了だ。

$ cd ..
$ cd mercurial
$ pwd
/home/t-randt/develop/sample/mercurial
$ hg status
$ hg log -r tip -r 7 -r 6
changeset:   8:06846c7cc667
branch:      test
tag:         tip
parent:      7:79715a23ef0f
parent:      6:18a85a40a048
user:        XXXXXXX
date:        Sat Dec 29 04:15:40 2007 +0900
summary:     merge from parent repo
 
changeset:   7:79715a23ef0f
branch:      test
parent:      5:8cedd42fdd57
user:        XXXXXXX
date:        Sat Dec 29 03:17:31 2007 +0900
summary:     hello.c modify after clone
 
changeset:   6:18a85a40a048
branch:      test
user:        XXXXXXX
date:        Sat Dec 29 03:32:37 2007 +0900
summary:     hello.c modify after clone

以上で具体例は終わりである。チェンジセットによってMercurialがどう変更管理しているか理解できたと思う。バージョン管理システムの基本部分を備えている事はCVSもMercurialも同じであるけれど、ソースコードの変更管理を運用していくからには、チェンジセットを扱えるMercurialの方に分がある。

チェンジセットを扱えるバージョン管理システムとして普及しつつあるもうひとつのシステムがSubversionである。MercurialとSubversionの違いを次で説明する。

Mercurilは「分散型」である

CVSに替わるバージョン管理システムが幾つか存在している。それらは大きく2つに分類できる。リポジトリを一所に置いて管理する「集中型」と、開発者が個々にリポジトリを持ちリポジトリ間で変更を調整できる「分散型」である。

「集中型」はリポジトリからソースコードをチェックアウトする必要がある。チェックアウトした瞬間からリポジトリから切り離され、あとの変更管理はそれぞれの方法で行う。変更が完了したらチェックイン(コミット)し、この時点でリポジトリが更新される。リポジトリが更新されれば、他の開発者が変更内容をチェックアウトできるようになる。

「分散型」はリポジトリからソースコードを切り離す必要が無い。リポジトリ自体をコピーするからだ。変更者は手元にリポジトリを置いておくことができるので、コピーされたリポジトリに変更内容をいつでもチェックインできる。そして、変更完了後にコピー元のリポジトリと、手元のリポジトリ間で変更内容を同期する。(必要がなければそのままでも良い)。

「分散型」のアドバンテージは、開発者間で変更内容を同期できることである。ひとつの開発案件を複数の開発者で担当し、それぞれの変更が完了した時点で結合テストを行いたい事もあるだろう。そのケースでは、開発者間でリポジトリをマージしあって同期すれば良い。結合テスト完了後コピー元のリポジトリと同期することになる。

The Internetが当たり前の世の中、開発チームは一所に集まる必要がなかったり、それによって、開発を行う時間帯も様々であったりというケースがある。そんな開発環境に「集中型」は向かない様である。開発者が分散していると「集中型」は取扱いに苦労するのだろう。特に開発者間で変更内容をマージできるのは便利なのだと思う。

なので、最近は「分散型」のバージョン管理システムが採用されることが多くなってきている。例えば、Linuxのソースコード管理はgitで行われているが、gitは「分散型」に分類される。

「分散型」であるというのもMercurialをお勧め重要な要素だ!

参考リンク

ご意見、ご指摘はこちらへ

この文章はbaeが書いています。有用な情報となるよう心がけていますが、利用される方は個人の責任でお願いします。お気付きの点、また、ご意見ありましたらご一報ください。公開鍵サーバに宛先を登録していますのでそちらを参照ください。

公開鍵情報