PR

Gitのマージ(merge)を「本質」から理解する3分間

IT-Skills

Gitは分散型のバージョン管理システムで、複数のブランチ(参考 ブランチの基本)で同時に開発が進むことを前提に設計されています。このような環境で、並行して進んでいた変更を1つにまとめる行為が「マージ(merge)」です。単にファイルを結合するだけでなく、履歴(コミットの流れ)を整合的に再結合し、将来的な履歴解析や管理を容易にする点がマージの特徴です。

本記事では、Gitマージの基本的な仕組みから内部動作、そして初心者が混乱しやすいポイントについて、順を追って解説します。

また、難しい話の後には、「こう考えるとシンプルに捉えられる」というコツも付け加えます。これにより、単純な操作手順以上に、なぜGitがこのような動きをするのか理解でき、トラブルシューティングやブランチ戦略の検討に役立つでしょう。

スポンサーリンク

Gitのデータモデル:コミットとスナップショット

Gitは、各コミットを「プロジェクト全体のスナップショット」として扱います。

  • コミットは、ファイル群の状態を示すスナップショットと、そのスナップショットに至るまでの履歴(親コミットへの参照)を持っています。
  • Gitは履歴を「DAG(有向非巡回グラフ)」という形で管理し、ブランチはそのDAG上の特定のコミットを指す「名前(ポインタ)」です。

「スナップショット」という考え方は、「差分管理」とは少し異なります。Gitは内部的には差分の圧縮を行いますが、基本的なモデルは「各コミットで全体を丸ごと記録している」ような感覚で構いません。

DAG(有向非巡回グラフ)とは?

DAGは、依存関係を表す際によく使われます。次のようなタスクの順序を考えてみます。

  1. 材料を買う
  2. ケーキの生地を作る
  3. オーブンを予熱する
  4. ケーキを焼く
  5. ケーキを冷ます
  6. デコレーションする

このタスクは「一方向の矢印で繋がる依存関係」として描けます(戻るループはありません)。

材料を買う → 生地を作る → ケーキを焼く → 冷ます → デコレーションする
                 ↑
            オーブンを予熱する

「材料を買う」が最初のタスク。そして「オーブンを予熱する」は「ケーキを焼く」の直前に必要なタスクです。冷ました後でしかデコレーションはできません。

このように、「依存関係を持つけれど戻らない」構造がDAGの特徴です。

ブランチとDAG構造

ブランチは複数の機能開発を並行して行うために作成されます。

  • ブランチAで新機能1を開発
  • ブランチBで新機能2を開発
    これらは、DAG上でコミットの流れが2つに分かれている状態です。

ここで注意して覚えておきたい点。それはブランチは「ディレクトリ」でも「コピー」ではないということ。単に「あるコミットを指す名札」だと思ってください。ブランチが増えても、実際にファイルの重複が増えるわけではありません。詳しくは参考ブランチとは?をご覧いただければと思いますが、ブランチとは、履歴の分岐点に置かれた旗印(フラグ)のようなもの」だというのが、マージを本質的に理解するうえでの重要ポイントです。

マージの核心:3-wayマージと共通祖先

Gitのマージは「3-wayマージ」という仕組みを用いています。

3-wayマージとは

マージ対象となる2つのブランチには、それらが分岐する前の「共通祖先」が必ず存在します。3-wayマージは以下の3点を比較します。

  1. 共通祖先コミット
  2. マージ元ブランチの先頭コミット
  3. マージ先ブランチの先頭コミット

共通祖先と各ブランチ先頭の差分を計算し、その差分を統合することで、両方の変更を取り込んだ新たなコミットを生成します。

「共通祖先」を探すのは、Gitが自動でやってくれるため、普段は意識しなくてもマージは可能です。ただ、なぜ共通祖先が必要なのかがわからないと、「どこが起点で、何を比べているのか」が見えにくくなります。

具体例

  • 共通祖先(A)は、まだ新機能が加えられる前のプロジェクト状態とします。
  • ブランチAの先頭(A')では、例えば「ファイルXに行追加」を行ったとします。
  • ブランチBの先頭(B')では、「ファイルYを修正」しました。
    このとき、A~A'がブランチAの進化、A~B'がブランチBの進化です。
    マージでは、この「A→A'」の変更と「A→B'」の変更を足し合わせるイメージです。

「共通祖先は『分かれ道の分岐点』、そこからそれぞれがどう違う方向に進んだのかを見て、最後にそれらを足し算する」と考えるとわかりやすいです。

マージプロセスの内部動作

作業ディレクトリ・インデックス・ツリー

マージは、裏側で以下の手順を踏みます。

  1. Gitは共通祖先と各ブランチ間の差分を取得します。
  2. その差分をもとに新しいスナップショット(ツリー)を統合的に生成します。
  3. 統合結果を元にマージコミットが作られます。

この際、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つのブランチ(mainfeature)が分岐し、その変更を統合するために「マージコミット」が作成されます。履歴には分岐と統合がそのまま記録されます。

      A---B---C   (main)
       \       \
        D---E---F (feature)
               /
      G-------H   (merge commit)

A, B, Cmainブランチのコミット。D, E, Ffeatureブランチのコミット。Hは「マージコミット」で、mainfeatureを統合した新しいコミットです。

リベースでは、featureブランチの履歴を、mainブランチの最新コミットの上に「移動」させます。この操作により、分岐がなかったかのような直線的な履歴が作られます。

      A---B---C   (main)
                \
                 D'---E'---F' (feature after rebase)

D', E', F'は、リベースによって新しく作られたコミットで、featureブランチの変更内容を反映しています。履歴が一本化されるため、分岐の痕跡は残りません。

リベースは少し上級者向けです。混乱する場合は、まずはマージになれてからで十分です。

このWebサイトは現役のエンジニアが以下3点を目的として運営しています。

  1. 勉強:一度理解した内容を忘れないように。
    → アウトプットは「最強のインプット」である! 
  2. 備忘:忘れたとしても後から見返せるように。
    → 未来の自分への「お手紙」を書いています。 
  3. 共有:〇〇ってこうだったんだ!の感動をシェアできるように。
    → あなたの知識は誰かにとっての「価値ある情報」です。 

副業ブログの始め方はこちらから

スポンサーリンク
IT-Skills
シェアする
ビズドットオンラインをフォローする
タイトルとURLをコピーしました