Git 🌙
Chapters ▾ 2nd Edition

3.6 Git のブランチ機能 - リベース

リベース

Git には、あるブランチの変更を別のブランチに統合するための方法が大きく分けて二つあります。 mergerebase です。 このセクションでは、リベースについて「どういう意味か」「どのように行うのか」「なぜそんなにもすばらしいのか」「どんなときに使うのか」を説明します。

リベースの基本

マージについての説明で使用した例を マージの基本 から振り返ってみましょう。 作業が二つに分岐しており、それぞれのブランチに対してコミットされていることがわかります。

シンプルな、分岐した歴史
図 35. シンプルな、分岐した歴史

このブランチを統合する最も簡単な方法は、先に説明したように merge コマンドを使うことです。 これは、二つのブランチの最新のスナップショット (C3C4) とそれらの共通の祖先 (C2) による三方向のマージを行い、新しいスナップショットを作成 (そしてコミット) します。

分岐した作業履歴をひとつに統合する
図 36. 分岐した作業履歴をひとつに統合する

しかし、別の方法もあります。 C3 で行った変更のパッチを取得し、それを C4 の先端に適用するのです。 Git では、この作業のことを リベース (rebasing) と呼んでいます。 rebase コマンドを使用すると、一方のブランチにコミットされたすべての変更をもう一方のブランチで再現することができます。

今回の例では、次のように実行します。

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

これは、まずふたつのブランチ (現在いるブランチとリベース先のブランチ) の共通の先祖に移動し、現在のブランチ上の各コミットの diff を取得して一時ファイルに保存し、現在のブランチの指す先をリベース先のブランチと同じコミットに移動させ、そして先ほどの変更を順に適用していきます。

`C4` の変更を `C3` にリベース
図 37. C4 の変更を C3 にリベース

この時点で、 master ブランチに戻って fast-forward マージができるようになりました。

$ git checkout master
$ git merge experiment
master ブランチの Fast-forward
図 38. master ブランチの Fast-forward

これで、C4' が指しているスナップショットの内容は、先ほどのマージの例で C5 が指すスナップショットと全く同じものになりました。 最終的な統合結果には差がありませんが、リベースのほうがよりすっきりした歴史になります。 リベース後のブランチのログを見ると、まるで一直線の歴史のように見えます。 元々平行稼働していたにもかかわらず、それが一連の作業として見えるようになるのです。

リモートブランチ上での自分のコミットをすっきりさせるために、よくこの作業を行います。 たとえば、自分がメンテナンスしているのではないプロジェクトに対して貢献したいと考えている場合などです。 この場合、あるブランチ上で自分の作業を行い、プロジェクトに対してパッチを送る準備ができたらそれを origin/master にリベースすることになります。 そうすれば、メンテナは特に統合作業をしなくても単に fast-forward するだけで済ませられるのです。

あなたが最後に行ったコミットが指すスナップショットは、リベースした結果の最後のコミットであってもマージ後の最終のコミットであっても同じものとなることに注意しましょう。 違ってくるのは、そこに至る歴史だけです。 リベースは、一方のラインの作業内容をもう一方のラインに順に適用しますが、マージの場合はそれぞれの最終地点を統合します。

さらに興味深いリベース

リベース先のブランチ以外でもそのリベースを再現することができます。 たとえば トピックブランチからさらにトピックブランチを作成した歴史 のような歴史を考えてみましょう。 トピックブランチ (server) を作成してサーバー側の機能をプロジェクトに追加し、それをコミットしました。 その後、そこからさらにクライアント側の変更用のブランチ (client) を切って数回コミットしました。 最後に、server ブランチに戻ってさらに何度かコミットを行いました。

トピックブランチからさらにトピックブランチを作成した歴史
図 39. トピックブランチからさらにトピックブランチを作成した歴史

クライアント側の変更を本流にマージしてリリースしたいけれど、サーバー側の変更はまだそのままテストを続けたいという状況になったとします。 クライアント側の変更のうちサーバー側にはないもの (C8C9) を master ブランチで再現するには、git rebase--onto オプションを使用します。

$ git rebase --onto master server client

これは「client ブランチに移動して client ブランチと server ブランチの共通の先祖からのパッチを取得し、master 上でそれを適用しろ」という意味になります。 ちょっと複雑ですが、その結果は非常にクールです。

別のトピックブランチから派生したトピックブランチのリベース
図 40. 別のトピックブランチから派生したトピックブランチのリベース

