0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【図解でわかるGit 第5回】コンフリクト解消 ― なぜ起きるのか、どう直すのか

0
Posted at

株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。

Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト


はじめに

この記事は 「図解でわかるGitの仕組み」シリーズ の第5回です。
Gitの内部構造を、1テーマ1記事で図解していきます。

チーム開発をしていると、いつか必ず遭遇するのが**コンフリクト(競合)**です。

「怖い」「よくわからないから触りたくない」という声を聞きますが、仕組みを理解すれば怖くありません。この記事では、コンフリクトがなぜ起きるのか、そしてどう直すのかを図解付きで解説します。

対象読者

  • git merge や git rebase の基本は知っている
  • コンフリクトに遭遇したことがあるが、解消手順に自信がない
  • チーム開発でコンフリクトを減らしたい

1. コンフリクトとは何か

コンフリクト(conflict) とは、Git が自動マージできない状態のことです。

Git は非常に賢く、多くの変更を自動でマージできます。しかし、同じファイルの同じ箇所を複数のブランチで別々に変更した場合、どちらを採用すべきか判断できないため、人間に判断を委ねます。これがコンフリクトです。


2. コンフリクトが発生する条件

発生する場合

同じファイルの同じ行(同じ領域)を、2つのブランチで別々に変更した場合にコンフリクトが発生します。

【図解】コンフリクトが発生するケース

  共通の祖先(base)           main ブランチ           feature ブランチ
  ┌──────────────┐       ┌──────────────┐       ┌──────────────┐
  │ 1: aaa       │       │ 1: aaa       │       │ 1: aaa       │
  │ 2: bbb       │  ──▶  │ 2: XXX  ← 変更 │       │ 2: YYY  ← 変更 │
  │ 3: ccc       │       │ 3: ccc       │       │ 3: ccc       │
  └──────────────┘       └──────────────┘       └──────────────┘
                                  │                       │
                                  └───── merge ──────────┘
                                            │
                                            ▼
                                     コンフリクト発生!
                                  「2行目は XXX? YYY?」

2行目を main では XXX に、feature では YYY に変更しています。Git はどちらを採用すべきか判断できないため、コンフリクトになります。

発生しない場合

同じファイルでも、変更箇所が離れていればコンフリクトは発生しません。

【図解】コンフリクトが発生しないケース

  共通の祖先(base)           main ブランチ           feature ブランチ
  ┌──────────────┐       ┌──────────────┐       ┌──────────────┐
  │ 1: aaa       │       │ 1: XXX  ← 変更 │       │ 1: aaa       │
  │ 2: bbb       │  ──▶  │ 2: bbb       │       │ 2: bbb       │
  │ 3: ccc       │       │ 3: ccc       │       │ 3: YYY  ← 変更 │
  └──────────────┘       └──────────────┘       └──────────────┘
                                  │                       │
                                  └───── merge ──────────┘
                                            │
                                            ▼
                                     自動マージ成功!
                                  ┌──────────────┐
                                  │ 1: XXX       │
                                  │ 2: bbb       │
                                  │ 3: YYY       │
                                  └──────────────┘

1行目と3行目はそれぞれ別のブランチでしか変更されていないため、Git は自動でマージできます。

その他のコンフリクトパターン

コンフリクトは「同じ行の変更」だけではありません。

パターン 説明
同じ行の変更 最も一般的。上記の図解の通り
片方が削除、片方が変更 ファイルや行を一方が削除し、他方が変更した場合
両方が新規ファイルを作成 同名のファイルを両方のブランチで異なる内容で作成した場合
ファイル名変更の競合 同じファイルを両方が別の名前にリネームした場合

3. コンフリクトマーカーの読み方

コンフリクトが発生すると、Git は該当ファイルにコンフリクトマーカーを挿入します。

基本形式(merge スタイル)

正常な行(コンフリクトなし)
<<<<<<< HEAD
現在のブランチ(HEAD)の内容
=======
マージしようとしているブランチの内容
>>>>>>> feature/login
正常な行(コンフリクトなし)

各マーカーの意味は以下の通りです。

マーカー 意味
<<<<<<< HEAD ここから「現在のブランチの内容」が始まる
======= 現在のブランチとマージ元ブランチの区切り
>>>>>>> feature/login ここまでが「マージしようとしているブランチの内容」

