Gitは分散型のバージョン管理システムで、複数のブランチ(参考 ブランチの基本)で同時に開発が進むことを前提に設計されています。このような環境で、並行して進んでいた変更を1つにまとめる行為が「マージ(merge)」です。単にファイルを結合するだけでなく、履歴(コミットの流れ)を整合的に再結合し、将来的な履歴解析や管理を容易にする点がマージの特徴です。
本記事では、Gitマージの基本的な仕組みから内部動作、そして初心者が混乱しやすいポイントについて、順を追って解説します。
また、難しい話の後には、「こう考えるとシンプルに捉えられる」というコツも付け加えます。これにより、単純な操作手順以上に、なぜGitがこのような動きをするのか理解でき、トラブルシューティングやブランチ戦略の検討に役立つでしょう。
Gitのデータモデル:コミットとスナップショット
Gitは、各コミットを「プロジェクト全体のスナップショット」として扱います。
- コミットは、ファイル群の状態を示すスナップショットと、そのスナップショットに至るまでの履歴(親コミットへの参照)を持っています。
- Gitは履歴を「DAG(有向非巡回グラフ)」という形で管理し、ブランチはそのDAG上の特定のコミットを指す「名前(ポインタ)」です。
「スナップショット」という考え方は、「差分管理」とは少し異なります。Gitは内部的には差分の圧縮を行いますが、基本的なモデルは「各コミットで全体を丸ごと記録している」ような感覚で構いません。
「Gitは各コミットごとに、プロジェクトの写真を1枚撮って、それを積み重ねていく」とイメージするとわかりやすいです。さらに詳しく理解したい方は↓ページをご覧ください。
ブランチとDAG構造
ブランチは複数の機能開発を並行して行うために作成されます。
- ブランチAで新機能1を開発
- ブランチBで新機能2を開発
これらは、DAG上でコミットの流れが2つに分かれている状態です。
ここで注意して覚えておきたい点。それはブランチは「ディレクトリ」でも「コピー」ではないということ。単に「あるコミットを指す名札」だと思ってください。ブランチが増えても、実際にファイルの重複が増えるわけではありません。詳しくは参考ブランチとは?をご覧いただければと思いますが、ブランチとは、履歴の分岐点に置かれた旗印(フラグ)のようなもの」だというのが、マージを本質的に理解するうえでの重要ポイントです。
マージの核心:3-wayマージと共通祖先
Gitのマージは「3-wayマージ」という仕組みを用いています。
3-wayマージとは
マージ対象となる2つのブランチには、それらが分岐する前の「共通祖先」が必ず存在します。3-wayマージは以下の3点を比較します。
- 共通祖先コミット
- マージ元ブランチの先頭コミット
- マージ先ブランチの先頭コミット
共通祖先と各ブランチ先頭の差分を計算し、その差分を統合することで、両方の変更を取り込んだ新たなコミットを生成します。
「共通祖先」を探すのは、Gitが自動でやってくれるため、普段は意識しなくてもマージは可能です。ただ、なぜ共通祖先が必要なのかがわからないと、「どこが起点で、何を比べているのか」が見えにくくなります。
具体例:
- 共通祖先(A)は、まだ新機能が加えられる前のプロジェクト状態とします。
- ブランチAの先頭(A')では、例えば「ファイルXに行追加」を行ったとします。
- ブランチBの先頭(B')では、「ファイルYを修正」しました。
このとき、A~A'がブランチAの進化、A~B'がブランチBの進化です。
マージでは、この「A→A'」の変更と「A→B'」の変更を足し合わせるイメージです。
「共通祖先は『分かれ道の分岐点』、そこからそれぞれがどう違う方向に進んだのかを見て、最後にそれらを足し算する」と考えるとわかりやすいです。
マージプロセスの内部動作
作業ディレクトリ・インデックス・ツリー
マージは、裏側で以下の手順を踏みます。
- Gitは共通祖先と各ブランチ間の差分を取得します。
- その差分をもとに新しいスナップショット(ツリー)を統合的に生成します。
- 統合結果を元にマージコミットが作られます。
この際、Gitは「インデックス(ステージングエリア)」という中間領域にファイル状況を反映し、最終的にgit commit
でマージコミットを記録します。
「インデックス」や「ツリー」という用語は初めて聞くと戸惑うかもしれません。インデックスは、Gitがコミットを作るために一時的に利用する「作業台」のようなものです。ツリーは「ディレクトリ構造を表すGit内部のデータ」です。
「インデックスはコミット前の作品展示台、ツリーは完成したコミットの作品目録」とイメージすると、少し理解が容易になるでしょう。
コンフリクト(衝突)の仕組み
マージ時、両方のブランチが同じ行を別々に変更していた場合、Gitは自動でまとめられず「コンフリクト」として開発者に判断を委ねます。
コンフリクトが発生すると、Gitは該当ファイルに特別なマーカー(<<<<<<<
, =======
, >>>>>>>
)を挿入します。これを見て、開発者はどちらの変更を採用するか、あるいは両方を統合するかを決めて、ファイルを修正した上で再コミットします。
# main.py <<<<<<< HEAD print("Hello from Branch A") ======= print("Hello from Branch B") >>>>>>> branch-b
各マーカーの意味
<<<<<<< HEAD
: 現在のブランチ(マージ先、通常はHEAD)の内容=======
: 変更箇所の境界線>>>>>>> branch-b
: マージ元ブランチ(この例ではbranch-b)の内容
ブランチAの内容を採用する場合
# main.py print("Hello from Branch A")
ブランチBの内容を採用する場合
# main.py print("Hello from Branch B")
両方の内容を統合する場合
# main.py print("Hello from Branch A") print("Hello from Branch B")
「コンフリクトになったらもうお手上げ」と思うかもしれません。しかし、実際には「ここが食い違っていますよ」というGitからの知らせです。あなたはその箇所を人間の目で判断し、最適な解決策(両方を合体する、片方のみ採用するなど)を見つければよいのです。
「コンフリクトは、Gitが『この部分はどうしたらいい?』と質問している状態」と考えると、恐れる必要はありません。
マージコミットと履歴構造
マージが成功すると、「マージコミット」という、親コミットを2つ以上持つ特別なコミットができます。これにより、DAG上で分岐していた履歴が再度一つに合流します。後から履歴をたどると、「ここで2つの開発ラインが1つになったのだな」と一目でわかるようになります。
なぜわざわざ「2つの親を持つコミット」を作るのでしょうか。
これは、開発の歴史を正直に残しているからです。分岐した履歴をそのまま残せば、「なぜこの変更が加わったのか」や「どの機能をどこで統合したのか」を後から正しく振り返ることができます。
リベースとの比較
マージとは別に、Gitには「リベース(rebase)」という操作もあります。
- マージ:過去の分岐を正直に残し、合流点を明確にします。
- リベース:分岐した歴史をなかったことにし、あたかも一直線の履歴が続いていたかのように書き換えます。
特徴 | マージ (Merge) | リベース (Rebase) |
---|---|---|
履歴の構造 | 分岐と統合が残る | 履歴が一本化される |
操作後のコミット数 | 元のコミット + マージコミット | 新しいコミットが作り直される |
利点 | 分岐の履歴が明確で、変更の流れを追いやすい | 履歴が簡潔で見やすい |
注意点 | コンフリクトが多いと、履歴が複雑化する可能性 | 履歴を改変するため、チーム全体での使用に注意が必要 |
マージでは、2つのブランチ(main
とfeature
)が分岐し、その変更を統合するために「マージコミット」が作成されます。履歴には分岐と統合がそのまま記録されます。
A---B---C (main) \ \ D---E---F (feature) / G-------H (merge commit)
A
, B
, C
はmain
ブランチのコミット。D
, E
, F
はfeature
ブランチのコミット。H
は「マージコミット」で、main
とfeature
を統合した新しいコミットです。
リベースでは、feature
ブランチの履歴を、main
ブランチの最新コミットの上に「移動」させます。この操作により、分岐がなかったかのような直線的な履歴が作られます。
A---B---C (main) \ D'---E'---F' (feature after rebase)
D'
, E'
, F'
は、リベースによって新しく作られたコミットで、feature
ブランチの変更内容を反映しています。履歴が一本化されるため、分岐の痕跡は残りません。
リベースは少し上級者向けです。混乱する場合は、まずはマージになれてからで十分です。