Réponse mise à jour (voir mise à jour dans la question)
Je pense que ce qui se passe ici a à voir avec choisir les commits à copier .
Notons, et ensuite mettons de côté, le fait que git rebase
peut utiliser soit git cherry-pick
ou git format-patch
y git am
pour copier certains commits. Dans la plupart des cas git cherry-pick
y git am
devrait permettre d'obtenir les mêmes résultats. (Le git rebase
documentation indique spécifiquement que les renommages de fichiers en amont sont un problème pour la méthode cherry-pick, par rapport à la méthode par défaut. git am
-Méthode basée sur les données pour la refonte non interactive. Voir aussi les diverses remarques entre parenthèses dans la réponse originale ci-dessous, et les commentaires).
La principale chose à considérer ici est quels commits doivent être copiés . Dans la méthode manuelle, vous copiez d'abord manuellement les commits D
y E
a D'
y E'
puis vous copiez manuellement F
y G
a F'
y G'
. C'est la quantité minimale de travail à faire et c'est exactement ce que nous voulons ; le seul inconvénient ici est tout le travail manuel d'identification des commit que nous devons faire.
Lorsque vous utilisez la commande :
git checkout <branch> && git rebase <upstream>
vous faites en sorte que Git automatise le processus de recherche des commits à copier. C'est très bien quand Git fait bien les choses, mais pas si Git fait mal les choses.
Alors comment fait Git a choisi ces commits ? La réponse simple, mais quelque peu erronée, se trouve dans cette phrase (tirée de la même documentation) :
Toutes les modifications apportées par les commits de la branche courante mais qui ne sont pas dans <upstream> sont enregistrées dans une zone temporaire. C'est le même ensemble de commits qui serait montré par git log <upstream>..HEAD
; ou par git log 'fork_point'..HEAD
si --fork-point
est actif (voir la description sur --fork-point
ci-dessous) ; ou par git log HEAD
si le --root
est spécifiée.
Le site --fork-point
est quelque peu nouvelle, depuis git 2.quelque chose, mais elle n'est pas "active" dans ce cas parce que vous avez spécifié un fichier <upstream>
et n'a pas précisé --fork-point
. L'effectif <upstream>
es master
les deux fois.
Maintenant, si vous avez vraiment exécuter chaque git log
(avec --oneline
pour le rendre plus agréable) :
git checkout next && git log --oneline master..HEAD
et :
git checkout other-next && git log --oneline master..HEAD
vous verrez que la première liste liste les commits D
y E
-excellent!-mais le second énumère D
, E
, F
y G
. Uh oh, D
y E
se produisent deux fois !
Le truc, c'est que parfois travaux. Eh bien, j'ai dit "quelque peu erroné" ci-dessus. Voici ce qui le rend faux, juste deux paragraphes plus bas de la citation précédente :
Notez que tous les commits dans HEAD qui introduisent les mêmes changements textuels qu'un commit dans HEAD..<upstream> sont omis (c'est-à-dire qu'un patch déjà accepté en amont avec un message de commit ou un timestamp différent sera ignoré).
Notez que HEAD..<upstream>
voici l'inverse de la <upstream>..HEAD
dans le git log
les commandes que nous venons de faire, où nous avons vu D
-à travers- G
.
Pour le premièrement rebase, il n'y a pas de commits dans git log HEAD..master
Il n'y a donc pas de commits qui pourraient être ignorés. C'est une bonne chose, parce qu'il n'y a pas de commits à sauter : nous sommes en train de copier E
y F
a E'
y F'
et c'est exactement ce que nous voulons.
Pour le deuxième rebase, qui se produit après que le premier rebase soit fait, git log HEAD..master
vous montrera les commits E'
y F'
: les deux copies que nous venons de faire. Ce sont potentiellement sautées : elles sont candidats à envisager de sauter .
"Potentiellement sauté" n'est pas "réellement sauté".
Alors comment fait Git décide des commits qu'il doit vraiment Sauter ? La réponse est dans git patch-id
bien qu'il soit en fait implémenté directement dans git rev-list
qui est une commande très sophistiquée et compliquée. Ni l'une ni l'autre ne le décrit vraiment bien, en partie parce qu'il est difficile à décrire. Voici quand même ma tentative :-)
Ce que Git fait ici est de regarder les différences, après avoir retiré les numéros de ligne d'identification, au cas où les correctifs se trouvent à des endroits légèrement différents (en raison de correctifs précédents déplaçant des lignes vers le haut ou vers le bas dans les fichiers). Il utilise les mêmes astuces que pour les fichiers - transformer le contenu des fichiers en hachages uniques - pour transformer chaque commit en un "patch ID". Le site ID de commission est un hash unique qui identifie un commit spécifique, et toujours ce même commit spécifique. Le site ID du patch est un ID de hachage différent (mais toujours unique pour un certain contenu) qui identifie toujours "le même" patch, c'est-à-dire quelque chose qui supprime et ajoute les mêmes diff-hunks, même s'il les supprime et les ajoute à partir d'emplacements différents.
Après avoir calculé un patch ID pour chaque commit, Git peut alors dire : "Aha, commit D
et commettre D'
ont le même patch-ID ! Je devrais éviter de copier D
parce que D'
est probablement le résultat d'une copie D
." Il peut faire de même pour E
vs E'
. Ce site souvent fonctionne - mais il échoue pour D
lorsque la copie de D
a D'
a nécessité une intervention manuelle (résolution des conflits de fusion), et il échoue de même pour E
lorsque la copie de E
a E'
a nécessité une intervention manuelle.
Un rebasement plus intelligent
Ce qui est nécessaire ici est une sorte de "rebase intelligent" qui peut regarder une série de branches et calculer, à l'avance, quels commits copier, une fois, pour toutes les branches à rebaser. Ensuite, une fois que toutes les copies sont faites, ce "smart rebase" ajuste tous les noms des branches.
Dans ce cas particulier, la copie D
par le biais de G
-c'est en fait assez facile, et vous pouvez le faire manuellement avec :
$ git checkout -q other-next && git rebase master
[here rebase copies D, E, F, and G, perhaps with your assistance]
suivi par :
$ git checkout next
[here git checks out "next", so that HEAD is ref: refs/heads/next
and refs/heads/next points to original commit E]
$ git reset --hard other-next~2
Cela fonctionne parce que other-next
l'engagement des noms G'
dont le parent est F'
dont le parent est à son tour E'
et c'est là que nous voulons next
au point. Puisque HEAD
se réfère à la branche next
, git reset
ajuste refs/heads/next
de pointer pour commettre E'
et nous avons terminé.
Dans des cas plus complexes, les commits qui doivent être copiés-exactement-une fois ne sont pas tous linéaires :
A1-A2-A3 <-- featureA
/
...--o--o--o--o--o--o--o <-- master
\
*--*--B3-B4-B5 <-- featureB
\
C3-C4 <-- featureC
Si nous voulons "multi-rebase" les trois fonctionnalités, nous pouvons rebasement featureA
indépendamment des deux autres - aucun des trois A
dépendent de quelque chose de "non-master" autre que les commandes précédentes. A
mais pour copier les cinq B
et les quatre C
nous devons copier les deux *
commits qui sont les deux B
et C
mais ne les copier qu'une seule fois, et ensuite copier les trois et deux commits restants (respectivement) sur la pointe du commit copié.
(Il serait être possible d'écrire un tel "rebase intelligent", mais l'intégrer correctement dans Git, de sorte que git status
le comprend vraiment, est considérablement plus difficile).
Réponse originale
J'aimerais bien voir un exemple reproductible. Dans la plupart des cas, votre modèle "in-head" devrait fonctionner. Il y a cependant un cas spécial connu.
Un site interactive rebasement, ou ajout -m
o --merge
à l'ordinaire git rebase
en fait fait utiliser git cherry-pick
tandis que le rebasement non interactif par défaut utilise la fonction git format-patch
y git am
à la place. Ce dernier n'est pas aussi bon pour la détection des renommages. En particulier, s'il y a un renommage de fichier dans l'amont, 1 l'interactivité ou --merge
On peut s'attendre à ce que rebase se comporte différemment (généralement, mieux).
(Notez également que les deux types de rebasement - à la fois la version orientée patch et la version basée sur le cherry-pick - sauteront les commits qui sont git patch-id
-identiques aux commits déjà présents dans l'amont, par l'intermédiaire de git rev-list --left-only --cherry-pick HEAD...<upstream>
ou équivalent. Voir la documentation pour git rev-list
en particulier la section sur --cherry-mark
y --left-right
ce qui, je pense, rend les choses plus compréhensibles. Cela devrait être le même pour les deux types de rebasement, cependant ; si vous faites un cherry-picking manuel, ce sera à vous de décider si vous faites cela).
1 Plus précisément, git diff --find-renames
doit croire il y a un changement de nom. Habituellement, il le croit s'il y en a un, mais comme c'est en détectant en comparant les arbres, ce n'est pas parfait.