web-dev-qa-db-fra.com

Git workflow et rebase vs fusion des questions

J'utilise Git depuis quelques mois sur un projet avec un autre développeur. J'ai plusieurs années d'expérience avec SVN , je suppose donc que j'apporte beaucoup de bagage à la relation.

J'ai entendu dire que Git est excellent pour la création de branches et la fusion, et jusqu'à présent, je ne le vois pas. Bien sûr, la création de branches est extrêmement simple, mais lorsque j'essaie de fusionner, tout se passe bien. Maintenant, je suis habitué à cela de SVN, mais il me semble que je viens d’échanger un système de gestion de versions sous-pair contre un autre.

Mon partenaire me dit que mes problèmes découlent de mon désir de fusionner bon gré mal gré et que je devrais utiliser rebase au lieu de fusionner dans de nombreuses situations. Par exemple, voici le flux de travail qu'il a défini:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature
git checkout master
git merge my_new_feature

Pour l'essentiel, créez une branche de fonctionnalité, TOUJOURS rebase du maître vers la branche et fusionnez de la branche vers le maître. Il est important de noter que la succursale reste toujours locale.

Voici le flux de travail avec lequel j'ai commencé

clone remote repository
create my_new_feature branch on remote repository
git checkout -b --track my_new_feature Origin/my_new_feature
..work, commit, Push to Origin/my_new_feature
git merge master (to get some changes that my partner added)
..work, commit, Push to Origin/my_new_feature
git merge master
..finish my_new_feature, Push to Origin/my_new_feature
git checkout master
git merge my_new_feature
delete remote branch
delete local branch

Il y a deux différences essentielles (je pense): j'utilise toujours la fusion au lieu de la refonte, et je transfère ma branche de fonctionnalité (et la validation de ma branche de fonctionnalité) vers le référentiel distant.

Mon raisonnement pour la branche distante est que je veux que mon travail soit sauvegardé pendant que je travaille. Notre référentiel est automatiquement sauvegardé et peut être restauré en cas de problème. Mon ordinateur portable n'est pas, ou pas aussi bien. Par conséquent, je déteste avoir sur mon ordinateur portable un code qui ne soit pas reproduit ailleurs.

Mon raisonnement pour la fusion au lieu de rebase est que la fusion semble être standard et que rebase semble être une fonctionnalité avancée. Mon instinct est que ce que j'essaie de faire n’est pas une configuration avancée, une rebase devrait donc être inutile. J'ai même parcouru le nouveau livre de programmation pragmatique sur Git, qui couvre beaucoup de fusions et parle à peine de rebase.

Quoi qu'il en soit, je suivais mon flux de travail sur une branche récente, et lorsque j'ai essayé de le fusionner pour devenir maître, tout est allé en enfer. Il y avait des tonnes de conflits avec des choses qui n'auraient pas dû avoir d'importance. Les conflits n'avaient tout simplement aucun sens pour moi. Il m'a fallu une journée pour tout régler, et a finalement abouti à un Push forcé vers le maître distant, puisque mon maître local avait résolu tous les conflits, mais que celui-ci n'était toujours pas heureux.

Quel est le flux de travail "correct" pour quelque chose comme ça? Git est censé rendre très facile la création de branches et la fusion, et je ne le vois tout simplement pas.

Mise à jour 2011-04-15

Cela semble être une question très populaire, alors j'ai pensé mettre à jour avec mes deux années d'expérience depuis ma première demande.

Il s'avère que le flux de travail d'origine est correct, du moins dans notre cas. En d'autres termes, c'est ce que nous faisons et cela fonctionne:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge my_new_feature

En fait, notre flux de travail est un peu différent, comme nous avons tendance à le faire squash fusionne au lieu de fusions brutes. ( Remarque: il s'agit d'un sujet controversé, voir ci-dessous. ) Cela nous permet de transformer toute notre branche de fonctionnalités en un seul commit sur maître. Ensuite, nous supprimons notre branche de fonctionnalité. Cela nous permet de structurer logiquement nos commits sur maître, même s'ils sont un peu brouillons sur nos branches. Alors, voici ce que nous faisons:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge --squash my_new_feature
git commit -m "added my_new_feature"
git branch -D my_new_feature

Controverse sur la fusion de squash - Comme l'ont souligné plusieurs commentateurs, la fusion de squash jettera toute l'historique de votre branche. Comme son nom l'indique, tous les commits sont réduits en un seul. Pour les petites fonctionnalités, cela a du sens car cela les condense en un seul package. Pour de plus grandes fonctionnalités, ce n'est probablement pas une bonne idée, surtout si vos commits individuels sont déjà atomiques. Cela dépend vraiment des préférences personnelles.

