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 に関するほとんどの問題を避けることができます。

gitrebasemergeversion-controlworkflow