Git contre Mercurial : pourquoi Git ?

Depuis la nuit des temps (1970), les geeks ont vécu une guerre longue et sanglante entre le vrai et le faux, le bien et le mal, Emacs et Vim. Plus récemment un autre type d'outils ont jeté de l'huile sur le feu en appelant une fois de plus les geeks à un nouveau combat par blogs interposés au lieu de réellement travailler. Je parle bien entendu de l'épineux débat entre Git et Mercurial.

Cet article prend parti pour Git et se penche sur quelques raisons pour lesquelles Git a fortement progressé dans sa domination au cours de cette lutte épique.

Mises en garde ooooouuuuuhh!

Avant tout, je voudrais être franc et admettre que je suis la dernière personne à affirmer que Git est parfait. Bien au contraire. J'ai passé beaucoup trop d'heures de ma vie à essayer d'expliquer pourquoi Git me donnait des résultats totalement inattendus. Plus particulièrement, je me sens nerveux et commence à être bizarre quand je dois expliquer les différents modes de la commande checkout. Malgré le fait que msysgit soit une version de Git pour Windows incroyablement efficace, après toutes ces années, il occupe encore la place d'un citoyen de second rang.

Quoi qu'il en soit, j'ai commencé mon usage des DVCS avec Mercurial, puis je suis passé à Git et ne suis jamais revenu en arrière.

Pourquoi en est-il ainsi?

Format de Stockage

Pour moi, l’unique partie la plus distinctive de Git est son modèle de stockage. Plusieurs avantages que je trouve à Git découlent de la façon dont il stocke et "pense" le contenu (de son référentiel).

D'un côté, Mercurial a tout misé sur les journaux incrémentaux, optimisant ainsi (raisonnablement) l'usage sur des disques lents. Quand à lui, Git stocke chaque fichier dans un référentiel simple. Tous les commits que vous faites, chaque version de chaque fichier, se retrouveront dans ce référentiel comme une entité distincte. Avant qu’il n’introduise l'archivage et la compression, ce procédé a été extrêmement inefficace. Mais l'idée était bonne, et est restée à ce jour. Il est important de noter que l'identité de chaque objet est un hash du contenu, ce qui signifie que tout est immuable. Pour changer quelque chose aussi simple qu'un message de commit, vous devez créer un nouvel objet commit avant. Cela conduit à...

Un historique plus fiable avec Git

Noooon, vraiment !

J'ai toujours été irrité par les affirmations disant que Git est « destructif ». Au contraire, je voudrais prétendre que Git est en fait la plus sûre de toutes les solutions de DVCS. Comme nous l'avons vu ci-dessus, Git ne vous laisse jamais changer réellement quoi que ce soit, il suffit de créer de nouveaux objets. Qu'en est-il de l'ancienne version alors ? Git, pourkoi-ta-pa-konservé-mes-modifications ?!?

Git assure en fait le suivi de chaque modification effectuée, en les stockant dans le reflog. Parce que chaque commit est unique et immuable, le reflog se contente de faire référence à chacune d'entre eux. Au bout de trente jours, Git supprime les entrées du reflog, de manière à ce qu'ils puissent être traités par le ramasse-miette. Il faut se le dire : Git ne supprimera aucun élément encore référencé. Les Branches sont évidemment la façon la plus utile de garder les références aux validations, mais le reflog est une autre et vous n'avez même pas à y penser!

La commande reflog, elle, vous permet d'inspecter cet historique des modifications, comme vous le feriez pour vos commits normaux et la commande "git log". A toujours garder sous le coude !

1
2
3
4
5
6
7
8
> git reflog
5adb986 HEAD@{0}: rebase: Use JSONObject instead of strings
6a34803 HEAD@{1}: checkout: moving from finagle to 6a3480325f3beeecbafd351d30877694963a3f01^0
74bd03e HEAD@{2}: commit: Use JSONObject instead of strings
36c9142 HEAD@{3}: checkout: moving from 36c9142e81482f6c3eb8ad110642206a4ea3dfec to finagle
36c9142 HEAD@{4}: commit: Finagle and basic folder/json
1090fb7 HEAD@{5}: commit: Ignore Eclipse files
d6e3e63 HEAD@{6}: checkout: moving from master to d6e3e63889fd98e89e12e53a79bf96b53cbf9396^0

