Que fait exactement le "rebase-preserve-merges" de git (et pourquoi?)

Git's la documentation pour la rebase commande est assez brève:

--preserve-merges
    Instead of ignoring merges, try to recreate them.

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).

alors que se passe-t-il quand vous utilisez --preserve-merges ? Comment est-il différent du comportement par défaut (sans drapeau)? Que signifie "recréer" une fusion, etc.

281
demandé sur Lernkurve 2013-04-10 05:29:50

2 réponses

comme pour une git rebase normale, git avec --preserve-merges identifie d'abord une liste de commits faite dans une partie du Graphe de commit, puis rejoue ces commits sur une autre partie. Les différences avec --preserve-merges concernent quelles propagations sont sélectionnées pour rejouer et comment cela fonctionne pour les propagations de fusion.

Pour être plus explicite sur les principales différences entre le normal et de fusion de la préservation de rebase:

  • Merge-preserving rebase est prêt à rejouer (certains) merge commits, alors que normal rebase ignore complètement merge commits.
  • parce qu'il est prêt à rejouer merge commits, merge-préserver rebase doit définir ce qu'il signifie pour rejouer une commit merge, et faire face à quelques rides supplémentaires
    • la partie la plus intéressante, sur le plan conceptuel, est peut-être de choisir ce que devraient être les parents fusionnants du nouveau commit.
    • replaying merge commits aussi Require explicitly checking out particular commits ( git checkout <desired first parent> ), alors que le rebase normal n'a pas à s'inquiéter de cela.
  • Fusion de la préservation de rebase considère profond ensemble de commits pour la relecture:
    • en particulier, il ne considérera que rejouer les propagations faites depuis la plus récente base de fusion(s) - c.-à-d. la la plus récente temps les deux branches divergeed--, alors que normal rebase pourrait rejouer commits remontant à la première les deux branches divergeaient.
    • pour être provisoire et imprécis, je crois qu'il s'agit en fin de compte d'un moyen d'éliminer la rediffusion de" vieilles commits "qui ont déjà été" incorporées dans " une commit de fusion.

D'abord je vais essayer de décrire "suffisamment exactement" ce que rebase --preserve-merges fait, et puis il y aura quelques exemples. On peut bien sûr commencer par les exemples, si cela semble plus utile.

L'algorithme dans "bref "

si vous voulez vraiment entrer dans les mauvaises herbes, téléchargez la source git et d'explorer le fichier git-rebase--interactive.sh . (Rebase ne fait pas partie du noyau C de Git, mais est plutôt écrit en bash. Et, dans les coulisses, il partage le code avec "interactive rebase".)

mais ici je vais esquisse de ce que je pense est l'essence. Afin de réduire le nombre de choses à penser, j'ai pris quelques libertés. (par exemple, je n'essaie pas de saisir avec une précision de 100% l'ordre précis dans lequel les calculs ont lieu, et d'ignorer certains sujets moins centraux, par exemple ce qu'il faut faire à propos des propagations qui ont déjà été sélectionnées entre les branches).

tout d'abord, Notez qu'un rebase ne préservant pas la fusion est assez simple. C'est plus ou moins:

Find all commits on B but not on A ("git log A..B")
Reset B to A ("git reset --hard A") 
Replay all those commits onto B one at a time in order.

Rebase --preserve-merges est relativement compliqué. Voici aussi simple que j'ai pu le faire sans perdre des choses qui semblent assez importantes:

Find the commits to replay:
  First find the merge-base(s) of A and B (i.e. the most recent common ancestor(s))
    This (these) merge base(s) will serve as a root/boundary for the rebase.
    In particular, we'll take its (their) descendants and replay them on top of new parents
  Now we can define C, the set of commits to replay. In particular, it's those commits:
    1) reachable from B but not A (as in a normal rebase), and ALSO
    2) descendants of the merge base(s)
  If we ignore cherry-picks and other cleverness preserve-merges does, it's more or less:
    git log A..B --not $(git merge-base --all A B)
