リベース
リベースは現在のブランチ固有のコミットを古い順に、別の基点へ再適用する。置き換え後も各変更とメッセージを保つが、親リンクが変わるため新しいオブジェクト ID になる。対象のブランチは動かさない。未共有の機能ブランチの更新や整理に向き、他の人が元 ID を使っている場合は先に調整が必要だ。
旧版の本への二つの修正を新版へ順に写すと考えられる。旧ページを動かすのではなく、新版を土台に替わりの記録を作る。後の修正が前の修正に依存するなら、順序を逆にできない。
リベースの仕組み
feature と main が分岐点を共有するとする。Git は feature の先端から戻り、main から到達できないコミットを集める。逆向きでは新しいものから見つかるため、再適用前に反転して古い順にする。
対象の先端から再適用を始める。各旧コミットの親と自身のスナップショットを比べ、差分を一時スナップショットへ適用し、新しいツリーとコミットを書く。原作者とメッセージは保持するが、親が変わり、コミッター情報は新しくなる。親ヘッダーはハッシュ対象なので、同じパッチでも ID が変わる。
作業中の HEAD は対象または最新の再適用コミットを直接指す分離状態になる。feature 参照は元の先端に残る。全て成功して初めて feature を一度動かし、HEAD を再接続する。元の先端が残るため、中止時の戻り先が明確になる。
元のコミットを編集したり即座に削除したりはしない。feature が離れるとブランチから到達できなくなる場合があるが、保持規則とガベージコレクションが処理するまでオブジェクト庫に残りうる。
実 Git は上流に同等のパッチを見つけると、対応するクリーンな cherry-pick を再適用キューから省くことがある。--reapply-cherry-picks は再適用を求める。本レッスンは等価パッチを走査せず、共通基点の後にある現在側の全コミットをキューへ入れる。
リベースのステップ
既定は一行ずつの app.py、README.md、config.json を使う。
commit baseが app.py=print("v1")、README=hello、config={"port":3000}を作り、オブジェクトの作成後に main を進める。branch featureが同じ基点に参照を作り、HEAD は main に残る。switch featureが HEAD を feature に接続する。commit feature-appが app.py をprint("v1")からprint("feature")にする。commit feature-v2がそれをprint("feature v2")にする。前の変更が作ったprint("feature")を必要とするため、こちらを先に適用すると一致する入力がない。switch mainで main に戻る。commit main-configが config だけを{"port":8080}に変える。main は設定変更、feature は二つの別変更を持ち、分岐する。switch featureで再び feature を選ぶ。rebase mainが基点を見つけ、feature-app、feature-v2 の順に並べる。一時的な分離 HEAD は main-config から始める。
一回目は main のポート 8080 を保ち、app.py を print("feature") にして新 feature-app を作る。二回目は期待する旧値を見つけ、print("feature v2") に変えて新 feature-v2 を作る。逆順なら後の変更の入力状態が存在しない。
二回とも成功すると feature を置換後の先端へ一度動かし、main は main-config に残し、HEAD を feature へ接続する。旧二コミットは保存されたまま、ブランチから到達できなくなる。画面では旧鎖が薄れ、プライム付きの新鎖が新基点につながる。対応表は旧コミットと置換コミットを示す。
リベースのコスト
再適用する数を k、差分計算と適用で検査する内容総量を D とする。履歴の走査は調べた祖先数に比例し、この単一親モデルの再適用はおよそ O(D)。最大 k 個の新コミットと、結果に必要なツリー、blob を書く。
実際の負担は競合処理にあることが多い。重ならないパッチは自動適用できるが、対象側の編集と重なると停止し、現在のファイル内容を決め直す必要がある。長いブランチでは、一度の大きなマージ競合が、各旧コミットでの複数の小さな競合へ分かれる場合がある。扱いやすさはコミットの構成に依存する。
未共有の機能ブランチをレビュー前に更新し、線形の系列にするならリベースが使える。チームが既に元 ID に依存しているなら、マージを選ぶか先に調整する。書き換えると、他のクローンは似た変更を別 ID で持つ二履歴を扱う必要がある。
本モデルはクリーンなローカルリポジトリ、唯一の基点、通常の非対話リベース、単一親コミットを扱う。パッチは固定の一行ファイルへの三方向マージとして適用し、Git のマージ機構を簡略化している。マージコミット、対話編集、--onto、fork-point、サブモジュール、改名の多いケース、競合後の継続は省く。拡張されても置換コミットが新オブジェクトであることは変わらない。
境界ケース
- 対象が既に現在の祖先なら、本モデルでは移植不要で、コミットも参照書き込みもゼロ。
- 現在が対象の祖先なら、現在のブランチを直接 Fast-forward でき、置換コミットは不要。
- 最初のパッチで競合すると、一時 HEAD は新基点、
REBASE_HEADは失敗した旧コミットを指し、現在ブランチは元先端に残る。 - 一部成功後に競合すると、一時 HEAD は最後の置換コミットを指す。ブランチは未移動なので、
git rebase --abortで元の接続状態へ戻れる。 - 中止で一時コミットは到達不能になるが、バイトが即座に消える保証はない。
- 対象や未誕生の現在ブランチにコミットがなければ、必要な二先端を解決できない。
- パッチが既に上流にあり、再適用してもスナップショットが変わらなければ、コミットを書かずに破棄する。Git の既定
--empty=dropと同じ。 - 上流の同等 cherry-pick は実 Git が省くことがあり、
--reapply-cherry-picksで選択を変えられる。本モデルはその走査を省く。
よくある間違い
- リベースがコミットを「移動する」と言う。旧オブジェクトを読み、新オブジェクトを書く。
- パッチとメッセージが同じなら ID も同じと思う。親が変わり、本文とハッシュが変わる。
- 方向を逆にする。feature 上の
git rebase mainは feature を書き換え、main を feature 上へ置かない。 - 対象ブランチを出力と考える。成功時に現在ブランチを動かし、対象参照は動かさない。
- 競合時にブランチが部分的な新鎖を指すと思う。進捗は分離 HEAD が持ち、ブランチは旧先端に残る。
- 共有済み ID を書き換えて、変わらなかったようにプッシュする。他の人は別オブジェクトの二履歴を調整する必要がある。
続けて学ぶ
同じ手法につながるレッスンや、次の概念への入口です。
誤りや気になる点を見つけましたか? フィードバックを送る。