🧯

マージを revert する方法を図と実践で理解する

2024/12/12に公開

はじめに

障害発見→止血対応フェーズで Hotfix の PR 作成を行うなどしたが、大小様々な学びがあった。その中の基本的なこととして Merge Commit を git revert するという判断ができずもたついてしまうという悔しい思いをした。手を動かしてマージコミットをリバートする際の挙動を理解したので、その記録を残す。

概要

マージコミットの revert は、単純に git revert <コミットハッシュ> すれば良いわけではない。 -m 1 や -m 2 といったオプションが必要である。では、この1や2とは何か。

図で示すと、下記のような意味がある。

branch-a には A というコミットがあり、branch-b には B というコミットがある。
branch-a において git merge branch-b をするとマージコミット C ができあがる。

その時のコミットログは下記のようなものになる

commit C
Merge: A B
Author: hogehoge
Date:   Thu Dec 12 23:00:00 2024 +0900

    Merge branch ‘branch-b’ into branch-a

マージコミットを revert する際に、git revert -m 1 C とすれば A に戻り、git revert -m 2 C とすれば、B に戻る。

1 と 2 というのは git log の Merge: A B に記載されている順番のことで、
1 は「マージ先」を指し、2 は「マージ元」ブランチを指す。
要するに、Merge: A B = A に B をマージした の意味である。

試してみる

準備

状況を再現するために下記のコマンドを適当なディレクトリで打つ。

下記のようなブランチとコミット、ファイルができあがる。

ブランチ・コミット:

ファイル:

% ls -1
a_first_file
a_second_file
b_first_file
b_second_file
main_first_file
main_second_file

コミット作成コマンド:

git init
git switch -c main

echo 'FIRST COMMIT' > main_first_file
git add . && git commit -m 'first commit'

git switch -c feature/a
echo '[A] FIRST COMMIT' > a_first_file
git add . && git commit -m '[a] first commit'
echo '[A] SECOND COMMIT' > a_second_file
git add . && git commit -m '[a] second commit'

git switch main
git switch -c feature/b
echo '[B] FIRST COMMIT' > b_first_file
git add . && git commit -m '[b] first commit'
echo '[B] SECOND COMMIT' > b_second_file
git add . && git commit -m '[b] second commit'

git switch main
echo '[main] SECOND COMMIT' > main_second_file
git add . && git commit -m '[main] second commit'

マージコマンド:

git merge feature/a
git merge feature/b

git revert -m 1 を試す

ここで、feature/b のマージコミット(Merge branch 'feature/a')を revert したいとする。

git log で コミットハッシュ<your feature/a commit>を取得したとして、普通の commit と同じように revert を実行すると下記のエラーが出る。

% git revert <your feature/a commit>
error: commit <your feature/a commit> is a merge but no -m option was given.
fatal: revert failed

これは、Revert した際に戻る先が複数あるので、どこに戻ればいいか指定しなさい、という意味である。

対策としては、オプションの -m(--mainline) parent-number を指定する必要がある。

parent-number は、マージ が 1, マージ が 2 となる。(git log を見ると マージコミットには Merge: xxxx yyyy とある。 xxxx が 1, yyyy が 2 となる。これがマージ先とマージ元の順になっている。)

気を取り直して -m 1 といった具合でマージ先である main を parent に指定し、revert 前後のファイルを比較すると、

% ls -1
a_first_file
a_second_file
b_first_file
b_second_file
main_first_file
main_second_file
git revert -m 1 <your feature/a commit>
% ls -1
b_first_file
b_second_file
main_first_file
main_second_file

このように、マージコミットMerge branch 'feature/a' で 追加したファイル, a_first_file, a_second_fileだけが消されている。

つまり、feature/a での commit を打ち消し、main ブランチ側にファイルデータを戻した、ということである。親を マージである main に指定した(parent-number 1) とも捉えられる。

git revert -m 2 を試す

では、git revert -m 2 を、feature/b のマージコミットMerge branch 'feature/b'に対して試してみる。

挙動はどうなるだろうか。[b] first commit[b] second commitが打ち消されて、b_first_file, b_second_file が消えるだろうか?それとも別な挙動になるだろうか?

revert -m 2 <your feature/b commit>
ls -1
b_first_file
b_second_file
main_first_file

なんと、[main] second commit が打ち消され、main_second_file が消えている。

これは、main での commit を打ち消し、feature/b ブランチ側にファイルデータを戻した、ということである。親を マージ である feature/b に指定した(parent-number 2) とも捉えられる。

先ほど feature/a の [a] second commit, [a] first commitに関しては revert してなかったことになっている(写真赤紫線部分)。
そのためmain側の1つ前のcommitは[main] second commit ということになる。
よって、feature/b の commit に処理が戻ると、mainブランチ側の [main] second commit が打ち消され、featuer/b のcommitが生き残ったということである。

なお、main と それ以外とのマージにおいて、-m 2 を指定することは殆ど無い。だが、仕組みとして理解することで安全に使うことができるかつ記憶にも残りやすい。

最後に、最初の図をもう一度見てみる。

実際に動かしてみると、この図のような挙動になることがわかる。

まとめ

git revert の挙動は少し厄介に思えるが、わかってしまえばなんということはないことにも感じる。

ちなみに main に直接マージした PR は、GitHub の機能で簡単に Revert PR の作成ができる。[1]

しかし、いくつかの場合手元で git revert を打つことになる。
コンフリクトが発生する場合や、GitHubの権限がない場合がそうである。
そして main <- release <- {feature1, feature2, ...} のような feature を
release に集めるようなブランチ運用下で 個別の feature の PR を Revert したい場合も、手元で revert を行う必要がある。

このときもたついてしまうと、スムーズなリリースができないので覚えておく必要がある。

参考: git/git/Documentation/howto/revert-a-faulty-merge.txt

脚注
  1. Pull Request を打ち消す ↩︎

Discussion