Les commits Git sont dupliqués dans la même branche après avoir effectué un rebase

131

Je comprends le scénario présenté dans Pro Git sur les dangers du rebasage . L'auteur vous indique essentiellement comment éviter les commits dupliqués:

Ne rebasez pas les commits que vous avez poussés vers un référentiel public.

Je vais vous parler de ma situation particulière car je pense que cela ne correspond pas exactement au scénario Pro Git et je me retrouve toujours avec des commits dupliqués.

Disons que j'ai deux succursales distantes avec leurs homologues locaux:

origin/master    origin/dev
|                |
master           dev

Les quatre branches contiennent les mêmes commits et je vais commencer le développement dans dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4
dev           : C1 C2 C3 C4

Après quelques validations, je pousse les changements à origin/dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4 C5 C6  # (2) git push
dev           : C1 C2 C3 C4 C5 C6  # (1) git checkout dev, git commit

Je dois revenir à masterpour faire une solution rapide:

origin/master : C1 C2 C3 C4 C7  # (2) git push
master        : C1 C2 C3 C4 C7  # (1) git checkout master, git commit

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C5 C6

Et revenons à devje rebase les modifications pour inclure la solution rapide dans mon développement actuel:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C7 C5' C6'  # git checkout dev, git rebase master

Si j'affiche l'historique des commits avec GitX / gitk, je remarque qu'il origin/devcontient maintenant deux commits identiques C5'et C6'qui sont différents de Git. Maintenant, si je pousse les changements, origin/devvoici le résultat:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6 C7 C5' C6'  # git push
dev           : C1 C2 C3 C4 C7 C5' C6'

Peut-être que je ne comprends pas parfaitement l'explication dans Pro Git, alors j'aimerais savoir deux choses:

  1. Pourquoi Git duplique-t-il ces commits lors du rebasage? Y a-t-il une raison particulière de faire cela au lieu de simplement postuler C5et C6après C7?
  2. Comment puis-je éviter cela? Serait-il sage de le faire?
élitalon
la source

Réponses:

87

Vous ne devriez pas utiliser le rebase ici, une simple fusion suffira. Le livre Pro Git que vous avez lié explique essentiellement cette situation exacte. Le fonctionnement interne peut être légèrement différent, mais voici comment je le visualise:

  • C5et C6sont temporairement retirés dedev
  • C7 est appliqué à dev
  • C5et C6sont lus par dessus C7, créant de nouveaux diffs et donc de nouveaux commits

Donc, dans votre devbranche, C5et C6effectivement n'existent plus: ils sont maintenant C5'et C6'. Lorsque vous poussez vers origin/dev, git voit C5'et en C6'tant que nouveaux commits et les applique à la fin de l'historique. En effet, si vous regardez les différences entre C5et C5'in origin/dev, vous remarquerez que même si le contenu est le même, les numéros de ligne sont probablement différents - ce qui rend le hachage du commit différent.

Je vais reformuler la règle Pro Git: ne jamais rebaser les commits qui ont jamais existé ailleurs que dans votre référentiel local . Utilisez plutôt la fusion.

Justin ᚅᚔᚈᚄᚒᚔ
la source
J'ai le même problème, comment je peux corriger l'historique de ma branche distante maintenant, y a-t-il une autre option que de supprimer la branche et de la recréer avec une sélection de cerises?
Wazery
1
@xdsy: Jetez un œil à ceci et cela .
Justin ᚅᚔᚈᚄᚒᚔ
2
Vous dites "C5 et C6 sont temporairement retirés de dev ... C7 est appliqué à dev". Si tel est le cas, alors pourquoi C5 et C6 apparaissent-ils avant C7 dans l'ordre des commits sur l'origine / dev?
KJ50
@ KJ50: Parce que C5 et C6 étaient déjà poussés vers origin/dev. Lorsqu'il devest rebasé, son historique est modifié (C5 / C6 temporairement supprimé et réappliqué après C7). La modification de l'historique des dépôts poussés est généralement une très mauvaise idée ™ à moins que vous ne sachiez ce que vous faites. Dans ce cas simple, le problème pourrait être résolu en effectuant une poussée forcée de devà origin/devaprès le rebase et en informant toute autre personne travaillant sur le origin/devfait qu'elle est probablement sur le point de passer une mauvaise journée. La meilleure réponse, encore une fois, est "ne faites pas ça ... utilisez plutôt la fusion"
Justin ᚅᚔᚈᚄᚒᚔ
3
Une chose à noter: le hachage de C5 et C5 'sont certes différents, mais pas à cause des numéros de ligne différents, mais pour les deux faits suivants dont l'un suffit pour la différence: 1) le hachage dont nous parlons est le hachage de l'arbre source entier après validation, pas le hachage de la différence delta, et donc C5 'contient tout ce qui vient du C7, alors que C5 ne le fait pas, et 2) Le parent de C5' est différent de C5, et cette information est également inclus dans le nœud racine d'une arborescence de validation affectant le résultat du hachage.
Ozgur Murat
113

Réponse courte

Vous avez omis le fait que vous avez exécuté git push, obtenu l'erreur suivante, puis exécuté git pull:

To [email protected]:username/test1.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Bien que Git essaie d'être utile, ses conseils «git pull» ne sont probablement pas ce que vous voulez faire .

Si vous êtes:

  • En travaillant uniquement sur une "branche de fonctionnalité" ou "branche de développeur" , vous pouvez exécuter git push --forcepour mettre à jour la télécommande avec vos commits post-rebase ( selon la réponse de user4405677 ).
  • Travailler sur une branche avec plusieurs développeurs en même temps, alors vous ne devriez probablement pas utilisergit rebase en premier lieu. Pour mettre devà jour avec les modifications de master, vous devriez, au lieu de courir git rebase master dev, exécuter git merge masterpendant que vous êtes allumé dev( selon la réponse de Justin ).

Une explication un peu plus longue

Chaque hachage de commit dans Git est basé sur un certain nombre de facteurs, dont l'un est le hachage du commit qui le précède.

Si vous réorganisez les validations, vous modifierez les hachages de validation; le rebasage (lorsqu'il fait quelque chose) changera les hachages de validation. Avec cela, le résultat de l'exécution git rebase master dev, où devest désynchronisé avec master, créera de nouveaux commits (et donc des hachages) avec le même contenu que ceux sur devmais avec les commits masterinsérés avant eux.

Vous pouvez vous retrouver dans une situation comme celle-ci de plusieurs manières. Je peux penser à deux façons:

  • Vous pourriez avoir des commits sur masterlesquels vous souhaitez baser votre devtravail
  • Vous pourriez avoir des commits sur devqui ont déjà été poussés vers une télécommande, que vous procédez ensuite à la modification (reformuler les messages de commit, réorganiser les commits, squash commits, etc.)

Comprenons mieux ce qui s'est passé - voici un exemple:

Vous avez un référentiel:

2a2e220 (HEAD, master) C5
ab1bda4 C4
3cb46a9 C3
85f59ab C2
4516164 C1
0e783a3 C0

Ensemble initial de commits linéaires dans un référentiel

Vous procédez ensuite à la modification des commits.

git rebase --interactive HEAD~3 # Three commits before where HEAD is pointing

(C'est là que vous devrez me croire sur parole: il existe plusieurs façons de modifier les commits dans Git. Dans cet exemple, j'ai changé l'heure de C3, mais vous insérez de nouveaux commits, modifiez les messages de commit, réorganisez les commits, squashing s'engage ensemble, etc.)

ba7688a (HEAD, master) C5
44085d5 C4
961390d C3
85f59ab C2
4516164 C1
0e783a3 C0

La même chose s'engage avec de nouveaux hachages

C'est là qu'il est important de noter que les hachages de validation sont différents. C'est un comportement attendu puisque vous avez changé quelque chose (n'importe quoi) à leur sujet. C'est bon, MAIS:

Un journal graphique montrant que le maître n'est pas synchronisé avec la télécommande

Essayer de pousser vous montrera une erreur (et un indice que vous devriez exécuter git pull).

$ git push origin master
To [email protected]:username/test1.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Si nous exécutons git pull, nous voyons ce journal:

7df65f2 (HEAD, master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 (origin/master) C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Ou, montré d'une autre manière:

Un journal graphique montrant un commit de fusion

Et maintenant, nous avons des commits en double localement. Si nous git pushdevions exécuter, nous les enverrions au serveur.

Pour éviter d'arriver à ce stade, nous aurions pu courir git push --force(où nous avons plutôt couru git pull). Cela aurait envoyé nos commits avec les nouveaux hachages au serveur sans problème. Pour résoudre le problème à ce stade, nous pouvons réinitialiser avant d'exécuter git pull:

Regardez le reflog ( git reflog) pour voir ce que le hachage commettras était avant nous avons couru git pull.

070e71d HEAD@{1}: pull: Merge made by the 'recursive' strategy.
ba7688a HEAD@{2}: rebase -i (finish): returning to refs/heads/master
ba7688a HEAD@{3}: rebase -i (pick): C5
44085d5 HEAD@{4}: rebase -i (pick): C4
961390d HEAD@{5}: commit (amend): C3
3cb46a9 HEAD@{6}: cherry-pick: fast-forward
85f59ab HEAD@{7}: rebase -i (start): checkout HEAD~~~
2a2e220 HEAD@{8}: rebase -i (finish): returning to refs/heads/master
2a2e220 HEAD@{9}: rebase -i (start): checkout refs/remotes/origin/master
2a2e220 HEAD@{10}: commit: C5
ab1bda4 HEAD@{11}: commit: C4
3cb46a9 HEAD@{12}: commit: C3
85f59ab HEAD@{13}: commit: C2
4516164 HEAD@{14}: commit: C1
0e783a3 HEAD@{15}: commit (initial): C0

Ci-dessus, nous voyons que ba7688ac'était le commit auquel nous étions avant de courir git pull. Avec ce hachage de validation en main, nous pouvons revenir à cela ( git reset --hard ba7688a) et ensuite exécuter git push --force.

Et nous avons terminé.

Mais attendez, j'ai continué à baser le travail sur les commits dupliqués

Si vous n'avez pas remarqué que les commits étaient dupliqués et que vous continuiez à travailler sur des commits en double, vous avez vraiment fait un gâchis pour vous-même. La taille du désordre est proportionnelle au nombre de commits que vous avez au-dessus des doublons.

À quoi ça ressemble:

3b959b4 (HEAD, master) C10
8f84379 C9
0110e93 C8
6c4a525 C7
630e7b4 C6
070e71d (origin/master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Journal Git montrant les commits linéaires sur les commits dupliqués

Ou, montré d'une autre manière:

Un graphique de journal montrant les commits linéaires au-dessus des commits dupliqués

Dans ce scénario, nous voulons supprimer les validations en double, mais conserver les validations que nous avons basées sur elles - nous souhaitons conserver C6 à C10. Comme pour la plupart des choses, il existe plusieurs façons de procéder:

Soit:

  • Créez une nouvelle branche au dernier commit dupliqué 1 , cherry-pickchaque commit (C6 à C10 inclus) sur cette nouvelle branche, et traitez cette nouvelle branche comme canonique.
  • Exécuter git rebase --interactive $commit, où $commitest le commit avant les deux commits dupliqués 2 . Ici, nous pouvons carrément supprimer les lignes des doublons.

1 Peu importe lequel des deux vous choisissez, l'un ba7688aou l' autre 2a2e220fonctionne bien.

2 Dans l'exemple, ce serait 85f59ab.

TL; DR

Définir advice.pushNonFastForwardsur false:

git config --global advice.pushNonFastForward false
Whymarrhe
la source
1
Il est normal de suivre le conseil "git pull ..." tant que l'on se rend compte que les points de suspension masque l'option "--rebase" (alias "-r"). ;-)
G.Sylvie Davies
4
Je recommanderais d'utiliser git pushde --force-with-leasenos jours car c'est un meilleur défaut
Whymarrh
4
C'est soit cette réponse, soit une machine à remonter le temps. Merci!
ZeMoon
Explication très soignée ... Je suis tombé sur un problème similaire qui a dupliqué mon code 5 à 6 fois après avoir tenté de rebaser à plusieurs reprises ... juste pour être sûr que le code est à jour avec master ... mais chaque fois qu'il a poussé new commits dans ma succursale, dupliquant également mon code. Pouvez-vous s'il vous plaît me dire si force push (avec option de bail) est sûr à faire ici si je suis le seul développeur travaillant sur ma branche? Ou fusionner le maître dans le mien au lieu de rebaser est le meilleur moyen?
Dhruv Singhal
12

Je pense que vous avez sauté un détail important lors de la description de vos étapes. Plus précisément, votre dernière étape, git pushsur le développement, vous aurait en fait généré une erreur, car vous ne pouvez normalement pas pousser les modifications non rapides.

C'est ce que vous avez fait git pullavant la dernière poussée, qui a abouti à un commit de fusion avec C6 et C6 'comme parents, c'est pourquoi les deux resteront répertoriés dans le journal. Un format de journal plus joli aurait pu rendre plus évident le fait qu'il s'agit de branches fusionnées de commits dupliqués.

Ou vous avez fait un git pull --rebase(ou sans explicite --rebasesi cela est impliqué par votre configuration) à la place, qui a récupéré les C5 et C6 d'origine dans votre développement local (et rebasé plus loin les suivants en de nouveaux hachages, C7 'C5' 'C6' ').

