Git 対 Mercurial: なぜ Git を選ぶのか?

これは Atlassian DVCS ガイド » の一記事です。

今回は Atlassian の開発者である Charles O’Farrell によるゲストブログです。チームが DVCS として Git を選択する理由について説明します。Charles はコーディングをほとんど DVCS 上で行い、また ClearCase から Git へユーザーを移行させる作業を行ってきました。

前回の記事では、分散バージョン管理システムとしてチームがなぜ Mercurial を選択するのかについて考えてみました。今回は、分散バージョン管理システム (DVCS) として なぜ Git が有力な選択肢であるのかについて考えてみましょう。

1970 年の黎明期から、ギークたちはどちらが善でどちらが悪かという血なまぐさい論争を長い間行ってきました。それが Vim と Emacs との間の戦いです。最近では、それとは別のツールセットについて、ギークたちは本来の仕事そっちのけに何時間もかけてブログ上でその議論を行い、自分たちの正しさを主張したりしています。ここで言っているのはもちろん、Git と Mercurial の間にある厳しい対立のことです。

今回のブログでは、Git という、ええ、まあ「勝ち組」の立場をとり、なぜこの壮大な闘争において Git が優位になりつつあるのか、その説得力のある理由についていくつか見ていきます。

“退屈な” 注意事項

まず、私は Git が完璧であると主張するのにはまったく向いていない人物であるということを正直に認めます。ええ、ちっとも向いていません。これまで私は、とてもたくさんの時間を費やして、Git が全く予期しない動作を行うということを説明してきました。特に、チェックアウトコマンドの「モード」の違いを説明しなければならなくなった時には、いつも緊張して、なんとか切り抜けようと振舞っていました。また  msysgit は Windows 用 Git として本当に素晴らしいリリースだったのでしょうけれど、時間が経ってみると、あいかわらず二流であるように感じています。

とは言うものの、私はもともと Mercurial で始めた DVCS 人生をその後 Git に切り替え、Mercurialの方がよかったと思うことはありませんでした。

それはなぜでしょうか?

ストレージフォーマット

私にとって、Gitの持つ唯一最大の差別化要因は、リポジトリフォーマットです。Git について私の好きな部分の多くは、コンテンツの保存方法とそれに対する考え方に関わっています。

Mercurial は追記専用ログに全てをかけています。これは、ゆっくりと回るディスク上のディスクシークを(非常に合理的に)最適化するためです。一方 Git は、ハッシュ化された単純なドキュメントリポジトリ内に、全てのコミット / ファイルを格納しています。全コミット、全ファイルの全バージョンは別のエンティティとしてリポジトリ内に保管されることになります。ごく初期のころ、パックファイルを導入する以前は、このプロセスはひどく非効率的でした。しかし、そのアイデアはしっかりとしたものであり、今日においても用いられています。注目すべき点は、各オブジェクトのアイデンティティはコンテンツのハッシュだということであり、それは全てが不変であることを意味しています。コミットメッセージのようにシンプルな何かを変更するためには、まず新しいコミットオブジェクトを作成する必要があります。 それは以下のことにつながっていきます…

Git の方が履歴が安全

ええ、本当です!

Git が「破壊的」であると言われると、いつも本当にいらいらします。逆に、実際には Git は全ての DVCS の中でもっとも安全だと言いたいぐらいです。上記したように、実際には Git はユーザーに何も変更させず、ただ新しいオブジェクトを作成するだけです。では古いバージョンに何が起こるのでしょうか?Git君、なぜ君は変更を保存しないんだい?!?

Git は実際には全ての変更を管理し、reflog 内に保存しています。全てのコミットが一意であり不変なので、reflog がすべきことは、それらへの参照を保存するということだけです。30日後に、Git は reflog からエントリーを削除し、ようやくその時にガーベッジコレクションが行われます。お分かりでしょうか、まだ参照されているものを Git は削除しません。ブランチはコミットへの参照を保つためにもっとも便利な方法でであることは明らかです。しかしもう一つの方法である reflog では、ユーザーはそれについて考える必要すらありません!

対応するコマンド reflog により、この変更履歴を見ることができます。それはちょうど ‘git log’ コマンドで通常のコミットの履歴を見れるのと同じです。このことは、常に覚えておいてください。

