【Git】mergeコミットを取り消す方法|revert -m 1徹底解説&再マージ問題の解決完全ガイド

【Git】mergeコミットを取り消して履歴を元に戻す方法 Git

mainブランチへのPRがマージされた直後に「あれ、今の変更は不完全だった」と気づく——Gitの現場でよくあるヒヤッとする瞬間です。単にgit reset --hardで巻き戻したいところですが、mergeコミットが既に公開されたリポジトリだと履歴破壊につながり、他メンバーのローカル環境を壊してしまいます。

「取り消したいmergeコミット」の例
*   9f8e7d6 Merge pull request #42 from feature/xxx   ← 取り消したいマージ
|\
| * a1b2c3d feat: 機能A実装
| * d4e5f6a feat: 機能A追加処理
|/
* e7f8g9a main: リリース前の整理

この記事では、mergeコミット専用の取り消し術を徹底解説します。もっとも重要なgit revert -m 1の使い方、-m 1 vs -m 2 の選び方、多くの人がハマる「revert後に再度mergeしても反映されない」罠とその対処、マージの種類別(normal/squash/octopus/fast-forward)の扱い、GitHub UIの「Revert」ボタンの実装、複数マージの連続取り消し、cherry-pickで必要な変更だけを救出する方法まで、現場で即使える形で整理しました。

この記事と他記事の住み分け

  • 進行中merge(未commit)・未push・push済みの3状態全体像 → マージの取り消し方法
  • コミット全般の取り消し(reset/revert/amend) → コミットの取り消し方
  • 本記事はmergeコミット特化の深掘り版。親番号の選択・再マージ問題・GitHub Revert機能・マージ種別まで網羅

この記事で学べること

  • mergeコミットの内部構造(親ポインタ2つ以上)と取り消しの原理
  • git revert -m 1-m 2の選び方
  • 再マージ問題:revert後に同じfeatureを再取り込みする方法
  • マージ4種別の取り消し(real/squash/octopus/fast-forward)
  • GitHub/GitLabの「Revert」ボタンの実装と限界
  • 複数mergeの連続取り消し戦略(時系列 vs 範囲指定)
  • revertせずに必要な変更だけ救出する方法(cherry-pick/restore)
  • 誤マージを防ぐ運用ルール(保護ブランチ・必須レビュー・ff設定)
スポンサーリンク

マージコミットの内部構造を理解する

通常のコミットは親を1つだけ持ちますが、mergeコミットは親を2つ以上持つ特殊な構造です。この親ポインタの扱いが、取り消し時の-mオプションの正体です。

マージコミットの親情報
# 対象マージコミットを確認
git show --no-patch --pretty=raw <マージSHA>
# 出力例:
# tree e4f5g6a7b8c9d0e1f2
# parent a1b2c3d4e5f6g7h8   ← 親1(通常はマージ先=main側)
# parent i9j0k1l2m3n4o5p6   ← 親2(通常は取り込んだ側=feature)
# author Taro &lt;taro@example.com&gt; 1705000000 +0900
# committer Taro &lt;taro@example.com&gt; 1705000000 +0900
# 
#     Merge pull request #42 from feature/xxx

# 親を視覚的に見るには
git log --graph --oneline --parents <マージSHA>~2..<マージSHA>

親ポインタの順序ルール

  • 親1(-m 1)git mergeを実行したブランチ側(マージ先)。通常はmain
  • 親2(-m 2):マージで取り込まれた側(feature)
  • GitHubのPRマージも同様:親1=base branch(main)/親2=head branch(feature)
  • octopus merge(3ブランチ以上同時)なら親3、親4…と続く

ポイント:マージコミットを「なかったことに」するには、どちらの親の状態に戻すかをGitに教える必要があります。これが-m(mainline=基準親)オプション。通常のfeature→mainマージなら、-m 1でmain側基準に戻すのが「マージ前のmainに戻す」操作になります。

-m 1 と -m 2 の選び方

多くの人が間違えるのが親番号の選択。-m 1を使うのがほぼ常に正解ですが、なぜそうなのかを理解しておくと確実です。

オプション 挙動 使用例
-m 1 親1(通常main)を基準として打ち消し
→「マージしなかった状態のmain」に戻る
99%のケース
-m 2 親2(通常feature)を基準として打ち消し
→「feature側にmainをマージしなかった状態」に戻る
feature側でmainを取り込んだマージを取り消す時など(稀)
指定なし エラー
「complex merge, none specified」
マージコミットには必ず-mが必要
視覚的に-m 1/-m 2を理解
# 取り消したいマージ M の状況
#                  ┌─ feature: C1 - C2 - C3
#                  │
# main: A - B - C ─┴─ M (merge)
#                      ↑ ここから取り消し

