web-dev-qa-db-fra.com

Qu'est-ce que git "rebase - preserve-merges" fait exactement (et pourquoi?)

Git's la documentation de la commande rebase est assez bref:

--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 lorsque vous utilisez --preserve-merges? En quoi diffère-t-il du comportement par défaut (sans cet indicateur)? Que signifie "recréer" une fusion, etc.

319
Chris

Comme avec un rebase git normal, git avec --preserve-merges identifie d'abord une liste de validations effectuées dans une partie du graphique de validation, puis rejoue ces validations par-dessus une autre partie. Les différences avec --preserve-merges concernent les commandes validées pour la relecture et le mode de fonctionnement de cette relecture pour les validations de fusion.

Pour être plus explicite sur les principales différences entre une base normale et une base préservant la fusion:

  • La base préservant la fusion accepte de rejouer (certains) les commits de fusion, alors que la base normale ignore complètement les commits de fusion.
  • Parce qu'il est prêt à rejouer les validations de fusion, le référentiel préservant la fusion doit définir ce que signifie la répétition d'un commit de fusion et la gestion de quelques rides supplémentaires
    • Sur le plan conceptuel, la partie la plus intéressante consiste peut-être à choisir ce que devraient être les parents fusionnés du nouvel engagement.
    • La relecture des commits de fusion nécessite également de vérifier explicitement des commits particuliers (git checkout <desired first parent>), alors qu'une rebase normale n'a pas à s'inquiéter de cela.
  • La base préservant la fusion considère un ensemble de commits moins profonds pour la relecture:
    • En particulier, il ne considérera que la relecture des commits effectués depuis la ou les bases de fusion les plus récentes - c'est-à-dire le moment le plus récent pendant lequel les deux branches ont divergé - , alors que rebase normal peut rejouer les commits en revenant à la première fois où les deux branches ont divergé.
    • Pour être provisoire et peu clair, je pense que c’est finalement un moyen de passer au crible les "anciens commits" déjà incorporés dans un "commit de fusion".

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

L'algorithme en "bref"

Si vous voulez vraiment vous attaquer aux mauvaises herbes, téléchargez le code source git et explorez le fichier git-rebase--interactive.sh. (Rebase ne fait pas partie du noyau C de Git, mais est écrit en bash. Et, dans les coulisses, il partage le code avec "rebase interactive".)

Mais ici, je vais dessiner ce que je pense être l’essence. Afin de réduire le nombre de sujets de réflexion, j'ai pris quelques libertés. (Par exemple, je n'essaie pas de capturer avec une précision de 100% l'ordre précis dans lequel les calculs sont effectués et d'ignorer certains sujets moins centraux, par exemple, que faire des commits déjà sélectionnés entre branches).

Tout d’abord, notons qu’un rebase ne conservant pas la fusion est plutôt 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.

La base --preserve-merges est relativement compliquée. 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")

La base avec un argument --onto C devrait être très similaire. Au lieu de démarrer la lecture à partir du HEAD de B, vous lancez la lecture à partir du HEAD de C. (Et utilisez C_new au lieu de B_new.)

Exemple 1

Par exemple, prenons un graphique de validation

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

m est un engagement de fusion avec les parents E et G.

Supposons que nous ayons rebasé le sujet (H) au-dessus du maître (C) en utilisant une base normale, ne préservant pas la fusion. (Par exemple, sujet à la caisse; maître de base .) Dans ce cas, git sélectionnerait les validations suivantes pour la réexécution:

  • choisir D
  • choisissez E
  • choisir F
  • choisir G
  • choisir H

puis mettez à jour le graphique de validation comme suit:

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

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

Notez que la validation de fusion m n'est pas sélectionnée pour la réexécution.

