複数人でソフトウェアや文書を開発する際、日常的に行われるのがGitの「マージ(変更の統合)」です。各自がブランチで作業を進め、成果を合流させるとき、離れた場所の変更であればGitは何事もなかったかのように自動で統合し、同じ場所を変更していれば「コンフリクト(競合)」として作業を止め、人間に判断を求めます。
しかし、Gitは一体どのような仕組みで変更同士を組み合わせ、何を基準に「衝突した」と判断しているのでしょうか。
Gitが変更を統合する根本の仕組みは「三方向マージ(Three-way merge)」です。この記事では、なぜ統合に過去の歴史が必要なのか、どのような規則で自動結合が行われるのか、そして最も見落としやすい「テキストの衝突が起きないのに中身が壊れる現象」まで、仕組みと注意点を論理的に解き明かします。
---

1. 2つの変更を比べるだけでは統合できない理由
マージの仕組みを理解するために、シンプルな思考実験から始めましょう。 あるファイルを、AliceとBobの2人が各自の環境で編集したとします。作業が終わり、2つの変更を合流させるとき、手元に「Aliceの変更後ファイル」と「Bobの変更後ファイル」の2つだけが渡されたらどうなるでしょうか。
たとえば、Aliceのファイルには新しい設定行が書かれており、Bobのファイルにはその行がなかったとします。このとき、統合後のファイルにその行を残すべきでしょうか、それとも消すべきでしょうか。
2つのファイルを見比べるだけ(二方向マージ/Two-way merge)では、この問いに答えることはできません。なぜなら、以下の2つの可能性を区別できないからです。
- Aliceが必要だから「新しい行を追加した」のか(この場合、行を残すべきです)
- Bobが不要だから「既存の行を削除した」のか(この場合、行を消すべきです)
2者だけの比較では、「追加」と「削除」の区別がつきません。どちらが新しい変更なのかを判定する基準が存在しないためです。
歴史の分岐点「共通祖先(マージベース)」の役割
この問題を解決するために、Gitは必ず「過去の歴史」を参照します。2人が作業を枝分かれさせる直前の状態、すなわち2人の共通の過去である「共通祖先コミット(Merge Base)」を取り出します。
共通祖先という基準点があることで、Gitは「共通祖先」「Aliceの変更」「Bobの変更」の3つを同時に比較できるようになります。これが「三方向マージ(Three-way merge)」です。
共通祖先を照らし合わせると、判定は明確になります。
- 共通祖先にその行が存在しなかった場合:
Aliceが「行を追加」し、Bobは「触らなかった」と分かります。したがって、Aliceの変更を採用して統合結果に行を残します。
- 共通祖先にその行が存在していた場合:
Aliceは「触らなかった」のに対し、Bobが「行を削除した」と分かります。したがって、Bobの変更を採用して統合結果から行を削除します。
客観的な基準点があるからこそ、Gitは誰がどのような変更を加えたのかを機械的に判定できるのです。
例外:三方向マージを行わない「Fast-forwardマージ」
ただし、Gitで行われるすべてのマージが三方向マージを実行するわけではありません。
メインブランチから作業ブランチを分岐させてコミットを進めたものの、その間にメインブランチ側には新しいコミットが1つも追加されていなかった場合、歴史は枝分かれしておらず一直線に並んでいます。 このようなケースでは、差分の比較を行う必要がありません。Gitは単に、メインブランチの参照位置を作業ブランチの先端まで進めるだけで統合を完了します。これを「Fast-forward(早送り)マージ」と呼びます。三方向マージが使われるのは、共通祖先から双方がそれぞれコミットを重ね、歴史が二股に分岐した状態を合流させる場合です。
共通祖先が複数ある場合(交差マージ)
ブランチが単純に分岐しただけであれば、共通祖先は過去の分岐点に1つだけ見つかります。しかし、お互いのブランチを相互にマージし合った複雑な歴史(交差マージ/criss-cross merge)では、共通祖先の候補が2つ以上現れることがあります。
近年のGit(デフォルトのort戦略など)は、候補となる複数のマージベース同士をまず内部で自動マージし、「仮想マージベース(virtual merge base)」と呼ばれる共通祖先を合成して三方向マージの基準点を組み立てます。どのような分岐履歴であっても、客観的な共通の過去を割り出す仕組みが備わっています。
---
2. 三方向マージの判定ルールと自動統合の仕組み
共通祖先が定まると、Gitは変更内容を突き合わせて自動統合を進めます。Gitはどのような規則に基づいて変更を結合しているのでしょうか。
理解のための基本ルール:3つの状態
ファイルを「行」の集まりとして単純化し、祖先(Base)、自分(Ours)、相手(Theirs)の3つの状態を比較する基本ルールは以下のとおりです。
- 祖先から「一方だけ」が変更した行
自分だけが編集し相手が触っていない行は、自分の変更を採用します。相手だけが編集した行は、相手の変更を採用します。行の追加や削除も同様で、片方だけの変更はそのまま結果に反映されます。
- 祖先から「双方がまったく同じ変更」をした行
双方が偶然にも同じ行をまったく同じ内容に書き換えた場合、その変更を採用します。双方の合意とみなせるため、衝突にはなりません。
- 祖先から「双方が異なる変更」をした行
同じ行に対して、自分が「A」、相手が「B」と異なる書き換えをした場合、Gitはどちらを採用すべきか機械的に判断できません。これが「コンフリクト(競合・衝突)」です。一方が行を修正し、もう一方がその行を削除した場合も同様に競合となります。
互いに干渉していない変更であれば、この論理規則に従って自動的に1つのファイルへと合成されます。
「異なる行なら安全」という誤解と変更領域(チャンク)
ここで注意すべきなのは、「編集している行番号が違っていれば衝突しない」という思い込みです。
Gitはファイルを単なる1行ごとのバラバラな単位で判定しているわけではありません。内部の差分アルゴリズムは、変更箇所とその前後の文脈行(コンテキスト行)をひとまとまりにした「変更領域(チャンク/Hunk)」という単位で差分を扱います。
そのため、2人が異なる行番号を編集していたとしても、その変更箇所が隣り合っていたり極めて近かったりすると、Gitはそれらを同一の変更領域として認識することがあります。その結果、行番号自体は異なっていても、変更領域が重なり合っていると判断されて競合が発生します。前述の「行ルール」は直感的な理解のための整理であり、実際のGitは前後の文脈を含んだ領域単位で判定を行っています。
ファイルの追加・削除・リネームの追跡
三方向マージの対象はファイル内のテキストだけではありません。ファイルの追加や削除、さらには「ファイル名の変更(リネーム)」も追跡されます。
たとえば、一方の開発者がファイル名を data_processor.py から processor.py へ変更し、同時に別の開発者が元の data_processor.py の中身を編集していたとします。Gitはファイル内容の類似度を計算するリネーム検知機構を持つため、名前が新しくなった processor.py の中に相手の変更を自動的にマージします。ファイルシステムの構造変化も含めて、三方向での統合が行われます。
---
3. テキストの衝突(コンフリクト)はどう検知され、どう解決するのか
同じ変更領域に対して双方が相容れない修正を加えた場合、Gitは自動統合を停止します。これが「テキスト競合(マージコンフリクト)」です。
競合マーカーの構造と読み方
競合が発生すると、Gitは対象ファイルの衝突箇所に「競合マーカー」を直接書き込み、処理を一時停止します。
<<<<<<< HEAD
const apiUrl = "https://api.staging.example.com";
=======
const apiUrl = "https://api.production.example.com";
>>>>>>> feature/update-endpointsマーカーの意味は次のとおりです。
<<<<<<< HEADから=======まで:現在チェックアウトしているブランチ(マージを受け入れる側)の内容です。=======:2つの変更の境界線です。=======から>>>>>>> feature/update-endpointsまで:合流させようとしている相手ブランチの内容です。末尾にはブランチ名やコミットIDが示されます。
Gitはどちらが正しいか推測せず、両方の主張を並べて人間に判断を委ねています。
4段階の解決手順
競合が発生したときは、以下の4段階の手順で解決します。
- 状況の把握と合意
git status を実行して競合が発生しているファイルを確認します。相手のコミット履歴や変更意図を確認し、チーム開発であれば方針を擦り合わせます。
- ファイルの手動編集
エディタで対象ファイルを開き、マーカー記号(<<<<<<<, =======, >>>>>>>)をすべて削除した上で、あるべき正しい内容に書き換えます。片方のコードをそのまま採用する場合でも、マーカー行は必ず消去します。
- 解決済み登録(ステージング)
編集を終えたら、git add <ファイル名> を実行します。ここでの git add は、衝突を手動で解決したことをGitに宣言する役割(インデックスへの登録)を果たします。
- 統合コミットの作成
すべての競合ファイルを git add したら、git commit(または git merge --continue)を実行します。これにより、2つの親コミットを持つマージコミットが作成され、統合が完了します。
---
4. 衝突ゼロでも壊れる「意味の矛盾」:見落とされやすいマージの落とし穴
「コンフリクトが起きなければ、マージは完全に成功した」と考えるのは禁物です。ここに、Gitを利用する上での最大の落とし穴があります。
「衝突がない=正しい」という錯覚の否定
Gitを使っていて、競合が1件も発生せず自動マージが完了したとき、中身が正しく統合されたと安心してしまうことがあります。しかし、以下の鉄則を理解しておく必要があります。
「テキスト行や変更領域の衝突が検出されないことは、統合結果の論理的・実質的な正しさを一切保証しない」
Gitが検査しているのは「テキストの変更領域が物理的に重なっているかどうか(排他的差分)」だけであり、文章の意味やプログラムの動作といった「論理の意味論(セマンティクス)」を解釈していません。変更領域が離れていれば、どれほど深刻な論理矛盾が生じていても、Gitは何の警告も出さずに自動統合を完了させます。
代表例:スケジュール固定文で見る意味の破綻
この現象を明確に示す具体例を見てみましょう。イベント運営の予定表をGitで管理している場面を想定します。
【共通祖先(元のスケジュール)】
開場:10:00
会場:第一ホール
スタッフ到着:09:00この運用には「スタッフは開場の1時間前に到着して準備する」という前提があります。元データでは、9時にスタッフが着いて10時に開場するため、前提は満たされています。
ここで、2人の担当者が別々のブランチで予定を調整しました。
【担当者Aliceの変更(開場を早める)】
開場:09:15
会場:第一ホール
スタッフ到着:09:00Aliceの視点では、到着(09:00)が開場(09:15)より15分前なので、「開場前にスタッフがいる」という前提は保たれています。
【担当者Bobの変更(スタッフ到着を遅らせる)】
開場:10:00
会場:第一ホール
スタッフ到着:09:30Bobの視点でも、到着(09:30)は開場(10:00)の30分前であり、前提を満たしています。
【Gitによる自動統合の結果】 この2つのブランチをマージするとどうなるでしょうか。 検証環境(Git 2.47.3、デフォルトのort戦略、UTF-8・LF改行)で実行すると、Aliceが変更した1行目とBobが変更した3行目の間に、未変更の「会場:第一ホール」という行が挟まれています。 そのため、Gitはこれらを独立した変更領域と判定し、テキスト競合を一切出さずに自動マージを完了させます。
出力される統合結果は次のとおりです。
開場:09:15
会場:第一ホール
スタッフ到着:09:30内容を確認すると、「一般来場者の開場が09:15」であるのに対し、「スタッフの到着が09:30」になっています。開場から15分後にスタッフが現れるという、現実の運用では破綻した結果が生じています。 単独ではそれぞれ成り立っていた変更同士が、組み合わさることで致命的な矛盾を引き起こしました。しかしGitは、この意味の破綻に一切気づくことができません。
プログラムコードにおける意味論的競合
同様の問題は、プログラミングにおいてより頻繁に発生します。
たとえば、Aliceがある関数の名前を calculateTotal() から computeTotal() へリファクタリングしたとします。同時に別のブランチで開発していたBobは、新機能の実装の中で既存の calculateTotal() を呼び出すコードを新しく追加しました。
Aliceは関数の「定義」を変更し、Bobは別の箇所で「呼び出し」を追加しました。テキストの編集領域は重なっていないため、Gitは何の警告も出さずにマージを完了させます。
しかし、統合されたコードを実行するとどうなるでしょうか。不整合の表面化の仕方は開発環境によって異なります。
- 静的型付け言語(TypeScript, Go, Java, Rustなど)の場合
マージ後のビルド(コンパイル)時に「calculateTotal が見つからない」というエラーが発生し、プログラムを実行する前に機械的に不整合を検知できます。
- 動的型付け言語(Python, JavaScript, Rubyなど)の場合
事前のコンパイルがないため、マージは通過します。テストが整備されていれば実行時に検知できますが、テストが不足していれば、本番環境でそのコードが実行された瞬間に NameError や TypeError で異常停止します。
- 仕様や計算ロジックの変更の場合
さらに深刻なのは、関数名ではなく「関数の振る舞い(税抜計算から税込計算への変更など)」が変わった場合です。この場合、ビルドエラーも実行時例外も発生せず、誤った計算結果が出力され続けるサイレント障害となり、発見が遅れる危険があります。
テキストの衝突がゼロであることと、プログラムが正常に動作することの間には、明確な断絶が存在するのです。
---
5. インタラクティブ教材ツールの読み方と限界
INTERACTIVE
実際に動かして確かめる
条件や数値を変え、本文で説明した関係を確認できます。教材ツールの目的と3つのケースの読み方
三方向マージの仕組みと、テキスト競合・意味の矛盾の違いを体験するため、この記事には操作できる教材ツールと静止した解説図を掲載しています。
ツール内では、以下の3つの代表的なシナリオを切り替えて確認できます。
- ケース1:片側のみの変更
共通祖先に対して片方のブランチだけが編集を加えた場合、未変更側の状態が保たれながら、安全に差分が自動採用される様子を確認できます。
- ケース2:テキストの衝突(コンフリクト)
同一の変更領域に対して双方が異なる修正を加えた場合、Gitが自動統合を止め、<<<<<<< などの競合マーカーを挿入して判断を保留する挙動を示しています。
- ケース3:意味の矛盾(セマンティック・コンフリクト)
前節で解説したスケジュール固定文(会場行を挟んだ予定表)を用います。テキスト競合が起きずに自動統合される一方で、結果として出力された内容が現実の前提(開場と到着の前後関係)を破綻させている様子を再現しています。
教材ツールの限界と検証条件
このツールを活用する上では、以下の前提と技術的限界を把握しておく必要があります。
- 固定教材データとしての位置づけ
本ツールは動作原理を直感的に伝えるための固定教材データを表示するコンポーネントであり、ブラウザ上で実際のGitバイナリをリアルタイムに実行しているわけではありません。
- 検証環境とアルゴリズムの境界挙動
ケース3の「競合を起こさずに意味だけが矛盾する」結果は、手元の標準検証環境(Git 2.47.3、デフォルトのort戦略、UTF-8・LF改行、システムやグローバルの個別設定なし)で再現確認された固定データに基づいています。 ただし、実際のGitにおける変更領域の判定基準は、マージ戦略(ort戦略か旧来のrecursive戦略か)、差分アルゴリズム(MyersやHistogramなど)、あるいはコンテキスト行の設定によって、近接する行の扱いが変動することがあります。教材ツールは、マージの論理構造と落とし穴を理解するためのモデルケースとして参照してください。
---
6. 安全な統合のために:Gitの限界を補う多層的な検証
Gitの仕組みを理解すると、テキスト処理に頼ることのリスクと、それを補うための対策が見えてきます。
道具の限界を見極める
Gitは知性を持った存在ではありません。決められた文字列比較の数式とルールを機械的に実行しているに過ぎません。競合マーカーを出すのも、親切心から警告しているのではなく、テキスト処理の規則上どちらを採用すべきか計算できないため処理を中断しているだけです。テキストの計算が成立しさえすれば、論理がどれほど矛盾していても統合を完了させます。
多層防御の構築
テキストの衝突がなくても意味が破綻する以上、安全な開発のためには、Gitの外側に「多層的な検証(多層防御)」を構築することが不可欠です。
- 静的解析と型システム
コンパイラや静的型チェッカー、リンターをCI(継続的インテグレーション)に組み込みます。名前の変更漏れや型の不一致、構文エラーは、マージ直後の機械的検証で即座に検知します。
- 自動テストの網羅
「マージ後に前提条件が崩れていないか」を検証する主要な防壁です。単体テストや機能テストをCIで自動実行し、セマンティックな矛盾が発生した際にビルドを失敗させます。
- 人間による設計・意図のレビュー
機械的検証をパスした変更に対し、人間が「仕様の意図や業務前提が衝突していないか」をレビューします。文法チェックをツールに任せることで、本質的な論理の確認に集中できます。
- ステージング環境での動作確認
本番環境へ反映する前に、実環境に近いステージング環境へデプロイし、システム全体の結合動作に異常がないかを最終確認します。
結び:変更を正しく統合するために
Gitの変更統合は、共通祖先を羅針盤として過去と現在の状態を比較する、精緻な三方向マージのアルゴリズムに基づいています。
共通祖先があるからこそ、分散環境でも追加と削除を取り違えずに統合でき、テキストが衝突した際には勝手な推測をせず人間に判断を委ねてくれます。しかし、Gitが保証するのは「テキスト領域の物理的な独立性」だけであり、論理の正しさではありません。
テキストの結合はGitに委ねつつ、意味の正しさは型チェック、テスト、レビューといった多層の仕組みで検証する。この役割分担を徹底することが、安全な開発を支える基本原則となります。
---
よくある質問(FAQ)
Q1. Fast-forwardマージと三方向マージはどのように使い分けますか?
A: チームのブランチ運用方針によって使い分けられます。 Fast-forwardマージは履歴が枝分かれせず一直線に並ぶため、履歴を簡潔に保ちたいトランクベース開発などで好まれます。一方、三方向マージ(または --no-ff を指定したマージ)を行うと、機能ブランチの作業履歴と合流時点がマージコミットとして記録に残ります。実務では「履歴の直線性を優先するか、機能単位の合流履歴の可視性を優先するか」をチーム内で合意して運用します。
Q2. コンフリクトが起きた際、両方の変更を活かして解決できますか?
A: 問題なく活かせます。 競合マーカー(<<<<<<<, =======, >>>>>>>)は、Gitが人間に判断を委ねている目印です。解決にあたっては、マーカーをすべて削除した上で、双方のコードを並べて共存させたり、両方の意図を統合した新しいコードに書き直したりして構いません。編集後に git add を行うことで、その手動編集結果が解決済みの内容として記録されます。
Q3. 構文や意味を理解して論理矛盾を防ぐマージ機能はGitにないのですか?
A: 標準のGitにはありませんが、抽象構文木(AST)を解析してコード構造を理解しながら統合する「セマンティックマージ」と呼ばれる外部ツールは存在します。 関数の定義位置が移動しただけの変更を賢く追従するなど高度な統合が可能ですが、言語ごとの文法対応の難しさや、今回取り上げたスケジュールの前後関係のような業務上の論理矛盾までは検出できません。そのため現場では、「Gitはテキスト統合に専念し、論理の検証は型検査やCIの自動テストで行う」という役割分担が一般的です。
---
