Copiando cambios entre ramas

Ahora usted y Carmen están trabajando en ramas paralelas del proyecto: usted está trabajando en una rama privada, y Carmen está trabajando en el tronco, o línea principal de desarrollo.

En proyectos que tienen grandes cantidades de contribuyentes, es habitual que la mayoría tengan copias locales del tronco. Cuando alguien necesita hacer un cambio a largo plazo que puede molestar en el tronco, un procedimiento estándar es crear una rama privada y realizar ahí los cambios hasta que todo el trabajo esté completo.

Así que las buenas noticias son que usted y Carmen no están interfiriendo en sus trabajos. Las malas noticias son que es muy fácil derivar demasiado lejos. Recuerde que uno de los problemas con la estrategia esconderse en un agujero es que para cuando haya terminado con su rama, puede que sea casi imposible fusionar los cambios con el tronco sin un alto número de conflictos.

En su lugar, usted y Carmen pueden continuar compartiendo cambios a medida que trabajan. Es tarea suya decidir qué cambios merece la pena compartir; Subversion le brinda la posibilidad de copiar cambios entre ramas de forma selectiva. Y para cuando haya finalizado completamente con su rama, su conjunto entero de cambios en la rama puede ser copiado en el tronco.

Copiando cambios específicos

El la sección anterior, mencionamos que tanto usted como Carmen han realizado cambios a integer.c en ramas diferentes. Si observa el informe de cambios de la revisión 344, puede ver que ella realizó algunas correcciones de ortografía. Sin duda, su copia del mismo fichero debe tener todavía los mismos errores de ortografía. Es probable que futuros cambios al fichero afectarán las mismas áreas que tienen estos errores ortográficos, lo que potencialmente generará conflictos cuando fusione su rama algún día. Es mejor, entonces, recibir ahora los cambios de Carmen, antes de que comience a trabajar de forma intensiva en el mismo sitio.

Es el momento de usar el comando svn merge. Este comando, descubrirá que es un primo cercano del comando svn diff (sobre el cual leímos en el tercer capítulo). Ambos comandos son capaces de comparar dos objetos cualquiera del repositorio y describir sus diferencias. Por ejemplo, puede preguntar a svn diff que le muestre el cambio exacto realizado por Carmen en la revisión 344:

$ svn diff -r 343:344 http://svn.example.com/repos/calc/trunk

Index: integer.c
===================================================================
--- integer.c	(revision 343)
+++ integer.c	(revision 344)
@@ -147,7 +147,7 @@
     case 6:  sprintf(info->operating_system, "HPFS (OS/2 or NT)"); break;
     case 7:  sprintf(info->operating_system, "Macintosh"); break;
     case 8:  sprintf(info->operating_system, "Z-System"); break;
-    case 9:  sprintf(info->operating_system, "CPM"); break;
+    case 9:  sprintf(info->operating_system, "CP/M"); break;
     case 10:  sprintf(info->operating_system, "TOPS-20"); break;
     case 11:  sprintf(info->operating_system, "NTFS (Windows NT)"); break;
     case 12:  sprintf(info->operating_system, "QDOS"); break;
@@ -164,7 +164,7 @@
     low = (unsigned short) read_byte(gzfile);  /* read LSB */
     high = (unsigned short) read_byte(gzfile); /* read MSB */
     high = high << 8;  /* interpret MSB correctly */
-    total = low + high; /* add them togethe for correct total */
+    total = low + high; /* add them together for correct total */
 
     info->extra_header = (unsigned char *) my_malloc(total);
     fread(info->extra_header, total, 1, gzfile);
@@ -241,7 +241,7 @@
      Store the offset with ftell() ! */
 
   if ((info->data_offset = ftell(gzfile))== -1) {
-    printf("error: ftell() retturned -1.\n");
+    printf("error: ftell() returned -1.\n");
     exit(1);
   }
 
@@ -249,7 +249,7 @@
   printf("I believe start of compressed data is %u\n", info->data_offset);
   #endif
   
