マージを戻したいとき、打つコマンドは3つに分かれます。分岐させるのは好みではなく、いま自分がどの状態にいるかです。コンフリクトの解決中なのか、マージコミットができてまだ手元にあるのか、すでに共有ブランチへpushしたのか。この記事では状態の見分け方から、git merge --abort・git reset --hard ORIG_HEAD・git revert -mの実行手順、取り消したブランチを再マージしても変更が戻ってこないGitの仕様、git reflogで復旧できる期限までをコマンド付きで扱います。mergeとrebaseのどちらを選ぶかという手前の判断はgit rebaseの使い方とmergeとの違いの解説に譲り、ここは「やってしまったマージをどう戻すか」に絞りました。
まとめ:状態で決まるgit merge取り消しの3コマンドと選び方
コンフリクトの解決中ならgit merge --abortです。公式ドキュメントはこれを「マージ前の状態を再構築しようとする」ものと定義しており、MERGE_HEADがある間だけ使えます。
マージコミットができていて、まだpushしていないならgit reset --hard ORIG_HEAD。Gitはmergeやpullの直前に必ず現在のブランチ先端をORIG_HEADへ退避しているため、SHAを調べなくても1コマンドで戻せます。
すでにpush済み、あるいは共有ブランチならgit revert -m 1 <マージコミット>だけです。履歴を書き換えずに打ち消しコミットを1つ足す方式なので、他のメンバーの手元を壊しません。
ただしrevertには後遺症があります。打ち消したマージと同じブランチをあとで再マージしても、変更は戻ってきません。Git公式のhowtoが明記している仕様で、この一点を知らずにrevertすると「マージしたのにファイルが空のまま」という二次災害になります。対処は本文の該当章で扱いました。
取り消し前に確認する3つの状態とHEAD・MERGE_HEADの見分け方
先に状態を確定させます。ここを飛ばしてコマンドを打つと、まだコミットされていない変更まで消える経路に入りかねません。
git statusとgit logで判別するマージ完了前後の3状態
判定に使うのは2つのコマンドです。git statusの冒頭に「You have unmerged paths.」や「All conflicts fixed but you are still merging.」が出ていれば、マージはまだ完了していません。出ていなければマージコミットは作られています。
git status
git log --oneline --graph --decorate -5
git log --oneline origin/main -1
3行目が効きます。ローカルの最新コミットがリモート追跡ブランチにも含まれているなら、それはpush済みです。手元のmainとorigin/mainが同じSHAを指していたら、reset系のコマンドはもう選べません。履歴の枝分かれを目で追いたい場合は、VS CodeのGit Graphでコミット履歴を可視化する方法を使うと親子関係が読みやすくなります。
MERGE_HEADの有無で分かれるマージの中断とやり直しの分岐
マージ実行中かどうかの正体は、.git直下のMERGE_HEADというファイルの有無です。git-mergeのドキュメントはgit merge --abortを「MERGE_HEADが存在するときgit reset --mergeと等価」と説明しています。つまりMERGE_HEADが消えた瞬間、--abortは使えなくなります。
git rev-parse --verify MERGE_HEAD
SHAが返れば中断できる状態、fatal: Needed a single revisionが返ればマージは確定済みです。この1行を先に打つ癖をつけておくと、分岐の判断を迷いません。
コンフリクト中に中断するgit merge –abortと–quitの違い
コンフリクトの解決中に方針が変わったときの選択肢は2つあります。片方は状態を巻き戻し、もう片方はマージの記録だけを捨てます。
git merge –abortが復元する範囲と未コミット変更が戻らない条件
git-mergeの公式ドキュメントは--abortを「現在のコンフリクト解決処理を中止し、マージ前の状態を再構築しようとする」と定義しています。注目したいのは「try to(しようとする)」という語です。続けてこう書かれています。マージ開始時に未コミットの作業ツリー変更があった場合、git merge --abortはそれらを再構築できないことがある、と。
git merge --abort
git status
だからドキュメント自身が、マージを走らせる前に必ずコミットかstashをしておくよう勧めています。中断が万能ではないという前提で、マージ前に手を空にしておく。この順番を守るだけで、取り消しの失敗はかなり減ります。
–quitでマージ状態だけ破棄しindexと作業ツリーを残す場面
--quitは挙動が違います。公式の定義は「進行中のマージを忘れる。indexと作業ツリーはそのまま残す」です。コンフリクトの解決作業そのものは手元に残したまま、Gitに対して「これはマージではない」と宣言するコマンドと考えると分かりやすいでしょう。
git merge --quit
git status
使い道は限られます。解決の過程で書いたコードは活かしたいが、マージコミットという形では残したくない、というケースです。実務では--abortで綺麗に戻してからやり直すほうが事故が少なく、--quitは例外的な手段として覚えておく程度で足ります。
push前のマージコミットをreset –hardとORIG_HEADで巻き戻す手順
マージコミットができていて、まだ誰にも渡していない。この条件がそろっているときだけ、履歴を書き換える方法を選べます。
ORIG_HEADを使ったreset –hardの実行手順と実行前の退避
git-resetのドキュメントには「pullやmergeは常に、現在のブランチの元の先端をORIG_HEADに残す」と書かれています。マージ直後であれば、戻したい地点はすでに名前で参照できる状態にあるということです。
git log --oneline ORIG_HEAD -1
git reset --hard ORIG_HEAD
git log --oneline --graph -5
1行目で戻り先を目視してから2行目を打ちます。ORIG_HEADは次のmergeやpullで上書きされるため、マージのあとに別のmergeを挟んでしまった場合は使えません。そのときはgit reflogでSHAを直接拾います。
–soft・–mixed・–hard・–mergeで変わる作業ツリーの扱い
4つのオプションは、indexと作業ツリーをどこまで巻き込むかで分かれます。git-resetの公式ドキュメントの定義を整理すると次のとおりです。
| オプション | index | 作業ツリー | マージ取り消しでの用途 |
|---|---|---|---|
| –soft | 変更しない | 変更しない | マージ内容をステージに残して作り直す |
| –mixed(既定) | HEADに合わせる | 変更しない | 変更は手元に残し、コミットだけ解く |
| –hard | HEADに合わせる | 指定コミットで上書き | マージをまるごと無かったことにする |
| –merge | リセットする | 差分のみ更新 | 未解決のindexエントリを片付ける |
--hardの説明には「すべてのファイルとディレクトリを指定コミットの版で上書きし、追跡外のファイルを上書きすることもある」とあります。指定コミットに含まれない追跡ファイルは、この操作による削除の対象です。ドキュメントの用例にも「他人に渡したコミットに対してこれをやってはいけない」という注意が添えられています。
push済みのマージをrevert -mで打ち消すときの親番号の決め方
共有ブランチに出てしまったマージは、履歴を書き換えずに打ち消します。ここで引っかかるのが-mという見慣れないオプションです。
git log –format=%Pで親番号を確認してからrevert -mを打つ流れ
git-revertの公式ドキュメントは「通常、マージはrevertできない。どちら側を本流と見なすべきか分からないためだ」と説明し、-mはその本流の親番号を1から数えて指定するものだと定義しています。マージコミットは親を2つ持つため、番号なしでは打ち消す向きが決まりません。
git log -1 --format=%P <マージコミットのSHA>
git revert -m 1 <マージコミットのSHA>
1行目は親のSHAを左から順に出します。左が親1、右が親2です。mainでfeatureを取り込んだマージなら、親1がmain側、親2がfeature側になります。残したいのはmain側なので-m 1。この対応は「どのブランチでマージコマンドを打ったか」で決まり、ブランチ名からは推測できません。必ず%Pで実物を確認してください。
revertが作る打ち消しコミットの中身と共有ブランチで選ぶ理由
revertは履歴を消しません。マージで入った差分を反転した新しいコミットを1つ積むだけです。既存のSHAは動かないので、他のメンバーがgit pullしてもコンフリクトは起きません。force pushも不要です。
公式ドキュメントは、自動生成されるメッセージに頼らず「なぜ元のコミットを取り消すのか」を書くよう強く推奨しています。あわせて、revertを繰り返すとReapply "Reapply "<original-subject>""のような読めない件名になるので、件名を短く書き直すようにというのも公式ドキュメントの指示です。打ち消しの理由を1行足しておくと、半年後に履歴を追う人が助かります。コミット単位の整理そのものに悩んでいるなら、squash・fixupでコミット履歴をまとめる手順のほうが目的に合う場合もあります。
revert後に再マージしても変更が戻らない理由とrevertのrevert
ここが取り消し作業でいちばん事故になる箇所です。revert自体は成功しているのに、あとから「マージしたはずの機能が入っていない」と言われる。原因はGitの設計にあります。
revert後の再マージで変更が入らない仕組みと公式howtoの説明
Gitリポジトリに同梱されているrevert-a-faulty-merge.adocは、この現象をこう書いています。更新したサイドブランチをマージしても、AやBで行った変更は結果に含まれない。なぜならWによってrevertされているからだ、と。
マージは共通祖先との差分を取り込む処理です。一度マージした時点で、AやBは「すでに取り込み済み」として記録されます。そのあとrevertで内容だけ打ち消しても、取り込み済みという記録は残ったままです。git-revertのドキュメントも「マージコミットのrevertは、そのマージが持ち込んだツリー変更を今後二度と必要としないと宣言することだ」と述べています。再マージで入るのは、revert後に新しく積まれたコミットだけになります。
revertのrevertとブランチ作り直しのどちらを選ぶかの判断基準
公式howtoが示す対処はgit revert W、つまり打ち消しコミット自体を打ち消す方法です。
git log --oneline --grep="Revert" -5
git revert <打ち消しコミットのSHA>
git merge feature
ただし同じhowtoは「考えなしにrevertのrevertをするな」と釘を刺しています。理由も具体的で、元のマージ後にサイドブランチを作り直していた場合は変更が重なり、大量のコンフリクトになるためです。
判断基準は1つに絞れます。revert後にそのブランチへ手を入れていないなら、revertのrevertを選ぶ。元のマージ以降にブランチを作り直した、あるいはrebaseで履歴を組み替えたなら、revertのrevertは選ばない。後者ではhowtoがgit rebase --no-ffで作り直してから再マージする道を示しています。どちらとも言えない状況というのは実際にはほとんどなく、「revert後にそのブランチを触ったか」を確認すれば決まります。
reset –hard後にreflogで復旧できる期間と90日・30日の既定値
reset --hardで消したつもりのコミットは、しばらくは残っています。ただし永久ではありません。
git reflogでreset前のコミットを特定して復旧する4手順
reflogはHEADが指してきた履歴の記録です。git-resetのドキュメントはHEAD@{1}を「元のreset実行前にHEADが指していたコミットを表す特別な記法」と説明しています。
git reflog -20
git show HEAD@{1} --stat
git branch rescue-merge HEAD@{1}
git reset --hard HEAD@{1}
3行目を挟むのが安全です。復旧先にいきなりresetせず、まず名前の付いたブランチとして固定しておく。こうしておけば、戻し先を間違えてもやり直せます。TUIで履歴を確認しながら操作したい場合はLazygitのインストールと使い方も選択肢になります。
gc.reflogExpire 90日・Unreachable 30日という復旧期限
復旧できる期間には既定値があります。git-reflogの公式ドキュメントによれば、有効期限は設定値gc.reflogExpireから取られ、既定は90日です。さらに、現在の先端から到達できないエントリにはgc.reflogExpireUnreachableが適用され、こちらの既定は30日と書かれています。
reset –hardで切り離したマージコミットは、まさに「到達できないエントリ」に該当します。実質的な猶予は30日と見ておくのが安全です。半年前のマージを戻したいという相談は、reflogでは解決しません。期限内に気づく仕組みのほうが大切で、そのためにCIやレビューで検知する設計が効いてきます。
GitHubのRevertボタンとsquashマージでの取り消しの違い
Revertボタンが作る打ち消しPRとsquashマージ後の違い
GitHub上でマージしたPull Requestには、Revertボタンが表示されます。GitHub Docsの説明では「マージ済みPull Requestのrevertは、元のマージコミットをrevertする新しいPull Requestを作成する」とあり、条件としてリポジトリへの書き込み権限が必要だと明記されています。
押した結果は自動マージではなく、新しいPull Requestです。レビューを経て取り消す運用になるため、共有ブランチの取り消し方法としては筋がいい仕組みだと言えます。
squashマージで取り込んだ場合は事情が変わります。squashは親を1つしか持たない通常のコミットを作るため、-mによる親番号の指定が要りません。git revert <SHA>だけで打ち消せます。同じドキュメントは、コンフリクトが起きる場合や元のPull RequestがGitHub上でマージされていない場合には、個別のコミットをrevertする必要があるかもしれないと注意しています。ボタンが出ない、押しても失敗する、という状況ではコマンド側へ切り替えてください。
取り消し事故を減らすブランチ保護設定とチーム内の運用ルール3点
ここまではコマンドの話でした。受託開発の現場で実際に効くのは、取り消しが必要になったときに誰でも同じ手順を踏める状態を先に作っておくことです。
共有ブランチでresetを禁止しrevertに寄せる運用の線引き
線引きは単純にします。リモートに出したコミットへのreset –hardは禁止、共有ブランチの取り消しはrevertのみ。例外は設けません。git-resetのドキュメント自身が「他人に渡したコミットに対してはやるな」と書いているとおりで、force pushで揃えられるのはチーム全員が同時に手を止められる場合だけです。参加者が5人を超えたあたりから、その調整コストは取り消し作業そのものより高くつきます。
仕組みで担保する方法は2つあります。GitHubのブランチ保護でforce pushを拒否する設定を入れること、そしてmainへの直接pushを止めてPull Request経由に寄せることです。設定を入れておけば、うっかりreset後のforce pushはリモート側で弾かれます。
取り消しを前提にしたマージ方式の選び方とsquashを見送る場面
マージ方式は取り消しやすさで選べます。機能単位で入れて機能単位で戻したいなら、squashマージが圧倒的に扱いやすい方式です。親が1つなので-mの判断が不要になり、取り消しがgit revert <SHA>の1コマンドで終わります。
逆に、squashマージを見送るべき場面もはっきりしています。長期のリリースブランチをmainへ統合する場合、あるいは複数人が同じブランチで並行開発していて個々のコミットの作者情報を残す必要がある場合です。ここでsquashを選ぶと履歴が1点に潰れ、あとから二分探索で原因コミットを特定するgit bisectが機能しなくなります。日々の機能開発はsquash、リリース統合は通常のマージコミット。この2本立てを最初に決めておけば、取り消しの手順も自動的に決まります。
引き渡したあとのリポジトリでこうした運用ルールが形骸化していく例は多く、force pushの禁止設定だけが残って取り消し手順は口伝、という状態をよく見かけます。運用ルールごと引き取ってほしいという相談は保守運用・内製化支援で承っています。支援の範囲は、ブランチ戦略の設計から、内製チームが自走できるまでの伴走までです。
よくある質問
マージの取り消しについて、検索でよく見かける質問に答えます。
git mergeを取り消すと相手のコミットも消えますか?
方法によって変わります。git reset --hard ORIG_HEADはマージコミットと、そのマージで取り込まれた相手ブランチの内容を手元のブランチから外しますが、相手ブランチ自体は残るのでコミットが失われるわけではありません。一方git revert -m 1は変更内容を打ち消すコミットを積むだけで、履歴上のコミットは全て残ります。消えるのは作業ツリーの中身であって、相手のブランチではありません。
push済みのマージをreset –hardで戻してforce pushしてもよいですか?
共有ブランチでは避けてください。既にpullしたメンバーの手元と履歴が食い違い、次のpullで大量のコンフリクトや意図しない復活を招きます。git-resetのドキュメントにも、他人へ渡したコミットに対して行ってはいけないという注意があります。自分しか触っていないfeatureブランチで、かつ全員に周知できる場合に限って検討する手段です。共有ブランチの取り消しはgit revertに寄せてください。
git revert -mの1と2はどちらを指定しますか?
git log -1 --format=%P <マージコミット>で親のSHAを確認して決めます。左が親1で、マージコマンドを実行したブランチ側です。mainでfeatureを取り込んだなら親1がmain、親2がfeatureなので、mainの状態へ戻したいときは-m 1を指定します。ブランチ名からは判断できないため、必ず実物のSHAと照合してください。指定を誤ると、残すべき側の変更が消えます。
マージ取り消し後に同じブランチを再マージするとどうなりますか?
revertで取り消した場合、元のマージで入った変更は再マージしても戻りません。Git公式のhowtoが「AやBで行った変更は結果に含まれない。Wによってrevertされているからだ」と明記している仕様です。再マージで入るのはrevert後に追加されたコミットだけになります。対処は打ち消しコミット自体をrevertする方法ですが、ブランチを作り直している場合はコンフリクトが多発するため、rebaseで作り直してからマージする道を選びます。
reset –hardで消したコミットは何日まで戻せますか?
既定では30日が実質的な期限です。git-reflogのドキュメントによれば、reflogの有効期限はgc.reflogExpireの既定90日、現在の先端から到達できないエントリはgc.reflogExpireUnreachableの既定30日で削除されます。reset –hardで切り離したマージコミットは到達できない側に入るため、30日を目安に考えてください。git reflog -20で対象を探し、復旧先をgit branchで固定してからresetするのが安全な手順です。
関連記事
- git rebaseとは?使い方・mergeとの違い・コンフリクト解決とやり直しを解説:取り消しの手前にある「mergeとrebaseのどちらで統合するか」の判断を扱っています。
- git commitが多すぎる時の対処法|squash・fixupでコミット履歴をまとめる手順と粒度の基準:マージを戻すのではなく履歴をまとめたい場合の手順です。
- Git Graphの使い方|VS Codeでコミット履歴・ブランチを可視化する拡張機能:マージコミットの親子関係をGUIで確認したいときに使います。
- Lazygit入門|Windows・Mac・Linuxのインストールと使い方・ショートカットを徹底解説:reflogやresetをターミナル上のUIで操作する方法をまとめています。
- Git Worktree(ワークツリー)とは|使い方・削除(add/remove/prune)とSubmoduleとの違い:マージ前の検証を別ディレクトリで行い、取り消し自体を減らす方法です。