Alpha
Alpha

Reputation: 7858

How to avoid rebase hell when merge commits are in the way?

I've got the following situation in my git tree:

1 -- 2 -- 3 -- 4 <-- master
      \         \
       5 -- 6 -- 7 -- 8 -- 9 <-- feature

I want to rebase and squash everything from feature so that I can advance master with a single commit of the feature being added.

Being that commit 7 is already a merge that solves all the conflicts, I tried the following:

git rebase -i -p master

The only options that were given me here were commits 7, 8 and 9. "Makes sense", I thought, "since the merge already includes 5 and 6, they can just be discarded". I proceeded to squash 7, 8 and 9 in a single commit we'll call "789". (I know, I'm the creative type.)

After this my tree looked like this:

1 -- 2 -- 3 -- 4 <-- master
                \
                 5 -- 6 -- 789 <-- feature

The presence of 5 and 6 in the same branch confused me, but again, since they were already included in 7 (which is now in 789), I could just discard them.

So I git rebase -i master again and this time I discarded 5 and 6.

However, conflicts arose here and there, so I aborted the whole thing.

I am currently at that stage but my remote branch hasn't been updated, so I may as well do a reset to the original state.

Which are the right steps that will land me where I want to without having to manually resolve all merge conflicts?

Upvotes: 6

Views: 5967

Answers (2)

user456814
user456814

Reputation:

Alternative to Ajedi32's solution

Ajedi32's solution is definitely the best and simplest one so far, but I just wanted to show an alternative that works just about the same way, for the sake of completeness. The --squash option for git merge basically replicates the entire state of the final commit of the branch that you're merging in, since each commit represents an entire snapshot1.

You can achieve the same effect by simply replacing the currnt working copy directory tree with the working tree from the final commit in the branch being merged in (this may only work if the feature branch contains all of the master branches changes though, I'm not sure, I'll have to think about it some more). So you can simply do

git rm -rf -- .
git checkout feature -- .
git add . # Is this necessary?
git commit

# Verify that the current working copy state
# is identical to the feature branch's
git diff feature

1 Another way to think of it is as a sum of all the differences that lead from the tip of the base branch to the tip of the branch being merged.

Alternative solution 2 (this was my old answer)

Ok, so two things:

  1. I have a solution that should work for you.
  2. The explanation of why what you're currently doing isn't working could be wrong.

A solution? (will this actually work?)

We'll rebase your feature branch in parts. The --preserve-merges flag of git rebase works best when the parents of those merge commits don't include commits from the other branch that you're trying to rebase on top of, as I'm sure you've already seen.

For the sake of brevity, I'll leave out some explanations about what the syntax for the following commands, since you can look that up in the documentation:

  1. First, make a backup branch in case things go horribly, horribly wrong, and we end up needing to hard reset back to your feature branches original state:

    git branch backup feature
    
  2. Next, we'll squash 5 and 6, then cherry-pick them onto master (you could rebase them onto master too, but since we're just cherry-picking one commit, rebase is a little over-kill for this):

    git checkout -b temp 6
    git reset --soft HEAD^
    git commit -m "Enter your squash commit message here"
    

    Now that we've squashed 5 and 6 together, we'll want to cherry-pick it onto master. Since you mentioned that the merge commit 7 resolved conflicts, this means that cherry-picking the new squashed commit (let's call it S) will also result in conflicts. However, since you've already resolved those conflicts in 7, we should be able to just copy those resolutions over to the new cherry-picked commit (though I'm not 100% sure about this):

    git checkout master
    git cherry-pick S
    
    # If no conflicts, then great!
    # Otherwise, completely delete the conflicted working directory tree,
    # and replace it with the directory tree from commit 7 instead:
    git rm -rf -- .
    git checkout 7 -- .
    git add . # Is this necessary?
    git cherry-pick --continue
    
    # When the cherry-pick is done, compare master to 7 and
    # verify that there are no differences
    git diff 7
    
  3. Next, we'll rebase 8 and 9. We skip 7 because it just represents a synchronization event of master into the feature branch at the point of commit 6; however, because cherry-picking/rebasing 5 and 6 on top of master produces an equivalent final state as a merge would, that merge commit is no longer necessary.

    git rebase --onto master 7 feature
    
    # Squash 8 and 9 into S (which should be the tip of master).
    # You might have been able to do this in the above step too,
    # but I don't have time to double-check, so this will also work:
    git reset --soft master
    git commit --amend --no-edit
    
    # Once again, verify that the final result
    # matches that original of the feature branch:
    git diff backup
    

That should be it (I think). If it doesn't work, then you can always hard reset your feature branch back with

git reset --hard backup
git checkout master
git reset --hard 4

Explanation of why what you were doing before didn't work as expected

Okay, so a complete explanation is going to take me a while to write. If I have time later, I'll come back to this answer and write out all the details. However, before I go, I just want to point out that:

  • Rebase reapplies commits as patches. When you leave out a commit from a rebase, the changes introduced in that commit are lost from all subsequent descendant commits (they're not preserved in the descendants).

Obligatory links to documentation

Upvotes: 5

Ajedi32
Ajedi32

Reputation: 48328

I want to rebase and squash everything from feature so that I can advance master with a single commit of the feature being added.

This is exactly what the --squash option for git merge does. From the docs for git merge:

--squash

Produce the working tree and index state as if a real merge happened (except for the merge information), but do not actually make a commit or move the HEAD, nor record $GIT_DIR/MERGE_HEAD to cause the next git commit command to create a merge commit. This allows you to create a single commit on top of the current branch whose effect is the same as merging another branch (or more in case of an octopus).

(Emphasis mine)

Usage example:

git checkout master
git merge feature --squash
git commit

Upvotes: 7

Related Questions