# -m 1(親1=main側基準)でrevert
# → 結果:main: A - B - C - M - R
#          Rはfeatureの全変更を取り消すコミット
#          つまり「M前のmain(=C)」相当の状態

# -m 2(親2=feature側基準)でrevert
# → 結果:「featureがmainを取り込んでなかった」動作になる
#          実質的にmainの変更を消してしまう(意図と逆)
親の関係を確実に確認
# 親1/親2 それぞれの先端コミットとの差分を確認
# 親1(main側)とマージコミットの差分
git diff <マージSHA>^1 <マージSHA>

# 親2(取り込み側)との差分
git diff <マージSHA>^2 <マージSHA>

# -m 1 で打ち消した時の結果を事前シミュレート(実際にcommitは作らない)
git revert --no-commit -m 1 <マージSHA>
# 内容確認
git diff --cached
# やめるなら
git revert --abort

注意:GitHub UIの「Revert」ボタンは内部的に-m 1相当です。ほぼ常にこれで問題ありませんが、feature側でmainをマージしたコミットを取り消す特殊ケースでは-m 2が正解。親関係をgit show --pretty=rawで確認してから実行すれば迷いません。

基本フロー:git revert -m 1 でmergeを取り消す

push済みマージコミットの取り消し
# STEP 1: 対象マージコミットを特定
git log --merges --oneline -5
# 9f8e7d6 Merge pull request #42 from feature/xxx  ← 取り消したいマージ

# STEP 2: 親関係を確認(推奨)
git show --no-patch --pretty=raw 9f8e7d6

# STEP 3: revert実行
git revert -m 1 9f8e7d6

# STEP 4: conflictが出たら解消
git add .
git revert --continue
# 中止するなら
git revert --abort

# STEP 5: push
git push origin main

revert実行時のエディタ

git revertは自動でエディタを開きコミットメッセージを確認させます。「Revert “Merge pull request #42…”」のようなテンプレートが入るので、取り消し理由を追記(例:「理由:リリース前検証で不具合発覚」)してから保存すると、後の履歴監査で役立ちます。-m <SHA> --no-editで既定メッセージのまま作ることも可能。

最重要:revert後の「再マージ問題」

これがmerge取り消しで多くのチームが詰まる最大の罠です。mergeをrevertしたあと、同じfeatureブランチをもう一度mergeしても反映されません。なぜ起こるか・どう解決するかを押さえておけば慌てずに済みます。

再マージが効かない現象
# タイムライン:
# 1. feature/xxx を main にmerge(M1)
# 2. 問題発見、M1 を revert -m 1(R1)
# 3. featureを修正して再度 mainへmergeしようとする
git merge feature/xxx
# → Already up to date.   ← 何も起きない!

# 理由:Gitは「feature側のcommit群は既にmainに取り込まれ、revertで打ち消された」と認識
# つまり「既に処理済み」扱いで再適用しない

対処法A:revert commit自体をrevertする

revert of revert(Linusも推奨する手法)
# 先にrevertしたコミット R1 を、さらにrevertする
git revert <R1のSHA>
# これで「打ち消しを打ち消す」→ featureの変更が復活

# 命名例: "Revert \"Revert pull request #42\" to re-apply after fix"

# その後 feature側で修正commitを追加して、さらに進める

対処法B:featureに新commitを追加して取り込み直す

feature側に修正を追加してre-merge
# feature/xxxに切り替え
git switch feature/xxx

# 修正commitを追加
git commit -am "fix: レビュー指摘事項を対応"

# 本線に対してrebase(mainの最新取り込み)
git fetch origin
git rebase origin/main

# 改めて main に merge
git switch main
git merge feature/xxx
# 今度は新しいSHAが増えた分だけ取り込まれる

ポイント:Linus Torvalds本人が公式文書で推奨しているのは対処法A(revert of revert)。「一度打ち消した機能を再導入したい」ときは、revertコミットを再度revertするだけで機能が復活します。対処法Bのほうが履歴の意図が分かりやすいケースも多いため、プロジェクトの歴史管理方針に合わせて選んでください。

マージの種類別対応

GitHubやGitLabには複数のマージ方式があり、取り消し手順が変わります。PRをマージした後に対象方式を特定しましょう。

