TT Lab
Get started
Learn Learning paths Courses

Git in Practice

A Reverted Feature Will Not Come Back With Merge

Continue in TT Lab

One-line summary

revert creates a new commit that reverses a change. So to undo a merge commit, you have to say which side to use as the base for reversing (-m), and if you later merge the reverted feature branch again, git answers "it is already merged". This is because the history is still there and only the content is gone.

Why this is needed

An outage occurs the day after a release. The cause is the feature branch merged yesterday, and many people have already pulled that history. You cannot use reset — it deletes commits that have gone to other people, so everyone's history gets out of sync. So you use revert.

But when you try to revert that merge commit, git stops.

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

A merge commit has two parents. "Undo" means going back to some state, but with two parents, which parent's state to go back to is not determined. -m 1 means to take the first parent (usually the side that was merged into, that is, main) as the base and reverse the changes brought by the second parent. -m 2 is the opposite.

Most people know this much and move on. The real trap comes next.

How it works

After reverting the merge with revert -m 1, if you fix the feature and try to put it back with git merge feature, this is what comes out.

Already up to date.

It looks like a bug, but it is the correct answer. merge judges by the reachability of the history. That merge commit is still in the history, and every commit of feature is reachable from it, so from merge's point of view there is nothing left to merge. The reason the content is missing is that the revert commit that came afterward erased it, not that it was not merged.

So the way to revive the reverted feature is to revert the revert.

git revert <되돌림 커밋>

git 2.x gives this commit the default subject Reapply "...". Both the undoing and the reviving remain in the history, so later, from the history alone, you can tell "why did this feature come in twice?". This is also why the Linux kernel documentation recommends this pattern — the history must tell what actually happened.

Leave the source on a cherry-pick

It is common to make a hotfix on the release branch and also put it into main. If you simply cherry-pick, the new commit does not record where the original was. If you add -x, one line is appended at the end of the message.

(cherry picked from commit fb42e70c24e9bab398dcef64c3b2bd316de8ca4e)

Later, when you investigate "did this fix also go into the release?", it makes a big difference whether this one line is there or not. However, when moving from a private repository to a public one, it would point to a hash that does not exist, so you do not use it then.

Whether the same change is already in — patch-id

"Has this commit already been applied to that branch?" cannot be known from the hash. This is because cherry-pick creates a new commit. Git instead uses the patch-id — it is a hash of the diff with whitespace and line numbers removed, so for the same change, the same value comes out even if the commit hashes differ.

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

git cherry -v <upstream> <head> does this calculation for you. A leading - means that change is already in upstream, and + means it is not there yet.

There is an important limitation here. A commit brought in after resolving a conflict has a different patch-id. This is because the context changed, so the diff differs too. So git cherry marks that commit as + (not yet there). If you trust the automatic verdict as it is and bring it in again, the same fix goes in twice — the question this tool answers is "is there the same diff?", not "has the same intent been applied?".

What it looks like in the field

What you will do in the next lab

You start by making a merge commit and trying to revert it without -m and being refused. After reverting with -m 1, you confirm that git merge says "Already up to date", and revive it with a revert of the revert. Then you bring in a hotfix with -x, and for one conflicting commit you back out with --abort and then finish it with --continue. Finally, if you count with git cherry and git patch-id what has already gone in, you can see why the commit resolved with a conflict remains as +.

The official documentation is git-revert, git-cherry-pick, git-cherry and git-patch-id.