Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to restore linear git history after nonlinear merge?

Few commits ago I accidentally did a nonlinear merge in my master branch. I have a habit of always trying to keep a linear history, so now I would like to restore the linearity.

I have made a dummy repo, which simulates the real situation I'm having for the purposes of making this more simple. Here's a GitHub link to it: https://github.com/ruohola/merge-question

Here's the output of git log --oneline --graph --date-order:

* 88a4b7e (HEAD -> master, origin/master, origin/HEAD) 11
* 5aae63c 10
*   5506f33 Merge branch 'other'
|\
| * b9c56c9 9
* | 3c72a2a 8
| * 8d2c1ea 7
| * 35f124b 6
* | 7ca5bc1 5
* | b9e9776 4
| * fd83f02 3
|/
* 4fa8b2e 2
* cbdcf50 1

Same graph in Sourcetree:

git log in sourcetree

And here is a mspaint visualization of how I would like to get my master to look like — it should essentially be like I would've rebased before the merge:
(The hashes would change ofc)

wanted end result

I know that this might not be the best practice and I am familiar with the consequences of rewriting history (no one else is working on this branch though), but would still want to be able to do this. How can this be accomplished?

like image 359
ruohola Avatar asked Aug 02 '19 14:08

ruohola


3 Answers

I think it's not that hard, just keep in mind it requires rewriting history of master:

git checkout b9c56c9
git rebase 3c72a2a # rebase on top of the other branch
git cherry-pick 5506f33..master # reapply changes from merge revision (dropping it) up until the tip of master
# if you like the results
git branch -f master
git checkout master

And now you could force-push the branch if you already have the old master in another remote

like image 134
eftshift0 Avatar answered Oct 03 '22 05:10

eftshift0


One approach would be to use rebase.

Regardless of the approach you choose, you will have to rewrite the history of your repository. You have to accept that, otherwise you will have to accept your current history.

Let's summarize the different sections of your history:

  • Commit 4, 5 and 8, these are on master
  • Commit 3, 6, 7 and 9, these are now also on master, but were originally on a different branch
  • Commit 10 and 11 are on master, after you merged the two parallel histories above

To solve this, I would do the following:

  1. Check out the "original branch", that is, commit nr. 9
  2. Create a new branch here, just to make sure we can play around a bit
  3. Rebase this new branch (consisting of commits 3, 6, 7 and 9) on top of master as it were when you originally merged, so on top of commit 8
  4. Resolve any merge conflicts (you also got these when you originally merged, but they may be need to be handled differently now due to the way rebase operates compared to merging)
  5. Once you've done this, check out the last previous commit on master, which is 11, and rebase commit 10 and 11 on top of your new branch
  6. If everything now looks good, you can hard reset master to this new branch and force-push to your remote to make it the new history

Here's diagrams of the process, step by step (commands follows):

Status now:

                         master
                            v
1---2---4---5---8---M--10--11
     \             /
      3---6---7---9

New branch for 9:

                         master
                            v
1---2---4---5---8---M--10--11
     \             /
      3---6---7---9
                  ^
                TEMP1

Rebase on top of 8, this creates 3', 6', 7', 9' (the ' means "copy of commit, same contents, new hash")

                            TEMP1
                              v
                  3'--6'--7'--9'
                 /
1---2---4---5---8---M--10--11
     \             /        ^
      3---6---7---9      master

Create a new branch for 11 (I don't like to mess with master)

                            TEMP1
                              v
                  3'--6'--7'--9'
                 /
1---2---4---5---8---M--10--11
     \             /        ^
      3---6---7---9      master
                            ^
                          TEMP2

Rebase this branch (10 and 11) on top of TEMP1:

                            TEMP1   TEMP2
                              v       v
                  3'--6'--7'--9'-10'-11'
                 /
1---2---4---5---8---M--10--11
     \             /        ^
      3---6---7---9      master

Verify that TEMP2 is identical to current master, nothing lost, nothing added, etc.

Then hard-reset master to TEMP2:

                                    master
                                      v
                            TEMP1   TEMP2
                              v       v
                  3'--6'--7'--9'-10'-11'
                 /
1---2---4---5---8---M--10--11
     \             /
      3---6---7---9

I would then delete branches TEMP1 and TEMP2.

Note that commit 3, 6, 7, 9, M, 10 and 11 still exists in the repository but they're not directly available because nothing refers to them. They're thus eligible for garbage collection and in reality the actual history of your repository now looks like this:

1---2---4---5---8---3'--6'--7'--9'-10'-11'
                                        ^
                                     master

The commands to perform these operations are:

(step 0: Make a complete copy of your local folder, complete with working folder and .git repository, then, if you can, do the following commands in that copy, if you screw up, delete the copy and start over, don't jump without a safety net)

  1. git checkout <HASH-OF-9>
  2. git checkout -b TEMP1 (yes, you can do this and the previous command in one command with git checkout -b TEMP1 <HASH-OF-9>)
  3. git rebase -i --onto <HASH-OF-8> <HASH-OF-2> TEMP1
  4. resolve merge conflicts and commit, if any
  5. git checkout -b TEMP2 <HASH-OF-11>
    git rebase --onto TEMP1 <HASH-OF-MERGE> TEMP2
  6. Check that everything is OK
  7. git checkout master
    git reset --hard TEMP2

Lastly, cleanup:

git branch -d TEMP1 TEMP2
git push -f

Only force-push when you know everything is OK

like image 39
Lasse V. Karlsen Avatar answered Oct 03 '22 06:10

Lasse V. Karlsen


Perhaps the simplest way this can be done is to "abuse" the default behavior of git rebase. That is, without explicitly passing --rebase-merges to git rebase, it will actually remove all merge commits from the history. This allows us to get the desired result extremely easily:

Before:

~/merge-question (master) $ git log --oneline --graph --date-order
* 88a4b7e (HEAD -> master, origin/master, origin/HEAD) 11
* 5aae63c 10
*   5506f33 Merge branch 'other'
|\
| * b9c56c9 9
* | 3c72a2a 8
| * 8d2c1ea 7
| * 35f124b 6
* | 7ca5bc1 5
* | b9e9776 4
| * fd83f02 3
|/
* 4fa8b2e 2
* cbdcf50 1

Running the command:

~/merge-question (master) $ git rebase 3c72a2a
First, rewinding head to replay your work on top of it...
Applying: 3
Applying: 6
Applying: 7
Applying: 9
Applying: 10
Applying: 11

After:

~/merge-question (master) $ git log --oneline --graph --date-order
* d72160d (HEAD -> master) 11
* 90a4718 10
* 3c773db 9
* ba00ecf 7
* 9e48199 6
* 24376c7 3
* 3c72a2a 8
* 7ca5bc1 5
* b9e9776 4
* 4fa8b2e 2
* cbdcf50 1

After this, just a simple git push --force-with-lease origin master and the remote's history is back to linear.

like image 8
ruohola Avatar answered Oct 03 '22 07:10

ruohola