git rebase コマンドは、初心者は避けるべき魔術的な Git ブードゥーであるという評判を得ていますが、実際には、慎重に使用すれば開発チームの作業を非常に容易にしてくれます。この記事では、git rebase と関連する git merge コマンドを比較し、典型的な Git ワークフローにリベースを組み込める可能性のあるすべての機会を特定します。

考え方の概要

git rebase で最初に理解すべきことは、git merge と同じ問題を解決するということです。これらのコマンドは両方とも、変更を 1 つのブランチから別のブランチに統合することを目的に設計されています。ただし、実現する方法は非常に異なっています。

専用ブランチの新しいフィーチャーで作業を開始し、その後で別のチームメンバーが master ブランチを新しいコミットで更新すると何が生じるか考えてみましょう。結果として、フォーク済み履歴が作成されます。これは、コラボレーションツールとして Git を使用したことがある人なら誰でもよく知っています。

A forked commit history

ここで、master の新しいコミットが作業中のフィーチャーに関連しているとします。新しいコミットを自分の フィーチャーブランチに取り込むには、マージまたはリベースの 2 つのオプションがあります。

マージオプション

最も容易なオプションは、次のようなコマンドを使用して master ブランチをフィーチャーブランチにマージすることです。

git checkout feature
git merge master

または、これを 1 本の直線に凝縮することができます。

git merge feature master

これは、両方のブランチの履歴を連結する新しい「マージコミット」を feature ブランチに作成します。ブランチの構造は次のようになります。

Merging master into the feature branch

マージは非破壊的な操作であるため問題ありません。既存のブランチは決して変更されません。これにより、リベースの潜在的な落とし穴がすべて回避されます (後述)。

逆に言えば、これは上流の変更を取り込む必要が生じるたびに feature ブランチに無関係なマージコミットが作成されることも意味します。master が非常に活発な場合、フィーチャーブランチの履歴がかなり乱雑になる可能性があります。高度な git log オプションでこの問題を軽減することはできますが、他の開発者にはプロジェクトの履歴が理解しづらくなる可能性があります。

リベースオプション

マージに変わる方法として、次のコマンドを使用し、feature ブランチを master ブランチにリベースできます。

git checkout feature
git rebase master

これにより、フィーチャーブランチ全体がマスターブランチの先端から開始され、新しいコミットのすべてを効率的にマスターに組み込むことができます。しかし、リベースは、マージコミットを使用する代わりに、元のブランチでコミットごとにまったく新しくコミットを作成することによってプロジェクト履歴を再書き込みします。

Rebasing the feature branch onto master

リベースの主な利点は、プロジェクト履歴が非常にすっきりすることです。第一に、git merge が必要とする不要なマージコミットが除去されます。第二に、上記の図からわかるように、リベースによって、履歴は完全に直線的になります。フィーチャーの先端からプロジェクトの開始までずっとフォークなしにたどっていくことができます。このため、git loggit bisect、および gitk のようなコマンドでプロジェクトをナビゲートすることが容易になります。

しかし、このすっきりとしたコミット履歴には、安全性とトレーサビリティという 2 つのトレードオフがあります。リベースの黄金律 に従わない場合、プロジェクト履歴を書き換えるとコラボレーションワークフローに大打撃を与える恐れがあります。さらに、重要度は低くなりますが、リベースはマージコミットによって提供されるコンテキストを失います。このため、ユーザーは上流の変更がいつフィーチャーに組み込まれたのかわからなくなります。

対話式リベース

対話式リベースでは、コミットが新しいブランチに移動するときにコミットを変更する機会があります。これは、ブランチのコミット履歴を完全に制御できるという点で、自動化されたリベースよりもはるかに強力です。一般的に、これはフィーチャーブランチを master にマージする前に乱雑な履歴をクリーンアップするために使用されます。

対話式リベースのセッションを開始するには、i オプションを git rebase コマンドに渡します。

git checkout feature
git rebase -i master

これにより、移動しようとしているすべてのコミットがリストされたテキストエディターが開きます。

pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

このリストは、リベース実行後のブランチの表示を正確に定義しています。pick コマンドを変更し、エントリーを並び替える (またはこのいずれかを行う) ことで、ブランチの履歴の表示を必要に応じて変えられます。たとえば、2 番目のコミットが最初のコミットの小さな問題を修正している場合、fixup コマンドを使用して単一のコミットに圧縮できます。

pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

ファイルを保存して閉じると、Git はユーザーの指示に従ってリベースを実行します。その結果、プロジェクト履歴は次のようになります。

