Subversion y Regreso al Pasado

September 20, 2006

5 A forest (The Cure)

Introducción

A veces, más de las que quisiéramos, hacemos un commit en nuestro scm que resulta, después de haberse hecho efectivo, un commit no deseado. Y todas esas veces uno se pregunta: ¿cómo puedo deshacer los cambios que he subido al repositorio? Veamos cómo se hace con la herramienta scm que utilizo en mi día a día: subversion. Nota: se presuponen conocimientos previos del uso y funcionamiento de esta herramienta.

Ejemplo

Para intentar ilustrar esto de la mejor forma posible, disponemos de un árbol de directorios y ficheros representando los contenidos tanto del repositorio (shared copy) como de la copia local (working copy). La revisión actual asociada a dicho árbol es la número 30, y tanto la copia local como el repositorio se encuentran sincronizados, por tanto, ambos en la revisión 30.

myproject tree

En la figura se puede ver que las fuentes del proyecto son muy sencillas, una clase java bajo src/ y un archivo de configuración de Ant, ambos bajo la rama principal del repositorio (trunk). También, siguiendo las convenciones de subversion, se dispone de un directorio para las tags del proyecto (tags/) y otro para las ramas (branches/).

Merging en subversion

Realmente el término merge no tiene en subversion su significado clásico: aplicar los cambios de una rama en otra. En subversion funciona de una forma un poco distinta, y básicamente lo que hace es comparar dos ramas y aplicar las diferencias a una copia local destino. Más formalmente, en una operación merge entran en juego 3 participantes:

  • El árbol del repositorio inicial (left side)
  • El árbol del repositorio final (right side)
  • Una copia local (target) sobre la que se aplican los cambios obtenidos de comparar leftside y rightside

Un aspecto que tiene que quedar claro, es que después del merge, tendremos en la copia local la diferencia entre ambos árboles en la forma de CAMBIOS SALIENTES. Posteriormente, deberemos hacer un commit de dichos cambios, o si no estamos de acuerdo, realizar una operación revert en la copia local.

El siguiente comando obtendría la diferencia entre la revisión número 15 de la rama experimental1 en el repositorio y la revisión número 30 -la última- de la rama principal, y posteriormente aplicaría dicha diferencia a la copia local myproject/:

$ svn merge http://localhost/svn/myproject/branches/experimental1@15
            http://localhost/svn/myproject/trunk@30 myproject/

Lo más importante aquí es comprender que podemos comparar cualesquiera versiones de cualesquiera dos árboles en el repositorio y aplicar la diferencia resultante a una copia local. Imaginemos que queremos comparar dos revisiones de un mismo árbol y aplicar el resultado de la comparación a nuestra copia local. Podríamos hacer:

$ svn merge -r29:30 http://localhost/svn/myproject/trunk myproject/

Mi principal objetivo no es dar una explicación exhaustiva del comando merge, y si tienes más interés en esto, recomiendo una visita a la sección de enlaces más abajo.

Haciendo cambios

Durante tu trabajo del hoy has tenido que cambiar la clase MyApp.java. El cambio consiste simplemente en que has puesto explícitamente un directorio asociado exclusivamente a tu máquina. La clase compila y pasa los tests manuales (sic). Y haces un commit de la clase.

$ svn commit -m "Changed directory path"
Sending        src/MyApp.java
Transmitting file data ...
Committed revision 31.

Ahora es demasiado tarde para no hacer saltar las alarmas, y a los cinco minutos ya tienes a un compañero en tu puesto quejándose de tu “imprudencia”.

Ouch! Deshaciendo cambios

¿Y qué puedo hacer? Lo primero que se te ocurre es obtener una copia de la revisión anterior (30) del fichero MyApp.java, sustituirlo en tu copia local y realizar un nuevo commit. Esto funciona para un fichero, para dos, para tres. Cuando nos encontramos con que el commit consiste en decenas de cambios, con ficheros añadidos, eliminaciones, reemplazos, modificaciones, etc el parche manual no parece tan atrayente, ¿verdad? Debe de existir una forma más sencilla de hacerlo. Lo que se explicó en las secciones anteriores nos servirá ahora para comprender cómo se deshace un commit no deseado. Para ello nos servimos de la capacidad del comando merge para comparar dos revisiones cualesquiera de dos árboles del directorio cualesquiera.

$ svn merge -r31:30 http://svn.example.com/repos/calc/trunk
U  src/MyApp.java

Por si se ha escapado: -r31:30 ¡Estamos obteniendo el resultado de comparar la revisión 31 con la 30! Es decir, la revisión origen es más reciente en el tiempo que la revisión destino. A esto se le llama una comparación backwards. Ahora tienes a MyApp.java marcado como modificado en tu copia local, de forma que puedes hacer un commit, y obtener el mismo árbol asociado a la antigua revisión número 30.

$ svn commit -m "Undoing commit"
Sending        src/MyApp.java
Transmitting file data ..
Committed revision 32.

Evidentemente, dado que subversion lleva un control de todo el historial de cambios, si alguien hace un checkout de la revisión 31 de la rama principal, obtendrá el fichero MyApp.java con la ruta del fichero escrita explícitamente. Pero ahora en el HEAD de la rama principal tenemos lo que realmente queremos, y dado que en la mayoría de los desarrollos lo que interesa es la estabilidad de la rama principal, es un efecto secundario sin gran repercusión.

Enlaces