git rebaseとは何か・履歴をまっすぐにする代わりに背負うリスク

前記事では、featureブランチをmainに取り込む方法として、fast-forward・マージコミット・squashという3種類の「マージ」を見ました。ただしmainの変更を自分のfeatureブランチ側に取り込みたい場面では、マージとは別にもう1つ選択肢があります。それがgit rebaseです。マージより履歴がきれいになる一方、使い方を誤ると自分だけでなくチーム全体を巻き込む厄介ごとに発展するため、仕組みを正しく理解してから使う必要があります。

分岐点そのものを、最新の位置に付け替える

rebaseもmainの変更を取り込む操作なのだから、結局はマージと同じことをしているのだろうと思うかもしれません。しかし両者がやっていることは根本的に違います。マージは「2つの流れが合流した」という事実を新しいコミットとして記録するのに対し、rebaseは自分のブランチが分岐した地点そのものを、mainの最新コミットの位置まで付け替えます。具体的には、自分のブランチにしかないコミットをいったん取り外し、mainの最新コミットの上に1つずつ再生(リプレイ)していく処理です。

# featureブランチの上で実行する
git checkout feature
git rebase main

# 内部的には、featureにしかないコミットを
# mainの最新コミットの上に1つずつ積み直している
# A---B---C  (旧feature、mainから分岐)
#      ↓ rebase後
#              A'--B'--C'  (新feature、mainの最新コミットの上)
🎯 読み解きポイント:古い土台の上に継ぎ足すか、最新の土台に建て直してから継ぎ足すか

マージが「今建っている家はそのままに、隣の棟と渡り廊下でつなぐ」やり方だとすると、rebaseは「一度部材(コミット)を全部下ろして、最新の土台の上に同じ部材で建て直す」やり方です。出来上がった家(履歴)は、最初から最新の土台の上で建てていたかのように、渡り廊下のない一直線の構造になります。ただし建て直す以上、部材そのもの(コミットの実体)は古いものではなく新しく作り直されたものになる、という点が次の話につながります。

共有した履歴には使えない、というのが唯一絶対のルール

履歴がまっすぐになるなら、チームの共有ブランチでも積極的にrebaseした方がきれいになるだろうと考えるかもしれません。ですがこれは、rebaseの仕組み上もっとも避けるべき使い方です。rebaseで「再生」されたコミットは、親コミットの情報が変わるためコミットハッシュも別物として計算し直され、見た目の内容が同じでも元のコミットとは完全な別人として扱われます。すでにpushして他の人がpullしてしまったコミットに対してrebaseを行うと、自分の手元と相手の手元とで「同じはずの変更」が別のハッシュを持つ2つのコミット群として食い違い、次にpull/pushしたときに重複コミットや大量のコンフリクトを引き起こします。

# 危険な例: 既にpushしてteammateがpullしたブランチをrebaseする
git checkout shared-feature
git rebase main
git push --force   # ← teammateの手元とハッシュが食い違う

# 安全なのは、まだ自分しか触っていないローカル専用ブランチだけ
git checkout my-local-only-branch
git rebase main    # 誰も参照していないので安全

これは「Golden Rule of Rebasing(rebaseの黄金律)」と呼ばれるほど広く知られた原則で、要約すると「一度でも他人と共有した履歴には絶対にrebaseしない、rebaseするのは自分だけが触っているローカルブランチに限る」という一点に尽きます。

🎯 読み解きポイント:ハクスラゲームのセーブデータを、配布後に作り直してしまう事故

ハクスラゲームで例えると、rebaseは「まだ誰にも配っていない自分専用のセーブデータ」を最新パッチの土台に作り直す操作です。ところが、そのセーブデータをすでに他のプレイヤーに配布した後で作り直してしまうと、配布済みのデータと手元のデータで同じキャラクターのはずなのに内部IDが食い違い、引き継ぎができなくなります。配る前に整えるか、配った後は絶対に作り直さないか、どちらかしかありません。

よくあるはまりポイント:コンフリクトを1回で済ませたはずが、コミットの数だけ繰り返す

マージよりrebaseの方が「モダンで正しいやり方」だと言われると、常にrebaseを選ぶべきだと思ってしまいがちです。ですがrebaseは、ブランチに含まれるコミットを1つずつ個別に再生する処理のため、コンフリクトもコミットごとに個別に発生し得ます。マージなら最終的な差分1回分のコンフリクト解消で済むところを、rebaseでは同じファイルの同じ行に対して、コミットの数だけ似たようなコンフリクト解消を繰り返す羽目になることがあります。特に長期間更新していないブランチをrebaseしようとすると、この「コンフリクトの繰り返し地獄」に陥りやすくなります。コンフリクトが多すぎて収拾がつかないと感じたら、git rebase --abortでいつでもrebase開始前の状態にそのまま戻せることを覚えておくと安心です。無理にrebaseを完走させるより、変更量が大きい場合は素直にマージを選ぶという判断も必要です。

この記事のキーワード
git rebase
自分のブランチの分岐点を、指定したブランチの最新コミットの位置に付け替える操作。マージコミットを作らず、履歴を1本の直線に整える
Golden Rule of Rebasing
「一度共有した履歴にはrebaseしない、rebaseするのは自分だけのローカルブランチに限る」という広く知られた原則
git rebase --abort
コンフリクトなどでrebaseが止まったとき、rebase開始前の状態にそのまま戻すコマンド
💡 ワンポイントアドバイス:迷ったら「まだ誰にも見せていないか」で判断する

rebaseとマージのどちらを使うべきか迷ったときは、「このブランチのコミットを、自分以外の誰かがすでにpullしているか」を基準に判断すると失敗しません。自分だけのローカルブランチならrebaseで履歴を整理し、pushして他の人と共有した後はマージ(またはrevert)で対応する、という使い分けを徹底しましょう。

rebaseはコミット済みの履歴を整理する操作ですが、そもそもコミットする前の中途半端な変更を、ブランチを切り替える前にどう扱えばいいのでしょうか。続くgit stashでは、その一時退避の方法を扱います。