Si nous faisons plutôt une --preserve-merges _ base sur H en haut de C. (Par exemple, checkout topic; rebase --preserve-merges master .) Dans ce nouveau cas, git sélectionnerait les commits suivants pour la relecture:

  • choisir D
  • choisissez E
  • choisissez F (sur D 'dans la branche' sous-rubrique ')
  • choisissez G (sur F 'dans la branche' sous-rubrique ')
  • choisir le sujet secondaire de la branche de fusion dans le sujet
  • choisir H

Maintenant m a été choisi pour la lecture . Notez également que les parents de fusion E et G ont été choisis pour être inclus avant l'engagement de fusion m.

Voici le graphe de commit résultant:

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

Encore une fois, D 'est une version choisie de D. (identique à celle recréée). Idem pour E', etc. Chaque commit qui n'est pas sur le maître a été rejoué. E et G (les parents fusionnés de m) ont été recréés en tant que E 'et G' pour servir de parents de m '(après rebase, l'historique de l'arbre reste identique).

Exemple 2

Contrairement à une base normale, une base conservant la fusion peut créer plusieurs enfants de la tête en amont.

Par exemple, considérons:

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

Si nous rebasons H (sujet) au-dessus de C (maître), les commits choisis pour rebase sont:

  • choisir D
  • choisissez E
  • choisir F
  • choisir G
  • choisir m
  • choisir H

Et le résultat est comme suit:

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

Exemple

Dans les exemples ci-dessus, à la fois le commit de fusion et ses deux parents sont rejoués, plutôt que les parents d'origine que le commit de fusion d'origine a. Cependant, dans d'autres rediffusions, une validation de fusion rejouée peut se retrouver avec des parents déjà présents dans le graphique de validation avant la fusion.

Par exemple, considérons:

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

Si nous re-basons le sujet sur le maître (en préservant les fusions), alors les commits à rejouer seront

  • choisir la fusion m
  • choisir F

Le graphe de commit réécrit va ressembler à ceci:

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

Ici, les commits de fusion rejoués obtiennent les parents qui existaient déjà dans le graphe de commit, à savoir D (le HEAD du maître) et E (l'un des parents du commit de fusion original m).

Exemple 4

Une rebase préservant la fusion peut être confuse dans certains cas de "commit vide". Au moins, cela n’est vrai que pour certaines versions plus anciennes de git (par exemple 1.7.8.)

Prenez ce graphique de commit:

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

Notez que commit m1 et m2 doivent avoir incorporé tous les changements de B et F.

Si nous essayons de faire git rebase --preserve-merges de H (sujet) sur D (maître), les commits suivants sont choisis pour la réexécution:

  • choisir m1
  • choisir H

Notez que les modifications (B, F) réunies dans m1 doivent déjà être intégrées à D. (Ces modifications doivent déjà être intégrées à m2, car m2 fusionnant les enfants de B et F.) Par conséquent, conceptuellement, rejouer m1 au-dessus de D devrait probablement soit être un no-op, soit créer un commit vide (c'est-à-dire où le diff entre les révisions successives est vide).

Cependant, à la place, git peut refuser la tentative de rejouer m1 au-dessus de D. Vous pouvez obtenir une erreur comme celle-ci:

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

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

433
Chris

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

"git rebase" a appris "--rebase-merges" à transplanter la topologie entière du graphe de validation ailleurs .

(Remarque: Git 2.22, prévu pour le T2 2019, en réalité ) déconseille --preserve-merge )

Voir commit 25cff9f , commit 7543f6f , commit 1131ec9 , commit 7ccdf65 , commit 537e7d6 , commit a9be29c , commit 8f6aed7 , commit 1644c7 , commit d1e8b01 , commit 4c68e7d , commit 9055e4 , commit cb5206e , commit a01c2a5 , commit 2f6b1d1 , commit bf5c057 (25 avril 2018) par Johannes Schindelin (dscho) .
Voir commit f431d7 (25 avril 2018) par Stefan Beller (stefanbeller) .
Voir commettre 2429335 (25 avril 2018) par Phillip Wood (phillipwood) .
(Fusionnée par Junio ​​C Hamano - gitster - dans commit 2c18e6a , 23 mai 2018)

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

Semblable au mode preserve en passant simplement l'option --preserve-merges à la commande rebase, le mode merges transmet simplement l'option --rebase-merges.

Cela permettra aux utilisateurs de baser facilement des topologies de validation non triviales lors de l'extraction de nouveaux commits, sans les aplatir.


La page de manuel git rebase contient désormais une section complète dédiée à la modification de l'historique avec les fusions .

Extrait:

Il existe des raisons légitimes pour lesquelles un développeur peut souhaiter recréer des commits de fusion: conserver la structure de branche (ou "topologie de validation") lors de l'utilisation de plusieurs branches inter-reliées.

Dans l'exemple suivant, le développeur travaille sur une branche de sujet refacturant la manière dont les boutons sont définis et sur une autre branche de sujet qui utilise ce refactoring pour implémenter un bouton "Signaler un bogue".
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 peut souhaiter redéfinir ces commits sur une nouvelle master tout en conservant la topologie de la branche, par exemple lorsque la première branche de sujet doit être intégrée à master beaucoup plus tôt que la seconde, par exemple. résolvez les conflits de fusion avec les modifications apportées à la classe DownloadButton qui l'a transformée en master.

Cette refonte peut être effectuée à l'aide de l'option --rebase-merges.


Voir commit 1644c7 pour un petit exemple:

rebase-helper--make-script: introduire un drapeau pour rebaser les fusions

Le séquenceur vient d'apprendre de nouvelles commandes destinées à recréer une structure de branche ( similaire dans l'esprit à --preserve-merges, mais avec une conception sensiblement moins brisée ).

Laissons le rebase--helper générer des listes de tâches utilisant ces commandes, déclenchées par la nouvelle option --rebase-merges.
Pour une topologie de validation telle que celle-ci (où le HEAD pointe vers C):

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

la liste de tâches 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?
Commit 8f6aed7 explique:

Il était une fois ce développeur qui pensait ici: ne serait-il pas agréable si, par exemple, les correctifs de Git pour Windows au-dessus du noyau Git pouvaient être représentés sous la forme d'un bosquet de branches et être rebasés au-dessus du noyau Git afin de: maintenir un ensemble sélectionnable de séries de patchs?

La tentative initiale de répondre à cette question était la suivante: git rebase --preserve-merges.

Cependant, cette expérience n’a jamais été conçue comme une option interactive et elle ne s’est greffée que sur git rebase --interactive, car l’implémentation de cette commande semblait déjà très très familière: elle a été conçue par la même personne que celle qui a conçu --preserve-merges: Votre sincèrement.

Et par "tien", l'auteur se réfère à lui-même: Johannes Schindelin (dscho), qui est la raison principale (avec quelques autres héros - Hannes, Steffen, Sebastian, ...) que nous avons Git pour Windows (même si retour dans la journée - 2009 - ce n'était pas facile ).
Il travaille maintenant chez Microsoft, car Microsoft utilise maintenant beaucoup Git et a besoin de ses services.
That La tendance a commencé en 2013 avec TFS . Depuis lors, Microsoft gère le plus grand référentiel Git sur la planète ! Et, depuis octobre 2018, Microsoft a acquis GitHub .

Vous pouvez voir Johannes parle dans cette vidéo pour Git Merge 2018 en avril 2018.

Quelque temps plus tard, un autre développeur (je vous regarde, Andreas! ;-)) a décidé qu'il serait judicieux de permettre à --preserve-merges d'être combiné à --interactive (avec des mises en garde!) Et le responsable de Git (c'est-à-dire le responsable intérimaire de Git en l'absence de Junio) a accepté, et c'est à ce moment-là que le charme du design --preserve-merges a commencé à s'effondrer assez rapidement et sans glamour.

