📈

Gitでもう一度マージする

2021/07/08に公開

マージの取り消し

Gitで構成管理をしていて、マージをしたものの、後になって取り消したくなることがある。そういうときは、マージ済みのコミットを指定して

$ git revert -m1 マージコミット

とするとマージで取り込まれた変更の逆の差分が適用され、ソースツリーが期待した状態になる。

例えばマージした変更に問題があり、それを一旦取り消した上でリリースを行いたいといった場面で、リバートによるマージの取り消しが利用できる。

取り消したマージをもう一度

マージを取り消した後、変更に問題があったというのが誤認識とわかり再度取り込みたい状況や、あるいは変更内容の全てに問題があったわけではなく、追加修正をして再度取り込みたい状況について考えよう。

取り消したマージをもう一度マージしようと、対象コミットを指定して git merge しても再適用はできない。

$ git merge --no-ff マージコミット^2
Already up to date.

ここで マージコミット^2 はマージコミットの2番目の親。1番目の親はマージ前のHEADで、2番目の親はマージコミットで取り込まれた対象のコミットである。

再適用できない理由は

$ git merge-base --is-ancestor マージコミット^2 HEAD
$ echo $?
0

でわかるように、履歴上に既にそのコミットが含まれているから。

Gitはコミット履歴をDAG(Directed Acyclic Graph, 有向非循環グラフ)で保持しており、同じコミットオブジェクトが履歴上に再度入ることを許さない(DAGの"A"、即ち"Acyclic"の制約)。

ではどうすれば良いか?取り消したマージを再適用するには、取り消しの取り消しをすれば良い。

$ git revert リバートコミット

これで、ソーツツリーはマージコミットが適用された状態になる。

取り消しの取り消しで解決しない場合

上で書いたリバートのリバートでだいたいの場合は方が付くのだが、それでは解決しない場合もある。例えば

  • 競合する二つの変更A, Bをフィーチャーブランチで行なっていた。
  • Aが先にマージされた
  • 続いてBがマージされた。マージ時にコンフリクト解消
  • (やりたいこと)Aを巻き戻してBのみ適用された状態にしたい

という状況が考えられる。詳しく見ていこう。

普通にやるなら git revert -m1 Aのマージコミット だが、それだと再度コンフリクト解消が必要になる。Bを先にマージしていたならばコンフリクト解消は不要だったのに、何とかならないものか。ひとまず両方のマージを取り消そう。

$ git revert -m1 Bのマージコミット
$ git revert -m1 Aのマージコミット

とすれば二つのマージが取り消された状態になる。ここで試しにBを再適用してみると、

$ git merge --no-ff Bのマージコミット^2
Already up to date.

やっぱり適用されない。Acyclicの制約がここでも現れた。

再適用するには

再適用するためには、対象のコミットたちを作り直す必要がある。再適用したいコミットが一つとか二つなら git cherry-pick で順に適用していくで何とかなる。cherry-pick はそれぞれのコミットの差分を適用し、コミット自体は作り直されるのでAcyclicの制約は回避される。

順に適用していくというのが面倒とか、コミットが多いのでいちいちやってられないという場合は git rebase でブランチのコミットを作り直した上でマージすれば良い。

$ git checkout -b branch-for-rebase-B Bのマージコミット^2
Switched to a new branch 'branch-for-rebase-B'
$ git rebase -f $(git merge-base Bのマージコミット^1 HEAD)
Current branch branch-for-rebase-B is up to date, rebase forced.
Successfully rebased and updated refs/heads/branch-for-rebase-B.
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff branch-for-rebase-B
Merge made by the 'recursive' strategy.

基点を動かさずにコミットを作り直すため rebase に -f オプションを付けていることに注意。

フォースが使える場合

ここまでは履歴の書き換えを行わずにマージを再適用する方法について書いてきたが、マージコミットをリモートにプッシュしてない場合、あるいはforce pushが許可されている場合には、別解として、

$ git reset --hard -m1 Aのマージコミット^1
$ git merge Bのマージコミット^2

として履歴を書き換える方法でも、コンフリクト発生せずマージを再適用できる。

この場合はブランチBのフィーチャーブランチでのコミットがそのまま(コミットを作り直さずに)反映できる一方で、一度Aをマージしていたという履歴自体がなかったことになることに注意。

まとめ

git revert でマージを取り消した後、再適用したい場合について考えた。Gitのコミット履歴がDAGで表現されていることから、単純に git merge での再適用はできない。ここでは、

  • リバートコミットを指定して git revert する方法
  • コミットを作り直した上で git merge する方法

で制約を回避して再適用できることを説明した。

Discussion