Node:Conceptos básicos sobre ramas, Next:, Up:Ramas



Conceptos básicos sobre 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).