The Powerful Code

スタートアップを技術で支える - iOS開発やその周辺技術など -

[Git] 使い分けできていますか?マージ(merge)&リベース(rebase)再入門

Git Logo by Jason Long is licensed under the Creative Commons Attribution 3.0 Unported License.

リモートに存在するブランチ(機能追加や不具合修正のために)新たに作成したブランチ … これらのブランチの変更箇所を別のブランチへ統合するには、 2つの方法 が存在します。

  • a) マージ(merge) を利用する方法
  • b) リベース(rebase) を利用する方法

この記事では、 2つの方法 を使い分けるための ヒント を紹介します。

うまく使い分けるには、それぞれの方法の良い点と悪い点をしっかりと把握しなければなりません。

マージ(merge)と リベース(rebase)

そもそも、 マージ(merge)リベース(rebase) とは何でしょうか。

ここで、2つのブランチが分岐している状態を考えます。 下の図では、masterブランチからbugfixブランチが分岐しています。

図1:分岐した2つのブランチ

※ 図は、Mac向けのGitクライアントである SourceTree の操作画面の一部をキャプチャしています。各行は コミット を表し、各列は左から、 グラフコメントSHA-1 を表しています。

この状況において、 マージ(merge)リベース(rebase) 、それぞれの操作をした場合を考えてみましょう。

マージ(merge)

bugfixブランチを、masterブランチへ マージ したのが下の図です。 masterブランチ上ではマージコミットが新たに生成され、bugfixブランチの変更分を取り込んでいます。

図2-a:マージを利用した結果

マージに必要なコマンドは次の通りです。

1
2
$ git checkout master
$ git merge bugfix

リベース(rebase)

bugfixブランチを、masterブランチへ リベース したのが下の図です。 bugfixブランチの分岐元が、masterブランチの最新のコミットへ移動しています。 コミットの履歴は一本化され、bugfixブランチ上のコミットはmasterブランチの変更分を取り込んだことになります。

図2-b:リベースを利用した結果

ここで注意したいのは、SHA-1の値が変更されたということです。 このことから、リベースの操作によって、bugfixブランチ上にあった2つのコミットは改変された(一度、破棄され、新たに作成された)ということがわかります。 マージの場合と異なり、リベースはコミットを改変する作業なのです。

リベースに必要なコマンドは次の通りです。

1
2
$ git checkout bugfix
$ git rebase master

2つのブランチの統合

さて、図1の状態から、 「masterブランチへbugfixブランチの変更分を取り込んで、2つのブランチを統合する」 という操作を考えてみましょう。

冒頭にも書いた通り、ブランチの統合には2つの方法があります。

a) マージ(merge)を利用する方法

マージの説明で示した図(図2-a)は、目標となる操作をすでに完了しています。 すなわち、 マージを利用する統合 をした状態とは、マージの説明で示した図の状態と同等であり、下の図のようになります。

図3-a:マージを利用するブランチの統合

bugfixブランチの変更分が、masterブランチから分離された状態で保持されています。 bugfixブランチでの変更のみを統合後に確認する際には都合が良いでしょう。

b) リベース(rebase)を利用する方法

リベースの説明で示した図(図2-b)の状態から、マージを利用することで、目標となる操作が完了します。 図2-bの状態から、bugfixブランチをmasterブランチへマージすれば良いのです。 リベース(rebase)を利用する統合 をした状態は、下の図のようになります。

図3-b:リベースを利用するブランチの統合

履歴が一本化され、とてもシンプルです。 マージコミットの生成を必要とせず、履歴をシンプルに保ちたい場合には都合が良いでしょう。

良い点 と 悪い点

先程、示した2つの統合の方法には、それぞれ 良い点悪い点 があります。 使い分ける上で必要となる知識ですので、しっかりと把握しておきましょう。

a) マージ(merge)を利用する方法の特徴

  • 良い点
    • 比較的、簡単な操作
    • 統合されるブランチのコミットを改変しない
    • 統合後もブランチの情報を分離して保持(後から確認するのが容易)
  • 悪い点
    • (特に、複数人が並行して同じブランチ上で作業をする際には、)多数のブランチの情報が残存したり、マージコミットが散在したりすることで、履歴が複雑化する原因になる

b) リベース(rebase)を利用する方法の特徴

  • 良い点
    • 不要なコミットが入り込むことがないので、直感的かつシンプルに履歴を保つ
  • 悪い点
    • 競合発生時の対処手順が、比較的、複雑
    • 統合されるブランチのコミットを改変するので注意が必要

コミットの改変について

(リベースの悪い点である)コミットの改変は、注意をしないと深刻な問題を引き起します。

仮に、リモートで共有された特定のブランチが複数の作業者からチェックアウトされ、並行して作業が進行しているとします。 そのブランチに対して、ある作業者がリベースの操作をすることで、コミットの改変を生じさせるとどうなるでしょうか。

コミット改変後も並行して作業が進行し続ければ、ある作業者は改変されたコミットに変更を加え、別の作業者は改変されていないコミットに変更を加えることになります。 後々、互いの作業内容を統合する際、統合に失敗するなど、深刻な問題が発生することは容易に想像できるでしょう。

使い分けのためのヒント

ここまで、2つの統合の方法についてと、それぞれの良い点と悪い点についてを説明しました。 それでは、 マージリベース はどのように使い分けをするべきなのでしょうか。

使い分けのためのヒント として、マージとリベースの使い分けを特に意識すべき状況を3つ挙げ、それぞれの状況でどのような操作をするべきかを説明します。

