Comment puis-je récupérer / resynchroniser après que quelqu'un pousse un rebase ou une réinitialisation dans une branche publiée?

88

Nous avons tous entendu dire qu'il ne faut jamais rebaser un travail publié, que c'est dangereux, etc. Cependant, je n'ai vu aucune recette publiée pour savoir comment gérer la situation au cas où un rebase serait publié.

Maintenant, notez que cela n'est vraiment faisable que si le référentiel n'est cloné que par un groupe de personnes connu (et de préférence petit), de sorte que quiconque pousse le rebase ou la réinitialisation puisse informer tout le monde qu'il devra faire attention la prochaine fois. chercher (!).

Une solution évidente que j'ai vue fonctionnera si vous n'avez pas de commits locaux fooet qu'elle est rebasée:

git fetch
git checkout foo
git reset --hard origin/foo

Cela rejettera simplement l'état local de fooau profit de son histoire selon le référentiel distant.

Mais comment gérer la situation si l'on a engagé des changements locaux substantiels sur cette branche?

Aristote Pagaltzis
la source
+1 pour la recette de cas simple. Il est idéal pour la synchronisation personnelle entre les machines, surtout si elles ont des OS différents. C'est quelque chose qui devrait être mentionné dans le manuel.
Philip Oakley
La recette idéale pour la synchronisation personnelle est git pull --rebase && git push. Si vous travaillez masteruniquement sur , cela fera presque toujours la bonne chose pour vous, même si vous avez rebasé et poussé à l'autre bout.
Aristote Pagaltzis
Parce que je synchronise et développe entre un PC et une machine Linux, je trouve que l'utilisation d'une nouvelle branche pour chaque rebase / mise à jour fonctionne bien. J'utilise aussi la variante git reset --hard @{upstream}maintenant que je sais que l'incantation magique de refspec pour "oubliez ce que j'ai / j'avais, utilisez ce que j'ai récupéré de la télécommande" Voir mon dernier commentaire sur stackoverflow.com/a/15284176/717355
Philip Oakley
Vous pourrez, avec Git2.0, retrouver l'ancienne origine de votre branche (avant que la branche amont ne soit réécrite avec a push -f): voir ma réponse ci
VonC

Réponses:

75

Se remettre en synchronisation après un rebase poussé n'est vraiment pas si compliqué dans la plupart des cas.

git checkout foo
git branch old-foo origin/foo # BEFORE fetching!!
git fetch
git rebase --onto origin/foo old-foo foo
git branch -D old-foo

C'est à dire. vous configurez d'abord un signet pour l'emplacement d'origine de la branche distante, puis vous l'utilisez pour rejouer vos commits locaux à partir de ce point sur la branche distante rebasée.

Rebasing est comme la violence: si cela ne résout pas votre problème, vous en avez juste besoin de plus. ☺

Vous pouvez le faire sans le signet bien sûr, si vous recherchez l' origin/fooID de validation pré-rebase et que vous l'utilisez.

C'est également ainsi que vous gérez la situation où vous avez oublié de créer un signet avant de le récupérer. Rien n'est perdu - il vous suffit de vérifier le reflog de la branche distante:

git reflog show origin/foo | awk '
    PRINT_NEXT==1 { print $1; exit }
    /fetch: forced-update/ { PRINT_NEXT=1 }'

Cela affichera l'ID de validation qui origin/foopointait avant la dernière extraction qui a changé son historique.

Vous pouvez alors simplement

git rebase --onto origin/foo $commit foo
Aristote Pagaltzis
la source
11
Note rapide: je pense que c'est assez intuitif, mais si vous ne connaissez pas bien awk ... ce one-liner regarde juste à travers la sortie de git reflog show origin/foola première ligne en disant "fetch: forcé-mise à jour"; c'est ce que git enregistre lorsqu'une extraction amène la branche distante à faire autre chose qu'une avance rapide. (Vous pouvez aussi le faire à la main - la mise à jour forcée est probablement la chose la plus récente.)
Cascabel
2
Cela n'a rien à voir avec la violence. La violence est parfois amusante
Iolo
5
@iolo C'est vrai, le rebasage est toujours amusant.
Dan Bechard
1
Comme la violence, évitez presque toujours de rebaser. Mais sachez comment.
Bob Stein
2
Eh bien, évitez de pousser un rebase là où d'autres seront affectés.
Aristotle Pagaltzis
11

