TT Lab
はじめる
学ぶ 学習パス コース

Git実戦

取り消した機能は、もう一度 merge しても戻らない

TT Labで続きを見る

一言でいうと

revertは、変更を打ち消す新しいコミットを作ります。そのため、マージコミットを元に戻すには、どちらの側を基準に打ち消すか(-m)を指定する必要があり、元に戻した機能ブランチをあとで再びmergeすると、gitは「すでにマージされている」と答えます。履歴はそのまま残っていて、内容だけが消えた状態だからです。

なぜ必要なのか

リリースの翌日に障害が起きます。原因は昨日マージした機能ブランチで、すでに複数の人がその履歴を取得しています。resetは使えません。他の人に渡ったコミットを消すことになり、全員の履歴がずれるからです。そのため、revertを使います。

ところが、そのマージコミットを元に戻そうとすると、gitが止まります。

$ git revert 685e75f
error: commit 685e75f is a merge but no -m option was given.
fatal: revert failed

マージコミットは、親が2つあります。「元に戻す」というのは、ある状態に戻るという意味ですが、親が2つあると、どちらの親の状態に戻るかが決まりません。-m 1は、1番目の親(たいてい、マージを受け入れた側、つまりmain)を基準にして、2番目の親が持ち込んだ変更を打ち消すという意味です。-m 2は、その逆です。

ここまでは、たいていの人が知っていて通り過ぎます。本当の落とし穴は、その次です。

どう動くのか

マージをrevert -m 1で元に戻したあと、機能を直して再び入れようとgit merge featureを実行すると、次のように表示されます。

Already up to date.

バグのように見えますが、正確な答えです。mergeは、履歴の到達可能性で判断します。そのマージコミットが今も履歴にあり、featureのすべてのコミットがそこから到達可能なので、mergeの立場では、マージするものが残っていません。内容がない理由は、そのあとに来たrevertコミットが消したからであり、マージされていないからではありません。

そのため、元に戻した機能を復活させる方法は、revertをrevertすることです。

git revert <되돌림 커밋>

git 2.xは、このコミットのデフォルトのタイトルをReapply "..."にしてくれます。履歴に、取り消しと復活の両方が残るため、あとで「この機能はなぜ2回入ったのか」を、履歴を読むだけでわかります。Linuxカーネルのドキュメントがこのパターンを勧める理由も同じです。履歴は、実際に起きたことを語る必要があります。

cherry-pickに出典を残す

ホットフィックスをリリースブランチで作り、mainにも入れることはよくあります。ただのcherry-pickでは、新しいコミットに、元がどこだったかが残りません。-xを付けると、メッセージの末尾に1行が付きます。

(cherry picked from commit fb42e70c24e9bab398dcef64c3b2bd316de8ca4e)

あとで、「この修正はリリースにも入ったのか」を調査するとき、この1行があるかないかで、大きく変わります。ただし、非公開リポジトリから公開リポジトリへ移すときは、存在しないハッシュを指すことになるため、そのときは使いません。

同じ変更がすでに入っているか: patch-id

「このコミットは、あのブランチにすでに反映されているか」は、ハッシュではわかりません。cherry-pickは、新しいコミットを作るからです。gitは、代わりにpatch-idを使います。diffから空白と行番号を取り除いてハッシュした値なので、同じ変更であれば、コミットのハッシュが違っても、同じ値が出ます。

$ git show <원본> | git patch-id --stable
37d09d56... fb42e70c...
$ git show <가져온 것> | git patch-id --stable
37d09d56... 075fc52e...

git cherry -v <upstream> <head>が、この計算を代わりに行ってくれます。先頭の-は、その変更がupstreamにすでにあるという意味で、+は、まだないという意味です。

ここに、重要な限界があります。衝突を解決して取り込んだコミットは、patch-idが変わります。文脈が変わったので、diffも違うからです。そのため、git cherryは、そのコミットを+(まだない)と表示します。自動判定をそのまま信じて再び取り込むと、同じ修正が2回入ります。このツールが答える問いは、「同じdiffがあるか」であり、「同じ意図が反映されたか」ではありません。

現場での姿

次のラボですること

マージコミットを作り、-mなしで元に戻そうとして拒否されるところから見ます。-m 1で元に戻したあと、git mergeが「Already up to date」を出すことを確認し、revertのrevertで復活させます。そのあと、ホットフィックスを-xで取り込み、衝突するコミット1つは--abortで引き返してから、再び--continueで完了させます。最後に、git cherryとgit patch-idで、何がすでに入っているかを数えてみると、衝突で解決したコミットがなぜ+のまま残るのかが、目に見えます。

公式ドキュメントは、git-revert、git-cherry-pick、git-cherry、git-patch-idです。