Github et Bitbucket (autres?) Requêtes de tirage - Si vous vous demandez comment la fusion/rebase est liée aux demandes de tirage, je vous recommande de suivre toutes les étapes ci-dessus. jusqu'à ce que vous soyez prêt à fusionner pour maîtriser. Au lieu de fusionner manuellement avec git, vous acceptez simplement le PR. Notez que cela ne fera pas une fusion de squash (du moins pas par défaut), mais non-squash, la non-avance rapide est la convention de fusion acceptée dans la communauté Pull Request (autant que je sache). Plus précisément, cela fonctionne comme ceci:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git Push # May need to force Push
...submit PR, wait for a review, make any changes requested for the PR
git rebase master
git Push # Will probably need to force Push (-f), due to previous rebases from master
...accept the PR, most likely also deleting the feature branch in the process
git checkout master
git branch -d my_new_feature
git remote Prune Origin

Je suis tombé amoureux de Git et je ne veux plus jamais revenir à SVN. Si vous avez du mal à vous en tenir, tenez-vous-en à cela et vous finirez par voir la lumière au bout du tunnel.

939
Micah

"Conflits" signifie "évolutions parallèles d'un même contenu". Donc, si cela va "tout à l'enfer" lors d'une fusion, cela signifie que vous avez des évolutions massives sur le même ensemble de fichiers.

La raison pour laquelle un rebase est alors préférable à une fusion est que:

  • vous réécrivez votre historique de commit local avec celui du maître (puis réappliquez votre travail, résolvant ainsi tout conflit éventuel)
  • la fusion finale sera certainement une "avance rapide", car elle contiendra tout l'historique de validation du maître, plus uniquement vos modifications à réappliquer.

Je confirme que dans ce cas, le flux de travaux correct (évolutions sur un ensemble de fichiers commun) est rebase d’abord, puis fusion.

