183 votes

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

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

Ne pas rebaser les commits que vous avez poussés vers un dépôt public.

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

Disons que j'ai deux branches distantes avec leurs équivalents locaux :

origin/master    origin/dev
|                |
master           dev

Les quatre branches contiennent les mêmes commits et je vais commencer le développement dans la section 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 commits, je pousse les changements vers 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 retourner à master pour faire une réparation 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 retour à dev Je rebase les changements 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 que origin/dev contient maintenant deux commits identiques C5' y C6' qui sont différentes de celles de Git. Maintenant, si je pousse les changements vers origin/dev voici 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 bien l'explication de Pro Git, alors je voudrais 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 d'appliquer simplement la commande C5 y C6 après C7 ?
  2. Comment puis-je éviter cela ? Serait-il judicieux de le faire ?

166voto

Whymarrh Points 2715

Réponse courte

Vous avez omis le fait que vous avez couru git push J'ai obtenu l'erreur suivante, et j'ai ensuite exécuté le programme git pull :

To git@bitbucket.org:username/test1.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to 'git@bitbucket.org: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.

Malgré le fait que Git essaie d'être utile, son conseil "git pull" n'est probablement pas ce que vous voulez faire. .

Si vous l'êtes :

  • Travailler sur une "feature branch" ou une "developer branch". seul alors vous pouvez exécuter git push --force pour mettre à jour la télécommande avec vos commits post-rebase ( selon la réponse de l'utilisateur4405677 ).
  • Travailler sur une branche avec plusieurs développeurs en même temps, alors vous ne devriez probablement pas utiliser git rebase en premier lieu. Pour mettre à jour dev avec des changements de master vous devriez, au lieu d'exécuter git rebase master dev , courir git merge master pendant que 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éordonnez les commits, vous changerez les hashs des commits ; le rebasage (quand il fait quelque chose) changera les hashs des commits. Avec cela, le résultat de l'exécution de git rebase master dev , donde dev n'est pas synchrone avec master créera nouveau (et donc des hachages) avec le même contenu que ceux de l'application dev mais avec les commits sur master inséré avant eux.

Vous pouvez vous retrouver dans une telle situation de plusieurs façons. Deux façons auxquelles je peux penser :

  • Vous pourriez avoir des commits sur master que vous voulez baser votre dev travailler sur
  • Vous pourriez avoir des commits sur dev qui ont déjà été poussés vers un serveur distant, que vous pouvez ensuite modifier (reformulation des messages de validation, réorganisation des validations, compression des validations, etc.)

Essayons de mieux comprendre 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

Initial set of linear commits in a repository

Vous procédez ensuite à la modification des commits.

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

(C'est ici que vous devrez me croire sur parole : il y a 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éordonnez les commits, écrasez les commits ensemble, etc.)

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

The same commits with new hashes

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

A graph log showing that master is out-of-sync with the remote

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

$ git push origin master
To git@bitbucket.org:username/test1.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'git@bitbucket.org: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 courons 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 :

A graph log showing a merge commit

Et maintenant nous avons des commits dupliqués localement. Si nous devions exécuter git push nous les enverrions au serveur.

Pour éviter d'arriver à ce stade, nous aurions pu exécuter git push --force (où nous avons plutôt exécuté git pull ). Cela aurait envoyé nos commits avec les nouveaux hashs au serveur sans problème. Pour résoudre le problème à ce stade, nous pouvons revenir à la situation antérieure à l'exécution de la commande git pull :

Regardez le reflog ( git reflog ) pour voir quel était le hash de la livraison 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

Nous voyons ci-dessus que ba7688a était le commit où nous étions avant de lancer git pull . Avec ce hash de commit en main, nous pouvons réinitialiser le tout ( git reset --hard ba7688a ), puis exécutez 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 avez continué à travailler sur des commits dupliqués, vous vous êtes vraiment mis dans le pétrin. La taille du désordre est proportionnelle au nombre de commits que vous avez au-dessus des duplicatas.

Ce à 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

Git log showing linear commits atop duplicated commits

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

A log graph showing linear commits atop duplicated commits

Dans ce scénario, nous voulons supprimer les commits dupliqués, mais garder les commits que nous avons basés sur eux - nous voulons garder C6 à C10. Comme pour la plupart des choses, il y a plusieurs façons de procéder :

Soit :

  • Créer une nouvelle branche au dernier commit dupliqué 1 , cherry-pick chaque commit (C6 à C10 inclus) sur cette nouvelle branche, et traiter cette nouvelle branche comme canonique.
  • Ou courir git rebase --interactive $commit , donde $commit est l'engagement avant aux deux commits dupliqués 2 . Ici, nous pouvons carrément supprimer les lignes pour les doublons.

1 Le choix de l'un ou l'autre n'a pas d'importance. ba7688a o 2a2e220 fonctionnent bien.

2 Dans l'exemple, ce serait 85f59ab .

TL;DR

Définir advice.pushNonFastForward a false :

git config --global advice.pushNonFastForward false

103voto

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

  • C5 y C6 sont temporairement retirés de dev
  • C7 est appliqué à dev
  • C5 y C6 sont lues par-dessus C7 créant de nouveaux diffs et donc de nouveaux commits.

Donc, dans votre dev branche, C5 y C6 n'existent effectivement plus : ils sont désormais C5' y C6' . Lorsque vous appuyez sur origin/dev , git voit C5' y C6' comme de nouveaux commits et les ajoutent à la fin de l'historique. En effet, si vous regardez les différences entre C5 y C5' en origin/dev vous remarquerez que bien que le contenu soit le même, les numéros de lignes sont probablement différents -- ce qui rend le hash du commit différent.

Je vais reformuler la règle de Pro Git : ne jamais rebaser des commits qui ont déjà existé ailleurs que dans votre dépôt local . Utilisez plutôt la fusion.

15voto

user4405677 Points 151

Je pense que vous avez omis un détail important dans la description de vos étapes. Plus précisément, ta dernière étape, git push sur dev, vous aurait en fait donné une erreur, car vous ne pouvez pas normalement pousser des changements non définitifs.

Donc vous avez fait git pull avant le dernier push, qui a résulté en un commit de fusion avec C6 et C6' comme parents, c'est pourquoi les deux restent listés dans le log. Un format de journal plus joli aurait pu rendre plus évident qu'il s'agit de branches fusionnées de commits dupliqués.

Ou vous avez fait un git pull --rebase (ou sans explicite --rebase s'il est impliqué par votre configuration) à la place, ce qui ramène les C5 et C6 originaux dans votre dev local (et re-fonde les suivants sur de nouveaux hashs, C7' C5'' C6'').

Une façon de s'en sortir aurait pu être git push -f pour forcer la poussée quand il a donné l'erreur et effacer C5 C6 de l'origine, mais si quelqu'un d'autre les avait aussi tirés avant que vous les effaciez, vous auriez beaucoup plus de problèmes... en fait, tout le monde qui a C5 C6 devrait faire des étapes spéciales pour s'en débarrasser. C'est exactement pour ça qu'ils disent qu'il ne faut jamais rebaser quelque chose qui a déjà été publié. C'est toujours faisable si ladite "publication" se fait au sein d'une petite équipe, cependant.

2voto

JN Gerbaux Points 101

J'ai découvert que dans mon cas, ce problème est la conséquence d'un problème de configuration de Git. (Impliquant pull et merge)

Description du problème :

Sympathiques : Commits dupliqués sur la branche enfant après le rebasement, ce qui implique de nombreuses fusions pendant et après le rebasement.

Flux de travail : Voici les étapes du flux de travail que j'effectuais :

  • Travail sur la branche "Features" (enfant de la branche "Develop")
  • Commit et Push sur "Features-branch".
  • Vérifiez "Develop-branch" (branche mère de Features) et travaillez avec elle.
  • Commit et push des changements sur "Develop-branch".
  • Vérifiez "Features-branch" et tirez les changements du dépôt (au cas où quelqu'un d'autre a commis du travail).
  • Repositionner "Features-branch" sur "Develop-branch".
  • Force de pression des changements sur "Feature-branch".

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

Le problème était dû à l'extraction des changements de la branche enfant avant le rebasement. La configuration par défaut des pull de Git est "merge". Cela revient à modifier les index des commits effectués sur la branche fille.

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

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

J'espère que cela pourra vous aider JN Grx

1voto

ScottyBlades Points 1262

Vous pouvez avoir tiré d'une branche distante différente de votre branche actuelle. Par exemple, vous pouvez avoir tiré de Master alors que votre branche est develop tracking develop. Git récupérera consciencieusement les commits dupliqués s'ils sont tirés d'une branche non suivie.

Si cela se produit, vous pouvez faire ce qui suit :

git reset --hard HEAD~n

donde 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 --rebase vous permettra de vous assurer que vous n'ajoutez pas de commits superflus qui pourraient brouiller l'historique des commits.

Voici un peu de tenue de main pour git rebase.

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