1
2
3
4
5
6
7
8
> git reflog
5adb986 HEAD@{0}: rebase: Use JSONObject instead of strings
6a34803 HEAD@{1}: checkout: moving from finagle to 6a3480325f3beeecbafd351d30877694963a3f01^0
74bd03e HEAD@{2}: commit: Use JSONObject instead of strings
36c9142 HEAD@{3}: checkout: moving from 36c9142e81482f6c3eb8ad110642206a4ea3dfec to finagle
36c9142 HEAD@{4}: commit: Finagle and basic folder/json
1090fb7 HEAD@{5}: commit: Ignore Eclipse files
d6e3e63 HEAD@{6}: checkout: moving from master to d6e3e63889fd98e89e12e53a79bf96b53cbf9396^0

履歴の再作成

Mercurial について、私がどうしても好きになれなかったのは、コミットをさかのぼって微修正するのがとても難しいという点です。「なぜそれがやりたいんですか?」とあなたは思うかもしれません。プルリクエストが多くのファイルに影響を与えたり、重大なリファクタリングに関わっているような場合、コミットが包括的なストーリーを説明していると、レビューがとても簡単になります。 Git では、必要に応じて以前のコミットを編集するために「時間をさかのぼる」ことは容易です。 結果として、Git 内のコミットログは、変更が実際に行われた順序にそった忠実な (しかしバラバラの) 記録ではなく、注意深く作成されたストーリーにすることができます。

Mercurial にも、基本的にはそれと同じ事を行う拡張機能があり、それは Mercurial Queues と呼ばれています。Mercurial Queues とは、実際のコミットを最終的に決定する前に再オーダーできるように、プリコミットを積み重ねる方法です。MQ には細かく分かれたたくさんのコマンド (SVN にはないもの!) があります。

1
2
3
4
5
6
hg qnew firstpatch
hg qrefresh
hg qdiff
hg qnew secondpatch
hg qrefresh
hg qcommit

Git においては、通常どおりただコミットし、何をするかは後で考えればいいのです。そしてその考えるときになったら、知るべきことは本当にたった一つです。それが、インタラクティブな rebase です。このコマンドを使えば、テキストエディタを起動し、満足するまで Git の履歴を変更できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> git rebase –interactive origin/master
pick 94f56db Debug an error raised when importing view
squash 772e7e8 Re-join comments using DELIM
reword a04f10e Error on filter branch – print line
pick e09b0a2 Added troubleshooting for msysgit + Cygwin
fixup 276c49a Added troubleshooting for missing master_cc branch
pick a2c08f6 Added exclude configuration
pick 4c09e5e Ignore errors from _really_ long file paths
pick 9f38cf0 Actually, use fnmatch for exclude# Rebase f698827..9f38cf0 onto f698827
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like “squash”, but discard this commit’s log message
# x, exec = run command (the rest of the line) using shell
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.

Mercurial にもほぼ同等の histedit 拡張機能がありますが、これは strip を用いて通常の追記専用リポジトリを更新し、外部バックアップファイルをはきだすものです。このバックアップからどのように変更を照会するのかは疑問です。どのくらいの期間、それをとっておくべきでしょうか?それを復元するのに実行する新しいコマンドも必要ではないでしょうか?

Git に話を戻します。 reflog の30日間が終了した後、コミットを失うことが心配されます。Git の1か月後のガーベッジコレクションを止める方法さえあればよいのですが。将来、参照するかもしれないので「念のため」というラベルを付ける方法さえあれば…

ブランチのように?

そのとおりです!これらの Git の「バックアップ」は単なるコミットなので、ブランチが無くても reflog は使えます。バックアップをどう扱うべきかを知るために、また別のコマンドセットを覚える必要はありません。

「ものごとはできるだけシンプルにしよう。ただし、シンプルにしすぎないように」

Git でブランチ作成

Git におけるブランチ作成は、長い間、「キラー機能」でした。Mercurial であれば、各ブランチごとにリポジトリをクローンすることをきっとすすめるでしょう(そして今でもすすめています)。でもちょっと待ってください。これは DVCS

であって、 SVN ではありませんよね?また、Mercurial には実際に「ブランチ」コマンドがあります。これは、あるコミットに永久にラベル付けするコマンドです。一度適用されると、最終的にマージするかクローズする場合を除いて、修正することは不可能になります。多くの要望により、 Git ブランチを直接的にまねたものとして、ついにブックマーク拡張機能が導入されました。ただ、最初はブックマークをサーバーにプッシュすることができませんでしたが。

