Git
目次 ▾ 第2版

3.6 Gitブランチ - リベース

リベース

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

基本的なリベース

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

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

既に説明したように、ブランチを統合する最も簡単な方法は`merge`コマンドです。これは、2つの最新のブランチのスナップショット(`C3`と`C4`)と、その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`ブランチの高速フォワード

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

多くの場合、コミットがリモートブランチにクリーンに適用されるようにするためにこれを行います。これは、貢献しようとしているが、維持管理していないプロジェクトの場合です。この場合、ブランチで作業を行い、メインプロジェクトにパッチを送信する準備ができたら、作業を`origin/master`にリベースします。このようにすると、メンテナは統合作業を行う必要がなくなり、高速フォワードまたはクリーンな適用だけで済みます。

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

より高度なリベース

リベースの再適用先をリベースターゲットブランチ以外にすることもできます。別のトピックブランチから分岐した履歴を例にとってみましょう。サーバーサイドの機能をプロジェクトに追加するためにトピックブランチ(`server`)を分岐し、コミットを行いました。その後、クライアントサイドの変更を行うためにそこから分岐し(`client`)、数回コミットしました。最後に、`server`ブランチに戻ってさらにいくつかのコミットを行いました。

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

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

$ git rebase --onto master server client

これは基本的に、「clientブランチを取り、serverブランチから分岐してからのパッチを特定し、これらのパッチをclientブランチに、masterブランチを直接ベースとしたかのように適用する」という意味です。少し複雑ですが、結果は非常に効果的です。

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

これで、masterブランチをfast-forwardできます(masterブランチをfast-forwardしてclientブランチの変更を含めるを参照)。

$ git checkout master
$ git merge client
Fast-forwarding your `master` branch to include the `client` branch changes
図41. masterブランチをfast-forwardしてclientブランチの変更を含める

serverブランチも取り込むことにするとします。git rebase <basebranch> <topicbranch>を実行することで、最初にチェックアウトすることなく、masterブランチ上にserverブランチをリベースできます。これは、トピックブランチ(この場合はserver)をチェックアウトし、ベースブランチ(master)に適用します。

$ git rebase master server

これにより、serverブランチをmasterブランチの上にリベースする図に示すように、master作業の上にserver作業が適用されます。

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

その後、ベースブランチ(master)をfast-forwardできます。

$ 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はコミットで導入されたパッチに基づいてチェックサムも計算します。これは「patch-id」と呼ばれます。

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

たとえば、前のシナリオで、ユーザーがリベースされたコミットをプッシュし、作業のベースとしていたコミットを破棄する時点でマージする代わりにgit rebase teamone/masterを実行すると、Gitは

  • ブランチに固有の作業(C2C3C4C6C7)を特定します。

  • マージコミットでないものを特定します(C2C3C4)。

  • ターゲットブランチに書き換えられていないものを特定します(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を実行することを知らせましょう。

リベースとマージ

リベースとマージが実際にどのように機能するかを見たので、どちらが良いのか疑問に思うかもしれません。これについて答える前に、少し立ち戻って、履歴の意味について説明しましょう。

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

反対の見方は、コミット履歴はプロジェクトの作成方法に関する物語であるということです。本の最初の草稿を公開しないのと同じように、散らかった作業を見せるのはなぜでしょうか?プロジェクトに取り組んでいるときは、すべての誤りや行き詰まった道の記録が必要になる場合がありますが、作業を世間に公開する準備ができたときは、AからBに到達する方法について、より一貫した物語を伝えたいと思うかもしれません。この陣営の人々は、rebasefilter-branchなどのツールを使用して、メインラインブランチにマージする前にコミットを書き換えます。彼らはrebasefilter-branchなどのツールを使用して、将来の読者にとって最適な方法で物語を伝えます。

では、マージとリベースのどちらが良いかという質問についてですが、それほど単純ではないことがわかるでしょう。Gitは強力なツールであり、履歴に対して多くのことができますが、すべてのチームとすべてのプロジェクトが異なるものです。これらの両方がどのように機能するかを知ったので、状況に応じてどちらが最適かを決定するのはあなた次第です。

両方の利点を活かすことができます。作業を整理するために、プッシュする前にローカルの変更をリベースしますが、どこかにプッシュしたものは決してリベースしません。

scroll-to-top