11

Our repository has a master branch, but it seems like one of our colleagues had a master branch with a different history. Yesterday he merged his master into the main master and pushed. So today some of us already pulled and started working. When we realized the issue a couple of hours of work had already gone by...

So my question is how can I undo his merge but still keep the code changes done today? The left side is our current state, the right side is what I am shooting for. Should I rebase? or there is a way to undo a merge? what's the best approach here?

enter image description here

Edit 1: I don't think this is a duplicate of Undo a Git merge that hasn't been pushed yet because the commit was pushed to the origin as well as I have other commits AFTER the mistake was made and I want to preserve history.

Edit 2: I tried to rebase, problem is that the rookie was merging into his master branch for the last 2 weeks... So even if I revert I don't get rid of the new "timeline" he created... I have no problems re-writing history as long as I get rid of his timeline...

enter image description here

Edit 3:

In the end, I ended up finding a version of the repository which was untouched and replying the modifications done after the incident...

simhumileco
  • 31,877
  • 16
  • 137
  • 115
Mac
  • 3,397
  • 3
  • 33
  • 58
  • what `git status` say? – Filip Bartuzi Oct 27 '15 at 17:08
  • When you say you want to keep work that was done for today, do you mean work that was on the branch that was mistakenly merged in, or work that comes in at different commits? – Makoto Oct 27 '15 at 17:38
  • @Makoto I want to keep all the commits from A going forward (upwards), so the work done of those 3 branches. – Mac Oct 27 '15 at 17:49
  • Possible duplicate of [Undo a Git merge?](http://stackoverflow.com/questions/2389361/undo-a-git-merge) – Makoto Oct 27 '15 at 17:51
  • Of specific note: [this answer](http://stackoverflow.com/a/6217372/1079354), which is the likely ideal solution for your use case (considering that you've already pushed). – Makoto Oct 27 '15 at 17:51

2 Answers2

6

I can see three options: reverting the merge, rebasing, or filtering.

Reverting

The "correct" solution, as others have advocated, is to revert the merge -- this means you're not "rewriting history", which is generally considered a Good Thing, not least because it means everyone else working on this code won't need to deal with the history changing.

You can't just use git revert as-is, however, because it doesn't know which branch of the history to keep. The solution is simply to give Git that extra bit of information:

git revert -m 2 <sha-of-B>

That -m 2 specifies you want to keep the second parent, i.e. the one that contains commit C; switch to -m 1 for the alternative.

One caution: if you ever want to merge that other branch into your main branch, you'll hit trouble due to Git thinking that branch is already in the master branch. There are several solutions to this, but the easiest (IMO) is to put the revert on a branch off master and, as well as merging it into master, merge it into that other branch. That branch will probably look like whatever version of master it was forked off, at which point you can revert the revert, and everything will be fine when you merge in future.

Rebasing

Rebasing is probably the simplest option: git rebase <sha-of-B> master --onto <sha-of-C>.

This will move all the commits from B until master onto C. The catch is that it will "linearise" the merge history. The merge history in the image is very simple, but that may be a problem if you want to preserve the history or if you're doing this on a more complicated repository.

This also has the catch that it's "rewriting history", and everyone else who's using this repository will find they're working on a completely different code branch to the one that's been reverted, since every change from C onwards will have a different sha1 hash.

Filtering

Using git filter-branch will allow you to get exactly what you're looking for in your image, i.e. preserving the merge history, but it's the most complicated option, and still involves rewriting history.

Effectively, you want to use git filter-branch to filter your commits to keep everything except that one duff merge. Doing this is sufficiently complex that I'm not going to try to write instructions; you'd need to combine --commit-filter to remove the offending merge and --tree-filter to reverse the changes of the merge.

Filtering redux

Okay, here's a fourth method that I'm including for completeness's sake. You could just checkout C, then manually cherry-pick and merge each of the changes you want to have in your master branch one-by-one.

Further reading

me_and
  • 15,158
  • 7
  • 59
  • 96
  • Ok Rebase seems to work great, but I lose the merging info. ie, it looks like all merges after the rebase were actually changes done to the master. is there a way to keep then? – Mac Oct 27 '15 at 19:33
  • @Mac yes. Any of the other options in my answer will keep the merge history rather than making it look linear on your master branch. Naturally, they're going to be more work, however. – me_and Oct 28 '15 at 08:43
  • I like the 4th option. I think if you detail that one more would use it. Its a shared repo, and cant change history. Checking out a new branch, from the commit before the merge, then cherry picking in the following work I did on my other branch is perfect. When the problem is fixed on Master, I can then merge it at that point, to my new branch. – blamb Sep 08 '22 at 12:43
1

What I would do is go for a revert, which you are simply looking for the inverse of the deltas you just merged in that merge commit, then commit that.

Pull the revert commit to your C branch then Revert that commit on C Branch and commit that. Double negative will cancel out, you will save all your history and when you merge again you will have the results you are looking for.

Git Extensions have a function for revert commits that makes this very easy.

Makoto
  • 104,088
  • 27
  • 192
  • 230
Sean Morse
  • 136
  • 1
  • 3