L'affirmation de la raison pour laquelle la fusion est meilleure dans un DVCS que dans Subversion était largement basée sur la façon dont la branche et la fusion fonctionnaient dans Subversion il y a quelque temps. Subversion antérieure à 1.5.0 ne stockait aucune information sur la fusion des branches, donc lorsque vous vouliez fusionner, vous deviez spécifier la plage de révisions à fusionner.
Alors pourquoi les fusions de Subversion sont-elles nulles ?
Méditez sur cet exemple:
1 2 4 6 8
trunk o-->o-->o---->o---->o
\
\ 3 5 7
b1 +->o---->o---->o
Lorsque nous voulons fusionner les modifications de b1 dans le tronc, nous émettons la commande suivante, tout en se tenant sur un dossier dont le tronc a été extrait:
svn merge -r 2:7 {link to branch b1}
… Qui tentera de fusionner les modifications de b1
dans votre répertoire de travail local. Et puis vous validez les modifications après avoir résolu tous les conflits et testé le résultat. Lorsque vous validez, l'arbre de révision ressemblerait à ceci:
1 2 4 6 8 9
trunk o-->o-->o---->o---->o-->o "the merge commit is at r9"
\
\ 3 5 7
b1 +->o---->o---->o
Cependant, cette façon de spécifier les plages de révisions devient rapidement incontrôlable lorsque l'arborescence des versions se développe, car subversion n'avait pas de métadonnées sur le moment et les révisions fusionnées. Réfléchissez à ce qui se passera plus tard:
12 14
trunk …-->o-------->o
"Okay, so when did we merge last time?"
13 15
b1 …----->o-------->o
C'est en grande partie un problème lié à la conception du référentiel de Subversion, afin de créer une branche, vous devez créer un nouveau répertoire virtuel dans le référentiel qui hébergera une copie du tronc mais il ne stocke aucune information concernant quand et quoi les choses ont de nouveau fusionné. Cela conduira parfois à de mauvais conflits de fusion. Ce qui était encore pire, c'est que Subversion a utilisé la fusion bidirectionnelle par défaut, ce qui présente certaines limitations paralysantes dans la fusion automatique lorsque deux têtes de branche ne sont pas comparées à leur ancêtre commun.
Pour atténuer cette Subversion stocke désormais les métadonnées pour la branche et la fusion. Cela résoudrait tous les problèmes, non?
Et oh, au fait, Subversion est toujours nul…
Sur un système centralisé, comme la subversion, les répertoires virtuels sont nuls. Pourquoi? Parce que tout le monde a accès pour les voir… même les déchets expérimentaux. La ramification est bonne si vous voulez expérimenter mais vous ne voulez pas voir l'expérimentation de tout le monde et de ses tantes . Il s'agit d'un grave bruit cognitif. Plus vous ajoutez de branches, plus vous verrez de conneries.
Plus vous avez de branches publiques dans un référentiel, plus il sera difficile de garder une trace de toutes les différentes branches. Donc, la question que vous vous posez est de savoir si la branche est toujours en développement ou si elle est vraiment morte, ce qui est difficile à dire dans un système de contrôle de version centralisé.
La plupart du temps, d'après ce que j'ai vu, une organisation utilisera par défaut une grande branche de toute façon. Ce qui est dommage car à son tour, il sera difficile de garder une trace des versions de test et de sortie, et tout ce qui est bon vient de la branche.
Alors pourquoi les DVCS, tels que Git, Mercurial et Bazaar, sont-ils meilleurs que Subversion pour créer des branches et fusionner?
Il y a une raison très simple: la ramification est un concept de première classe . Il n'y a pas de répertoires virtuels de par leur conception et les branches sont des objets durs dans DVCS qui doivent être tels pour fonctionner simplement avec la synchronisation des référentiels (c'est -à- dire pousser et tirer ).
La première chose que vous faites lorsque vous travaillez avec un DVCS est de cloner des référentiels (git clone
, hg clone
et bzr branch
). Le clonage est conceptuellement la même chose que la création d'une branche dans le contrôle de version. Certains appellent cela une fourche ou une ramification (bien que cette dernière soit souvent utilisée pour désigner des branches colocalisées), mais c'est la même chose. Chaque utilisateur exécute son propre référentiel, ce qui signifie que vous avez une branche par utilisateur en cours.
La structure de la version n'est pas un arbre , mais plutôt un graphique à la place. Plus précisément un graphe acyclique dirigé (DAG, c'est-à-dire un graphe sans cycle). Vous n'avez vraiment pas besoin de vous attarder sur les spécificités d'un DAG autre que chaque commit a une ou plusieurs références parentes (sur lesquelles était basé le commit). Les graphiques suivants montreront donc les flèches entre les révisions à l'envers à cause de cela.
Un exemple très simple de fusion serait celui-ci; imaginez un référentiel central appelé origin
et un utilisateur, Alice, clonant le référentiel sur sa machine.
a… b… c…
origin o<---o<---o
^master
|
| clone
v
a… b… c…
alice o<---o<---o
^master
^origin/master
Ce qui se passe pendant un clone est que chaque révision est copiée dans Alice exactement comme elle était (ce qui est validé par les identifiants de hachage identifiables de manière unique), et marque où se trouvent les branches de l'origine.
Alice travaille ensuite sur son dépôt, s'engageant dans son propre référentiel et décide de pousser ses modifications:
a… b… c…
origin o<---o<---o
^ master
"what'll happen after a push?"
a… b… c… d… e…
alice o<---o<---o<---o<---o
^master
^origin/master
La solution est plutôt simple, la seule chose que le origin
référentiel doit faire est de prendre en compte toutes les nouvelles révisions et de déplacer sa branche vers la dernière révision (que git appelle "fast-forward"):
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
a… b… c… d… e…
alice o<---o<---o<---o<---o
^master
^origin/master
Le cas d'utilisation, que j'ai illustré ci-dessus, n'a même pas besoin de fusionner quoi que ce soit . Le problème n'est donc pas vraiment lié à la fusion des algorithmes, car l'algorithme de fusion à trois voies est à peu près le même entre tous les systèmes de contrôle de version. Le problème concerne plus la structure qu'autre chose .
Alors que diriez-vous de me montrer un exemple qui a une vraie fusion?
Certes, l'exemple ci-dessus est un cas d'utilisation très simple, alors faisons-en un bien plus tordu bien que plus courant. Rappelez-vous que cela a origin
commencé avec trois révisions? Eh bien, le gars qui les a fait, appelons-le Bob , a travaillé seul et a fait un commit sur son propre référentiel:
a… b… c… f…
bob o<---o<---o<---o
^ master
^ origin/master
"can Bob push his changes?"
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
Maintenant, Bob ne peut pas pousser ses modifications directement vers le origin
référentiel. La façon dont le système le détecte consiste à vérifier si les révisions de Bob descendent directement de celles origin
de, ce qui n'est pas le cas dans ce cas. Toute tentative de poussée entraînera dans le système quelque chose qui s'apparente à " Euh ... je crains que je ne puisse pas vous laisser faire ça Bob ."
Donc, Bob doit faire un pull-in puis fusionner les modifications (avec git's pull
, ou hg's pull
et merge
; or bzr's merge
). Il s'agit d'un processus en deux étapes. Bob doit d'abord récupérer les nouvelles révisions, qui les copieront telles quelles depuis le origin
référentiel. On voit maintenant que le graphique diverge:
v master
a… b… c… f…
bob o<---o<---o<---o
^
| d… e…
+----o<---o
^ origin/master
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
La deuxième étape du processus d'extraction consiste à fusionner les conseils divergents et à valider le résultat:
v master
a… b… c… f… 1…
bob o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
^ origin/master
Espérons que la fusion ne rencontrera pas de conflits (si vous les anticipez, vous pouvez effectuer les deux étapes manuellement dans git avec fetch
et merge
). Ce qui doit être fait plus tard est de réintroduire ces modifications dans origin
, ce qui entraînera une fusion rapide car la validation de la fusion est une descendante directe de la dernière du origin
référentiel:
v origin/master
v master
a… b… c… f… 1…
bob o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
v master
a… b… c… f… 1…
origin o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
Il existe une autre option pour fusionner dans git et hg, appelée rebase , qui déplacera les modifications de Bob après les dernières modifications. Comme je ne veux pas que cette réponse soit plus verbeuse, je vous laisse plutôt lire les documents git , mercurial ou bazaar à ce sujet.
En tant qu'exercice pour le lecteur, essayez de découvrir comment cela fonctionnera avec un autre utilisateur impliqué. Il en est de même pour l'exemple ci-dessus avec Bob. La fusion entre référentiels est plus facile que vous ne le pensez car toutes les révisions / validations sont identifiables de manière unique.
Il y a aussi le problème de l'envoi de correctifs entre chaque développeur, ce qui était un énorme problème dans Subversion qui est atténué dans git, hg et bzr par des révisions identifiables de manière unique. Une fois que quelqu'un a fusionné ses modifications (c'est-à-dire qu'il a effectué une validation de fusion) et l'envoie pour que tous les autres membres de l'équipe les consomment en les poussant vers un référentiel central ou en envoyant des correctifs, ils n'ont plus à se soucier de la fusion, car cela s'est déjà produit . Martin Fowler qualifie cette façon de travailler d' intégration de promiscuité .
Étant donné que la structure est différente de Subversion, en utilisant à la place un DAG, elle permet de créer des branchements et des fusions de manière plus simple non seulement pour le système mais aussi pour l'utilisateur.