6

We have a master branch into which we've merged about 10 feature branches, one at a time.

So the recent history looks like this:

merged feat/10 (HEAD of master)
merged feat/9
merged feat/8
merged feat/7
merged feat/6
merged feat/5
...

Now we found out that feat/7 was bad and we want to take it out of master. Reverting that merge commit isn't enough because we don't want that broken commit to exist in our history at all. We can't really use interactive rebase because that will flatten out the history to make it look as if it was all done on a single branch, and we want to preserve all that good merge history.

Is there a way to zap out a particular merge commit from a branch's history?

I'll note that the real history is much more complex than what you see in the example above, so manually re-doing all the merges since feat/7 wouldn't be a good option.

Edit

To clarify for those who vote to close this as a dup: this isn't the FAQ about how to take out a commit with rebase, which of course has been answered many times. The question here is about taking out a commit without flattening the merge history.

Assaf Lavie
  • 73,079
  • 34
  • 148
  • 203
  • possible duplicate of [How do you remove a specific revision in the git history?](http://stackoverflow.com/questions/37219/how-do-you-remove-a-specific-revision-in-the-git-history) – Billy Moon Aug 06 '12 at 08:45
  • @BillyMoon the question you put out as a possible duplicate is really unrelated as I specifically mentioned why a simple rebase here is not the solution. Please revert your close vote if possible. People who may not really understand the nuance might actually think this is a dup. – Assaf Lavie Aug 06 '12 at 08:52
  • 1
    Are you sure it is necessary to flatten your history in order to rebase? Can you not just rebase feat/8 onto feat/7, and specify you don't want to keep the code from feat/7? – Billy Moon Aug 06 '12 at 09:22
  • I edited my answer Check it :) – Learath2 Aug 06 '12 at 09:22
  • @BillyMoon, rebasing feat/8 will do nothing towards rebasing all the merges that followed it into master. feat/8 "ends", as it were, in its last commit... in other words, feat/8 is not the problem. The problem is in master, which would not be effected by the suggested rebase. – Assaf Lavie Aug 06 '12 at 09:34
  • What I mean by rebase 8 onto 7, is to rebase the master branch, from the commit where you merged 8 onto the commit where you merged 7, not that you rebase your feature branches onto each other. I am not convinced that this is not possible. – Billy Moon Aug 06 '12 at 09:50
  • That, along with -p as Learath2 suggestion, might work. I'll try to make it happen. Thanks. – Assaf Lavie Aug 06 '12 at 10:02
  • Why not just revert the merge? Having revert commits in history isn't great, but it's one command and it's significantly easier than getting your collaborators to reset WIP for a `filter-branch` or `rebase`... – Christopher Aug 06 '12 at 12:22

4 Answers4

1

If your history currently looks like that and you didnt delete the branches yet you can simply git reset --hard HEAD~4 this will reset your code back to state before you merged in 7 then you can simply git merge the good ones back in. This is the simplest way I can think of off my head.

EDIT : You can use -p switch on rebase to preserve merges but using this switch with -i might have consequances. Check man git-rebase page and see the BUGS part to see current bugs.

EDIT2 : I don't take any responsibility if you don't take proper precautions before using this command. Don't use it before reading the manpage too.

Learath2
  • 20,023
  • 2
  • 20
  • 30
  • I appreciate your answer, but the example above is actually a simplified one. In the real world there are many more branches and the history could be going way back, so manually re-merging is not a good option for us. Looking for an automatic solution. – Assaf Lavie Aug 06 '12 at 08:50
  • I wonder you can have an automatic solution because if you take out one revision, all subsequent merge is different from before, that means there may be conflicts that didn't happen before etc. You have to handle them manually – Adrian Shum Aug 06 '12 at 09:00
  • Well you didn't state that in your question state it please. I simply tought you just gave me the real history – Learath2 Aug 06 '12 at 09:03
  • Well the same could be said of any rebase operation... I think this question is actually a specialized case of a more general one: is it possible to rewrite a branch's history without losing merge history along the way? – Assaf Lavie Aug 06 '12 at 09:04
  • You can't "simply `git merge`" changes back, as it would merge back the unwanted parts as well. – Evgeny Aug 06 '12 at 09:56
  • Great suggestion about -p! Thanks. If only it worked properly. The bugs section in the man page do indicate clearly that using it with interactive rebases doesn't work that well. I tried it (essentially deleting the merge line in question) and the resulting branch was very weird and somehow did contain that commit after all. I'll try to see if I can use it without -i. – Assaf Lavie Aug 06 '12 at 10:01
  • You can try and use `-p` while specifying range of commits and `--onto`, although no guarantees that it would work. Like this: ``git rebase -p --onto feat/6 feat/8 feat/10`` – Evgeny Aug 06 '12 at 10:18
  • @Evgeny you simply didn't read the question can you tell me why does feat/8 contain feat/7 ? – Learath2 Aug 06 '12 at 10:52
  • because its git and a commit includes its parents when you merge it. unless what you meant was that he needs to go through all the history beyond feat/7 and do merge-or-cherrypick depending on the next upcoming commit, thus manually merging each merge and cherry-picking each commit one-by-one. – Evgeny Aug 06 '12 at 11:50
  • Where in his question he said feat/8 was based on feat/7 ? – Learath2 Aug 06 '12 at 13:06
  • you can't really tell from the graph where feat/8 (or 9, or 10) branched off from, before or after feat/7 in the history. just merging these back into the new master will bring with it parts of the old master incl. feat/7 – Evgeny Aug 07 '12 at 16:42
