This is because of the limitation of original algorithm. When handling merge-commits, the original algorithm uses a simplified criteria for cutting off unrelated parents. In particular, it checks, if there is a parent, which has the same tree. If such a parent found, it would collapse the merge commit and use the parent commit instead, assuming that other parents have changes unrelated to the sub-tree. In some cases this would result in dropping parts of history, which has actual changes to the sub-tree. In particular it would drop sequences of commits, which would touch a sub-tree, but result in the same sub-tree value.
Lets see an example (which you can easily reproduce) to better understand how this works. Consider the following history (the line format is: commit [tree] subject):
% git log --graph --decorate --pretty=oneline --pretty="%h [%t] %s"
* E [z] Merge branch 'master' into side-branch
|\
| * D [z] add dir/file2.txt
* | C [y] Revert "change dir/file1.txt"
* | B [x] change dir/file1.txt
|/
* A [w] add dir/file1.txt
In this example, we are splitting on dir
. Commits D
and E
have the same tree z
, because we have commit C
, which undone commit B
, so B-C
sequence does nothing for dir
even though it has changes to it.
Now lets do splitting. First we split on commit C
.
% git log `git subtree split -P dir C` ...
* C' [y'] Revert "change dir/file1.txt"
* B' [x'] change dir/file1.txt
* A' [w'] add dir/file1.txt
Next we split on commit E
.
% git log `git subtree split -P dir E` ...
* D' [z'] add dir/file2.txt
* A' [w'] add dir/file1.txt
Yes, we lost two commits. This results in the error when trying to push the second split, since it doesn't have those two commits, which already got into the origin.
Usually you can tolerate this error by using push --force
, since dropped commits generally won't have critical information in them. In the long term, the bug needs to be fixed, so the split history would actually have all commits, which touch dir
, as expected. I would expect the fix to include a deeper analysis of parent commits for hidden dependencies.
For reference, here is the portion of original code, responsible for the behavior.
copy_or_skip()
...
for parent in $newparents; do
ptree=$(toptree_for_commit $parent) || exit $?
[ -z "$ptree" ] && continue
if [ "$ptree" = "$tree" ]; then
# an identical parent could be used in place of this rev.
identical="$parent"
else
nonidentical="$parent"
fi
...
if [ -n "$identical" ]; then
echo $identical
else
copy_commit $rev $tree "$p" || exit $?
fi