チャプター ▾ 第2版

3.6 Git ブランチ - リベース

リベース

Git には、あるブランチの変更を別のブランチに統合する主な方法が 2 つあります。それは mergerebase です。このセクションでは、リベースとは何か、その方法、なぜそれが非常に素晴らしいツールなのか、そしてどのような場合にリベースを使用すべきでないかについて学びます。

基本的なリベース

基本的なマージの以前の例に戻ると、作業を分岐させ、2 つの異なるブランチでコミットを行ったことがわかります。

Simple divergent history
図 35. 単純な分岐履歴

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

Merging to integrate diverged work history
図 36. 分岐した作業履歴を統合するためのマージ

しかし、別の方法もあります。C4 で導入された変更のパッチを取得し、C3 の上に再適用できます。Git では、これは *リベース* と呼ばれます。rebase コマンドを使用すると、あるブランチでコミットされたすべての変更を取得し、それらを別のブランチでリプレイできます。

この例では、experiment ブランチをチェックアウトし、次のコマンドで master ブランチにリベースします。

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

この操作は、2 つのブランチ(現在のブランチとリベースするブランチ)の共通の祖先に戻り、現在のブランチの各コミットによって導入された差分を取得し、それらの差分を一時ファイルに保存し、現在のブランチをリベース先のブランチと同じコミットにリセットし、最後に各変更を順番に適用することで機能します。

Rebasing the change introduced in `C4` onto `C3`
図 37. C4 で導入された変更を C3 の上にリベースする

この時点で、master ブランチに戻り、ファストフォワードマージを実行できます。

$ git checkout master
$ git merge experiment
Fast-forwarding the `master` branch
図 38. master ブランチをファストフォワードする

これで、C4' が指すスナップショットは、マージの例C5 が指していたものとまったく同じになりました。統合の最終的な結果に違いはありませんが、リベースによって履歴がよりクリーンになります。リベースされたブランチのログを調べると、線形履歴のように見えます。元の作業が並行して行われた場合でも、すべての作業が連続して行われたように見えます。

多くの場合、コミットがリモートブランチにクリーンに適用されることを確認するためにこれを行います。おそらく、あなたが貢献しようとしているが、あなたが保守していないプロジェクトの場合です。この場合、あなたはブランチで作業を行い、メインプロジェクトにパッチを提出する準備ができたら、作業を origin/master にリベースします。そうすれば、メンテナーは統合作業を行う必要がなく、ファストフォワードまたはクリーンな適用を行うだけです。

最終的に得られる最後のコミットが指すスナップショットは、リベースのリベースされたコミットの最後であろうと、マージ後の最終マージコミットであろうと、同じスナップショットであることに注意してください。異なるのは履歴だけです。リベースは、作業の変更を別の作業のラインに、導入された順序で再適用しますが、マージはエンドポイントを取り、それらを結合します。

より興味深いリベース

リベースの対象ブランチ以外のものにリベースをリプレイさせることもできます。例えば、別のトピックブランチから派生したトピックブランチの履歴のような履歴を考えてみましょう。プロジェクトにいくつかのサーバーサイド機能を追加するためにトピックブランチ (server) を分岐させ、コミットを行いました。次に、そこから分岐してクライアントサイドの変更 (client) を行い、数回コミットしました。最後に、server ブランチに戻って、さらにいくつかコミットを行いました。

A history with a topic branch off another topic branch
図 39. 別のトピックブランチから派生したトピックブランチの履歴

クライアント側の変更をリリース用のメインラインにマージしたいが、サーバー側の変更はさらにテストするまで保留したいとします。client 上にあるが server にはない変更 (C8C9) を取得し、git rebase--onto オプションを使用して master ブランチにリプレイできます。

$ git rebase --onto master server client

これは基本的に、「`client` ブランチを取り、`server` ブランチから分岐してからのパッチを特定し、これらのパッチを `client` ブランチに、まるでそれが直接 `master` ブランチに基づいていたかのようにリプレイする」という意味です。少し複雑ですが、結果はかなりクールです。

