Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

git rebase "--preserve-merges --onto" doesn't preserve merges

Tags:

git

rebase

Using git v1.7.1 I'm trying to do a rebase with both the --preserve-merges and --onto features at the same time. The end results seems to be without the merge commits, and so appears linear. I'd rather preserve the merge commits, for the same reason that people would often use --preserve-merges (easier to see the group of commits that was logically a separate feature and developed in its own branch).

My master branch (the destination for the rebase) is boring:

A-B-C

The feature branch I want to take from has a sub-feature branch that has been merged into it. Like:

     X - Y
   /      \
V-W ------ Z

Where Z is a merge commit that is the head of the feature branch to take from, and X and Y were on a sub-feature branch.

I'm using: git rebase --preserve-merges --onto C V Z

I'd like to end up with:

         X - Y
        /     \
A-B-C-W ------ Z

But instead I'm getting:

A-B-C-W-X-Y

Since Z was a conflict-free merge, the final state of the code is correct, but the history isn't as expressive as I would like.

Is there a way to get what I want?

edit to address @Bombe: I've written a bash script to construct my example. On my system (RHEL 6.2 with git 1.7.1) this demonstrates my problem.

#! /bin/bash
# start a new empty repo
git init
# make some commits on the master branch
git checkout master
touch A.txt; git add A.txt; git commit -m "add A.txt"; git tag Atag
touch B.txt; git add B.txt; git commit -m "add B.txt"; git tag Btag
touch C.txt; git add C.txt; git commit -m "add C.txt"; git tag Ctag
# now build the feature branch
# start at Btag (more or less arbitrary; point is it's before C)
git checkout Btag
git checkout -b feature
touch V.txt; git add V.txt; git commit -m "add V.txt"; git tag Vtag
touch W.txt; git add W.txt; git commit -m "add W.txt"; git tag Wtag
# now a subfeature
git checkout -b subfeature
touch X.txt; git add X.txt; git commit -m "add X.txt"; git tag Xtag
touch Y.txt; git add Y.txt; git commit -m "add Y.txt"; git tag Ytag
# merge the subfeature into the feature
# preserves branch history with --no-ff
git checkout feature
git merge --no-ff subfeature
# the merge commit is our Z
git tag Ztag
# one more commit so that merge isn't the tip (for better illustration of Z missing later)
touch postZ.txt; git add postZ.txt; git commit -m "add postZ.txt"; git tag postZtag
# now do the rebase
git rebase --preserve-merges --onto Ctag Vtag
# optionally move the master branch forward to the top of feature branch
git checkout master
git merge feature

Before the rebase I get:

        X-Y
       /   \
    V-W-----Z-postZ
   / 
A-B-C

After the rebase I get:

        X-Y
       /   \
    V-W-----Z-postZ
   / 
A-B-C-W'-X'-Y'-postZ'

Note the lack of Z' between Y' and postZ'.

like image 713
RaveTheTadpole Avatar asked Mar 01 '12 17:03

RaveTheTadpole


People also ask

Does rebase preserving merge commits?

Merge-preserving rebase is willing to replay (some) merge commits, whereas normal rebase completely ignores merge commits.

What is the benefit to rebase on merge?

The Rebase Option But, instead of using a merge commit, rebasing re-writes the project history by creating brand new commits for each commit in the original branch. The major benefit of rebasing is that you get a much cleaner project history. First, it eliminates the unnecessary merge commits required by git merge .

Does rebase makes merge history messy?

Interactive rebasing gives you complete control over what your project history looks like. This affords a lot of freedom to developers, as it lets them commit a "messy" history while they're focused on writing code, then go back and clean it up after the fact.


Video Answer


2 Answers

Many thanks to Bombe and an offline friend for pointing out that is does work for some people. With that inspiration, I am now able to answer my own question.

Short answer: git versions before 1.7.5.2 will exhibit this bad behavior.

Long answer: in git's own source repo, commit c192f9c865dbdae48c0400d717581d34cd315fb8 on Apr 28th 2011 was explicitly a fix for this problem.

To quote the commit message (by Andrew Wong):

git-rebase--interactive.sh: preserve-merges fails on merges created with no-ff

'git rebase' uses 'git merge' to preserve merges (-p).  This preserves
the original merge commit correctly, except when the original merge
commit was created by 'git merge --no-ff'.  In this case, 'git rebase'
will fail to preserve the merge, because during 'git rebase', 'git
merge' will simply fast-forward and skip the commit.  For example:

               B
              / \
             A---M
            /
    ---o---O---P---Q

If we try to rebase M onto P, we lose the merge commit and this happens:

                 A---B
                /
    ---o---O---P---Q

To correct this, we simply do a "no fast-forward" on all merge commits
when rebasing.  Since by the time we decided to do a 'git merge' inside
'git rebase', it means there was a merge originally, so 'git merge'
should always create a merge commit regardless of what the merge
branches look like. This way, when rebase M onto P from the above
example, we get:

                   B
                  / \
                 A---M
                /
    ---o---O---P---Q

Solution: get a new version of git, building from source if needed.

Btw, I used git bisect to figure this out. Awesome tool.

like image 79
RaveTheTadpole Avatar answered Oct 17 '22 06:10

RaveTheTadpole


I came across this problem. Note my git version, it is 1.7.10.2.

I am doing a rebase of a commit range (identified by its SHA1 hashes) onto a branch and also lacking the last merge commit.

My solution was to rebase W to X onto C (without --preserve-merges) and afterwards rebase (with --preserve-merges) Y, Z and postZ onto X'.

Hope this helps.

like image 32
s.froehlich Avatar answered Oct 17 '22 06:10

s.froehlich