La afirmación de por qué la fusión es mejor en un DVCS que en Subversion se basó en gran medida en cómo la ramificación y la fusión funcionaron en Subversion hace un tiempo. Subversion anterior a 1.5.0 no almacenaba ninguna información sobre cuándo se fusionaron las sucursales, por lo tanto, cuando deseaba fusionar, tenía que especificar qué rango de revisiones debía fusionar.
Entonces, ¿por qué las fusiones de Subversion apestan ?
Medita este ejemplo:
1 2 4 6 8
trunk o-->o-->o---->o---->o
\
\ 3 5 7
b1 +->o---->o---->o
Cuando queremos fusionar los cambios de b1 en el tronco, emitimos el siguiente comando, mientras estamos parados en una carpeta que tiene el tronco extraído:
svn merge -r 2:7 {link to branch b1}
... que intentará fusionar los cambios b1
en su directorio de trabajo local. Y luego confirma los cambios después de resolver cualquier conflicto y probar el resultado. Cuando confirme el árbol de revisión se vería así:
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
Sin embargo, esta forma de especificar rangos de revisiones se pierde rápidamente cuando el árbol de versiones crece, ya que Subversion no tenía metadatos sobre cuándo y qué revisiones se fusionaron. Medita sobre lo que sucede después:
12 14
trunk …-->o-------->o
"Okay, so when did we merge last time?"
13 15
b1 …----->o-------->o
Esto es en gran parte un problema por el diseño del repositorio que tiene Subversion, para crear una rama necesita crear un nuevo directorio virtual en el repositorio que albergará una copia del tronco pero no almacena ninguna información sobre cuándo y qué las cosas se fusionaron nuevamente. Eso conducirá a desagradables conflictos de fusión a veces. Lo que era aún peor es que Subversion usaba la fusión bidireccional por defecto, lo que tiene algunas limitaciones paralizantes en la fusión automática cuando dos cabezas de rama no se comparan con su antepasado común.
Para mitigar esta Subversión ahora almacena metadatos para ramificar y fusionar. Eso resolvería todos los problemas, ¿verdad?
Y, por cierto, Subversion todavía apesta ...
En un sistema centralizado, como la subversión, los directorios virtuales apestan. ¿Por qué? Porque todos tienen acceso para verlos ... incluso los de basura experimentales. La ramificación es buena si quieres experimentar pero no quieres ver la experimentación de todos y sus tías . Este es un ruido cognitivo grave. Cuantas más ramas agregue, más basura podrá ver.
Cuantas más ramas públicas tenga en un repositorio, más difícil será hacer un seguimiento de todas las diferentes ramas. Entonces, la pregunta que tendrá es si la rama aún está en desarrollo o si está realmente muerta, lo cual es difícil de distinguir en cualquier sistema de control de versiones centralizado.
La mayoría de las veces, por lo que he visto, una organización usará por defecto una gran sucursal de todos modos. Lo cual es una pena porque a su vez será difícil hacer un seguimiento de las versiones de prueba y lanzamiento, y cualquier otra cosa buena proviene de la ramificación.
Entonces, ¿por qué los DVCS, como Git, Mercurial y Bazaar, son mejores que Subversion en la ramificación y fusión?
Hay una razón muy simple: la ramificación es un concepto de primera clase . No hay directorios virtuales por diseño y las ramas son objetos duros en DVCS que deben ser tales para poder trabajar simplemente con la sincronización de repositorios (es decir, push and pull ).
Lo primero que debe hacer cuando trabaja con un DVCS es clonar repositorios (git's clone
, hg's clone
y bzr's branch
). La clonación es conceptualmente lo mismo que crear una rama en el control de versiones. Algunos llaman a esto bifurcación o bifurcación (aunque este último también se usa a menudo para referirse a sucursales ubicadas conjuntamente), pero es exactamente lo mismo. Cada usuario ejecuta su propio repositorio, lo que significa que tiene una ramificación por usuario .
La estructura de la versión no es un árbol , sino un gráfico . Más específicamente, un gráfico acíclico dirigido (DAG, que significa un gráfico que no tiene ningún ciclo). Realmente no es necesario profundizar en los detalles de un DAG que no sea cada confirmación tiene una o más referencias principales (en lo que se basa la confirmación). Entonces, los siguientes gráficos mostrarán las flechas entre las revisiones en reversa debido a esto.
Un ejemplo muy simple de fusión sería este; imagine un repositorio central llamado origin
y una usuaria, Alice, clonando el repositorio en su máquina.
a… b… c…
origin o<---o<---o
^master
|
| clone
v
a… b… c…
alice o<---o<---o
^master
^origin/master
Lo que sucede durante un clon es que todas las revisiones se copian a Alice exactamente como estaban (lo que se valida por los hash-id identificables de forma única) y marca dónde están las ramas del origen.
Luego, Alice trabaja en su repositorio, se compromete en su propio repositorio y decide impulsar sus cambios:
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 solución es bastante simple, lo único que origin
debe hacer el repositorio es tomar todas las revisiones nuevas y mover su rama a la revisión más reciente (que git llama "avance rápido"):
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
El caso de uso, que ilustré arriba, ni siquiera necesita fusionar nada . Entonces, el problema realmente no es con los algoritmos de fusión, ya que el algoritmo de fusión de tres vías es prácticamente el mismo entre todos los sistemas de control de versiones. El problema es más sobre la estructura que cualquier otra cosa .
Entonces, ¿qué tal si me muestras un ejemplo que tiene una fusión real ?
Es cierto que el ejemplo anterior es un caso de uso muy simple, así que hagamos uno mucho más retorcido, aunque más común. ¿Recuerdas que origin
comenzó con tres revisiones? Bueno, el tipo que los hizo, vamos a llamarlo Bob , ha estado trabajando por su cuenta e hizo un compromiso en su propio repositorio:
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
Ahora Bob no puede enviar sus cambios directamente al origin
repositorio. La forma en que el sistema detecta esto es verificando si las revisiones de Bob descienden directamente de origin
las de él, lo que en este caso no. Cualquier intento de presionar dará como resultado que el sistema diga algo parecido a " Uh ... me temo que no puedo dejar que hagas eso Bob ".
Entonces Bob tiene que ingresar y luego fusionar los cambios (con git pull
; o hg's pull
y merge
; o bzr's merge
). Este es un proceso de dos pasos. Primero Bob tiene que buscar las nuevas revisiones, que las copiarán tal como están desde el origin
repositorio. Ahora podemos ver que el gráfico 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
El segundo paso del proceso de extracción es fusionar los consejos divergentes y confirmar el resultado:
v master
a… b… c… f… 1…
bob o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
^ origin/master
Esperemos que la fusión no tenga conflictos (si los anticipa, puede hacer los dos pasos manualmente en git con fetch
y merge
). Lo que más adelante debe hacerse es introducir esos cambios nuevamente origin
, lo que dará como resultado una fusión de avance rápido ya que la confirmación de fusión es un descendiente directo de lo último en el origin
repositorio:
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<--+
Hay otra opción para fusionarse en git y hg, llamada rebase , que moverá los cambios de Bob a los cambios más recientes. Como no quiero que esta respuesta sea más detallada, te dejaré leer los documentos de git , mercurial o bazar sobre eso.
Como ejercicio para el lector, intente dibujar cómo funcionará con otro usuario involucrado. Se hace de manera similar al ejemplo anterior con Bob. La fusión entre repositorios es más fácil de lo que parece porque todas las revisiones / confirmaciones son identificables de forma exclusiva.
También está el problema de enviar parches entre cada desarrollador, que fue un gran problema en Subversion que se mitiga en git, hg y bzr mediante revisiones identificables de forma única. Una vez que alguien ha fusionado sus cambios (es decir, ha realizado una confirmación de fusión) y lo envía para que todos los demás en el equipo lo consuman, ya sea presionando a un repositorio central o enviando parches, entonces no tienen que preocuparse por la fusión, porque ya sucedió . Martin Fowler llama a esta forma de trabajar la integración promiscua .
Debido a que la estructura es diferente de Subversion, al emplear un DAG, permite que la ramificación y la fusión se realicen de una manera más fácil no solo para el sistema sino también para el usuario.