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'.
Merge-preserving rebase is willing to replay (some) merge commits, whereas normal rebase completely ignores merge commits.
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 .
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With