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

マージ競合

エディターで直したように見えても、Git が継続を拒むことがある。解決済みかは作業ファイルのマーカーではなく、インデックスの状態で決まる。マーカーは文字列で、ステージ番号が状態を表す。

作業ファイルの編集は草稿の変更にすぎず、git add がその版をインデックスへ渡す。そのため、マーカーのないファイルが未解決のままでも、マーカーを含むファイルをステージできても不思議ではない。

マージ競合の仕組み

分岐した履歴のマージは三つの不変スナップショットを使う。base は共通の過去、ours はマージ前の HEAD、theirs はコマンドで指定した元の先端だ。

本モデルは一ファイル、その場で編集する三行、最大一つの競合ブロックに限る。各行で、両側が同じならどちらかを採る。ours が base と同じなら theirs、theirs が base と同じなら ours、三つが異なれば競合する。Git は一行ずつではなくハンク単位でマージする。一方の編集がもう一方と重なるか接し、間に変更のない行がなければ、一行だけなら規則で決まる場合でも、両方の編集は一つの競合ブロックに入る。

一行目は base が title=VizLab、両側が独立して title=VizLearn に変わっているので、等値規則で VizLearn を採る。HEAD が優先されるからでも、base と同じだからでもない。

競合は半完成のコミットを作らない。HEAD と現在のブランチはマージ前に残り、MERGE_HEAD が元先端を記録する。未マージパスは stage 0 を除き、stage 1 に base、2 に ours、3 に theirs を持つ。作業ファイルには次の結果とマーカーを書く。

title=VizLearn
<<<<<<< HEAD
accent=navy
=======
accent=teal
>>>>>>> feature
footer=v1

マーカーを消すだけでは足りない。git add app.txt が現在の作業バイトを blob にし、stage 1/2/3 を除いて一つの stage 0 を書く。その後 git merge --continue がツリーと順序付き二親コミットを書き、現在のブランチを進め、MERGE_HEAD を消せる。

add はバイトを保存するだけで、内容の良し悪しを判断しない。マーカーが残っていても stage 0 に入り、通常の文字列としてコミットできてしまう。

マージ競合のステップ

既定は十操作からなる。

commit base:          title=VizLab   | accent=blue   | footer=v1
git branch feature
commit main-theme:    title=VizLearn | accent=navy   | footer=v1
git switch feature
commit feature-theme: title=VizLearn | accent=teal   | footer=v1
git switch main
git merge feature
edit app.txt:         title=VizLearn | accent=indigo | footer=v1
git add app.txt
git merge --continue
  1. base は title=VizLab と accent=blue を持ち、main と feature が最初にそこを指す。
  2. main-theme はタイトルを VizLearn、二行目を accent=navy にする。feature-theme は同じタイトル変更と accent=teal を選ぶ。両先端は基点を共有するが互いを含まない。
  3. main に戻ってマージし、ours=main-theme、theirs=feature-theme を固定する。唯一の基点は base。MERGE_HEAD は feature を指し、HEAD は動かない。
  4. 一行目は両側で VizLearn なので採用し、三行目は全て未変更。二行目は base=blue、ours=navy、theirs=teal で全て違い、一つの競合を記録する。
  5. app.txt のインデックスは stage 1/2/3 となり、作業ファイルは七行のマーカー形式になる。追加コミットと現在参照の書き込みはゼロ。
  6. エディターでマーカーを accent=indigo に替える。作業ツリーだけが変わり、インデックスは stage 1/2/3 のままなので未解決だ。
  7. git add app.txt が解決案の三行を stage 0 blob にする。未マージの三ステージは消えるが、MERGE_HEAD は残り、ブランチは動かない。
  8. continue が未解決パスのないことを確かめ、stage 0 からツリーを作る。第一親をマージ前 main、第二親を固定した feature とするコミットを作る。
  9. 最後に main を進め、feature は動かさず、MERGE_HEAD を消す。インデックスと作業ツリーは accent=indigo を保持する。

