2010年12月29日水曜日

操作体系から見る、GitとMercurialの8つの違い

このエントリーをはてなブックマークに追加
Clip to Evernote
Pocket

つい先日、SVNからMercurialに移行するべき8つの理由をまとめたが、Twitterやはてなブックマークのコメントを見ていると、同じ分散バージョン管理システムとしてGitとMercurialとの比較に関心が高く、Windowsでの動作でMercurialを評価する人が多いように感じられた。

それも一つの側面で間違いでは無いのだが、日々の開発作業で使っていくと、むしろ操作体系の方が気になるものだ。GitとMercurialの両方を使う機会があったので、操作体系の面で気づいた違いを列挙した上で、Gitに対するMercurialの優位点を考察してみる。

1. 管理対象ファイルの指定方法

.gitignoreや.hgignoreで管理外のファイル名を指定でき、正規表現も使える点は良く似ている。

しかしGitはcommit前にコミット対象を毎回git-addで指定するが、Mercurialは一度hg-addをしたらその後は自動的にcommit対象となる。編集中で対象に含めたく無い場合は、逆にMercurialはhg-forgetで、除外することを毎回指定する必要がある。

なお、Git自体はgit addしたときに変更ファイルはstagedされており、git add後にファイルに変更を加えてもcommitには反映されない。Mercurialにはこの概念は無いようで、扱いがシンプルだ。

2. コミット(changeset)の指定方法

Gitはハッシュ値のIDでコミットを管理している。Mercurialも同様のアプローチをとっているが、ハッシュ値の入力は一部で良くても煩わしいときもある。Mercurialでは、changesetに連番のリビジョン番号もふられており、簡単な数字で各種の操作を行う事ができる。

3. ソースコード分岐の管理方法

GitとMercurialを両方使うと、ソースコードの分岐方法と、分岐したソースコードの指定方法の違いが最も気になる。

Gitでコードを分岐させる場合、git-branchで明示的にブランチを作成する必要があり、ブランチ名が管理に必要となっている。マージや古いchangesetの展開ではブランチの指定が必ず必要になる。また、ブランチは、ブランチのコピーとして作成される。ブランチの消去は、他のブランチに影響を与えない。以下の図でブランチ#2を削除しても、#1と#3には影響が無い。

Mercurialは、hg-updateで分岐元になるchangesetを展開したあとに、それを編集してcommitするだけで、自動的に無名ブランチ(un-named branch)を作る事ができる。マージなどの操作では、changesetのリビジョン番号とハッシュ値のどちらかを指定すればよい。Mercurialの無名ブランチは、changesetの親子関係でしかない。ゆえにchangesetの削除はリポジトリ全体に大きな影響を及ぼす。以下の図で、もし(4)を削除すると、その子孫である(5)(6)(7)(8)も削除される事になる。

さらにMercurialは、同じchangesetに対する変更は、無名ブランチで自動分岐されるので、push/pull時に衝突が発生することがない。構造上、changesetの同期とマージ作業が分離されている。Gitも開発者ごとにブランチを分けるなどの運用でpush時の衝突を回避できるが、Mercurialはシステム的にchangesetの同期が円滑に行くようになっている。

4. ブランチの性質

GitとMercurialでは、操作でのブランチの必要性だけではなく、ブランチの性質そのものも異なっている。

4.1. 名前付ブランチ
Mercurialにも名前付ブランチがある。commit前に行うhg-branchでブランチ名を変更するのだが、このブランチ名は全ての子changesetに引き継がれる。つまり名前付ブランチ内で、無名ブランチで分岐が発生する場合は、複数の分岐コードが同一のブランチ名を持つ。名前付ブランチは、分岐を表すものではなく、あるchangesetの子孫の集合だと考えた方が良い。子孫集合の中で最も新しいchangesetを指すために、名前付ブランチは用いられる。
4.2. ブランチの削除
Gitのブランチは消すことができるが、Mercurialのブランチは消すことができない。Mercurialのブランチはchangesetの別名なので、消すとその子孫のchangesetに影響が出る可能性があるためだと考えられる。
不要なchangesetがリポジトリに残る可能性もあるが、特定のリビジョンを指定したhg-cloneを行うと、そのリビジョンに関係の無いchangesetはコピーされないため、不要なchangesetは消去することができる。
4.3. tagとbookmark
Mercurialでは、tagとbookmarkによって、リリース・バージョン等に目印をつけることもできる。ただし、Mercurialのtagは異なるリポジトリ間で共有ができ、Mercurialはbookmarkが、Gitのtagと基本的に同じ機能となっている。
4.4. 他のリポジトリの更新を取り込む(pull)時の挙動
MercurialはGitと異なり、チェンジセットをpullしてきても、自動的にワーキングツリーに反映しない。Mercurialでpullしたチェンジセットは、編集中のファイルの親になるとは限らないからだ。マージを行うには、編集中のワーキングツリーを一度コミットした後に、hg-mergeを行う必要がある。
Gitでは、ブランチ自体は一本道であるため、編集中のファイルはpullしたチェンジセットの子供になる。そのため、pullした内容は編集中のワーキングツリーに自動的に反映される。