しかし、相変わらず Git にはひとつ強みが残っています。Mercurial のブックマークは一つの名前空間を共有してしまうのです。これが何を意味するのかを理解するために、誰かがいくつかの変更をサーバーにプッシュするという、よくあるシナリオを見てみましょう。

1
2
3
4
5
6
7
8
9
10

> git fetch
From bitbucket.org:atlassian/helloworld
* [new branch] test> origin/test
565ad9c..9e4b1b8 master –> origin/master

> git log –graph –oneline –decorate –all
* 9e4b1b8 (origin/master, origin/test) Remove unused variable
| * 565ad9c (HEAD, master) Added Hello example
|/
* 46f0ac9 Initial commit

本当のマスターブランチがどれか分かりますか?もちろん、これに何か問題があるというわけではありません。たまたま ‘master’ という同じ名前を持ったブランチがふたつあるということです。サーバーの名前空間 (この場合では元の方) を見れば、どちらがどちらであるのかがわかります。

Mercurial ではどうでしょう?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

> hg pull
pulling from default
importing bookmark test
divergent bookmark master stored as master@default

> hg glog
o changeset: 2:98c63da09bb1
| bookmark: master@default
| bookmark: test
| summary: Third commit
|
| o changeset: 1:d9989a0da93e
| | bookmark: master
| | summary: Second commit
|/
o changeset: 0:2e92d3b3d020
summary: First commit

プルを行うと、自分のマスターと衝突するブランチがひとつあり、衝突しない ‘test’ ブランチがあるとわかるでしょう。 名前空間の概念がないので、どのブックマークがローカルで、どれがリモートなのかを知るすべがありません。そしてそれらをなんと呼ぶのかによっては、また衝突が起きるかもしれません

ステージング

これは Git について好き嫌いが別れるものです。Git には紛らわしいことに「インデックス」と呼ばれる、奇妙なものがあります。「インデックス」のことを、ステージング領域と呼ぶ人も中にはいます。呼び方はなんでもよいのですが。

Git 内でコミットに追加されるものは全て、最初にインデックスを通過する必要があります。どのようにインデックスにコンテンツを追加できるのでしょうか?’git add’ を呼び出すことによってです。新しいファイルを使う SVN ユーザーにとってこれは理にかなったものですが、すでにコミットしたファイルに対して行う時は少し混乱するかもしれません。心に留めておくべきことは、「追加」しているのは変更であり、ファイルそのものではないということです。これについて良いと思うのは、毎回コミットされるものが何なのか正確に分かるという点です。

これを詳しく説明するために、私が他の何よりも多く使用しているコマンドを例に挙げましょう。それが、パッチです。パッチを使うことで、オール・オア・ナッシングのアプローチではなく、ファイルから特定の hunk やスニペットを追加できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

> git add –patch

