Reverting Git Merge

git revert is a time machine superpowered.

Reverting Git Merge

In Git, we merge a branch into another branch; the first branch is called theirs and the second ours. We can also revert a merge later, however, you have to specify which branch to regard as the mainline... Why? Isn't the mainline the branch into which we merged another branch? Is it really possible to take the incoming branch as the mainline? Let's jump into a series of petit experiments to figure it out by moving our hands.

Preparation

Hint: You can skip this entire section if you can understand what we set up by looking at the following git log output. It's ok if you don't understand it. Just follow the rest of this section.

*   6aa9d3f (HEAD -> main) Merge branch 'holiday' into 'main'
|\
| * f77b929 (holiday) Insert Play
| | diff --git a/todo.md b/todo.md
| | index 689b833..74d7ff4 100644
| | --- a/todo.md
| | +++ b/todo.md
| | @@ -1,5 +1,6 @@
| |  # TODO
| |
| |  1. Eat
| | -2. Sleep
| | +2. Play
| | +3. Sleep
| |
* | a21d5d1 Insert Work
|/
|   diff --git a/todo.md b/todo.md
|   index 689b833..827d9bc 100644
|   --- a/todo.md
|   +++ b/todo.md
|   @@ -1,5 +1,6 @@
|    # TODO
|
|    1. Eat
|   -2. Sleep
|   +2. Work
|   +3. Sleep
|
* f9912b0 Initial commit
  diff --git a/todo.md b/todo.md
  new file mode 100644
  index 0000000..689b833
  --- /dev/null
  +++ b/todo.md
  @@ -0,0 +1,5 @@
  +# TODO
  +
  +1. Eat
  +2. Sleep
  +

First, let's create a git repository, add todo.md with the following content, and commit as the initial commit in main branch.

# TODO

1. Eat
2. Sleep
todo.md in the initial commit (f9912b0)
* f9912b0 (HEAD -> main) Initial commit
Git log after the initial commit

Then, let's create a branch called holiday from main, update todo.md with the following content, and commit.

# TODO

1. Eat
2. Play
3. Sleep
todo.md at the tip of holiday branch (f77b929).
* f77b929 (HEAD -> holiday) Insert Play
* f9912b0 (main) Initial commit
Git log after the "Insert Play" commit

Then, let's switch back to main branch and update todo.md with the following content, and commit.

# TODO

1. Eat
2. Work
3. Sleep
todo.md at the tip of main branch (a21d5d1)
* a21d5d1 (HEAD -> main) Insert Work
| * f77b929 (holiday) Insert Play
|/
* f9912b0 Initial commit
Git log after the "Insert Work" commit

Now that we have prepared good branches for our experiments, let's merge holiday into main. Note that, at this stage, HEAD is pointing at main and we are incorporating the changes from holiday.

/t/git-merge-revert-exmaple > git merge holiday
Auto-merging todo.md
CONFLICT (content): Merge conflict in todo.md
Automatic merge failed; fix conflicts and then commit the result.
Git merge fails as expected

Auto-merge fails because main and holiday have a conflicting line. Let's resolve the conflict by updating todo.md to the following content.

# TODO

1. Eat
2. Work and Play
3. Sleep
todo.md after merging
*   6aa9d3f (HEAD -> main) Merge branch 'holiday' into 'main'
|\
| * f77b929 (holiday) Insert Play
* | a21d5d1 Insert Work
|/
* f9912b0 Initial commit
Resolved the conflict by hand

Now the preparation for merge-revert experiment is done. Run git log --graph --pretty=oneline -p --abbrev-commit --all to get the output shown at the beginning of this chapter.

At the end of preparation

Experiment 1: Natural Revert

In this section, we're going to conduct a natural revert, i.e. we'll try deleting the merge commit and move HEAD back to the last commit of main.

/t/git-merge-revert-exmaple > git log --pretty=short --abbrev-commit
commit 6aa9d3f (HEAD -> main)
Merge: a21d5d1 f77b929
Author: Yuji Tabata <ytabata@example.com>

    Merge branch 'holiday' into 'main'

commit a21d5d1
Author: Yuji Tabata <ytabata@example.com>

    Insert Work

commit f77b929 (holiday)
Author: Yuji Tabata <ytabata@example.com>

    Insert Play

commit f9912b0
Author: Yuji Tabata <ytabata@example.com>

    Initial commit