Je dirais que la récupération à partir de la section rebase en amont de la page de manuel git-rebase couvre à peu près tout cela.

Ce n'est vraiment pas différent de la récupération de votre propre rebase - vous déplacez une branche et rebasez toutes les branches qui l'avaient dans leur histoire sur sa nouvelle position.

Cascabel
la source
4
Ah, c'est le cas. Mais bien que je comprenne maintenant ce qu'il dit, je ne l'aurais pas fait avant, avant de comprendre cela par moi-même. Et il n'y a pas de recette de livre de cuisine (peut-être à juste titre dans une telle documentation). J'avancerai également qu'appeler le «cas dur» dur est FUD Je soutiens que l'histoire réécrite est trivialement gérable à l'échelle de la plupart des développements internes. La manière superstitieuse dont ce sujet est toujours traité me gêne.
Aristotle Pagaltzis
4
@Aristotle: Vous avez raison de dire que c'est très gérable, étant donné que tous les développeurs savent comment utiliser git, et que vous pouvez communiquer efficacement avec tous les développeurs. Dans un monde parfait, ce serait la fin de l'histoire. Mais beaucoup de projets sont suffisamment importants pour qu'un rebase en amont soit vraiment effrayant. (Et puis il y a des endroits comme mon lieu de travail, où la plupart des développeurs n'ont même jamais entendu parler d'un rebase.) Je pense que la "superstition" est juste un moyen de fournir les conseils les plus sûrs et les plus génériques possibles. Personne ne veut être celui qui cause une catastrophe dans le repo de quelqu'un d'autre.
Cascabel
2
Oui, je comprends le motif. Et je suis entièrement d'accord avec cela. Mais il y a un monde de différence entre «n'essayez pas ceci si vous ne comprenez pas les conséquences» et «vous ne devriez jamais faire cela parce que c'est mal», et cela seul, je suis en désaccord. Il vaut toujours mieux instruire que susciter la peur.
Aristotle Pagaltzis
@Aristote: D'accord. J'essaie de tendre vers la fin "Assurez-vous de savoir ce que vous faites", mais surtout en ligne, j'essaie de lui donner suffisamment de poids pour qu'un visiteur occasionnel de Google en prenne note. Vous avez raison, une grande partie devrait probablement être atténuée.
Cascabel
11

A partir de git 1.9 / 2.0 Q1 2014, vous ne devez marquer l' origine précédente de la branche avant rebasage sur la branche amont réécrite, comme décrit dans Aristote Pagaltzis de réponse :
Voir commit 07d406b et engage d96855f :

Après avoir travaillé sur la topicbranche créée avec git checkout -b topic origin/master, l'historique de la branche de suivi à distance a origin/masterpeut-être été rembobiné et reconstruit, conduisant à un historique de cette forme:

                   o---B1
                  /
  ---o---o---B2--o---o---o---B (origin/master)
          \
           B3
            \
             Derived (topic)

origin/masterutilisé pour point commits B3, B2, B1et maintenant il pointe à B, et votre topicbranche a commencé au - dessus de ce retour en origin/masterétait à B3.

Ce mode utilise le reflog de origin/masterpour trouver B3comme point de fourche, de sorte que le topicpeut être rebasé au-dessus de la miseorigin/master à jour par:

$ fork_point=$(git merge-base --fork-point origin/master topic)
$ git rebase --onto origin/master $fork_point topic

C'est pourquoi la git merge-basecommande a une nouvelle option:

--fork-point::

Trouvez le point auquel une branche (ou tout historique qui mène à <commit>) a bifurqué d'une autre branche (ou de toute référence) <ref>.
Cela ne cherche pas seulement l'ancêtre commun des deux commits, mais prend également en compte le reflog de <ref>pour voir si l'histoire menant à <commit>bifurqué d'une incarnation antérieure de la branche<ref> .


