Bisecando con Mercurial (o encontrando changesets con bugs)

Hace un tiempo vengo leyendo y estudiando Mercurial, un sistema de control de versiones distribuido. Sin embargo todo lo que se es teoría, ya que mis amigos (con los que podría probarlo) no quieren dejar Subversion. Pero bueno… sin decir quiénes son, y al saber que ellos leen mi blog, sólos se darán cuenta que están en el error 🙂

A grandes rasgos (y según lo que yo entendí y veo más notorio), la gran diferencia entre los sistemas de control de versiones centralizados como Subversion o CVS y los distribuidos como Git, Mercurial y Bazaar, es que los primeros imponen más restricciones que sus pares distribuidos. En Subversion por ejemplo, si accedo a mi repositorio vía Internet, y en algún momento no tengo conexión, entonces no puedo hacer commits. Si quiero ver los logs de algunas revisiones o realizar otras operaciones que no sean updates o commits, necesito acceder al repositorio central, lo que es más lento.

En los sistemas centralizados hay un servidor central, y varias copias de trabajo, una por cada desarrollador. En los sistemas distribuidos puede o no haber un servidor central con el repositorio, pero la diferencia es que todos los desarrolladores tienen el repositorio completo, no una copia de trabajo. Es decir que cada persona posee la versión actual más toda la historia del proyecto en su computadora, lo que permite operar en modo offline. Esto puede parecer ineficiente, pero de hecho no lo es: los grandes proyectos de Software Libre como Linux, NetBeans, OpenJDK, Mozilla, y en un futuro GNOME también, usan estos sistemas, ya que ofrecen grandes beneficios (especialmente para los proyectos Open Source). Imagínense hacer commits, crear branches, etc, sin necesidad de conexión. Y cuando termino mi trabajo, luego de revisarlo bien, ahí recién puedo subir todo, o sino decirle a mi colega desarrollador que actualice su repositorio desde el mío.

En definitiva, con un sistema distribuido puedo trabajar con menos restricciones que con uno centralizado.


Estos días estaba leyendo el capítulo 9 de un libro de Mercurial. Uno de los subcapítulos está dedicado a la búsqueda de revisiones o changesets que han introducido bugs. Realizar esto con Mercurial es fácil, ya que tengo todo el repositorio disponible para trabajar offline, por lo que las búsquedas entre changesets son mucho más rápidas.

Imagínense que encontramos un error en nuestro proyecto, y queremos ver en qué revisión fue introducido para realizar un backout del mismo. Para esto, en Mercurial, podemos utilizar el comando “bisect”. Hagamos el ejemplo que se muestra en el libro para ser breves y correctos. Ejecutamos:

$ hg init test
$ cd test

Con un script bash creo automáticamente algunos changesets, e introduzco un bug en el commit 22:

#!/bin/bash

buggy_change=22
for (( i = 0; i < 35; i++ )); do
	if [[ $i = $buggy_change ]]; then
		echo 'i have a gub' > myfile$i
		hg commit -q -A -m 'buggy changeset'
	else
		echo 'nothing to see here, move along' > myfile$i
		hg commit -q -A -m 'normal changeset'
	fi
done

Ejecuto el script en el directorio del repositorio. Esperamos unos momentos a que finalice. Encontrarán varios archivos myfile*. Actualmente estamos, obviamente, en la revisión tip (o HEAD en Subversion):

$ hg parent
changeset:   34:a1c784850dca
tag:         tip
user:        Milton Pividori <miltondp@gmail.com>
date:        Wed Sep 03 10:53:58 2008 -0300
summary:     normal changeset

$

Para saber si en la revisión actual se encuentra el bug, simplemente ejecutamos esto:

$ grep 'i have a gub' myfile*
myfile22:i have a gub
$

Para comenzar las pruebas, ejecutamos:

$ hg bisect --reset

Le indicamos a Mercurial que la revisión tip (o sea, la 34 en nuestro caso, y la actual de la copia de trabajo), es “bad”, ya que contiene el bug (o mejor dicho, posee esa característica de la cual queremos saber en qué changeset se introdujo. Digo esto, porque podríamos querer buscar qué changeset solucionó determinado bug, con lo cual la revisión “bad” sería la que no tiene el error):

$ hg bisect --bad
$

De alguna forma, sabemos que la revisión 10 no tiene el bug, así que la marcamos como “good”. En este caso y a los fines del ejemplo saber esto es sencillo, pero digo “de alguna forma” porque en la práctica debería ser la revisión más tardía sin el bug, lo cual podría llevarnos un tiempo.

$ hg bisect --good 10
Testing changeset 22:3103b305bc80 (24 changesets remaining, ~4 tests)
0 files updated, 0 files merged, 12 files removed, 0 files unresolved
$

