機能開発ブランチ(feature)を本番用のメインブランチ(main)にマージした直後、重大な不具合が発覚し、即座にマージ前の状態に差し戻さなければならない緊急事態が発生することがあります。
通常のコミットであれば単に git revert <コミットハッシュ> を実行すれば打ち消しコミットを作成できますが、マージコミットを単純に revert しようとするとエラー(fatal: commit <hash> is a merge but no -m option was given.)が発生します。
本記事では、マージコミットを安全に取り消すための git revert -m オプションの仕様、および「一度 revert した機能を後から再適用する」際の落とし穴について解説します。
1. なぜマージコミットの revert には -m が必要なのか?
通常のコミットは「親コミット(直前の状態)」を1つしか持ちません。そのため、そのコミットで行われた差分を引き算(打ち消し)することは容易です。
一方、マージコミットは親コミットを2つ以上持っています(分岐元のメインブランチと、合流したフィーチャーブランチ)。 Gitはどちらの親コミット(歴史の経路)を基準にして打ち消し処理を行えばよいのか判断できないため、エラーを出します。
そこで、-m(--mainline)オプションを使用して、「どちらの親をメインライン(本流)とするか」を数値で明示する必要があります。
2. git revert -m の具体的な操作手順
ステップ1: マージコミットの親の番号を確認する
対象のマージコミットの情報を表示します。
git show <マージコミットハッシュ>
出力結果の上部に以下のような行があります。
commit a1b2c3d4e5f6...
Merge: 9e8d7c6 5a4c3d2
この Merge: に並んでいるコミットハッシュが親です。
- 左側(
9e8d7c6): 親1(通常はマージを「受け入れた」側のブランチ、例:main) - 右側(
5a4c3d2): 親2(マージ「された」側のブランチ、例:feature)
ステップ2: 親1(メインライン)を指定して revert を実行する
通常は、マージ受け入れ側である main ブランチの歴史を維持したいため、親「1」を指定して revert を実行します。
git revert -m 1 a1b2c3d4e5f6
実行するとエディタが開き、revertコミットメッセージの保存を求められます。保存して閉じれば、安全にマージ内容が打ち消され、メインブランチが元の状態に戻ります。
3. 最大の罠:revertしたブランチの再マージ(Re-revert)
一度マージコミットを revert で打ち消したあと、フィーチャーブランチ側でバグを修正し、もう一度同じブランチを main にマージしようとすると、非常に奇妙な現象に遭遇します。
**「バグ修正した部分のコードしかマージされず、以前実装した(バグのない)大半のコードが main に反映されない」**という問題です。
なぜ起こるのか?
Gitにとって、フィーチャーブランチ内のオリジナルのコミット群は「すでに一度 main にマージされたコミット」と認識されたままになっています。その後の revert は「あくまで main 上に新しく追加された打ち消しコード」という位置づけです。そのため、もう一度単純にマージしても、Gitは「オリジナルのコミットは適用済み」と判断して差分を取り込みません。
解決策(Re-revertの実行):
一度打ち消した機能ブランチを再び適用したい場合は、フィーチャーブランチを直接マージするのではなく、**「かつて行った revert コミット自体を、さらに revert(打ち消しの打ち消し)する」**操作を行います。
# かつて行った revert コミットを打ち消す
git revert <revertコミットのハッシュ>
これにより、以前マージして打ち消されたコード全体が再び main 上に息を吹き返します。
まとめ
マージコミットの差し戻しは、焦って行うとリポジトリの歴史を混乱させる原因になります。
- マージコミットの revert には
-m 1オプションを付与する -m 1は「マージを受け入れた側のブランチの歴史を基準にする」という意味- 差し戻したブランチを再度適用する際は、ブランチのマージではなく「revert コミットの revert」を行う
不測の事態に備えて、この手順をしっかり整理しておきましょう。