Replay the commits:
  Create a branch B_new, on which to replay our commits.
  Switch to B_new (i.e. "git checkout B_new")
  Proceeding parents-before-children (--topo-order), replay each commit c in C on top of B_new:
    If it's a non-merge commit, cherry-pick as usual (i.e. "git cherry-pick c")
    Otherwise it's a merge commit, and we'll construct an "equivalent" merge commit c':
      To create a merge commit, its parents must exist and we must know what they are.
      So first, figure out which parents to use for c', by reference to the parents of c:
        For each parent p_i in parents_of(c):
          If p_i is one of the merge bases mentioned above:
            # p_i is one of the "boundary commits" that we no longer want to use as parents
            For the new commit's ith parent (p_i'), use the HEAD of B_new.
          Else if p_i is one of the commits being rewritten (i.e. if p_i is in R):
            # Note: Because we're moving parents-before-children, a rewritten version
            # of p_i must already exist. So reuse it:
            For the new commit's ith parent (p_i'), use the rewritten version of p_i.
          Otherwise:
            # p_i is one of the commits that's *not* slated for rewrite. So don't rewrite it
            For the new commit's ith parent (p_i'), use p_i, i.e. the old commit's ith parent.
      Second, actually create the new commit c':
        Go to p_1'. (i.e. "git checkout p_1'", p_1' being the "first parent" we want for our new commit)
        Merge in the other parent(s):
          For a typical two-parent merge, it's just "git merge p_2'".
          For an octopus merge, it's "git merge p_2' p_3' p_4' ...".
        Switch (i.e. "git reset") B_new to the current commit (i.e. HEAD), if it's not already there
  Change the label B to apply to this new branch, rather than the old one. (i.e. "git reset --hard B")

Rebase avec un argument --onto C devrait être très similaire. Juste au lieu de commencer la lecture de commit à la tête de B, vous commencez la lecture de commit à la tête de C. (Et utilisez C_new au lieu de B_new.)

exemple 1

par exemple, prendre commit graph

  B---C <-- master
 /                     
A-------D------E----m----H <-- topic
         \         /
          F-------G

m est une fusion s'engager avec les parents E et G.

supposons que nous rebasons le sujet (H) au-dessus du maître (C) en utilisant un normal, non-conservation de fusion rebase. (Par exemple, thème de paiement; maître de rebase .) Dans ce cas, git sélectionnez la suite s'engage pour la relecture:

  • de sélection D
  • choisir E
  • pick F
  • pick G
  • pick H

et puis mettre à jour le graphe commit comme suit:

  B---C <-- master
 /     \                
A       D'---E'---F'---G'---H' <-- topic

(D' est l'équivalent rejoué de D, etc..)

notez que merge commit m n'est pas sélectionné pour rejouer.