Réécrire l’Histoire

Ce que je n’ai jamais aimé avec Mercurial, c'est qu'il rend la tâche très difficile quand il s'agit de modifier rétroactivement les commits. Vous pouvez vous demander « Pourquoi voudrais-je faire cela ? ». Si une contribution affecte de nombreux fichiers ou implique une re-factorisation significative, il est plus facile d'examiner les modifications dans un contexte compréhensible. Avec Git, il est facile de « remonter dans le temps » et modifier les modifications antérieures si nécessaire. Ainsi, les journaux de modifications dans Git peuvent être des histoires soigneusement élaborées, plutôt que de fidèles (mais obscures) suites de modifications réalisées les unes après les autres.

Il existe une extension pour Mercurial qui fait essentiellement la même chose appelée Mercurial Queues (MQ). Les MQ sont un moyen d'empiler des pre-modifications de manière à pouvoir les réorganiser avant de décider finalement d'en faire un validation réelle. MQ est livré avec un tas de bouquet de commandes séparées (qui ne sont pas dans SVN !).

1
2
3
4
5
6
hg qnew firstpatch
hg qrefresh
hg qdiff
hg qnew secondpatch
hg qrefresh
hg qcommit

Dans Git, il suffit de faire ses commit comme d'habitude sans se soucier de ce qu’on en fera plus tard. Le moment venu, il y a juste chose que vous devez savoir faire : rebase interactif. Cette commande lance un éditeur de texte et vous permet de modifier l'histoire de Git au centre de votre contenu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> git rebase --interactive origin/master
pick   94f56db Debug an error raised when importing view
squash 772e7e8 Re-join comments using DELIM
reword a04f10e Error on filter branch - print line
pick   e09b0a2 Added troubleshooting for msysgit + Cygwin
fixup  276c49a Added troubleshooting for missing master_cc branch
pick   a2c08f6 Added exclude configuration
pick   4c09e5e Ignore errors from _really_ long file paths
pick   9f38cf0 Actually, use fnmatch for exclude

# Rebase f698827..9f38cf0 onto f698827
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.

Mercurial possède à peu près l’équivalent en l'extension histedit mais cette dernière utilise strip pour mettre à jour le référentiel qui est normalement incrémental, ce qui pour conséquence de générer un fichier de sauvegarde externe. Je me demande comment on peut extraire des informations historiques de cette sauvegarde ? Combien de temps dois-je les conserver ? De quelle nouvelle commande ai-je besoin pour la restaurer ?

Revenons à Git. Ce que je crains, c'est de perdre un commit plus vieux que la période de rétention de 30 jours du reflog. Si seulement il y avait un moyen d'empêcher Git de d'appliquer le ramasse-miettes au bout d'un mois ! Une façon, vous savez, d'apposer une étiquette pour une référence ultérieure "juste au cas où"...

Une branche, par exemple ?

Exact ! Parce que ces « sauvegardes » dans Git sont en fait juste des commits (les branches en moins), nous avons à notre disposition le reflog et, de fait, vous n'avez pas besoin d'apprendre un autre ensemble de commandes pour savoir les utiliser.

« Rendre les choses aussi simples que possible, mais pas plus simple ».

Git et les branches

Pendant un moment, la gestion des branches dans Git était la « killer feature ». Mercurial recommandait (et le recommande encore maintenant) de cloner un dépôt pour chaque branche. Attendez, on parle de DVCS et pas de SVN là ? Ils avaient aussi une commande dédiée « branch », qui appose une marque à chaque commit. Une fois apposée, le marquage était impossible à modifier sauf à fusionner ou clore la branche. Plus tard, suite à une forte demande, l'extension "Bookmark" a été introduite comme un clone direct de branches de Git, même si au départ vous ne pouviez pas pousser les bookmarks sur le serveur distant.

1
2
3
4
5
6
7
8
9
10
> git fetch
From bitbucket.org:atlassian/helloworld
* [new branch]      test       -> origin/test
565ad9c..9e4b1b8  master     -> origin/master

> git log --graph --oneline --decorate --all
* 9e4b1b8 (origin/master, origin/test) Remove unused variable
| * 565ad9c (HEAD, master) Added Hello example
|/
* 46f0ac9 Initial commit

Un avantage qui demeure cependant, c'est que les bookmarks dans Mercurial partagent un espace de noms unique. Pour comprendre ce que cela signifie, nous allons jeter un coup d’oeil à un scénario assez normal où quelqu'un a poussé certaines modifications sur le serveur.

"Je demande à la vraie branche master de bien vouloir se lever !" Il n'y a évidemment rien de choquant à demander cela. Il se trouve simplement qu'il y a 2 branches qui ont le même nom, master. Mentionner l'espace de nom du serveur ("origin" dans notre cas) rend les choses beaucoup plus claires.

Qu’en est-il de Mercurial?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> hg pull
pulling from default
importing bookmark test
divergent bookmark master stored as master@default

> hg glog
o changeset: 2:98c63da09bb1
| bookmark: master@default
| bookmark: test
| summary: Third commit
|
| o changeset: 1:d9989a0da93e
| | bookmark: master
| | summary: Second commit
|/
o changeset: 0:2e92d3b3d020
summary: First commit

Quand nous faisons un pull, vous pouvez voir que nous avons une branche (master) qui entre en conflit avec notre propre master et une autre (test) qui ne pose pas de souci. Parce qu'il n'y a pas de notion d'espaces de noms, nous n'avons aucun moyen de savoir quels bookmarks sont locaux et celles qui sont distants, et selon notre qualification de ces derniers, nous pourrions commencer à avoir des conflits.

Staging

Dans Git, le staging, soit on l'adore, soit on le déteste ! Git a cette chose étrange qu'il appelle confusément l'index. Certains l'appellent aussi "zone de transit". Soit.

Toute chose qu'on souhaite ajouter à un commit dans Git doit d'abord passer par l'index. Comment placer le contenu dans l'index ? Avec « git add ». Cela est logique pour les utilisateurs SVN pour les nouveaux fichiers, mais ça peut devenir assez déroutant de devoir le faire pour les fichiers qui ont déjà un historique de modifications. La chose à garder à l'esprit est que vous « ajoutez » vos modifications, et non les fichiers eux-mêmes. Ce que j'aime à ce sujet, c'est que vous savez exactement ce qui va être envoyé dans l'historique.

Pour expliquer ce que je veux dire, une commande que j'utilise le plus souvent que d’autre est "patch". "patch" vous permet d’ajouter des morceaux ou des extraits spécifiques dans un fichier, au-delà d'une approche tout ou rien.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
> git add --patch