1

You can use git filter-branch --parent-filter to rewrite the feat/8 commit so that its parent points to the feat/6 commit. Leaving the parents of all other commits (9-10) as they are, which should preserve merge commits in history as they were.

Only problem with this is what will happen to conflicts that result in the removed code changed ... there is no real way of knowing, and it might be the culprit.

Evgeny
  • 6,533
  • 5
  • 58
  • 64
1

This isn't quite what you want to do ("zap" the merge commit, so to speak), but in practical terms it's way easier than convincing your collaborators to git reset --hard after a rebase or filter-branch. Just revert the merge.

git revert -m 1 <commit_for_feat7>

I don't particularly like polluting my master branches with reverts, but there is nothing inherently wrong with it. If you're not going to be patching feat7 for a while, or just want its change sets out of history, this solution is much less trouble than history-revision.

Christopher
  • 42,720
  • 11
  • 81
  • 99
  • +1 this is actually the best solution, as you can also fix any conflicts during the revert, and it preserves history (a good thing). – Evgeny Aug 06 '12 at 12:41
  • The only draw back is that it pollutes the history with noise. From a practical standpoint it also prevents you from using things like git-bisect which rely on all commits living up to some standard such as at least compiling without error. Naturally this question doesn't even arise if we're willing to accept a bad commit somewhere in the history. – Assaf Lavie Aug 06 '12 at 16:26
  • 2
    Both completely valid complaints, although work-aroundable (e.g. `git bisect skip `). It's just about where you want to put the work: future debugging or resetting current work in progress? – Christopher Aug 06 '12 at 18:07
0

You can't just zap stuff out of git because the current hash depends on the entire history.

One option is to create a new branch that starts from feat/6 and then has the merges starting from feat/8 so that the branch head can point to a different hash without the changes from feat/7.

Another option, if I'm not mistaken, is git replace (I think it used to be called grafts). It can let you "replace" the pointer at feat/8 from feat/7 to feat/6. I'm not entirely sure how exactly it achieves this, but it looks like it isn't a true replacement because feat/8 still does have a pointer somewhere to feat/7 because the hash doesn't change, but git replace somehow adds an alternate history in which feat/8 points to feat/6. From the man page:

git replace [-f] <object> <replacement>   
git replace -d <object>…  
git replace -l [<pattern>]

Adds a replace reference in .git/refs/replace/

The name of the replace reference is the SHA1 of the object that is replaced. The content of the replace reference is the SHA1 of the replacement object.

Unless -f is given, the replace reference must not yet exist in .git/refs/replace/ directory.

Replacement references will be used by default by all git commands except those doing reachability traversal (prune, pack transfer and fsck).

It is possible to disable use of replacement references for any command using the --no-replace-objects option just after git.

EDIT: on second thought git replace might be a bad idea since changes from feat/7 would exist in the merge of feat/8. You should probably just go with the first option: start a new branch off of the merged feat/6 and remerge starting from feat/8

habeanf
  • 1
  • 1
  • When you 'remerge' starting from feat/8 of the original master, you will also merge-in feat/7. – Evgeny Aug 06 '12 at 10:13
  • I think not, since each feat/n is a different branch with a different feature. When remerging feat/8 it is not necessarily true that feat/7 will be merged too – habeanf Aug 06 '12 at 12:03
  • what if `feat/10` was branched from `feat/8` commit on `master` and then merged back, when you try and merge `feat/10` you'll "bring back" old history that includes `feat/7`. besides, merging or cherry-picking each commit manually one-by-one is not really a good solution. – Evgeny Aug 06 '12 at 12:39