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.