Squashing a commit with an interactive rebase

このような重要でないコミットを削除すると、フィーチャーの履歴がはるかに理解しやすくなります。これは git merge ではまったくできないことです。

リベースの黄金律

リベースの特徴を理解できたら、次に最も重要なことは、実行してはいけないときを知ることです。git rebase の黄金律は、リベースを public ブランチでは決して使用しないことです。

たとえば、master を自分の フィーチャーブランチにリベースした場合に生じることを考えてみましょう。

Rebasing the master branch

リベースは master のすべてのコミットを feature の先端に移動します。問題は、これがあなたのリポジトリのみで生じたことです。その他のすべての開発者は依然として元の master で作業しています。リベースはまったく新しくコミットを作成することになるので、Git はあなたの master ブランチの履歴は他のすべての人の履歴から分岐したと考えます。

2 つの master ブランチを同期する唯一の方法は、マージして1 つに戻すことしかありませんが、その結果、マージコミットが追加され、さらに同じ変更を含むコミットが 2 セット生じます (元のブランチの変更と、リベースしたブランチの変更)。言うまでもなく、これは非常に紛らわしい状況です。

したがって、git rebase を実行する前に、常に「このブランチは他の人にも表示されているだろうか」と、自問してください。答えが「はい」の場合、キーボードから手を離し、非破壊的な変更方法 (git revert コマンドなど) を検討してください。「いいえ」の場合は、自由に履歴を再書き込みしても安全です。

プッシュの強制

リベースした master ブランチをリモートリポジトリにプッシュバックしようとしても、Git はその実行を許可しません。これはリベース済みのブランチがリモート側の master ブランチと競合するためです。しかし、次のように --force フラグを渡すことによって、プッシュの実行を強制することが可能です。

# このコマンドの使用には注意が必要です!
git push --force

これは、使用中のリポジトリからリベースしたブランチと一致するようにリモート側のマスターブランチを上書きし、チームの他のメンバーに多大な混乱を招くことになります。したがって、自分が何をしようとしているか正確にわかっている場合にのみ、細心の注意を払ってこのコマンドを使用してください。

強制的にプッシュする必要があるのは、(バックアップ目的などで) プライベートフィーチャーブランチをリモートリポジトリにプッシュしたにローカルクリーンアップを実行した場合です。これは、「しまった、このフィーチャーブランチの元のバージョンではなく、こっちの現在のブランチをプッシュしたかったのに」と言っているようなものです。ここで重要なのは、フィーチャーブランチの元のバージョンのコミットから作業している人は誰もいないことです。

ワークフローの手引き

リベースは、チームが快適に感じる程度に応じて、既存の Git ワークフローに組み込むことができます。このセクションでは、フィーチャーの発展のさまざまな段階でリベースがもたらすメリットを見ていきます。

git rebase を利用するあらゆるワークフローでの最初のステップは、各フィーチャーの専用ブランチを作成することです。これにより、リベースを安全に使用するために必要なブランチ構造が得られます。

Developing a feature in a dedicated branch

ローカルクリーンアップ

ワークフローにリベースを組み込む最良の方法の 1 つは、ローカルの進行中のフィーチャーをクリーンアップすることです。対話式リベースを定期的に実行することで、フィーチャーブランチ内の各コミットに焦点を当て、意義があることを確認できます。これにより、コードを分割して個別にコミットすることを心配せずにコードを記述できます。コードは後で修正できます。

git rebase を呼び出す場合、新規ベース用に 2 つのオプションがあります。フィーチャーの親ブランチ (master など)、またはフィーチャー内の以前のコミットです。最初のオプションの例は、対話式リベースセクションで確認しました。後者のオプションは、最新のいくつかのコミットの修正のみ行う場合に便利です。たとえば、次のコマンドは最新の 3 つのコミットに対してのみ対話式リベースを開始します。

git checkout feature
git rebase -i HEAD~3

新規ベースとして HEAD~3 を指定することによって、実際にはブランチを移動しておらず、単にそのブランチの後に続く 3 つのコミットを対話式に再書き込みしているだけです。これは、上流の変更をフィーチャーブランチに取り込まないので注意してください。

Rebasing onto Head~3

この方法でフィーチャー全体を再書き込みする場合、git merge-base コマンドを使用してフィーチャーブランチの元のベースを見つけると便利です。次のコマンドは、元のベースのコミット ID を返します。これを git rebase に渡すことができます。

git merge-base feature master