これで、master ブランチを fast-forward することができるようになりました (master ブランチを fast-forward し、client ブランチの変更を含める を参照ください)。

$ git checkout master
$ git merge client
master ブランチを fast-forward し、client ブランチの変更を含める
図 41. master ブランチを fast-forward し、client ブランチの変更を含める

さて、いよいよ server ブランチのほうも取り込む準備ができました。 server ブランチの内容を master ブランチにリベースする際には、事前にチェックアウトする必要はなく git rebase [basebranch] [topicbranch] を実行するだけでだいじょうぶです。 このコマンドは、トピックブランチ (ここでは server) をチェックアウトしてその変更をベースブランチ (master) 上に再現します。

$ git rebase master server

これは、server での作業を master の作業に続け、結果は server ブランチを master ブランチ上にリベースする のようになります。

server ブランチを master ブランチ上にリベースする
図 42. server ブランチを master ブランチ上にリベースする

これで、ベースブランチ (master) を fast-forward することができます。

$ git checkout master
$ git merge server

ここで client ブランチと server ブランチを削除します。 すべての作業が取り込まれたので、これらのブランチはもはや不要だからです。 これらの処理を済ませた結果、最終的な歴史は 最終的なコミット履歴 のようになりました。

$ git branch -d client
$ git branch -d server
最終的なコミット履歴
図 43. 最終的なコミット履歴

ほんとうは怖いリベース

あぁ、このすばらしいリベース機能。しかし、残念ながら欠点もあります。その欠点はほんの一行でまとめることができます。

公開リポジトリにプッシュしたコミットをリベースしてはいけない

この指針に従っている限り、すべてはうまく進みます。 もしこれを守らなければ、あなたは嫌われ者となり、友人や家族からも軽蔑されることになるでしょう。

リベースをすると、既存のコミットを破棄して新たなコミットを作成することになります。 新たに作成したコミットは破棄したものと似てはいますが別物です。 あなたがどこかにプッシュしたコミットを誰かが取得してその上で作業を始めたとしましょう。 あなたが git rebase でそのコミットを書き換えて再度プッシュすると、相手は再びマージすることになります。 そして相手側の作業を自分の環境にプルしようとするとおかしなことになってしまいます。

いったん公開した作業をリベースするとどんな問題が発生するのか、例を見てみましょう。 中央サーバーからクローンした環境上で何らかの作業を進めたものとします。 現在のコミット履歴はこのようになっています。

リポジトリをクローンし、なんらかの作業をすませた状態
図 44. リポジトリをクローンし、なんらかの作業をすませた状態

さて、誰か他の人が、マージを含む作業をしてそれを中央サーバーにプッシュしました。 それを取得し、リモートブランチの内容を作業環境にマージすると、その歴史はこのような状態になります。

さらなるコミットを取得し、作業環境にマージした状態
図 45. さらなるコミットを取得し、作業環境にマージした状態

次に、さきほどマージした作業をプッシュした人が、気が変わったらしく新たにリベースし直したようです。 なんと git push --force を使ってサーバー上の歴史を上書きしてしまいました。 あなたはもう一度サーバーにアクセスし、新しいコミットを手元に取得します。

誰かがリベースしたコミットをプッシュし、あなたの作業環境の元になっているコミットが破棄された
図 46. 誰かがリベースしたコミットをプッシュし、あなたの作業環境の元になっているコミットが破棄された

さあたいへん。 ここであなたが git pull を実行すると、両方の歴史の流れを含むマージコミットができあがり、あなたのリポジトリはこのようになります。

同じ作業を再びマージして新たなマージコミットを作成する
図 47. 同じ作業を再びマージして新たなマージコミットを作成する

歴史がこんな状態になっているときに git log を実行すると、同じ作者による同じメッセージのコミットが二重に表示されてしまいます。 さらに、あなたがその歴史をサーバにプッシュすると、リベースされたコミット群を中央サーバーに送り込むことになり、他の人たちをさらに混乱させてしまいます。 他の開発者たちは、C4C6 を歴史に取り込みたくないはずです。だからこそ、最初にリベースしたのでしょうからね。

リベースした場合のリベース

もしそんな状況になってしまった場合でも、Git がうまい具合に判断して助けてくれることがあります。 チームの誰かがプッシュした変更が、あなたの作業元のコミットを変更してしまった場合、どれがあなたのコミットでどれが書き換えられたコミットなのかを判断するのは大変です。

