Casos habituales de fusionado

Hay muchos usos diferentes para svn merge, y esta sección describe los más comunes que posiblemente usará alguna vez.

Fusionando una rama completa con otra

Para completar el ejemplo que hemos ido mostrando, nos moveremos hacia adelante en el tiempo. Suponga que han pasado varios días, y se han realizado muchos cambios tanto en el tronco como en su rama privada. Suponga que ya ha terminado de trabajar en su rama privada; la característica o corrección está finalmente completada, y ahora quiere fusionar todos los cambios de su rama en el tronco para que otros puedan disfrutarlos.

¿Cómo usamos svn merge en esta situación? Recuerde que este comando compara dos árboles, y aplica las diferencias en una copia local. Así que para recibir los cambios, necesita tener una copia local del tronco. Asumiremos que todavía tiene una copia original del tronco por alguna parte (completamente actualizada), o que recientemente ha obtenido una copia local fresca de /calc/trunk.

¿Pero qué dos árboles deberían ser comparados? A primera vista, la respuesta puede parecer obvia: simplemente compare el último árbol del tronco con el último árbol de su rama. ¡Pero cuidado—esta asunción es incorrecta, y ha confundido a muchos usuarios nuevos! Dado que svn merge funciona como svn diff, comparar el tronco y su rama no describirá meramente el conjunto de cambios realizados en su rama. Tal comparación muestra demasiados cambios: no solo mostraría la adición de los cambios en su rama, sino también el borrado de cambios en el tronco que nunca ocurrieron en su rama.

Para expresar únicamente los cambios que ocurrieron en su rama, necesita comparar el estado inicial de su rama con su estado final. Usando svn log en su rama, puede ver que ésta fue creada en la revisión 341. Y el estado final puede obtenerlo con la revisión HEAD. Esto significa que quiere comparar las revisiones 341 y HEAD del directorio de su rama, y aplicar esas diferencias a una copia local del tronco.

[Sugerencia] Sugerencia

Un buen método para encontrar la revisión en la cual fue creada una rama (la "base" de la misma) es usar la opción --stop-on-copy con svn log. El sub comando log normalmente muestra todo cambio realizado en la rama, incluyendo los de la copia a partir de la cual fue creada. Así que normalmente verá también la historia del tronco. La opción --stop-on-copy detendrá la salida del comando en cuanto svn log detecte que la rama fue copiada o renombrada.

Así que en nuestro ejemplo continuo:

$ svn log --verbose --stop-on-copy \
          http://svn.example.com/repos/calc/branches/my-calc-branch
…
------------------------------------------------------------------------
r341 | usuario | 2002-11-03 15:27:56 -0600 (Thu, 07 Nov 2002) | 2 lines
Changed paths:
   A /calc/branches/my-calc-branch (from /calc/trunk:340)

$

Como esperábamos, el número final de revisión mostrado por este comando es la revisión en la cual my-calc-branch fue creado a raíz de una operación de copiado.

De nuevo, aquí está el procedimiento final de fusionado:

$ cd calc/trunk
$ svn update
At revision 405.

$ svn merge -r 341:HEAD http://svn.example.com/repos/calc/branches/my-calc-branch
U   integer.c
U   button.c
U   Makefile

$ svn status
M   integer.c
M   button.c
M   Makefile

# ...examine the diffs, compile, test, etc...

$ svn commit -m "Merged my-calc-branch changes r341:405 into the trunk."
Sending        integer.c
Sending        button.c
Sending        Makefile
Transmitting file data ...
Committed revision 406.

De nuevo, fíjese que el mensaje de cambios menciona de forma explícita el rango de cambios que fue fusionado en el tronco. Recuerde siempre hacer esto, porque es información crítica que necesitará más adelante.

Por ejemplo, suponga que decide continuar trabajando en su rama otra semana, para completar una mejora a una de las características originales o corregir un fallo. La revisión HEAD del repositorio es ahora 480, y está listo para realizar otra fusión desde su rama privada al tronco. Pero como se discutió en “Procedimientos ideales de fusionado”, no quiere fusionar los cambios que ya fueron fusionados; sólo quiere fusionar todo lo nuevo en su rama desde la última vez que fue fusionada. El truco es descubrir qué es lo nuevo.