As you can see in the merge commit's log message, the merge happened between a21d5d1 (from main) and f77b929 (from holiday). Let's revert the merge by specifying a21d5d1 as the mainline, i.e. -m 1.

/t/git-merge-revert-exmaple > git revert 6aa9d3f -m 1 # take the left commit hash as the mainline, i.e. a21d5d1
[main 70ffe2a] Revert "Merge branch 'holiday' into 'main'" by specifying -m 1
 1 file changed, 1 insertion(+), 1 deletion(-)
* 70ffe2a (HEAD -> main) Revert "Merge branch 'holiday' into 'main'" by specifying -m 1
*   6aa9d3f Merge branch 'holiday' into 'main'
|\
| * f77b929 (holiday) Insert Play
* | a21d5d1 Insert Work
|/
* f9912b0 Initial commit
Git log after git revert 6aa9d3f -m 1

The content of todo.md looks like below; it's same as the one in the last commit of main branch, a21d5d1.

# TODO

1. Eat
2. Work
3. Sleep
At the end of the natural revert

Clean up of Experiment 1

We have seen natural revert works as expected. Let's make the git history back to the state before the revert for the next experiment.

/t/git-merge-revert-exmaple > git reset --hard HEAD~
*   6aa9d3f (HEAD -> main) Merge branch 'holiday' into 'main'
|\
| * f77b929 (holiday) Insert Play
* | a21d5d1 Insert Work
|/
* f9912b0 Initial commit

Experiment 2: Strange Revert

This time, let's revert the merge by taking the other side of the merge, i.e. -m 2.

/t/git-merge-revert-exmaple > git revert 6aa9d3f -m 2 # take the right commit hash as the mainline, i.e. f77b929
[main ef9678b] Revert "Merge branch 'holiday' into 'main'" by specifying '-m 2'
 1 file changed, 1 insertion(+), 1 deletion(-)
* ef9678b (HEAD -> main) Revert "Merge branch 'holiday' into 'main'" by specifying '-m 2'
*   6aa9d3f Merge branch 'holiday' into 'main'
|\
| * f77b929 (holiday) Insert Play
* | a21d5d1 Insert Work
|/
* f9912b0 Initial commit
git log after git revert 6aa9d3f -m 2

The contents of todo.md is shown below. It teaches us that no matter which branch was acting as the mainline when a merge happened, you can pick an arbitrary branch as the mainline when you perform a revert.

# TODO

1. Eat
2. Play
3. Sleep
At the end of strange revert. The commit intrinsic to the past mainline (a21d5d1) is lost!

Conclusion

We have set up two branches conflicting against each other and merged them manually. Then, we reverted that merge in two ways; the first scenario was to choose the original mainline as the new mainline, and the second was to pick the original non-mainline as the new mainline. Although it goes against intuition, we saw that the second scenario worked as well as the first one. That means git revert puts us in the seat of deciding the past world line for the merged branch after the fact. git revert is a time machine superpowered.

Appendix: Reverting a Revert

Although git revert requires a mainline to revert a merge commit, it doesn't accept one for other kinds of commits including a revert commit. As an example, let's revert the revert commit we made at the end of Experiment 2. Here is the current git log.

* ef9678b (HEAD -> main) Revert "Merge branch 'holiday' into 'main'" by specifying '-m 2'
*   6aa9d3f Merge branch 'holiday' into 'main'
|\
| * f77b929 (holiday) Insert Play
* | a21d5d1 Insert Work
|/
* f9912b0 Initial commit

We'll revert ef9678b by the following command. Note that it doesn't have an -m option.

/t/git-merge-revert-exmaple > git revert ef9678b

Now, the git log looks like the following.

* 373098a (HEAD -> main) Revert "Revert "Merge branch 'holiday' into 'main'" by specifying '-m 2'"
* ef9678b Revert "Merge branch 'holiday' into 'main'" by specifying '-m 2'
*   6aa9d3f Merge branch 'holiday' into 'main'
|\
| * f77b929 (holiday) Insert Play
* | a21d5d1 Insert Work
|/
* f9912b0 Initial commit

The content of todo.md is the same as that of the merge commit. git revert has restored the content of our manual merge.

# TODO

1. Eat
2. Work and Play
3. Sleep

The benefit (or at least one of the benefits) of creating a revert commit is that you can later revert those revert commits. If git revert had deleted the merge commit to go back in history, the record of how we ingeniously resolved the merge would've lost forever. So, creating a revert commit seems redundant but it is actually a way to go back in history without losing the record of our intellectual work.