チャプター ▾ 第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. 別のトピックブランチから分岐したトピックブランチを持つ履歴

リリースに向けてクライアント側の変更をメインラインにマージしたいが、サーバー側の変更はさらにテストするまで待機したいとします。serverにないclient上の変更(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 <basebranch> <topicbranch>を実行することで、最初にチェックアウトすることなく、serverブランチをmasterブランチにリベースできます。これは、トピックブランチ(この場合はserver)をチェックアウトし、それをベースブランチ(master)に再適用します。

$ git rebase master server

これは、masterブランチの上にserverブランチをリベースするに示されているように、masterでの作業の上にserverでの作業を再適用します。

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

次に、ベースブランチ(master)をファストフォワードできます。

$ git checkout master
$ git merge server

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

$ 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は以下を行います。

  • 自分のブランチに固有の作業(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を実行することを知っていることを確認してください。

リベース vs. マージ

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

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

対立する見方は、コミット履歴はプロジェクトがどのように作成されたかの物語であるというものです。本の初稿を出版しないでしょうから、なぜ乱雑な作業を見せる必要があるのでしょうか?プロジェクトに取り組んでいるとき、すべての誤りや行き詰まった道の記録が必要かもしれませんが、自分の作業を世界に示すときには、AからBへのより一貫した物語を伝えたいと思うかもしれません。この派の人々は、メインラインブランチにマージされる前に、rebasefilter-branchのようなツールを使用してコミットを書き換えます。彼らはrebasefilter-branchのようなツールを使用して、将来の読者にとって最善の方法で物語を伝えます。

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

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

scroll-to-top