マージとリベース
git rebase コマンドは、初心者は避けるべき Git の魔法の呪文であるという評判を得ていますが、実際には、慎重に使用すれば開発チームの作業を非常に容易にしてくれます。この記事では、git rebase を関連する git merge コマンドと比較し、典型的な Git ワークフローにリベースを組み込める可能性のあるすべての機会を特定します。
考え方の概要
git rebase で最初に理解すべきことは、git merge と同じ問題を解決するということです。これらのコマンドは両方とも、変更を1つのブランチから別のブランチに統合することを目的に設計されています。ただし、実現する方法は非常に異なっています。
専用ブランチの新しいフィーチャーで作業を開始した後で別のチーム メンバーが main ブランチを新しいコミットで更新すると、何が起きるかを考えてみましょう。答えは、フォーク済み履歴が作成されることです。これは、コラボレーション ツールとして Git を使用したことがある人なら誰でもよく知っています。
ここで、main の新しいコミットが作業中のフィーチャーに関連しているとします。新しいコミットを自分の feature ブランチに取り込むには、マージまたはリベースの 2 つのオプションがあります。
マージ・オプション
最も簡単なオプションは、次のようなコマンドを使用して main ブランチを feature ブランチにマージすることです。
git checkout feature
git merge mainまたは、これを1本の直線に凝縮することができます。
git merge feature mainこれは、両方のブランチの履歴を連結する新しい「マージコミット」を feature ブランチに作成します。ブランチの構造は次のようになります。
マージは非破壊的な操作であるため安心です。既存のブランチは決して変更されません。これにより、リベースの潜在的な落とし穴がすべて回避されます (後述)。
逆に言えば、これは上流の変更を取り込む必要が生じるたびに feature ブランチに無関係なマージ コミットが作成されることも意味します。main が非常に活発な場合は、feature ブランチの履歴がかなり乱雑になりかねません。高度な git log オプションでこの問題を軽減できるものの、他の開発者にはプロジェクトの履歴が理解しづらくなる可能性があります。
リベース・オプション
マージに代わる方法として、次のコマンドを使用して feature ブランチを main ブランチにリベースできます。
git checkout feature
git rebase mainこれによって、feature ブランチ全体が main ブランチの先端から開始されて、新しいコミットのすべてを効率的にマスターに組み込めます。しかし、リベースはマージ コミットを使用する代わりに、元のブランチでコミットごとにまったく新しいコミットを作成することによってプロジェクト履歴を再書き込みします。
リベースの主な利点は、プロジェクト履歴が非常にすっきりすることです。第一に、git merge が必要とする不要なマージコミットが除去されます。第二に、上記の図からわかるように、リベースによって、履歴は完全に直線的になります。フィーチャーの先端からプロジェクトの開始までずっとフォークなしにたどっていくことができます。このため、git log、git bisect、および gitk のようなコマンドでプロジェクトをナビゲートすることが容易になります。
しかし、この初期のコミット履歴には、安全性とトレーサビリティという2つのトレードオフがあります。リベースの黄金律 に従わない場合、プロジェクト履歴を書き換えるとコラボレーションワークフローに大打撃を与える恐れがあります。さらに、重要度は低くなりますが、リベースはマージコミットによって提供されるコンテキストを失います。このため、ユーザーは上流の変更がいつフィーチャーに組み込まれたのかわからなくなります。
対話式リベース
対話式リベースでは、コミットが新しいブランチに移動するときにコミットを変更する機会があります。これは、ブランチのコミット履歴を完全に制御できるという点において、自動化されたリベースよりもはるかに優れています。一般的には、feature ブランチを main にマージする前に乱雑な履歴をクリーン アップするために使用されます。
対話式リベースのセッションを開始するには、i オプションを git rebase コマンドに渡します。
git checkout feature
git rebase -i mainこれにより、移動しようとしているすべてのコミットがリストされたテキストエディタが開きます。
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 はユーザーの指示に従ってリベースを実行します。その結果、プロジェクト履歴は次のようになります。
このような重要でないコミットを削除すると、フィーチャーの履歴がはるかに理解しやすくなります。これは git merge ではまったくできないことです。
リベースの黄金律
リベースの特徴を理解できたら、次に最も重要なことは、実行してはいけないときを知ることです。git rebase の黄金律は、リベースを public ブランチでは決して使用しないことです。
たとえば、main を自分の feature ブランチにリベースした場合に生じることを考えてみましょう。
リベースは main のすべてのコミットを feature の先端に移動します。問題は、これがあなたのリポジトリのみで発生したことです。その他のすべての開発者は依然として元の main で作業しています。リベースはまったく新しいコミットを作成することになるので、Git ではあなたの main ブランチの履歴は他のすべての人の履歴から分岐したと判断します。
2 つの main ブランチを同期する唯一の方法はマージして 1 つに戻すことですが、その結果としてマージ コミットが追加されて、さらに同じ変更を含む 2 セットのコミットが生成されます (元のブランチの変更とリベースしたブランチの変更)。非常に紛らわしい状況であることは言うに及びません。
したがって、git rebase を実行する前に、常に「このブランチは他の人にも表示されているだろうか」と、自問してください。答えが「はい」の場合、キーボードから手を離し、非破壊的な変更方法 (git revert コマンドなど) を検討してください。「いいえ」の場合は、自由に履歴を再書き込みしても安全です。
プッシュの強制
リベースした main ブランチをリモート リポジトリにプッシュ バックしようとしても、リベース済みのブランチがリモート側の main ブランチと競合するため、Git はその実行を許可しません。しかし、次のように --force フラグを渡すことによって、プッシュの実行を強制できます。
# Be very careful with this command! git push --forceこれは、自分のリポジトリからリベースしたブランチと一致するようにリモート側の main ブランチを上書きして、チームの他のメンバーに多大な混乱を招くことになります。したがって、自分が何をしようとしているかを正確に把握している場合にのみ、細心の注意を払ったうえでこのコマンドを使用してください。
強制的にプッシュする必要があるのは、(バックアップ目的などで) プライベートフィーチャーブランチをリモートリポジトリにプッシュした後、ローカルクリーンアップを実行した場合です。これは、「しまった、フィーチャーブランチの元のバージョンではなく、こっちの現在のブランチをプッシュしたかったのに」と言っているようなものです。ここで重要なのは、フィーチャーブランチの元のバージョンのコミットから作業している人は誰もいないことです。
ワークフローの手引き
リベースは、チームが快適に感じる程度に応じて、既存の Git ワークフローに組み込むことができます。このセクションでは、機能の開発の様々な段階でリベースがもたらすメリットを見ていきます。
git rebase を利用するあらゆるワークフローでの最初のステップは、各フィーチャーの専用ブランチを作成することです。これにより、リベースを安全に使用するために必要なブランチ構造が得られます。
ローカル・クリーンアップ
ワークフローにリベースを組み込む最良の方法の1つは、ローカルの進行中のフィーチャーをクリーンアップすることです。対話式リベースを定期的に実行することで、フィーチャーブランチ内の各コミットに焦点を当て、意義があることを確認できます。これにより、コードを分割して個別にコミットすることを心配せずにコードを記述できます。コードは後で修正できます。
git rebase を呼び出す場合は、フィーチャーの親ブランチ (main など) とフィーチャー内の以前のコミットと、新規ベース用に 2 つのオプションがあります。最初のオプションの例は、対話式リベース セクションで確認しました。後者のオプションは、最新のいくつかのコミットを修正する必要があるのみの場合に便利です。たとえば、次のコマンドは最新の 3 つのコミットに対してのみ対話式リベースを開始します。
git checkout feature git rebase -i HEAD~3新規ベースとして HEAD~3 を指定することによって、実際にはブランチを移動しておらず、単にそのブランチの後に続く3つのコミットを対話式に再書き込みしているだけです。これは、上流の変更をフィーチャーブランチに取り込まないので注意してください。
この方法でフィーチャー全体を書き換える場合、git merge-base コマンドを使用して feature ブランチの元のベースを見つけると便利です。次のコマンドは、元のベースのコミット ID を返します。これを git rebase に渡すことができます。
git merge-base feature mainこのような対話式リベースの利用は、ローカルブランチにのみ影響を与えるため、git rebase をワークフローに導入するには優れた方法です。他の開発者に表示されるのは、完了した製品のみであり、必然的にフィーチャーブランチ履歴はすっきりとして、追跡しやすくなります。
しかし、これもプライベートフィーチャーブランチに関してのみ有効です。同じフィーチャーブランチを介して他の開発者とコラボレーションしている場合、そのブランチはパブリックであり、その履歴を書き換えることはできません。
git merge には、対話式リベースによるローカルコミットのクリーンアップに代わる方法はありません。
上流の変更をフィーチャーに取り込む
「考え方の概要」セクションでは、feature ブランチが git merge または git rebase のいずれかを使用して、main から上流の変更を取り込めることを確認しました。マージは自分のリポジトリの全履歴を保持する安全なオプションで、リベースは feature ブランチを main の先端に移動することで直線的な履歴を作成します。
このような git rebase の使用はローカル クリーンアップに似ています (さらに同時に実行可能です) が、処理中に main から上流のコミットを取り込みます。
main の代わりにリモート ブランチにリベースすることは、完全に合理的であることを覚えておいてください。同じフィーチャーで別の開発者とコラボレーションしていてその変更を自分のリポジトリに取り込む必要がある場合に、このようなことが発生する可能性があります。
たとえば、あなたと John という名前の別の開発者がコミットを feature ブランチに追加した場合、リモート側の feature ブランチを John のリポジトリから取り出した後のあなたのリポジトリは、次のようになります。
このフォークは、main の上流の変更を統合する際とまったく同じ方法で解決できます。つまり、ローカルのフィーチャーと John のフィーチャーをマージするか、ローカルのフィーチャーを John のフィーチャーの先端にリベースします。
このリベースでは、あなたのローカルの feature コミットのみが移動し、その前にあるものはそのままであるため、リベースの黄金律に違反しません。これは、「私の変更内容を John が既に行ったことに追加する」と言っているのと同じです。ほとんどの状況で、これはマージコミットを介してリモートブランチと同期するよりも直感的です。
既定では、git pull コマンドはマージを実行しますが、--rebase オプションを渡すことで、リモートブランチをリベースで統合するように強制できます。
プル・リクエストを使用してフィーチャーをレビューする
コードレビュープロセスの一環としてプルリクエストを使用する場合は、プルリクエストを作成した後に git rebase を使用しないでください。プルリクエストを行うとすぐに、他の開発者があなたのコミットを見ることになります。つまり、それは パブリック ブランチです。履歴を書き換えると、Git とチームメイトはフィーチャーに追加された後続のコミットを追跡できなくなります。
他の開発者の変更は git merge で取り込む必要があり、 git rebase ではありません。
このため、プルリクエストを送信する前に、対話式リベースを使用してコードをクリーンアップすることが一般的に勧められます。
承認済みフィーチャーの統合
フィーチャーがチームに承認された後は、そのフィーチャーを main ブランチの先端にリベースしてから git merge を使用してフィーチャーをメインのコードベースに統合するというオプションがあります。
これは上流の変更を feature ブランチに組み込むのと似た状況ですが、main ブランチではコミットを書き換えられないため、最終的に git merge を使用してフィーチャーを統合する必要があります。しかし、マージする前にリベースを実行するとマージが早送りされていると把握されて、完全に線形の (体系化された) 履歴を確実に得られます。また、これによって、プル リクエスト中に追加された後続のコミットをまとめて 1 つのコミットにする機会も得られます。
git rebase に慣れていない人は、一時的なブランチでいつでもリベースを実行できます。そうすれば、誤ってフィーチャーの履歴を壊してしまった場合、元のブランチをチェックアウトしてもう一度やり直すことができます。以下に例を示します。
git checkout feature
git checkout -b temporary-branch
git rebase -i main
# [Clean up the history]
git checkout main
git merge temporary-branch要約
ブランチのリベースを開始するために実際に知っておく必要があるのはこれだけです。不要なマージコミットのないすっきりした、直線的な履歴にしたいのであれば、別のブランチから変更を統合する際に、git merge ではなく、git rebase を使用するようにします。
反対に、プロジェクトの完全な履歴を保持し、パブリックコミットを再書き込みするリスクを回避したい場合は、git merge の一点張りで通すこともできます。いずれのオプションも完全に有効ですが、少なくともこれで git rebase の利点を活用するオプションを使用できるようになりました。