si nous avons plutôt fait un --preserve-merges rebase de H au dessus de C. (par exemple, thème de caisse; rebase --préserver-fusionne maître .) Dans ce nouveau cas, git sélectionnerait les commits suivants pour rejouer:

  • de sélection D
  • pick E
  • pick F (sur la D "dans la" sous-rubrique " succursale)
  • pick G (sur F "dans la" sous-rubrique " succursale)
  • pick Fusion de la branche des "sous-rubrique" dans la rubrique
  • pick H

Maintenant m était choisi pour rejouer. Notez également que les parents fusionnant E et G ont été choisi pour inclusion avant de fusionner.

voici le graphe de propagation résultant:

 B---C <-- master
/     \                
A      D'-----E'----m'----H' <-- topic
        \          / 
         F'-------G'

encore une fois, D' est une version de d (recréée) cueillie à la cerise.idem pour E', etc.. Tout ce qui n'est pas sur Maître a été rejoué. E et G (les parents fusionnants de m) ont été recréés comme E 'et G' pour servir de parents de m' (après rebase, l'arbre l'histoire est toujours la même chose).

exemple 2

contrairement à la normale rebase, la sauvegarde de la fusion rebase peut créer plusieurs les enfants de la tête en amont.

par exemple, considérer:

  B---C <-- master
 /                     
A-------D------E---m----H <-- topic
 \                 |
  ------- F-----G--/ 

si nous rebasons H (topic) sur C (master), alors les commits choisis pour rebase sont:

  • de sélection D
  • choisir E
  • pick F
  • pick G
  • pick m
  • pick H

Et le résultat est comme suit:

  B---C  <-- master
 /    | \                
A     |  D'----E'---m'----H' <-- topic
       \            |
         F'----G'---/

exemple 3

dans les exemples ci-dessus, la commit de fusion et ses deux parents sont rejoués commits, plutôt que les parents originaux que la commit de fusion originale ont. Cependant, dans d'autres rebases un commit de fusion rejoué peut finir avec des parents qui étaient déjà dans le graphe de commit avant la fusion.

par exemple, considérer:

  B--C---D <-- master
 /    \                
A---E--m------F <-- topic

si nous rebasons le sujet sur master (préserver les fusions), alors les commits à rejouer seront

  • pick fusion commettre m
  • pick F

le graphique réécrit de commit ressemblera à ceci:

                     B--C--D <-- master
                    /       \             
                   A-----E---m'--F'; <-- topic

Ici relus fusion commettre m' obtient les parents qui pré-existait dans le commit graphique, à savoir D (la TÊTE de master) et E (l'un des parents de la fusion d'origine commettre m).

exemple 4

le rebase qui préserve la fusion peut être confondu dans certains cas de" propagation vide". Au moins ceci n'est vrai que pour certaines versions plus anciennes de git (par exemple 1.7.8.)

prenez ce graphique d'engagement:

                   A--------B-----C-----m2---D <-- master
                    \        \         /
                      E--- F--\--G----/
                            \  \
                             ---m1--H <--topic

il est à Noter que s'engager m1 et m2 devrait inclure toutes les modifications à partir de B et F.

si nous essayons de faire git rebase --preserve-merges de H (topic) sur d (master), alors les commits suivants sont choisis pour rejouer:

  • pick m1
  • pick H

noter que les modifications (B, F) united in m1 doivent déjà être incorporées dans D. (Ces modifications par conséquent, sur le plan conceptuel, rejouer m1 au-dessus de D devrait probablement être soit un no-op, soit créer une commit vide (c'est-à-dire une commit où la différence entre les révisions successives est vide).

au lieu de cela, cependant, git peut rejeter la tentative de rejouer m1 sur le dessus de D. Vous pouvez obtenir une erreur comme ceci:

error: Commit 90caf85 is a merge but no -m option was given.
fatal: cherry-pick failed

on dirait qu'on a oublié de passer un drapeau à git, mais le le problème sous-jacent est que git n'aime pas créer des propagations vides.

392
répondu Chris 2017-03-28 10:58:07

Git 2.18 (T2 2018) améliorera considérablement l'option --preserve-merge en ajoutant une nouvelle option.

" git rebase "appris" --rebase-merges " greffe de l'ensemble de la topologie du graphique de commit ailleurs .

Voir commettre 25cff9f , commettre 7543f6f , commettre 1131ec9 , commettre 7ccdf65 , commit 537e7d6 , commettre a9be29c , commettre 8f6aed7 , commettre 1644c73 , commettre d1e8b01 , commettre 4c68e7d , commettre 9055e40 , commettre cb5206e , commettre a01c2a5 , commettre 2f6b1d1 , commettre bf5c057 (25 Avril 2018) par Johannes Schindelin ( dscho ) .

Voir commettre f431d73 (25 Avril 2018) par Stefan Beller ( stefanbeller ) .

Voir commettre 2429335 (25 Avril 2018) par Phillip Bois ( phillipwood ) .

(fusionné par Junio C. Hamano -- gitster -- in commit 2c18e6a , 23 mai 2018)

pull : accepter --rebase=merges pour recréer la topologie de la branche

similaire au mode preserve passant simplement le --preserve-merges option à la commande rebase , le mode merges passe simplement le --rebase-merges option.

cela permettra aux utilisateurs de rebaser commodément non-trivial commit topologie en tirant de nouveaux engage, sans les aplatir.


git rebase man page a maintenant une section complète consacrée à rebaser l'histoire avec des fusions .

extrait:

il y a des raisons légitimes pour lesquelles un développeur peut vouloir recreate merge commits: pour conserver la structure de la branche (ou " commit topologie") en cas de travail sur plusieurs, inter-branches apparentées.

Dans l'exemple suivant, le développeur travaille sur un sujet branche refacteurs la façon dont les boutons sont définis, et sur une autre branche de sujet qui utilise ce remaniement pour implémenter un bouton" Report a bug".

La sortie de git log --graph --format=%s -5 peut ressembler à ceci:

*   Merge branch 'report-a-bug'
|\
| * Add the feedback button
* | Merge branch 'refactor-button'
|\ \
| |/
| * Use the Button class for all buttons
| * Extract a generic Button class from the DownloadButton one

le développeur pourrait vouloir rebaser ces commits à un nouveau master tout en gardant la branche topologie, par exemple quand le premier sujet la branche devrait être intégré dans master beaucoup plus tôt que les deuxièmement, disons, Pour résoudre les conflits de fusion avec des changements à la DownloadButton classe qui en fait master .

ce rebase peut être effectué en utilisant l'option --rebase-merges .


voir commit 1644c73 pour un petit exemple:

rebase-helper --make-script : introduire un drapeau pour rebase merges

le séquenceur vient d'apprendre de nouvelles commandes destinées à recréer la branche structure ( semblable dans l'esprit à --preserve-merges , mais avec un modèle sensiblement moins brisé ).

permettons au rebase--helper de générer des listes todo en utilisant ces commandes, déclenchées par la nouvelle option --rebase-merges .

Pour une topologie de comitologie comme celle-ci (où la tête pointe vers C):

- A - B - C (HEAD)
    \   /
      D

la liste de todo générée ressemblerait à ceci:

# branch D
pick 0123 A
label branch-point
pick 1234 D
label D

reset branch-point
pick 2345 B
merge -C 3456 D # C

Quelle est la différence avec --preserve-merge ?

Commettre 8f6aed7 , explique:

il était une fois, ce développeur a pensé: ne serait-il pas agréable si, dire, Git pour Windows correctifs sur core Git pourrait être représenté comme un fourré de branches, et être rebasé sur le dessus du noyau Git afin de maintenir une série de patch?

L'original tenter de répondre à cette a été: git rebase --preserve-merges .

cependant, cette expérience n'a jamais été conçue comme une option interactive, et il ne jumelée sur git rebase --interactive parce que mise en œuvre du commandement regardait déjà très, très bien: c'était conçu par la même personne qui a conçu --preserve-merges : votre serviteur.

et par "votre serviteur", l'auteur se réfère à lui-même: Johannes Schindelin ( dscho ) , qui est la principale raison (avec quelques autres héros -- Hannes, Steffen, Sebastian,...) que nous avons Git pour Windows (même si dans le temps -- 2009 -- ce n'était pas facile ).

Il travaille maintenant chez Microsoft, puisque Microsoft gère le plus grand dépôt Git sur la planète !

vous pouvez voir Johannes parler dans cette vidéo pour git Merge 2018 en avril 2018.

quelque temps plus tard, un autre développeur (je te regarde, Andreas! ;-)) a décidé que ce serait une bonne idée pour permettre de --preserve-merges être combiné avec --interactive (avec des mises en garde!) et le responsable de Git (Eh bien, le responsable provisoire de Git pendant L'absence de Junio, c'est-à-dire) d'accord, et c'est là que le glamour de la --preserve-merges conception a commencé à s'effondrer assez rapidement et sans glamour.

Jonathan parle de Andreas Schwab de Suse.

Vous pouvez voir certains de leur discussions retour en 2012 .

la raison? en mode --preserve-merges , les parents d'une fusion commentent (ou d'ailleurs, sur tout commit) n'ont pas été mentionnés explicitement, mais ont été implicite par le nom de commit passé à la pick commande .

