Infinito Nirone 7

白羽の矢を刺すスタイル

git merge の 3 つの動き

いまや Git なしでは生きていけないくらい Git に支えられている生活を送っていますが、Git のブランチをマージする手段にはいろいろあって、実際 GitHub でもプルリクエストをマージするときには 3 種類の方法から選ぶよう促されます。

git merge

ブランチをマージするコマンドですが、ブランチの状況によって挙動が変わります。具体的には Fast Forward と non-Fast Forward の 2 種類があり、オプションで指定しない限りは Git が勝手に判断してくれます。ここでは、masterから派生したtopic/aブランチをmasterにマージすることを想定したときの動きを説明します。

Fast Forward

topic/aの派生元のリビジョンがmasterの最新リビジョンと一致するときは、単純にtopic/aの最新リビジョンをmasterの最新リビジョンに変えるだけでブランチがマージできたことになります。このように、マージ先ブランチの最新リビジョンをマージ元ブランチの最新リビジョンにするだけでマージが完了することを Fast Forward といいます。

マージ前の状態
マージ前の状態

Fast Forward マージ後
Fast Forward マージ後

git logで歴史を見ると、マージされたtopic/aブランチはmasterの歴史の一部となっていて、別のブランチが存在したようには見えなくなります。

強制的に Fast Forward でマージする場合は git merge --ff-only とします(Fast Forward とならない場合はエラーとしてマージできない)が、オプションなしで git merge する場合のデフォルトの挙動も Fast Forward です(Fast Forward とならない場合は non-Fast Forward でマージしようとする)。

non-Fast Forward

topic/aを派生したあとにmasterに異なるコミットがある場合は、masterの最新リビジョンを変えてしまうとtopic/aを派生したあとに積み上げたコミットが無かったことになってしまうため、Fast Forward でマージできません。そこで、topic/aにあるコミットをmasterに混ぜた上で、マージをしたことを示すマージコミットを作成します。

マージ前の状態
マージ前の状態

non-Fast Forward マージ後
non-Fast Forward マージ後

git logで歴史を見ると、マージされたtopic/aブランチの持っていた歴史は時間順にmasterの歴史に取り込まれつつ、マージコミットによって別のブランチが存在していたこともわかるようになります。

強制的に non-Fast Forward でマージする場合は git merge --no-ff とします(Fast Forward でマージ可能な場合でも non-Fast Forward でマージコミットを作る)。

Squash

これは上記の 2 つとは異なり、明示的にオプションで指定(--squash)しない限りこの挙動にはなりません。

topic/aにある全コミットを1つに集約しmasterにコミットを作るマージのことを Squash マージといいます。git logで歴史を見ると、Fast Forward のようにtopic/amasterの歴史の一部になったようには見えなくなります。Squash マージしたあとでさらにtopic/aにコミットを積み上げ Squash マージをすると、もういちど topic/aの全コミットを1つに集約しなおしてコミットを作ろうとします。

Squash マージ後
Squash マージ後

余談

git rebase

なにかと怖がられがちですが、してはいけないこと*1はとてもシンプルかつたいていどこかでエラーを起こして失敗する*2ので、もっと安心して使えるコマンドです。 GitHub のプルリクエストをマージするときの選択肢に出てくるうちの1つでもあります。

名前のとおり、ブランチの派生元リビジョンを変える(re base)ためのコマンドで、手作業で同等のコマンドを打つとするなら、ブランチを目的の派生元リビジョンから切りなおしたうえで rebase 前のブランチにあるコミットを1つずつ cherry-pick するようなものです。たとえば、masterの歴史とtopic/aの歴史がそれぞれに進んでいるときにtopic/amasterに rebase すると、topic/aの派生元はmasterの最新リビジョンとなり、topic/aにあったコミットは別のリビジョン番号を持って積み直されます。

git pull

リモートリポジトリからコミットを取得するコマンドです。デフォルトでは git fetch かつ git merge の動きをしますが、オプション(--rebase)をつけると git fetch かつ git rebase の動きをします。

たとえば、mastergit pull をすると、Fast Forward できるときはマージコミットは作られず、non-Fast Forward の場合はマージコミットができます。また mastergit pull --rebase をする場合、一旦リモートリポジトリのmasterを取り込んだ上で、手元のmasterに積み上がっていたコミットを積み直します。

merge や rebase を取り消したくなったとき

ちなみに、merge や rebase が仮にうまく完了したあとでそれらを取り消したくなった場合、リモートリポジトリに push していなければ*3git reset --hard ORIG_HEADでコマンドを打つ前の状態に戻れます。

*1:共同作業しているブランチでrebaseしてpush

*2:force push しなければ失敗する

*3:リモートリポジトリに push しててもできなくはないけど歴史の辻褄が合わなくなる