変更を統合する2つの方法
Gitのbranchで作業していると、あるbranchの変更を別のbranchに取り込む必要が出てきます。Gitはこのための主要な戦略を2つ提供しています:mergeとrebase。どちらも同じ最終結果を達成します -- branchにすべての最新の変更が含まれます -- しかし根本的に異なる方法で行われ、適切なものを選ぶことがプロジェクトの履歴やチームのworkflowにとって重要です。
Mergeの仕組み
mergeは2つのbranchを取り、新しいmerge commitを作成して統合します。このmerge commitには2つの親があります:現在のbranchの先端と、統合するbranchの先端です。
git checkout feature/login
git merge main
この後、フィーチャーbranchにはmainからのすべての変更がmerge commitで結合されて含まれます。両方のbranchの履歴は実際に起こったとおりに正確に保存されます。commitグラフを見ると、2つの開発ラインがmergeポイントで合流しているのがわかります。
mergeの主な特徴:
- 非破壊的。既存のcommitは一切変更されません。履歴は実際に起こったことそのものです。
- Merge commitを作成する。この追加のcommitが2つの履歴を結び付けます。
- コンテキストを保存する。branchがいつ作成され、いつ統合されたかを常に確認できます。
Fast-forward merge
branchがターゲットから分岐していない場合(branchを作成してからターゲットに新しいcommitがない場合)、Gitはデフォルトでfast-forward mergeを実行します。branchポインタを単純に前に進めるだけです。必要ないためmerge commitは作成されません。この場合でもmerge commitを強制することができます:
git merge --no-ff feature/login
mergeが些細なものであっても、作業が別のbranchで行われたという事実を保存したい場合に便利です。
Rebaseの仕組み
rebaseは異なるアプローチを取ります。merge commitを作成する代わりに、別のbranchの上にcommitを再適用します。結果として、ターゲットbranchの最新ポイントから作業を開始したように見える線形の履歴になります。
git checkout feature/login
git rebase main
裏側で起こっていること:
- Gitが2つのbranchの共通の祖先を見つけます。
- branchのcommitを一時的に取り除きます。
- branchポインタをmainの先端に移動します。
- commitを1つずつその上に再適用します。
結果の履歴は完全に線形です。merge commitなし、グラフで分岐して合流する線もなし。mainの最新の変更の後にすべてのコードを書いたように見えます。実際にはそうでなくても。
rebaseの主な特徴:
- 線形の履歴を作成する。commitグラフがきれいで読みやすくなります。
- Commitハッシュを書き換える。再適用されたcommitは、内容が同じでも新しいSHAハッシュを取得します。
- Merge commitなし。branchは追加のcommitなしできれいに統合されます。
Mergeを使うべき場面
mergeはいくつかの状況でより安全でわかりやすい選択です:
- 共有branch。複数の人が同じbranchで作業している場合、mergeは他の人が依存しているcommitを書き換えることなく全員の履歴を保存します。
- 履歴の保存が重要。変更がいつどのように統合されたかを正確に追跡する必要があるプロジェクトでは、mergeが完全で改変されていない記録を提供します。
- 長期branchの統合。リリースbranchをmainにmergeする場合や、大きく分岐した2つのbranchを結合する場合、merge commitが統合ポイントを明確にマークします。
- シンプルさを求める。mergeは概念的に簡単です。特にGitに慣れていない開発者にとって、問題が起こりにくいです。
Rebaseを使うべき場面
rebaseは、きれいで線形のプロジェクト履歴が欲しい場合に力を発揮します:
- フィーチャーbranchの更新。機能をmainにmergeする前に、最新のmainでrebaseすると、merge commitなしで直接適用できるきれいなcommitセットが得られます。
- きれいな履歴の維持。線形の履歴は
git logでのナビゲーションが容易で、バグ追跡時のbisectが容易で、pull requestでのレビューも容易です。 - ローカルでの整理。作業中に小さなインクリメンタルcommitを行っていた場合、branchを共有する前にrebaseで整理できます。
Rebaseの黄金ルール
共有branchにpushされたcommitは絶対にrebaseしない。
これは覚えておくべき最も重要なルールです。rebaseはcommitハッシュを書き換えるため、元のcommitに基づいて作業を行った他の人に問題が発生します。彼らの履歴はrebaseされた履歴と一致しなくなり、commitの重複、コンフリクト、混乱を引き起こします。
ルールはシンプルです:他の人があなたのcommitをpullした可能性がある場合、rebaseしないでください。rebaseは自分のローカルbranchのみ、または自分が唯一の貢献者であるbranchでのみ使用してください。
誤って共有branchをrebaseした場合、チームの他の開発者は分岐した履歴に対処する必要があります。これは通常、force-push(リモートを上書き)と、全員がローカルコピーをresetすることを意味します。混乱を招き、回避可能な事態です。
インタラクティブRebase:精密な履歴の書き換え
インタラクティブrebase(git rebase -i)は単なるcommitの移動にとどまりません。commit履歴そのものを変更できます:
git rebase -i HEAD~5
直近5件のcommitのリストが開き、以下の操作ができます:
- pick -- commitをそのまま保持
- reword -- commitメッセージを変更
- edit -- 一時停止してcommitを修正
- squash -- 前のcommitと結合
- fixup -- squashと同様だが、commitメッセージを破棄
- drop -- commitを完全に削除
- reorder -- 行を並べ替えて順序を変更
インタラクティブrebaseは、merge前の乱雑なbranchを整理するのに強力です。「タイポ修正」commitを親にsquashしたり、不明確なメッセージを書き直したり、commitを論理的なストーリーになるよう並べ替えたりできます。
実践的なワークフロー
多くのチームは両方の戦略のいいとこ取りをする組み合わせアプローチを使用しています:
- mainからフィーチャーbranchを作成する。
- 作業中、フィーチャーbranchを定期的にmainの上にrebaseして最新の状態を保つ。
- Pull requestを開く前に、インタラクティブrebaseでcommitを整理する。
- フィーチャーbranchをmainにmergeする(多くの場合、統合ポイントを示すためにmerge commitを使用)。
これにより、フィーチャーbranchではきれいで線形の履歴が、mainでは明確なmergeポイントが得られます。ほとんどのチームにとってうまく機能する実用的な中間点です。
GitSquidでのRebaseとMerge
GitSquidはインターフェースから直接mergeとrebaseの両方をサポートしています。commitグラフ上の任意のbranchラベルのコンテキストメニューからbranchをmergeできます。rebaseについては、GitSquidにはビジュアルなインタラクティブrebaseエディターが含まれており、ターミナルでテキストファイルを編集する代わりに、ドラッグ&クリックでcommitの並べ替え、squash、fixup、reword、dropができます。これにより、コマンドラインバージョンに不安を感じる方でもインタラクティブrebaseが使いやすくなります。
まとめ
| Merge | Rebase | |
|---|---|---|
| 履歴 | 非線形、branchを保存 | 線形、きれい |
| Commitハッシュ | 変更なし | 書き換えられる |
| Merge commit | あり(fast-forward以外) | なし |
| 共有branchで安全 | はい | いいえ |
| 最適な用途 | 共有作業の統合 | フィーチャーbranchの整理 |
mergeもrebaseもどちらが普遍的に優れているということはありません。異なる状況のためのツールです。それぞれが履歴にどう影響するかを理解し、rebaseの黄金ルールに従い、状況に合ったものを選びましょう。将来の自分がgit logを読むとき、感謝するはずです。