具体例

main ブランチと feature/login ブランチでログインメッセージを別々に変更した場合を考えます。

public class LoginService {
    public String getWelcomeMessage() {
<<<<<<< HEAD
        return "ようこそ!ログインしました。";
=======
        return "Welcome! You have logged in.";
>>>>>>> feature/login
    }
}
  • HEAD(= main)側は日本語メッセージに変更
  • feature/login 側は英語メッセージに変更

diff3 スタイル(推奨)

merge.conflictStyle を diff3 に設定すると、共通の祖先(base)の内容も表示されます。

git config --global merge.conflictStyle diff3
public class LoginService {
    public String getWelcomeMessage() {
<<<<<<< HEAD
        return "ようこそ!ログインしました。";
||||||| base
        return "Hello!";
=======
        return "Welcome! You have logged in.";
>>>>>>> feature/login
    }
}

||||||| base セクションに元の内容が表示されるため、「何がどう変わったのか」を把握しやすくなります。チーム開発では diff3 スタイルの設定を推奨します。


4. 手動でのコンフリクト解消手順

ステップ 1: コンフリクトの発生を確認する

$ git merge feature/login
Auto-merging src/LoginService.java
CONFLICT (content): Merge conflict in src/LoginService.java
Automatic merge failed; fix conflicts and then commit the result.

git status でもコンフリクト中のファイルを確認できます。

$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   src/LoginService.java

ステップ 2: コンフリクト箇所を確認する

エディタでファイルを開き、コンフリクトマーカーを探します。

public class LoginService {
    public String getWelcomeMessage() {
<<<<<<< HEAD
        return "ようこそ!ログインしました。";
=======
        return "Welcome! You have logged in.";
>>>>>>> feature/login
    }
}

ステップ 3: 正しい内容に修正する

コンフリクトの解消方法は4パターンあります。

パターン A: HEAD 側(現在のブランチ)を採用する

public class LoginService {
    public String getWelcomeMessage() {
        return "ようこそ!ログインしました。";
    }
}

パターン B: マージ元ブランチを採用する

public class LoginService {
    public String getWelcomeMessage() {
        return "Welcome! You have logged in.";
    }
}

パターン C: 両方を組み合わせる

public class LoginService {
    public String getWelcomeMessage(Locale locale) {
        if (locale.equals(Locale.JAPANESE)) {
            return "ようこそ!ログインしました。";
        }
        return "Welcome! You have logged in.";
    }
}

パターン D: まったく新しい内容に書き換える

public class LoginService {
    public String getWelcomeMessage() {
        return "ログイン完了 / Login successful";
    }
}

いずれの場合も、コンフリクトマーカー(<<<<<<<, =======, >>>>>>>)をすべて削除してください。マーカーが残っているとコンパイルエラーや実行時エラーの原因になります。

ステップ 4: 解消したファイルをステージに追加する

git add src/LoginService.java

git add することで、Git に「このファイルのコンフリクトは解消済み」と伝えます。

ステップ 5: マージコミットを作成する

git commit

コミットメッセージのエディタが開きます。デフォルトのマージコミットメッセージが入力済みなので、そのまま保存して閉じても問題ありません。必要に応じてコンフリクト解消の意図を追記します。

# コミットメッセージを直接指定する場合
git commit -m "Merge feature/login: コンフリクトを解消(日本語メッセージを採用)"

git merge --continue も同じ動作をします(Git 2.12 以降)。

【図解】コンフリクト解消の全体フロー

  git merge feature/login
         │
         ▼
  ┌─────────────────┐
  │ コンフリクト発生  │
  │ CONFLICT (content)│
  └────────┬────────┘
           │
     ┌─────┴──────┐
     │            │
     ▼            ▼
 解消する      中止する
     │        git merge --abort
     │            │
     ▼            ▼
 ファイルを     マージ前の
 手動修正      状態に戻る
     │
     ▼
 git add <file>
     │
     ▼
 git commit
     │
     ▼
 マージ完了!

補足: マージを中止したい場合

コンフリクトが複雑で手に負えないときは、マージを中止して元の状態に戻せます。

git merge --abort