マージ方式 履歴への影響 取り消し方法
通常のmerge(–no-ff) マージコミット生成、親2つ git revert -m 1 <マージSHA>
Squash merge 単一commitに圧縮、親1つ(通常コミット) git revert <SquashSHA>-m不要)
Rebase merge fast-forward、複数commitが直線的に並ぶ 各commitを個別にrevert or 範囲指定revert
Octopus merge 親3つ以上のマージコミット git revert -m 1 <SHA>+追加対処
fast-forward(マージコミットなし) mainが単にfeature先端まで進む 各commitを個別にrevert or reset(未push)

Squash mergeの取り消し

squashは通常commit扱い
# GitHubの "Squash and merge" で作られたコミット
# 履歴上は親1つの通常コミット

# 通常のrevert(-m 不要)
git revert <SquashSHA>
git push

# -m を付けるとエラー
# error: commit <SquashSHA> is not a merge.

Rebase mergeの取り消し

複数commitを範囲で打ち消す
# 最後にマージされた feature のcommit範囲を確認
git log --oneline origin/main~10..origin/main

# 範囲指定でまとめてrevert(逆順に適用される)
git revert --no-commit <最古SHA>..<最新SHA>
# または個別に
git revert <SHA3> <SHA2> <SHA1>   # 新しい順で指定

git commit -m "revert: feature/xxx の変更を一括で取り消し"
git push

Octopus mergeの取り消し

親が3つ以上ある複雑マージ
# 親確認
git show --no-patch --pretty=raw <OctopusSHA>
# parent行が3つ以上

# -m 1 で最初の親(main)基準に打ち消し
git revert -m 1 <OctopusSHA>

# 2親目以降も個別に追加対処が必要な場合がある
# (conflict頻発のため、各親との差分を確認しながら進める)

GitHub/GitLabの「Revert」ボタン

最近のGit系ホスティングにはPR画面からワンクリックでrevertできるボタンが用意されています。CLIより安全で、PRとの紐付けも自動なので通常運用ではこちらがおすすめ。

GitHub のRevert機能

  1. mergeされたPR画面を開く
  2. 「Merged」表示の横にある Revert ボタンをクリック
  3. 自動で「Revert PR #42」という新しいPRが作られる
  4. そのPRをレビューしてマージすれば取り消し完了
  5. conflictがある場合は自動生成に失敗するので、CLIで対応

GitLab/Bitbucket/Azure DevOpsのRevert

  • GitLab:Merge Requestページ下部の「Revert」ボタン
  • Bitbucket:Pull Requestの「・・・」メニューから「Revert pull request」
  • Azure DevOps:PRページの「Revert」オプション

ポイント:UIのRevertボタンは内部的に-m 1相当の動作で、新しいPRとして作成されるためレビューも通せるのが利点。conflictが起きた場合や、複雑な手順が必要な場合のみCLIで対応するのが現実的です。「UI優先、必要ならCLI」と覚えておけば迷いません。

複数マージを連続で取り消す

「リリース前に直近5つのマージを全部取り消したい」のようなケース。時系列に沿って一つずつ取り消すのが安全ですが、まとめてやる方法もあります。

複数マージの順次revert
# 取り消し対象マージを時系列で特定(新しい順)
git log --merges --oneline -10

# 一つずつ取り消し(conflict対応しやすい)
git revert -m 1 <M3>
git revert -m 1 <M2>
git revert -m 1 <M1>
# それぞれcommitが作られる

# まとめて1つのcommitにする場合
git revert --no-commit -m 1 <M3> <M2> <M1>
git commit -m "revert: 直近3マージを一括取り消し(リリース延期のため)"

revert用の一時ブランチで安全に作業

PR経由で慎重に進める
# revert作業用のブランチを切る
git switch -c revert/release-freeze

# 順次revert
git revert -m 1 <M3>
git revert -m 1 <M2>
git revert -m 1 <M1>

# push してPR作成
git push -u origin revert/release-freeze

# PR画面でレビュー → CI通過 → マージ
# mainに直接revertするより格段に安全

注意:複数マージを時系列と逆順(新→旧)で取り消すと、conflictが起きにくいです。古いマージから先に取り消そうとすると後のマージが前提としていた変更が消えるため、複雑な衝突を招きやすい。基本は新しいマージから順に取り消すのが鉄則です。

取り消し後に必要な変更だけ救出する

「マージは取り消したいが、中の一部commitは使いたい」という場合はcherry-pickで必要な変更だけを再適用します。revert済みマージの中から価値のあるcommitを選別できます。

cherry-pickで部分救済
# マージ元ブランチのcommitを確認
git log --oneline feature/xxx

# 必要なcommitだけmainへ取り込み
git switch main
git cherry-pick <C1> <C2>

