How to undo a merge and individually reapply each commit from the merge in order?

Issue

A merge with a commits squashed introduced a bug to my master branch. I would like the to undo the merge request and then break it into its individual commits and then apply them in order to figure out which commit introduced the bug. What would be the best way to do this? I have tried using git reset HEAD~ which unstaged all of the changes from the merge, however this just tell me the files that changed. I have also looked into using a interactive rebase, but I am not sure how to properly do it for my use case. Any advice on tackling this issue?

Solution

1. Recover the branch.

If you already deleted the branch, recreate it using git reflog. Find the commit which was the tip of the branch, and git branch <name> <that commit>.

2. Undo the squash merge.

Just before your squash merge, assuming some more work was done on main, your repository looked like this.

A - B - C - D [main]
     \
      1 - 2 - 3 [branch]

After the squash merge, your repository looks like this.

$ git merge --squash branch 

A - B - C - D - 123 [main]
     \
      1 - 2 - 3 [branch]

First step is to undo that squash merge. git reset --hard HEAD~.

$ git reset --hard HEAD~ 

A - B - C - D [main]
     \
      1 - 2 - 3 [branch]

3. Bring your branch up to date.

Next step is to bring your branch up to date. You should do this before any merge. It should be part of your normal workflow to periodically bring your branch up to date. This allows you to test what will be merged before merging avoiding surprises after merging and polluting main with bugs. Doing this periodically throughout your branch work lets you catch bugs before they’re buried in more commits.

You can do this in two ways. You can merge main into the branch…

$ git checkout branch
$ git merge main

A - B - C ----- D [main]
     \           \
      1 - 2 - 3 - M [branch]

I dislike this because it complicates history with a lot of "update" merges which have no historical value.

Or you can rebase the branch on top of the merge. This rewrites each commit in the branch on top of main.

$ git checkout branch
$ git rebase main

A - B - C - D [main]
             \           
              1A - 2A - 3A [branch]

I prefer this as it keeps history simple, and forces you to deal with any conflicts with the new main one commit at a time. The result is as if you wrote your branch on top of the current main all along.

If you already pushed your branch, git push will fail; push will only add to existing branches, not replace them. Instead do git push --force-with-lease (don’t use --force). I like to alias this as git repush.

Pull with git pull --rebase. Instead of merging your branch with the pulled commits, this will rewrite your changes on top of the pulled commits. This, again, avoids meaningless "update" merges. I like to configure git pull to always rebase.

4. Bisect your branch

Now that your branch is up-to-date with main, the code in your branch is exactly the same as if you’d merged it with main. This means you can test your merge without merging.

git-bisect is a tool to discover when a bug was introduced in a branch without having to checkout and test every commit. It does a binary search to very efficiently find the commit which introduced the bug. I won’t go into how to use it here, the docs have good examples, and there’s plenty of tutorials out there.

5. Merge your branch.

Once you’ve found and fixed the bug, merge your branch.

I recommend against squash merges as they destroy history; all your careful work splitting up your commits into careful, easy to understand units will be lost. Instead, merge as normal. Since you’re already up to date this will result in a linear history with "history bubbles".

Since you’re up to date with main, this merge will result in no changes to the code. Because branch is already up to date with main, Git will forego the merge and simply move main to branch (a "fast-forward"). While this results in a linear history, it also loses what work was done in what branch, and thus the associated issue.

$ git checkout main
$ git merge branch

                             [main]
A - B - C - D - 1A - 2A - 3A [branch]

Instead, force Git to make a merge commit with git merge --no-ff (no fast-forward). Use the commit message to document what the branch was about, and links to any issue tracker.

$ git checkout main
$ git merge --no-ff branch

A - B - C - D ------------ M [main]
             \            /
              1A - 2A - 3A [branch]

This workflow means main remains linear, but the purpose of a group of commits is retained in a "history bubble" and the merge commit log.

Answered By – Schwern

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published