🔖

Gitマージコンフリクト解決とrebaseの安全な使い方

に公開

はじめに

チーム開発におけるバージョン管理システムとしてGitはデファクトスタンダードである。その運用において、複数の開発者が並行して作業を進める上でgit mergegit rebaseは不可欠な操作であるが、同時にマージコンフリクトという厄介な問題を引き起こすことがある。

コンフリクトの解決を誤ると、意図しないコードの欠損や、過去の変更が上書きされるといった重大なバグに繋がりかねない。特に、コミット履歴を綺麗に保つために利用されるgit rebaseは、その強力さゆえに誤った使い方をするとリポジトリの整合性を破壊する危険性もはらんでいる。

本記事は、チーム開発で発生する典型的なマージコンフリクトの解決手順と、履歴を安全に書き換えるためのgit rebaseの適切な運用方法について、将来の自分自身への備忘録としてまとめたものである。

対象環境

  • マシン: MacBook Pro (14-inch, M1 Pro)
  • OS: macOS Sequoia 15.6
  • シェル: Zsh (macOS標準)
  • Git: 2.45.1 (Homebrew経由でインストール)
  • エディタ: Visual Studio Code 1.90.0

マージコンフリクトの原因

マージコンフリクトは、Gitが変更を自動で統合できない場合に発生する。具体的には、2つの異なるブランチで、同じファイルの同じ行がそれぞれ別の内容に編集され、それらのブランチをマージしようとした際に起こる。

例えば、mainブランチから派生したfeature-Afeature-Bという2つのブランチが存在すると仮定する。

main.js
function greet() {
  console.log("Hello, World!");
}

feature-Aで以下のように変更された。

main.js
function greet() {
  console.log("Hello, Feature A!");
}

一方、feature-Bでは以下のように変更された。

main.js
function greet() {
  console.log("Hello, Feature B!");
}

この状態でfeature-Amainにマージした後、feature-Bmainにマージしようとすると、main.jsの2行目についてどちらの変更を採用すべきかGitは判断できず、コンフリクトが発生する。

解決策

コンフリクトの解決には、手動での修正とGitへの通知が必要である。ここでは、基本的なmerge時のコンフリクト解決と、より高度なrebaseを用いた方法を解説する。

1. 基本的なgit mergeにおけるコンフリクト解決

最も一般的なコンフリクト解決のシナリオである。

手順1: コンフリクトの発生と確認

mainブランチにfeature-Bをマージしようとすると、以下のようなメッセージが表示される。

zsh
$ git switch main
$ git merge feature-B
Auto-merging main.js
CONFLICT (content): Merge conflict in main.js
Automatic merge failed; fix conflicts and then commit the result.

git statusコマンドで、どのファイルがコンフリクトしているかを確認できる。

zsh
$ git status
On branch main
Your branch is ahead of 'origin/main' by 2 commits.
  (use "git push" to publish your local commits)

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:   main.js

no changes added to commit (use "git add" and/or "git commit -a")

手順2: コンフリクトの解消

コンフリクトが発生したファイルをエディタで開くと、以下のようなコンフリクトマーカーが挿入されている。

main.js
function greet() {
<<<<<<< HEAD
  console.log("Hello, Feature A!");
=======
  console.log("Hello, Feature B!");
>>>>>>> feature-B
}
  • <<<<<<< HEAD: 現在のブランチ (main) での変更内容を示す。
  • =======: 変更内容の区切り。
  • >>>>>>> feature-B: マージしようとしているブランチ (feature-B) での変更内容を示す。

これらのマーカーを参考に、手動でファイルを正しい状態に編集する。例えば、両方の挨拶を残すことにした場合は以下のようになる。

main.js
function greet() {
  console.log("Hello, Feature A!");
  console.log("Hello, Feature B!");
}

VS Codeなどのエディタには、コンフリクトをGUIで解決する機能("Accept Current Change", "Accept Incoming Change", "Accept Both Changes"など)も備わっており、これらを利用するとより効率的に作業できる。

手順3: 解決の通知とコミット

ファイルの編集が完了したら、git addコマンドでコンフリクトが解決したことをGitに通知する。

zsh
$ git add main.js

最後に、git commitコマンドでマージを完了させる。コミットメッセージはGitが自動で生成してくれる。

zsh
$ git commit

もし、マージ作業を中断してマージ前の状態に戻したい場合は、git merge --abortコマンドを実行する。

zsh
$ git merge --abort

2. git rebaseによるコンフリクト解決と安全な運用

git rebaseは、ブランチの基点となるコミットを変更し、コミット履歴を直線的に書き換える強力なコマンドである。これにより、マージコミットを作らずにクリーンな履歴を保つことができる。

rebaseの基本的な使い方

featureブランチで作業中に、mainブランチの最新の変更を取り込みたい場合を考える。

zsh
# 最新のリモートブランチ情報を取得
$ git fetch origin

# featureブランチに切り替え
$ git switch my-feature

# origin/mainブランチの最新コミットを基点として、my-featureのコミットを再生する
$ git rebase origin/main

rebase中にコンフリクトが発生した場合、プロセスは一時停止する。解決手順はmergeの場合とほぼ同じである。

  1. コンフリクトしたファイルを修正する。
  2. git add <file>で解決を通知する。
  3. git rebase --continuerebaseプロセスを再開する。

このプロセスを、rebase対象のすべてのコミットでコンフリクトがなくなるまで繰り返す。中断したい場合はgit rebase --abortを使用する。

rebaseの黄金律

3. cherry-pickによる特定のコミットの適用

cherry-pickは、別のブランチにある特定のコミットだけを現在のブランチにコピーするコマンドである。例えば、あるfeatureブランチで行ったバグ修正コミットだけを、緊急でmainブランチに適用したい場合などに有効である。

zsh
# mainブランチに切り替え
$ git switch main

# 適用したいコミットのハッシュ値を指定
$ git cherry-pick <commit-hash>

cherry-pickでもコンフリクトが発生することがある。その場合の解決手順もrebaseと同様で、ファイルを修正しgit addした後、git cherry-pick --continueでプロセスを続行する。中断はgit cherry-pick --abortである。

4. Git Flowにおける運用例

Git Flowのようなブランチ戦略では、rebaseは特にfeatureブランチで効果を発揮する。

featureブランチでの開発が完了し、developブランチにマージする前に、developの最新の変更を取り込むのが一般的である。

zsh
# featureブランチで作業
$ git switch feature/new-login

# developの最新状態を取得
$ git fetch origin

# developの最新コミットを基点にrebaseする
$ git rebase origin/develop

これにより、feature/new-loginブランチはdevelopの最新の変更の上に自身の変更を積み直した形になる。この状態でPull Requestを作成すれば、コンフリクトが発生する可能性を低減でき、レビュワーも差分を確認しやすくなる。

GitHubのPull Requestにおけるマージ方法として "Rebase and merge" を選択すると、マージコミットを作らずに履歴を直線的に保つことができ、このワークフローと親和性が高い。

おわりに

マージコンフリクトは、チーム開発において避けられないプロセスの一部である。しかし、その発生メカニズムと正しい解決手順を理解していれば、恐れるに足らない。

  • git merge: 基本的な統合方法。マージ履歴がコミットとして残る。
  • git rebase: コミット履歴をクリーンに保つ強力なツール。ただし、共有ブランチには絶対に使用しない。
  • git cherry-pick: 特定のコミットだけを取り込む際に有用。

これらのコマンドの特性を理解し、プロジェクトのワークフローやチームのルールに応じて適切に使い分けることが、健全なバージョン管理の鍵となる。

参考資料

Discussion