mainブランチへのPRがマージされた直後に「あれ、今の変更は不完全だった」と気づく——Gitの現場でよくあるヒヤッとする瞬間です。単にgit reset --hardで巻き戻したいところですが、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 <taro@example.com> 1705000000 +0900 # committer Taro <taro@example.com> 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 の状況 # ┌─ 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を取り消す
# 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したコミット R1 を、さらにrevertする git revert <R1のSHA> # これで「打ち消しを打ち消す」→ featureの変更が復活 # 命名例: "Revert \"Revert pull request #42\" to re-apply after fix" # その後 feature側で修正commitを追加して、さらに進める
対処法B:featureに新commitを追加して取り込み直す
# 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をマージした後に対象方式を特定しましょう。
Squash mergeの取り消し
# GitHubの "Squash and merge" で作られたコミット # 履歴上は親1つの通常コミット # 通常のrevert(-m 不要) git revert <SquashSHA> git push # -m を付けるとエラー # error: commit <SquashSHA> is not a merge.
Rebase mergeの取り消し
# 最後にマージされた 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の取り消し
# 親確認 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機能
- mergeされたPR画面を開く
- 「Merged」表示の横にある Revert ボタンをクリック
- 自動で「Revert PR #42」という新しいPRが作られる
- そのPRをレビューしてマージすれば取り消し完了
- 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つのマージを全部取り消したい」のようなケース。時系列に沿って一つずつ取り消すのが安全ですが、まとめてやる方法もあります。
# 取り消し対象マージを時系列で特定(新しい順) 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用の一時ブランチで安全に作業
# 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を選別できます。
# マージ元ブランチの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前限定。
# マージ直前の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 --hard+push --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判断を入れる工程設計
# 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マージ直後の取り消し
# 方法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後に修正して再度取り込みたい
# R1が先のrevertコミット git revert <R1のSHA> # featureの変更が復活 # さらに修正を追加 git switch feature/xxx git commit -am "fix: 追加修正" git switch main git cherry-pick <追加修正SHA> git push
シナリオ③ 複数の直近マージを一括取り消し
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を取り消したい
# 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 revertかfeatureに新commitで対処してください。知らずに--allow-unrelated-histories等を試して履歴が混乱する事態は避けられます。
複数マージを古い順に取り消して conflict 地獄
時系列が古いマージから先にrevertすると、後続マージが「既にrevertされた変更」に依存しているためconflictが多発します。必ず新しい順(最新のマージから)に取り消すのが基本。conflictが出にくく、精神衛生上もずっと楽です。
Revert PRをレビューなしでマージ
「取り消しだから安全」という油断で直接mainへpushするのは避けましょう。Revert PRもレビュー・CI通過を経てからマージするのが鉄則。特にrevert時のconflict解消の正しさは、機械的検証だけでは気づけないケースもあります。
よくある質問
git revert <SquashSHA>だけでOK。-mを付けると「is not a merge」エラーになります。マージ方式を確認してから実行しましょう。git revert -m 1 <SHA>を実行するのが安全です。まとめたいならgit revert --no-commit -m 1 <SHA1> <SHA2>でstageだけ作り、最後に1コミットにまとめられます。revert用ブランチでPR経由で進めるのが推奨。git restore --source <M^> -- path/to/fileでマージ前の状態から特定ファイルだけ復元できます。マージ全体をrevertするより差分が小さく、レビューもしやすいです。git revert -m 1 <SHA>でローカル解消し、revert専用ブランチ経由でPR作成→レビュー→マージの流れで進めましょう。git reset --hard <マージ前SHA>かgit reset --hard ORIG_HEADでマージ自体を消せます。ORIG_HEADはmerge実行前のHEADを自動で指すので便利。ただしpush後は必ずrevertで対応してください。関連記事
- 【Git】マージの取り消し方法 — マージ取消の総合(3状態の全体像)
- 【Git】rebaseとmergeの違いと使い分け — 統合戦略の選び方
- 【Git】コミットの取り消し方 — reset/revert/amendの全体像
- 【Git】revertとresetの違いと使い分け — 概念の深堀り
- 【Git】pull後にマージコミットが大量発生する原因と履歴整理方法 — マージコミット増殖問題
- 【Git】コミット履歴が二重化する原因と修正方法 — rebase/cherry-pick由来の重複
- 【Git】特定のコミットまで戻す方法 — reset全般
- 【Git】pushを取り消す方法 — push済み変更の対処
まとめ
- マージコミットには親が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で「取り消しやすい履歴を作る」運用をチームで統一していきましょう。

