git merge の取り消し方法。reset と revert コマンドについて。

最終更新:2018-02-19 by Joe

git merge を取り消す方法をまとめました。

git merge を取り消す

通常のコミットは、git でコミットを打ち消すコミットを作成する「git revert」を使って取り消すのが定石です。しかし、直前のコミットが git merge により作られたマージコミットである場合、それを打ち消す時は注意が必要になります。

さて、git revertでマージコミットを取り消すには、下記の2つの方針があります。

  1. git reset --hard によって、ブランチの歴史を強制的に巻き戻す
  2. git revert を使って、revert commitを作成する

ひとつずつ見ていきましょう。

方法1git resetを使って元に戻す。

git reset --hard は、ブランチの歴史を強制的に書き換えてしまう強力なコマンドです。もしマージコミットをpushしてないのであれば、強気で使ってしまってよいでしょう。

git reset --hard <戻りたい地点の過去のコミット>

例えば、マージコミットの作成直後です。

git merge の実行直後

git reset HEAD^ を実行します。今回は、--hardオプションを付けます。

git merge の実行直後

注意点としては、--hard オプションはインデックスと作業ツリーも強制的に書き換えますので、もし保存していない変更が作業ツリーやインデックスにあれば、消し飛びますので、実行前にgit statusでなにか作業が残っていないか確認してみて下さい。

また、望めば、--soft で、HEADの位置だけを変更したり、--mixed(オプション無しと同義)で、インデックスのみをリセットすることもできます。git reset の詳細な説明はこちらをぜひご覧ください。

方法2git revert を使ってマージコミットを取り消す

git revert はコミットを取り消す公式な方法です。指定したコミットをちょうど打ち消すようなコミットを追加することで、取り消しを実現します。

git revertの基本的な使い方

通常は、git revert で打ち消しコミットを作成するには、打ち消したいコミットを指定するだけです。範囲を指定することもできます。

// 直前のコミットを打ち消す
git revert HEAD

// 範囲を指定して打ち消す
git revert HEAD~7...HEAD

マージコミットを取り消す場合

もし revert で打ち消す対象がマージコミットであれば、上記の方法では、エラーが発生します。マージの際に統合された2つの歴史のうち、どちらを「正」とすればよいか、git 側が判断できないからです。

// 試しに、マージコミットをリバートしてみる
git revert 05de76

// エラーが発生して失敗・・。
error: commit 05de76 is a merge but no -m option was given.
fatal: revert failed

この場合、git revert のオプション「-m <親番号>」もしくは「--mainline <親番号>」とともに、親番号を指定することで、特定の履歴を残して打ち消し操作が可能になります。

git revert -m 1 05de76

「親番号」は1から始まる整数です。数え方は、もしあなたがmasterブランチにいて、another-branch をマージしたのであれば、master が1,another-branch が2となります。(これの数え方はある程度直感的だと思います。)

もしよくわからなければ、git show <マージコミット>を表示するとよいでしょう。Merge の欄に、マージ元のコミット番号が記載してあります。この順番に従って親番号が振られます。

git show 05deeb4

// この場合、ff71f80 が「1」、1be9949 が「2」
commit 05deeb4967965d32bee9b810f879de4d0391ec76
Merge: ff71f80 1be9949
Author: Alex Johanson <alexj89@gmail.com>
Date:   Sat Jul 1 19:42:06 2017 +0900

    Merge branch 'master' into prod

注意が必要なのは、このようにマージコミットを打ち消すと、打ち消されたブランチに含まれていた変更が、その後、そのブランチを再度マージしたとしても、取り込みが発生しない、という事になります。これは、revert により変更自体は打ち消されても、マージの事実が歴史上に残り続けるからです。

すこし混乱しそうですが、下記のようなイメージです。

// マージコミット「M」で取り込まれた another 側の変更を、
// git revetにより、打ち消しコミット「M'」が作られたならば、
// その後の、再びマージを行って、コミット「M2」が作られても、
// 「D」と「E」の変更のみが取り込まれる 
master -----O---O---M--O--M'--M2---
                   /         /
anther ---A---B---C---D----E

 

下記は、revert-a-faulty-merge.txt より抜粋です。リーナスによる言及との事です。

Reverting a regular commit just effectively undoes what that commit did, and is fairly straightforward. But reverting a merge commit also undoes the _data_ that the commit changed, but it does absolutely nothing to the effects on _history_ that the merge had.

So the merge will still exist, and it will still be seen as joining the two branches together, and future merges will see that merge as the last shared state – and the revert that reverted the merge brought in will not affect that at all.

[抄訳]
通常のコミットの revert は、そのコミットが行った事を効率的に取り消しでき、かなり直感的だ。ただ、マージコミットの revert においては、たしかにコミットが変更した「データ」は打ち消すが、マージが与えた「履歴」への影響に対しては、一切何も行わない。

つまりマージ自体は残る。2つのブランチは一度統合したものとみなされ続け、それ移行にマージを行う際も、そのマージコミットが最後の共有地点であり続ける。revert コマンドが及ぼした「打ち消し」は、この事に何の影響も与えない。

revert によるマージコミットの打ち消しは、この事を念頭において実行する必要があるでしょう。

git merge の取り消しに関する参考文献

git 公式ドキュメントです。