cela rendait impossible, par exemple, la réordination des .

Pour ne pas mentionner de déplacer s'engage entre les branches ou, Deity forbid, de diviser les branches du sujet en deux.

hélas, ces lacunes ont également empêché ce mode (dont l'origine le but était de servir Git pour les besoins de Windows, avec l'espoir supplémentaire qu'il peut être utile à d'autres) de servir de Git pour Windows besoin.

Cinq ans plus tard, quand il est devenu vraiment intenable d'avoir un lourd, Big hodge-podge patch série de patches partiellement apparentés, partiellement non apparentés dans Git pour Windows qui a été rebasé sur les tags de Git du noyau de temps à le temps (gagner la colère imméritée du développeur de l'infortuné git-remote-hg série qui Premier git obsolète pour Windows' en concurrence approche, seulement pour être abandonnées sans mainteneur plus tard) était vraiment intenable, le " Git jardin cisailles " sont nés : un script, piggy-Back sur le rebase interactif, qui serait d'abord déterminer la topologie des branches des patches à rebâtir, créer un pseudo liste todo pour une édition ultérieure, transformez le résultat en un réel liste des choses à faire (en utilisant fortement la commande exec pour" mettre en œuvre " la il manque les commandes todo list) et finalement recréer la série patch sur haut de la nouvelle base de commettre.

