25 enero 2014

Mantén a salvo tu código (tutorial de Mercurial)

En uno de mis artículos anteriores escribía acerca de algunas de las opciones disponibles para guardar versiones del código conforme este evolucionaba. Analizamos las principales opciones utilizadas por los desarrolladores freelance: Git y Mercurial, y los principales proveedores en la nube para ellos: GitHub y Bitbucket. Mi conclusión fue que, dado que desarrollo principalmente en Python, mi elección lógica era Mercurial y Bitbucket, mayoritaria dentro de la comunidad Python. En este artículo vamos a aprender los principales comandos para usar Mercurial y mantener un repositorio en Bitbucket.

Mercurial cuenta con instaladores para Windows, Linux y MacOS. Además, se pueden utilizar herramientas gráficas para gestionarlo (como TortoiseHg) o limitarse a controlarlo desde la consola. En este tutorial vamos a centrarnos en la versión para linux (concretamente la de Ubuntu) gestionada mediante comandos de consola. Usar la consola tiene la ventaja de que es más claro e inmediato explicar los conceptos.

Para instalar Mercurial basta con teclear:

$ sudo aptitude install mercurial

Una vez instalado, se puede ejecutar como un usuario normal sin privilegios, pero antes se debe hacer una configuración mínima: en la raíz del directorio home del usuario se debe crear un fichero llamado ".hgrc" (ojo al punto inicial. Este fichero sirve para fijar las variables globales utilizadas por Mercurial. La configuración mínima necesaria para Mercurial es el usuario y la dirección de correo que se quiera usar para marcar cada actualización del repositorio. En mi caso, el fichero tiene el siguiente contenido:
$ cat .hgrc [ui] username = dante [extensions] graphlog= $

Con  cambiar el contenido por el nombre y el correo de cada una ya está, esa es toda la configuración que necesita Mercurial para funcionar. La parte del Graphlog nos permitirá mostrar información muy útil cuando expliquemos las ramas (branches), algo más tarde en este artículo.

Ahora vayamos a la carpeta donde tenemos el código fuente que queremos controlar y le diremos a Mercurial que cree allí el repositorio. Supongamos que el fichero de código fuente se llama sólo "source" y que su contenido es:
source$ ls source$

Para crear ahí un repositorio de Mercurial basta con hacer:
source$ ls source$ hg init source$ ls source$

Espera, ¿no ha cambiado nada?, ¿esto es normal?. En realidad sí porque Mercurial esconde su directorio de trabajo para protegerlo de borrados accidentales:
source$ ls -la total 12 drwxrwxr-x 3 dante dante 4096 ene 17 22:11 . drwxrwxr-x 6 dante dante 4096 ene 17 22:09 .. drwxrwxr-x 3 dante dante 4096 ene 17 22:11 .hg source$

Ahí está, el directorio ".hg" que usa Mercurial. Dentro de él, Mercurial guardará las versiones de nuestro ficheros de código. Mientras la carpeta ".hg" esté a salvo también lo estará nuestro código.

Con "hg status" podemos ver qué pasa en nuestro repositorio. Si lo tecleamos con un repositorio recien creado en una carpeta que aún no tenga ficheros, "hg status" devolverá una respuesta vacía:
source$ hg status source$

Sin embargo, si creamos dos ficheros:
source$ touch code_1.txt source$ touch code_2.txt source$ ls code_1.txt  code_2.txt source$ hg status ? code_1.txt ? code_2.txt source$

Esas dos interrogaciones de la salida de "hg status" nos dicen que Mercurial ha detectado dos ficheros en la carpeta que todavía no están incluidos en el repositorio. Para añadirlos debemos hacer:
source$ hg add code_1.txt source$ hg add code_2.txt source$ hg status A code_1.txt A code_2.txt source$
Ahora las interrogaciones han cambiado a "A" lo que significa que esos ficheros acaban de añadirse al repositorio. Esta vez hemos añadido los ficheros uno a uno, pero podríamos haberlo hecho de una vez haciendo solamente "hg add .". También podríamos usar comodines en este comando. Además podemos crear listas de exclusión si creásemos el fichero ".hgignore" dentro de la carpeta del fichero fuente. De esta manera podemos ajustar en detalle qué ficheros se incluirán en el repositorio de Mercurial y cuales no. Por ejemplo, lo normal es tener en el repositorio ficheros textuales de código fuente, no ficheros compilados (de ese código fuente) o bases de datos de prueba que puedan ser regeneradas fácilmente. Lo mejor es guardar en el repositorio sólo los ficheros que realmente se necesiten con el fin de mantener el tamaño del repositorio lo más pequeño posible. Hay que tener en cuenta que si alojamos nuestro repositorio en Bitbucket (u otro alojamiento de código fuente), seguramente tendremos un límite de tamaño máximo para nuestro repositorio en la nube si queremos seguir como usuarios gratuitos.

Los cambios en el repositorio no serán válidos hasta que los validemos con "hg commit":
source$ hg commit -m "Two initial files just created empty." source$ hg status source$

El parámetro "-m" en el "hg commit" nos permite comentar la versión de manera que podamos saber de un vistazo qué cambios contiene. Una vez que el cambio es validado desaparece de "hg status", esa es la razón por la que en nuestro anterior ejemplo vuelve a salir vacío de nuevo. Si modificamos uno de los ficheros:
source$ hg status source$ echo "Hello" >> code_1.txt source$ hg status M code_1.txt source$

La "M" en la salida de hg status significa que Mercurial ha detectado que un fichero incluido en el repositorio ha cambiado con respecto a la versión que había registrada. Para incluir dicha modificación en el repositorio tenemos que validar el cambio:
source$ hg commit -m "Code_2 modified." source$ hg status source$

¡Pero un momento! ¡espera! ¡hemos cometido un error!, el mensaje es incorrecto porque el fichero modificado es code_1 no code_2. Mercurial nos permite corregir la última validación con el parámetro "--amend":
source$ hg log changeset:   1:4161fbd0c054 tag:         tip user:        dante date:        Fri Jan 17 23:09:00 2014 +0100 summary:     Code_2 modified. changeset:   0:bf50392b0bf2 user:        dante date:        Fri Jan 17 22:43:34 2014 +0100 summary:     Two initial files just created empty. source$ hg commit --amend -m "Code_1 modified." saved backup bundle to /home/dante/Desarrollos/source/.hg/strip-backup/4161fbd0c054-amend-backup.hg source$ hg log changeset:   1:17759dec5135 tag:         tip user:        dante date:        Fri Jan 17 23:09:00 2014 +0100 summary:     Code_1 modified. changeset:   0:bf50392b0bf2 user:        dante date:        Fri Jan 17 22:43:34 2014 +0100 summary:     Two initial files just created empty. source$

"hg log" muestra el histórico de validaciones. A través de ese histórico podemos ver que el mensaje de la última actualización ha sido corregido gracias al parámetro "--amend". Sin embargo, con ese parámetro sólo se puede arreglar la última validación. Cambiar validaciones más antiguas se considera peligroso y no hay una manera fácil de hacerlo (aunque se puede, pero es bastante delicado).

¿Qué pasa si uno se da cuenta de que ya no necesita uno de los ficheros del proyecto y quiere retirarlo del repositorio para que Mercurial no le haga seguimiento?. Una opción sería borrar el fichero de la carpeta del código fuente...
source$ ls code_1.txt  code_2.txt source$ rm code_2.txt source$ ls code_1.txt source$ hg status ! code_2.txt source$

... pero se puede ver que Mercurial alerta (el mensaje con la exclamación "!") de que no puede encontrar un fichero al que le hace seguimiento por estar en el repositorio. Para decirle a Mercurial que deje de hacerle ese seguimiento a un fichero en particular:
source$ hg status ! code_2.txt source$ hg remove code_2.txt source$ hg status R code_2.txt source$ hg commit -m "Code_2 removed." source$ hg status source$

Con "hg remove" se puede marcar un fichero para ser retirado del repositorio, esa es la razón por la que "hg log" muestra una "R" que significa que el fichero va a ser borrado del repositorio en la próxima validación.

Vale, hemos borrado un fichero del repositorio pero entonces nos damos cuenta de que en realidad lo necesitábamos y que borrarlo fue un error. Tenemos dos opciones. El primero es devolver el repositorio a la última versión en la que el fichero borrado estaba presente, copiarlo a un fichero temporal, devolver al repositorio al últimos estado actualizado y copiar el fichero de la carpeta temporal a la del código:
source$ hg log changeset:   2:88ac7cad647e tag:         tip user:        dante date:        Sat Jan 18 00:39:50 2014 +0100 summary:     Code_2 removed. changeset:   1:17759dec5135 user:        dante date:        Fri Jan 17 23:09:00 2014 +0100 summary:     Code_1 modified. changeset:   0:bf50392b0bf2 user:        dante date:        Fri Jan 17 22:43:34 2014 +0100 summary:     Two initial files just created empty. source$ hg update 1 1 files updated, 0 files merged, 0 files removed, 0 files unresolved source$ ls code_1.txt  code_2.txt source$ cp code_2.txt /tmp/code_2.txt source$ hg update 2 0 files updated, 0 files merged, 1 files removed, 0 files unresolved source$ ls code_1.txt source$ cp /tmp/code_2.txt code_2.txt source$ hg status ? code_2.txt source$ hg add code_2.txt source$ hg status A code_2.txt source$

Como se puede ver, con el comando "hg update" podemos hacer que nuestra carpeta de código fuente viaje en el tiempo al estado que tenía en una versión concreta. Sólo hay que tener en cuenta que el número que usa "hg update" es el primero que figura en el id de revisión mostrado por "hg log". Por ejemplo, si quisiéramos volver a este estado:

changeset:   1:17759dec5135
user:        dante
date:        Fri Jan 17 23:09:00 2014 +0100
summary:     Code_1 modified.

deberíamos usar "hg update 1" debido a que la versión dice "changeset 1:...", ¿se entiende?.

El problema de este enfoque es que es poco elegante y resulta fácil cometer un error. Un enfoque más directo sería localizar el último estado en el que está presente el fichero a recuperar y traerlo de vuelta con "hg revert":
source$ ls code_1.txt source$ hg status source$ hg log -l 1 code_2.txt changeset:   0:bf50392b0bf2 user:        dante date:        Fri Jan 17 22:43:34 2014 +0100 summary:     Two initial files just created empty. source$ hg revert -r 0 code_2.txt source$ hg log changeset:   2:88ac7cad647e tag:         tip user:        dante date:        Sat Jan 18 00:39:50 2014 +0100 summary:     Code_2 removed. changeset:   1:17759dec5135 user:        dante date:        Fri Jan 17 23:09:00 2014 +0100 summary:     Code_1 modified. changeset:   0:bf50392b0bf2 user:        dante date:        Fri Jan 17 22:43:34 2014 +0100 summary:     Two initial files just created empty. source$ hg status A code_2.txt source$ source$ hg commit -m "Code_2 recovered." source$ hg log changeset:   3:9214d0557080 tag:         tip user:        dante date:        Sat Jan 18 01:07:24 2014 +0100 summary:     Code_2 recovered. changeset:   2:88ac7cad647e user:       dante date:        Sat Jan 18 00:39:50 2014 +0100 summary:     Code_2 removed. changeset:   1:17759dec5135 user:        dante date:        Fri Jan 17 23:09:00 2014 +0100 summary:     Code_1 modified. changeset:   0:bf50392b0bf2 user:        dante date:        Fri Jan 17 22:43:34 2014 +0100 summary:     Two initial files just created empty. source$ ls code_1.txt  code_2.txt source$

Lo principal es que "hg log -l 1 code_2.txt" muestra que la última versión en el que el fichero fue modificado. Con esa versión podemos hacer que Mercurial rescate el fichero deseado desde alí. ("hg revert -r 0 code_2.txt). No hay que olvidar realizar una validación al finalizar el rescate.

Ahora elevemos las apuestas. A veces uno quiere intentar desarrollar nuevas funcionalidades para nuestro programa pero no quiere enredar con los ficheros estables. Ahí es donde entran en juego las ramas. Al crear una rama podemos desarrollar sobre una copia aparte de nuestra rama principal (denominada "default"). Una vez que estemos seguros de que la rama está ista para entrar en producción podemos fundir (merge) la rama con la principal, incluyendo los cambios en los ficheros estables de la rama principal.

Supongamos que queremos desarrollar dos funcionalidades, podemos crear dos ramas: "feature1" y " feature2":
source$ hg branches default 0:03e7ab9fb0c6 source$ hg branch feature1 marked working directory as branch feature1 (branches are permanent and global, did you want a bookmark?) source$ hg branches default 0:03e7ab9fb0c6 source$ hg status source$ hg commit -m "Feature1 branch created." source$ hg branches feature1 1:6c061eff633f default 0:03e7ab9fb0c6 (inactive) source$

"hg branches" muestra las ramas del repositorio pero estas no se crean en realidad hasta que se validan con un "hg commit", tras hacer un "hg branch". Esa es la razón por la que el primer "hg branches" del ejemplo anterior sólo muestra la rama principal.
source$ touch code_feature1.txt source$ ls code_1.txt  code_2.txt  code_feature1.txt source$ hg status ? code_feature1.txt source$ hg add code_feature1.txt source$ hg commit -m "code_feature1.txt created"

Para cambiar de una rama a otra hay que usar "hg update":
source$ hg update default 0 files updated, 0 files merged, 1 files removed, 0 files unresolved source$ ls code_1.txt  code_2.txt source$

Cuando cambiamos de una rama a otra los ficheros se crean o borran del directorio para crear el esquema de la versión de la rama.
source$ hg branch feature2 marked working directory as branch feature2 (branches are permanent and global, did you want a bookmark?) source$ hg commit -m "Feature2 branch created" source$ touch code_feature2.txt source$ hg add code_feature2.txt source$ hg commit -m "code_feature2.txt created" source$ ls code_1.txt  code_2.txt  code_feature2.txt source$ hg branches feature2                       7:42123cefb28c feature1                       5:09f18d24ae0e default                        3:9214d0557080 (inactive) source$ hg update default 0 files updated, 0 files merged, 1 files removed, 0 files unresolved source$ ls code_1.txt  code_2.txt source$

Por supuesto podemos seguir trabajando en la rama principal:

source$ ls code_1.txt  code_2.txt source$ touch code_3.txt source$ ls code_1.txt  code_2.txt  code_3.txt source$ hg add code_3.txt source$ hg commit -m "code_3.txt created" source$

Cuando uno trabaja simultáneamente con varias ramas es natural sentirse un poco perdido. Para saber en que rama estamos en cada momento se puede teclear "hg branch" sin nada detrás. Para conseguir una representación gráfica de los cambios validados a las distintas ramas se puede usar "hg log -G":

source$ hg log -G @  changeset:   8:09e718575633 |  tag:         tip |  parent:      3:9214d0557080 |  user:        dante |  date:        Sat Jan 18 20:53:06 2014 +0100 |  summary:     code_3.txt created | | o  changeset:   7:42123cefb28c | |  branch:      feature2 | |  user:        dante | |  date:        Sat Jan 18 20:40:56 2014 +0100 | |  summary:     code_feature2.txt created | | | o  changeset:   6:52f1c855ba6b |/   branch:      feature2 |    parent:      3:9214d0557080 |    user:        dante |    date:        Sat Jan 18 20:39:05 2014 +0100 |    summary:     Feature2 branch created | | o  changeset:   5:09f18d24ae0e | |  branch:      feature1 | |  user:        dante | |  date:        Sat Jan 18 20:22:35 2014 +0100 | |  summary:     code_feature1.txt created | | | o  changeset:   4:2632a2e93070 |/   branch:      feature1 |    user:        dante |    date:        Sat Jan 18 20:20:28 2014 +0100 |    summary:     Feature1 branch created | o  changeset:   3:9214d0557080 |  user:        dante |  date:        Sat Jan 18 01:07:24 2014 +0100 |  summary:     Code_2 recovered. | o  changeset:   2:88ac7cad647e |  user:        dante |  date:        Sat Jan 18 00:39:50 2014 +0100 |  summary:     Code_2 removed. | o  changeset:   1:17759dec5135 |  user:        dante |  date:        Fri Jan 17 23:09:00 2014 +0100 |  summary:     Code_1 modified. | o  changeset:   0:bf50392b0bf2    user:        dante    date:        Fri Jan 17 22:43:34 2014 +0100    summary:     Two initial files just created empty. source$

Para usar el parámetro "-G" con "hg log" hay que incluir las siguientes líneas en el fichero ".hgrc" que mencionábamos al comienzo del artículo:

[extensions]
graphlog=

Una vez que llegamos a la conclusión de que nuestra rama está lo suficientemente madura como para agregar sus cambios a la rama principal, podemos usar "hg merge":
 source$ hg update feature1 1 files updated, 0 files merged, 1 files removed, 0 files unresolved source$ ls code_1.txt  code_2.txt  code_feature1.txt source$ cat code_1.txt Hello source$ echo "World" >> code_1.txt source$ cat code_1.txt Hello World source$ hg status M code_1.txt source$ hg commit -m "code_1.txt modified with world" source$ hg update default 2 files updated, 0 files merged, 1 files removed, 0 files unresolved source$ ls code_1.txt  code_2.txt  code_3.txt source$ cat code_1.txt Hello source$ hg merge feature1 2 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) source$ ls code_1.txt  code_2.txt  code_3.txt  code_feature1.txt source$ cat code_1.txt Hello World source$


Es importante tener en cuenta que antes de hacer un merge hay que ponerse en la rama donde se quieren insertar los cambios. Una vez allí se llama a "hg merge" con el nombre de rama desde donde se quieren importar los cambios. Por supuesto, los cambios no se incorporan de manera efectiva al repositorio hasta que son validados:
source$ hg status M code_1.txt M code_feature1.txt source$ hg commit -m "Feature1 merged to default branch" source$

Fijémonos en cómo el log graph ha cambiado para mostrar la unión entre ramas:
source$ hg log -G @    changeset:   10:677a88f54dd3 |\   tag:         tip | |  parent:      8:1b93d501259a | |  parent:      9:8b55fb7eec71 | |  user:        dante | |  date:        Sun Jan 19 00:07:54 2014 +0100 | |  summary:     Feature1 merged to default branch | | | o  changeset:   9:8b55fb7eec71 | |  branch:      feature1 | |  parent:      5:197964afe12f | |  user:        dante | |  date:        Sat Jan 18 23:57:03 2014 +0100 | |  summary:     code_1.txt modified with world | | o |  changeset:   8:1b93d501259a | |  parent:      3:132c0505c7b2 | |  user:        dante | |  date:        Sat Jan 18 23:56:24 2014 +0100 | |  summary:     code_3.txt created | | | | o  changeset:   7:86391749b3c3 | | |  branch:      feature2 | | |  user:        dante | | |  date:        Sat Jan 18 23:55:04 2014 +0100 | | |  summary:     code_feature2.txt created | | | +---o  changeset:   6:30decd2ffa21 | |    branch:      feature2 | |    parent:      3:132c0505c7b2 | |    user:        dante | |    date:        Sat Jan 18 23:54:38 2014 +0100 | |    summary:     Feature2 branch created | | | o  changeset:   5:197964afe12f | |  branch:      feature1 | |  user:        dante | |  date:        Sat Jan 18 23:53:43 2014 +0100 | |  summary:     code_feature1.txt created | | | o  changeset:   4:4bbf5ca2e0b6 |/   branch:      feature1 |    user:        dante |    date:        Sat Jan 18 23:52:26 2014 +0100 |    summary:     Feature1 branch created | o  changeset:   3:132c0505c7b2 |  user:        dante |  date:        Sat Jan 18 23:52:02 2014 +0100 |  summary:     Code_2 recovered. | o  changeset:   2:05e0a410c49d |  user:        dante |  date:        Sat Jan 18 23:51:24 2014 +0100 |  summary:     Code_2 removed. | o  changeset:   1:552e1b95fffe |  user:        dante |  date:        Sat Jan 18 23:49:35 2014 +0100 |  summary:     Code_1 modified. | o  changeset:   0:a22ab902f1a7    user:        dante    date:        Sat Jan 18 23:48:55 2014 +0100    summary:     Two initial files just created empty. source$

Cuando se ha acabado el trabajo en una rama y no se planea hacer ninguna mejora más en dicha rama, se puede hacer para cerrar una rama de manera que ya no aparezca en la lista de "hg branches":
source$ hg branches default                       10:677a88f54dd3 feature2                       7:86391749b3c3 feature1                       9:8b55fb7eec71 (inactive) source$ hg update feature1 0 files updated, 0 files merged, 1 files removed, 0 files unresolved source$ hg commit --close-branch -m "Feature1 included in default. No further work planned here" source$ hg branches default                       10:677a88f54dd3 feature2                       7:86391749b3c3 source$

La ramas cerradas se pueden abrir de nuevo entrando en ellas con un "hg update" y validando el cambio con "hg commit".

Hasta ahora hemos aprendido lo básico para trabajar con Mercurial en una carpeta de código fuente local. Normalmente es difícil borrar accidentalmente un directorio oculto como ".hg", pero siempre podemos perder nuestro disco duro por un fallo hardware (o uno puede meter la pata haciendo un "rm -rf" mientras escribía este artículo). En ese caso perderíamos el repositorio. Además cuando trabajemos con un equipo necesitaremos un repositorio central en el que mezclar los avances de cualquier miembro en la rama principal. Bitbucket es la respuesta para ambas necesidades. Por eso vamos a ver cómo podemos conservar un backup de nuestro repositorio en la nube de Bitbucket.

Una vez que nos hayamos registrados en Bitbucket podemos crear un nuevo repositorio:


Se puede configurar el repositorio tanto como público como privado, podemos hacer que pueda ser usado tanto con Git como con Mercurialo incluso incluir una Wiki en el repositorio de la página web. Si nuestro equipo es de menos de cinco personas, Bitbucket nos ofrecerá sus servicios de manera gratuita.

Cuando se crea un repositorio podemos subir a él nuestra copia del código fuente con el comando "hg push":

source$ hg push https://dante@bitbucket.org/dante/sourcecode pushing to https://dante@bitbucket.org/dante/sourcecode http authorization required realm: Bitbucket.org HTTP user: dante password: searching for changes remote: adding changesets remote: adding manifests remote: adding file changes remote: added 12 changesets with 7 changes to 5 files (+1 heads) source$

Con el repositorio subido a Bitbucket, todos los miembros pueden conseguir una copia local del proyecto con "hg clone":
source2$ ls source2$ hg clone https://dante@bitbucket.org/dante/sourcecode . http authorization required realm: Bitbucket.org HTTP user: borjalopezm password: requesting all changes adding changesets adding manifests adding file changes added 12 changesets with 7 changes to 5 files (+1 heads) updating to branch default 4 files updated, 0 files merged, 0 files removed, 0 files unresolved source2$ ls code_1.txt  code_2.txt  code_3.txt  code_feature1.txt source2$ hg update 0 files updated, 0 files merged, 0 files removed, 0 files unresolved source2$

Hay que fijarse en el punto (".") que hay justo después de la url del "hg clone", si no lo usamos los ficheros se descargarán en una carpeta denominada "sourcecode" dentro de "source2". Suele ser una buena idea hacer un "hg update" para asegurarse de que estamos trabajando en la versión más actualizada del proyecto.

Tras eso, el miembro del equipo ya podrá trabajar en su repositorio local. Para subir los avances a Bitbucket habría que hacer un "hg push" como hicimos antes para subir los ficheros por primera vez a Bitbucket:
source2$ ls code_1.txt  code_2.txt  code_3.txt  code_feature1.txt source2$ touch code_4.txt source2$ hg add code_4.txt source2$ hg commit -m "Code_4.txt added" source2$ hg push https://dante@bitbucket.org/dante/sourcecode pushing to https://dante@bitbucket.org/dante/sourcecode http authorization required realm: Bitbucket.org HTTP user:dante password: searching for changes remote: adding changesets remote: adding manifests remote: adding file changes remote: added 1 changesets with 1 changes to 1 files source2$

Tras el clone inicial, los otros miembros del equipo pueden conseguir actualizaciones (como code_4.txt) con un "hg pull":

source$ hg pull https://dante@bitbucket.org/dante/sourcecode http authorization required realm: Bitbucket.org HTTP user: dante password: pulling from https://dante@bitbucket.org/dante/sourcecode searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (run 'hg update' to get a working copy) source$ hg update default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved source$ ls code_1.txt  code_2.txt  code_3.txt  code_4.txt  code_feature1.txt source$

¿Pero qué pasa si dos miembros hacen la modificación sobre el mismo fichero?. Supongamos que un miembro hace:
source$ ls code_1.txt  code_2.txt  code_3.txt  code_4.txt  code_feature1.txt source$ cat code_1.txt Hello World source$ echo "Hello WWW" > code_1.txt source$ hg commit -m "One line hello WWW" source$ cat code_1.txt Hello WWW source$ hg push https://dante@bitbucket.org/dante/sourcecode pushing to https://dante@bitbucket.org/dante/sourcecode http authorization required realm: Bitbucket.org HTTP user: dante password: searching for changes remote: adding changesets remote: adding manifests remote: adding file changes remote: added 1 changesets with 1 changes to 1 files source$

Y justo un poco después otro miembro hace en su propio repositorio:
source2$ ls code_1.txt  code_2.txt  code_3.txt  code_4.txt  code_feature1.txt source2$ cat code_1.txt Hello World source2$ echo "Wide Web" >> code_1.txt source2$ cat code_1.txt Hello World Wide Web source2$ hg commit -m "Code_1 added Wide Web" source2$ hg push https://dante@bitbucket.org/dante/sourcecode pushing to https://dante@bitbucket.org/dante/sourcecode http authorization required realm: Bitbucket.org HTTP user: dante password: searching for changes abort: push creates new remote head e716387febe4! (you should pull and merge or use push -f to force) source2$

Lo que ha pasado es que Bitbucket ha detectado que el segundo push contenía una versión conflictiva del fichero code_1.txt. Cuando tenemos dos versiones de un fichero en la misma rama y nivel de versión estamos ante lo que la terminología del control de versiones denomina "dos cabezas" ("two heads"). Por defecto, Bitbucket no permite que tengamos dos cabeza y recomienda que nos bajemos las últimas actualizaciones con "hg pull" y mezclarlas con nuestra versión local con un "hg  merge":
source2$ hg heads changeset:   13:e716387febe4 tag:         tip user:        dante date:        Mon Jan 20 21:46:00 2014 +0100 summary:     Code_1 added Wide Web changeset:   7:86391749b3c3 branch:      feature2 user:        dante date:        Sat Jan 18 23:55:04 2014 +0100 summary:     code_feature2.txt created source2$ hg branch default source2$

En este punto se puede ver que tenemos una cabeza por cada rama. Esta es una situación normal, pero si actualizamos:
source2$ hg pull https://dante@bitbucket.org/dante/sourcecode http authorization required realm: Bitbucket.org HTTP user: dante password: pulling from https://dante@bitbucket.org/dante/sourcecode searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) source2$

Fijémonos en el último mensaje que nos alerta de que la última actualización ha creado múltiples cabezas. De hecho, si ejecutamos "hg heads":
source2$ hg heads changeset:   14:c3a688edd25a tag:         tip parent:      12:53443797a7da user:        dante date:        Mon Jan 20 21:46:25 2014 +0100 summary:     One line hello WWW changeset:   13:e716387febe4 user:        dante date:        Mon Jan 20 21:46:00 2014 +0100 summary:     Code_1 added Wide Web changeset:   7:86391749b3c3 branch:      feature2 user:        dante date:        Sat Jan 18 23:55:04 2014 +0100 summary:     code_feature2.txt created source2$

Podemos ver que tenemos dos cabezas en la rama principal. Por eso es el momento de mezclarlas con un "hg merge":
source2$ hg merge merging code_1.txt 3 archivos que editar 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) source2$ cat code_1.txt Hello WWW World Wide Web source2$ hg commit -m "Code_1 merged with repository" source2$ hg heads changeset:   15:fed327662238 tag:         tip parent:      13:e716387febe4 parent:      14:c3a688edd25a user:        dante date:        Mon Jan 20 22:06:39 2014 +0100 summary:     Code_1 merged with repository changeset:   7:86391749b3c3 branch:      feature2 user:        dante date:        Sat Jan 18 23:55:04 2014 +0100 summary:     code_feature2.txt created source2$ hg https://dante@bitbucket.org/dante/sourcecode pushing to https://dante@bitbucket.org/dante/sourcecode http authorization required realm: Bitbucket.org HTTP user: dante password: searching for changes remote: adding changesets remote: adding manifests remote: adding file changes remote: added 2 changesets with 2 changes to 1 files source2$

