web-dev-qa-db-fra.com

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

Je comprends le scénario présenté dans Pro Git à propos de les risques de git rebase . En gros, l’auteur vous explique comment éviter les commits en double:

Ne rebassez pas les commits que vous avez envoyés dans un référentiel public.

Je vais vous dire ma situation particulière parce que je pense que cela ne correspond pas exactement au scénario de Pro Git et que je finis toujours par avoir des commits en double.

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 commits, je renvoie les modifications à 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 à master pour apporter 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 à dev je reformule 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 que Origin/dev contient maintenant deux commits identiques C5' et C6' qui sont différents de Git. Maintenant, si je pousse les modifications à Origin/dev c'est 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 tout à fait l'explication dans Pro Git, alors j'aimerais savoir deux choses:

  1. Pourquoi Git duplique-t-il ces commits lors du rebasement? Y a-t-il une raison particulière de faire cela au lieu d'appliquer simplement C5 et C6 après C7?
  2. Comment puis-je éviter cela? Serait-il sage de le faire?
103
elitalon

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

  • C5 et C6 sont temporairement retirés de dev
  • C7 est appliqué à dev
  • C5 et C6 sont lus au-dessus de C7, créant de nouveaux diffs et donc de nouveaux commits

Donc, dans votre branche dev, C5 et C6 En réalité, ils n'existent plus: ils sont maintenant C5' et C6'. Lorsque vous appuyez sur Origin/dev, git voit C5' et C6' comme nouveaux commet et les pointe vers la fin de l’histoire. En effet, si vous regardez les différences entre C5 et C5' dans Origin/dev, vous remarquerez que, même si le contenu est identique, les numéros de ligne sont probablement différents - ce qui rend le hachage du commit différent.

Je vais reformuler la règle de Pro Git: ne redéfinissez jamais les commits qui ont jamais existé ailleurs que dans votre référentiel local. Utilisez fusionner à la place.

77

Réponse courte

Vous avez omis le fait que vous avez exécuté git Push, Que vous avez rencontré l'erreur suivante et que vous avez ensuite 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 ait essayé d’être utile, , son conseil de "tirage" n’est probablement pas ce que vous voulez faire .

Si vous êtes:

  • Travailler sur une "branche de fonctionnalité" ou une "branche de développeur" seule , vous pouvez alors exécuter git Push --force Pour mettre à jour la télécommande avec votre rebase commits ( selon la réponse de user4405677 ).
  • Travaillant 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 les modifications de master, vous devriez, au lieu d'exécuter git rebase master dev, Exécuter git merge master Avec 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 commits, vous changerez les hachages de commits; le rebasement (quand il fait quelque chose) changera les hachages de commit. Avec cela, le résultat de l'exécution de git rebase master dev, Où dev n'est pas synchronisé avec master, créera nouveau commits (et donc des hachages ) avec le même contenu que ceux sur dev mais avec les commits sur master insérés avant eux.

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

  • Vous pouvez avoir des commits sur master sur lesquels vous voulez baser votre travail dev
  • Vous pouvez avoir des commits sur dev qui ont déjà été poussés vers une télécommande, que vous allez ensuite modifier (reformulation des messages de validation, des modifications de validation, des modifications de squash, etc.).

Faisons 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 pour modifier les commits.

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

(C’est là que vous devrez vous fier à ma parole: il existe plusieurs façons de modifier les commits dans Git. Dans cet exemple, j’ai modifié l’heure de C3, Mais vous insérez de nouvelles commissions, en modifiant messages validés, réordonnancement des commits, écrasement des 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 ici qu’il est important de noter que les hachages de validation sont différents. C'est le comportement attendu puisque vous avez changé quelque chose (n'importe quoi) à leur sujet. C'est bon, mais:

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

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 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 nous avons maintenant des commises en double localement. Si nous devions exécuter git Push, Nous les enverrions au serveur.