(Le Git de jardin cisailles script est référencé dans le patch commettre 9055e40 )

C'était en 2013.

Et il a fallu environ trois semaines pour trouver le design et le mettre en œuvre comme un script hors arbre. Il va sans dire que la mise en œuvre a nécessité quelques années pour se stabiliser, alors que la conception elle-même s'est révélée saine.

avec ce patch, la bonté des ciseaux de jardin Git vient à git rebase -i elle-même .

passer l'option --rebase-merges générera une liste de choses à faire qui peuvent être comprises facilement, et où il est évident comment réorganiser engage .

De nouvelles branches peuvent être introduites en insérant des commandes label et en appelant merge <label> .

et une fois que ce mode sera devenu stable et universellement accepté, nous pouvons déprécier l'erreur de conception c'était --preserve-merges .


Git 2.19 (Q3 2018) améliore la nouvelle option --rebase-merges en la faisant fonctionner avec --exec .

le" --exec "option de " git rebase --rebase-merges " placé l'exec commandes au mauvais endroits, ce qui a été corrigé.

Voir commettre 1ace63b (09 Août 2018), et commettre f0880f7 (06 Août 2018) par Johannes Schindelin ( dscho ) .

(fusionné par Junio C. Hamano -- gitster -- dans commit 750eb11 , 20 août 2018)

rebase --exec : faites-le fonctionner avec --rebase-merges

L'idée de --exec est d'ajouter un exec appel après chaque pick .

depuis l'introduction de fixup! /s quash! commits, cette idée a été étendue pour appliquer à "pick, éventuellement suivi d'un fixup / chaîne de squash", c.-à-d. un exec ne serait pas insérée entre un pick et l'un de ses correspondants fixup ou squash .

l'implémentation actuelle utilise un sale truc pour y parvenir: il suppose qu'il y a seulement choisir/correction/squash commandes, puis inserts le "1519400920 lignes" en avant toute pick , mais le premier, et ajoute une finale.

avec les listes todo générées par git rebase --rebase-merges , ce la mise en œuvre simple montre ses problèmes: il produit le tort exact chose quand il y a des commandes label , reset et merge .

changeons l'implémentation pour faire exactement ce que nous voulons: cherchez pick , sautez les chaînes fixup / squash, puis insérez le exec la ligne . Mouillez, rincez, répétez.

Note: Nous nous efforçons d'insérer avant chaque fois que possible, comme vide s'engage sont représentés par commenté de choisir les lignes (et nous vous voulez insérer une ligne exec de pick précédent avant une telle ligne, pas par la suite).

pendant qu'il, ajouter également exec lignes après merge commandes, parce qu'ils sont similaires dans l'esprit aux commandes pick : ils ajoutent de nouvelles commits.

14
répondu VonC 2018-08-21 06:45:08