En caso de conflictos como este, "hg merge" abre un editor con paneles (que no muestro aquí) con el que comparar las versiones del mismo fichero que entran en conflicto entre si y modificarlas de manera que la copia local sea compatible con la de Bitbucket. Por lo general prefiero usar Mercurial desde la consola en vez de utilizar las múltiples aplicaciones gráficas existentes (como TortoiseHG), pero tengo que admitir que el editor de consola que utiliza Mercurial es un poco árido al estar basado en Vim (soy de los que prefiere Nano frente a Vim).

Una vez mezclado y validado, podemos ver que el número total de cabezas se ha reducido de nuevo a dos (una por rama) por lo que esta vez un push a Bitbucket funcionará como la seda.

Con todas estas herramientas, un equipo de desarrolladores puede trabajar simultaneamente sin pisarse los unos a los otros. Pero Bitbucket ofrece formas para que podemos contribuir con un projecto incluso si no formamos parte de su equipo de desarrollo y no tenemos acceso de escritura a su repositorio. Nos referimos a lo que se denomina forking.



Cuando hacemos fork del repositorio de otro usuario lo que ocurre en segundo plano es que el repositorio se clona en nuestra cuenta de Bitbucket. De esa manera tendremos la oportunidad de escribir y probar modificaciones contra nuestro propio repositorio. Una vez que nuestro código está preparado, podemos solicitar un "pull request" al autor original. Si él lo acepta, se realizará la mezcla entre los dos repositorios y los cambios se incorporarán al repositorio original.