# conflictが出たら解消
git add .
git cherry-pick --continue
ファイル単位での復元
# マージコミットMの1つ手前(M^)の状態から
# 特定ファイルだけ現在に持ってくる
git restore --source <M^> -- path/to/file

# あるいは M自体の状態からファイル取得
git restore --source <M> -- path/to/desired_file

# 確認してcommit
git add path/to/file
git commit -m "restore: file を Mマージの状態へ合わせる"

ポイント:revert→cherry-pickの組み合わせは、「全体は失敗したが一部は残したい」状況で最強です。feature全体をrevertしてから、本当に必要だった修正commitだけをcherry-pickで取り込み直せば、履歴は綺麗かつ必要な変更は残せます。ファイル単位で復元する場合は消したファイルを戻す方法も参照。

未push段階ならgit resetで完全に消せる

まだリモートにマージコミットがpushされていなければ、git resetでマージそのものを履歴から消去できます。これは履歴書き換えなのでpush前限定

resetでマージ前に戻す
# マージ直前のSHAを確認
git log --oneline -5

# 作業ツリーごと戻す
git reset --hard <マージ前のSHA>

# 変更を残して履歴だけ戻す
git reset --mixed <マージ前のSHA>

# 直前のmergeなら ORIG_HEAD が使える(便利)
git reset --hard ORIG_HEAD
# ORIG_HEADはmerge実行前のHEADを指す

警告:push済みのmainブランチでreset --hardpush --force他メンバーのローカル環境を破壊します。公開済みマージは必ずrevert -m 1で取り消してください。個人ブランチや未pushの作業ブランチに限ってresetを使うのが鉄則です。

誤マージを防ぐ運用

運用ルールのベストプラクティス

  • 保護ブランチ(Branch protection rules)でmainへの直接pushを禁止
  • PR必須必須レビューCI必須で統合を制限
  • Required approvals(GitHub)で複数レビュアーの承認を必須化
  • Require linear history設定で不要なマージコミットを抑止
  • Conventional Commitsでmergeメッセージを明確化(Revertも追跡しやすい)
  • Deploy前のQA段階でrevert判断を入れる工程設計
Git設定で事故予防
# pullでmergeが生まれるのを防ぐ
git config --global pull.ff only
# あるいは
git config --global pull.rebase true

# default で merge commit を作る(ff無効化)
git config --global merge.ff false
# ↑ チームで統一するなら推奨。本記事のrevert -mが効く状態を作る

# 参照掃除
git config --global fetch.prune true

ポイント:merge.ff=falseを設定するとmergeは常にマージコミットが作られるため、後からrevert -m 1で取り消しやすい履歴になります。逆にfast-forwardマージだと取り消しが複雑になるため、チームで統合戦略を揃えておくのが大切です。

実践シナリオ

シナリオ① GitHub PRマージ直後の取り消し

UI経由が最速
# 方法A: GitHub UIの Revert ボタン
#   1. PR画面を開く
#   2. Revert ボタンをクリック
#   3. 自動生成されたRevert PRをマージ

# 方法B: CLIで
git switch main
git pull
git log --merges --oneline -3
git revert -m 1 <マージSHA>
git push

シナリオ② revert後に修正して再度取り込みたい

revert of revert パターン
# R1が先のrevertコミット
git revert <R1のSHA>
# featureの変更が復活

# さらに修正を追加
git switch feature/xxx
git commit -am "fix: 追加修正"
git switch main
git cherry-pick <追加修正SHA>
git push

シナリオ③ 複数の直近マージを一括取り消し

revert用ブランチで PR 経由
git switch -c revert/rollback-release
git revert -m 1 <M3>
git revert -m 1 <M2>
git revert -m 1 <M1>
git push -u origin revert/rollback-release
# PRを作成してチームレビュー → マージ

シナリオ④ Squash mergeを取り消したい

-m オプションは不要
# Squash mergeは通常コミット扱い
git revert <SquashSHA>
# -m 1 を付けるとエラーになるので注意
git push

やってはいけない落とし穴

公開済みmainにreset –hard + force push

mainブランチの履歴を書き換えると、他メンバーがpullした時点で「分岐した履歴」が発生します。ローカル環境が壊れ、CI/CDのキャッシュや外部サービスとも不整合が起きるため、公開済みマージは必ずrevert -m 1で取り消してください。

-m オプションなしでmerge commit にrevert

git revert <マージSHA>(-m なし)を実行すると「error: commit <SHA> is a merge but no -m option was given」とエラーで止まります。マージコミットには必ず-m 1を付けるのが鉄則。Squash mergeやRebase mergeは通常コミット扱いなので-mは不要です(方式によって異なります)。

