4 votes

Pourquoi git cherry-pick produirait-il moins de conflits que git rebase ?

Je rebase souvent. De temps en temps, le rebase est particulièrement problématique (beaucoup de conflits de fusion) et ma solution dans de tels cas est de cherry-pick les commits individuels sur la branche master. Je fais cela parce que presque chaque fois que je le fais, le nombre de conflits est considérablement réduit.

Ma question est de savoir pourquoi ce serait le cas.

Pourquoi y a-t-il moins de conflits de fusion lorsque je cherry-pick que lorsque je rebase ?

Dans mon modèle mental, une rebase et un cherry-pick font la même chose.

Exemple de rebasement

A-B-C (master)
   \
    D-E (next)

git checkout next
git rebase master

produit

A-B-C (master)
     \
      D`-E` (next)

et ensuite

git checkout master
git merge next

produit

A-B-C-D`-E` (master)

Exemple de sélection

A-B-C (master)
   \
    D-E (next)

git checkout master 
git cherry-pick D E

produit

A-B-C-D`-E` (master)

D'après ce que j'ai compris, le résultat final est le même. (D et E sont maintenant sur master avec un historique de commit propre (en ligne droite)).

Pourquoi cette dernière méthode (cherry picking) produirait-elle moins de conflits de fusion que la première (rebasement) ?

ACTUALISER ACTUALISER ACTUALISER

J'ai finalement pu reproduire ce problème et je réalise maintenant que j'ai peut-être trop simplifié l'exemple ci-dessus. Voici comment j'ai pu le reproduire...

Disons que j'ai ce qui suit (remarquez la branche supplémentaire)

A-B-C (master)
   \
    D-E (next)
       \
        F-G (other-next)

Et ensuite je fais ce qui suit

git checkout next
git rebase master
git checkout master
git merge next

Je me retrouve avec ce qui suit

A-B-C-D`-E` (master)
   \ \
    \ D`-E` (next)
     \
      D-E
         \
          F-G (other-next)

A partir de là, je vais soit rebaser, soit sélectionner

Exemple de rebasage

git checkout other-next
git rebase master 

produit

A-B-C-D`-E`-F`-G` (master)

Exemple de "cherry picking

git checkout master
git cherry-pick F G

donne le même résultat

A-B-C-D`-E`-F`-G` (master)

mais avec beaucoup moins de conflits de fusion que la stratégie de rebasage.

Ayant finalement reproduit un exemple similaire, je pense que je comprends pourquoi il y avait plus de conflits de fusion avec le rebasement qu'avec le cherry picking, mais je laisse à quelqu'un d'autre (qui fera probablement un meilleur travail (et plus précis) que moi) le soin de répondre.

4voto

torek Points 25463

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.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X