Ciò è dovuto alla limitazione dell'algoritmo originale. Quando si gestiscono i merge-commit, l'algoritmo originale utilizza criteri semplificati per tagliare i genitori non correlati. In particolare, controlla se c'è un genitore che ha lo stesso albero. Se un tale genitore lo trova, comprimerà il commit di unione e utilizzerà invece il commit genitore, assumendo che altri genitori abbiano modifiche non correlate al sottoalbero. In alcuni casi, ciò comporterebbe l'eliminazione di parti della cronologia, che presenta modifiche effettive al sottoalbero. In particolare, eliminerebbe sequenze di commit, che toccherebbero un sottoalbero, ma restituirebbero lo stesso valore del sottoalbero.
Vediamo un esempio (che puoi facilmente riprodurre) per capire meglio come funziona. Considera la seguente cronologia (il formato della riga è: 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 questo esempio, ci stiamo dividendo dir
. Esegue il commit D
e E
ha lo stesso albero z
, perché abbiamo il commit C
, che ha annullato il commit B
, quindi la B-C
sequenza non fa nulla dir
anche se ha delle modifiche.
Ora facciamo la divisione. Per prima cosa ci siamo separati su 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
Successivamente ci siamo separati su commit E
.
% git log `git subtree split -P dir E` ...
* D' [z'] add dir/file2.txt
* A' [w'] add dir/file1.txt
Sì, abbiamo perso due commit. Ciò si traduce nell'errore quando si tenta di spingere la seconda divisione, poiché non ha quei due commit, che sono già entrati nell'origine.
Di solito è possibile tollerare questo errore utilizzando push --force
, poiché i commit eliminati generalmente non contengono informazioni critiche. A lungo termine, il bug deve essere risolto, quindi la cronologia divisa avrebbe effettivamente tutti i commit, che si toccano dir
, come previsto. Mi aspetto che la correzione includa un'analisi più approfondita dei commit padre per le dipendenze nascoste.
Per riferimento, ecco la porzione di codice originale, responsabile del comportamento.
copy_or_skip()
...
for parent in $newparents; do
ptree=$(toptree_for_commit $parent) || exit $?
[ -z "$ptree" ] && continue
if [ "$ptree" = "$tree" ]; then
identical="$parent"
else
nonidentical="$parent"
fi
...
if [ -n "$identical" ]; then
echo $identical
else
copy_commit $rev $tree "$p" || exit $?
fi