追記(2011/08/17 01:28):rebase拡張を有効にして、hg pull --rebaseを行うと、MercurialもGitのようにpullする事ができる。

5. コミットの修正方法

コミットの修正方法は、GitとMercurialの性質の違いが端的に分かる部分である。取り消し、やり直し、メッセージ変更が、以下のように異なる。

5.1. コミットの取り消し
Gitはgit-reset、Mercurialはhg-rollbackで取り消しを行える。ただし、git-resetは任意の過去のバージョンまでやり直しを行えるが、hg-rollbackは直前のコミットを取り消す事しかできない。
Mercurialでやり直しをする場合は、基本的に古いchangesetを消すのではなく、過去のchangesetからの派生コードを新たに無名ブランチを作成することになる。
ただし、拡張機能MQのhg-stripを使えば、特定changesetとその子孫を履歴から消去することができる。もしくは、hg-cloneで特定のchangesetを指定すると、そのchangesetにchangesetはcloneされないため、古いバージョンのchangesetを指定すれば、それ以降のコミット履歴は全て消すことができる。
5.2. コミットのやり直し
Gitはgit-resetで古いバージョンに戻しすぎたときに、git-reflogでREDO可能な情報を確認し、git resetを再実行することで、コミットをやり直す事ができる。
Mercurialでは、そもそも過去に遡ってcommitを取り消す事ができないので、コミットのやり直しが必要になるケースは恐らく無い。拡張機能MQのhg-stripを使った場合は、hg-unbundleで行う事ができる。
5.3. コミット・メッセージの変更
Gitにはgit-amendがあり、直前のコミット・メッセージを変更できる。
Mercurialには該当するコマンドは無いが、hg-rollbackでやり直すのは難しくない。hg-rollbackを行ってもワークツリーのファイルは変更されないし、Mercurialのコミット対象ファイルの指定は半自動だから、git-addのようにファイル指定のやり直しも無い。

6. 高度な管理機能

GitとMercurialでは以下のように、それぞれ特徴とされる機能がある。

6.1. 同時にマージできるブランチの数
Gitは同時にマージできるブランチの数に制限は無いが、Mercurialはマージできるchangesetは二つまでとなっている。大規模プロジェクトで、平行した複数の更新が行われる場合は、Gitの方が効率的かも知れない。
6.2. git-cherry-pick
Gitにある他のブランチから特定のコミットのみを反映する機能だが、Mercurialにはデフォルトでは無い。ただし、拡張機能でhg-transplantを使うか、該当changesetのpatchを作成し、それを取り込むことで同様の事は実現はできる。
6.3. git-submodule
リポジトリが依存するライブラリ等の外部リポジトリを登録する機能。複数のプロジェクトが連係するときに効果を発揮すると考えられる。外部プロジェクトのバージョンによって、主プロジェクトのソースコードを変更する必要があるときに、二つのプロジェクトのバージョンの対応関係をGitで管理することができる。Mercurialだと、拡張機能Subrepositoryで同様のことができる。
6.4. git-merge --squash
複数のchangesetを一つのchangesetに統合して、マージを行う。履歴が消失するデメリットもあるが、CTRL+sを押すようにコミットをする人々には便利な機能なはずだ。Mercurialだと、拡張機能MQで複数のchangesetをパッチ形式にしてから統合することができる。
6.5. hg-backout
以前の特定のchangesetによる変更を、打ち消す事ができる機能。ただしpatchを作ってリバースをすれば無くても同様の事が可能になる。

7. オプション的な機能の実現方法

Perlスクリプト等を書いて、Gitのフィルター機能やフック機能で連係しないと出来ない事が、Mercurialだと.hgrc(WindowsではMercurial.ini)で拡張機能を設定するだけで簡単に行う事ができる。

7.1. パッチマネージャ
データベースの接続情報やホスト名などを、環境依存の情報がソースコード中に書かれている場合は、ローカルのワークスペースでは情報を書き換える必要がある。しかし、書き換えた情報をcommitしてしまうと、他のプログラマのリポジトリに混乱を与える事になる。
以前、GitでPHPのソースコードを管理したときは、サーバーとローカルの設定情報の差分でpatchを作成し、共有リポジトリからpullして来たときにpatchをあて、commitする前にpatch -Rを行って元に戻していた。もちろん作成したpatchは、バージョン管理システム外におかれる。
Mercurialでは、このpatchによる整合性維持を拡張機能であるMerqurial MQによって行う事ができるので、バージョン管理システムでpatchも管理できる。なお、GitにもMQを移植したguiltがあるが、Windows環境で動くmsysGitには含まれていないようだ。
7.2. キーワード展開
GitでもMercurialでも非推奨の機能なのだが、SVNができたので$Id$や$Date$をリビジョン番号やその日付に展開するのは人気の高い機能だ。
Gitでこれを実現する場合、フィルター機能やフック機能にスクリプトを仕掛ける必要があり手軽では無い(Gitでフィルターを使え!)。
Mercurialでは、ユーザー設定で拡張機能Keyword Extensionを有効にすれば容易に実現する事ができる。
7.3. 改行コードの置換
Linux/UNIXとWindowsでファイルを共有する場合に、改行コードが問題になるときもある。Gitの場合はフィルターを書く必要が出てくるが、Mercurialの場合はユーザー設定で拡張機能EOL Extensionを有効にすれば実現できる。