El primer paso es ejecutar svn log en el tronco, y buscar un mensaje de cambios sobre la última vez que realizó una fusión desde su rama:

$ cd calc/trunk
$ svn log
…
------------------------------------------------------------------------
r406 | usuario | 2004-02-08 11:17:26 -0600 (Sun, 08 Feb 2004) | 1 line

Merged my-calc-branch changes r341:405 into the trunk.
------------------------------------------------------------------------
…

¡Ajá! Dado que todos los cambios que ocurrieron en la rama entre las revisiones 341 y 405 fueron fusionados con el tronco como la revisión 406, ahora sabe que sólo quiere fusionar los cambios de la rama posteriores a esto—comparando las revisiones 406 y HEAD.

$ cd calc/trunk
$ svn update
At revision 480.

# We notice that HEAD is currently 480, so we use it to do the merge:

$ svn merge -r 406:480 http://svn.example.com/repos/calc/branches/my-calc-branch
U   integer.c
U   button.c
U   Makefile

$ svn commit -m "Merged my-calc-branch changes r406:480 into the trunk."
Sending        integer.c
Sending        button.c
Sending        Makefile
Transmitting file data ...
Committed revision 481.

Ahora el tronco contiene la segunda oleada completa de cambios realizados en la rama. En este punto, puede o bien borrar su rama (discutiremos esto más adelante), o continuar trabajando en su rama y repetir este procedimiento en siguientes operaciones de fusionado.

Deshaciendo cambios

Otro uso común de svn merge es deshacer un cambio que acaba de ser enviado al repositorio. Suponga que está trabajando felizmente en su copia local de /calc/trunk, y descubre que el cambio realizado hace tiempo en la revisión 303, que modificó integer.c, está completamente mal. Nunca debería haber llegado al repositorio. Puede usar svn merge para deshacer el cambio en su copia local, y entonces enviar las modificaciones locales al repositorio. Todo lo que necesita hacer es especificar la diferencia al revés:

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

$ svn status
M  integer.c

$ svn diff
…
# verify that the change is removed
…

$ svn commit -m "Undoing change committed in r303."
Sending        integer.c
Transmitting file data .
Committed revision 350.

Una forma de pensar sobre las revisiones del repositorio es como un grupo de cambios (algunos sistemas de control de versiones los llaman changesets). Al usar el parámetro -r, puede pedirle a svn merge que aplique el changeset, o el rango completo de changesets sobre su copia local de trabajo. En nuestro caso para deshacer un cambio usamos svn merge para aplicar el changeset #303 a nuestra copia local al revés.

Tenga presente que deshacer un cambio de esta manera es como realizar cualquier otra operación svn merge, así que debería usar svn status y svn diff para confirmar que su trabajo está en el estado que usted desea, y entonces usar svn commit para enviar la versión final al repositorio. Tras el envío, este changeset particular ya no se verá reflejado en la revisión HEAD.

De nuevo puede estar preguntándose: vaya, eso no deshizo el cambio anterior, ¿verdad? El cambio todavía existe en la revisión 303. Si alguien obtiene una versión del proyecto calc entre las revisiones 303 y 349, seguirán viendo todavía el cambio malo, ¿verdad?

Si, en efecto. Cuando hablamos sobre eliminar un cambio, estamos hablando sobre eliminarlo de HEAD. El cambio original todavía existe en la historia del repositorio. En la mayoría de las situaciones, esto es suficiente. De todos modos la mayoría de las personas sólo están interesadas en seguir la versión HEAD de un proyecto. No obstante, hay casos especiales en los que realmente desearía destruir toda evidencia de un cambio. (Quizás alguien puso accidentalmente en el repositorio un documento confidencial.) Esto, tal y como verá, no es tan fácil porque Subversion fue diseñado deliberadamente para nunca perder información. Las revisiones son árboles inmutables que se construyen unos sobre otros. Eliminar una revisión de la historia podría causar un efecto dominó, creando caos en todas las revisiones siguientes y posiblemente invalidando todas las copias locales de trabajo. [16]

Resucitando elementos borrados

La mejor característica de los sistemas de control de versiones es que la información nunca se pierde. Incluso cuando borra un fichero o un directorio, puede haberse esfumado de la revisión HEAD, pero el objeto todavía existe en revisiones anteriores. Una de las preguntas más comunes realizadas por nuevos usuarios es, ¿Cómo puedo recuperar mi fichero o directorio antiguo?