これにより、マージ開始前の状態に復元されます。


5. git mergetool でGUIツールを使う

コンフリクトの解消をテキストエディタで行うのが基本ですが、GUIのマージツールを使うと視覚的に解消できます。

基本的な使い方

# コンフリクト発生後に実行
git mergetool

コンフリクトしているファイルが順番に開かれ、GUIツール上で解消作業を行えます。

使用するツールの設定

# デフォルトのマージツールを設定
git config --global merge.tool vimdiff

# 利用可能なツールの一覧を表示
git mergetool --tool-help

代表的なマージツールには以下があります。

ツール 特徴
vimdiff ターミナル内で動作。Vim ユーザー向け
VS Code 統合されたコンフリクト解消UI。ボタンクリックで選択可能
kdiff3 3ウェイマージ対応のGUIツール
meld 直感的なGUI。Linux で人気

VS Code をマージツールとして設定する

VS Code はコンフリクトマーカーを検出すると、Accept Current Change / Accept Incoming Change / Accept Both Changes のボタンを表示します。マージツールとして明示的に設定するには以下のようにします。

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait --merge $REMOTE $LOCAL $BASE $MERGED'
# ※ Git 2.38+ / VS Code 1.69+ で利用可能な --merge オプションにより3-wayマージ比較が可能

バックアップファイル .orig の扱い

git mergetool は解消前のファイルを .orig 拡張子でバックアップします。不要な場合は以下で無効化できます。

git config --global mergetool.keepBackup false

6. マージ時とリベース時のコンフリクトの違い

コンフリクトは git merge だけでなく git rebase でも発生します。しかし、両者には重要な違いがあります。

マージ時のコンフリクト

git switch main
git merge feature/login
【図解】マージ時のコンフリクト

  main:    A ── B ── C ─────────── M(マージコミット)
                      \           /
  feature:             D ── E ── F

  コンフリクト発生タイミング: M を作成する1回だけ
  HEAD(ours)   = main の最新(C)
  MERGE_HEAD(theirs)= feature の最新(F)
  • コンフリクトの解消は1回で済む
  • <<<<<<< HEAD は main ブランチ(現在のブランチ) の内容
  • >>>>>>> feature/login は feature ブランチ(マージ元) の内容

リベース時のコンフリクト

