Fast-forward マージ
現在のブランチが取り込む側のブランチの祖先なら、Git は Fast-forward マージを行える。スナップショットの統合やマージコミットの作成は不要で、現在のブランチ参照を既存コミットへ移す。
先に祖先関係を証明し、その後で参照を書く。成功時に変わるのは取り込み先のブランチで、取り込み元やコミットグラフではない。アニメーションは実際の親リンクをたどり、参照パネルで動く名前を示し、検証欄で参照書き込みとコミット数を数える。
グラフの条件
main が A、feature が C を指すとする。
A ← B ← C
↑ ↑
main feature
コミットは親を指すので、C から C、B、A と戻る。現在の main の先端 A が見つかれば、main から到達できる履歴は全て feature に含まれる。条件は:
is_ancestor(current_tip, source_tip)
成立後の更新はこれだけだ。
refs/heads/main: A → C
feature は C に残り、A、B、C の ID、親、ツリーも変わらず、四つ目のコミットは作られない。git merge は統合作業の名前で、新コミットが必要かはグラフと選んだ方式で決まる。
Fast-forward マージのステップ
既定は空コミットでローカルリポジトリを作る。
git commit --allow-empty -m "base"
git branch feature
git switch feature
git commit --allow-empty -m "feature-1"
git commit --allow-empty -m "feature-2"
git switch main
git merge --ff-only feature
固定の空ツリーでファイル変化を除き、参照の動きを見る。ID は表示だけの仮名ではなく、再現可能な Git 形式のオブジェクト ID だ。
- 最初のコミットが根オブジェクトを書き、未誕生の main をそこへ進める。
- feature を同じ根に作る。オブジェクトをコピーせず、HEAD も切り替えない。
- feature に切り替えた後、二回の空コミットで子オブジェクトを作り、feature だけを進める。main は根に残る。
- main への切り替えはシンボリック HEAD だけを変え、両先端は動かさない。
- merge が現在の main の先端と、取り込み元 feature の先端を解決する。
- 祖先カーソルが元の先端から親を一辺ずつたどり、旧 main に到達する。
- 証明後、main を元の先端へ移す。feature は動かない。
- オブジェクト数が変わっていないことを確認する。既存参照への書き込みは一回、コミット作成はゼロ。
祖先確認より先に参照を動かすと、現在の独立した履歴への入口を失う恐れがある。分類してから書く必要がある。
境界ケース
- 現在が取り込み元より後ろ: 現在の先端が元の祖先にあり、取り込み先を元の先端へ進める。書き込みは一回。
- 両先端が同じ: 最新状態なので移動せず、書き込みはゼロ。双方向で祖先関係だが、エンジンは等値を先に扱い、探索も省く。
- 取り込み元が現在より後ろ: このレッスンのモデルは、元から現在が見つからなければ現在の先端から戻って調べる(実際の Git はマージ基点を一度求め、両方の答えをそこから読む)。元が現在の祖先なら既に全履歴を含み、最新状態と報告して書き込みはゼロ。
--ff-only が拒否するとき
共通の基点から双方が別々にコミットすると、次の形になる。
M ← main
/
A
\
F ← feature
F から M へも、M から F へも戻れず、どちらも他方の祖先ではない。履歴は分岐している。git merge --ff-only feature は停止し、main、feature、オブジェクトを変えない。通常のマージならスナップショットを調整して二親コミットを作れるが、それは次のレッスンで扱う。ff-only は予期しない分岐を明示的な失敗にするために使える。
取り込み元のブランチ
取り込み元は統合対象のコミットを指定する入力であり、Fast-forward が更新するのはチェックアウト中の取り込み先だけだ。既定の完了後は main と feature が同じ値になるが、独立した参照のまま。以後はコミットした側だけが進み、名前同士が永久につながるわけではない。
本レッスンの HEAD は常にシンボリックで、取り込み先のブランチを名前に持つ。取り込み元は git merge --ff-only の引数で指定する。
よくある間違い
- 「元のコミットを現在のブランチへコピーする。」 ブランチ専用コピーはなく、参照を動かして同じ履歴へ二つの名前から到達できるようにする。
- 「マージは毎回コミットを作る。」 Fast-forward は元の先端を再利用する。新オブジェクトが必要かは方式とグラフで決まる。
- 「取り込み元も動く。」 既定の成功トレースは現在の参照へ一回、元へゼロ回の書き込みを行う。
- 「共通祖先があれば十分。」 分岐した枝にも共通祖先はある。一方の先端が他方の祖先でなければならず、前進するには現在が元の祖先である必要がある。
- 「最新状態なら先端が同じ。」 現在が元より先に進み、その履歴を含む場合も最新状態になる。
- 「作業ファイルが同じなら安全。」 資格はコミットグラフで決まる。実 Git は作業ツリーとインデックスの上書き安全性も調べるが、同じ内容は祖先関係の証明にならない。
試せるプリセット
マージ直前のプリセットは main を feature より後ろに置き、feature を選んで実行できる。同じ先端では探索ゼロ、書き込みゼロ。取り込み元が後ろならモデルは現在の先端から戻って調べ、分岐済みなら共通基点だけでは足りないことを示す。存在しないブランチはグラフを変えずに失敗する。成功後にもう一度コミットすれば、等しい二参照が独立していることも確認できる。
表示は三ブランチ、七コミット、二十コマンドまで。Git の制限ではなくレイアウト上の制限だ。
モデルの範囲
クリーンなローカルリポジトリ、接続状態の HEAD、単一親コミットだけを扱い、全コミットで固定の空ツリーを使う。作業ツリー、インデックス、チェックアウト競合、未追跡ファイル、マージコミット、内容調整、改名検出、リモート、上流設定、タグ、reflog、フック、署名、サブモジュール、置換参照、参照の並行更新、ガベージコレクションは省く。
実際の Git はロックファイルで参照を更新し、ローカル変更が上書きされないかも検査するが、現在のグラフモデルの対象外だ。ここでの規則は、現在の先端が元の祖先なら前進でき、グラフ上では取り込み先参照が既存コミットへ動くこと。
基礎はブランチと HEAD、コミットスナップショットのレッスンを参照できる。コマンドと祖先関係の詳細は git merge、git merge-base --is-ancestor、gitglossary にある。