Vean que Mercurial nos está diciendo cuántos changesets va a procesar, y la cantidad aproximada de tests que vamos a tener que realizar. Como utiliza búsqueda binaria o dicotómica, la cantidad de comparaciones o pruebas es logarítmica, lo cual obviamente reduce mucho el proceso de búsqueda.

Al ejecutar el comando anterior, la copia de trabajo se actualiza a la revisión 22 ( (34 + 10) / 2 = 22 ). Ahora realizamos el test en esta revisión y le decimos a Mercurial si la misma es “bad” o “good”:

$ grep 'i have a gub' myfile*
myfile22:i have a gub
$

Lo anterior nos indica que es “bad”. Claro que esto se puede automatizar tanto como queramos. Podemos crear un script que haga casi todo:

#!/bin/bash

if ( grep -q 'i have a gub' myfile* )
then
	result=bad
else
	result=good
fi
echo this revision is $result
hg bisect --$result

Lo ejecutamos:

$ ./test.sh 
this revision is bad
Testing changeset 16:2deb07b463cf (12 changesets remaining, ~3 tests)
0 files updated, 0 files merged, 6 files removed, 0 files unresolved
$

Ahora Mercurial nos sitúa en la revisión 16 ( (10 + 22) / 2 = 16 ). Faltan unos 3 tests aún. Volvemos a correr nuestro script:

$ ./test.sh 
this revision is good
Testing changeset 19:dc33070521ae (6 changesets remaining, ~2 tests)
3 files updated, 0 files merged, 0 files removed, 0 files unresolved

$ ./test.sh 
this revision is good
Testing changeset 20:98953c61831c (3 changesets remaining, ~1 tests)
1 files updated, 0 files merged, 0 files removed, 0 files unresolved

$ ./test.sh 
this revision is good
Testing changeset 21:d1616a476608 (2 changesets remaining, ~1 tests)
1 files updated, 0 files merged, 0 files removed, 0 files unresolved

$ ./test.sh 
this revision is good
The first bad revision is:
changeset:   22:3103b305bc80
user:        Milton Pividori <miltondp@gmail.com>
date:        Wed Sep 03 10:53:56 2008 -0300
summary:     buggy changeset

La revisión que introdujo el bug que estamos buscando es la 22. Con algún tipo de software que automatice la práctica de “Integración continua”, podríamos indicar que seamos notificados cuando alguna revisión o changeset (independientemente del sistema de control de versiones utilizado) no pasa las pruebas o no compila por ejemplo. Esto nos habilitaría a detectar exactamente qué commit introdujo errores o bugs.

Sin embargo, puede pasar que no haya una prueba determinada que me indique que cierta revisión tiene un error, sino que nos demos cuenta de eso más tarde, y nos interese saber en qué changeset se introdujo el mismo para revertirlo (obviamente que esto no es tan sencillo. Habría que estudiar ese changeset para saber dónde está el error). Luego de introducir la prueba de unidad que verifica ese error que estamos buscando, podemos utilizarla para encontrar la revisión que introdujo el problema con algún script y el comando “bisect” que vimos anteriormente, automatizando la operación. Luego, en caso de ser los cambios de dicha revisión simples como el ejemplo, podemos revertirlo de esta forma en Mercurial (primero vuelvo a la revisión tip con “hg update”):

$ hg update
$ hg backout -m "Elimino el bug" 22
removing myfile22
created new head
changeset 35:37d16077cdac backs out changeset 22:a7281f19f281
the backout changeset is a new head - do not forget to merge
(use "backout --merge" if you want to auto-merge)

Ahora hay dos “heads” en mi repositorio (un “head” es un changeset que no tiene hijos), por lo tanto debemos, como nos está indicando Mercurial, hacer un merge entre ambos (podríamos haber utilizado la opción –merge del comando backout directamente).

Con el comando “hg view” podemos ver esto gráficamente:

Notar que el “padre” de la nueva revisión 35 (que soluciona el bug) es el changeset 22. O sea que este cambio no introduce todos los cambios entre ésta revisión y la 34. Por lo tanto debemos hacer, como ya nos había indicado Mercurial, un merge:

$ hg merge
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
(branch merge, don't forget to commit)
$ hg status
R myfile22
$ hg commit -m "Merge hecho"
$

Otra vez, ejecuto “hg view” para ver todo esto gráficamente:

Notar que los changeset de la nueva revisión tip 36 son los commits 34 y 35.

Si bien nunca hice estas cosas con Subversion, por lo que me acuerdo de haber leído, en este sistema de control de versiones hacer esto no es tan sencillo. Justamente una de las críticas a Subversion es que es fácil hacer branches, pero difícil fusionarlos.