GitのRebaseかMerge?チームに合った戦略を選ぶ
1. 概要
git merge と git rebase はどちらもブランチ間の変更を統合するために使われますが、まったく異なるコミット履歴を生成します。戦略を間違えるとログが乱れ、レビューが難しくなり、さらに悪いことに同僚の履歴を破損させるかもしれません。この記事では、具体例を通じて各コマンドのメカニズムを理解し、いつ merge を使い、いつ rebase を使うべきか、そして team が rebase を使う際の一般的なエラーを避ける方法を説明します。
2. 例のシナリオ
main ブランチから feature/payment ブランチをチェックアウトして作業しているとしましょう。あなたがコードを書いている間に、同僚がすでに main にいくつかのコミットを merge しました。
git log --oneline --graph --all
* c3f9a1d (main) PaymentValidatorを追加
* 7b2e4aa (main) OrderServiceのバグを修正
| * 9d1a0ef (feature/payment) PaymentService.processPayment()を追加
| * 4f3c2bb (feature/payment) PaymentServiceを初期化
|/
* a1b2c3d 共通ベースコミット
2つのブランチは a1b2c3d から分岐しています。pull request を作成する前に、feature/payment を main に同期させたいとします。2つの方法があります:merge か rebase かです。
3. Git Merge:履歴を保持し、マージコミットを作成
3.1. 使用方法
git checkout feature/payment
git merge main
このコマンドは、2つの親を持つ新しい マージコミット を作成します。2つの親とは、feature/payment の最後のコミットと main の最後のコミットです。
* e5f6a7b (HEAD -> feature/payment) Merge branch 'main' into feature/payment
|\
| * c3f9a1d (main) PaymentValidatorを追加
| * 7b2e4aa (main) OrderServiceのバグを修正
* | 9d1a0ef PaymentService.processPayment()を追加
* | 4f3c2bb PaymentServiceを初期化
|/
* a1b2c3d 共通ベースコミット
3.2. 特徴
- 両方のブランチの元の履歴は 100%保持 されます。どのコミットも修正されません。
- 2つのブランチが統合されたタイミングを記録する追加のマージコミットが作られます。
- プッシュ済みのブランチでは完全に安全です。誰にも悪影響を与えません。
注意:
git mergeは既存のコミットの SHA を書き直すことはありません。これが shared なブランチに対して常に安全である理由です。
4. Git Rebase:履歴を書き直し、一直線のブランチを作成
4.1. 使用方法
git checkout feature/payment
git rebase main
Rebase は feature/payment 上のブランチ分岐点(a1b2c3d)から以降の各コミットを取り出し、main の先端に「再生」(replay)します。結果として、SHA が異なる新しいコミットが作られます。
* 2a9e1cc (HEAD -> feature/payment) PaymentService.processPayment()を追加
* d8b4f0a PaymentServiceを初期化
* c3f9a1d (main) PaymentValidatorを追加
* 7b2e4aa (main) OrderServiceのバグを修正
* a1b2c3d 共通ベースコミット
履歴は今や一直線(linear history)であり、マージコミットはありません。
4.2. 特徴
9d1a0efと4f3c2bbはもう存在しません。代わりに2a9e1ccとd8b4f0aという 新しい SHA のコミットになります。- ログがきれいで、読みやすく、
git bisectが簡単です。 - 他の人がすでにプルしたブランチの場合は危険です。彼らは「消滅した」古いコミットを持ったままです。
5. Conflictの処理:操作を行う際の最大の違い
merge の場合、conflict(ある場合)はマージコミットで 一度だけ 解決する必要があります:
git merge main
# CONFLICT (content): Merge conflict in PaymentService.java
# ... ファイルを修正して解決 ...
git add PaymentService.java
git commit
rebase の場合、複数のコミットがあると、再生中の 各コミットで conflict が発生する可能性があります:
git rebase main
# コミット 4f3c2bb で CONFLICT
# ファイルを修正して解決:
git add PaymentService.java
git rebase --continue
# 次のコミットも conflict があればこれを繰り返す
Rebase を中止して rebase 前の状態に戻したい場合:
git rebase --abort
注意: 10個のコミットを持つブランチを rebase するということは、場合によっては 10回も conflict を解決する必要があることを意味します。これが rebase 前に squash を行う(セクション 7 参照)ことが非常に有用である理由です。
6. 黄金律:共有(shared)ブランチを rebase しない
これは rebase を使う際の最も重要なルールです。
# main ブランチはすでに5人の他の人がプルしている
git checkout main
git rebase feature/old-stuff # ❌ 非常に危険
git push --force origin main
Rebase はすべてのコミットに新しい SHA を付けるため、push --force 後に main をプルした人は remote の履歴と分岐した履歴を持つようになります。彼らの次回の pull はエラーになるか、大量の重複コミットが作られます。
error: failed to push some refs to 'origin'
hint: Updates were rejected because the tip of your current branch is behind
Rebase 後に force-push が必須の場合は、--force の代わりに --force-with-lease を使って、他人のコミットを上書きするのを避けます:
git push --force-with-lease origin feature/payment
--force-with-lease は最後に fetch してからリモートに誰も追加コミットを push していないかチェックします。もしあれば、無差別に上書きする代わりにコマンドが失敗します。
黄金律: ブランチは 個人用 で、まだ共有されていない、あるいは自分だけが作業しているブランチでのみ rebase します。main や develop、またはどの shared なブランチでも rebase してはいけません。
7. Interactive rebase:merge 前の履歴のクリーンアップ
Rebase の非常に実用的な応用は、pull request を作成する前に個人ブランチの「ゴミ」コミットをクリーンアップすることです。
git checkout feature/payment
git rebase -i HEAD~3
エディタが開きます:
pick 4f3c2bb PaymentServiceを初期化
pick 9d1a0ef PaymentService.processPayment()を追加
pick 1c8f3aa fix typo
最後の2行を squash に変更して最初のコミットにまとめます:
pick 4f3c2bb PaymentServiceを初期化
squash 9d1a0ef PaymentService.processPayment()を追加
squash 1c8f3aa fix typo
保存します。Git は新しいコミットメッセージの入力を求めます。結果:3つの細かいコミットが1つのきれいで意味のあるコミットになります。これは PR を開く前に非常に適切です。そうすれば、reviewer は「fix typo」「wip」「ああ、import を忘れた」などのコミットを読む必要がありません。
8. 実用的な戦略:両方を組み合わせる
ほとんどの効果的な team は、どちらか一方を絶対に選ぶのではなく、文脈に応じて組み合わせます:
| 状況 | 推奨 |
|---|---|
個人ブランチを最新の main に更新 |
rebase |
| PR を開く前にコミットをクリーンアップ | rebase -i (squash) |
レビュー済みの feature ブランチを main にマージ |
merge (通常は PR 経由、squash-merge 可能) |
| 複数の人が共作・pull しているブランチ | merge、絶対に rebase しない |
| 統合のタイムスタンプを監査目的で保持する必要がある | merge |
実際の一般的なワークフロー:
# 1. PR を開く前:rebase して main に更新し、後の conflict を回避
git checkout feature/payment
git rebase main
# 2. 必要に応じてコミットをクリーンアップ
git rebase -i main
# 3. 個人ブランチを push(自分だけが使うので安全)
git push --force-with-lease origin feature/payment
# 4. PR 経由で main にマージ(マージコミットまたは squash-merge、main を rebase しない)
この方法では、PR での feature/payment のログがきれいで一直線になり、レビューしやすくなります。main は、approve された PR からのマージコミット(または squash コミット)を受け取るだけなので安全です。
9. Squash merge:中間的な選択肢
多くのプラットフォーム(GitHub、GitLab)は「Squash and merge」ボタンを提供しています。これは PR のすべてのコミットを 1つ の単一コミットにまとめて main にマージします。
git checkout main
git merge --squash feature/payment
git commit -m "追加:支払い処理機能(#42)"
結果:main には新しいコミットが1つだけあり、マージコミットはなく、feature/payment の履歴(リモート上)はブランチを削除していない限り保持されます。これは PR workflow を使う team の最も一般的な選択です。main のログがきれいで、元の履歴に触れないからです。
10. よくあるエラー
- Rebase 後に通常の push(
--forceなし): Git はエラーを報告します。履歴が分岐しているからです。通常のgit pullを使って「修正」しないでください。望まないマージコミットが作られ、rebase で作ったばかりの一直線の履歴を台無しにします。 - Rebase しているブランチが shared なのを忘れた: 常に
git log origin/<branch>をチェックして、他の誰かがすでに古いコミットに依存していないかを確認します。 - Conflict を解決した後に
--continueを忘れた: rebase 状態は「中断」のままです。git statusは rebase の途中にいることを明確に示します。 - ブランチ上で merge を何度も使う: ログが小さなマージコミットでいっぱいになり、読みにくくなります。
mainを何度もブランチにマージするのではなく、rebase を使って更新する方が良いです。 - Squash をやりすぎる: 無関係な変更をいくつか1つのコミットにまとめるため、
git bisectと部分的なgit revertが無用になります。
11. 結論
merge は安全で、履歴を正当に保持し、shared なブランチに適しています。rebase はきれいで一直線の履歴を作成しますが、SHA を書き直すため、個人的で共有されていないブランチでのみ使用できます。実際には、どちらか一方の側を絶対に選ぶ必要はありません。個人ブランチを更新・クリーンアップするには rebase を使い、main に統合するには merge(または squash-merge)を PR 経由で使用します。黄金律を覚えておいてください。他人が依拠しているブランチを決して rebase しないでください。そうすれば、team の Git に関するほとんどの問題を避けることができます。