8. リポジトリの保守

本質的ではないが、Mercurialの方が共有リポジトリの作成が楽(--bare --shared=groupオプションが不要)で、メンテナンス・コマンド(git-gc)が不要で、僅かではあるが保守性が高くなっている。

まとめ

第1節、第2節、第7節、第8節で見たように、Mercurialの方がユーザー負荷が少ない設計になっている面は多い。GitよりMercurialの方が、ユーザビリティは意識しているようだ。また、第3節~第5節で見たMercurialの独創的な履歴管理方法は、ブランチの自動分岐ができ、履歴の保護が強いことから、ライト・ユーザーに優しくなっている。バージョン管理システムに人的リソースを割きたく無いプロジェクトには、GitよりMercurialの方が適していると言えるであろう。

もちろん欠点もあって、プロジェクトの性質によっては、第6節で見たような差異でGitの方が適している状況もあるだろう。さらに、Mercurialはchangesetの親子関係が基本となっており、SVNやGitにおけるブランチを持たない。SVNやGitに慣れ親しんでいるユーザーは、ブランチの利用方法に戸惑うかも知れない。無名ブランチに親しめるかが、Mercurialを簡単に扱えるかのポイントになる。

コマンド体系以外にも、Windowsでの動作や、各種統合開発環境での利便性、GitHubに対する無料でプライベート・リポジトリが作れるBitBucketも、Mercurialのアドバンテージだと考える事ができる。日本語での情報量はGitの方が多いように感じるが、利用に困るほどMercurialの情報が少ないわけではない。流行はGitではあるが、それを押しのけてMercurialを採用する理由は、十分にあるように感じられる。

5 コメント:

suchi さんのコメント...
このコメントはブログの管理者によって削除されました。
suchi さんのコメント...

失礼、オプションが間違ってました。

7.3 は、git config [--global] core.autocrlf=(false|input|true) と全体やリポジトリ単位で設定可能です。

uncorrelated さんのコメント...

>> suchi さん
御指摘ありがとうございます。
最初のコメントは消して起きますね。

flying-foozy さんのコメント...

「Git との比較」という観点から、あえて省略なされたのかもしれません
が、幾つか言及されていない点がありましたので、このページを読まれる
方への注釈がてら、コメントさせて頂きます。

なお、git にはあまり精通していないため、誤解している点が有りました
ら申し訳有りません。

● 「Mercurialはhg-forgetで、除外することを毎回指定する必要がある」

"hg commit" 実行時には、ファイル名/合致パターン指定が可能ですので、
「対象指定無しで commit する」という前提が必要無ければ、一旦 "hg
add" したファイルでも、都度 forget する必要はありません。


● 「Mercurialのtagは異なるリポジトリ間で共有ができ」

タグ生成時に --local 指定することで、共有されないタグも生成可能で
す。但し、local tag を事後的に共有することは出来ません。

# 別途、共有可能なタグの生成が必要です

また bookmark は、参照先リビジョンが変動することを前提としています
ので、厳密には git の tag には相当しないと思われます。


● 「チェンジセットをpullしてきても、自動的にワーキングツリーに反映しない」

fetch 拡張を有効にした場合、"hg fetch" 実行により pull +
update(or merge)が自動化可能です。

git の fetch が「リポジトリ to リポジトリ」なのに対して、hg の
fetch が「リポジトリ to リポジトリ+作業領域」なのが、両者の「デー
タ授受主体」に対する認識の差を表しているようで、面白い所です。


● 「過去のchangesetからの派生コードを新たに無名ブランチを作成」

"hg backout" を使用することで、誤った変更側の無名ブランチを放置状
態にする事無く、「変更実施を無かったこと」にできます。

backout 作業の一環でマージを実施するため、枝分かれが解消されます。


● 「他のブランチから特定のコミットのみを反映」

transplant 以外に "hg graft" (こちらは標準機能) を使用可能です。

graft は 2011/11 リリースの 2.0 版で導入されました。

uncorrelated さんのコメント...

>>flying-foozy さん
色々と御指摘ありがとうございました。
後で幾つか本文に反映させて頂くと思います。

コメントを投稿