Rebasing a topic branch off another topic branch
図 40. 別のトピックブランチからのトピックブランチをリベースする

これで、master ブランチを早送りできます (「client ブランチの変更を含めるように master ブランチを早送りする」を参照してください)。

$ git checkout master
$ git merge client
Fast-forwarding your `master` branch to include the `client` branch changes
図 41. client ブランチの変更を含めるように master ブランチを早送りする

server ブランチも取り込みたいとします。最初にチェックアウトすることなく、git rebase を実行して server ブランチを master ブランチにリベースできます。これは、トピックブランチ (この場合は server) をチェックアウトし、ベースブランチ (master) の上にリプレイします。

$ git rebase master server

これは、`server` ブランチを `master` ブランチの上にリベースするに示すように、`master` の作業の上に `server` の作業をリプレイします。

Rebasing your `server` branch on top of your `master` branch
図 42. server ブランチを master ブランチの上にリベースする

その後、ベースブランチ (master) を早送りできます。

$ git checkout master
$ git merge server

これで、すべての作業が統合され、不要になったため、client および server ブランチを削除できます。このプロセス全体の履歴は、最終コミット履歴のようになります。

$ git branch -d client
$ git branch -d server
Final commit history
図 43. 最終コミット履歴

リベースの危険性

ああ、しかしリベースの至福には欠点がないわけではありません。それは次の一言に要約できます。

リポジトリ外に存在し、他の人が作業のベースにした可能性のあるコミットはリベースしないでください。

このガイドラインに従えば、大丈夫です。もし従わなければ、人々はあなたを憎み、友人や家族から軽蔑されるでしょう。

リベースを行うと、既存のコミットを破棄し、類似しているが異なる新しいコミットを作成します。コミットをどこかにプッシュし、他の人がそれらをプルダウンして作業のベースにした後、git rebase でそれらのコミットを書き換え、再度プッシュすると、協力者は作業を再マージしなければならなくなり、彼らの作業を自分の作業に戻そうとすると事態が混乱します。

公開した作業をリベースすると、どのように問題を引き起こすか例を見てみましょう。中央サーバーからクローンし、そこから作業を行うとします。コミット履歴は次のようになります。

Clone a repository, and base some work on it
図 44. リポジトリをクローンし、その上に作業をベースにする

さて、誰かがさらに作業を行い、マージを含め、その作業を中央サーバーにプッシュします。それをフェッチし、新しいリモートブランチを作業にマージすると、履歴は次のようになります。

Fetch more commits, and merge them into your work
図 45. さらにコミットをフェッチし、それらを自分の作業にマージする

次に、マージされた作業をプッシュした人が、代わりに自分の作業をリベースすることに決めます。彼らは git push --force を実行してサーバー上の履歴を上書きします。あなたはその後、そのサーバーからフェッチし、新しいコミットをダウンロードします。

Someone pushes rebased commits, abandoning commits you’ve based your work on
図 46. 誰かがリベースされたコミットをプッシュし、あなたが作業のベースにしたコミットを破棄する

さて、あなたは二人とも窮地に陥っています。git pull を実行すると、両方の履歴ラインを含むマージコミットが作成され、リポジトリは次のようになります。

You merge in the same work again into a new merge commit
図 47. 同じ作業を新しいマージコミットに再度マージする

履歴がこの状態のときに git log を実行すると、作者、日付、メッセージが同じコミットが2つ表示され、混乱するでしょう。さらに、この履歴をサーバーにプッシュし直すと、リベースされたすべてのコミットを中央サーバーに再導入することになり、さらに人々を混乱させる可能性があります。他の開発者が履歴に C4C6 を入れたくないと考えているのは、彼らが最初にリベースした理由なので、かなり安全に推測できます。

リベースするときはリベースする

このような状況に陥った場合でも、Gitには役立つ魔法があります。もしチームの誰かが、あなたがベースにしていた作業を上書きする変更を強制プッシュした場合、あなたの課題は、何があなたのものか、何が彼らによって書き換えられたかを把握することです。