diff –git a/OddEven.java b/OddEven.java
index 99c0659..911da1b 100644
a/OddEven.java
+++ b/OddEven.java
@@32,6 +32,7 @@ public class OddEven {
* Object) and initializes it by calling the constructor. The next line of code calls
* the “showDialog()” method, which brings up a prompt to ask you for a number
*/
+ System.out.println(“Debug”);
OddEven number = new OddEven();
number.showDialog();
}
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? n
@@49,7 +50,7 @@ public class OddEven {
* After that, this method calls a second method, calculate() that will
* display either “Even” or “Odd.”
*/
– this.input = Integer.parseInt(JOptionPane.showInputDialog(“Please Enter A Number”));
+ this.input = Integer.parseInt(JOptionPane.showInputDialog(“Please enter a number”));
this.calculate();
} catch (final NumberFormatException e) {
/*
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? y

デバッグステートメントを削除し忘れたことがお分かりでしょう。コミットする前にチェックするのはいい習慣ですね!ここでうれしいのは、お望みなら、デバッグステートメントをそのままにしておいて、後から ‘hunk’ を受け入れることもできるということです。すべて同じファイルにあるのに、間違いに気付いた後で再編集する必要はないのです。

当然ながら、Mercurial にもこの振る舞いを模倣した Record 拡張機能があります。しかし、それは単なる拡張機能(あるいは少なくとも基本的なもの)であるので、ステージングしていない変更を一時的な場所にコピーし、動作しているストレージファイルを更新し、コミットした上で変更を元に戻す必要があります。もし間違えた場合は、最初からやりなおさなければなりません。Git のアプローチの素晴らしい点は、基本的なレベルにおいて、Git がインデックスを取り扱うので、ファイルに触れる必要がないという点です。変更をステージングした後、status を実行する時に、先に進める前に全て正しいかどうかをダブルチェックできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

> git status

# On branch master
# Changes to be committed:
# (use “git reset HEAD …” to unstage)
#
# modified: OddEven.java
#
# Changes not staged for commit:
# (use “git add …” to update what will be committed)
# (use “git checkout — …” to discard changes in working directory)
#
# modified: OddEven.java
#

ステージングしていない変更をテストすることが心配な人のために、コミットされないものを全て一時的にアーカイブする ‘git stash -keep- index’ があります。余談ですが、この stash は、当然ながら他のコミットとまったく同じように格納され、前述の reflog によって見ることができます。

1
2
> git reflog –all
b7004ea refs/stash@{0}: WIP on master: 46f0ac9 Initial commit

責任のなすり合い (blame ごっこ)

Git について面白いのは、実際に名前の変更を追跡していないということです。これは一部人たちにとって懸念の材料になりますが、Git は「正しいこと」™を行なっていると私は思います。ではともかく、名前変更とは何なのでしょう?ただ、コンテンツをあるファイルの場所から別の場所へ移動させているだけです。しかし、ファイルの一部だけを移動させたら何が起こるでしょうか?Git blame は便利なコマンドで、通常はファイルの各行に最後に触れたコミットを表示します。 魔法の ‘-C’ オプションを用いることにより、ファイルをまたいで移動した行を検出することもできます。(この場合の ‘-s’ は、日付や作者といったノイズを隠すために使っています。)

1
2
3
4
5
6
7
8
9
10
11

> git blame -s -C OddEven.java

d46f0ac9 OddEven.java public void run() {
d46f0ac9 OddEven.java OddEven number = new OddEven();
d46f0ac9 OddEven.java number.showDialog();
d46f0ac9 OddEven.java }
d46f0ac9 OddEven.java
565ad9cd Hello.java public static void main(final String[] args) {
565ad9cd Hello.java new Hello();
565ad9cd Hello.java }
d46f0ac9 OddEven.java }

全ての行がこの一つのファイルに由来しているわけではないことに注意してください。この悪いヤツと同等の拡張機能は、Mercurial にはありません。

まとめ

Gitを使うということは、「何かをしておくべきだ」と言う必要がないということです。しかし、Mercurial を使う場合には、全く同じこと言ったことが何度かあります。コミットのリベースや修正をしたくなったとき、あるいはシングルリポジトリのブランチ(別名ブックマーク)を使用したくなったとき(こうしたことは、毎日行なっているのですが)には、Mercurial を快適に使えなくなってしまいます。 追記専用のリポジトリフォーマットは、意図的にこの振る舞いを考慮に入れずに設計されています。(GitHub の)Scott Chacon は Mercurial がまるで “Git Lite” のようだと言いましたが、私も同じように思います。

Git は完璧ではありません。しかし、コマンドラインが素晴らしいということよりも、もっと大事なことがあります。もちろん、Git がもう少し良くできていて、不可解なエラーが少なく、Windows 上でもっと速く動作したりすれば、それはいいことだと思います。しかし、結局のところ、これらはただ表面上のことにすぎません。もし好きではないコマンドがあれば、どうぞエイリアスを書いてください。Windows 上での使用をやめてください(冗談です)。リポジトリフォーマットは、DVCS ツールによって可能になることを、今も将来も推進していきます。

Git と Mercurial のチートシート

この記事と、Git に対する Mercurial の優位性を説明した前回の記事により、両システムの長所、短所が明らかになればいいなと思っています。このシリーズの次の記事は、集中型バージョン管理システム Subversion から Git あるいは Mercurial へ移行するユーザー向けの “チートシート” を提供する予定です。

(査読:グロースエクスパートナーズ株式会社 和智右桂)

*本ブログは Atlassian Blogs を翻訳したものです。本文中の日時などは投稿当時のものですのでご了承ください。
*原文 : 2012 年 3 月 13 日投稿 "Git vs Mercurial: Why Git?"