1. Introduction

When controlling file versioning with Git, there are times when we need to resolve differences between separate versions of the same file. This is often the case during a merge operation. However, we might perform a merge by mistake.

In this tutorial, we discuss Git merging and how to revert or undo a merge. First, we establish a sample repository. After that, we briefly refresh our knowledge about the merge subcommand and its mechanics. Next, we turn to conflict resolution during merges. Then, we see how to abort a merge. Finally, we explore multiple ways to undo a completed merge.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15 and Git 2.39.2. Unless otherwise specified, it should work in most POSIX-compliant environments.

2. Sample Repository

To begin with, let’s create a Git repository:

$ git init

After that, we can create several commits and a new branch:

$ git log --all --decorate --oneline --graph
* 85e775e (HEAD -> feature1) divergence
| * d609c33 (master) more additions
|/
* b6362f6 basic data
* 2da9437 init commit

Notably, we see master and feature1 are the only two branches. Further, the cherry subcommand shows us that the feature1 branch diverges from master by only one commit:

$ git cherry -v master
+ 85e775e7d9857cc46dc36ec84576cc7f0b6981a2 divergence

Now, master may continue development, while feature1 also evolves. This is usually how a pull request (PR) functions: a separate branch that expands in parallel with master.

Once all features and additions planned for the PR branch are complete, we can include them in the primary branch.

3. Git merge

After working on feature1, we can continue by merging the feature branch into the primary master branch.

First, we perform a checkout on master:

$ git checkout master
Switched to branch 'master'

Next, we verify the current status:

$ git status
On branch master
nothing to commit, working tree clean

Now, we can use the merge subcommand:

$ git merge feature1
Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.

In this case, we attempt to merge the local feature1 branch into the local primary master. If we don’t supply an argument, Git tries to synchronize the current branch with its upstream link. Since we don’t have any remote or other upstream, we provide feature1 as the argument.

As it happens, the merge operation failed due to conflicts.

Let’s recheck the status of the repository:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   file

no changes added to commit (use "git add" and/or "git commit -a")

Notably, we can see that file is the unmerged conflict and we also get a hint about fixing that before using git commit.

4. merge Conflict Resolution

Conflict resolution is a fairly basic process. After having a list of files with conflicts, we manually review each and resolve the conflict.

Let’s see an example with file from above:

$ cat file
identical content
<<<<<<< HEAD
master content
=======
feature content
>>>>>>> feature1

Notably, Git replaces the conflict files with a mix of their content that includes the differences between the two versions clearly laid out via a ======= separator. Each diff section begins with <<<<<<< followed by the merge destination and ends with >>>>>>> and the merge source. Thus, we can decide which content to keep.

In this case, we see identical content is in both versions, but the master content and feature content lines are a conflicting line between the two branches.

Let’s resolve this:

$ cat file
identical content
master content // kept from master

In this case, we decide to keep the master content, but also add a comment to signify the action. Importantly, we’re free to leave the file version as we wish, even with the diff formatting.

Once done, we add each conflicting file to staging and commit:

$ git add file
$ git commit
[master f0b29b7] Merge branch 'feature1'

The default merge comment is Merge branch ‘<SOURCE_BRANCH_NAME>’, but we can change that as usual.

Let’s verify the full log after the successful merge:

$ git log --all --decorate --oneline --graph
*   f0b29b7 (HEAD -> master) Merge branch 'feature1'
|\
| * 85e775e (feature1) divergence
* | d609c33 more additions
|/
* b6362f6 basic data
* 2da9437 init commit

As expected, the feature1 branch veers back into master at the current commit.

5. Aborting a merge

Sometimes, merges may be unexpected, accidental, hard, or dangerous.

In such cases, we can decide to –abort the merge and return to the state before committing:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   file

no changes added to commit (use "git add" and/or "git commit -a")
$ vi file
[...]
$ git merge --abort
$ git status
On branch master
nothing to commit, working tree clean

Notably, even if we make changes to files, the –abort reverts the working and staging tree accordingly.

6. Undo a merge

Finally, if we miss the chance to abort a merge and continue to commit with an unwanted merging operation, there are several ways to undo the whole operation.

Notably, considerations have to be made in case the merge is a squash.

6.1. Save Current Modifications

Critically, all changes related to the merge are lost, but, if not careful, more data loss can also occur. If we need to save any modifications

If we want to preserve any uncommitted changes since the merge, we can also use the stash:

$ git stash
[REVERT]
$ git stash pop

This way, we protect at least the working tree. Naturally, we can use the –staged flag as well.

6.2. Find Correct Commit

In most cases below, COMMIT can be any commit-like expression:

To find the one we need, the reflog subcommand is usually the preferable option to log, since it has a proper timeline. This way, we don’t need to check whether special refs like HEAD~, HEAD^, HEAD~1, and similar point to the correct commit.

If we want to synchronize with a remote that’s not behind, we can also use something like origin/<BRANCH>.

Still, perhaps the easiest ref (reference) to use just after a merge is ORIG_HEAD, as it points to the last HEAD commit.

6.3. reset to Specific Commit

Perhaps the most intuitive way to restore from a merge is by doing a reset:

$ git reset --hard <COMMIT>

So, we can decide to go back one commit in the past:

$ git reset --hard ORIG_HEAD

Notably, the –hard flag ensures the index and working tree are also reset. This isn’t always necessary.

To avoid this and keep differences between the working and commit tree, we can employ –merge:

$ git reset --merge ORIG_HEAD

This way, we don’t touch uncommitted changes.

Either way, we lose the original commit.

6.4. revert to Specific Commit

Another subcommand to undo a merge is revert:

$ git revert <COMMIT>

Unlike the reset approach, using revert preserves the original merge commit in the history. In addition, revert creates a new commit that patches back the changes since that original commit.

In addition, we can also specify the –mainline (-m) option to preselect the primary branch in the original merge:

$ git revert --mainline=1 <COMMIT>

Either way, depending on the original changes during the merge, we might need to resolve more conflicts:

$ git revert HEAD~1
Auto-merging file
CONFLICT (content): Merge conflict in file
error: could not revert d609c33... more additions
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git revert --continue".
hint: You can instead skip this commit with "git revert --skip".
hint: To abort and get back to the state before "git revert",
hint: run "git revert --abort".

Naturally, we can –abort the revert and use another approach if we don’t want to perform manual conflict resolution.

6.5. rebase to Specific Commit

Similarly, we can use the rebase subcommand:

$ git rebase --no-autostash <COMMIT>

Of course, we can omit –no-autostash if we don’t handle the stash manually or don’t need it.

Otherwise, the effect is more or less similar to the commands already discussed.

7. Summary

In this article, we explored the Git merge operation, how to resolve conflicts, and ways to revert merges.

In conclusion, although merging can seem daunting, knowing the many options to go about doing and undoing it can help when dealing with complex scenarios.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments