Contenido

Hg vs Git

Vamos con tópicos: Mercurial vs Git.

Voy a intentar desmitificar o corroborar rumores, de manera unívoca y que cualquiera pueda reproducir.

Este artículo y los siguientes admiten colaboraciones :D

Git vs Mercurial

Este artículo es un coñazo

Pues sí. Este artículo es un coñazo total. Por eso voy a comenzar por el final: las conclusiones. Así, los crédulos, pueden evitarse la parte de demostración y leer sólo las conclusiones. Los incrédulos pueden llegar hasta el final y comprobar que he tratado de ser todo lo objetivo posible.

Conclusiones

Aquí tenemos las conclusiones finales:

  • Si en Mercurial renombras un archivo, el repositorio requiere de nuevo el espacio ocupado por el archivo. En git, la diferencia es inapreciable.
  • En Mercurial, dos “push” simultáneos sin conflicto pueden provocar que el segundo se cancele. En git no ocurre.

Esto demuestra que Git da un mejor rendimiento que Mercurial.

Críticas

Por favor, estoy dispuesto a soportar cualquier tipo de crítica. Sin embargo, me gustaría indicar que este artículo me ha costado MUCHO, ya que no es nada sencillo pensar en las demostraciones y menos realizarlas de manera que se puedan repetir. Y también ha sido compleja la maquetación. Por esa razón, espero que cualquier crítica venga acompañada de demostraciones.

Sé de algunos otros problemas, pero no puedo demostrarlos, así que me los cayo. Estas pruebas son completamente OBJETIVAS.

También acepto demostraciones a favor/en contra de ambos. Sed creativos.

Agradecimientos

Agradezco a David Villa su paciencia haciendo code review de estos scripts. Me equivoqué al pasarle el de Mercurial y se puede decir que lo rehizo él solo.

Scripts

Los Scripts necesarios para reproducir lo que expongo aquí se pueden encontrar en github. Allí será donde haga modificaciones a los mismos.

Demostraciones

Si mueves un archivo en mercurial, ocupará espacio de nuevo

Mercurial Git
$ seq 10000000 > $WC1/file
$ ls -lh $WC1
total 6,6M
-rw-r--r-- 1 miguel miguel 6,6M may 27 19:39 file
$ hg -R $WC1 add $WC1/file
$ seq 10000000 > $WC1/file
$ ls -lh $WC1
total 6,6M
-rw-r--r-- 1 miguel miguel 6,6M may 27 19:39 file
$ git --git-dir=$WC1/.git --work-tree=$WC1 add $WC1/file
$ export WC1=/tmp/wc1-hg
$ hg init $WC1
$ export WC1=/tmp/wc1-git
$ git init $WC1
Initialized empty Git repository in /tmp/wc1-git/.git/
$ hg -R $WC1 commit -m "initial"
$ git --git-dir=$WC1/.git --work-tree=$WC1 commit -m "initial"
[master (root-commit) 50f537c] initial
 1 file changed, 1000000 insertions(+)
 create mode 100644 file
$ du -hs
8,7M /tmp/wc1-hg
$ du -hs /tmp/wc1-git
8,9M /tmp/wc1-git
$ hg -R $WC1 mv $WC1/file $WC1/example
$ hg -R $WC1 commit -m "second"
$ du -hs $WC1
11M /tmp/wc1-hg
$ git --git-dir=$WC1/.git --work-tree=$WC1 mv file example
$ git --git-dir=$WC1/.git --work-tree=$WC1 commit -am "second"
[master f206fe1] second
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename file => example (100%)
$ du -hs $WC1
8,9M /tmp/wc1-git

Conclusión: Si mueves un archivo en Mercurial, volverá a ocupar espacio. Si mueves un archivo en git, el repositorio seguirá ocupando lo mismo (prácticamente).

Conservando historia entre moves

Este apartado continúa el script anterior:

Mercurial Git
$ hg -R $WC1 mv $WC1/example $WC1/file
$ hg -R $WC1 commit -m "third"
$ du -hs $WC1
11M /tmp/wc1-hg
$ hg -R $WC1 log $WC1/file --follow
changeset: 2:3d54fb888504
tag: tip
user: Miguel Angel Garcia <magmax@example.org>
date: Thu Jun 13 05:31:08 2013 +0200
summary: third

changeset: 1:f13ffb871397
user: Miguel Angel Garcia <magmax@example.org>
date: Thu Jun 13 05:31:07 2013 +0200
summary: second

changeset: 0:e47df7ca3541
user: Miguel Angel Garcia <magmax@example.org>
date: Thu Jun 13 05:31:07 2013 +0200
summary: initial
$ git --git-dir=$WC1/.git --work-tree=$WC1 mv example file
$ git --git-dir=$WC1/.git --work-tree=$WC1 commit -am "third"
[master dd45fed] third
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename example => file (100%)
$ du -hs $WC1
8,9M /tmp/wc1-git
$ git --git-dir=$WC1/.git --work-tree=$WC1 log -- file
commit dd45fedd449babb33a019f23a2dae244e18f400c
Author: Miguel Angel Garcia <magmax@example.org>
Date: Mon May 27 19:46:49 2013 +0200

    third

commit f206fe12540494bf1f6f5193bd6234865da75e13
Author: Miguel Angel Garcia <magmax@example.org>
Date: Mon May 27 19:46:48 2013 +0200

    second

commit 50f537cac82b6499c8519af7c7710211bf96c0c3
Author: Miguel Angel Garcia <magmax@example.org>
Date: Mon May 27 19:46:48 2013 +0200

    initial

Conclusión: Tanto Mercurial como Git siguen correctamente la historia del archivo. (Gracias, Juan Penalta)

Como dato curioso, se observa que al volver a mover el archivo, en esta ocasión no ocupa espacio en mercurial.

Dos pushes simultáneos sin conflicto

NOTA: En esta demostración se utilizará el archivo annotate-output, que se encuentra en el paquete “devscripts”. Tan solo añade la hora y el tipo de salida (stdout o stderr) y redirecciona todo a la salida estándar.

Mercurial Git
$ export SERVER=server-hg
$ export WC1=/tmp/wc1-hg
$ export WC2=/tmp/wc2-hg
$ hg init $SERVER
$ export SERVER=/home/miguel/server-git
$ export WC1=/tmp/wc1-git
$ export WC2=/tmp/wc2-git
$ git --bare init $SERVER
Initialized empty Git repository in /tmp/server-git/
$ echo -e '[hooks]\npretxnchangegroup.sleep=sleep 2' > $SERVER/.hg/hgrc
$ echo -e '#!/bin/bash\nsleep 2' > $SERVER/hooks/post-receive
$ chmod 755 $SERVER/hooks/post-receive
$ hg clone ssh://localhost/$SERVER $WC1
no changes found
updating to branch default
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ git clone $SERVER $WC1
Cloning into '/tmp/wc1-git'...
warning: You appear to have cloned an empty repository.
done.
$ touch $WC1/file
$ hg -R $WC1 add $WC1/file
$ hg -R $WC1 commit -m "initial version"
$ hg -R $WC1 push
pushing to ssh://localhost//tmp/server-hg
searching for changes
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 1 changesets with 1 changes to 1 files
$ touch $WC1/INITIAL
$ git --git-dir=$WC1/.git --work-tree=$WC1 add INITIAL
$ git --git-dir=$WC1/.git --work-tree=$WC1 commit -m "master branch creation"
[master (root-commit) 38576fe] master branch creation
 0 files changed
 create mode 100644 INITIAL
$ git --git-dir=$WC1/.git --work-tree=$WC1 push origin master
To /tmp/server-git
 * [new branch] master -> master
$ hg clone ssh://localhost/$SERVER $WC2
requesting all changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
updating to branch default
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ git clone $SERVER $WC2
Cloning into '/tmp/wc2-git'...
done.
$ touch $WC1/file2
$ hg -R $WC1 branch branch1
marked working directory as branch branch1
(branches are permanent and global, did you want a bookmark?)
$ hg -R $WC1 add $WC2/file2
$ hg -R $WC1 commit -m "changes on wc1"
$ touch $WC1/file
$ git --git-dir=$WC1/.git --work-tree=$WC1 checkout -b working-copy-1
Switched to a new branch 'working-copy-1'
$ git --git-dir=$WC1/.git --work-tree=$WC1 add file
$ git --git-dir=$WC1/.git --work-tree=$WC1 commit -m "change 1"
[working-copy-1 de8c105] change 1
 0 files changed
 create mode 100644 file
$ touch $WC2/fileB
$ hg -R $WC2 branch branch2
marked working directory as branch branch2
(branches are permanent and global, did you want a bookmark?)
$ hg -R $WC2 add $WC2/fileB
$ hg -R $WC2 commit -m "changes on wc2"
$ touch $WC2/fileB
$ git --git-dir=$WC2/.git --work-tree=$WC2 checkout -b working-copy-2
Switched to a new branch 'working-copy-2'
$ git --git-dir=$WC2/.git --work-tree=$WC2 add fileB
$ git --git-dir=$WC2/.git --work-tree=$WC2 commit -m "changes on wc2"
[working-copy-2 eee3889] changes on wc2
 0 files changed
 create mode 100644 fileB
$ annotate-output +"WC__1__%H:%M:%S" hg -R $WC1 push --new-branch &
$ annotate-output +"WC__2__%H:%M:%S" hg -R $WC2 push --new-branch
WC__2__13:31:54 I: Started hg -R /tmp/wc2-hg push --new-branch
WC__1__13:31:54 I: Started hg -R /tmp/wc1-hg push --new-branch
WC__1__13:31:57 O: pushing to ssh://localhost//tmp/server-hg
WC__1__13:31:57 O: searching for changes
WC__1__13:31:57 O: remote: adding changesets
WC__1__13:31:57 O: remote: adding manifests
WC__1__13:31:57 O: remote: adding file changes
WC__1__13:31:57 O: remote: added 1 changesets with 1 changes to 1 files
WC__1__13:31:57 I: Finished with exitcode 0
WC__2__13:31:58 E: abort: push failed:
WC__2__13:31:58 O: pushing to ssh://localhost//tmp/server-hg
WC__2__13:31:58 E: 'unsynced changes'
WC__2__13:31:58 O: searching for changes
WC__2__13:31:58 O: remote: waiting for lock on repository /tmp/server-hg held by
'nightcrawler:6267'
WC__2__13:31:58 I: Finished with exitcode 255
$ annotate-output +"WC__1__%H:%M:%S" git --git-dir=$WC1/.git --work-tree=$WC1
push origin master &
$ annotate-output +"WC__2__%H:%M:%S" git --git-dir=$WC2/.git --work-tree=$WC2
push origin master
WC__1__13:30:35 I: Started git --git-dir=/tmp/wc1-git/.git
--work-tree=/tmp/wc1-git push origin working-copy-1:working-copy-1
WC__2__13:30:35 I: Started git --git-dir=/tmp/wc2-git/.git
--work-tree=/tmp/wc2-git push origin working-copy-2:working-copy-2
WC__2__13:30:37 E: To /tmp/server-git
WC__1__13:30:37 E: To /tmp/server-git
WC__2__13:30:37 E: * [new branch] working-copy-2 -> working-copy-2
WC__1__13:30:37 E: * [new branch] working-copy-1 -> working-copy-1
WC__2__13:30:37 I: Finished with exitcode 0
WC__1__13:30:37 I: Finished with exitcode 0

Conclusión: Dos pushes simultáneos sobre ramas diferentes en Mercurial dará un error a la segunda que entre, si la primera se acepta. En git ambas podrán pasar.

¿Por qué ha ocurrido esto en Mercurial? ¿Qué es “Unsynced changes”? Mirando el código es un problema en el protocolo; Mercurial realiza 4 fases principales:

  1. Saludo
  2. Solicitar un hash con el estado de las cabezas del servidor.
  3. Ejecutar hooks
  4. Enviar los deltas con el hash. Si el hash no coincide, abortará.

En nuestro caso, un proceso se ha quedado bloqueado en el paso 3 y el otro ha obtenido una hash que no coincidirá más tarde. El resultado es que ambas ramas se bloquean hasta poder alcanzar el paso 4. La primera que llegue, modificará las cabezas y, por tanto, la hash, que hará que la segunda aborte. Si el primer proceso falla (un hook que no devuelve 0), la segunda pasará.

Nota personal: Subjetivamente, encuentro mucho más útiles los mensajes de Git que de Mercurial.