Dans CVS et SVN, l'unité de base d'information est la revision ou version, correspondant à l'état précis d'un fichier (pour CVS) ou de l'ensemble des fichiers du dépôt (pour SVN). Dans Mercurial, l'unité de base est le changeset , littéralement « ensemble de changements ». Pour simplifier, on peut dire que CVS et SVN stockent les versions successives des fichiers (et calculent ensuite les diff à la volée), tandis que Mercurial stocke les différences entre les versions (et recalcule les fichiers à la volée). Ça n'est pas toujours vrai techniquement, mais c'est une bonne approximation !
Pour bien se représenter un dépôt Mercurial, il faut l'imaginer comme un graphe orienté acyclique (pour éviter d'employer les grands mots : un arbre dans lequel certaines branches peuvent se rejoindre) :
Lorsqu'on souhaite identifier une revision, on peut le faire de plusieurs façons différentes :
CVS et SVN sont des systèmes centralisés. Il y a un dépôt qui sert de référence. On dispose bien sûr d'une copie locale de ses fichiers, qu'on peut modifier à loisir ; mais à chaque fois que l'on souhaite comparer sa copie locale avec celle du dépôt, ou revenir à une ancienne version, il est nécessaire d'être connecté à ce dépôt central.
Mercurial, tout comme GIT et BZR par exemple, est comparable à un système peer-to-peer. On dispose dans sa copie locale de toutes les versions précédentes du projet (ce qui permet d'effectuer des comparaisons et des retours en arrière sans se connecter au dépôt à chaque fois). On peut aussi créer des nouvelles versions (faire un commit) sans être connecté à ce dépôt. En fait, chaque développeur dispose de son propre dépôt, en local sur son ordinateur. L'échange avec les autres développeurs peut passer par le biais d'un dépôt central, ou bien par plusieurs dépôts.
Voici quelques exemples d'organisations possibles :
Créer un dépôt :
mkdir toto ; cd toto ; hg init
Récupérer un dépôt existant :
hg clone http://adresse.du.depot/chemin/
Ajouter un fichier au dépôt, renommer un fichier, effacer un fichier :
hg add toto.py ; hg mv toto.py titi.py ; hg rm titi.py
Enregistrer ses modifications dans son dépôt local :
hg commit -m "modification du zbouib" toto.py
Si on ne spécifie pas de message de commit, un éditeur se lance ; si on ne spécifie pas de fichier à enregistrer, tous les fichiers modifiés sont pris en compte.
Envoyer ses modifications au dépôt distant :
hg push
Récupérer les modifications distantes (cela ne modifie pas votre copie de travail) :
hg pull
Voir les 4 dernières modifications (les 4 derniers commits) :
hg log -r tip:-4
Intégrer dans sa copie de travail les nouvelles modifications :
hg update
Marquer une version précise :
hg tag 1.0.4
Marquer une branche (c'est comme un tag, sauf que cela reste sur les versions suivantes) :
hg branch sans_support_opengl
Lister les branches, les tags :
hg branches
et
hg tags
Commencer à travailer sur une autre branche, une autre version ... :
hg update -r nom_branche_ou_nom_tag_ou_hash_de_version
On peut créer un dépôt indépendant et le servir (sur un réseau local ou sur Internet, si on est sur une machine accessible de l'extérieur) sans avoir besoin d'avoir les droits root ou d'installer des logiciels supplémentaires. En effet, Mercurial contient un petit serveur intégré. Il suffit de créer un fichier de configuration minimal, pour indiquer à ce petit serveur qu'on peut l'utiliser en lecture-écriture (avec l'option allow_push), car par défaut, il est en lecture seule ; et qu'on autorise les connexions non cryptées (avec push_ssl).
# mkdir oeufdepaques # cd oeufdepaques oeufdepaques# hg init oeufdepaques# cat >.hg/hgrc <<EOF > [web] > allow_push = * > push_ssl = false > EOF oeufdepaques# hg serve
Bien sûr, il est possible (et même recommandé) de configurer Apache pour servir ses dépôts Mercurial ; mais il y a déjà des tonnes de documentation sur le net, qui expliquent comment faire cela, donc nous laisserons cette tâche en exercice au lecteur.
Une fois que le serveur de l'étape précédente est lancé, on peut aller dans un autre répertoire, et récupérer le dépôt (qui est vide pour l'instant) :
# hg clone http://localhost:8000/ lapin no changes found updating working directory 0 files updated, 0 files merged, 0 files removed, 0 files unresolved # cd lapin
On peut aussi créer un dépôt vide, puis le configurer à la main pour dire que par défaut, les opérations push et pull devront « parler » avec un certain dépôt distant. La suite de commandes ci-dessous est strictement équivalente à l'autre ci-dessus :
# mkdir lapin # cd lapin lapin# hg init lapin# cat >.hg/hgrc <<EOF > [paths] > default = http://localhost:8000/ > EOF lapin# hg pull pulling from http://localhost:8000/ no changes found
Et si l'on souhaite juste faire un pull ponctuel à partir d'un dépôt, on peut le spécifier en argument :
# mkdir lapin # cd lapin lapin# hg init lapin# hg pull http://localhost:8000/ pulling from http://localhost:8000/ no changes found
On va créer quelques fichiers et les ajouter au dépôt. Les commandes sont ici totalement identiques à celles que l'on utilise déjà pour CVS ou SVN :
lapin# touch chocolat.py lait.py lapin# hg add chocolat.py lait.py "ajout des premiers fichiers du projet" lapin# echo 'from cacao import beans' >> chocolat.py lapin# hg commit -m "correction d'un import manquant"
Si on souhaite voir les dernières modifications faites sur le dépôt, rien de plus simple. La commande hg log affiche toutes les modifications (sur un projet assez avancé, elle sera donc peu pratique à utiliser telle quelle, car elle risque d'afficher quelques milliers de lignes ; on utilisera alors, par exemple hg log -r tip:-4 pour indiquer qu'on veut voir le log depuis le tip - la dernière version en date - et remonter 4 versions en arrière) :
lapin# hg log changeset: 1:86e652ec797a tag: tip user: skaya@enix.org date: Mon Apr 13 12:17:57 2009 +0200 summary: correction d'un import manquant changeset: 0:803f0bf542b9 user: skaya@enix.org date: Mon Apr 13 12:17:02 2009 +0200 summary: ajout des premiers fichiers du projet
Si l'on souhaite revenir à une ancienne version, on fait comme avec SVN : on fait un update, mais en spécifiant une version spécifique en argument :
lapin# hg update -r 01 files updated 0 files merged, 0 files removed, 0 files unresolved
Et pour revenir à la version la plus récente, rien de plus simple :
lapin# hg update # ou hg update -r tip1 files updated 0 files merged, 0 files removed, 0 files unresolved
Les tags permettent d'identifier de manière lisible et stable une version donnée. Lisible, car il est plus facile de taper hg update -r release_1.0.4 que hg update -r 86e652ec797a
Et stable, car les numéros locaux de version peuvent changer d'un dépôt à l'autre, ou bien si on fait un pull qui ajoute des versions intermédiaires. En réalité, les tags sont stockés dans un fichier .hgtags , lui aussi contrôlé par Mercurial (comme n'importe quel fichier du dépôt). Il n'y a donc aucune « magie » là-dedans. Notons que la commande hg tag fait implicitement un commit, et crée donc une nouvelle version :
lapin# hg tag avant_split_chocolat lapin# cat .hgtags 86e652ec797a672d413b556d2ccec28f3cddb96d avant_split_chocolat
Faisons quelques autres modifications :
lapin# mkdir chocolat lapin# touch chocolat/noir.py chocolat/blanc.py lapin# hg add chocolat lapin# hg rm chocolat.py lapin# hg commit -m "séparation de chocolat.py en deux sous-modules"
Et regardons ensuite le journal des modifications :
lapin# hg log changeset: 3:d92c1aae7b39 tag: tip user: skaya@enix.org date: Mon Apr 13 12:22:53 2009 +0200 summary: séparation de chocolat.py en deux sous-modules changeset: 2:46e88f07aaef user: skaya@enix.org date: Mon Apr 13 12:20:36 2009 +0200 summary: Added tag avant_split_chocolat for changeset 86e652ec797a changeset: 1:86e652ec797a tag: avant_split_chocolat user: skaya@enix.org date: Mon Apr 13 12:17:57 2009 +0200 summary: correction d'un import manquant changeset: 0:803f0bf542b9 user: skaya@enix.org date: Mon Apr 13 12:17:02 2009 +0200 summary: ajout des premiers fichiers du projet
Comme promis, on peut utiliser le tag comme argument d'un hg update (et on pourra l'utiliser pour toutes les commandes prenant un argument -r et attendant un numéro de version comme argument) :
lapin# hg update -r avant_split_chocolat 1 files updated, 0 files merged, 3 files removed, 0 files unresolved
Comment Mercurial gère-t-il la division du développement en plusieurs branches séparées ? Contrairement à SVN qui ne propose aucun mécanisme particulier pour cela (c'est à chacun de se discipliner et de créer des sous-répertoires, d'où la disposition typique d'un dépôt Subversion avec les répertoires trunk, branches et tags), Mercurial dispose du concept de head . Mettons-le à l'épreuve :
lapin# echo 'from farm import cow, goat' >> lait.py lapin# hg commit -m "imports manquants dans lait.py" created new head
Mercurial nous indique que ce 'commit' a crée une nouvelle « tête » - on a donc crée un « split » du code, ou une « branche », appelons-cela comme on veut. Par défaut, cette branche n'a pas de nom particulier ; mais nous verrons plus tard qu'on peut associer un nom à une branche afin de s'y retrouver plus facilement.
Une commande bien pratique et très simple permet de voir toutes les heads . Bien sûr, sur un projet très complexe, il peut y avoir plusieurs dizaines de heads , et la commande n'est alors plus si pratique que ça.
lapin# hg heads changeset: 4:a8f6f55f6674 tag: tip parent: 1:86e652ec797a user: skaya@enix.org date: Mon Apr 13 12:25:43 2009 +0200 summary: imports manquants dans lait.py changeset: 3:d92c1aae7b39 user: skaya@enix.org date: Mon Apr 13 12:22:53 2009 +0200 summary: séparation de chocolat.py en deux sous-modules
Vous vous sentez perdu ? Vous avez besoin d'un plan ? Pas de problème, Mercurial a pensé à tout. Essayez donc : lapin# hg view
On peut commencer à envoyer nos modifications au dépôt qui a été créé au début de la démo. La commande push sert à cela, et elle a le bon goût de nous indiquer laconiquement qu'on a créé une nouvelle head sur le dépôt distant.
lapin# hg push pushing to http://localhost:8000/ searching for changes adding changesets adding manifests adding file changes added 5 changesets with 7 changes to 5 files (+1 heads)
Un autre développeur peut commencer à travailler avec notre code, sur son dépôt local :
# hg clone http://localhost:8000/ carotte requesting all changes adding changesets adding manifests adding file changes added 5 changesets with 7 changes to 5 files (+1 heads) updating working directory 2 files updated, 0 files merged, 0 files removed, 0 files unresolved # cd carotte carotte# hg heads changeset: 4:a8f6f55f6674 tag: tip parent: 1:86e652ec797a user: skaya@enix.org date: Mon Apr 13 12:25:43 2009 +0200 summary: imports manquants dans lait.py changeset: 3:d92c1aae7b39 user: skaya@enix.org date: Mon Apr 13 12:22:53 2009 +0200 summary: séparation de chocolat.py en deux sous-modules
Lorsqu'on fait un update qui fait passer d'une branche à l'autre, Mercurial nous demande si l'on souhaite fusionner (merge) ces deux branches, ou si l'on souhaite simplement passer d'une branche à l'autre (pour travailler sur une autre portion du code, par exemple) :
carotte# hg update -r 3 abort: crosses branches (use 'hg merge' or 'hg update -C') carotte# hg update -r 3 --clean # le "--clean", ou "-C", veut dire "oublie mes modifs locales" 4 files updated, 0 files merged, 1 files removed, 0 files unresolved
Cela se passe le plus simplement du monde : on utilise la commande hg branch. Une branch dans Mercurial, c'est un peu comme un tag , mais qui n'identifie pas une version précise, mais plutôt toutes les versions à partir d'une version donnée. Si on a en tête l'image du dépôt sous forme d'arbre (ou plus précisément, de graphe acyclique orienté, comme évoqué en introduction), la notion de branche est assez intuitive. Notons que la création d'une branche, contrairement à celle d'un tag, ne fait pas de commit implicite :
carotte# hg branch bichoco marked working directory as branch bichoco carotte# hg commit -m "création de la branche bichoco avec le sous-rep chocolat" carotte# hg update -r 4 # on revient à la branche où on n'a pas séparé les fichiers carotte# hg branch monochoco marked working directory as branch monochoco carotte# hg commit -m "création de la branche avec un seul module chocolat.py" carotte# hg branches monochoco 6:c62a89e0b18e bichoco 5:f69e772d76b7 default 4:a8f6f55f6674 (inactive)
Une branche est « inactive » si elle ne contient pas de head . Cela se tient : a priori, quand on fait un commit, c'est toujours sur une head ; si une branche ne contient pas de head , on ne peut pas y faire de commit, la branche est donc inactive. Bien sûr, on peut à tout instant créer une head n'importe où (et donc rendre une branche active).
On peut passer d'une branche à l'autre avec hg update, avec exactement la même syntaxe que pour les tags et les numéros de version :
carotte# hg update -r bichoco 4 files updated, 0 files merged, 1 files removed, 0 files unresolved carotte# touch chocolat/noisettes.py carotte# hg add chocolat/noisettes.py carotte# hg commit -m "ajout d'une variété de chocolat" carotte# hg update -r monochoco 2 files updated, 0 files merged, 4 files removed, 0 files unresolved carotte# echo 'cow.debug()' >> lait.py carotte# hg commit -m "activation du debug sur les vaches"
Tiens, et si on a oublié de faire un commit, et qu'on essaie de changer de branche ? Pas de panique, Mercurial ne nous laisse pas faire, et nous demande si on souhaite abandonner les modifications locales :
carotte# echo 'goat.debug()' >> lait.py carotte# hg update -r bichoco abort: crosses named branches (use 'hg update -C' to discard changes) carotte# hg update -r bichoco --clean # tant pis pour les chèvres 5 files updated, 0 files merged, 1 files removed, 0 files unresolved
Parfois (souvent même), on a deux branches qui évoluent indépendamment (par exemple, la branche « avec le nouveau driver vidéo » et la branche « avec le nouveau driver son »), et à un point donné, on souhaite les réunir, les fusionner.
La commande à utiliser est hg merge -r 57 si on souhaite par exemple fusionner la version en cours avec la version 57. Bien entendu, comme pour toutes les autres commandes acceptant un numéro de version, on peut spécifier un numéro local, un tag, un hash, ou un nom de branche.
Si on est dans une head et qu'il y a exactement une autre head dans la repository, on peut taper hg merge sans argument.
Petit exemple - on commence par poser un tag avant de diviser le code en deux branches :
carotte# hg tag avant_separation_lait carotte# mkdir lait carotte# touch lait/ecreme.py lait/entier.py carotte# hg add lait adding lait/ecreme.py adding lait/entier.py carotte# hg rm lait.py carotte# hg commit -m "séparation du module lait.py en sous-modules"
Puis on revient à la version sur laquelle on avait posé le tag, et on fait d'autres modifications, afin de créer la nouvelle branche :
carotte# hg update -r avant_separation_lait 2 files updated, 0 files merged, 2 files removed, 0 files unresolved carotte# touch cloche.py carotte# hg add cloche.py carotte# hg commit -m "Pacques avec des cloches, c'est mieux" created new head carotte# echo 'from modules import *' >> cloche.py carotte# hg commit -m "imports pour le fichier cloche"
Arrivé à ce stade, on a trois heads :
carotte# hg heads changeset: 12:d085363e36ad branch: bichoco tag: tip user: skaya@enix.org date: Tue Apr 14 11:26:31 2009 +0200 summary: imports pour le fichier cloche changeset: 10:ca090e171d78 branch: bichoco user: skaya@enix.org date: Tue Apr 14 08:48:15 2009 +0200 summary: séparation du module lait.py en sous-modules changeset: 8:c81783a7b4c2 branch: monochoco parent: 6:c62a89e0b18e user: skaya@enix.org date: Tue Apr 14 08:37:19 2009 +0200 summary: activation du debug sur les vaches
On revient sur une de ces heads pour faire une modification :
carotte# hg update -r 10 -C carotte# touch lait/demiecreme.py carotte# hg add lait/demiecreme.py carotte# hg commit -m "ajout du lait demi-écrémé" carotte# hg heads changeset: 13:546203b18b0a branch: bichoco tag: tip parent: 10:ca090e171d78 user: skaya@enix.org date: Tue Apr 14 11:37:39 2009 +0200 summary: ajout du lait demi-écrémé changeset: 12:d085363e36ad branch: bichoco user: skaya@enix.org date: Tue Apr 14 11:26:31 2009 +0200 summary: imports pour le fichier cloche changeset: 8:c81783a7b4c2 branch: monochoco parent: 6:c62a89e0b18e user: skaya@enix.org date: Tue Apr 14 08:37:19 2009 +0200 summary: activation du debug sur les vaches
Et on demande explicitement de fusionner la head courante avec la version locale numéro 12 :
carotte# hg merge -r 12 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) carotte# hg diff diff -r 546203b18b0a cloche.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cloche.py Tue Apr 14 13:48:53 2009 +0200 @@ -0,0 +1,1 @@ +from modules import * carotte# hg commit -m "fusion de la branche lait.py/module lait séparé" carotte# hg heads changeset: 14:845a003202fd branch: bichoco tag: tip parent: 13:546203b18b0a parent: 12:d085363e36ad user: skaya@enix.org date: Tue Apr 14 13:50:52 2009 +0200 summary: fusion de la branche lait.py/module lait séparé changeset: 8:c81783a7b4c2 branch: monochoco parent: 6:c62a89e0b18e user: skaya@enix.org date: Tue Apr 14 08:37:19 2009 +0200 summary: activation du debug sur les vaches
Puis, on envoie nos modifications vers le dépôt, pour les autres développeurs :
carotte# hg push pushing to http://localhost:8000/ searching for changes adding changesets adding manifests adding file changes added 10 changesets with 8 changes to 7 files
Que se passe-t-il si un autre développeur fait des modifications sur les mêmes fichiers que nous ? Au moment de faire un push (pour envoyer ses données sur le dépôt), il va recevoir un message lui disant « push creates new remote heads! », c'est-à-dire que quelqu'un d'autre a fait un commit au même endroit que lui. Il y a alors deux options : forcer le push (et créer une ou plusieurs heads sur le dépôt distant), ou bien faire d'abord un pull et tenter un éventuel merge avant de refaire le push. L'exemple qui suit montre le second cas (la manière « propre ») :
lapin# echo 'lang=en' > eastereggs.conf lapin# hg add eastereggs.conf lapin# hg commit -m "ajout du fichier de config" lapin# hg push pushing to http://localhost:8000/ searching for changes abort: push creates new remote heads! (did you forget to merge? use push -f to force) lapin# hg pull pulling from http://localhost:8000/ searching for changes adding changesets adding manifests adding file changes added 10 changesets with 8 changes to 7 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) lapin# hg heads changeset: 15:845a003202fd branch: bichoco tag: tip parent: 14:546203b18b0a parent: 13:d085363e36ad user: skaya@enix.org date: Tue Apr 14 13:50:52 2009 +0200 summary: fusion de la branche lait.py/module lait séparé changeset: 9:c81783a7b4c2 branch: monochoco parent: 7:c62a89e0b18e user: skaya@enix.org date: Tue Apr 14 08:37:19 2009 +0200 summary: activation du debug sur les vaches changeset: 5:9fc05d4f2214 user: skaya@enix.org date: Fri Apr 17 21:18:13 2009 +0200 summary: ajout du fichier de config lapin# hg merge -r 9 lapin# 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) lapin# hg diff diff -r 9fc05d4f2214 lait.py --- a/lait.py Fri Apr 17 21:18:13 2009 +0200 +++ b/lait.py Fri Apr 17 21:20:56 2009 +0200 @@ -1,1 +1,2 @@ from farm import cow, goat +cow.debug() lapin# hg commit -m "merge de la conf dans la branche monochoco" lapin# hg parents -r tip changeset: 5:9fc05d4f2214 user: skaya@enix.org date: Fri Apr 17 21:18:13 2009 +0200 summary: ajout du fichier de config changeset: 9:c81783a7b4c2 branch: monochoco parent: 7:c62a89e0b18e user: skaya@enix.org date: Tue Apr 14 08:37:19 2009 +0200 summary: activation du debug sur les vaches lapin# hg update -r 5 1 files updated, 0 files merged, 0 files removed, 0 files unresolved lapin# hg merge -r 15 local changed lait.py which remote deleted use (c)hanged version or (d)elete? d 8 files updated, 0 files merged, 2 files removed, 0 files unresolved (branch merge, don't forget to commit) lapin# hg commit -m "merge de la conf dans la branche bichoco" lapin# hg push pushing to http://localhost:8000/ searching for changes adding changesets adding manifests adding file changes added 3 changesets with 1 changes to 1 files
Mais on peut aussi forcer le push, cela ne pose pas de problème particulier (à part que cela crée de nouvelles heads , ce qui n'est pas forcément ce qu'on veut!) :
carotte# echo 'lang=fr' > eastereggs.conf carotte# hg add eastereggs.conf carotte# hg commit -m "ajout du fichier de conf en français" carotte# hg push pushing to http://localhost:8000/ searching for changes abort: push creates new remote heads! (did you forget to merge? use push -f to force) carotte# hg push -f pushing to http://localhost:8000/ searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) carotte# hg pull pulling from http://localhost:8000/ searching for changes adding changesets adding manifests adding file changes added 3 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) carotte# hg heads changeset: 18:3f43056373ce tag: tip parent: 16:9fc05d4f2214 parent: 14:845a003202fd user: skaya@enix.org date: Fri Apr 17 21:28:23 2009 +0200 summary: merge de la conf dans la branche bichoco changeset: 17:925a9da09222 parent: 16:9fc05d4f2214 parent: 8:c81783a7b4c2 user: skaya@enix.org date: Fri Apr 17 21:22:12 2009 +020 summary: merge de la conf dans la branche monochoco changeset: 15:cf32f157731c branch: bichoco user: skaya@enix.org date: Fri Apr 17 21:30:16 2009 +0200 summary: ajout du fichier de conf en français carotte# hg merge -r 17
Je suis perdu !
Il vous faut un plan ? Essayez : hg view :-)
Nous avons vu comment :
Quelques commandes simples n'ont pas été détaillées, mais leur utilisation est très intuitive :
hg cat -r 57 toto.py qui permet d'afficher le fichier toto.py tel qu'il était dans la version numéro 57 ;
hg grep -r 4 import qui permet de rechercher la chaîne "import" dans tous les fichiers de la version numéro 4 ;
hg diff -r release_1.0:422 qui permet d'afficher les différences entre la version ayant le tag « release_1.0 » et la version numéro 422.
Il reste quelques concepts qui n'ont pas été abordés par ce document, tels que (liste non exhaustive) :
Actions sur le document