下の二枚のカード間の矢印は方向を示す。競合の表示は三ステージのインデックスから作業ツリーへ流れ、add は編集したバイトを作業ツリーから stage 0 へ戻す。

インデックスが三ステージを持つ理由

ステージ番号はインデックスのキーに含まれる。通常の app.txt は stage 0 一項目だが、内容競合中は同じパスに三つの blob ID が必要になる。

ステージ役割このマージでの意味
1base共通祖先のファイル
2oursマージ前 HEAD のファイル
3theirsMERGE_HEAD のファイル
0resolvedadd が選んだ単一の版

三つの blob はマージツールに必要な証拠を保ち、赤いフラグ一つより多くを表す。作業ファイルは編集可能な提案だ。stage 0 への置換後は次のツリーへ入る正確なバイトが決まるが、その論理が正しいとは保証しない。

本エンジンが各先端の祖先を集めるのは到達可能なコミットグラフで O(V + E)。共通祖先から最良のものを選ぶには共通祖先の組ごとにグラフをたどるので最悪 O(V²·(V + E)) だが、八コミットの上限で小さく収まる。行の処理は論理行数 L とバイト数 B に対して O(L + B)。解決内容のハッシュ、ツリーとコミットの直列化は対応する内容量に線形となる。これは限定エンジンの計算量で、実 ort の性能を表さない。

競合を解決する手順

  1. git status の unmerged paths で、Git が未解決とみなすパスを確認する。
  2. git ls-files -u で三入力を見る。1 が base、2 が ours、3 が theirs。
  3. ファイルを編集し、プログラムの意図する動作を決め、関連テストを実行する。まだ候補の解決案だ。
  4. git add app.txt 後、git diff --cached でステージ結果を確認する。stage 1/2/3 は stage 0 に置き換わる。
  5. 正しければ git merge --continue。本レッスンのクリーンな開始状態へ戻すなら git merge --abort を使う。

境界ケース

  • main が一行目、feature が三行目を変えれば、変更のない二行目が編集を隔てるので両方を採れ、stage 1/2/3 もマーカーも生じない。隣り合う行の編集は接して一つの競合ブロックになる。
  • 編集後に add せず continue しても、インデックスは未解決なのでコミットも参照移動も起きない。
  • マーカー付きで add しても有効な stage 0 になり、continue がその文字列をコミットしうる。
  • abort は本レッスンのマージ前作業ツリーがクリーンという条件で、開始時 HEAD からインデックスと作業ファイルを戻し、一時状態を消す。両ブランチは動かない。
  • MERGE_HEAD がなければ continue と abort は失敗する。本レッスンの編集・add 操作は停止中のマージでだけ使える。
  • 八コミット、三ブランチ、二十四コマンドは表示制限で、Git の制限ではない。

よくある間違い

  • マーカーを競合そのものと考える。権威ある状態はインデックスの stage 1/2/3 で、マーカーは作業テキストだ。
  • エディターの保存で Git 状態も解決したと思う。作業ツリーだけが変わり、add が未マージ項目を置き換える。
  • add が解決内容を検証すると思う。残存マーカー、デバッグ出力、誤った組み合わせも、そのままステージされる。
  • 競合コミットが即座にできると思う。結果ツリー、マージコミット、参照更新は解決後になる。
  • 両ブランチを動かすと考える。現在のブランチだけが進み、元は固定される。
  • 親順を忘れる。第一親は旧現在先端、第二親は MERGE_HEAD で、その順序もハッシュ対象だ。

モデルの範囲

クリーンなローカルリポジトリ、接続 HEAD、一つの app.txt、初期三論理行、その場での行編集、唯一の基点、最大一競合ブロックを扱う。追加、削除、改名、ディレクトリとファイルの競合、バイナリ、複数基点、独自ドライバー、属性、rerere、フック、署名、サブモジュール、疎・浅い履歴、既存の作業編集、リモートは省く。実 ort は競合時に AUTO_MERGE ツリーも書くが、本レッスンは模擬していない。

実際の動作は git merge、git ls-files、Git Book の競合例 を参照できる。