OK, con esto finalizamos el artículo. Ahora estamos en condiciones de dominar las bases del control de versiones con Mercurial y Bitbucket. Siento la extensión del artículo pero quería cubrir todos los temas habituales que se puede encontrar habitualmente un proyecto independiente. Mercurial y Bitbucket tienen otras muchas opciones y refinamientos pero normalmente sólo nos los encontraremos en proyectos más complejos.

Por último, no quiero acabar este artículo sin mencionar que la mayor parte de los conceptos de este artículo son similares a los que usan Git y Github. Recomiendo visitar este tutorial introductorio a Git en el que se puede ver lo similar que es a Merccurial.

15 enero 2014

GNS3 busca apoyo para desarrollar su próxima versión

GNS3, el entorno de virtualización de laboratorios open source desarrollado en Python, acerca del cual escribí en uno de mis artículos anteriores, está buscando apoyo financiero para desarrollar su próxima versión. Su campaña en Crowdhoster ha sido un gran éxito. Con un objetivo inicial de 35.000$, han alcanzado hasta ahora 280.000$.

Las nuevas funcionalidades son muy interesantes, pero sobre todo se encuentra la inclusión de las capacidades de conmutación que nos permitirán librarnos de los trucos con los Cisco 3540 y sus tarjetas NM-16ESW para simular entornos conmutados. Otras funcionalidades nuevas son los laboratorios de seguridad, el procesado bajo demanda en la nube, etc. Hay que resaltar las opciones para empresas. Aquellas en el negocio de las redes de datos se pueden beneficiar enormemente de las posibilidades para la formación de GNS3.