Gitコンフリクトを3つの実践テクニックで素早く解消する

チーム開発でGitコンフリクトが発生した際、焦らずに対処できるかがプロジェクトの効率を大きく左右します。本記事では、コンフリクトの発生原因から自動解決・手動解決・予防策まで、実務で今すぐ使える3つのテクニックを段階的に解説します。

Gitコンフリクトが発生する仕組みを理解する

Gitコンフリクトは、異なるブランチで同じファイルの同じ行を異なる方法で編集した場合に発生します。例えば、複数の開発者がfeatureブランチで並行作業を進めた後、mainブランチにマージしようとすると、Gitが「どちらの変更を採用すべきか」判断できず、コンフリクトが生じるという仕組みです。

コンフリクト状態を放置すると、マージが完了できず、プッシュできません。重要なのは「コンフリクトは悪いことではなく、Gitが変更の矛盾を検出してくれた信号」という認識です。

テクニック1:自動マージツールで競合を検出・解決する

コンフリクトの発生を確認するコマンド

まずは、現在の状態がコンフリクト状態かどうかを確認します。

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/rm ..." as appropriate to mark resolution)
	both modified:   src/app.js
	both modified:   config.json

このように「both modified」と表示されたファイルがコンフリクトの対象です。

mergetoolで視覚的に解決する

Gitの組み込みmergetoolを使うと、コンフリクト箇所を視覚的に確認・解決できます。

git mergetool

このコマンドで、設定されたマージツール(VS Code、meld、Kdiff3など)が起動します。左側に現在のブランチの内容、右側にマージ元ブランチの内容が表示され、どちらを採用するか直感的に選択できます。

VS Codeをmergetoolとして使用するには、以下の設定をしておくと便利です:

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

テクニック2:手動解決でコンフリクト箇所を直接編集する

コンフリクトマーカーを読む

mergetoolを使わずに手動で解決する場合、Gitは自動的にコンフリクトマーカーをファイルに埋め込みます。例えば以下のようになります:

function calculateTotal(items) {
<<<<<<< HEAD
  // 現在のブランチ(main)での実装
  return items.reduce((sum, item) => sum + item.price, 0);
=======
  // マージ元ブランチ(feature/discount)での実装
  return items.reduce((sum, item) => sum + (item.price * item.discount), 0);
>>>>>>> feature/discount
}

<<<<<<< HEAD=======の間は現在のブランチの内容、=======>>>>>>> feature/discountの間がマージ元の内容です。

実践的な解決例

実際のビジネスロジックを考慮して、どちらの変更を採用するか判断します。この場合、割引機能を採用する必要があるなら:

function calculateTotal(items) {
  // 割引を適用した合計計算
  return items.reduce((sum, item) => sum + (item.price * item.discount), 0);
}

または、両方の変更を統合する必要がある場合もあります:

function calculateTotal(items, applyDiscount = true) {
  if (applyDiscount) {
    return items.reduce((sum, item) => sum + (item.price * item.discount), 0);
  }
  return items.reduce((sum, item) => sum + item.price, 0);
}

解決後の確認と確定

コンフリクトマーカーをすべて削除し、ファイルを編集したら、以下のコマンドで解決をGitに報告します:

git add src/app.js config.json
git commit -m "Merge feature/discount: 割引計算ロジックを統合"

マージが完了しました。

テクニック3:コンフリクト発生を予防する運用

定期的にmainブランチから最新を取得する

コンフリクトを未然に防ぐ最良の方法は、長く分岐したブランチを避けることです。featureブランチで作業中でも、定期的にmainの最新変更を取り込みます:

git fetch origin
git rebase origin/main
# または
git merge origin/main

git rebaseはコミット履歴を一直線にする方法で、git mergeはマージコミットを作成する方法です。チーム方針に応じて使い分けてください。

小まめなコミット&プッシュで競合範囲を最小化

1日に数回に分けてプッシュすることで、コンフリクト箇所を限定できます。また、1つのブランチで複数の機能を実装せず、1機能1ブランチに徹することも重要です。

コンフリクト解決戦略を設定する

ファイルの種類によって、自動的にどちらの変更を優先するかを指定できます。例えば、.gitattributesファイルで設定します:

*.json merge=union
*.lock merge=ours

merge=unionは両方の変更を統合し、merge=oursは現在のブランチの変更を優先します。

よくあるハマりポイント:rebaseとmergeの違い

git rebaseでコンフリクトが発生した場合、以下のコマンドで段階的に解決します:

# コンフリクトを手動で解決したら
git add .
git rebase --continue

# または中止する場合
git rebase --abort

git mergeの場合と異なり、--continueで各コミットごとにコンフリクト解決を繰り返す可能性があることに注意してください。

実践シナリオ別トラブルシューティング

シナリオ1:複数ファイルのコンフリクトを一括確認したい

git diff --name-only --diff-filter=U

このコマンドで、コンフリクト中のファイル一覧を確認できます。

シナリオ2:特定ファイルだけ別ブランチの内容を完全に採用したい

git checkout --theirs src/app.js  # マージ元ブランチの内容を採用
# または
git checkout --ours src/app.js    # 現在のブランチの内容を保持

シナリオ3:マージを中止したい

git merge --abort

マージ前の状態に完全に戻ります。

テスト環境・動作確認

本記事の内容は以下の環境で動作確認しています:

  • Git 2.40.0 以上(macOS、Linux、Windows WSL2)
  • VS Code 1.85 以上(mergetoolとして)
  • Node.js プロジェクト対象

参考資料

Git公式ドキュメント:ブランチのマージ

よくある質問

A: 未コミット状態なら、以下のコマンドで解決前の状態に戻せます。

A: チーム方針によって異なります。rebaseはコミット履歴を一直線に保つため見やすい反面、公開済みブランチではリスクがあります。mergeはマージコミットが増えますが、変更履歴が明確です。一般的に、個人ブランチではrebaseを、公開ブランチではmergeを推奨します。

A: はい。複雑なビジネスロジックの変更や、ファイル構造の大幅な変更が同時に行われた場合、自動ツールでは判断できず手動解決が必要です。その場合、チーム内で変更意図を共有しながら解決することが重要です。

まとめ

  • コンフリクト発生は正常な現象: Gitが変更の矛盾を検出してくれた信号です。焦らず段階的に対処しましょう。
  • 3つの解決方法を使い分け: 自動mergetool、手動編集、予防的な運用を状況に応じて組み合わせることが効率的です。
  • 予防が最良の対策: 定期的なfetch・rebase、小まめなプッシュ、1機能1ブランチ方針でコンフリクト発生を最小化できます。
  • チーム方針の統一が重要: rebase/merge、コミットメッセージ形式などをドキュメント化し、全員で遵守することでコンフリクト自体を減らせます。
K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →