大きな画面での利用をおすすめします — アニメーションと操作がより快適になります。
入力

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 だ。

  1. 最初のコミットが根オブジェクトを書き、未誕生の main をそこへ進める。
  2. feature を同じ根に作る。オブジェクトをコピーせず、HEAD も切り替えない。
  3. feature に切り替えた後、二回の空コミットで子オブジェクトを作り、feature だけを進める。main は根に残る。
  4. main への切り替えはシンボリック HEAD だけを変え、両先端は動かさない。
  5. merge が現在の main の先端と、取り込み元 feature の先端を解決する。
  6. 祖先カーソルが元の先端から親を一辺ずつたどり、旧 main に到達する。
  7. 証明後、main を元の先端へ移す。feature は動かない。
  8. オブジェクト数が変わっていないことを確認する。既存参照への書き込みは一回、コミット作成はゼロ。

祖先確認より先に参照を動かすと、現在の独立した履歴への入口を失う恐れがある。分類してから書く必要がある。

境界ケース

  • 現在が取り込み元より後ろ: 現在の先端が元の祖先にあり、取り込み先を元の先端へ進める。書き込みは一回。
  • 両先端が同じ: 最新状態なので移動せず、書き込みはゼロ。双方向で祖先関係だが、エンジンは等値を先に扱い、探索も省く。
  • 取り込み元が現在より後ろ: このレッスンのモデルは、元から現在が見つからなければ現在の先端から戻って調べる(実際の 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 にある。