Cependant, cela signifie que, si vous poussez votre branche locale (pour des raisons de sauvegarde), cette branche ne doit pas être extraite (ou du moins utilisée) par qui que ce soit (car l'historique de validation sera réécrit par la base successive).


Sur ce sujet (rebase puis fusion du workflow), barraponto mentionne dans les commentaires deux articles intéressants, tous deux de randyfay.com :

En utilisant cette technique, votre travail se place toujours au-dessus de la branche publique, comme un correctif mis à jour avec le HEAD actuel.

(une technique similaire existe pour Bazaar )

362
VonC

TL; DR

Un flux de travail git rebase ne vous protège pas des personnes qui ont une mauvaise résolution des conflits ou qui sont habituées à un flux de travail SVN, comme suggéré dans Eviter les désastres git: une histoire sanglante . Cela ne fait que rendre la résolution des conflits plus fastidieuse pour eux et rend plus difficile la reprise après une mauvaise résolution des conflits. Au lieu de cela, utilisez diff3 pour que ce ne soit pas si difficile en premier lieu.


Le workflow Rebase n’est pas meilleur pour la résolution des conflits!

Je suis très pro-rebase pour nettoyer l'histoire. Cependant, si je rencontre un conflit, j'abandonne immédiatement la base et effectue une fusion! Cela me tue vraiment que les gens recommandent un flux de travail de base comme meilleure alternative à un flux de fusion pour la résolution des conflits (ce qui est exactement le sujet de cette question).

Si ça va "tous les enfers" lors d'une fusion, ça ira "tous les enfers" lors d'un rebasement, et potentiellement beaucoup plus enfer aussi! Voici pourquoi:

Raison n ° 1: Résoudre les conflits une fois, au lieu d'une fois pour chaque commit

Lorsque vous rebasez au lieu de fusionner, vous devrez effectuer la résolution du conflit autant de fois que vous vous êtes engagé à le rebaser, pour le même conflit!

Scénario réel

Je branche hors de master pour refactoriser une méthode compliquée dans une branche. Mon travail de refactorisation comprend 15 commits au total, alors que je travaille à le refactoriser et à obtenir des révisions de code. Une partie de ma refactorisation consiste à réparer les onglets mixtes et les espaces présents auparavant dans master. Ceci est nécessaire, mais malheureusement, cela entrera en conflit avec toute modification apportée par la suite à cette méthode dans master. En effet, pendant que je travaille sur cette méthode, quelqu'un apporte un changement simple et légitime à la même méthode dans la branche maître, qui devrait être fusionné avec mes modifications.

Quand il est temps de fusionner ma branche avec maître, j'ai deux options:

fusion de git: Je reçois un conflit. Je vois le changement qu'ils ont fait pour le maîtriser et le fusionner avec (le produit final de) ma branche. Terminé.

git rebase: Je reçois un conflit avec mon premiercommit. Je résous le conflit et continue la rebase. Je suis en conflit avec mon secondecommit. Je résous le conflit et continue la rebase. Je suis en conflit avec mon tierscommit. Je résous le conflit et continue la rebase. Je suis en conflit avec mon quatrièmecommit. Je résous le conflit et continue la rebase. Je suis en conflit avec mon cinquièmecommit. Je résous le conflit et continue la rebase. Je suis en conflit avec mon sixièmecommit. Je résous le conflit et continue la rebase. Je suis en conflit avec mon septièmecommit. Je résous le conflit et continue la rebase. Je suis en conflit avec mon huitièmecommit. Je résous le conflit et continue la rebase. Je suis en conflit avec mon neuvièmecommit. Je résous le conflit et continue la rebase. Je suis en conflit avec mon dixièmecommit. Je résous le conflit et continue la rebase. Je reçois un conflit avec mon onzièmecommit. Je résous le conflit et continue la rebase. Je suis en conflit avec mon douzièmecommit. Je résous le conflit et continue la rebase. J'ai un conflit avec mon treizièmecommit. Je résous le conflit et continue la rebase. Je reçois un conflit avec mon quatorzièmecommit. Je résous le conflit et continue la rebase. Je suis en conflit avec mon quinzièmecommit. Je résous le conflit et continue la rebase.

Vous vous moquez de moi si ceciest votre flux de travail préféré. Tout ce qu’il faut, c’est une solution d’espace qui entre en conflit avec une modification apportée sur le maître; chaque commit sera en conflit et devra être résolu. Et ceci est un simplescénario avec seulement un conflit d'espaces. Dieu vous préserve d'un réel conflit impliquant des modifications importantes du code d'un fichier à un autre et vous devez résoudre celaplusieurs fois.

Avec toute la résolution de conflit supplémentaire que vous devez faire, cela augmente simplement la possibilité que vous fassiez une erreur . Mais les erreurs sont bien commises puisque vous pouvez annuler, non? Sauf bien sur ...

Raison n ° 2: avec rebase, il n'y a pas d'annulation!

Je pense que nous pouvons tous convenir que la résolution des conflits peut être difficile et que certaines personnes sont très mauvaises. Il peut être très sujet aux erreurs, ce qui explique pourquoi il est si génial que git le rend facile à annuler!

Lorsque vous fusionnez une branche, git crée un commit de fusion qui peut être ignoré ou modifié si la résolution du conflit ne va pas bien. Même si vous avez déjà poussé le commit de fusion incorrect vers le référentiel faisant autorité/public, vous pouvez utiliser git revert pour annuler les modifications introduites par la fusion et rétablir correctement la fusion dans un nouveau commit de fusion.

Lorsque vous rebasez une branche, dans l'éventualité probable que la résolution du conflit soit mal faite, vous êtes foutu. Chaque commit contient maintenant la mauvaise fusion et vous ne pouvez pas simplement refaire la rebase *. Au mieux, vous devez revenir en arrière et modifier chacun des commits concernés. Pas drôle.

Après un rebase, il est impossible de déterminer ce qui faisait initialement partie des commits et ce qui a été introduit à la suite d'une résolution de conflit incorrecte.

* Il peut être possible d'annuler une rebase si vous pouvez extraire les anciennes références des journaux internes de git, ou si vous créez une troisième branche qui pointe vers la dernière validation avant la refonte.

Sortez de la résolution des conflits: utilisez diff3

Prenons ce conflit par exemple:

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

En regardant le conflit, il est impossible de dire ce que chaque branche a changé ou quelle était son intention. C’est la principale raison, à mon avis, pour laquelle la résolution des conflits est difficile et confuse.

diff3 à la rescousse!

git config --global merge.conflictstyle diff3

Lorsque vous utilisez diff3, chaque nouveau conflit aura une 3ème section, l’ancêtre commun fusionné.

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
||||||| merged common ancestor
EmailMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

Examinons d’abord l’ancêtre commun fusionné. Ensuite, comparez chaque côté pour déterminer l'intention de chaque branche. Vous pouvez voir que HEAD a changé EmailMessage en TextMessage. Son objectif est de changer la classe utilisée en TextMessage, en passant les mêmes paramètres. Vous pouvez également voir que l'intention de cette branche de fonctionnalité est de transmettre false au lieu de true pour l'option: include_timestamp. Pour fusionner ces modifications, combinez l’intention des deux:

TextMessage.send(:include_timestamp => false)

En général:

  1. Comparez l'ancêtre commun avec chaque branche et déterminez quelle branche a le changement le plus simple.
  2. Appliquez cette modification simple à la version du code de l'autre branche, de sorte qu'elle contienne à la fois la modification la plus simple et la plus complexe
  3. Supprimez toutes les sections de code de conflit autres que celle dans laquelle vous venez de fusionner les modifications.

Autre solution: résoudre en appliquant manuellement les modifications de la branche

Enfin, certains conflits sont terribles à comprendre même avec diff3. Cela se produit surtout lorsque diff trouve des lignes communes qui ne sont pas sémantiquement communes (par exemple, il est arrivé que les deux branches aient une ligne vierge au même endroit!). Par exemple, une branche modifie l'indentation du corps d'une classe ou réordonne des méthodes similaires. Dans ces cas, une meilleure stratégie de résolution peut consister à examiner le changement des deux côtés de la fusion et à appliquer manuellement le diff à l'autre fichier.

Voyons comment résoudre un conflit dans un scénario où la fusion de Origin/feature1lib/message.rb est conflictuelle.

  1. Déterminez si notre branche actuellement extraite (HEAD ou --ours) ou la branche que nous fusionnons (Origin/feature1 ou --theirs) constitue une modification plus simple à appliquer. L'utilisation de diff avec un triple point (git diff a...b) montre les modifications survenues sur b depuis sa dernière divergence par rapport à a, ou en d'autres termes, comparez l'ancêtre commun de a et b avec b.

    git diff HEAD...Origin/feature1 -- lib/message.rb # show the change in feature1
    git diff Origin/feature1...HEAD -- lib/message.rb # show the change in our branch
    
  2. Découvrez la version plus compliquée du fichier. Cela supprimera tous les marqueurs de conflit et utilisera le côté que vous choisissez.

    git checkout --ours -- lib/message.rb   # if our branch's change is more complicated
    git checkout --theirs -- lib/message.rb # if Origin/feature1's change is more complicated
    
  3. Une fois le changement compliqué vérifié, relevez le diff du changement plus simple (voir l’étape 1). Appliquez chaque modification de ce diff au fichier en conflit.

374
Edward Anderson

Dans mon flux de travail, je rebase autant que possible (et j'essaie de le faire souvent. Ne pas laisser les divergences s'accumuler réduit considérablement l'ampleur et la gravité des collisions entre branches).

Cependant, même dans un flux de travail basé principalement sur une base de rebase, les fusions ont leur place.

Rappelez-vous que cette fusion crée en réalité un nœud qui a deux parents. Considérons maintenant la situation suivante: j'ai deux entités indépendantes A et B, et je souhaite maintenant développer des éléments sur la branche C, qui dépend de A et de B, tandis que A et B sont en cours de révision.

Ce que je fais alors, est le suivant:

  1. Créez (et vérifiez) la branche C au-dessus de A.
  2. Fusionner avec B

Maintenant, la branche C inclut les modifications de A et de B et je peux continuer à les développer. Si je modifie A, je reconstruis le graphe de branches de la manière suivante:

  1. créer la branche T sur le nouveau sommet de A
  2. fusionner T avec B
  3. rebaser C sur T
  4. supprimer la branche T

De cette façon, je peux réellement maintenir des graphiques de branches arbitraires, mais faire quelque chose de plus complexe que la situation décrite ci-dessus est déjà trop complexe, étant donné qu’il n’existe pas d’outil automatique pour redéfinir la base lorsque le parent change.

31
Alex Gontmakher

N'UTILISEZ PAS git Push Origin --mirror SOUS PRESQUE TOUT CIRCONSTANCE.

Il ne vous demande pas si vous êtes sûr de vouloir faire cela, et vous feriez mieux de l'être, car cela effacera toutes vos branches distantes qui ne se trouvent pas sur votre ordinateur local.

http://Twitter.com/dysinger/status/1273652486

21
Scott Brown

J'ai une question après avoir lu votre explication: se pourrait-il que vous n'ayez jamais fait

git checkout master
git pull Origin
git checkout my_new_feature

avant de faire le "maître de fusion/fusion git" dans votre branche de fonctionnalité?

Parce que votre branche maîtresse ne se mettra pas à jour automatiquement à partir du référentiel de vos amis. Vous devez faire cela avec le git pull Origin. C'est à dire. Peut-être voudriez-vous toujours vous baser sur une branche maître locale en constante évolution? Et puis, venez le temps Push, vous poussez dans un référentiel qui a des commits (locaux) que vous n'avez jamais vus et donc le Push échoue.

14
knweiss

Dans votre situation, je pense que votre partenaire a raison. Ce qui est bien avec le changement de base, c’est que, pour l’étranger, vos modifications semblent s’être déroulées dans un ordre propre. Ça signifie

  • vos modifications sont très faciles à revoir
  • vous pouvez continuer à faire Nice, de petits commits et pourtant vous pouvez rendre publics des ensembles de ces commits (en fusionnant avec master)
  • quand vous regardez la branche principale publique, vous voyez différentes séries de commits pour différentes fonctionnalités de différents développeurs, mais elles ne seront pas toutes mélangées

Vous pouvez toujours continuer à pousser votre branche de développement privée vers le référentiel distant pour des raisons de sauvegarde, mais les autres utilisateurs ne doivent pas la traiter comme une branche "publique", car vous allez changer de base. BTW, une commande facile à faire est git Push --mirror Origin.

L'article logiciel de packaging utilisant Git fait un assez bon travail en expliquant les compromis entre la fusion et le rebasement. Le contexte est un peu différent, mais les principes sont les mêmes. Il s’agit essentiellement de savoir si vos succursales sont publiques ou privées et de la manière dont vous envisagez de les intégrer dans le réseau principal.

13
Pat Notz

Quoi qu'il en soit, je suivais mon flux de travail sur une branche récente, et lorsque j'ai essayé de le fusionner pour devenir maître, tout est allé en enfer. Il y avait des tonnes de conflits avec des choses qui n'auraient pas dû avoir d'importance. Les conflits n'avaient tout simplement aucun sens pour moi. Il m'a fallu une journée pour tout régler, et a finalement abouti à un Push forcé vers le maître distant, puisque mon maître local avait résolu tous les conflits, mais que celui-ci n'était toujours pas heureux.

Ni dans le flux de travail de votre partenaire, ni dans ceux que vous suggérez, si vous avez rencontré des conflits insensés. Même si vous l'aviez fait, si vous suiviez les flux de travail suggérés, une résolution "forcée" ne devrait pas être nécessaire après la résolution. Cela suggère que vous n'avez pas réellement fusionné la branche sur laquelle vous étiez en train de pousser, mais que vous avez dû pousser une branche qui n'est pas un descendant du conseil distant.

Je pense que vous devez regarder attentivement ce qui s'est passé. Quelqu'un d'autre pourrait-il (délibérément ou non) rembobiner la branche maîtresse distante entre votre création de la branche locale et le point auquel vous avez tenté de la fusionner dans la branche locale?

Par rapport à de nombreux autres systèmes de contrôle de version, j'ai constaté que l'utilisation de Git impliquait moins de lutte contre l'outil et vous permettait de résoudre les problèmes fondamentaux pour vos flux sources. Git n'effectue pas la magie, donc les changements conflictuels causent des conflits, mais cela devrait faciliter la tâche d'écriture en traçant les liens de parenté.

12
CB Bailey

D'après ce que j'ai observé, la fusion de git a tendance à garder les branches séparées même après la fusion, alors que rebase puis la fusion la combine en une seule branche. Ce dernier ressort beaucoup plus propre, alors que dans le premier cas, il serait plus facile de savoir quelle commet appartient à quelle branche, même après la fusion.

7
Pepe

"Même si vous êtes un seul développeur avec seulement quelques branches, il vaut la peine de prendre l'habitude d'utiliser rebase et de fusionner correctement. Le schéma de travail de base ressemblera à ceci:

  • Créer une nouvelle branche B à partir de la branche existante A

  • Ajouter/valider les modifications sur la branche B

  • Rebase les mises à jour de la branche A

  • Fusionner les modifications de la branche B vers la branche A "

https://www.atlassian.com/git/tutorials/merging-vs-rebasing/

6
Rakka Rage

Avec Git, il n’ya pas de workflow "correct". Utilisez ce qui flotte sur votre bateau. Cependant, si vous rencontrez constamment des conflits lors de la fusion de branches, vous devriez peut-être mieux coordonner vos efforts avec ceux de vos collègues développeurs? On dirait que vous continuez à éditer les mêmes fichiers. Faites également attention aux mots-clés espaces et Subversion ("$ Id $" et autres).

2
Bombe