El primer paso es definir exactamentequé elemento está intentando resucitar. Aquí tiene una metáfora útil: puede pensar que todo objeto en el repositorio existe en una especie de sistema de coordenadas bidimensional. Las abscisas marcan una revisión particular del árbol, y las ordenadas una ruta de fichero dentro de ese árbol. Así que cualquier versión de su fichero o directorio puede definirse por un par específico de coordenadas.

Subversion no tiene un directorio Attic como CVS, [17] por lo que necesita usar el comando svn log para descubrir la pareja de coordenadas exactas que quiere resucitar. Una buena estrategia es ejecutar svn log --verbose en un directorio que solía contener el elemento borrado. La opción --verbose muestra una lista de todos los elementos modificados en cada revisión; todo lo que necesita hacer es encontrar la revisión en la cual borró el fichero o directorio. Puede hacer esto visualmente, o usando otra herramienta para examinar el informe de cambios (vía grep, o quizás con una búsqueda incremental en un editor.)

$ cd parent-dir
$ svn log --verbose
…
------------------------------------------------------------------------
r808 | jose | 2003-12-26 14:29:40 -0600 (Fri, 26 Dec 2003) | 3 lines
Changed paths:
   D /calc/trunk/real.c
   M /calc/trunk/integer.c

Added fast fourier transform functions to integer.c.
Removed real.c because code now in double.c.
…

En el ejemplo, estamos asumiendo que está buscando el fichero borrado real.c. Leyendo los informes de cambios del directorio padre, hemos encontrado que este fichero fue borrado en la revisión 808. Por lo tanto la última versión del fichero existió en la revisión inmediatamente anterior a esa. Conclusión: quiere resucitar la ruta /calc/trunk/real.c de la revisión 807.

Eso fue la parte difícil—la investigación. Ahora que sabe lo que quiere recuperar, tiene dos posibles opciones.

Una de las opciones es usar el comando svn merge para aplicar la revisión 808 al revés. (Ya hemos discutido cómo deshacer cambios, lea “Deshaciendo cambios”.) Esto tendría el efecto de reañadir real.c como modificación local. El fichero se programaría para ser añadido, y tras enviar los cambios, el fichero existiría de nuevo en HEAD.

No obstante, en este ejemplo particular, ésta probablemente no es la mejor estrategia. Aplicar al revés la revisión 808 no solamente programaría la adición de real.c, ya que el mensaje de cambios indica que también se desharían ciertos cambios a integer.c, cosa que no desea. Ciertamente, podría fusionar al revés la revisión 808 y entonces ejecutar svn revert sobre la modificación local de integer.c, pero esta técnica no es muy escalable. ¿Qué pasa si hay 90 ficheros modificados en la revisión 808?

Otra estrategia más precisa es usar svn copy en lugar de svn merge. Simplemente copie la pareja de coordenadas exactas de revisión y ruta del repositorio a su copia de trabajo local:

$ svn copy --revision 807 \
           http://svn.example.com/repos/calc/trunk/real.c ./real.c

$ svn status
A  +   real.c

$ svn commit -m "Resurrected real.c from revision 807, /calc/trunk/real.c."
Adding         real.c
Transmitting file data .
Committed revision 1390.

El símbolo de suma en la salida del comando indica que el elemento no sólo se ha programado para ser añadido, sino para ser añadido con historia. Subversion recuerda de dónde fue copiado. En el futuro, ejecutar svn log sobre este fichero recorrerá su historia hasta la resurrección y continuará también con la que tenía antes de la revisión 807. En otras palabras, este nuevo real.c no es realmente nuevo; es un descendiente directo del fichero original que fue borrado.

A pesar de que nuestro ejemplo muestra cómo resucitar un fichero, tenga en cuenta que esta misma técnica proporciona los mismos resultados al resucitar directorios borrados.



[16] Sin embargo, el proyecto Subversion tiene planes para implementar algún día un comando svnadmin obliterate que realizaría la tarea de borrar información permanentemente. Mientras tanto, lea “svndumpfilter” para una posible solución alternativa.

[17] Dado que CVS no hace versionado de árboles, crea un área Attic dentro de cada directorio del repositorio para poder recordar los ficheros borrados.