Git実務ワークフロー #2 rebase vs merge — 判断基準と禁則

分岐した2つの履歴を合流させる方法として、Git基礎 #3 ブランチとマージ — fast-forwardと3-wayではmergeを扱いました。fast-forwardできない状況ではマージコミットが生まれ、分岐と合流の痕跡が履歴にそのまま残る方式でした。ところが実務のリポジトリの履歴を見ると、マージコミットなしで一直線にきれいに整理されたケースによく出会います。その背後にあるツールがrebaseです。rebaseは強力ですが、誤って使うとチーム全体の履歴を混乱させる唯一の基礎コマンドでもあります。この記事で動作原理と判断基準、そして禁則を一度に整理します。

全7回で構成します。

  • #1 ブランチ戦略 — GitHub Flowとtrunk-based
  • #2 rebase vs merge — 判断基準と禁則 ← この記事
  • #3 interactive rebase — squash・fixupでコミット整理
  • #4 コンフリクト解決 — 構造とmergetool・rerere
  • #5 PR運用 — レビュー単位・コミットメッセージ・draft PR
  • #6 stash・cherry-pick・bisect — 日常ツールセット
  • #7 モノレポとGit — sparse-checkout・サブモジュール・LFS

rebaseの動作原理 — 移動ではなく作り直し #

mergeと同じ出発状況から始めます。ブランチが分岐した後、両側とも前進した状態です。

rebase前 — 両側が分岐した状態
A --- B --- C --- F        ← main
              \
                D --- E    ← feature (HEAD)

featureでgit rebase mainを実行すると、履歴は次のように変わります。

featureをmainの上にrebase
git switch feature
git rebase main
# Successfully rebased and updated refs/heads/feature.
rebase後 — 一直線になった履歴
A --- B --- C --- F                ← main
                    \
                      D' --- E'    ← feature (HEAD)

見た目にはDとEをmainの先端に移したようですが、Gitが実際に行ったことは違います。DとEの変更内容をFの上に1つずつ適用し直し、新しいコミットD’とE’を作りました。コミットには親へのポインタが入っているので、親が変わったコミットは同じコミットではあり得ません。D’はDと変更内容が同じでも、ハッシュが違うまったく新しいコミットです。元のDとEはどのブランチからも指されない状態で残り、後で整理されます。

この状態でmainに移動してマージすればfast-forwardが可能なので、マージコミットなしでポインタだけが前進します。一直線の履歴はこうして作られます。

mergeとrebase — 何を残すかの違い #

2つの方式の違いは機能ではなく履歴の哲学の違いです。

区分mergerebase
履歴分岐と合流をそのまま保存一直線に書き直す
コミットハッシュ既存コミットを維持再適用されたコミットはすべて新ハッシュ
マージコミット生まれる(3-wayの場合)なし
グラフの読みやすさブランチが多いと複雑シンプル
事実の記録実際の作業順そのまま整理された順序に再構成

mergeは「実際に何があったのか」を残し、rebaseは「こう作業したことにすれば読みやすい」を残します。どちらが常に正しいとは言えず、チームが履歴をどう読みたいかによって選択が分かれます。

黄金律 — 共有ブランチはrebaseしない #

rebaseには例外なく守るべきルールが1つあります。すでにpushされ、他の人がbaseとして使っているブランチはrebaseしないというルールです。理由を構造で確認します。

共有ブランチをrebaseすると起きること
[rebase前 — 同僚がCまで受け取り、その上で作業中]
A --- B --- C              ← origin/feature
A --- B --- C --- G        ← 同僚のローカルfeature