Ici Jonathan parle de Andreas Schwab de Suse.
Vous pouvez voir certaines des leurs discussions en 2012 .

La raison? En mode --preserve-merges, les parents d'un commit de fusion (ou d'ailleurs, de aucun commit) n'étaient pas indiqués explicitement, mais étaient impliqués par le nom de validation transmis à la commande pick.

Cela rendait impossible, par exemple, de réorganiser les commits .
Sans parler de déplacer commet entre branches ou, divinité interdite, de scinder les branches de sujet en deux.

Hélas, ces lacunes ont également empêché ce mode (dont le but initial était de répondre aux besoins de Git pour Windows, tout en espérant qu'il pourrait également être utile à d'autres), de répondre aux besoins de Git pour Windows.

Cinq ans plus tard, quand il devint vraiment intenable d’avoir dans Git pour Windows une série importante de correctifs compliqués, partiellement corrélés et partiellement non corrélés, qui était de temps à autre rebasée sur les balises du noyau de Git (méritant la colère injustifiée du développeur) de la série git-remote-hg malheureuse qui a d'abord rendu obsolète l'approche concurrente de Git pour Windows, mais qui a été abandonnée sans maintenance plus tard) était vraiment intenable, le " cisaille de jardin Git " sont nées : un script, superposant au-dessus de la base interactive, déterminant d'abord la topologie de branche des patchs à rebaser, crée une pseudo liste de tâches à éditer, transforme le résultat en une vraie liste de tâches (en faisant un usage intensif de la commande exec pour "implémenter" les commandes de liste de tâches manquantes) et enfin recréer la série de correctifs au-dessus du nouveau commit de base.

