Is it possible to rewrite a branch's history without losing merge info along the way?

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:

  • How to create multiple PRs for specific set of commits
  • Add commit from git repository to GitHub
  • can't pop named stash in git
  • Git Push from local repo to remote repo on a server for wordpress deployment
  • How can I use Visual Studio to work with a Node.js project from an Openshift Git repository?
  • How can I commit with cloned submodule in my repo?
  • 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.

  • Generating online books
  • git checkout remote reference
  • Git - “Your branch is ahead of 'origin/master' by 3 commits.”
  • Git Merging Issue two branches
  • git branch with multiple upstreams
  • Git pull results in extraneous “Merge branch” messages in commit log
  • 4 Solutions collect form web for “Is it possible to rewrite a branch's history without losing merge info along the way?”

    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.

    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.

    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.

    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

    Git Baby is a git and github fan, let's start git clone.