リベースはN回のマージだ
一言でいうと
コミットオブジェクトの中には、親のハッシュが入っています。親が変わると、コミットのハッシュは必ず変わります。つまり、リベースはコミットを移動させるコマンドではなく、書き換えるコマンドです。
なぜ必要なのか
「すでにプッシュしたコミットはリベースしてはいけない」という言い方は、少し不正確です。本質は、プッシュしたかどうかではなく、他の人のリポジトリにそのコミットオブジェクトがあるか、そしてその上に誰かの作業が載っているかです。そのため、黄金律の正確な形は、次のとおりです。他の人の作業がその上に積まれているコミットは、書き換えません。
1人だけで使うリモートブランチを整理するためにリベースするのは、問題ありません。問題は、他の人がその上で作業しているときです。
どう動くのか
同じ衝突がリベースで繰り返される理由も、構造から来ています。マージは、両端の点と共通祖先の3地点だけを比較しますが、リベースは、コミットを1つずつ再適用し、そのたびに3-wayマージを新しく行います。リベースは、1回のマージではなく、N回のマージです。そのため、コミット5つをリベースすると、同じ衝突を5回見ることがあります。rerere.enabled trueをオンにしておくと、一度解決した衝突の解決方法を記憶して、再適用します。
衝突状態とは、インデックスに、1つのパスの候補が3つ(1=共通祖先 / 2=ours / 3=theirs)入っている状態です。merge.conflictStyle zdiff3が勧められる理由が、ここにあります。相手が何を変えたかではなく、双方が何から出発したかを知ってはじめて、正しく統合できます。
インタラクティブリベースの動詞は6つです。pick(保持)、reword(メッセージだけ)、edit(内容の修正)、squash(統合し、メッセージも統合)、fixup(統合し、メッセージは捨てる)、drop(削除)。
--force-with-leaseは、リモート追跡参照が、自分が最後に見た値と同じかどうかを確認します。ただし、抜け穴があります。IDEがちょうどfetchしていた場合、参照がすでに更新されているため、自分が見ていないコミットが入ってきたのに、チェックを通過してしまいます。push.useForceIfIncludes trueで補完します。
現場での姿
リベース中に衝突が起きたとき、最もよくあるミスは、片側をまるごと選んでしまうことです。--oursや--theirsで素早く済ませると、相手の作業が静かに消えます。しかも、リベース中は、oursとtheirsの意味が、直感と逆に感じられます。再生されるコミット側がtheirsだからです。
もう1つは、中断のしかたを知らないことです。git rebase --abortは、開始前の状態に正確に戻します。行き詰まったときに、無理に押し進めるより、中断して計画し直すほうが、ほぼ常に良いです。
いつリベースして、いつマージするか
ルールは1つです。他の人が見ているコミットは、書き直しません。
| 状況 | 何を使うか |
|---|---|
| 自分のローカルブランチを最新のmainの上に | rebase: 履歴がすっきりします |
| すでにpushした共有ブランチ | merge: 書き直すと、他の人の履歴が壊れます |
| PRをmainに入れるとき | チームのルール(squash・rebase・mergeのいずれかに統一) |
| 元に戻す(公開されたもの) | revert: 新しいコミットで取り消します |
すでにプッシュしたブランチをどうしても書き直す必要があるなら、--force-with-leaseを使います。--forceと違い、リモートが自分が最後に見た状態と同じときだけプッシュします。その間に誰かがプッシュしていれば拒否されるため、他の人のコミットを消しません。
衝突を減らすツール
rerere: 同じ衝突に再び出会うと、以前に解決したとおりに自動で適用します。長いブランチを何度もリベースするときに、効果が大きいです。
git config --global rerere.enabled true
--onto: ブランチの根元だけを移動します。機能ブランチから分岐したブランチを、mainの上に移すときに使います。
A---B---C main
D---E feature
F---G hotfix ← D,E 없이 F,G 만 main 위로
git rebase --onto main feature hotfix
autosquash: レビューの指摘を直すとき、git commit --fixup <해시>(プレースホルダーはハッシュです)で印を付けておき、git rebase -i --autosquashで一度にまとめます。どのコミットに統合するかを手で選ばなくても構いません。
事故が起きたときの取り戻し方
リベース中に間違えても、たいてい取り戻せます。gitはコミットをすぐには消しません。
git reflog # HEAD 가 거쳐 온 모든 자리
git reset --hard HEAD@{5} # 그중 하나로 되돌아간다
# 리베이스 중이라면 그냥 그만둘 수 있다
git rebase --abort
reflogは、デフォルトで90日間保管されます。「コミットが消えた」は、たいてい事実ではなく、指しているブランチがなくなっただけです。ハッシュさえわかれば、取り戻せます。
git fsck --lost-foundは、どこからも指されていないオブジェクトまで探してくれます。reflogにもないときの、最後の手段です。
次のラボですること
分岐した2つのブランチを作り、リベース前後のハッシュを自分で比べ、早送りで一直線の履歴を作り、衝突をわざと起こして、両方の変更を保存しながら解決し、一度はわざと中断して、元の状態に復旧することを確認します。そのあと、インタラクティブリベースでコミット3つを1つにまとめて1つを捨て、黄金律を自分の文でまとめます。