このような対話式リベースの利用は、ローカルブランチにのみ影響を与えるため、git rebase をワークフローに導入するには優れた方法です。他の開発者に表示されるのはあなたの完了したプロダクトのみであり、必然的にフィーチャーブランチ履歴はすっきりとして、追跡しやすくなります。

しかし、これもプライベートフィーチャーブランチに関してのみ有効です。同じフィーチャーブランチを介して他の開発者とコラボレーションしている場合、そのブランチはパブリックであり、その履歴を書き換えることはできません。

git merge には、対話式リベースによるローカルコミットのクリーンアップに代わる方法はありません。

上流の変更をフィーチャーに取り込む

考え方の概要」セクションでは、フィーチャーブランチが git merge または git rebase のいずれかを使用して、master から上流の変更を取り込めることを確認しました。マージは自分のリポジトリの全履歴を保持する安全なオプションですが、リベースは、フィーチャーブランチを master の先端に移動することで直線的な履歴を作成します。

このような git rebase の利用は、ローカルクリーンアップに似ています (さらに、同時に実行可能です) が、処理中に master から上流のコミットを取り込みます。

master の代わりにリモートブランチにリベースすることは、まったく正しいことを覚えておいてください。同じフィーチャーで別の開発者とコラボレーションしていて、その変更を自分のリポジトリに取り込む必要がある場合にこのようなことが生じる可能性があります。

たとえば、あなたと John という名前の別の開発者がコミットをフィーチャーブランチに追加した場合、リモート側のフィーチャーブランチを John のリポジトリから取り出した後のあなたのリポジトリは、次のようになります。

Collaborating on the same feature branch

このフォークは、master の上流の変更を統合する場合とまったく同じ方法で解決できます。つまり、ローカルのフィーチャーJohn のフィーチャーをマージするか、ローカルのフィーチャーJohn のフィーチャーの先端にリベースします。

Merging vs. rebasing onto a remote branch

このリベースでは、あなたのローカルのフィーチャーコミットのみが移動し、その前にあるものはそのままであるため、リベースの黄金律に違反しません。これは、「私の変更内容を John が既に行ったことに追加する」と言っているのと同じです。ほとんどの状況で、これはマージコミットを介してリモートブランチと同期するよりも直感的です。

既定では、git pull コマンドはマージを実行しますが、--rebase オプションを渡すことで、リモートブランチをリベースで統合するように強制できます。

プルリクエストを使用してフィーチャーをレビューする

コードレビュープロセスの一環としてプルリクエストを使用する場合は、プルリクエストを作成した後に git rebase を使用しないでください。プルリクエストを行うとすぐに、他の開発者があなたのコミットを見ることになります。つまり、それは パブリック ブランチです。履歴を書き換えると、Git とチームメイトはフィーチャーに追加された後続のコミットを追跡できなくなります。

他の開発者の変更は git merge で取り込む必要があり、git rebase ではありません。

このため、プルリクエストを送信する前に、対話式リベースを使用してコードをクリーンアップすることが一般的に勧められます。

承認済みフィーチャーの統合

フィーチャーがチームに承認された後、そのフィーチャーを master ブランチの先端にリベースしてから git merge を使用してフィーチャーをメインのコードベースに統合するというオプションがあります。

これは上流の変更をフィーチャーブランチに組み込むのと似た状況ですが、master ブランチではコミットを書き換えることができないため、最終的に git merge を使用してフィーチャーを統合する必要があります。しかし、マージの前にリベースを実行するとマージが早送りされ、完全に線形の履歴を確実に得ることができます。また、これにより、プルリクエスト中に追加された後続のコミットをまとめて 1 つのコミットにする機会も得られます。

Integrating a feature into master with and without a rebase

git rebase に慣れていない人は、一時的なブランチでいつでもリベースを実行できます。そうすれば、誤ってフィーチャーの履歴を壊してしまった場合、元のブランチをチェックアウトしてやり直すことができます。例:

git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [Clean up the history]
git checkout master
git merge temporary-branch

概要

ブランチのリベースを開始するために実際に知っておく必要のあることはこれだけです。不要なマージコミットのないすっきりした、直線的な履歴にしたいのであれば、別のブランチから変更を統合する際に、git merge ではなく、git rebase を使用するようにします。

反対に、プロジェクトの完全な履歴を保持し、パブリックコミットを再書き込みするリスクを回避したい場合は、git merge の一点張りで通すこともできます。いずれのオプションも完全に有効ですが、少なくともこれで git rebase の利点を活用するオプションを使用できるようになりました。