git rebase: 仕組み、対話モード、コミットのまとめ方(squash)
git rebase は、現在のブランチのコミットを新しい基点の上に付け替えます。Git はコミットを一時的に取り除き、ブランチを新しい基点の先端へ移してから、コミットを新しいコミットとして順に適用し直します。これにより、マージコミットのない一直線の履歴が得られます。git rebase -i では、コミットをまとめたり(squash)、名前を変えたり、削除したりできます。書き換えてよいのは、まだ他の人と共有していないコミットだけです。
git rebase の働き
git rebase は、あなたのブランチを履歴の別の地点に「載せ替え」ます。たとえば、1週間前に main から feature を分岐させ、その後 main が先に進んだとします。rebase の後、あなたのブランチは、まるで今しがた最新の main のコミットから作業を始めたかのように見えます。
git switch feature
git fetch origin
git rebase origin/main
内部では3つの手順が行われます。Git は2つのブランチの共通の祖先を見つけ、その後に行われた各コミットの変更を一時的に保存し、ブランチを origin/main の先端へ移し、保存した変更を1つずつ適用し直します。その結果、内容やメッセージは同じでも、ハッシュが違う新しいコミットが作られます。
変更前: A---B---C---F---G main
\
D---E feature
変更後: A---B---C---F---G main
\
D'---E' feature
D' と E' の「'」は飾りではありません。これらは別のオブジェクトで、ハッシュも親も異なります。次に説明する基本ルールは、ここから来ています。
黄金律: 公開済みのコミットは rebase しない
rebase は履歴を書き換えます。コミットがあなたのものだけなら安全です。しかし、同僚がすでにブランチを取得している場合、rebase 後は同僚の手元に古い D と E が残り、あなたの手元には新しい D' と E' があります。次の pull で履歴が二重になり、コンフリクトを改めて解決することになります。
実践的なルールは、rebase を自分のローカルのブランチや、1人だけが作業する個人の feature ブランチに使うことです。main、develop、リリースブランチのような共有ブランチは、決して rebase しません。
rebase 後にブランチを送る
ブランチがすでにサーバーにあった場合、通常の git push は拒否されます。ローカルの履歴が、リモートの履歴の続きではなくなっているからです。必要なのは、安全な形の強制 push です。
git push --force-with-lease
--force と違い、--force-with-lease は、あなたが見たことのないコミットがサーバーに増えている場合に上書きを拒否します。そのため、誰かの作業を誤って消してしまうことを防げます。
rebase のコンフリクト
コミットは1つずつ適用されるため、コンフリクトはどのコミットでも起こり得ます。複数回続けて起こることもあります。Git は止まって、適用できなかったコミットを報告します。
$ git rebase origin/main
Auto-merging src/api.js
CONFLICT (content): Merge conflict in src/api.js
error: could not apply 4e1f9a2... Добавить кэш запросов
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
作業の手順は次のとおりです。
- コンフリクトしたファイルを開き、正しい版を選び、マーカー
<<<<<<<、=======、>>>>>>>を削除します。 - ファイルを解決済みとしてマークします:
git add src/api.js。ここでコミットする必要はありません。 - 続けます:
git rebase --continue。Git は次のコミットを適用します。
その他の操作コマンドは次のとおりです。
| コマンド | 動作 |
|---|---|
git rebase --continue | コンフリクトを解決した後に続ける |
git rebase --skip | 現在のコミットを丸ごと飛ばす(その変更は失われる) |
git rebase --abort | rebase を中止し、ブランチを元の状態に戻す |
「自分の側」と「相手の側」の呼び方にも注意が必要です。rebase 中、HEAD は移し先の基点であり、入ってくる変更は自分のコミットです。そのため、rebase 中の git checkout --ours は main の版を、--theirs は自分の版を取ります。merge の場合はその逆です。
繰り返すコンフリクト: rerere
長く続くブランチを何度も rebase していて、同じコンフリクトを毎回解決しているなら、git config --global rerere.enabled true を有効にしてください。Git は、どのようにコンフリクトを解決したかを記憶し、次回から自動で同じ解決を適用します。
対話的な rebase: 公開前に履歴を整える
rebase で最も価値があるのは対話モードです。レビューに出す前に、自分のコミットを整理できます。「fix」「fix typo」「もう一度修正」を1つの意味のあるコミットにまとめたり、メッセージを書き直したり、順番を入れ替えたり、不要なものを捨てたりできます。
git rebase -i HEAD~4 # 直近の4つのコミット
git rebase -i origin/main # main にはない、ブランチのすべてのコミット
Git は、エディタを開き、コミットの一覧を表示します。古いものから新しいものの順で並んでいます(git log とは逆です)。
pick 1a2b3c4 Добавить форму входа
pick 5d6e7f8 fix
pick 9a0b1c2 Валидация e-mail
pick 3d4e5f6 опечатка в валидации
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash" but keep only the previous commit's log message
# d, drop = remove commit
行頭のコマンドを書き換え、保存してエディタを閉じると、Git は上から順に計画を実行します。
| コマンド | 効果 |
|---|---|
pick | コミットをそのまま残す |
reword | 変更は残し、メッセージだけを書き直す |
edit | コミットで止まり、内容を修正(git commit --amend)したり、複数に分割したりできる |
squash | 直前のコミットに統合し、メッセージをまとめて編集する |
fixup | 直前のコミットに統合し、このコミットのメッセージは捨てる |
drop | 変更ごとコミットを削除する |
行の順序を入れ替えるとコミットの順番が変わります。行を削除することは drop と同じ意味です。
git squash: 複数のコミットを1つにまとめる
Git には git squash というコマンドはありません。コミットをまとめる方法は3つあり、対話的な rebase が最も柔軟です。
方法1: 対話的な rebase
上の例で、4つのコミットを2つの整ったコミットにまとめてみましょう。
pick 1a2b3c4 Добавить форму входа
fixup 5d6e7f8 fix
pick 9a0b1c2 Валидация e-mail
fixup 3d4e5f6 опечатка в валидации
まとめる対象のグループの最初のコミットは pick のままにしてください。squash や fixup は、コミットを直前のコミットに統合するので、一番最初のコミットには統合先がありません。
方法2: fixup コミットと autosquash
どのコミットを修正するか分かっている場合は、修正をあらかじめ fixup として作れます。
git commit --fixup=1a2b3c4
git rebase -i --autosquash origin/main
Git は fixup のコミットを対象のコミットの下に自動で並べ、fixup と印を付けます。あとは計画を保存するだけです。この動作を既定にするには git config --global rebase.autoSquash true を使います。
方法3: ブランチ全体を1つのコミットにする
git reset --soft $(git merge-base HEAD origin/main)
git commit -m "メールアドレスによるログインと検証を追加"
reset --soft は、ブランチの参照を分岐点まで戻しますが、すべての変更はインデックスに残ります。あとは1つのコミットを作るだけです。別の方法として、主要なブランチへ統合する際に git merge --squash を使うか、pull request で「Squash and merge」ボタンを使うこともできます。
rebase の便利なオプション
git pull --rebase
通常の pull は、リモートのブランチをあなたのブランチに統合し、同僚と並行してコミットしていればマージコミットを作ります。一方 git pull --rebase は、あなたのローカルのコミットを新しいリモートのコミットの上に載せ替えます。既定の動作にするには git config --global pull.rebase true を使います。これは rebase の中でも安全な使い方です。ローカルのコミットはまだ誰にも見られていないからです。
--onto: ブランチの一部を移す
feature-a から feature-b を分岐させていたが、feature-b を feature-a のコミットを含めずに、直接 main から分岐したように見せたい場合は、次のように使います。
git rebase --onto main feature-a feature-b
意味は次のとおりです。feature-b にあって feature-a にないコミットを取り出し、main の上に載せ替えます。
--exec: 各コミットを検証する
git rebase -i --exec "npm test" origin/main
コミットを適用するたびにテストが実行され、失敗すると rebase は問題のコミットで止まります。履歴のすべてのコミットがビルドできることが重要な場合に便利です。
--update-refs: 依存するブランチの積み重ね
互いに依存するブランチの連なりを持っている場合は、--update-refs(Git 2.38 以降)を使うと、最上位のブランチを rebase するときに、途中のブランチも一緒に動かせます。
rebase を取り消す
処理の途中なら git rebase --abort を使います。rebase が完了していて結果が気に入らない場合でも、古いコミットは消えていません。参照のログに残っています。
$ git reflog
e5f6a7b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e5f6a7b HEAD@{1}: rebase (pick): Валидация e-mail
...
c3d4e5f HEAD@{5}: commit: опечатка в валидации
$ git reset --hard HEAD@{5}
git reflog で、rebase を始める前の記録を見つけ、そこにブランチをリセットします。rebase 直後であれば、git reset --hard ORIG_HEAD という短い方法もあります。reflog はローカルに保存され、既定では約90日間記録が残るため、rebase に失敗しても慌てる必要はありません。
よくある間違い
- 共有ブランチを rebase して強制 push した。 同僚の履歴が食い違いました。対処としては、正しい状態を持つ人の reflog から復元し、サーバーで強制 push を保護されたブランチに禁止します。
--force-with-leaseの代わりに--forceを使った。 同じブランチに同僚が push したばかりのコミットを上書きしてしまいます。- 対話計画の最初の行で squash を使った。 Git は「cannot 'squash' without a previous commit」というエラーを出します。
- 作業ツリーが汚れた状態で rebase を始めた。 Git は実行を拒否します。コミットするか、
git stashで退避するか、git rebase --autostashを使ってください。 - コンフリクトを解決した後で
git rebase --continueではなくgit commitを実行した。 余計なコミットができます。後で対話的な rebase を使えば取り除けます。
まとめ
git rebase は、自分のコミットを新しい基点の上で作り直し、履歴を一直線にきれいに保ちます。対話モードでは、下書きのコミットを整えられます。squash と fixup でまとめ、reword でメッセージを直し、drop で不要なものを取り除けます。基本ルールは1つです。まだ他人に渡していないコミットだけを書き換え、rebase 後の自分のリモートブランチは --force-with-lease で送ります。rebase と履歴の整理は、Gitコースで実践的に扱っています。
よくある質問
git rebase はコードの内容を変えますか?
コンフリクトがなければ、最終的なファイルの内容は merge と同じになります。ただし、コミットは新しいハッシュと新しい親を持って作り直されます。コンフリクトの解決方法が異なれば、途中のコミットの内容も変わります。
ブランチを最初のコミットの上に rebase するにはどうすればよいですか?
git rebase -i --root を使います。計画にはルートのコミットを含むすべてのコミットが並びます。最初のコミットのメッセージを直すなど、ごくまれな場面で使います。
rebase で作者や日付は保持されますか?
作者と作成日は保持されます。コミッターとコミット日時は現在のものに変わります。--committer-date-is-author-date を付けると、コミット日時も作成日時と同じになります。
rebase は merge コミットを扱えますか?
既定では、rebase は履歴を一直線にし、マージコミットを捨てます。マージの構造を残したい場合は git rebase --rebase-merges を使います。
rebase が進行中かどうかはどう確認しますか?
git status に interactive rebase in progress または You are currently rebasing と表示されます。使えるコマンドも案内されます。多くのシェルや IDE も、プロンプトや画面にこの状態を表示します。 </content>