Gitで最も避けられているツール
開発者の集まりで「どのGitコマンドが不安か」と聞けば、git rebase -iは間違いなく上位に入るでしょう。インタラクティブrebaseには、履歴を書き換える、エディタに見慣れないtodo listが開く、何かを壊してしまいそうで危険、という評判がつきまといます。だから多くの人は単純に避けてしまい、その結果、pull requestには「wip」「fix typo」「actually fix it」といったcommitが積まれたまま届くのです。
これは実にもったいない話です。インタラクティブrebaseは、散らかったbranchをクリーンでレビューしやすい履歴に変えるための最高のツールだからです。このガイドでは、具体的な実例を1つ通して使いながら、その仕組みを解き明かします。9個の雑然としたcommitを持つ機能branchを、pull requestを開く前に3個のクリーンなcommitに整形します。そもそもrebaseが自分のワークフローに適した選択なのか迷っている場合は、まずrebaseとmergeの比較から読んでください。この記事は、rebaseすると決めた上で、それをうまくやりたい人向けです。
クリーンな履歴が重要な理由
pull requestの前にcommitを整理するのは、見た目の問題ではありません。整った履歴は、非常に実用的な3つの形で報われます:
- レビューのしやすさ。「Add login form with validation」「Add remember me option」「Add error messages」という3つの論理的なcommitを読むレビュアーは、それぞれの変更を独立してレビューできます。同じコードが9個の中途半端なcommitに分散していると、diff全体を一度にレビューせざるを得なくなり、レビューの質が下がります。
- git bisect。数週間後にバグが見つかったとき、
git bisectは履歴をたどってバグを混入させたcommitを特定します。これは、すべてのcommitが単体でビルドでき、意味を持つ場合にのみ機能します。コンパイルすら通らない「wip」commitだらけの履歴では、bisectはほとんど役に立ちません。 - revertのしやすさ。機能を取り消す必要が生じた場合、自己完結したcommitを1つrevertするのは簡単です。9個の入り組んだcommitに散らばった機能をrevertするのは、午後いっぱいかかる発掘作業になります。
実例:9個の雑然としたcommit
これから整理するbranchがこちらです。ごく普通の作業履歴で、実際みんなこうやってコーディングしています:
$ git log --oneline main..feature/login
c9d0e1f oops forgot the css file
b8c9d0e add error messages
a7b8c9d wip 2
f6a7b8c add remember me checkbox
e5f6a7b actually fix it
d4e5f6a fix typo
c3d4e5f add form validation
b2c3d4e wip
a1b2c3d add login form skeleton
作業中にこのようにcommitすること自体は何も悪くありません。問題なのは、このまま出荷されてしまうことだけです。私たちのゴールは、最終的に次の3つのcommitにすることです:
- Add login form with validation
- Add remember me option
- Add error messages with styling
rebaseの開始:git rebase -i HEAD~9
直近9個のcommitを書き換えるには、次を実行します:
git rebase -i HEAD~9
HEAD~9は「現在のcommitから9個前のcommit」を意味し、その地点より後のすべてが編集対象になります。Gitはエディタを開き、todo listを表示します:
pick a1b2c3d add login form skeleton
pick b2c3d4e wip
pick c3d4e5f add form validation
pick d4e5f6a fix typo
pick e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
pick a7b8c9d wip 2
pick b8c9d0e add error messages
pick c9d0e1f oops forgot the css file
何かに触れる前に理解しておくべきことが2つあります。第一に、これはただのテキストファイルです。保存して閉じるまで何も起こりませんし、すべての行を削除したり、意味のある変更を保存せずに閉じたりすれば、rebaseは何の害もなくキャンセルされます。第二に、並び順は古いものが先頭で、git logとは逆です。Gitはcommitを上から下へ順番に再生し、各行の先頭にある動詞を適用していきます。
6つの動詞
pick
commitをそのまま保持します。すべての行のデフォルトです。
reword
commitの変更内容は保持しつつ、commitメッセージを編集するために一時停止します。コードに触れずに「wip 2」を直したいときにぴったりです。
edit
このcommitでrebaseを一時停止します。amendしたり、複数のcommitに分割したり、テストしたりした後、git rebase --continueで続行できます。最も高度な動詞であり、使う頻度は最も低いでしょう。
squash
このcommitを1つ上のcommitに統合し、エディタを開いて2つのcommitメッセージを1つにまとめられるようにします。
fixup
squashと同様ですが、このcommitのメッセージを完全に破棄します。上のcommitのメッセージはそのまま残ります。「fix typo」のようなcommitに使いたいのがこれです。そのメッセージには残す価値のある情報がないからです。
drop
commitとその変更を完全に削除します。todo listから行を削除しても同じ効果がありますが、dropと書くことで意図が明確になります。
ステップバイステップでbranchを整理する
では、これを9個のcommitに適用してみましょう。必要な操作は2つ:並べ替えと動詞の変更です。
まずは並べ替えです。diffを見ると、「oops forgot the css file」は実はエラーメッセージ用ではなく、remember meチェックボックス用のスタイルシートです。todo listでの並べ替えは、行を移動するだけです。c9d0e1fの行を切り取り、remember meグループの直後に貼り付けます。Gitは新しい順序でcommitを再生します。
次に動詞です。「actually fix it」までのすべてはログインフォームに属するので、最初のcommitにまとめます。「add form validation」にはsquashを使います。そのメッセージを最終的なメッセージに統合したいからです。一方、メッセージが消えてしかるべきノイズcommitにはfixupを使います。編集後のtodo listがこちらです:
pick a1b2c3d add login form skeleton
fixup b2c3d4e wip
squash c3d4e5f add form validation
fixup d4e5f6a fix typo
fixup e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
fixup a7b8c9d wip 2
fixup c9d0e1f oops forgot the css file
reword b8c9d0e add error messages
保存して閉じます。Gitはcommitを再生し、2回停止します。1回目は最初のグループの統合メッセージを書くため(「Add login form with validation」と入力)、2回目はrewordのため(「Add error messages with styling」と入力)です。remember meのcommitについては、軽くrewordしてもよかったのですが、ここではpickのままにしました。元のメッセージがすでに十分まともだったからです。結果はこうなります:
$ git log --oneline main..feature/login
3f4e5d6 Add error messages with styling
2e3d4c5 Add remember me option
1d2c3b4 Add login form with validation
9個のcommitが3個になり、それぞれが自己完結していてレビュー可能です。すべてのハッシュが変わっていることに注目してください。これらは完全に新しいcommitです。この点を覚えておいてください。下で説明する黄金律の核心です。
上級者の技:--autosquash
慣れてきたら、最後にまとめて整理するのではなく、作業しながら整理の準備をしておくことができます。以前のcommitの内容を修正したときは、次のように記録します:
git commit --fixup a1b2c3d
Gitはfixup! add login form skeletonというメッセージのcommitを作成します。後で、次を実行します:
git rebase -i --autosquash main
Gitはtodo listを事前に埋めてくれます。すべてのfixup! commitがターゲットの下に移動され、fixupとしてマークされた状態です。あとは保存して閉じるだけです。これをデフォルトの動作にするには、一度だけ設定します:
git config --global rebase.autosquash true
黄金律:共有されたcommitは決してrebaseしない
rebaseはcommitを修正するのではなく、新しいcommitで置き換えます。チームメイトがすでに古いcommitをpullしてその上に作業を積んでいる場合、それらのcommitを書き換えると2つの分岐した履歴が生まれ、次のmergeは重複とコンフリクトの泥沼になります。
ルールはシンプルです:まだpushしていないcommit、または自分だけが作業するbranch上のcommitだけをrebaseする。pull requestを開く前に自分の機能branchを整理するのは、教科書どおりの安全なケースです。すでにbranchをpushしていて、それでも自分がその唯一の作者である場合は、git push --force-with-leaseで書き換えた履歴をpushしましょう。このコマンドは、その間に他の誰かがpushした作業を上書きすることを拒否してくれます。mainや、チームがpullしているbranchは決して書き換えないでください。
うまくいかないとき
rebaseの途中で起こりうることは2つです。再生中のcommitが以前の変更とコンフリクトした場合、Gitは一時停止してコンフリクトの解決を求めます。解決したらファイルをgit addして、git rebase --continueを実行します。そして、どこかの時点で迷子になったと感じたら、完全な脱出ボタンがあります:
git rebase --abort
これは、開始前とまったく同じ状態にbranchを戻します。問答無用です。--abortの存在さえ覚えていれば、進行中のrebaseに閉じ込められることは決してありません。
rebaseが完了した後で、結果が間違っていたと気づいたら?それでも、何も失われていません。古いcommitはまだ存在しています。Gitは数週間それらを保持しますし、reflogはbranchがこれまで指してきたすべての位置を記録しているので、reset一発でrebase前の状態に戻れます。この安全網については、git reflogで失われたcommitを復元するで詳しく解説しています。また、ミスがもっと小さい場合、つまり最後のcommitだけを変更すればよい場合は、rebaseはまったく必要ありません。Gitのcommitを取り消す方法をご覧ください。
GitSquidでのインタラクティブrebase
todo listは強力ですが、テキストファイルで動詞を編集するという作業こそ、まさに人を不安にさせる部分です。GitSquidはこれをビジュアルなインタラクティブrebaseに置き換えます。drag & dropでcommitを並べ替え、各commitのアクション(pick、squash、fixup、reword、drop)をドロップダウンピッカーから選びます。実行前にプラン全体が一目で見渡せるので、恐怖の大部分が取り除かれます。
GitSquidは開始前の段階でも助けになります。無料プランに含まれるConflict Predictorは、rebaseを実行する前にどのファイルがコンフリクトするかを表示するので、途中でコンフリクトに遭遇して驚く代わりに、何が待っているのかを事前に把握できます。rebaseはcommitグラフ上のbranchとcommitのコンテキストメニューから直接利用できます。
クイックリファレンス
| コマンド / 動詞 | 動作 |
|---|---|
git rebase -i HEAD~n |
直近n個のcommitをインタラクティブに書き換える |
pick |
commitをそのまま保持する |
reword |
変更内容は保持し、メッセージを編集する |
edit |
このcommitで一時停止してamendまたは分割する |
squash |
前のcommitに統合し、メッセージを結合する |
fixup |
前のcommitに統合し、このメッセージを破棄する |
drop |
commitを完全に削除する |
git commit --fixup <hash> |
以前のcommitに向けた修正を記録する |
git rebase -i --autosquash |
fixup! commitをtodo listに自動配置する |
git rebase --continue |
コンフリクト解決後に再開する |
git rebase --abort |
キャンセルしてrebase前の状態に戻す |
インタラクティブrebaseは、少しの練習に大きな力で応えてくれます。使い捨てのbranchで始めて、--abortを頭に入れておき、黄金律を守れば、「wip、fix typo、actually fix it」をクリーンな履歴に変えることは日常作業になります。テキストファイルを編集する代わりにプランを目で見たいなら、GitSquidを無料でダウンロードして、次の機能branchでビジュアルrebaseを試してみてください。