コミットの SHA-1 チェックサムに加えて、Git はコミットで導入されたパッチのみに基づいてチェックサムも計算します。これは「パッチID」と呼ばれます。

書き換えられた作業をプルダウンし、パートナーからの新しいコミットの上にリベースすると、Git は多くの場合、独自にあなたのものであるものを正確に特定し、新しいブランチの上にそれらを再適用することができます。

たとえば、前のシナリオで、誰かがリベースされたコミットをプッシュし、あなたが作業のベースにしたコミットを破棄するの状態でマージを行う代わりに git rebase teamone/master を実行すると、Git は次のようになります。

  • 自分のブランチに固有の作業を特定する (C2, C3, C4, C6, C7)

  • マージコミットではないものを判断する (C2, C3, C4)

  • ターゲットブランチに書き換えられていないものを判断する (C4C4' と同じパッチなので、C2C3 のみ)

  • これらのコミットを teamone/master の上部に適用する

Rebase on top of force-pushed rebase work
図 48. 強制プッシュされたリベース作業の上にリベースする

これは、パートナーが作成した C4C4' がほぼ完全に同じパッチである場合にのみ機能します。そうでなければ、リベースはそれが重複していることを識別できず、別の C4 のようなパッチを追加します(変更がすでに少なくともある程度存在するため、クリーンに適用できない可能性が高いです)。

通常の git pull の代わりに git pull --rebase を実行することで、これを簡素化することもできます。または、この場合、git fetch の後に git rebase teamone/master を手動で実行することもできます。

git pull を使用していて、--rebase をデフォルトにしたい場合は、git config --global pull.rebase true のように pull.rebase の設定値を設定できます。

自分のコンピュータから一度も出ていないコミットのみをリベースする限りは、問題ありません。プッシュされたが、他の誰もコミットのベースにしていないコミットをリベースする場合も問題ありません。すでに公開され、他の人がそのコミットをベースに作業している可能性のあるコミットをリベースすると、イライラする問題に巻き込まれ、チームメイトから軽蔑される可能性があります。

もしあなたやパートナーが、ある時点でどうしても必要だと感じた場合は、全員に git pull --rebase を実行して、発生後の苦痛を少しでも和らげるように周知徹底してください。

リベース vs. マージ

リベースとマージの動作を見てきたので、どちらが良いのか疑問に思っているかもしれません。これに答える前に、少し立ち止まって履歴が何を意味するかについて話しましょう。

これに対する1つの見方は、リポジトリのコミット履歴は**実際に起こったことの記録**であるというものです。それはそれ自体で価値のある歴史的な文書であり、改ざんされるべきではありません。この観点からすると、コミット履歴を変更することはほとんど冒涜的です。あなたは実際に起こったことについて**嘘をついている**ことになります。では、一連の汚いマージコミットがあったらどうでしょうか?それが起こった方法であり、リポジトリはそれを後世のために保存すべきです。

反対の視点は、コミット履歴は**プロジェクトがどのように作られたかという物語**であるというものです。本の初稿を公開しないように、なぜ汚い作業を見せる必要があるのでしょうか?プロジェクトに取り組んでいる間は、すべての失敗や行き止まりの道の記録が必要かもしれませんが、作業を世に発表する際には、AからBに到達するまでのより一貫した物語を語りたいと思うかもしれません。この考え方の人は、メインラインブランチにマージされる前にコミットを書き換えるために rebasefilter-branch のようなツールを使用します。彼らは rebasefilter-branch のようなツールを使用して、将来の読者にとって最適な方法で物語を語ります。

さて、マージとリベースのどちらが良いかという質問ですが、それがそれほど単純ではないことがお分かりいただけたでしょうか。Gitは強力なツールであり、履歴に対して多くのことを行うことを可能にしますが、チームやプロジェクトはそれぞれ異なります。両方の仕組みが分かったので、あなたの特定の状況にどちらが最適かを決定するのはあなた次第です。

両方の良いとこ取りができます。プッシュする前にローカルの変更をリベースして作業をクリーンアップしますが、どこかにプッシュしたものは決してリベースしないでください。

scroll-to-top