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 |
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 |
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 |
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 |
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 |
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() { |
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.