La git pull --rebasecommande " " calcule le point de branchement de la branche rebasée en utilisant les entrées reflog de la basebranche " " (généralement une branche de suivi à distance) sur laquelle le travail de la branche était basé, afin de faire face au cas où la "base" branche a été rembobinée et reconstruite.

Par exemple, si l'historique ressemblait à où:

  • la pointe actuelle de la basebranche " " est à B, mais plus tôt fetch a observé que sa pointe était B3avant B2et puis B1 avant d'arriver au commit actuel, et
  • la branche rebasée au-dessus de la dernière "base" est basée sur commit B3,

il essaie de trouver B3en passant par la sortie de « git rev-list --reflog base» (c. -à B, B1, B2, B3) jusqu'à ce qu'il trouve un commettras qui est un ancêtre de l'actuelle pointe « Derived (topic)».

En interne, nous get_merge_bases_many()pouvons calculer cela en une seule fois.
Nous voudrions une base de fusion entre Derivedet un commit de fusion fictif qui résulterait de la fusion de tous les conseils historiques de " base (origin/master)".
Lorsqu'un tel commit existe, nous devrions obtenir un seul résultat, qui correspond exactement à l'une des entrées reflog de " base".


Git 2.1 (Q3 2014) ajoutera pour rendre cette fonctionnalité plus robuste à ceci: voir commit 1e0dacd par John Keeping ( johnkeeping)

gérer correctement le scénario où nous avons la topologie suivante:

    C --- D --- E  <- dev
   /
  B  <- master@{1}
 /
o --- B' --- C* --- D*  <- master

où:

  • B'est une version corrigée de Bqui n'est pas identique au patch B;
  • C*et D*sont identiques au patch Cet Drespectivement et sont en conflit textuellement s'ils sont appliqués dans le mauvais ordre;
  • Edépend textuellement de D.

Le résultat correct de git rebase master devest - ce que Best identifié comme le point fourche devet master, de sorte que C, D, Esont les commits qui doivent être rejoué sur master; mais Cet Dsont identiques au patch avec C*and D*et peuvent donc être supprimés, de sorte que le résultat final est:

o --- B' --- C* --- D* --- E  <- dev

Si le point de bifurcation n'est pas identifié, alors choisir Bune branche contenant B'entraîne un conflit et si les commits identiques au correctif ne sont pas correctement identifiés, alors choisir Cune branche contenant D(ou de manière équivalente D*) entraîne un conflit.


Le " --fork-point" mode de " git rebase" a régressé lorsque la commande a été réécrite en C à l'ère 2.20, ce qui a été corrigé avec Git 2.27 (T2 2020).

Voir commit f08132f (09 décembre 2019) par Junio ​​C Hamano ( gitster) .
(Fusionné par Junio ​​C Hamano - gitster- dans commit fb4175b , 27 mars 2020)

rebase: --fork-pointcorrection de régression

Signé par: Alex Torok
[jc: remanié le correctif et utilisé les tests d'Alex]
Signé par: Junio ​​C Hamano

" git rebase --fork-point master" fonctionnait bien, comme il l'appelait en interne " git merge-base --fork-point" qui savait comment gérer le nom de référence court et le dwimer avec le nom complet avant d'appeler la get_fork_point()fonction sous-jacente .

Ce n'est plus vrai après la réécriture de la commande en C, car son appel interne effectué directement à get_fork_point()ne dwim une courte référence.

Déplacez la logique "dwim the refname vers le refname complet" qui est utilisée dans "git merge-base" vers la get_fork_point()fonction sous-jacente , de sorte que l'autre appelant de la fonction dans l'implémentation de "git rebase" se comporte de la même manière pour corriger cette régression.

VonC
la source
1
Notez qu'un git push --force peut maintenant (git 1.8.5) être effectué plus prudemment: stackoverflow.com/a/18505634/6309
VonC