Git は、コミットの SHA-1 チェックサム以外にもうひとつのチェックサムを計算しています。これは、そのコミットで投入されたパッチから計算したものです。 これを「パッチ ID」と呼びます。

書き換えられたコミットをプルして、他のメンバーのコミットの後に新たなコミットをリベースしようとしたときに、 Git は多くの場合、どれがあなたのコミットかを自動的に判断し、そのコミットを新しいブランチの先端に適用してくれます。

たとえば先ほどの例で考えてみます。誰かがリベースしたコミットをプッシュし、あなたの作業環境の元になっているコミットが破棄された の場面で、マージする代わりに git rebase teamone/master を実行すると、Git は次のように動きます。

  • 私たちのブランチにしかない作業を特定する (C2, C3, C4, C6, C7)

  • その中から、マージコミットではないものを探す (C2, C3, C4)

  • その中から、対象のブランチにまだ書き込まれていないものを探す (C4 は C4' と同じパッチなので、ここでは C2 と C3 だけになる)

  • そのコミットを teamone/master の先端に適用する

リベース後、強制的にプッシュした作業へのリベース
図 48. リベース後、強制的にプッシュした作業へのリベース

これがうまくいくのは、あなたの C4 と他のメンバーの C4' がほぼ同じ内容のパッチである場合だけです。 そうでないと、これらが重複であることを見抜けません (そして、おそらくパッチの適用に失敗するでしょう。その変更は、少なくとも誰かが行っているだろうからです)。

この操作をシンプルに行うために、通常の git pull ではなく git pull --rebase を実行してもかまいません。 あるいは手動で行う場合は、git fetch に続けて、たとえば今回の場合なら git rebase teamone/master を実行します。

git pull を行うときにデフォルトで --rebase を指定したい場合は、 設定項目 pull.rebase を指定します。たとえば git config --global pull.rebase true などとすれば、指定できます。

プッシュする前の作業をきれいに整理する手段としてだけリベースを使い、まだ公開していないコミットだけをリベースすることを心がけていれば、何も問題はありません。 すでにプッシュした後で、他の人がその後の作業を続けている可能性のあるコミットをリベースした場合は、やっかいな問題を引き起こす可能性があります。 チームメイトに軽蔑されてしまうかもしれません。

どこかの時点でどうしてもそうせざるを得ないことになったら、みんなに git pull --rebase を使わせるように気をつけましょう。 そうすれば、その後の苦しみをいくらか和らげることができます。

リベースかマージか

リベースとマージの実例を見てきました。さて、どちらを使えばいいのか気になるところです。 その答えをお知らせする前に、「歴史」とはいったい何だったのかを振り返ってみましょう。

あなたのリポジトリにおけるコミットの歴史は、実際に発生したできごとの記録 だと見ることもできます。 これは歴史文書であり、それ自体に意味がある。従って、改ざんなど許されないという観点です。 この観点に沿って考えると、コミットの歴史を変更することなどあり得ないでしょう。 実際に起こってしまったことには、ただ黙って 従う べきです。 マージコミットのせいで乱雑になってしまったら? 実際そうなってしまったのだからしょうがない。 その記録は、後世の人々に向けてそのまま残しておくべきでしょう。

別の見方もあります。コミットの歴史は、そのプロジェクトがどのように作られてきたのかを表す物語である という考えかたです。 最初の草稿の段階で本を出版したりはしないでしょう。また、自作ソフトウェア用の管理マニュアルであれば、しっかり推敲する必要があります。 この立場に立つと、リベースやブランチフィルタリングを使って、将来の読者にとってわかりやすいように、物語を再編しようという考えに至ります。

さて、元の問いに戻ります。 マージとリベースではどちらがいいのか。 お察しのとおり、単純にどちらがよいとは言い切れません。 Git は強力なツールで、歴史に対していろんな操作をすることができます。しかし、チームやプロジェクトによって、事情はそれぞれ異なります。 あなたは既に、両者の特徴を理解しています。あなたが今いる状況ではどちらがより適切なのか、それを判断するのはあなたです。

一般論として、両者のいいとこどりをしたければ、まだプッシュしていないローカルの変更だけをリベースするようにして、 歴史をきれいに保っておきましょう。プッシュ済みの変更は決してリベースしないようにすれば、問題はおきません。

scroll-to-top