-m 1 と -m 2 を逆に指定

-m 2を指定すると「feature側が基準」となり、本来消したい変更が残ったままになります。事前にgit show --no-patch --pretty=raw <マージSHA>でparent順を確認してから実行すれば事故は防げます。親1=マージ先(通常main)が基本です。

revert後の再マージが効かない罠を知らない

「revertしたのに、同じfeatureを再度mergeしても変更が反映されない」で悩む人が非常に多いです。本記事の「再マージ問題」章を参照し、revert of revertfeatureに新commitで対処してください。知らずに--allow-unrelated-histories等を試して履歴が混乱する事態は避けられます。

複数マージを古い順に取り消して conflict 地獄

時系列が古いマージから先にrevertすると、後続マージが「既にrevertされた変更」に依存しているためconflictが多発します。必ず新しい順(最新のマージから)に取り消すのが基本。conflictが出にくく、精神衛生上もずっと楽です。

Revert PRをレビューなしでマージ

「取り消しだから安全」という油断で直接mainへpushするのは避けましょう。Revert PRもレビュー・CI通過を経てからマージするのが鉄則。特にrevert時のconflict解消の正しさは、機械的検証だけでは気づけないケースもあります。

よくある質問

Q-m 1 と -m 2 どっちを使う?
A99%のケースは-m 1です。親1(通常main側)を基準にマージを打ち消すのが「マージ前のmainに戻す」意味になります。feature側でmainを取り込んだ逆向きマージを取り消す稀なケースのみ-m 2を使います。GitHub UIのRevertボタンは内部的に-m 1相当。
Qrevert後に再マージしても何も起きない
A「再マージ問題」です。Gitは「feature側の変更は既に処理済み」と判断するため、単純なmergeでは反映されません。revertコミット自体をさらにrevert(revert of revert)するか、feature側に新commitを追加してから再マージしましょう。
QSquash mergeは -m を付けるとエラー
ASquash mergeで作られたコミットは親1つの通常コミットなので-mは不要です。git revert <SquashSHA>だけでOK。-mを付けると「is not a merge」エラーになります。マージ方式を確認してから実行しましょう。
Q複数マージを一括取り消ししたい
A新しい順に一つずつgit revert -m 1 <SHA>を実行するのが安全です。まとめたいならgit revert --no-commit -m 1 <SHA1> <SHA2>でstageだけ作り、最後に1コミットにまとめられます。revert用ブランチでPR経由で進めるのが推奨。
Qマージコミット内の特定ファイルだけ戻したい
Agit restore --source <M^> -- path/to/fileでマージ前の状態から特定ファイルだけ復元できます。マージ全体をrevertするより差分が小さく、レビューもしやすいです。
QGitHub UIの「Revert」ボタンが無効
Amainブランチでconflictが発生する場合、UIのRevertボタンは無効化されCLI対応が必要です。その場合はgit revert -m 1 <SHA>でローカル解消し、revert専用ブランチ経由でPR作成→レビュー→マージの流れで進めましょう。
Q未pushならresetでOK?
Aはい。未pushならgit reset --hard <マージ前SHA>git reset --hard ORIG_HEADでマージ自体を消せます。ORIG_HEADはmerge実行前のHEADを自動で指すので便利。ただしpush後は必ずrevertで対応してください。

関連記事

まとめ

  • マージコミットには親が2つ以上、取り消しには-mオプションが必須
  • 99%のケースは-m 1(親1=main側基準)で正解
  • 再マージ問題は「revert of revert」か「feature側に新commit」で解決
  • Squash mergeは通常commit扱いなので-m不要
  • 複数マージは新しい順に取り消すとconflictが少ない
  • GitHub UIのRevertボタン=-m 1相当で安全・レビュー可能
  • 未pushならgit reset --hard ORIG_HEADで完全消去も可能
  • 予防:保護ブランチ+PR必須+merge.ff=falseでrevertしやすい履歴を

mergeコミットの取り消しは親番号の選択再マージ問題の理解が鍵です。親1基準のrevert -m 1を覚えておき、「打ち消したfeatureを再度取り込みたい」時はrevert of revertで対処する——この2つさえ押さえれば大半のケースに対応できます。CLIよりGitHub UIのRevertボタンを活用し、複雑なケースのみCLIで丁寧に進める運用が現代的。保護ブランチ+PR必須+merge.ff=false「取り消しやすい履歴を作る」運用をチームで統一していきましょう。