diff --git a/OddEven.java b/OddEven.java
index 99c0659..911da1b 100644
--- a/OddEven.java
+++ b/OddEven.java
@@ -32,6 +32,7 @@ public class OddEven {
* Object) and initializes it by calling the constructor.  The next line of code calls
* the "showDialog()" method, which brings up a prompt to ask you for a number
*/
+        System.out.println("Debug");
OddEven number = new OddEven();
number.showDialog();
}
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? n
@@ -49,7 +50,7 @@ public class OddEven {
* After that, this method calls a second method, calculate() that will
* display either "Even" or "Odd."
*/
-            this.input = Integer.parseInt(JOptionPane.showInputDialog("Please Enter A Number"));
+            this.input = Integer.parseInt(JOptionPane.showInputDialog("Please enter a number"));
this.calculate();
} catch (final NumberFormatException e) {
/*
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? y

Vous pouvez constater que j'avais oublié de supprimer une instruction de débogage, c’est une bonne chose de l'avoir vu avant le commit ! Ce qui est bien c’ est que, si je le veux, je peux le laisser là mais en acceptant le deuxième « morceau ». Tout cela vient du même fichier et sans avoir à ré-éditer quoi que ce soit après que je me sois rendu compte de mon erreur.

Sans surprise, Mercurial a une extension de l'enregistrement qui imite ce comportement. Mais parce que c'est juste une extension (ou du moins une extension basique), il faut copier les modifications non mises en transit vers un emplacement temporaire, mettre à jour les fichiers de travail de stockage, valider et ensuite annuler les modifications. Si vous faites une erreur, vous devrez recommencer. Ce qui est agréable au sujet de l’approche Git, c’est que de manière fondamentale, Git connaît, sait et prend en charge l'index et ne doit pas toucher à vos fichiers. Lorsque vous exécutez un "status" après avoir fait vos modifications, vous pouvez vérifier si tout semble correct avant de continuer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> git status

# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#   modified:   OddEven.java
#
# Changes not staged for commit:
#   (use "git add ..." to update what will be committed)
#   (use "git checkout -- ..." to discard changes in working directory)
#
#   modified:   OddEven.java
#

Pour ceux qui ont besoin de tester des changements qui ne sont pas dans l'index, il y a toujours "git stash --keep-index" pour archiver temporairement ce qui ne sera pas commité. Soit dit en passant, cette archive est stockée, sans surprise, comme un autre commit qui peut être mentionné par notre vieil ami reflog.

1
2
> git reflog --all
b7004ea refs/stash@{0}: WIP on master: 46f0ac9 Initial commit

Le jeu du blâme (git blame)

Une des choses intéressantes dans Git, c'est qu'il ne fait pas le suivi des renommages. Il s'agit d'une source de préoccupation pour certaines personnes, mais je pense que "Git a raison". Ceci dit, qu'est-ce qu'un renommage ? C'est simplement le déplacement d'un contenu d'un emplacement vers un autre. Mais que se passe-t-il si nous déplaçons seulement des parties du fichier ? "git blame" est une commande très utile qui affiche normalement les dernier commits qui ont affecté chaque ligne d'un fichier. Avec la magie de l’option "-C" , il détecte les lignes déplacées d'un fichier à un autre. (Le "-s" dans ce cas consiste à supprimer certaines information de commits inutiles).

1
2
3
4
5
6
7
8
9
10
11
> git blame -s -C OddEven.java

d46f0ac9 OddEven.java     public void run() {
d46f0ac9 OddEven.java         OddEven number = new OddEven();
d46f0ac9 OddEven.java         number.showDialog();
d46f0ac9 OddEven.java     }
d46f0ac9 OddEven.java
565ad9cd Hello.java       public static void main(final String[] args) {
565ad9cd Hello.java           new Hello();
565ad9cd Hello.java       }
d46f0ac9 OddEven.java }

Notez que ce ne sont pas toutes les lignes qui proviennent de cet unique fichier. Cet enfant terrible n'a pas d'équivalent Mercurial.

Conclusion

Git signifie ne jamais avoir à dire, « vous auriez dû ... ». Il arrive cependant que Mercurial dise exactement cela. A la seconde ou vous voulez re-baser ou modifier un commit ou utiliser des branches de référentiel unique (aussi appelés bookmarks) — je sais que je fais ça tous les jours — vous marchez à l'extérieur de la zone de confort de Mercurial. Le format du référentiel conçu pour des ajout incrémentaux a été intentionnellement conçu sans cette possibilité à l'esprit. Je suis d'accord avec Scott Chacon (de GitHub) qui dit que Mercurial est un « Git Lite ».

Git n'est pas parfait. Cependant, je dirais qu'il y a des choses plus importantes que d'avoir une ligne de commande en peluche. Bien sûr, ce serait bien si Git faisait un peu mieux les choses, affichait des messages d’erreurs moins cryptiques, était plus performant sous Windows, etc.. Mais à la fin de la journée, on se rend compte que ces choses sont superficielles. Utilisez un alias si vous n'aimez pas une commande particulière. Arrêtez d'utiliser Windows (non sérieusement !). Le format du référentiel décide de ce qui est possible avec nos outils DVCS, maintenant et pour l'avenir.

Cet article est une traduction d'un article de Charles O’Farrell, développeur chez Atlassian, qui se focalise sur les raisons pour lesquelles une équipe pourrait choisir Git plutôt que son DVCS habituel. Charles a pris le temps de coder sous de multiples DVCS et a passé un moment à basculer des utilisateurs de ClearCase à Git.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.