git switch feature/login
git rebase main
【図解】リベース時のコンフリクト

  リベース前:
  main:    A ── B ── C
                \
  feature:       D ── E ── F

  リベース中(D, E, F を1つずつ C の後ろに再適用):
  main:    A ── B ── C ── D' ── E' ── F'
                                ↑
                          ここでコンフリクトが起きうる
                          (D', E', F' のそれぞれで発生する可能性)
  • コンフリクトの解消はコミットごとに発生する可能性がある(D, E, F の各コミットで起きうる)
  • ours と theirs が逆になる(公式ドキュメントに明記されている重要な仕様)
マージ時 リベース時
ours(<<<<<<< HEAD) 現在のブランチ(main) リベース先(main)+ これまでに再適用済みのコミット
theirs マージ元ブランチ(feature) 再適用中の自分のコミット(feature の各コミット)
解消回数 1回 コミット数分(最大)
解消後の操作 git commit git rebase --continue
中止の操作 git merge --abort git rebase --abort

リベース時は ours / theirs が直感と逆になります。--ours や --theirs オプションを使う場合は特に注意してください。

リベースでのコンフリクト解消手順

# 1. コンフリクトが発生して rebase が停止する
$ git rebase main
CONFLICT (content): Merge conflict in src/LoginService.java
error: could not apply d1a2b3c... ログインメッセージを変更
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".

# 2. コンフリクトを手動で解消する(マーカーを削除して正しい内容にする)

# 3. 解消したファイルを add する
$ git add src/LoginService.java

# 4. リベースを続行する
$ git rebase --continue

# ※ 次のコミットでもコンフリクトが発生した場合は 2〜4 を繰り返す

リベースを完全に中止して元の状態に戻す場合は以下を実行します。

git rebase --abort

特定のコミットをスキップして次に進む場合は以下を使います(そのコミットの変更は適用されません)。

git rebase --skip

7. 便利なコンフリクト解消コマンド

ファイル単位で一括採用する

コンフリクトの解消時に、ファイル全体をどちらか一方の内容で採用したい場合があります。

# マージ時: 現在のブランチ(HEAD)の内容を採用
git checkout --ours src/LoginService.java
git add src/LoginService.java

# マージ時: マージ元ブランチの内容を採用
git checkout --theirs src/LoginService.java
git add src/LoginService.java

リベース時は ours / theirs が逆になることを忘れないでください(前章参照)。

コンフリクトしているファイルの各バージョンを確認する

Git はマージ中、ステージングエリア(インデックス)にコンフリクトしているファイルの3つのバージョンを保持しています。

# 共通の祖先(base)の内容
git show :1:src/LoginService.java

# 現在のブランチ(HEAD / ours)の内容
git show :2:src/LoginService.java

# マージ元ブランチ(theirs)の内容
git show :3:src/LoginService.java

8. コンフリクトを防ぐためのベストプラクティス

コンフリクトを完全にゼロにすることはできませんが、発生頻度を減らし、解消を容易にする工夫はあります。

(1) こまめに最新の変更を取り込む

# feature ブランチで作業中に、main の最新を取り込む
git switch feature/login
git merge main        # または git rebase main

長期間 main から分岐したまま作業すると、差分が大きくなりコンフリクトのリスクが高まります。少なくとも1日1回は main の変更を取り込む習慣をつけましょう。

(2) ブランチの寿命を短くする

【図解】ブランチの寿命とコンフリクトリスク

  ❌ 悪い例: 長寿命ブランチ(2週間以上)
  main:    A ── B ── C ── D ── E ── F ── G
                \
  feature:       X ── Y ── Z ── W ── V ── U ── T
                 ↑                              ↑
              分岐                          マージ → コンフリクト大量

  ○ 良い例: 短寿命ブランチ(数日以内)
  main:    A ── B ── C ── M1 ── D ── E ── M2
                \        /       \        /
  feature1:      X ── Y          Z ── W
                 ↑    ↑          ↑    ↑
              分岐  マージ     分岐  マージ → コンフリクト最小

(3) ファイルの責務を分割する

1つのファイルに多くの機能を詰め込むと、複数人が同時に編集する確率が上がります。

  • 1ファイル = 1クラス / 1モジュール
  • 関数は小さく保つ
  • 設定ファイルはセクションごとに分割できるなら分割する

(4) コミュニケーションを取る

技術的な工夫だけでなく、チーム内のコミュニケーションも重要です。

  • 「今からこのファイルを大幅に変更します」と事前に共有する
  • コードレビューを早めに行い、マージまでの時間を短縮する
  • 大規模なリファクタリングは他の機能開発と時期をずらす

(5) diff3 スタイルを設定する

前述の通り、共通の祖先の内容が表示されるため、コンフリクト解消の判断がしやすくなります。

git config --global merge.conflictStyle diff3

(6) rerere(Reuse Recorded Resolution)を有効にする

rerere は、過去に解消したコンフリクトのパターンを記録し、同じコンフリクトが再発した際に自動的に解消してくれる機能です。

git config --global rerere.enabled true

リベースを頻繁に行うワークフローで特に有効です。


まとめ

項目 内容
コンフリクトの原因 同じファイルの同じ箇所を複数ブランチで別々に変更した
コンフリクトマーカー <<<<<<<(ours)、=======(区切り)、>>>>>>>(theirs)
解消手順 マーカーを削除 → 正しい内容に修正 → git add → git commit
マージ vs リベース リベースはコミットごとに発生。ours/theirs が逆になる
予防策 こまめな取り込み、短寿命ブランチ、ファイル分割、コミュニケーション

コンフリクトは「間違い」ではなく、複数人が同時に開発している証拠です。仕組みを理解して、落ち着いて対処できるようになりましょう。


次回予告

第6回では 「reset/revert/checkout」 を図解で解説します。「間違えたコミットを取り消したい」「特定のファイルだけ戻したい」―― 似ているようで異なる3つの操作を図解で整理します。


シリーズ目次

# テーマ
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 トラブルシューティング ― 「やらかした!」を救うコマンド集

参考


「この記事が役に立った」と思ったら、LGTM とストックをお願いします。

@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?