web-dev-qa-db-fra.com

Différence entre 'rebase master' et 'rebase --onto master' d'une branche dérivée d'une branche de master

Étant donné la structure de branche suivante:

  *------*---*
Master        \
               *---*--*------*
               A       \
                        *-----*-----*
                        B         (HEAD)

Si je veux fusionner mes modifications B (et niquement mes modifications B, pas de modifications A) en maître, quelle est la différence entre ces deux ensembles de commandes?

>(B)      git rebase master
>(B)      git checkout master
>(master) git merge B

>(B)      git rebase --onto master A B
>(B)      git checkout master
>(master) git merge B

Je suis principalement intéressé à savoir si le code de la branche A pourrait devenir maître si j'utilise la première façon.

32
punkrockbuddyholly

Soyez indulgent avec moi pendant un moment avant de répondre à la question posée. L'une des réponses précédentes est juste, mais il y a des problèmes d'étiquetage et d'autres problèmes relativement mineurs (mais potentiellement déroutants), donc je veux commencer par les dessins de branche et les étiquettes de branche. De plus, les gens venant d'autres systèmes, ou peut-être même tout simplement nouveaux dans le contrôle des révisions et git, pensent souvent aux branches comme des "lignes de développement" plutôt que des "traces de l'histoire" (git les implémente comme ces dernières, plutôt que les premières, donc un commit n'est pas nécessairement sur une "ligne de développement" spécifique).

Tout d'abord, il y a un problème mineur avec la façon dont vous avez dessiné votre graphique:

  *------*---*
Master        \
               *---*--*------*
               A       \
                        *-----*-----*
                        B         (HEAD)

Voici exactement le même graphique, mais avec les étiquettes dessinées différemment et quelques têtes de flèche supplémentaires ajoutées (et j'ai numéroté les nœuds de validation à utiliser ci-dessous):

0 <- 1 <- 2         <-------------------- master
           \
            3 <- 4 <- 5 <- 6      <------ A
                       \
                        7 <- 8 <- 9   <-- HEAD=B

Ce qui importe, c'est que git est assez vague sur ce que signifie qu'un commit soit "sur" une branche - ou peut-être une meilleure expression est de dire qu'un commit est "contenu dans" un ensemble de branches. Les validations ne peuvent pas être déplacées ou modifiées, mais les étiquettes de branche peuvent et doivent se déplacer.

Plus précisément, un nom de branche comme master, A ou B pointe vers un commit spécifique . Dans ce cas, master points à valider 2, A points à valider 6 et B points à valider 9. Les premiers commits de 0 à 2 sont contenus dans les trois branches; les validations 3, 4 et 5 sont contenues à la fois dans A et B; commit 6 est contenu uniquement dans A; et les validations 7 à 9 sont contenues uniquement dans B. (Par ailleurs, plusieurs noms peuvent pointer vers le même commit, et c'est normal lorsque vous créez une nouvelle branche.)

Avant de continuer, permettez-moi de redessiner le graphique encore une façon:

0
 \
  1
   \
    2     <-- master
     \
      3 - 4 - 5
              |\
              | 6   <-- A
               \
                7
                 \
                  8
                   \
                    9   <-- HEAD=B       

Cela souligne simplement que ce n'est pas une ligne horizontale de commits qui compte, mais plutôt les relations parent/enfant. L'étiquette de branche pointe vers un commit de départ, puis (au moins la façon dont ces graphiques sont dessinés) nous nous déplaçons à gauche, peut-être aussi en montant ou en descendant comme nécessaire, pour trouver les validations parentales.


Lorsque vous rebase des validations, vous êtes en fait copier ces validations.

Git ne peut jamais changer aucun commit

Il y a un "vrai nom" pour chaque commit (ou en fait n'importe quel objet dans un dépôt git), qui est son SHA-1: cette chaîne de 40 chiffres hexadécimaux comme 9f317ce... Que vous voyez dans git log par exemple. Le SHA-1 est un cryptographique1 somme de contrôle du contenu de l'objet. Le contenu est l'auteur et l'auteur (nom et e-mail), les horodatages, une arborescence source et la liste des validations parentales. Le parent du commit # 7 est toujours le commit # 5. Si vous faites une copie presque exacte de la validation # 7, mais que vous définissez son parent sur la validation # 2 au lieu de la validation # 5, vous obtenez une validation différente avec un ID différent. (Je n'ai plus de chiffres à ce stade — normalement j'utilise des lettres majuscules simples pour représenter les ID de validation, mais avec des branches nommées A et B je pensais que ce serait déroutant. Je vais appeler une copie de # 7, # 7a, ci-dessous.)

Que fait git rebase

Lorsque vous demandez à git de rebaser une chaîne de validations - comme les validations # 7-8-9 ci-dessus - il doit les copier , du moins si elles vont se déplacer n'importe où (s'ils ne bougent pas, il suffit de laisser les originaux en place). Par défaut, il copie les validations de la branche actuellement extraite, donc git rebase N'a besoin que de deux informations supplémentaires:

  • Quels engagements doit-il copier?
  • Où les copies devraient-elles atterrir? Autrement dit, quel est l'ID parent cible pour le premier commit copié? (Les validations supplémentaires pointent simplement vers la première copie, la deuxième copie, etc.)

Lorsque vous exécutez git rebase <upstream>, Vous laissez git comprendre les deux parties à partir d'une seule information. Lorsque vous utilisez --onto, Vous pouvez expliquer à git séparément les deux parties: vous fournissez toujours un upstream mais il ne calcule pas la cible à partir de <upstream>, il calcule uniquement les commits pour copier à partir de <upstream>. (Par ailleurs, je pense que <upstream> N'est pas un bon nom, mais c'est ce que rebase utilise et je n'ai rien de mieux, alors restons-en ici. Rebase les appels target <newbase>, mais je pense que target est un bien meilleur nom.)

Jetons d'abord un œil à ces deux options. Les deux supposent que vous êtes sur la branche B en premier lieu:

  1. git rebase master
  2. git rebase --onto master A

Avec la première commande, l'argument <upstream> De rebase est master. Avec le second, c'est A.

Voici comment git calcule ce qui s'engage à copier: il remet la branche courante à git rev-list , et il remet également <upstream> À git rev-list, Mais en utilisant --not - ou plus précisément, avec l'équivalent de la notation à deux points exclude..include. Cela signifie que nous devons savoir comment git rev-list Fonctionne.

Alors que git rev-list Est extrêmement compliqué, la plupart des commandes git finissent par l'utiliser; c'est le moteur de git log, git bisect, rebase, filter-branch, etc. — ce cas particulier n'est pas trop difficile: avec la notation à deux points , rev-list Répertorie tous les commit accessibles depuis le côté droit (y compris le commit lui-même), à ​​l'exclusion de tous les commit accessibles depuis le côté gauche.

Dans ce cas, git rev-list HEAD Trouve toutes les validations accessibles depuis HEAD— c'est-à-dire presque toutes les validations: les validations 0-5 et 7-9 — et git rev-list master Trouve toutes les validations accessibles depuis master, qui est la validation #s 0, 1 et 2. La soustraction de 0 à 2 de 0 à 5, 7 et 9 laisse 3, 5, 7 et 9. Ce sont les candidats qui s'engagent à copier, comme indiqué par git rev-list master..HEAD.

Pour notre deuxième commande, nous avons A..HEAD Au lieu de master..HEAD, Donc les commits à soustraire sont 0-6. La validation # 6 n'apparaît pas dans l'ensemble HEAD, mais c'est bien: soustraire quelque chose qui n'est pas là, ne le laisse pas là. Le résultat à copier est donc 7-9.

Cela nous laisse encore à déterminer la cible du rebase, c'est-à-dire, où les terrains de commit copiés devraient-ils être copiés? Avec la deuxième commande, la réponse est "la validation identifiée par l'argument --onto". Puisque nous avons dit --onto master, Cela signifie que la cible est la validation # 2.

rebaser # 1

git rebase master

Cependant, avec la première commande, nous n'avons pas spécifié de cible directement, donc git utilise le commit identifié par <upstream>. Le <upstream> Que nous avons donné était master, ce qui indique la validation # 2, donc la cible est la validation # 2.

La première commande va donc commencer par copier le commit # 3 avec toutes les modifications minimales nécessaires pour que son parent soit le commit # 2. Son parent est déjà commit # 2. Rien ne doit changer, donc rien ne change, et rebaser ne fait que réutiliser le commit # 3 existant. Il doit ensuite copier # 4 pour que son parent soit # 3, mais le parent est déjà # 3, donc il réutilise juste # 4. De même, # 5 est déjà bon. Il ignore complètement # 6 (ce n'est pas dans l'ensemble des commits à copier); il vérifie les # 7-9 mais ils sont tous bons aussi, donc tout le rebase finit par réutiliser tous les commits originaux. Vous pouvez quand même forcer des copies avec -f, Mais vous ne l'avez pas fait, donc tout ce rebase finit par ne rien faire.

rebaser # 2

git rebase --onto master A

La deuxième commande de rebase a utilisé --onto Pour sélectionner # 2 comme cible, mais a dit à git de copier juste valide 7-9. Le parent du commit # 7 est le commit # 5, donc cette copie doit vraiment faire quelque chose.2 Donc git fait un nouveau commit - appelons cela # 7a - qui a le commit # 2 comme parent. Le rebase passe au commit # 8: la copie a maintenant besoin du # 7a comme parent. Enfin, le rebase passe à la validation # 9, qui a besoin de # 8a comme parent. Avec toutes les validations copiées, la dernière chose que rebase fait est de déplacer l'étiquette (rappelez-vous, les étiquettes se déplacent et changent!). Cela donne un graphique comme celui-ci:

          7a - 8a - 9a       <-- HEAD=B
         /
0 - 1 - 2                    <-- master
         \
          3 - 4 - 5 - 6      <-- A
                    \
                     7 - 8 - 9   [abandoned]

OK, mais qu'en est-il de git rebase --onto master A B?

C'est presque la même chose que git rebase --onto master A. La différence est que B supplémentaire à la fin. Heureusement, cette différence est très simple: si vous donnez à git rebase Cet argument supplémentaire, il exécute git checkout Dessus argument en premier.3

Vos commandes originales

Dans votre premier ensemble de commandes, vous avez exécuté git rebase master Sur la branche B. Comme indiqué ci-dessus, c'est un gros no-op: puisque rien n'a besoin de bouger, git ne copie rien du tout (sauf si vous utilisez -f/--force, Ce que vous n'avez pas fait). Vous avez ensuite extrait master et utilisé git merge B, Qui — s'il lui est demandé de4: Crée un nouveau commit avec la fusion. Par conséquent la réponse de Dherik , au moment où je l'ai vu au moins, est correct ici: le commit de fusion a deux parents, dont l'un est la pointe de la branche B, et cette branche revient en arrière à travers trois commits qui sont sur la branche A et donc certains de ce qui est sur A finit par être fusionné dans master.

Avec votre deuxième séquence de commandes, vous avez d'abord extrait B (vous étiez déjà sur B donc c'était redondant, mais faisait partie du git rebase). Vous avez ensuite rebasé la copie de trois validations, produisant le graphique final ci-dessus, avec les validations 7a, 8a et 9a. Vous avez ensuite extrait master et effectué une validation de fusion avec B (voir à nouveau la note de bas de page 4). Encore une fois, la réponse de Dherik est correcte: la seule chose qui manque est que les validations originales et abandonnées ne sont pas intégrées et il n'est pas aussi évident que les nouvelles validations fusionnées sont des copies.


1Cela n'a d'importance que dans la mesure où il est extrêmement difficile de cibler une somme de contrôle particulière. Autrement dit, si quelqu'un vous la confiance vous dit "Je fais confiance au commit avec l'ID 1234567 ...", c'est presque impossible pour quelqu'un d'autre - quelqu'un que vous pouvez pas trop confiance - pour trouver un commit qui a le même ID, mais qui a un contenu différent. Les chances que cela se produise par accident sont de 1 sur 2160, ce qui est beaucoup moins probable que vous ayez une crise cardiaque en étant frappé par la foudre en vous noyant dans un tsunami tout en étant enlevé par des extraterrestres. :-)

2La copie réelle est faite en utilisant l'équivalent de git cherry-pick : git compare l'arbre du commit avec l'arbre de son parent pour obtenir un diff, puis applique le diff à l'arbre du nouveau parent.

3Ceci est en fait, littéralement vrai à l'heure actuelle: git rebase Est un script Shell qui analyse vos options, puis décide du type de rebase interne à exécuter: le non interactif git-rebase--am Ou le interactif git-rebase--interactive. Une fois qu'il a compris tous les arguments, s'il reste le seul argument de nom de branche restant, le script fait git checkout <branch-name> Avant de démarrer le rebase interne.

4Puisque master pointe vers la validation 2 et la validation 2 est un ancêtre de la validation 9, cela ne ferait normalement pas de validation de fusion après tout, mais ferait plutôt ce que Git appelle un rapide- opération avant . Vous pouvez demander à Git de ne pas faire ces avances rapides en utilisant git merge --no-ff. Certaines interfaces, telles que l'interface Web de GitHub et peut-être certaines interfaces graphiques, peuvent séparer les différents types d'opérations, de sorte que leur "fusion" force une véritable fusion comme celle-ci.

Avec une fusion rapide, le graphique final pour le premier cas est:

0 <- 1 <- 2         [master used to be here]
           \
            3 <- 4 <- 5 <- 6      <------ A
                       \
                        7 <- 8 <- 9   <-- master, HEAD=B

Dans les deux cas, les validations 1 à 9 sont désormais sur les deux branches , master et B. La différence par rapport à la véritable fusion est que, à partir du graphique, vous pouvez voir l'historique qui inclut la fusion.

En d'autres termes, l'avantage d'une fusion à avance rapide est qu'elle ne laisse aucune trace de ce qui est par ailleurs une opération triviale. L'inconvénient d'une fusion à avance rapide est, bien, qu'elle ne laisse aucune trace. Donc, la question de savoir s'il faut autoriser l'avance rapide est vraiment une question de savoir si vous voulez laisser une fusion explicite dans l'historique formé par les commits.

97
torek

Avant l'une des opérations données, votre référentiel ressemble à ceci

           o---o---o---o---o  master
                \
                 x---x---x---x---x  A
                                  \
                                   o---o---o  B

Après un rebase standard (sans --onto master) la structure sera:

           o---o---o---o---o  master
               |            \
               |             x'--x'--x'--x'--x'--o'--o'--o'  B
                \
                 x---x---x---x---x  A

...où le x' sont des validations de la branche A. (Notez comment ils sont maintenant dupliqués à la base de la branche B.)

Au lieu de cela, un rebase avec --onto master créera la structure plus simple et plus propre suivante:

           o---o---o---o---o  master
               |            \
               |             o'--o'--o'  B
                \
                 x---x---x---x---x  A
16
Johannes Thorn

Les différences:

Premier set

  • (B) git rebase master

    *---*---* [master]
             \
              *---*---*---* [A]
                       \
                        *---*---* [B](HEAD)
    

Rien ne s'est passé. Il n'y a eu aucun nouveau commit dans la branche master depuis la création de la branche B.

  • (B) git checkout master

    *---*---* [master](HEAD)
             \
              *---*---*---* [A]
                       \
                        *---*---* [B]
    
  • (Maître) git merge B

    *---*---*-----------------------* [Master](HEAD)
             \                     /
              *---*---*---* [A]   /
                       \         /
                        *---*---* [B]
    

Deuxième set

  • (B) git rebase --onto master A B

    *---*---*-- [master]
            |\
            | *---*---*---* [A]
            |
            *---*---* [B](HEAD)
    
  • (B) git checkout master

    *---*---*-- [master](HEAD)
            |\
            | *---*---*---* [A]
            |
            *---*---* [B]
    
  • (Maître) git merge B

    *---*---*----------------------* [master](HEAD)
            |\                    /
            | *---*---*---* [A]  /
            |                   /  
            *---*--------------* [B]
    

Je veux fusionner mes modifications B (et seulement mes modifications B, pas de modifications A) en maître

Faites attention à ce que vous comprenez pour "seulement mes changements B".

Dans le premier ensemble, la branche B est (avant la fusion finale):

 *---*---*
          \
           *---*---*
                    \
                     *---*---* [B]

Et dans le deuxième ensemble, votre branche B est:

*---*---*
        |
        |
        |
        *---*---* [B]

Si je comprends bien, ce que vous voulez, c'est seulement les commits B qui ne sont pas dans la branche A. Donc, le deuxième ensemble est le bon choix pour vous avant la fusion.

11
Dherik

git log --graph --decorate --oneline A B master (ou un outil GUI équivalent) peut être utilisé après chaque commande git pour visualiser les changements.

Il s'agit de l'état initial du référentiel, avec B comme branche actuelle.

(B) git log --graph --oneline --decorate A B master
* 5a84c72 (A) C6
| * 9a90b7c (HEAD -> B) C9
| * 2968483 C8
| * 187c9c8 C7
|/  
* 769014a C5
* 6b8147c C4
* 9166c60 C3
* 0aaf90b (master) C2
* 8c46dcd C1
* 4d74b57 C0

Voici un script pour créer un référentiel dans cet état.

#!/bin/bash

commit () {
    for i in $(seq $1 $2); do
        echo article $i > $i
        git add $i
        git commit -m C$i
    done
}

git init
commit 0 2

git checkout -b A
commit 3 6

git checkout -b B HEAD~
commit 7 9

La première commande de rebase ne fait rien.

(B) git rebase master
Current branch B is up to date.

Extraire master et fusionner B pointe simplement master sur le même commit que B, (c'est-à-dire 9a90b7c). Aucun nouveau commit n'est créé.

(B) git checkout master
Switched to branch 'master'

(master) git merge B
Updating 0aaf90b..9a90b7c
Fast-forward
<... snipped diffstat ...>

(master) git log --graph --oneline --decorate A B master
* 5a84c72 (A) C6
| * 9a90b7c (HEAD -> master, B) C9
| * 2968483 C8
| * 187c9c8 C7
|/  
* 769014a C5
* 6b8147c C4
* 9166c60 C3
* 0aaf90b C2
* 8c46dcd C1
* 4d74b57 C0

La deuxième commande rebase copie les validations dans la plage A..B et les pointe sur master. Les trois validations de cette plage sont 9a90b7c C9, 2968483 C8, and 187c9c8 C7. Les copies sont de nouveaux validations avec leurs propres ID de validation; 7c0e241, 40b105d, et 5b0bda1. Les branches master et A sont inchangées.

(B) git rebase --onto master A B
First, rewinding head to replay your work on top of it...
Applying: C7
Applying: C8
Applying: C9

(B) log --graph --oneline --decorate A B master
* 7c0e241 (HEAD -> B) C9
* 40b105d C8
* 5b0bda1 C7
| * 5a84c72 (A) C6
| * 769014a C5
| * 6b8147c C4
| * 9166c60 C3
|/  
* 0aaf90b (master) C2
* 8c46dcd C1
* 4d74b57 C0

Comme précédemment, extraire master et fusionner B pointe simplement master sur le même commit que B, (c'est-à-dire 7c0e241). Aucun nouveau commit n'est créé.

La chaîne de validations d'origine que B pointait existe toujours.

git log --graph --oneline --decorate A B master 9a90b7c
* 7c0e241 (HEAD -> master, B) C9
* 40b105d C8
* 5b0bda1 C7
| * 5a84c72 (A) C6
| | * 9a90b7c C9    <- NOTE: This is what B used to be
| | * 2968483 C8
| | * 187c9c8 C7
| |/  
| * 769014a C5
| * 6b8147c C4
| * 9166c60 C3
|/  
* 0aaf90b C2
* 8c46dcd C1
* 4d74b57 C0
2
sigjuice

Vous pouvez l'essayer vous-même et voir. Vous pouvez créer un référentiel git local pour jouer avec:

#! /bin/bash
set -e
mkdir repo
cd repo

git init
touch file
git add file
git commit -m 'init'

echo a > file0
git add file0
git commit -m 'added a to file'

git checkout -b A
echo b >> fileA
git add fileA
git commit -m 'b added to file'
echo c >> fileA
git add fileA
git commit -m 'c added to file'

git checkout -b B
echo x >> fileB
git add fileB
git commit -m 'x added to file'
echo y >> fileB
git add fileB
git commit -m 'y added to file'
cd ..

git clone repo rebase
cd rebase
git checkout master
git checkout A
git checkout B
git rebase master
cd ..

git clone repo onto
cd onto
git checkout master
git checkout A
git checkout B
git rebase --onto master A B
cd ..

diff <(cd rebase; git log --graph --all) <(cd onto; git log --graph --all)
1
choroba