schoetbi
schoetbi

Reputation: 12846

Git rebase -i --preserve-merges loses changes

I have the follwing situation:

$ git --version
git version 2.7.3.windows.1

$ git log --graph --oneline
* 83e3254 version 1.1
*   34188af merge of feature into master
|\
| * 784ba31 awesome change
|/
* 6eec273 added file1
* 84d80a5 added version file

To reproduce this in a new directory

rm -rf .git
git init
echo version 1.0 > version.txt
git add version.txt
git commit -m "added version file"

echo file1 > file1
git add file1
git commit -m "added file1"

git checkout -b feature
echo awesome change >> file1 && git commit -am "awesome change"

git checkout master
git merge --no-ff --no-commit feature
echo "small fixup" >> file1
git commit -am "merge of feature into master"

echo version 1.1 > version.txt
git commit -am "version 1.1"

Now I saw this feature was meant for version 1.1. So I did this:

git rebase --preserve-merges -i master~3

with this as git-rebase-todo

pick 6eec273 added file1
pick 83e3254 version 1.1
pick 784ba31 awesome change
pick 34188af merge of feature into master

and got this:

$ git log --graph --oneline
*   34188af merge of feature into master
|\
| * 784ba31 awesome change
|/
* 6eec273 added file1
* 84d80a5 added version file

what happened to 83e3254? Have I missed something? Should I use 34188af in the todofile when I need to preserve merges?

Upvotes: 4

Views: 1294

Answers (2)

torek
torek

Reputation: 487725

Melebius' answer is correct (and upvoted); it has to do with the way interactive rebase tries to replay the commits as a series of cherry-picks. Since a merge is not actually a cherry-pick, the preserve-merge mode uses a secret side channel1 to figure out how to re-perform merges, and this side channel depends on the commit ordering.

There is, however, another problem with re-performing a merge. The merge is redone by running git merge. Here is your original merge:

git checkout master
git merge --no-ff --no-commit feature
echo "small fixup" >> file1             # DANGER WILL ROBINSON
git commit -am "merge of feature into master"

(note my added remark). This "small fixup" is not part of the two inputs: it was added manually via the --no-commit feature and, well, manual manipulation. This is sometimes called an "evil merge":

When git rebase -p "replays" the merge, it does so without --no-commit, and then automatically moves on to the next linearized commit. So any changes deliberately introduced like this will be lost, even if you preserve the commit ordering and hence get an otherwise correct rebased history.

This is documented—see the quoted text, which says:

Merge conflict resolutions or manual amendments to merge commits are not preserved.

—but in my opinion this sentence needs to be in bold blinking 72-point font or something. :-)


1It's not really very secret at all: simply view the interactive rebase code. Using your file viewer or editor, look at $(git --exec-path)/git-rebase--interactive. Search for the string "preserv" (as in preserve or preserving) and observe the function pick_one_preserving_merges and the code in pick_one that calls it if a directory named $rewritten exists. The usage of the $rewritten directory is rather complex, though (it applies to both merge-preserving and non-merge-preserving interactive rebases and relies on the fact that without the --preserve or -p option, the initial "todo" list contains no actual merges and therefore simply flattens them away).

Upvotes: 5

Melebius
Melebius

Reputation: 6695

This is probably related with the documented imperfectness of the rebase mechanism. From the git rebase documentation:

-p
--preserve-merges

Recreate merge commits instead of flattening the history by replaying commits a merge commit introduces. Merge conflict resolutions or manual amendments to merge commits are not preserved.

This uses the --interactive machinery internally, but combining it with the --interactive option explicitly is generally not a good idea unless you know what you are doing (see BUGS below).

The -i and --interactive options are synonymous.

Let’s see the BUGS section:

The todo list presented by --preserve-merges --interactive does not represent the topology of the revision graph. Editing commits and rewording their commit messages should work fine, but attempts to reorder commits tend to produce counterintuitive results.

For example, an attempt to rearrange

1 --- 2 --- 3 --- 4 --- 5

to

1 --- 2 --- 4 --- 3 --- 5

by moving the "pick 4" line will result in the following history:

        3
       /
1 --- 2 --- 4 --- 5

Upvotes: 5

Related Questions