Node:Conceptos básicos sobre ramas, Next:Fusión de cambios desde las ramas al tronco, Up:Ramas
¿Por qué son útiles las ramas?
Volvamos por un momento a la situación del desarrollador que, mientras está trabajando en una nueva versión del programa, recibe un informe de fallo relativo a una versión publicada anteriormente. Suponiendo que el desarrollador corrija el problema, aún tiene que encontrar una forma de enviar la corrección al cliente. No le servirá de nada limitarse a tomar una copia vieja del programa, parchearla a espaldas de CVS, y enviarla tal cual: no quedaría registro alguno de lo que ha hecho, CVS no sabría nada de esta corrección, y más adelante, si por un casual se descubriera un fallo en el propio parche, nadie tendría un punto desde el que comenzar a intentar reproducir el problema.
Es incluso peor intentar solucionar el fallo en la versión actual e inestable de las fuentes y entregar esto al cliente. Oh sí, el fallo del que se ha dado parte quizás quedase resuelto, pero el resto del código está a medio cocer y por lo general falto de un proceso de pruebas pertinente. Puede darse el caso de que funcione, pero es seguro que no está listo para llegar al gran público.
Dado que se supone que la última versión distribuida es estable (dejando aparte este fallo), la solución ideal es ir atrás y corregir el fallo en la antigua versión; esto es, crear un universo alternativo en el que la última versión pública incluye la correspondiente corrección.
Y aquí es donde entran en juego las ramas. El desarrollador planta una rama, que parte de la línea principal de desarrollo (el tronco), no en su revisiones más recientes, sino en el punto de la última distribución pública. Entonces el desarrollador solicita una copia de trabajo de esta rama, realiza todos los cambios necesarios para solventar el fallo, y los envía a esa rama, de forma que quede un registro de la corrección del fallo. Ahora puede hacer público un parche intermedio basado en esta rama, y enviarlo al cliente.
Su cambio no habrá afectado al código del tronco, puesto que tampoco le interesaría hacerlo sin antes averiguar si el tronco necesita o no que se aplique sobre él la misma corrección. En caso positivo, el desarrollador puede fusionar los cambios realizados sobre la rama con el código presente en el tronco. Durante el proceso de fusión, CVS calcula los cambios hechos en la rama desde el momento en el que ésta salió del tronco hasta el extremo de la misma (su estado más reciente), y entonces aplica estas diferencias al proyecto, en el extremo final del tronco. La diferencia entre la raíz de la rama y su extremo final vendría a ser, por supuesto, la corrección que se ha realizado.
Otra buena forma de imaginar una fusión es como un caso especial del proceso de actualización; la diferencia estriba en que, durante una fusión, los cambios a incorporar se derivan de la comparación entre la raíz y el extremo de la rama, en lugar de comparar la copia de trabajo con el contenido del repositorio.
El proceso de actualización es en sí mismo similar a recibir parches directamente de los autores, y aplicarlos a mano; de hecho, para hacer una actualización, CVS calcula la diferencia (entendiendo como "diferencia" el resultado que devolvería el comando diff al comparar dos ficheros distintos) entre la copia de trabajo y el repositorio, para a continuación aplicar la diferencia a la copia de trabajo tal y como lo haría el programa "patch". Esto equivale a la forma en que un desarrollador obtiene cambios del mundo exterior, aplicando manualmente parches creados por otros.
Así pues, fusionar con el tronco la rama donde se ha hecho la corrección
es exactamente lo mismo que aceptar un parche que ha hecho otra persona
para corregir el fallo. El autor de ese parche habría generado el parche
a partir de la última versión hecha pública, de la misma forma que los
cambios en la rama lo son respecto a esa versión. Si esa parte del código
en las fuentes actuales no ha variado mucho desde la última versión pública,
la fusión tendrá éxito sin ningún problema. Ahora bien, si el código es
en este momento lo suficientemente diferente, la fusión derivará en
conflicto (en otras palabras, el parche será rechazado), y será necesario
cierto trabajo extra. Normalmente esto se resuelve examinando la parte
donde ha surgido el conflicto, haciendo manualmente los cambios necesarios,
y enviando esos cambios al repositorio. La Figura 2.3 muestra gráficamente
lo que sucede en una rama y en una fusión.
(rama donde se corrigió el error) .---------------->---------------. / | / | / | / | / V (<---- lugar de la fusión) ====*===================================================================> (línea principal de desarrollo) [Figura 2.3: Una rama que termina con una fusión. El tiempo transcurre de izquierda a derecha.]
Ahora vamos a ver los pasos necesarios para llevar a cabo el procedimiento descrito. Recuerde que no es realmente el tiempo lo que fluye de izquierda a derecha en el diagrama, sino más bien el historial de revisiones. La rama no se habrá hecho en el momento de la distribución, sino que es creada más tarde, aunque enraizada en las revisiones que formaban parte de la distribución.
En nuestro caso, supongamos que los ficheros del proyecto han pasado por
muchas revisiones desde que fueron marcados como Release-1999_05_01
, y que
quizás se hayan añadido también nuevos ficheros. Al recibir el informe de fallos
relativo a la antigua distribución, lo primero que querremos hacer será crear
una rama que parta de la antigua distribución, que tuvimos el acierto de marcar
conmo Release-1999_05_01
.
Una forma de hacer esto sería obtener primero una copia de trabajo basada
en dicha marca, y a continuación crear la rama volviendo a marcar con la
opción -b (de "branch", o "rama" en inglés - N. del T.):
floss$ cd .. floss$ ls miproyecto/ floss$ cvs -q checkout -d miproyecto_antigua_dis -r Release-1999_05_01 miproyecto U miproyecto_antigua_dis/README.txt U miproyecto_antigua_dis/hello.c U miproyecto_antigua_dis/a-subdir/loquesea.c U miproyecto_antigua_dis/a-subdir/subsubdir/fish.c U miproyecto_antigua_dis/b-subdir/random.c floss$ ls miproyecto/ miproyecto_antigua_dis/ floss$ cd miproyecto_antigua_dis floss$ ls CVS/ README.txt a-subdir/ b-subdir/ hello.c floss$ cvs -q tag -b Release-1999_05_01-bugfixes T README.txt T hello.c T a-subdir/loquesea.c T a-subdir/subsubdir/fish.c T b-subdir/random.c floss$
Observe bien el último comando. Puede parecer un tanto arbitrario el utilizar
"tag" para crear ramas, pero en realidad hay una razón para ello: el nombre
de la marca servirá como una etiqueta con la cual podremos más tarde hacer
alusión a esta rama. Las marcas en las ramas no tienen un aspecto diferente
al de las utilizadas en otra parte, y de hecho están sujetas a las mismas
limitaciones. A algunas personas les gusta incluir siempre la palabra "rama"
en el nombre de la marca (por ejemplo Release-1999_05_01-ramadecorrección
) para
poder distinguir fácilmente entre marcas de rama y otros tipos de marcas. Tal
vez le interese también a usted hacer lo mismo si alguna que otra vez se
confunde y solicita la rama equivocada.
(Y ya que estamos, observe la opción -d miproyecto_antigua_dis que pasamos al comando "checkout" en la primer comando CVS. Esto le dice a checkout que instale la copia de trabajo en un directorio llamado miproyecto_antigua_dis, de forma que no confundamos estos ficheros con la versión actual de miproyecto. Tenga cuidado de no confundir este uso de la -d con la opción global homónima, o con la opción -d del comando "update".)
Por supuesto, la simple ejecución del comando "tag" no pone la copia de trabajo en consonancia con la rama. El hecho de marcar no afecta nunca a la copia de trabajo; tan sólo guarda información adicional en el repositorio para permitirle a usted recuperar en un momento posterior las revisiones de esa copia de trabajo (como una parte estática del historial o como una rama, según el caso).
La recuperación puede hacerse de dos formas (a estas alturas, seguramente
ya se esperaba oir esto). Puede solicitar una nueva copia de trabajo tomada
de la rama:
floss$ pwd /home/loquesea floss$ cvs co -d miproyecto_rama -r Release-1999_05_01-bugfixes miproyecto
o pasar a ella una copia de trabajo ya existente:
floss$ pwd /home/loquesea/miproyecto floss$ cvs update -r Release-1999_05_01-bugfixes
El resultado final es el mismo (bueno, el nombre del directorio raíz de la nueva copia de trabajo puede ser distinto, pero respecto a los fines de CVS esto no importa). Si su copia de trabajo actual tiene cambios aún sin enviar, probablemente querrá utilizar "checkout" en lugar de "update" para acceder a la rama; de lo contrario, CVS intentará fusionar los cambios habidos en su copia de trabajo antes de colocarla en la rama. En este caso podría encontrarse con algún conflicto, y aún en caso de que no fuese así, seguiría sin tener una rama pura: esos ficheros no reflejarán realmente el estado del programa de acuerdo con la marca designada, puesto que algunos de ellos contendrán modificaciones hechas por usted.
Sea como fuere, vamos a suponer que de una forma o de otra usted obtiene
una copia de trabajo operativa desde la rama deseada:
floss$ cvs -q status hello.c =================================================================== File: hello.c Status: Up-to-date Working revision: 1.5 Tue Apr 20 06:12:56 1999 Repository revision: 1.5 /usr/local/cvs/miproyecto/hello.c,v Sticky Tag: Release-1999_05_01-bugfixes (branch: 1.5.2) Sticky Date: (none) Sticky Options: (none) floss$ cvs -q status b-subdir/random.c =================================================================== File: random.c Status: Up-to-date Working revision: 1.2 Mon Apr 19 06:35:27 1999 Repository revision: 1.2 /usr/local/cvs/miproyecto/b-subdir/random.c,v Sticky Tag: Release-1999_05_01-bugfixes (branch: 1.2.2) Sticky Date: (none) Sticky Options: (none) floss$
(El contenido de las líneas Sticky Tag
se explicará en
breve.) Si modifica hello.c y random.c y envía los cambios al repositorio,
floss$ cvs -q update M hello.c M b-subdir/random.c floss$ cvs ci -m "corregidos los viejos fallos de puntuación" cvs commit: Examining . cvs commit: Examining a-subdir cvs commit: Examining a-subdir/subsubdir cvs commit: Examining b-subdir Checking in hello.c; /usr/local/cvs/miproyecto/hello.c,v <- hello.c new revision: 1.5.2.1; previous revision: 1.5 done Checking in b-subdir/random.c; /usr/local/cvs/miproyecto/b-subdir/random.c,v <- random.c new revision: 1.2.2.1; previous revision: 1.2 done floss$
se dará cuenta de que ocurre algo curioso con los números de revisión:
floss$ cvs -q status hello.c b-subdir/random.c =================================================================== File: hello.c Status: Up-to-date Working revision: 1.5.2.1 Wed May 5 00:13:58 1999 Repository revision: 1.5.2.1 /usr/local/cvs/miproyecto/hello.c,v Sticky Tag: Release-1999_05_01-bugfixes (branch: 1.5.2) Sticky Date: (none) Sticky Options: (none) =================================================================== File: random.c Status: Up-to-date Working revision: 1.2.2.1 Wed May 5 00:14:25 1999 Repository revision: 1.2.2.1 /usr/local/cvs/miproyecto/b-subdir/random.c,v Sticky Tag: Release-1999_05_01-bugfixes (branch: 1.2.2) Sticky Date: (none) Sticky Options: (none) floss$
¡Ahora tienen cuatro dígitos en lugar de dos!
Un vistazo más de cerca nos revela que el número de revisión de cada fichero
es simplemente el número de la rama (tal como se indica en la línea Sticky Tag
),
con un dígito extra al final.
Lo que está presenciando es tan sólo una parte del funcionamiento interno
de CVS. Aunque casi siempre utilizará una rama para marcar una divergencia
que afecte a la globalidad del proyecto, en realidad CVS registra la rama
de forma individual, fichero a fichero. Este proyecto tenía cinco
ficheros en el momento de crear la rama, así que en realidad se han
creado cinco ramas, todas ellas con la misma marca: Release-1999_05_01-bugfixes
.
La mayoría de la gente considera esta forma de hacer las cosas como una implantación bastante poco elegante por parte de CVS, pero en realidad lo que estamos viendo aquí es parte del legado de RCS: RCS no sabía cómo agrupar ficheros en los proyectos, y a pesar de que CVS sí lo hace, sigue utilizando código heredado de RCS para manejar las ramas.
Por regla general, usted no necesitará preocuparse demasiado por cómo CVS registra las cosas de forma interna, pero en este caso, resulta útil comprender la relación que existe entre números de ramas y números de revisiones. Veamos el fichero hello.c; todo lo que estoy a punto de decirle sobre hello.c se aplica a cualquier otro fichero presente en la rama, cambiando los números de revisión y de rama según convenga.
En el momento del nacimiento de la rama, el fichero hello.c se encontraba
en su revisión 1.5. Cuando creamos la rama, se añadió un nuevo número al
final para así formar un número de rama (CVS elige el primer número entero
par que no sea cero y que esté libre). Por tanto, en este caso, el número
de rama terminó siendo 1.5.2
. El número de la rama no es en sí mismo
un número de revisión, pero sí es la raíz (es decir, el prefijo) de todos
los números de revisión para hello.c que se emplearán en esta rama.
Sin embargo, cuando ejecutamos aquel primer comando "CVS status" en una
copia de trabajo ramificada, el número de revisión de hello.c apareció
como 1.5
solamente, y no como 1.5.2.0
o algo parecido. Esto se
debe a que la revisión inicial de una rama es siempre la misma que la
revisión que el fichero tiene en el tronco, donde nació la rama. Por tanto,
CVS mostrará el número de revisión del tronco en el informe de estado
mientras el fichero sea el mismo tanto en la rama como en el tronco.
Una vez que enviamos una nueva revisión al repositorio, hello.c ya no era
igual en el tronco que en la rama: la copia que estaba en la rama había
cambiado, mientras que la copia presente en el tronco seguía igual. Es
por ello por lo que se asignó a hello.c su primer número de revisión de
rama, tal como pudimos comprobar después de hacer el envío en el informe
de estado, donde su número de revisión aparecía claramente como 1.5.2.1
.
Esta misma situación se aplica al fichero random.c. Su número de
revisión en el momento de crear la rama era 1.2
, así que su
primera rama es 1.2.2
, y el primer nuevo envío de random.c en
esta rama recibió el número de revisión 1.2.2.1
.
No existe ninguna relación numérica entre 1.5.2.1
y 1.2.2.1
.
No hay razón alguna para pensar que forman parte de la misma rama
excepto por el hecho de que ambos ficheros están marcados con
Release-1999_05_01-bugfixes
, y que la marca está asociada a los
números de rama 1.5.2
y 1.2.2
en los respectivos ficheros.
Por tanto, el nombre de la marca es su único recurso en la rama para
concebirla como una entidad global. Aunque es perfectamente posible
trasladar un fichero a una rama usando directamente el número de revisión,
floss$ cvs update -r 1.5.2.1 hello.c U hello.c floss$
casi siempre es una mala idea hacerlo, puesto que estaría mezclando la revisión en la rama de un fichero con las revisiones fuera de rama de otros. ¿Quién sabe qué ficheros podría perder? Es mejor usar la marca de la rama para referirse a la rama y tratar todos los ficheros de una sola vez, evitando referirnos a ningún fichero en concreto; de esta forma no tiene que conocer ni preocuparse del número de revisión de rama de ningún fichero en particular.
También es posible hacer ramas que nacen de otras ramas, hasta llegar
a niveles que podrían considerarse absurdos. Por ejemplo, si un fichero
tuviese el número de revisión 1.5.4.37.2.3
, el historial de sus
revisiones podría esquematizarse con algo como esto:
1.1 | 1.2 | 1.3 | 1.4 | 1.5 / \ / \ / \ (1.5.2) (1.5.4) <--- (éstos son números de ramas) / \ 1.5.2.1 1.5.4.1 | | 1.5.2.2 1.5.4.2 | | (etc) (...) <--- (34 revisiones omitidas por brevedad) | 1.5.4.37 / / (1.5.4.37.2) <--- (esto es también un número de rama) / / 1.5.4.37.2.1 | 1.5.4.37.2.2 | 1.5.4.37.2.3 [Figura 2.4: Un número extrañamente elevado de ramificaciones. El tiempo transcurre hacia abajo.]
Naturalmente, sólo circunstancias muy especiales harían necesario tal
grado de ramificaciones, pero, ¿no es agradable saber que CVS llegará
todo lo lejos que usted se proponga? Las ramas anidadas se crean de la misma
forma que cualquier otra rama: obtenga una copia de trabajo de la rama
N
, ejecute "cvs tag -b nombre_de_rama" sobre ella, y de esta forma
creará la rama N.M
en el repositorio (donde N
representa el
número de revisión de rama apropiado en cada fichero, como por ejemplo
1.5.2.1
, mientras que M
representa la siguiente rama
disponible al final de ese número, como por ejemplo 2
).