株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
はじめに
この記事は 「図解でわかるGitの仕組み」シリーズ の第3回です。
Gitの内部構造を、1テーマ1記事で図解していきます。
ブランチを切って開発を進めたあと、その変更を別のブランチに取り込む操作は日常的に発生します。このとき選べる手段が git merge と git rebase の2つです。
どちらも「変更を統合する」という目的は同じですが、履歴の残り方がまったく異なります。この違いを正確に理解することで、チームのブランチ運用に合った戦略を選べるようになります。
1. マージ(merge)の仕組み
1-1. fast-forward マージ
最もシンプルなケースです。main ブランチから feature ブランチを切ったあと、main に新しいコミットが追加されていない場合に発生します。
【マージ前】
A --- B --- C (main)
\
D --- E (feature) ← HEAD
この状態で main に feature をマージすると、main のポインタを E まで前進させるだけで統合が完了します。新しいコミットは作られません。
【fast-forward マージ後】
A --- B --- C --- D --- E (main, feature)
↑ HEAD
操作手順:
# featureブランチの変更をmainに統合する
git switch main
git merge feature
fast-forward マージが行われた場合、Git は以下のようなメッセージを出力します。
Updating C..E
Fast-forward
file.txt | 2 ++
1 file changed, 2 insertions(+)
ポイント: fast-forward マージは「ポインタの移動」にすぎず、マージコミットは生成されません。履歴は完全に一直線になります。
1-2. 3-way マージ(ノンファストフォワードマージ)
main にも feature にも、分岐後にそれぞれコミットが追加されている場合、fast-forward はできません。このとき Git は 3-way マージ を行います。
「3-way」とは、以下の 3つのスナップショット を比較してマージ結果を決定する手法です。
- 共通祖先(merge base) ― 2つのブランチが分岐した地点のコミット
-
main の先端 ― 現在の
mainの最新コミット - feature の先端 ― 統合したいブランチの最新コミット
【マージ前】
F --- G (main) ← HEAD
/
A --- B --- C
\
D --- E (feature)
※ コミットCが「共通祖先」(両ブランチの分岐点)
【3-way マージ後】
F --- G --- M (main) ← HEAD
/ /
A --- B --- C /
\ /
D --- E -- (feature)
※ Mが「マージコミット」(親が2つ: GとE)
操作手順:
git switch main
git merge feature
3-way マージが行われた場合、Git はマージコミットのメッセージ入力を求めます(デフォルトは Merge branch 'feature')。
Merge made by the 'ort' strategy.
file.txt | 2 ++
1 file changed, 2 insertions(+)
1-3. マージコミットとは何か
マージコミットは 親コミットを2つ持つ 特殊なコミットです。通常のコミットが1つの親しか持たないのに対し、マージコミットは「統合した2つのブランチの先端」をそれぞれ親として記録します。
# マージコミットの親を確認する
git cat-file -p HEAD
tree 4b825dc...
parent aabbcc1... ← mainブランチ側の親(コミットG)
parent ddeeff2... ← featureブランチ側の親(コミットE)
author ...
committer ...
Merge branch 'feature'
ポイント: マージコミットがあることで、「いつ、どのブランチが統合されたか」という情報が履歴に残ります。git log --graph で可視化すると、分岐と合流の構造が確認できます。
1-4. --no-ff オプション
fast-forward が可能な状況でも、あえてマージコミットを作成したい場合があります。--no-ff(no fast-forward)オプションを使います。
git switch main
git merge --no-ff feature
【--no-ff マージ後】
A --- B --- C --- M (main) ← HEAD
\ /
D --- E (feature)
※ fast-forward可能だが、マージコミットMを強制生成
--no-ff を使う理由:
- feature ブランチの開始と終了が履歴に明示されるため、「どの作業がひとまとまりか」を後から追跡しやすい
- Git Flow などのブランチ戦略では
--no-ffがルールとして推奨されることが多い
2. リベース(rebase)の仕組み
2-1. リベースとは
リベースは、あるブランチのコミット列を 別のブランチの先端に付け替える 操作です。マージが「合流」なら、リベースは「移植」に近いイメージです。
【リベース前】
F --- G (main)
/
A --- B --- C
\
D --- E (feature) ← HEAD
feature ブランチ上で git rebase main を実行すると、以下のことが起きます。
-
featureがmainから分岐した地点(コミットC)以降のコミット(D, E)を一時的に退避する -
featureの起点をmainの先端(コミットG)に移動する - 退避したコミットを 1つずつ順番に 新しい起点の上に再適用する
【リベース後】
A --- B --- C --- F --- G (main)
\
D' --- E' (feature) ← HEAD
※ D', E' はD, Eと同じ変更内容だが、異なるコミットハッシュを持つ
操作手順:
# featureブランチ上で実行する
git switch feature
git rebase main
Successfully rebased and updated refs/heads/feature.
重要: リベースで生成される D', E' は、元の D, E と変更内容(diff)は同じですが、コミットハッシュは異なります。これは「親コミットが変わる」ため、ハッシュの計算結果も変わるからです。つまり、D' と E' は Git の内部的には D, E とは別のコミットです。
2-2. リベース後のマージ
リベースで feature のコミット列を main の先端に付け替えたあと、main で feature をマージすると fast-forward になります。
git switch main
git merge feature
【リベース後にマージ】
A --- B --- C --- F --- G --- D' --- E' (main, feature)
↑ HEAD
結果として、履歴が一直線になります。これがリベースの最大の特徴です。
2-3. リベース中のコンフリクト
リベースはコミットを1つずつ再適用するため、各コミットの適用時にコンフリクトが発生する可能性があります。
# コンフリクトが発生した場合
git rebase main
# ... コンフリクト発生 ...
# 1. コンフリクトを手動で解消する
# 2. 解消したファイルをステージに追加する
git add <解消したファイル>
# 3. リベースを続行する
git rebase --continue
# リベースを中止して元の状態に戻したい場合
git rebase --abort
ポイント: マージではコンフリクトの解消は1回で済みますが、リベースではコミットの数だけコンフリクトが発生する可能性があります。コミット数が多い場合、リベースの方が手間がかかることがあります。
3. マージとリベースの比較
3-1. 履歴の形
| 項目 | マージ(3-way) | リベース + fast-forward |
|---|---|---|
| 履歴の形 | 分岐・合流が残る | 一直線になる |
| マージコミット | 生成される | 生成されない |
| 元のコミットハッシュ | 保持される | 変わる(新しいハッシュになる) |
| 分岐の経緯 | 「いつ分岐し、いつ統合したか」がわかる | 分岐の痕跡は残らない |
3-2. 使い分けの指針
マージが適しているケース:
- チームで共有しているブランチ(
main,developなど)への統合 - 「いつ統合したか」の記録を残したい場合
- 複数人が関わるプルリクエストのマージ
- 安全性を優先したい場合
リベースが適しているケース:
-
自分だけが使っている ローカルの feature ブランチを最新の
mainに追従させたい場合 - プルリクエスト作成前にコミット履歴を整理したい場合
- 一直線のきれいな履歴を保ちたい場合
【よくあるワークフロー】
1. feature ブランチで作業する
2. main に新しいコミットが追加される
3. feature ブランチ上で git rebase main を実行し、最新のmainに追従する
4. プルリクエストを作成する
5. main に feature を --no-ff でマージする
4. リベースの注意点
4-1. 公開済みブランチをリベースしてはいけない
これはリベースにおける最も重要なルールです。
リベースはコミットハッシュを書き換えます。すでにリモートリポジトリにプッシュして他のメンバーと共有しているコミットをリベースすると、他のメンバーのローカルリポジトリとの整合性が崩れます。
【危険な操作の例】
# 他のメンバーがすでにpullしたブランチをリベースする
git switch shared-feature
git rebase main
git push --force ← 他のメンバーの履歴と矛盾する
絶対ルール: git push 済みのコミットに対してリベースを行ってはいけません。リベースは「まだプッシュしていないローカルのコミット」に対してのみ使用してください。
公式ドキュメントでも以下のように述べられています。
Do not rebase commits that exist outside your repository and that people may have based work on.
(リポジトリの外に存在し、他の人がそのうえに作業を進めている可能性のあるコミットをリベースしてはならない)
4-2. --force-with-lease を使う場面
自分だけが使っている feature ブランチをリベース後にプッシュする場合、通常の git push は拒否されます(リモートの履歴と食い違うため)。この場合、--force-with-lease を使います。
git push --force-with-lease origin feature
--force-with-lease は --force と異なり、リモートブランチが自分の想定と一致している場合にのみ強制プッシュを許可します。他の誰かがそのブランチにプッシュしていた場合は拒否されるため、--force より安全です。
5. 操作まとめ
| 操作 | コマンド | 説明 |
|---|---|---|
| マージ(通常) | git merge <branch> |
可能なら fast-forward、不可なら 3-way マージ |
| マージ(ff禁止) | git merge --no-ff <branch> |
必ずマージコミットを作成 |
| マージ(ff限定) | git merge --ff-only <branch> |
fast-forward できない場合はエラーにする |
| リベース | git rebase <branch> |
現在のブランチのコミットを指定ブランチの先端に付け替え |
| リベース中止 | git rebase --abort |
リベースを中止して元の状態に戻る |
| リベース続行 | git rebase --continue |
コンフリクト解消後にリベースを続行 |
| リベース後プッシュ | git push --force-with-lease |
リモートと整合性を確認しつつ強制プッシュ |
まとめ
- マージ は2つのブランチを「合流」させる操作。3-way マージではマージコミット(親を2つ持つコミット)が生成され、分岐と合流の履歴が残る
- fast-forward マージ は分岐がない場合にポインタを前進させるだけの操作。マージコミットは生成されない
- リベース はコミット列を別のブランチの先端に「付け替える」操作。履歴が一直線になるが、コミットハッシュが変わる
- 公開済みのコミットをリベースしてはいけない ― これがリベースの最重要ルール
- マージとリベースは排他的ではなく、ワークフローの中で組み合わせて使うのが一般的
次回予告
第4回「リモートとローカル ― push/pull/fetchの裏側」 では、origin とは何か、push / pull / fetch がそれぞれどのような通信を行っているのかを図解します。
シリーズ目次
| # | テーマ |
|---|---|
| 1 | Gitの全体像 ― ワークツリー・ステージ・リポジトリの3層構造 |
| 2 | ブランチとHEAD ― 「枝分かれ」の正体を理解する |
| 3 | マージとリベース ― 統合戦略の違いを図で比較(この記事) |
| 4 | リモートとローカル ― push/pull/fetchの裏側 |
| 5 | コンフリクト解消 ― なぜ起きるのか、どう直すのか |
| 6 | reset/revert/checkout ― 「戻す」操作を正しく使い分ける |
| 7 | stash/cherry-pick/rebase -i ― 実務で頼れる便利コマンド |
| 8 | .gitignore・フック・設定 ― Gitをカスタマイズする |
| 9 | Git Flow vs GitHub Flow ― ブランチ戦略の選び方 |
| 10 | トラブルシューティング ― 「やらかした!」を救うコマンド集 |
参考
- Git公式ドキュメント - git-merge
- Git公式ドキュメント - git-rebase
- Git公式ブック - 3.2 Git Branching - Basic Branching and Merging
- Git公式ブック - 3.6 Git Branching - Rebasing
「この記事が役に立った」と思ったら、LGTM とストックをお願いします。
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!