Pour éviter d'atteindre 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 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 commit é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 ba7688a Était l'engagement que nous avions avant d'exécuter git pull. Avec ce hash en main, nous pouvons revenir à cela (git reset --hard ba7688a) Et ensuite exécuter git Push --force.

Et nous avons fini.

Mais attendez, je continuais à baser mon travail sur les commits dupliqués

Si, d'une manière ou d'une autre, vous n'avez pas remarqué que les commits étaient dupliqués et que vous avez continué à travailler au-dessus des commits en double, vous vous êtes vraiment mis à semer la pagaille. La taille du gâchis est proportionnelle au nombre de commits que vous avez au dessus des doublons.

A 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 en double, mais conserver les commits que nous avons basés sur eux. Nous souhaitons conserver les c6 à c10. Comme pour la plupart des choses, il y a plusieurs façons de s'y prendre:

Non plus:

  • Créer une nouvelle branche à la dernière validation dupliquée1, cherry-pick chaque commit (C6 à C10 inclus) sur cette nouvelle branche et traite cette nouvelle branche comme canonique.
  • Exécutez git rebase --interactive $commit, Où $commit Est le commit antérieur aux deux commits dupliqués.2. Ici, nous pouvons supprimer les lignes pour les doublons.

1 Peu importe lequel des deux que vous choisissez, que ba7688a Ou 2a2e220 Fonctionne correctement.

2 Dans l'exemple, ce serait 85f59ab.

TL; DR

Définissez advice.pushNonFastForward sur false:

git config --global advice.pushNonFastForward false
85
Whymarrh

Je pense que vous avez omis un détail important lors de la description de vos étapes. Plus précisément, votre dernière étape, git Push sur dev, vous aurait en fait provoqué une erreur, car vous ne pouvez normalement pas appliquer les modifications non rapides.

Alors vous avez fait git pull avant le dernier Push, ce qui a entraîné une fusion avec C6 et C6 'en tant que parents, ce qui explique pourquoi les deux resteront répertoriés dans le journal. Un format de journal plus joli aurait peut-être rendu plus évident le fait qu'ils sont des branches fusionnées de validations dupliquées.

Ou vous avez fait un git pull --rebase _ (ou sans --rebase si cela est implicite dans votre configuration), ce qui a ramené les C5 et C6 d'origine dans votre développement local (et redéfini les bases suivantes en un nouveau hachage, C7 'C5' 'C6' ').

Un moyen de s'en sortir aurait pu être git Push -f pour forcer la Push quand elle a donné l'erreur et effacer C5 C6 de Origin, mais si quelqu'un d'autre les faisait également tirer avant de les effacer, vous auriez beaucoup plus de problèmes ... en gros tous ceux qui ont C5 C6 devrait prendre des mesures spéciales pour s'en débarrasser. C'est exactement pourquoi ils disent que vous ne devriez jamais refonder tout ce qui a déjà été publié. C'est toujours faisable si on dit que "l'édition" est au sein d'une petite équipe.

12
user4405677

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

Description du problème:

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

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

  • Travaillez sur "Features-branch" (enfant de "Develop-branch")
  • Commit et Push change sur "Features-branch"
  • Checkout "Develop-branch" (branche mère de fonctionnalités) et de travailler avec elle.
  • Commit et Push change sur "Develop-branch"
  • Commander "Features-branch" et extraire les modifications du référentiel (Si quelqu'un d'autre a validé le travail)
  • Rebase "Features-branch" sur "Develop-branch"
  • Force de changement des "fonctions"

En conséquence de ce flux de travail, la duplication de tous les commits de "Feature-branch" depuis la précédente refonte ... :

--- (Le problème était dû à l'extraction des modifications de la branche enfant avant la base. La configuration d'extraction par défaut de Git est "fusion". Cela modifie les index des commits effectués sur la branche enfant.

La solution: dans le fichier de configuration Git, configurez pull pour fonctionner en mode rebase:

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

J'espère que cela peut aider JN Grx

1
JN Gerbaux