Une façon de s'en sortir aurait pu être git push -fde forcer la poussée quand elle a donné l'erreur et d'effacer le C5 C6 de l'origine, mais si quelqu'un d'autre les avait également retirés avant de les effacer, vous auriez beaucoup plus de problèmes. En gros, tous ceux qui ont C5 C6 devraient faire des mesures spéciales pour s'en débarrasser. C'est exactement pourquoi ils disent que vous ne devriez jamais rebaser tout ce qui est déjà publié. C'est toujours faisable si la «publication» se fait au sein d'une petite équipe.

user4405677
la source
1
L'omission de git pullest cruciale. Votre recommandation git push -f, bien que dangereuse, est probablement ce que les lecteurs recherchent.
Whymarrh
En effet. À l'époque où j'ai écrit la question que j'ai réellement posée git push --force, juste pour voir ce que Git allait faire. J'ai beaucoup appris sur Git depuis lors et fait aujourd'hui rebasepartie de mon flux de travail normal. Cependant, je le fais git push --force-with-leasepour éviter d'écraser le travail de quelqu'un d'autre.
elitalon
L'utilisation --force-with-leaseest un bon défaut, je vais également laisser un commentaire sous ma réponse
Whymarrh
2

J'ai découvert que dans mon cas, ce problème était la conséquence d'un problème de configuration Git. (Impliquant l'extraction et la fusion)

Description du problème:

Sympthoms: Commits dupliqués sur la branche enfant après le rebase, impliquant de nombreuses fusions pendant et après le rebase.

Workflow: voici les étapes du workflow que j'effectuais:

  • Travailler sur la "branche Features" (enfant de "Develop-branch")
  • Valider et pousser les modifications sur "Features-branch"
  • Vérifiez "Develop-branch" (branche mère des fonctionnalités) et travaillez avec.
  • Valider et pousser les modifications sur "Develop-branch"
  • Checkout "Features-branch" et extraire les modifications du référentiel (au cas où quelqu'un d'autre aurait commis un travail)
  • Rebase "Features-branch" sur "Develop-branch"
  • Pousser la force des changements sur "Feature-branch"

Comme conséquences de ce workflow, duplication de tous les commits de "Feature-branch" depuis le rebase précédent ... :-(

Le problème était dû à l'attraction des modifications de la branche enfant avant le rebase. La configuration d'extraction par défaut de Git est "merge". Cela modifie les index des validations effectuées sur la branche enfant.

La solution: dans le fichier de configuration Git, configurez pull pour qu'il fonctionne en mode rebase:

...
[pull]
    rebase = preserve
...

J'espère que cela peut aider JN Grx

JN Gerbaux
la source
1

Vous avez peut-être tiré d'une branche distante différente de votre actuelle. Par exemple, vous avez peut-être retiré de Master lorsque votre branche développe le suivi du développement. Git extraira consciencieusement des commits en double s'il est extrait d'une branche non suivie.

Si cela se produit, vous pouvez effectuer les opérations suivantes:

git reset --hard HEAD~n

n == <number of duplicate commits that shouldn't be there.>

Ensuite, assurez-vous que vous tirez de la bonne branche, puis exécutez:

git pull upstream <correct remote branch> --rebase

Tirer avec --rebasegarantira que vous n'ajoutez pas de commits superflus qui pourraient brouiller l'historique des commits.

Voici un peu de prise de main pour git rebase.

ScottyBlades
la source