[自分がCをC'にrebaseしてforce pushした後]
A --- B --- C'             ← origin/feature (履歴が書き換えられた)
A --- B --- C --- G        ← 同僚のローカル (originと分岐)

同僚のローカルにあるCは、リモートから消えたコミットになりました。同僚がpullした瞬間、CとC’が同じ内容の別コミットとして衝突し、それを解こうとmergeすれば重複コミットが生まれ、さらにpushすれば次の人の履歴が乱れます。1回のrebaseがチーム全体のforce pushの連鎖に広がる構造です。

逆に、まだpushしていないローカルコミットや、自分だけが使うブランチならrebaseは何の問題もありません。書き換えられるコミットをbaseにしている人がいないからです。黄金律は一文に要約できます。自分のコミットを書き直すのは自由ですが、他の人が立脚しているコミットは書き直しません。

安全に使える3つの状況 #

実務でrebaseが安全かつ有用な使いどころは明確です。

第一に、自分の機能ブランチを最新のmainの上に載せ直すときです。PRを開く前にgit rebase mainでブランチを最新の状態の上に立て直せば、レビュアーは最新コード基準のdiffを見ることになり、マージ時点のコンフリクトも先に解消されます。ブランチを自分だけが使っているなら、pushの有無に関係なく安全です。すでにpushしたブランチならrebase後にpushが拒否されますが、そこで必要になるforce pushと安全装置--force-with-leaseは次回詳しく扱います。

第二に、pullで生まれる不要なマージコミットをなくすときです。ローカルにコミットがある状態でgit pullを実行すると、デフォルトの動作はmergeなので「Merge branch ‘main’ of …」のようなマージコミットが生まれます。自分のコミットをリモートのコミットの上に再適用するgit pull --rebaseを使えば、このノイズが消えます。

pullをrebase方式に
# 今回だけ
git pull --rebase

# デフォルトに設定
git config --global pull.rebase true

# rebase前に作業中の変更を自動でstashし、後で復元
git config --global rebase.autoStash true

rebase.autoStashまで有効にしておけば、作業の途中でもpullがスムーズに通ります。このとき再適用されるのは、まだpushしていない自分のローカルコミットだけなので、黄金律にも反しません。

第三に、push前にローカルコミットを整理するときです。タイポ修正やデバッグ出力の削除のような細かいコミットを意味単位にまとめて整えるinteractive rebaseがこの用途ですが、次回のテーマなので、ここでは存在だけ押さえておきます。

ヒント
rebaseの途中でコンフリクトが起きたら、mergeと同じ要領で解決しますが、締めのコマンドが違います。コンフリクトを整理してgit addした後、git rebase --continueで再適用を続け、全体を中断するならgit rebase --abortでrebase前の状態に戻ります。コミットを1つずつ再適用する構造なので、コンフリクトもコミット単位に分かれて出てくるという点は、mergeよりむしろ扱いやすい面です。

判断基準の整理 #

ここまでの内容を判断ルールに圧縮します。

  • ローカル専用、自分だけのブランチ — rebase自由。履歴をいくらでも整えて構いません。
  • 共有ブランチ、保護ブランチ(mainなど) — rebase禁止。合流はmergeかPRのマージボタンで行います。
  • PRで合流する機能ブランチ — 開く前にgit rebase mainで整え、マージ方式はチームのPRポリシー(merge commitでもsquashでも)に従います。

最後の項目が示すとおり、個人のrebaseの好みよりチームの履歴ポリシーが優先です。一直線の履歴を望むチームなら、PRのSquash and mergeやRebase and mergeが同じ結果をより安全に作ってくれることもあります。

まとめ #

この記事の要点は3つです。

  • rebaseはコミットを移動するのではなく、新しいコミットとして作り直して再適用するコマンドで、だからハッシュがすべて変わります。
  • mergeは履歴を保存し、rebaseは履歴を書き直します。チームが履歴をどう読みたいかが選択基準です。
  • 黄金律は1つ、他の人がbaseとして使う共有ブランチはrebaseしないというルールです。自分のローカルコミットと個人ブランチは自由です。

次回の「Git実務ワークフロー #3 interactive rebase — squash・fixupでコミット整理」では、rebaseの最も生産的な使いどころを扱います。雑然と積み上がったローカルコミットをレビューしやすい単位に整える方法、そして書き直したブランチを安全にpushする--force-with-leaseまで確認します。

X