-  /* Set postion eight bytes from the end of the file. */
+  /* Set position eight bytes from the end of the file. */
 
   if (fseek(gzfile, -8, SEEK_END)) {
     printf("error: fseek() returned non-zero\n");

El comando svn merge es casi exactamente idéntico. En lugar de mostrar las diferencias en su terminal, no obstante, las aplica directamente a su copia local como modificaciones locales:

$ svn merge -r 343:344 http://svn.example.com/repos/calc/trunk
U  integer.c

$ svn status
M  integer.c

La salida del comando svn merge muestra que su copia de integer.c fue parcheada. Ahora contiene el cambio de Carmen—el cambio ha sido copiado desde el tronco a su copia local de la rama privada, y existe ahora como una modificación local. En este punto, es tarea suya revisar la modificación local para asegurarse de que funciona correctamente.

En otra situación, es posible que las cosas no hayan ido tan bien, y que integer.c haya entrado en un estado de conflicto. Puede tener que resolver el conflicto usando el procedimiento estándar (vea el tercer capítulo), o si decide que la fusión fue una mala idea, simplemente dé se por vencido y use svn revert para revertir el cambio local.

Pero asumiendo que ha revisado el cambio fusionado, puede guardar sus cambios como es habitual con svn commit. En ese punto, el cambio ha sido fusionado en su rama del repositorio. En terminología de control de versiones, este acto de copiar cambios entre ramas se denomina portar cambios.

Cuando guarda su modificación local, asegúrese de que el mensaje de cambios indica que está portando un cambio específico de una rama a otra. Por ejemplo:

$ svn commit -m "integer.c: ported r344 (spelling fixes) from trunk."
Sending        integer.c
Transmitting file data .
Committed revision 360.

Tal y como verá en las siguientes secciones, es muy importante seguir esta práctica recomendada.

Aviso: aunque los comandos svn diff y svn merge son similares en concepto, tienen una diferente sintaxis en muchos casos. Asegúrese de leer en el capítulo 8 los detalles, o pregunte a svn help. Por ejemplo, svn merge requiere una ruta a una copia local como destino, es decir, un lugar donde debe aplicar los cambios del árbol. Si el destino no es especificado, asume que está intentando realizar una de las siguientes operaciones comunes:

  1. Quiere fusionar cambios de directorio directamente en su copia local.

  2. Quiere fusionar los cambios de un fichero específico en un fichero que existe con el mismo nombre en su directorio de trabajo actual.

Si está fusionando un directorio y no ha especificado una ruta destino, svn merge asume el primer caso mencionado e intenta aplicar los cambios en su directorio actual. Si está fusionando un fichero, y ese fichero (o un fichero con el mismo nombre) existe en su directorio actual de trabajo, svn merge asume el segundo caso mencionado e intenta aplicar los cambios al fichero local de mismo nombre.

Si quiere aplicar los cambios en otro lugar, tendrá que especificarlo. Por ejemplo, si todavía está en el directorio padre de su copia local, tendrá que especificar el directorio destino para recibir los cambios:

$ svn merge -r 343:344 http://svn.example.com/repos/calc/trunk my-calc-branch
U   my-calc-branch/integer.c

Procedimientos ideales de fusionado

Realizar fusiones de forma manual

Fusionar cambios suena bastante simple, pero en la práctica puede convertirse en un dolor de cabeza. El problema es que si fusiona cambios con frecuencia de una rama a otra, puede acabar fusionando accidentalmente el mismo cambio dos veces. Cuando esto ocurre, a veces las cosas seguirán funcionando bien. Cuando aplica un parche a un fichero, Subversion normalmente se da cuenta de que el fichero ya contiene el cambio, y no hace nada. Pero si el cambio ya existente fue modificado de algún modo, obtendrá un conflicto.

Idealmente, su sistema de control de versiones debería prevenir la doble aplicación de cambios a una rama. Debería recordar automáticamente qué cambios fueron recibidos en la rama, y ser capaz de mostrarle un listado de los mismos. Debería poder usar esta información para automatizar las fusiones tanto como sea posible.

Desafortunadamente, Subversion no es tal sistema. Al igual que CVS, Subversion 1.0 no almacena ninguna información sobre operaciones de fusionado. Cuando envía sus cambios locales, el repositorio no tiene idea si esos cambios vinieron de una ejecución del comando svn merge, o de una modificación manual de los ficheros.

¿Qué significa esto para usted, el usuario? Significa que hasta que llegue el día que Subversion desarrolle esta característica, tendrá que gestionar usted mismo la información de fusionado. El mejor lugar para hacerlo es en el informe de cambios cuando envía cambios al repositorio. Tal y como se demostró en el ejemplo anterior, es recomendable que su informe de cambios mencione el número de revisión específico (o rango de revisiones) que está siendo fusionado en su rama. Más tarde, puede ejecutar svn log para revisar qué cambios contiene ya su rama. Esto le permitirá construir con cuidado siguientes comandos svn merge que no serán redundantes con cambios previamente portados.

En la siguiente sección, le mostraremos algunos ejemplos de esta técnica en acción.

Visualización previa de fusiones

Dado que el fusionado sólo genera modificaciones locales, normalmente no es una operación de alto riesgo. Si se equivoca en el fusionado la primera vez, simplemente ejecute svn revert para ignorar los cambios y pruebe de nuevo.

Es posible, no obstante, que su copia local ya tenga algunas modificaciones. Los cambios aplicados por una fusión se mezclarán con sus modificaciones locales, por lo que ejecutar svn revert ya no es una opción. Los dos conjuntos de cambios quizás sean imposibles de separar.

En casos como este, la gente se reconforta con la idea de ser capaces de predecir o examinar las fusiones antes de que ocurran. Una manera simple de hacerlo es ejecutar svn diff con los mismos argumentos que planea pasar a svn merge, tal y como ya le enseñamos en el primer ejemplo de fusionado. Otro método de visualización previa es pasar la opción --dry-run al comando de fusionado:

$ svn merge --dry-run -r 343:344 http://svn.example.com/repos/calc/trunk
U  integer.c

$ svn status
#  nothing printed, working copy is still unchanged.

La opción --dry-run en realidad no aplica ningún cambio a su copia local. Únicamente muestra los códigos de estado que serían mostrados durante una fusión de verdad. Esto es útil para obtener una visualización previa de alto nivel de la potencial fusión, para aquellos casos en los que ejecutar svn diff proporcionaría demasiados detalles.

Teniendo en cuenta o ignorando ascendencia

Cuando hable con un desarrollador de Subversion, es muy posible que oiga alguna referencia al término ascendencia. Esta palabra es usada para describir la relación entre dos objetos en un repositorio: si ambos están relacionados, entonces se dice que un objeto es el ancestro del otro.

Por ejemplo, suponga que envía cambios al repositorio en la revisión 100, los cuales incluyen un cambio al fichero foo.c. Entonces foo.c@99 es un ancestro de foo.c@100. Por otro lado, suponga que hace efectivo el borrado de foo.c en la revisión 101, y entonces añade un nuevo fichero con el mismo nombre en la revisión 102. En este caso, foo.c@99 y foo.c@102 pueden parecer relacionados (tienen la misma ruta), pero de hecho son objetos completamente diferentes en el repositorio. No comparten historial o ascendencia.

La razón de mencionar esto ahora es para señalar una diferencia importante entre svn diff y svn merge. El primer comando ignora la ascendencia, mientras que el segundo es muy sensitivo a ella. Por ejemplo, si usase svn diff para comparar las revisiones 99 y 102 de foo.c, vería ficheros diferenciales basados en líneas; el comando diff esta comparando a ciegas ambas rutas. Pero si usase svn merge para comparar ambos objetos, se daría cuenta de que no están relacionados y primero intentaría borrar el fichero antiguo, y entonces añadir el nuevo; vería D foo.c seguido por A foo.c.

En la mayoría de las fusiones se comparan árboles que están relacionados por ascendencia, y por esta razón svn merge sigue este comportamiento por defecto. No obstante, ocasionalmente puede querer que el comando de fusionado compare dos árboles no relacionados. Por ejemplo, puede haber importado dos árboles de código fuente representando diferentes versiones de un proyecto de software (lea “Ramas de proveedores”). ¡Si usase svn merge para comparar los dos árboles, vería que el primero es borrado completamente, seguido de una adición completa del segundo!

En estas situaciones, querrá que svn merge realice una comparación basada únicamente en rutas de fichero, ignorando cualquier relación existente entre ficheros y directorios. Añada la opción --ignore-ancestry a su comando de fusionado, y se comportará del mismo modo que svn diff. (Y de modo similar, la opción --notice-ancestry hará que svn diff se comporte como un comando de fusionado.)



[15] En el futuro, el proyecto Subversion planea usar (o inventar) un formato de parche que describe cambios a árboles.