※ 以後、 トピックブランチ統合ブランチ という語を用います。 2つのブランチの意味は、概ね以下の通りです。(図1~3でも、コメントとしてこれらの語を用いているので参考にしてください)

  • トピックブランチ
    (不具合修正や機能追加といった、あるトピックに対する)特定の作業を進行・完了させるために分岐させるブランチ。
  • 統合ブランチ
    トピックブランチの分岐元になり、トピックブランチでの変更分を集結するブランチ。通常はmasterブランチが該当する。

【状況 1】リモートの統合ブランチに対する変更箇所を、ローカルの統合ブランチへ統合する場合

複数人で作業を進めると、刻々とリモートの統合ブランチには変更が加えられていきます。 リモートの統合ブランチに対する変更箇所を、ローカルの統合ブランチへ統合する場面もあるでしょう。

下の図は、リモートの統合ブランチに対して変更箇所が新たに追加された状況を想定しています。 Aさん、Bさんが共有リポジトリで並行して作業をしています。 Bさんから変更箇所が新たに追加され、Aさんはローカルの統合ブランチへ変更箇所を統合しなくてはなりません。

図4-1-a:リモートの統合ブランチとローカルの統合ブランチ

pullを実行すれば、リモートの統合ブランチの変更箇所を取得して、ローカルの統合ブランチへ反映させることができます。

この際、統合の作業が自動的に実行されるのですが、pullコマンドに何のオプションも指定しなければ、マージを利用する統合が実行されることになります。 つまり、オプションを指定しないpullコマンドは、マージコミットを発生させるのです。 (ただし、fast-forwardマージの場合はマージコミットが発生しないので、例外はあります。)

図4-1-aの状況の場合、統合ブランチに対する変更箇所を反映させたいので、マージコミットを発生させずに履歴を一本化するのが良さそうです。 pullコマンドには、rebaseオプションが存在し、リベースを利用して統合する方法に切替えることができます。 rebaseオプションを指定してpullを実行したのが下の図です。

図4-1-b:リモートの統合ブランチの変更箇所を、ローカルの統合ブランチへ統合

Bさんの変更箇所を反映させ、Aさん自身の変更分をリベースしてブランチの先頭へ移動させることで、履歴を一本化することができています。 この後、pushをすれば、リモートの統合ブランチにAさんの変更箇所を反映させることもできます。

リモートとローカル間で統合ブランチの変更箇所を統合する状況では、 リベースを利用する統合 を積極的に用いて履歴をシンプルに保ちましょう。

この場合、使用するコマンドは次の通りです。

1
2
$ git checkout master
$ git pull --rebase

【状況 2】 ローカルにおいて、トピックブランチに対する変更箇所を統合ブランチへ統合する場合

ローカルの統合ブランチから、不具合修正や機能追加のために、トピックブランチを作成して作業を進めることがあります。 トピックブランチでの作業が完了すれば、統合ブランチへ変更分を統合する必要があります。

下の図は、統合ブランチから分岐したトピックブランチでの作業が完了した状況を表しています。

図4-2-a:作業が完了したローカルのトピックブランチと統合ブランチ

このような状況では、トピックブランチでの変更分を統合ブランチから分離して保持できると良さそうです。 マージを利用する統合 を用いましょう。

mergeコマンドには、fast-forwardマージを許容しないno-ffオプションが存在します。 (ここではfast-forwardマージの説明は省きますが、no-ffオプションを指定することで、統合されるブランチの情報が保持されるのを保証されると考えてください。)

no-ffオプションを指定して、マージを利用する統合をしたのが下の図です。

図4-2-b:ローカルのトピックブランチの変更箇所を統合ブランチへ統合

作業が完了したトピックブランチの変更箇所を統合ブランチへ統合する状況では、 マージを利用する統合 とno-ffオプションを用いて、トピックブランチでの変更分を保持するようにしましょう。

この場合、使用するコマンドは次の通りです。

1
2
$ git checkout master
$ git merge --no-ff bugfix

【状況 3】 ローカルにおいて、統合ブランチに対する変更箇所をトピックブランチへ統合する場合

トピックブランチを作成して作業を進めている際、トピックブランチ作成時にはなかった変更が統合ブランチに加えられることがあります。 このような場合、統合ブランチの修正分を取り込みたいこともあるでしょう。

下の図は、統合ブランチから同時に分岐した2つのトピックブランチのうち、bugfix/Aブランチでの作業が完了し、統合ブランチへ変更箇所が統合された状況を表しています。 bugfix/Bで作業をするも、統合ブランチへ統合されたbugfix/Aブランチでの変更箇所が、bugfix/Bでも必要になったと仮定してください。 bugfix/Bブランチに対して、統合ブランチの変更箇所を統合する必要があります。

図4-3-a:統合されたローカルの統合ブランチと作業途中のトピックブランチ

このような状況では、bugfix/Bブランチの分岐元を移動させることで、履歴を一本化するのが良さそうです。 リベースを利用する統合 を用いましょう。

リベースをして、bugfix/Bブランチの分岐元を統合ブランチの最新のコミットへ移動させたのが下の図です。

図4-3-b:ローカルの統合ブランチの変更箇所をトピックブランチへ統合

統合ブランチの変更箇所をトピックブランチへ統合する状況では、 リベースを利用する統合 を用いて、履歴を一本化するようにしましょう。

この場合、使用するコマンドは次の通りです。

1
2
$ git checkout bugfix/B
$ git rebase master

最後に

この記事では、マージとリベースについて扱いました。 マージとリベースの特徴が理解できると、様々な状況でもどちらを利用すべきか判断しやくすなるのではないでしょうか。

なお、この記事では統合時に競合が発生するのを考慮せずに説明を進めました。 競合が発生した場合の対処などは、適宜、書籍や他のブログ記事などを参照してください。

参考

定期購読をおすすめします!

follow us in feedly

 RSSリーダーで購読する