7月26日、cachebag.shが「git rebase -i is not that scary」と題した記事を公開した。git rebase -iを「怖い」と感じて避け続けている開発者に向けて、その実態と安全な使い方を解説した内容だ。
筆者がジュニア開発者として働き始めた頃、驚いたのはベテランの同僚たちですらこのコマンドを怖がっていたことだったという。PRレビュー文化が定着し、AIアシスタントが細かい単位でコミットを積み上げる今、コミット履歴を整理するスキルの重要性は増す一方だ。しかし「rebaseは危険」「履歴が壊れる」というイメージが根強く残り、git rebase -iを日常的に使いこなしている開発者は依然として少ない。
実際に何が起きているかを見れば、拍子抜けするほど地味だ。git rebase -i HEAD~4 を実行すると、gitがやることはテキストファイルを開くだけだ。
pick a1b2c3d Add user model
pick e4f5g6h Fix typo in user model
pick i7j8k9l Add login endpoint
pick m0n1o2p WIP debugging login
# Commands:
# p, pick
重要なのは、この時点では何も実行されていないという点だ。これは「計画書」であって、操作ではない。気が変わったらgit rebase --abortを実行するだけで、ブランチは元の状態に戻る。
各行はコミットへの命令であり、pick(そのまま適用)、r(メッセージを書き換え)、d(削除)などを組み合わせられる(コマンド一覧は公式ドキュメント参照)。たとえば以下のように編集すると:
r a1b2c3d Add user model
pick e4f5g6h Fix typo in user model
pick i7j8k9l Add login endpoint
d m0n1o2p WIP debugging login
1つ目のコミットはメッセージを書き直し、最後のWIPコミットは履歴から削除され、中間の2つはそのまま残る。結果として4つあったコミットが3つに整理される。それだけだ。
「壊したらどうする」という不安に対して、筆者は3つの答えを示している。
第一に、いつでも中断できる。git rebase --abortを実行すればrebase開始前の状態に即座に戻る。第二に、rebaseはコミットを書き換えない。既存のコミットを上書きするのではなく、新しいコミットを作成してブランチのポインタを移動させるだけだ。古いコミットはgitのオブジェクトデータベースに参照されないまま残り続ける(ガベージコレクションが走るまでは)。つまり「元のコミット」は即座には消えない。
第三が最強の安全網だ。gitはreflogというジャーナルに、ブランチが指し示してきたすべての場所を記録している。git reflogでrebase前のエントリを探し、
git reset --hard HEAD@{4}
と実行するだけでrebase全体が取り消せる。最悪のケースでも、数分のreflog調査で完全に復元できる。さらに保険をかけたければ、git branch backup-before-rebase でrebase前の状態に名前をつけておくだけでよい。
コンフリクトについても、筆者は「mergeより実は簡単」と述べている。mergeはブランチ全体の変更がぶつかるのに対し、rebaseでは一度に一つのコミット分だけを相手にすれば済む。変更の範囲が小さいぶん、何が起きているかを把握しやすい。コンフリクトが発生したらmergeと同様に解消してgit addし、git rebase --continueで再開する。
実践上のルールとして、筆者は自分のフィーチャーブランチであればレビューの前後を問わず自由にrebaseしてよいとし、リモートへのpushにはgit push --force-with-leaseを推奨している。通常の--forceとの違いは、自分以外の誰かがその間にpushしていた場合は拒否してくれる点だ。なお共有ブランチ(mainやdevelopなど)へのrebaseは別の話であり、本記事の対象外だと明示されている。
詳細はgit rebase -i is not that scaryを参照していただきたい。