(Le script Git Garden Shears est référencé dans ce correctif dans commit 9055e4 )

C'était en 2013.
Et il a fallu environ trois semaines pour concevoir le projet et le mettre en œuvre en tant que script hors arbre. Inutile de dire que la mise en œuvre a pris plusieurs années à se stabiliser, le design s’est avéré solide.

Avec ce patch, la bonté de la cisaille à jardin Git revient à git rebase -i lui-même .
Le passage de l'option --rebase-merges générera une liste de tâches pouvant être facilement comprise, et indiquant clairement comment réorganiser les commits .
De nouvelles branches peuvent être introduites en insérant les commandes label et en appelant merge <label>.
Et une fois que ce mode sera devenu stable et universellement accepté, nous pourrons déconseiller l'erreur de conception qui était --preserve-merges.


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

L'option "--exec" de "git rebase --rebase-merges" a placé les commandes exec à des emplacements incorrects, ce qui a été corrigé.

Voir commit 1ace63b (09 août 2018), et commit f0880f7 (06 août 2018) par Johannes Schindelin (dscho) .
(Fusion par Junio ​​C Hamano - gitster - dans commit 750eb11 , 20 août 2018)

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

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

Depuis l'introduction de fixup!/squash! commits, cette idée a été étendue à "pick, éventuellement suivie d'une chaîne de correction/squash", c'est-à-dire qu'un exec ne serait pas inséré entre un pick et n'importe laquelle de ses lignes fixup ou squash correspondantes.

L’implémentation actuelle utilise un truc sale pour y parvenir: elle suppose qu’il n’existe que des commandes pick/fixup/squash, puis insère les lignes exec avant tout pick mais le premier et ajoute un dernier.

Avec les listes de tâches générées par git rebase --rebase-merges, cette implémentation simple montre ses problèmes: elle produit exactement la mauvaise chose quand il y a des commandes label, reset et merge.

Modifions l’implémentation pour que nous fassions exactement ce que nous voulons: , recherchez les lignes pick, ignorez les chaînes fixup/squash, puis insérez la ligne exec. Faire mousser, rincer, répéter.

Remarque: nous nous efforçons d’insérer avant les lignes de commentaire autant que possible, car les commits vides sont représentés par des lignes de sélection commentées (et nous voulons insérer la ligne précédente d’une ligne de commande précédente avant une telle ligne, pas après).

Pendant que vous y êtes, ajoutez également exec lignes après les commandes merge, car leur esprit est semblable à celui de pick: elles ajoutent de nouveaux commits.


Git 2.22 (T2 2019) corrige l'utilisation de la hiérarchie refs/rewritten/pour stocker les états intermédiaires d'une base, ce qui crée de manière inhérente la hiérarchie par arbre de travail.

Voir commit b9317d5 , commit 90d31ff , commit 09e6564 (07 mars 2019) par Nguyễn Thái Ngc Duy (pclouds ) .
(Fusionnée par Junio ​​C Hamano - gitster - dans commit 917f2cd , 9 avril 2019)

Assurez-vous que refs/rewritten/est par arbre de travail

a9be29c (séquenceur: références ref générées par la commande label worktree-local, 2018-04-25, Git 2.19) ajoute refs/rewritten/ en tant qu'espace de référence par arbre de travail.
Malheureusement, certains endroits doivent être mis à jour pour s’assurer qu’il s’agit vraiment d’un arbre à l’autre.

- add_per_worktree_entries_to_dir() est mis à jour pour s'assurer que la liste de références examine bien l'arborescence par utilisateur refs/rewritten/ au lieu de celle par référent.

  • common_list[] est mis à jour pour que git_path() renvoie l'emplacement correct. Cela inclut "rev-parse --git-path".

Ce désordre est créé par moi.
J'ai commencé à essayer de résoudre ce problème en introduisant refs/worktree, où toutes les références seront exécutées par arbre de travail sans traitements spéciaux.
Les références/réécriture malheureuses sont venues avant les références/worktree, c’est tout ce que nous pouvons faire.

54
VonC