Copyright © 2006, 2007, 2008, 2009 Bryan O'Sullivan
Table of Contents
changegroup
—after
remote changesets addedcommit
—after a new
changeset is createdincoming
—after one
remote changeset is addedoutgoing
—after
changesets are propagatedprechangegroup
—before starting
to add remote changesetsprecommit
—before
starting to commit a changesetpreoutgoing
—before
starting to propagate changesetspretag
—before
tagging a changesetpretxnchangegroup
—before
completing addition of remote changesetspretxncommit
—before
completing commit of new changesetpreupdate
—before
updating or merging working directorytag
—after tagging a
changesetupdate
—after
updating or merging working directoryseries
fileseries
fileList of Figures
hello
my-hello
et my-new-hello
.
my-hello
vers le dépôt my-new-hello
List of Tables
Table of Contents
Il y a quelques années, quand j'ai voulu expliquer pourquoi je pensais que la gestion de révisions distribuée était importante, le domaine était encore si nouveau qu'il n'y avait presque aucune littérature publiée pour servir de référence aux personnes intéressées.
Bien qu'à cette époque je passais beaucoup de temps à travailler sur les entrailles de Mercurial, je me suis mis à la rédaction de ce livre parce que ça me semblait être la manière la plus efficace d'aider notre logiciel à toucher un vaste public, toujours avec l'idée que la gestion de révisions devrait être distribuée par nature. J'ai publié ce libre en ligne sous une licence libre pour la même raison : pour diffuser la parole auprès du monde.
Il y a un rythme familier à un bon livre sur un logiciel qui ressemble de près au fait de conter une histoire : Pourquoi ceci est ? Pourquoi ceci est important ? Comment peut-il m'aider ? Comment m'en servir ? Dans ce livre, j'essaye de répondre à toutes ces questions pour la gestion de révisions distribuée en général, et pour Mercurial en particulier.
En achetant une copie de ce livre, vous soutenez le développement et la liberté de Mercurial en particulier, et dans l'Open Source, au logiciel libre en général. O'Reilly Media et moi-même donnons les revenus issus des ventes de ce livre à la Software Freedom Conservancy (http://www.softwarefreedom.org/) qui fournit un support juridique à Mercurial et à de nombreux autres projets Open Source proéminents et de qualité.
Ce livre n'aurait pas vu le jour sans les efforts de Matt Mackal, l'auteur et le chef du projet Mercurial. Il est assisté très efficacement par des centaines de contributeurs volontaires à travers le monde.
Les enfants, Cian et Ruairi, ont toujours été prêt à m'aider à me reposer avec de merveilleux et impulsif jeux d'enfants. Je tiens aussi à remercier mon ex-femme, Shannon, pour son soutien.
Mes collègues et amis m'ont aidé et assisté de de nombreuses manières. Cette liste de personnes est forcément très incomplète : Stephen Hahn, Karyn Ritter, Bonnie Corwin, James Vasile, Matt Norwood, Eben Moglen, Bradley Kuhn, Robert Walsh, Jeremy Fitzhardinge, Rachel Chalmers.
J'ai conçu ce livre de manière ouverte, en publiant des brouillons de chapitres du livre sur des site web, au fur et à mesure que je les réalisais. Leurs lecteurs m'ont fait des retours utilisant l'application web que j'avais développée. À la fin de son écriture, plus de 100 personnes m'avaient fait des commentaires, un chiffre incroyable quand on considère que ce système de commentaire n'a tourné que dans les deux derniers mois de la rédaction du livre.
J'aimerais particulièrement remercier les personnes suivantes, dont les commentaires représentent plus du tiers de l'ensemble de ces derniers. Je voudrais les remercier pour leurs attentions et efforts à me faire des retours très détaillés.
Martin Geisler, Damien Cassou, Alexey Bakhirkin, Till Plewe, Dan Himes, Paul Sargent, Gokberk Hamurcu, Matthijs van der Vleuten, Michael Chermside, John Mulligan, Jordi Fita, Jon Parise.
Je souhaite aussi remercier l'aide des personnes qui ont découvert des erreurs et fourni des suggestions avisées à travers tout le livre.
Jeremy W. Sherman, Brian Mearns, Vincent Furia, Iwan Luijks, Billy Edwards, Andreas Sliwka, Paweł Sołyga, Eric Hanchrow, Steve Nicolai, Michał Masłowski, Kevin Fitch, Johan Holmberg, Hal Wine, Volker Simonis, Thomas P Jakobsen, Ted Stresen-Reuter, Stephen Rasku, Raphael Das Gupta, Ned Batchelder, Lou Keeble, Li Linxiao, Kao Cardoso Félix, Joseph Wecker, Jon Prescot, Jon Maken, John Yeary, Jason Harris, Geoffrey Zheng, Fredrik Jonson, Ed Davies, David Zumbrunnen, David Mercer, David Cabana, Ben Karel, Alan Franzoni, Yousry Abdallah, Whitney Young, Vinay Sajip, Tom Towle, Tim Ottinger, Thomas Schraitle, Tero Saarni, Ted Mielczarek, Svetoslav Agafonkin, Shaun Rowland, Rocco Rutte, Polo-Francois Poli, Philip Jenvey, Petr Tesałék, Peter R. Annema, Paul Bonser, Olivier Scherler, Olivier Fournier, Nick Parker, Nick Fabry, Nicholas Guarracino, Mike Driscoll, Mike Coleman, Mietek Bák, Michael Maloney, László Nagy, Kent Johnson, Julio Nobrega, Jord Fita, Jonathan March, Jonas Nockert, Jim Tittsler, Jeduan Cornejo Legorreta, Jan Larres, James Murphy, Henri Wiechers, Hagen Möbius, Gábor Farkas, Fabien Engels, Evert Rol, Evan Willms, Eduardo Felipe Castegnaro, Dennis Decker Jensen, Deniz Dogan, David Smith, Daed Lee, Christine Slotty, Charles Merriam, Guillaume Catto, Brian Dorsey, Bob Nystrom, Benoit Boissinot, Avi Rosenschein, Andrew Watts, Andrew Donkin, Alexey Rodriguez, Ahmed Chaudhary.
Les conventions typographiques suivantes sont utilisées dans ce livre :
Indique les termes nouveaux, les URLs, les adresses mail, les noms de fichiers et les extensions de fichier.
Chasse fixe
Utilisé pour les extraits de code, comme dans les paragraphes pour référer aux éléments du programme, tels que les variables ou les noms de fonctions, de bases de données, de types de données, de variables d'environnement, d'instructions, et de mots clés.
Taille constante avec gras
Affiche les commandes ou autres textes qui devraient être saisis par l'utilisateur.
Chasse fixe avec italique
Affiche les textes qui devraient être remplacés par une valeur définie par l'utilisateur ou des valeurs définies selon le contexte.
Ce livre est ici pour vous aider dans votre travail. De manière générale, vous pouvez donc utiliser le code de ce livre dans vos programmes et votre documentation. Vous n'avez pas à nous contacter pour nous demander la permission de le faire, à moins que vous ne reproduisiez une partie significative du code. Par exemple, écrire un programme qui utilise plusieurs extraits de code du livre ne demande aucune autorisation particulière. Vendre ou distribuer un CD-ROM provenant des livres O'Reilly demande à l'inverse une autorisation. Répondre à une question en citant ce livre ou ses exemples de code ne demande aucune autorisation préalable. Intégrer une grande quantité des codes d'exemples de ce livre dans votre propre ouvrage demande une autorisation de notre part.
Nous apprécions, sans l'exiger, que vous citiez l'ouvrage dans vos écrits l'utilisant, en indiquant le titre, l'auteur, l'éditeur et son ISBN. Par exemple: “Titre du livre par Son Auteur. Copyright 2008 O’Reilly Media, Inc., 978-0-596-xxxx-x.”
Si vous estimez que votre usage des exemples de code
dépasse le cadre défini ci dessus, n'hésitez pas à nous contacter :
<permissions@oreilly.com>
.
Safari offre une solution qui est meilleure que les e-books. C'est une bibliothèque virtuelle qui vous laisse aisément rechercher dans des milliers de livres, mais aussi copier-coller leurs exemples, télécharger des chapitres, et trouver des réponses rapides quand vous avez besoin d'une information précise et à jour. Essayez le gratuitement : http://my.safaribooksonline.com.
Merci d'adresser vos commentaires et vos questions sur ce livre à son éditeur:
O’Reilly Media, Inc. |
1005 Gravenstein Highway North |
Sebastopol, CA 95472 |
800-998-9938 (in the United States or Canada) |
707-829-0515 (international or local) |
707 829-0104 (fax) |
Nous avons une page web pour cet ouvrage, où nous publions des errata, des exemples, et encore d'autres informations additionnelles. Vous pouvez accéder à cette page par l'URL suivante:
http://www.oreilly.com/catalog/<catalog page> |
N'oubliez pas de mettre à jour l'attribut <url> aussi.
Pour commenter ou poser des questions techniques sur cet ouvrage, envoyez un email à :
<bookquestions@oreilly.com> |
Pour plus d'informations sur nos livres, nos conférences, nos centres d'informations, et le réseau O’Reilly, voyez notre site web :
http://www.oreilly.com |
Table of Contents
La gestion de révisions est un processus permettant de gérer différentes versions de la même information. Dans sa forme la plus simple, c'est ce que tout le monde fait manuellement : quand vous modifiez un fichier, vous le sauvegardez sous un nouveau nom contenant un numéro, à chaque fois plus grand que celui de la version précédente.
Ce genre de gestion de révisions manuelle, ne serait-ce que d'un seul fichier, est cependant facilement sujette aux erreurs, ainsi, depuis longtemps, des logiciels existent pour résoudre cette problématique. Les premiers outils de gestion de révisions étaient destinés à aider un seul utilisateur, à automatiser la gestion des versions d'un seul fichier. Durant les dernières décennies, cette cible s'est largement agrandie, ils gèrent désormais de multiples fichiers, et aident un grand nombre de personnes à travailler ensemble. Les outils les plus modernes n'ont aucune difficulté à gérer plusieurs milliers de personnes travaillant ensemble sur des projets regroupant plusieurs centaines de milliers de fichiers.
L'arrivée de la gestion de révisions distribuée est relativement récente, et, pour le moment, ce nouveau domaine a grandi grâce à la volonté des gens d'explorer ces territoires encore inconnus.
J'écris un livre sur la gestion de révisions distribuée parce que je pense qu'il s'agit d'un sujet important qui mérite un guide de terrain. J'ai choisi d'écrire un livre sur Mercurial car il est l'outil le plus facile pour découvrir ce nouveau domaine, tout en étant un outil efficace qui répond aux demandes d'environnements réels et difficiles, là où d'autres outils de gestion de révisions s'effondrent.
Il y a de nombreuses raisons pour que vous ou votre équipe souhaitiez utiliser un outil automatisant la gestion de révisions pour votre projet.
L'outil se chargera de suivre l'évolution de votre projet, sans que vous ayez à le faire. Pour chaque modification, vous aurez à votre disposition un journal indiquant qui a fait quoi, pourquoi il l'a fait, quand il l'a fait, et ce qu'il a modifié.
Quand vous travaillez avec d'autres personnes, les logiciels de gestion de révisions facilitent le travail collaboratif. Par exemple, quand plusieurs personnes font, plus ou moins simultanément, des modifications incompatibles, le logiciel vous aidera à identifier et à résoudre les conflits.
L'outil vous aidera à réparer vos erreurs. Si vous effectuez un changement qui se révèle être une erreur, vous pourrez revenir à une version antérieure d'un fichier ou même d'un ensemble de fichiers. En fait, un outil de gestion de révisions vraiment efficace vous permettra d'identifier à quel moment le problème est apparu (voir la section Section 9.5, “Finding the source of a bug” pour plus de détails).
L'outil vous permettra aussi de travailler sur plusieurs versions différentes de votre projet et de gérer l'écart entre chacune.
La plupart de ces raisons ont autant d'importances —du moins en théorie— que vous travailliez seul sur un projet, ou avec une centaine d'autres personnes.
Une question fondamentale à propos des outils de gestion de révisions, qu'il s'agisse du projet d'une personne ou d'une grande équipe, est quels sont ses gains par rapport à ses coûts. Un outil qui est difficile à utiliser ou à comprendre exigera un lourd effort d'adaptation.
Un projet de cinq mille personnes s'effondrera très certainement de lui même sans aucun processus et outil de gestion de révisions. Dans ce cas, le coût d'utilisation d'un logiciel de gestion de révisions est dérisoire puisque sans celui-ci, l'échec est presque garanti.
D'un autre coté, un “rapide hack” d'une personne peut sembler un contexte bien pauvre pour utiliser un outil de gestion de révisions, car, bien évidement le coût d'utilisation dépasse le coût total du projet. N'est-ce pas ?
Mercurial supporte ces deux échelles de travail. Vous pouvez apprendre les bases en quelques minutes seulement, et, grâce à sa performance, vous pouvez l'utiliser avec facilité sur le plus petit des projets. Cette simplicité signifie que vous n'avez pas de concept obscur ou de séquence de commandes défiant l'imagination, sans aucune corrélation avec ce que vous êtes réellement en train de faire. En même temps, ses mêmes performances et sa nature “peer-to-peer” vous permettent d'adapter, sans difficulté, son utilisation à de très grands projets.
Aucun outil de gestion de révisions ne peut sauver un projet mal mené, mais un bon outil peut rendre beaucoup plus fluide votre travail.
La gestion de source est un domaine tellement large qu'il n'existe pas qu'un seul nom ou acronyme pour le désigner. Voici quelques noms ou acronymes que vous rencontrerez le plus souvent.
:
Certaines personnes prétendent que ces termes ont en fait des sens différents mais en pratique ils se recouvrent tellement qu'il n'y a pas réellement de manière pertinente de les distinguer.
Ce livre prend une approche non usuelle pour les exemples de code. Tous les exemples sont en “live” — chacun est actuellement le résultat d'un script shell qui exécute les commandes Mercurial que vous voyez. Chaque fois qu'une image du livre est construite à partir des sources, tous les scripts d'exemples sont lancés automatiquement, et leurs résultats effectifs sont comparés aux résultats attendus.
L'avantage de cette approche est que les exemples sont toujours précis ; ils décrivent exactement la comportement de la version de Mercurial qui est mentionnée en entête du livre. Si je mets à jour la version de Mercurial que je suis en train de documenter, et que la sortie de certaines commandes change, la construction du livre échoue.
Il existe un petit désavantage à cette approche qui est que les dates et heures que vous verrez dans les exemples tendent à être “écrasés” ensemble, dans le sens où elles ne sont pas celles qu'elles auraient été si un humain avait tapé les commandes. En effet, un humain ne peut pas taper plus d'une commande toutes les quelques secondes, avec le temps qui s'écoule, mes scripts d'exemples exécutent plusieurs commandes en une seconde.
Comme exemple de ceci, plusieurs commits
consécutifs dans un exemple peuvent apparaître comme ayant eu lieu
durant la même seconde.
Vous pouvez observer le phénomène dans l'exemple bisect
dans Section 9.5, “Finding the source of a bug”
Donc, lorsque vous lisez ces exemples, ne prêtez pas trop d'importance aux dates et heures que vous voyez dans la sortie des commandes. Cependant, soyez confiants que le comportement que vous voyez est cohérent et reproductible.
Il y a eu une tendance évidente dans le développement et l'utilisation d'outils de gestion de source depuis les quatre dernières décennies, au fur et à mesure que les utilisateurs se sont habitués à leur outils et se sont sentis contraints par leurs limitations.
La première génération commença simplement par gérer un fichier unique sur un ordinateur individuel. Cependant, même si ces outils présentaient une grande avancée par rapport à la gestion manuelle des versions, leur modèle de verrouillage et leur utilisation limitée à un seul ordinateur rendaient leur utilisation possible uniquement dans une très petite équipe.
La seconde génération a assoupli ces contraintes en adoptant une architecture réseau et centralisée, permettant de gérer plusieurs projets entiers en même temps. Alors que les projets grandirent en taille, ils rencontrèrent de nouveaux problèmes. Avec les clients discutant régulièrement avec le serveur, la montée en charge devint un réel problème sur les gros projets. Une connexion réseau peu fiable pouvait complètement empêcher les utilisateurs distants de dialoguer avec le serveur. Alors que les projets Open Source commencèrent à mettre en place des accès en lecture seule disponible anonymement, les utilisateurs sans les privilèges de “commit” réalisèrent qu'ils ne pouvaient pas utiliser les outils pour collaborer naturellement avec le projet, comme ils ne pouvaient pas non plus enregistrer leurs modifications.
La génération actuelle des outils de gestion de révisions est “peer-to-peer” par nature. Tous ces systèmes ont abandonné la dépendance à un serveur central, et ont permis à leurs utilisateurs de distribuer les données de leur gestion de révisions à qui en a besoin. La collaboration à travers Internet a transformé la contrainte technologique en une simple question de choix et de consensus. Les outils modernes peuvent maintenant fonctionner en mode déconnecté sans limite et de manière autonome, la connexion au réseau n'étant nécessaire que pour synchroniser les modifications avec les autres dépôts.
Même si les gestionnaire de révisions distribués sont depuis plusieurs années assez robustes et aussi utilisables que leurs prédécesseurs, les utilisateurs d'autres outils n'y ont pas encore été sensibilisés. Les gestionnaires de révisions distribués se distinguent particulièrement de leurs équivalents centralisés de nombreuses manières.
Pour un développeur individuel, ils restent beaucoup plus rapides que les outils centralisés. Cela pour une raison simple : un outil centralisé doit toujours dialoguer à travers le réseau pour la plupart des opérations, car presque toutes les métadonnées sont stockées sur la seule copie du serveur central. Un outil distribué stocke toute ses métadonnées localement. À tâche égale, effectuer un échange avec le réseau ajoute un délai aux outils centralisés. Ne sous-estimez pas la valeur d'un outil rapide : vous allez passer beaucoup de temps à interagir avec un logiciel de gestion de révisions.
Les outils distribués sont complètement indépendants des aléas de votre serveur, d'autant plus qu'ils répliquent les métadonnées à beaucoup d'endroits. Si votre serveur central prend feu, vous avez intérêt à ce que les médias de sauvegardes soient fiables, et que votre dernier “backup” soit récent et fonctionne sans problème. Avec un outil distribué, vous avez autant de “backups” que de contributeurs.
En outre, la fiabilité de votre réseau affectera beaucoup moins les outils distribués. Vous ne pouvez même pas utiliser un outil centralisé sans connexion réseau, à l'exception de quelques commandes, très limitées. Avec un outil distribué, si votre connexion réseau tombe pendant que vous travaillez, vous pouvez ne même pas vous en rendre compte. La seule chose que vous ne serez pas capable de faire sera de communiquer avec des dépôts distants, opération somme toute assez rare en comparaison des opérations locales. Si vous avez une équipe de collaborateurs très dispersée ceci peut être significatif.
Si vous prenez goût à un projet Open Source et que vous décidez de commencer à toucher à son code, et que le projet utilise un gestionnaire de révisions distribué, vous êtes immédiatement un "pair" avec les personnes formant le “cœur” du projet. S'ils publient leurs dépôts, vous pouvez immédiatement copier leurs historiques de projet, faire des modifications, enregistrer votre travail en utilisant les mêmes outils qu'eux. Par comparaison avec un outil centralisé, vous devez utiliser un logiciel en mode “lecture seule” à moins que quelqu'un ne vous donne les privilèges de “commit” sur le serveur central. Avant ça, vous ne serez pas capable d'enregistrer vos modifications, et vos propres modifications risqueront de se corrompre chaque fois que vous essayerez de mettre à jour à votre espace de travail avec le serveur central.
Il a été souvent suggéré que les gestionnaires de révisions distribués posent un risque pour les projets Open Source car ils facilitent grandement la création de “fork”. Un “fork” apparait quand il y des divergences d'opinion ou d'attitude au sein d'un groupe de développeurs qui aboutissent à la décision de ne plus travailler ensemble. Chaque parti s'empare d'une copie plus ou moins complète du code source du projet et continue dans sa propre direction.
Parfois ces différents partis décident de se réconcilier. Avec un serveur central, l'aspect technique de cette réconciliation est un processus douloureux, et essentiellement manuel. Vous devez décider quelle modification est “la gagnante”, et replacer, par un moyen ou un autre, les modifications de l'autre équipe dans l'arborescence du projet. Ceci implique généralement la perte d'une partie de l'historique d'un des partis, ou même des deux.
Ce que les outils distribués permettent à ce sujet est probablement la meilleure façon de développer un projet. Chaque modification que vous effectuez est potentiellement un “fork”. La grande force de cette approche est que les gestionnaires de révisions distribués doivent être vraiment très efficaces pour fusionner (merge) des “forks”, car les “forks”, dans ce contexte, arrivent tout le temps.
Si chaque altération que n'importe qui effectue, à tout moment, est vue comme un “fork” à fusionner, alors ce que le monde de l'Open Source voit comme un “fork” devient uniquement une problématique sociale. En fait, les outils de gestions de révisions distribués réduisent les chances de “fork” :
Ils éliminent la distinction sociale qu'imposent les outils centralisés entre les membres du projets (ceux qui ont accès au “commit”) et ceux de l'extérieur (qui ne l'ont pas).
Ils rendent plus facile la réconciliation après un “fork” social, car tout ce qu'elle implique est une simple fusion.
Certaines personnes font de la résistance envers les gestionnaires de révisions distribués parce qu'ils veulent garder un contrôle ferme sur leur projet, et ils pensent que les outils centralisés leur fournissent ce contrôle. Néanmoins, si c'est votre cas, sachez que si vous publiez votre dépôt CVS ou Subversion de manière publique, il existe une quantité d'outils disponibles pour récupérer entièrement votre projet et son historique (quoique lentement) et le récréer ailleurs, sans votre contrôle. En fait, votre contrôle sur votre projet est illusoire, vous ne faites qu'interdire à vos collaborateurs de travailler de manière fluide, en disposant d'un miroir ou d'un “fork” de votre historique.
Beaucoup de projets commerciaux sont réalisés par des équipes éparpillées à travers le globe. Les contributeurs qui sont loin du serveur central devront subir des commandes lentes et même parfois peu fiables. Les solutions propriétaires de gestion de révisions tentent de palier ce problème avec des réplications de sites distants qui sont à la fois coûteuses à mettre en place et lourdes à administrer. Un système distribué ne souffre pas de ce genre de problèmes. En outre, il est très aisé de mettre en place plusieurs serveurs de références, disons un par site, de manière à ce qu'il n'y ait pas de communication redondante entre les dépôts, sur une connexion longue distance souvent onéreuse.
Les systèmes de gestion de révisions supportent généralement assez mal la montée en charge. Il n'est pas rare pour un gestionnaire de révisions centralisé pourtant onéreux de s'effondrer sous la charge combinée de quelques dizaines d'utilisateurs concurrents seulement. Une fois encore, la réponse à cette problématique est généralement encore la mise en place d'un ensemble complexe de serveurs synchronisés par un mécanisme de réplication. Dans le cas d'un gestionnaire de révisions distribué, la charge du serveur central — si vous en avez un— est largement inférieure (car toutes les données sont déjà répliquées ailleurs), un simple serveur, peu onéreux, peut gérer les besoins d'une plus grande équipe, et la réplication pour répartir la charge devient le travail d'un simple script.
Si vous avez des employés sur le terrain, en train de chercher à résoudre un souci sur le site d'un client, ils bénéficieront aussi d'un gestionnaire de révisions distribué. Cet outil leur permettra de générer des versions personnalisées, d'essayer différentes solutions, en les isolant aisément les unes des autres, et de rechercher efficacement à travers l'historique des sources, la cause des bugs ou des régressions, tout ceci sans avoir besoin de la moindre connexion au réseau de votre société.
Mercurial a plusieurs caractéristiques qui en font un choix particulièrement pertinent pour la gestion de révisions :
Si vous êtes déjà familier d'un outil de gestion de révisions, vous serez capable de l'utiliser en moins de 5 minutes. Sinon, ça ne sera pas beaucoup plus long. Les commandes utilisées par Mercurial, comme ses fonctionnalités, sont généralement uniformes et cohérentes, et vous pouvez ainsi garder en tête simplement quelques règles générales, plutôt que de nombreuses exceptions.
Sur un petit projet, vous pouvez commencer à travailler avec Mercurial en quelques instants. Ajouter des modifications ou des branches, transférer ces modifications (localement ou via le réseau), et les opérations d'historique ou de statut sont aussi très rapides. Mercurial ne vous encombre pas grâce à sa simplicité d'utilisation et sa rapidité d'exécution.
L'utilité de Mercurial ne se limite pas à de petits projets : il est aussi utilisé par des projets ayant des centaines ou même des milliers de contributeurs, avec plusieurs dizaines de milliers de fichiers, et des centaines de méga octets de code source.
Si les fonctionnalités au cœur de Mercurial ne sont pas suffisantes pour vous, il est très aisé d'en construire d'autres. Mercurial est adapté à l'utilisation de scripts, et son implémentation interne en Python, propre et claire, rend encore plus facile l'ajout de fonctionnalités sous forme d'extensions. Il en existe déjà un certain nombre de très populaires et très utiles, dont le périmètre va de la recherche de bugs à l'amélioration des performances.
Avant que vous n'alliez plus loin, comprenez bien que cette section reflète mes propres expériences, et elle est donc (j'ose le dire) peu objective. Néanmoins, j'ai utilisé les outils de gestion de source listés ci dessous, dans la plupart des cas, pendant plusieurs années.
Subversion est un des outils de gestion de révisions les plus populaire, développé pour remplacer CVS. Il a une architecture client/serveur centralisée.
Subversion et Mercurial ont des noms de commandes très similaires pour les mêmes opérations, ainsi si vous êtes familier avec l'un, c'est facile d'apprendre l'autre. Ces deux outils sont portables sur tous les systèmes d'exploitation populaires.
Avant la version 1.5, Subversion n'offrait aucune forme de support pour les fusions. Lors de l'écriture de ce livre, ses capacités de fusion étaient nouvelles, et réputées pour être complexes et buguées.
Mercurial dispose d'un avantage substantiel en terme de performance par rapport à Subversion sur la plupart des opérations que j'ai pu tester. J'ai mesuré une différence de performance allant de deux à six fois plus rapide avec le système de stockage de fichier local de Subversion 1.4.3 (ra_local), qui est la méthode d'accès la plus rapide disponible. Dans un déploiement plus réaliste, impliquant un stockage réseau, Subversion serait encore plus désavantagé. Parce que la plupart des commandes Subversion doivent communiquer avec le serveur et que Subversion n'a pas de mécanisme de réplication, la capacité du serveur et la bande passante sont devenues des goulots d'étranglement pour les projets de taille moyenne ou grande.
En outre, Subversion implique une surcharge
substantielle dans le stockage local de certaines données, pour
éviter des transactions avec le serveur, pour certaines opérations
communes, telles que la recherche des fichiers modifiés
(status
) et l'affichage des modifications par
rapport à la révision courante (diff
). En
conséquence, un répertoire de travail Subversion a souvent la même
taille, ou est plus grand, qu'un dépôt Mercurial et son espace de
travail, et ceci bien que le dépôt Mercurial contienne l'intégralité
de l'historique.
Subversion est largement supporté par les outils tiers. Mercurial est actuellement encore en retrait de ce point de vue. L'écart se réduit néanmoins, en effet, certains des outils graphiques sont maintenant supérieurs à leurs équivalents Subversion. Comme Mercurial, Subversion dispose d'un excellent manuel utilisateur.
Parce que Subversion ne stocke pas l'historique chez ses clients, il est parfaitement adapté à la gestion de projets qui doivent suivre un ensemble de larges fichiers binaires et opaques. Si vous suivez une cinquantaine de versions d'un fichier incompressible de 10MB, l'occupation disque coté client d'un projet sous Subversion restera à peu près constante. À l'inverse, l'occupation disque du même projet sous n'importe lequel des gestionnaires de révisions distribués grandira rapidement, proportionnellement aux nombres de versions, car les différences entre chaque révision seront très grandes.
En outre, c'est souvent difficile ou, généralement, impossible de fusionner des différences dans un fichier binaire. La capacité de Subversion de verrouiller des fichiers, pour permettre à l'utilisateur d'être le seul à le mettre à jour (“commit”) temporairement, est un avantage significatif dans un projet doté de beaucoup de fichiers binaires.
Mercurial peut importer l'historique depuis un dépôt Subversion. Il peut aussi exporter l'ensemble des révisions d'un projet vers un dépôt Subversion. Ceci rend très facile de “prendre la température” et d'utiliser Mercurial et Subversion en parallèle, avant de décider de migrer vers Mercurial. La conversion de l'historique est incrémentale, donc vous pouvez effectuer une conversion initiale, puis de petites additions par la suite pour ajouter les nouvelle modifications.
Git est un outil de gestion de révisions distribué qui a été développé pour gérer le code source de noyau de Linux. Comme Mercurial, sa conception initiale a été inspirée par Monotone.
Git dispose d'un ensemble conséquent de commandes, avec plus de 139 commandes individuelles pour la version 1.5.0. Il a aussi la réputation d'être difficile à apprendre. Comparé à Git, le point fort de Mercurial est clairement sa simplicité.
En terme de performance, Git est extrêmement rapide. Dans la plupart des cas, il est plus rapide que Mercurial, tout du moins sur Linux, alors que Mercurial peut être plus performant sur d'autres opérations. Néanmoins, sur Windows, les performances et le niveau de support général fourni par Git, au moment de l'écriture de cet ouvrage, est bien derrière celui de Mercurial.
Alors que le dépôt Mercurial ne demande aucune maintenance, un dépôt Git exige d'exécuter manuellement et régulièrement la commande “repacks” sur ses métadonnées. Sans ceci, les performances de git se dégradent et la consommation de l'espace disque augmente rapidement. Un serveur qui contient plusieurs dépôts Git qui ne sont pas régulièrement et fréquemment “repacked” deviendra un vrai problème lors des “backups” du disque, et il y eu des cas, où un “backup” journalier pouvait durer plus de 24 heures. Un dépôt fraichement “repacked” sera légèrement plus petit qu'un dépôt Mercurial, mais un dépôt non “repacked” est beaucoup plus grand.
Le cœur de Git est écrit en C. La plupart des commandes Git sont implémentées sous forme de scripts Shell ou Perl, et la qualité de ces scripts varie grandement. J'ai plusieurs fois constaté que certains de ces scripts étaient chargés en mémoire aveuglément et que la présence d'erreurs pouvait s'avérer fatale.
CVS est probablement l'outil de gestion de révisions le plus utilisé aujourd'hui dans le monde. À cause de son manque de clarté interne, il n'est plus maintenu depuis plusieurs années.
Il a une architecture client/serveur centralisée. Il ne regroupe pas les modifications de fichiers dans une opération de “commit” atomique, ce qui permet à ses utilisateurs de “casser le build” assez facilement : une personne peut effectuer une opération de “commit” sans problème puis être bloquée par besoin de fusion, avec comme conséquence néfaste, que les autres utilisateurs ne récupèreront qu'une partie de ses modifications. Ce problème affecte aussi la manière de travailler avec l'historique du projet. Si vous voulez voir toutes les modifications d'une personne du projet, vous devrez injecter manuellement les descriptions et les timestamps des modifications de chacun des fichiers impliqués (si vous savez au moins quels sont ces fichiers).
CVS a une notion étrange des tags et des branches que je n'essayerai même pas de décrire ici. Il ne supporte pas bien les opérations de renommage d'un fichier ou d'un répertoire, ce qui facilite la corruption de son dépôt. Il n'a presque pas pour ainsi dire de contrôle de cohérence interne, il est donc pratiquement impossible de dire si un dépôt est corrompu ni à quel point. Je ne recommanderai pas CVS pour un projet existant ou nouveau.
Mercurial peut importer l'historique d'un projet CVS. Néanmoins, il y a quelques principes à respecter ; ce qui est vrai aussi pour les autres outils d'import de projet CVS. À cause de l'absence de “commit” atomique et gestion de versions de l'arborescence, il n'est pas possible de reconstruire de manière précise l'ensemble de l'historique. Un travail de “devinette” est donc nécessaire, et les fichiers renommés ne sont pas détectés. Parce qu'une bonne part de l'administration d'un dépôt CVS est effectuée manuellement, et est donc, sujette à erreur, il est courant que les imports CVS rencontrent de nombreux problèmes avec les dépôt corrompus (des timestamps de révision complètement buggés et des fichiers verrouillés depuis des années sont deux des problèmes les moins intéressants dont je me souvienne).
Perforce a une architecture client/serveur centralisée, sans aucun mécanisme de mise en cache de données côté client. Contrairement à la plupart des outils modernes de gestion de révisions, Perforce exige de ses utilisateurs d'exécuter une commande pour informer le serveur central de tout fichier qu'ils souhaitent modifier.
Les performances de Perforce sont plutôt bonnes pour des petites équipes, mais elles s'effondrent rapidement lorsque le nombre d'utilisateurs augmente au delà de quelques dizaines. Des installations de Perforce assez larges nécessitent le déploiement de proxies pour supporter la montée en charge associée.
À l'exception de CVS, tous les outils listés ci-dessus ont des forces qui leurs sont propres et qui correspondent à certaines formes de projet. Il n'y a pas un seul meilleur outil de gestion de révisions qui correspondrait le mieux à toutes les situations.
En guise exemple, Subversion est un très bon choix lorsqu'on travaille avec beaucoup de fichiers binaires, qui évoluent régulièrement, grâce à sa nature centralisée et sa capacité à verrouiller des fichiers.
Personnellement, je préfère Mercurial pour sa simplicité, ses performances et sa bonne capacité de fusion, et il m'a très bien rendu service de plusieurs années maintenant.
Mercurial est livré avec une extension nommée convert
, qui peut, de manière incrémentale
importer des révisions depuis différents autres outils de gestion de
source. Par “incrémental”, j'entends que vous pouvez
convertir l'historique entier du projet en une seule fois, puis
relancer l'outil d'import plus tard pour obtenir les modifications
effectuées depuis votre import initial.
Les outils de gestion de révisions supportés par convert
sont :
En outre, convert
peut
exporter les modifications depuis Mercurial vers Subversion. Ceci rend
possible d'essayer Subversion en parallèle avant de choisir une
solution définitive, sans aucun risque de perte de données.
La commande convert est très simple à utiliser. Simplement, indiquez le chemin ou l'URL du dépôt de source, en lui indiquant éventuellement le nom du chemin de destination, et la conversion se met en route. Après cet import initial, il suffit de relancer la commande encore une fois pour importer les modifications effectuées depuis.
Le plus célèbre des anciens outils de gestion de révisions est SCCS (Source Code Control System)}, que Marc Rochkind conçu dans les laboratoires de recherche de Bell (Bell Labs), dans le début des années 70. SCCS ne fonctionnait que sur des fichiers individuels, et obligeait chaque personne travaillant sur le projet d'avoir un accès à un répertoire de travail commun, sur le même système. Seulement une seule personne pouvait modifier un fichier au même moment, ce fonctionnement était assuré par l'utilisation de verrou (“lock”). Il était courant que des personnes verrouillent des fichiers, et plus tard, oublient de le déverrouiller ; empêchant n'importe qui d'autre de travailler sur ces fichiers sans l'aide de l'administrateur...
Walter Tichy a développé une alternative libre à SCCS au début des années 80, qu'il nomma RCS (Revision Control System). Comme SCCS, RCS demandait aux développeurs de travailler sur le même répertoire partagé, et de verrouiller les fichiers pour se prémunir de tout conflit issu de modifications concurrentes.
Un peu plus tard dans les années 1980, Dick Grune utilisa RCS comme une brique de base pour un ensemble de scripts shell qu'il intitula cmt, avant de la renommer en CVS (Concurrent Versions System). La grande innovation de CVS était que les développeurs pouvaient travailler simultanément et indépendamment dans leur propre espace de travail. Ces espaces de travail privés assuraient que les développeurs ne se marchent pas mutuellement sur les pieds, comme c'était souvent le cas avec RCS et SCCS. Tous les développeurs disposaient donc de leur copie de tous les fichiers du projet, et ils pouvaient donc librement les modifier. Ils devaient néanmoins effectuer la “fusion” (“merge”) de leurs fichiers, avant d'effectuer le “commit” de leurs modifications sur le dépôt central.
Brian Berliner reprit les scripts de Grune's et les réécrit en C, qu'il publia en 1989. Depuis, ce code a été modifié jusqu'à devenir la version moderne de CVS. CVS a acquis ainsi la capacité de fonctionner en réseau, transformant son architecture en client/serveur. L'architecture de CVS est centralisée, seul le serveur a une copie de l'historique du projet. L'espace de travail client ne contient qu'une copie de la dernière version du projet, et quelques métadonnées pour indiquer où le serveur se trouve. CVS a été un grand succès, aujourd'hui il est probablement l'outil de gestion de révisions le plus utilisé au monde.
Au début des années 1990, Sun Microsystems développa un premier outil de gestion de révisions distribué, nommé TeamWare. Un espace de travail TeamWare contient une copie complète de l'historique du projet. TeamWare n'a pas de notion de dépôt central. (CVS utilisait RCS pour le stockage de l'historique, TeamWare utilisait SCCS).
Alors que les années 1990 avançaient, les utilisateurs ont pris conscience d'un certain nombre de problèmes avec CVS. Il enregistrait simultanément des modifications sur différents fichiers individuellement, au lieu de les regrouper dans une seule opération cohérente et atomique. Il ne gère pas bien sa hiérarchie de fichiers, il est donc assez aisé de créer le chaos en renommant les fichiers et les répertoires. Pire encore, son code source est difficile à lire et à maintenir, ce qui augmente largement le “niveau de souffrance” associé à la réparation de ces problèmes d'architecture de manière prohibitive.
En 2001, Jim Blandy et Karl Fogel, deux développeurs qui avaient travaillé sur CVS, initièrent un projet pour le remplacer par un outil qui aurait une meilleure architecture et un code plus propre. Le résultat, Subversion, ne quitte pas le modèle centralisé et client/serveur de CVS, mais ajoute les opérations de “commit” atomique sur de multiples fichiers, une meilleure gestion des espaces de noms, et d'autres fonctionnalités qui en font un meilleur outil que CVS. Depuis sa première publication, il est rapidement devenu très populaire.
Plus ou moins simultanément, Graydon Hoare a commencé sur l'ambitieux système de gestion distribuée Monotone. Bien que Monotone corrige plusieurs défauts de CVS tout en offrant une architecture “peer-to-peer”, il va aussi plus loin que la plupart des outils de gestion de révisions de manière assez innovante. Il utilise des “hashs” cryptographiques comme identifiants, et il a une notion complète de “confiance” du code issu des différentes sources.
Mercurial est né en 2005. Bien que très influencé par Monotone, Mercurial se concentre sur la facilité d'utilisation, les performances et la capacité à monter en charge sur de très gros projets.
Table of Contents
Des paquetages binaires de Mercurial sont disponibles pour la plupart des systèmes d'exploitation, ce qui rend facile l'utilisation immédiate de Mercurial sur votre ordinateur.
La meilleure version de Mercurial pour Windows est TortoiseHg, qui peut être téléchargée ici : http://bitbucket.org/tortoisehg/stable/wiki/Home. Ce logiciel n'a aucune dépendance exterieure ; il fonctionne “et c'est tout”. Il fournit aussi bien les outils en ligne de commmande qu'une interface graphique.
Lee Cantey publie un installeur de Mercurial pour Mac OS X sur http://mercurial.berkwood.com.
Parce que chaque distribution de Linux a ses propres outils de gestion de paquets, politiques et rythmes de développements, il est difficile de donner un ensemble d'instructions unique pour installer les binaires de Mercurial. La version de Mercurial avec laquelle vous vous retrouverez dépendra grandement de l'activité de la personne en charge du paquetage pour la distribution.
Pour rester simple, je me concentrerai sur
l'installation de Mercurial en ligne de commande, sous les
distributions les plus courantes. La plupart des distributions
fournissent des gestionnaires graphiques de paquetage qui vous
permettront d'installer Mercurial en quelques clicks. Le paquetage
devrait se nommer mercurial
.
SunFreeWare, à http://www.sunfreeware.com, fournit des paquets précompilés pour Mercurial.
Pour commencer, nous utiliserons la commande hg version pour vérifier si Mercurial est installé proprement. L'information de version affichée n'est pas réellement importante en soi, c'est surtout de savoir si elles s'affichent qui nous intéresse.
$
hg version
Mercurial Distributed SCM (version 1.6.4) Copyright (C) 2005-2010 Matt Mackall <mpm@selenic.com> and others This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Mercurial fournit un système d'aide intégré, ce qui est inestimable quand vous vous retrouvez coincé à essayer de vous rappeler comment lancer une commande. Si vous êtes bloqué, exécutez simplement hg help ; elle affichera une brève liste des commandes, avec une description pour chacune. Si vous demandez de l'aide sur une commande spécifique (voir ci-dessous), elle affichera des informations plus détaillées.
$
hg help init
hg init [-e CMD] [--remotecmd CMD] [DEST] create a new repository in the given directory Initialize a new repository in the given directory. If the given directory does not exist, it will be created. If no directory is given, the current directory is used. It is possible to specify an "ssh://" URL as the destination. See "hg help urls" for more information. Returns 0 on success. options: -e --ssh CMD specify ssh command to use --remotecmd CMD specify hg command to run on the remote side use "hg -v help init" to show global options
Pour un niveau d'informations encore plus détaillé
(ce dont vous aurez rarement besoin), exécutez hg
help -v
. L'option
-v
est l'abréviation de
--verbose
, et indique à Mercurial
d'afficher plus d'informations que d'habitude.
Avec Mercurial, tout se déroule au sein d'un dépôt. Le dépôt d'un projet contient tous les fichiers qui “appartiennent” au projet.
Il n'y a rien de particulièrement magique au sujet de ce dépôt, c'est simplement une arborescence sur votre système de fichiers que Mercurial traite de manière spéciale. Vous pouvez renommer ou effacer ce répertoire à n'importe quel moment, en utilisant la ligne de commande ou votre explorateur de fichiers.
Copier un dépôt est juste un peu spécial. Bien que vous puissiez utiliser une commande habituelle de copie pour copier votre dépôt, il vaut mieux utiliser une commande fournie par Mercurial. Cette commande est appelée hg clone, car elle crée une copie identique à un dépôt existant.
$
hg clone http://hg.serpentine.com/tutorial/hello
destination directory: hello requesting all changes adding changesets adding manifests adding file changes added 5 changesets with 5 changes to 2 files updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
Un avantage de la commande hg clone est que, comme nous l'avons vu ci dessus, elle nous permet de cloner les dépôts à travers le réseau. Un autre est qu'elle se rappelle d'où a été cloné un dépôt, ce qui est utile quand on veut mettre à jour le clone.
Si votre opération de clonage réussit, vous devriez maintenant
avoir un répertoire local appelé hello
.
Ce répertoire contiendra quelques fichiers.
$
ls -l
total 0 drwxr-xr-x 3 oracle dba 120 Mar 17 05:09 hello$
ls hello
Makefile hello.c
Ces fichiers ont le même contenu et historique dans votre dépôt qu'ils ont dans le dépôt que vous avez cloné.
Chaque dépôt Mercurial est complet, autonome et indépendant. Il contient sa propre copie privée des fichiers du projet et de leur historique. Le clone d'un dépôt se souvient de la localisation du dépôt à partir duquel il a été cloné, mais il ne communique pas avec ce dernier, ou un autre, à moins que vous ne lui demandiez.
Tout ceci signifie pour le moment que nous sommes libres d'expérimenter avec ce dépôt, confiants dans le fait qu'il s'agit d'un “bac à sable” qui n'affectera personne d'autre.
Prêtons plus attention un instant au contenu d'un dépôt.
Nous voyons qu'il contient un répertoire nommé .hg
. C'est ici que Mercurial conserve toutes ses
métadonnées.
$
cd hello
$
ls -a
. .. .hg Makefile hello.c
Le contenu du répertoire .hg
et ses sous-répertoires sont les seuls propres à Mercurial.
Tous les autres fichiers et répertoires dans le dépôt sont à vous, et
vous pouvez en faire ce que vous voulez.
Pour introduire un peu de terminologie, le répertoire
.hg
est un “vrai”
dépôt, et tous les fichiers et les répertoires qui coexistent avec lui,
sont désignés sous le nom espace de travail. Une
manière facile de se rappeler cette distinction est de retenir que le
dépôt contient l'historique
de votre projet, alors que l'espace de travail
contient un "snapshot" de votre projet à un certain
point de son historique.
Une des premières choses que vous aurez envie de faire avec un nouveau dépôt, sera de comprendre son historique. La commande hg log vous donne une vue de l'historique.
$
hg log
changeset: 4:2278160e78d4 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:16:53 2008 +0200 summary: Trim comments. changeset: 3:0272e0d5a517 user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:08:02 2008 +0200 summary: Get make to generate the final binary from a .o file. changeset: 2:fef857204a0c user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:05:04 2008 +0200 summary: Introduce a typo into hello.c. changeset: 1:82e55d328c8c user: mpm@selenic.com date: Fri Aug 26 01:21:28 2005 -0700 summary: Create a makefile changeset: 0:0a04b987be5a user: mpm@selenic.com date: Fri Aug 26 01:20:50 2005 -0700 summary: Create a standard "hello, world" program
Par défaut, cette commande affiche à l'écran un bref paragraphe pour chaque révision enregistrée pour ce projet. Dans la terminologie de Mercurial, nous appelons chacun de ces évènements enregistrés un changeset, parce qu'il contient un ensemble de modifications sur plusieurs fichiers.
La commande hg log affiche ainsi ces informations :
changeset
: Ce champ contient
un nombre, séparé par deux points (:), d'une chaine hexadécimale. Il
s'agit en fait d'identifiants d'un “changeset”. Il y a
deux identifiants car le numéro de la révision est plus court et plus
facile à saisir qu'une séquence hexadécimale.
user
: L'identité de la personne
qui a créé ce
“changeset”. C'est un champ libre de forme, mais la plupart du
temps il contient le nom et l'email de la personne.
date
: La date et l'heure à
laquelle le “changeset” a été créé, ainsi que le fuseau horaire dans
lequelle il a été créé. (La date et l'heure sont locales à ce
“fuseau”, elles indiquent donc quelle date et heure il était
pour la personne qui a créé ce “changeset”.)
summary
: La première ligne du
message que le créateur a associé à son “changeset” pour le décrire.
Certains “changesets”, comme le premier de la
liste ci-dessus ont un champ tag
. Le tag est une autre
façon d'identifier un changeset en lui donnant un nom simple à retenir.
(Le tag nommé tip
est spécial : il fait toujours
référence au dernier changement dans le dépôt.)
Par défaut, la commande hg log n'affiche qu'un résumé, il manque beaucoup de détails.
La figure Figure 2.1, “Historique graphique du dépôt hello
” fournit une
représentation graphique de l'historique du dépôt hello
, pour voir plus facilement dans quelle direction
l'historique se “déroule”. Nous reviendrons régulièrement
sur cette représentation dans ce chapitre et ceux qui suivent.
Comme l'anglais est réputé pour être un langage maladroit, et que l'informatique est la source de bien des erreurs de terminologie (pourquoi utiliser un seul terme quand quatre feront l'affaire ?), la gestion de révisions a une variété de mots et de phrases qui veulent dire la même chose. Si vous discutez d'historique de Mercurial avec d'autres personnes, vous constaterez que souvent, le mot “changeset” est contracté simplement en “change” ou (à l'écrit) “cset”, et même parfois “révision”, abrégé en “rev”.
Bien que le mot que vous utilisez pour désigner le concept de changeset importe peu, l'identifiant que vous utilisez pour désigner un changeset spécifique a une grande importance. Rappelez vous que le champ “changeset” affiché par la commande hg log identifie un “changeset” à la fois avec un numéro de révision et une séquence hexadécimale.
La distinction est importante. Si vous envoyez un email
à quelqu'un en parlant de la “révision 33”, il est très
probable que sa “révision 33” ne sera pas la même
que la votre. La raison de ceci est que le numéro de révision dépend
de l'ordre dans lequel les modifications sont arrivées dans le dépôt,
et il n'y a aucune garantie que les mêmes changements soient arrivés
dans le même ordre dans différents dépôts. Trois modifications
a, b, c
peuvent aisément apparaitre dans un dépôt
comme 0, 1, 2
, et dans un autre comme 0, 2, 1
.
Mercurial utilise les numéros de révision uniquement comme des raccourcis pratiques. Si vous devez discuter d'un “changeset” avec quelqu'un, ou identifer un “changeset” pour une quelconque raison (par exemple, un rapport de “bug”), utilisez la séquence hexadécimale.
Pour réduire la sortie de hg log
à une seule révision, utilisez l'option -r
(ou --rev
). Vous pouvez utiliser
le numéro de révision ou la séquence hexadécimale comme identifiant, et
demander autant de révisions que vous le souhaitez.
$
hg log -r 3
changeset: 3:0272e0d5a517 user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:08:02 2008 +0200 summary: Get make to generate the final binary from a .o file.$
hg log -r 0272e0d5a517
changeset: 3:0272e0d5a517 user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:08:02 2008 +0200 summary: Get make to generate the final binary from a .o file.$
hg log -r 1 -r 4
changeset: 1:82e55d328c8c user: mpm@selenic.com date: Fri Aug 26 01:21:28 2005 -0700 summary: Create a makefile changeset: 4:2278160e78d4 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:16:53 2008 +0200 summary: Trim comments.
Si vous voulez voir l'historique de plusieurs révisions
sans avoir à les énumérer, vous pouvez utiliser un intervalle
de numérotation qui vous permet d'exprimer l'idée “je
veux toutes les révisions entre abc
et def
inclus”.
$
hg log -r 2:4
changeset: 2:fef857204a0c user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:05:04 2008 +0200 summary: Introduce a typo into hello.c. changeset: 3:0272e0d5a517 user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:08:02 2008 +0200 summary: Get make to generate the final binary from a .o file. changeset: 4:2278160e78d4 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:16:53 2008 +0200 summary: Trim comments.
Mercurial respecte aussi l'ordre dans lequel vous spécifiez
les révisions, ainsi hg log -r 2:4
affichera 2, 3, 4
alors que hg
log -r 4:2 affichera 4, 3, 2
.
Le résumé affiché par hg log
est suffisant si vous savez déjà ce que vous cherchez. En
revanche, vous aurez probablement besoin de voir une description
complète du changement, ou une liste des fichiers modifiés si vous
cherchez à déterminer qu'un “changeset” est bien celui que vous
recherchez. L'option -v
de la commande hg
log (ou --verbose
) vous
donne ces informations supplémentaires.
$
hg log -v -r 3
changeset: 3:0272e0d5a517 user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:08:02 2008 +0200 files: Makefile description: Get make to generate the final binary from a .o file.
Si vous voulez voir à la fois la description
et le contenu d'une modification, ajoutez l'option -p
(ou
--patch
). Ceci affiche le contenu d'une modification
comme un diff unifié
(si vous n'avez jamais vu de diff unifié avant, consultez la
section Section 12.4, “Understanding patches” pour un rapide
survol).
$
hg log -v -p -r 2
changeset: 2:fef857204a0c user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:05:04 2008 +0200 files: hello.c description: Introduce a typo into hello.c. diff -r 82e55d328c8c -r fef857204a0c hello.c --- a/hello.c Fri Aug 26 01:21:28 2005 -0700 +++ b/hello.c Sat Aug 16 22:05:04 2008 +0200 @@ -11,6 +11,6 @@ int main(int argc, char **argv) { - printf("hello, world!\n"); + printf("hello, world!\"); return 0; }
L'option -p
est
incroyablement utile, il est donc important dans s'en rappeler.
Avant d'aller plus loin sur le fonctionnement des commandes de Mercurial, étudions un moment comment elles fonctionnent de manière générale. Vous trouverez ça probablement utile pour la suite de notre parcours.
Mercurial utilise une approche directe et cohérente pour interpréter les options que vous passez aux commandes. Il suit une convention commune à la plupart des systèmes Unix et Linux modernes.
Chaque option a un nom complet. Par exemple,
comme nous l'avons déjà vu, la commande hg
log accepte l'option --rev
.
La plupart des options disposent de
noms abrégés. Aussi, au lieu d'utiliser --rev
, vous pouvez utiliser -r
.
(Les options qui n'ont pas de nom abrégé sont généralement
rarement utilisées).
Les noms complets commencent par deux
tirets (par exemple --rev
),
alors que les options courtes commencent avec un seul (par exemple
-r
).
Les noms des options sont cohérents
entre les commandes. Par exemple, chaque commande qui accepte
un “changeset ID” ou un numéro de révision accepte aussi -r
et --rev
comme arguments.
Dans les exemples de ce livre, j'utilise les noms abrégés plutôt que les noms complets. Ceci est une préférence personnelle, pas une recommandation.
La plupart des commandes qui affichent une quelconque sortie
à l'écran, afficheront davantage avec l'option
-v
(ou --verbose
), et
moins avec l'option -q
(ou
--quiet
).
Maintenant que nous avons une bonne idée des commandes pour consulter l'historique de Mercurial, regardons comment faire des modifications et les examiner.
La première chose que nous allons faire est d'isoler notre exercice dans un dépôt à part. Nous allons utiliser la commande hg clone, mais nous n'avons pas besoin de faire une copie de dépôt distant. Comme nous avons déjà une copie locale, nous pouvons juste faire un clone de celle-ci à la place. C'est beaucoup plus rapide que de faire une copie à travers le réseau, et un dépôt cloné localement prend également moins d'espace disque[1].
$
cd ..
$
hg clone hello my-hello
updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd my-hello
On notera au passage qu'il est souvent considéré comme une bonne pratique de conserver une copie “immaculée” du dépôt distant, à partir de laquelle vous pourrez faire des copies locales temporaires pour créer des “bacs à sable” pour chaque tâche sur laquelle vous souhaitez travailler. Ceci vous permet de travailler sur plusieurs choses en parallèle, chacunes isolées les unes des autres en attendant que ces tâches soient finies et que vous soyez prêt à les réintégrer. Parce que les copies locales sont peu coûteuses, il est très rapide de créer ou détruire des dépôts dès que vous n'en avez plus besoin.
Dans notre dépôt my-hello
, nous avons un fichier
hello.c
qui contient le classique “hello,
world”.
$
cat hello.c
/* * Placed in the public domain by Bryan O'Sullivan. This program is * not covered by patents in the United States or other countries. */ #include <stdio.h> int main(int argc, char **argv) { printf("hello, world!\"); return 0; }
Éditons ce fichier pour qu'il affiche une autre ligne sur la sortie standard.
# ... edit edit edit ...$
cat hello.c
/* * Placed in the public domain by Bryan O'Sullivan. This program is * not covered by patents in the United States or other countries. */ #include <stdio.h> int main(int argc, char **argv) { printf("hello, world!\"); printf("hello again!\n"); return 0; }
La commande Mercurial hg status nous dira ce que Mercurial sait des fichiers du dépôts.
$
ls
Makefile hello.c$
hg status
M hello.c
La commande hg status
n'affichera pas le contenu des fichiers, mais une ligne commençant par
“M
” pour hello.c
.
À moins que vous lui demandiez, la commande hg
status n'affichera aucune information sur les fichiers que
vous n'avez pas modifiés.
Le “M
” indique que
Mercurial a remarqué que nous avons modifié le fichier
hello.c
. Nous n'avons pas besoin
d'informer Mercurial que nous allons modifier un
fichier avant de commencer à le faire, ou que nous avons modifié un
fichier après avoir commencé à le faire, il est capable de le découvrir
tout seul.
C'est déjà pratique de savoir que nous avons modifié le
fichier hello.c
, mais nous préférerions savoir
exactement ce que nous avons changé. Pour ceci, nous
utilisons la commande hg diff.
$
hg diff
diff -r 2278160e78d4 hello.c --- a/hello.c Sat Aug 16 22:16:53 2008 +0200 +++ b/hello.c Thu Mar 17 05:09:25 2011 +0000 @@ -8,5 +8,6 @@ int main(int argc, char **argv) { printf("hello, world!\"); + printf("hello again!\n"); return 0; }
![]() |
Comprendre les patches |
---|---|
Penser à jeter un oeil à Section 12.4, “Understanding patches” si vous n'arrivez pas à lire la sortie ci-dessus. |
Nous pouvons modifier des fichiers, compiler et tester nos modifications, et utiliser les commandes hg status et hg diff pour voir les modifications effectuées, jusqu'à ce que nous soyons assez satisfaits pour décider d'enregistrer notre travail dans un “changeset”.
La commande hg commit vous laisse créer une nouvelle révision, nous désignerons généralement cette opération par “faire un commit” ou “commiter”.
Quand vous exécutez la commande hg commit pour la première fois, il n'est pas garanti qu'elle réussisse du premier coup. En effet, Mercurial enregistre votre nom et votre adresse avec chaque modification que vous effectuez, de manière à ce que vous soyez capable (ou d'autres le soient) de savoir qui a fait telle modification. Mercurial essaye automatiquement de découvrir un nom d'utilisateur qui ait un minimum de sens pour effectuer l'opération de “commit” avec. Il va essayer chacune des méthodes suivantes, dans l'ordre :
Si vous spécifiez l'option -u
avec la commande hg commit, suivi d'un nom
d'utilisateur, ceci aura toujours la priorité sur les autres
méthodes ci dessous.
Si vous avez défini une variable
d'environnement HGUSER
, c'est cette valeur qui est
alors utilisée.
Si vous créez un fichier nommé .hgrc
dans votre répertoire
\textit{home}, avec une entrée username
, c'est la valeur associée
qui sera utilisée. Pour voir à quoi ressemble le contenu de ce
fichier regardez la section Section 2.7.1.1, “Créer un fichier de configuration pour Mercurial”
ci-dessous.
Si vous avez défini une variable
d'environnement EMAIL
celle ci sera utilisée
ensuite.
Enfin, Mercurial interrogera votre système pour trouver votre nom d'utilisateur local ainsi que le nom de la machine hôte, et il fabriquera un nom d'utilisateur à partir de ces données. Comme il arrive souvent que ce genre de nom soit totalement inutile, il vous préviendra en affichant un message d'avertissement.
Si tous ces mécanismes échouent, Mercurial n'exécutera pas la commande, affichant un message d'erreur. Dans ce cas, il ne vous laissera pas effectuer de “commit” tant que vous n'aurez pas défini un nom d'utilisateur.
Vous devriez penser à utiliser la variable
d'environement HGUSER
et l'option -u
comme moyen pour
changer le nom d'utilisateur par défaut. Pour
une utilisation normale, la manière la plus simple et robuste
d'opérer est de créer un fichier .hgrc
, voir ci-dessous pour les détails
à ce sujet.
Pour définir un nom d'utilisateur, utilisez votre
éditeur de texte favori pour créer un fichier .hgrc
dans votre répertoire home.
Mercurial va utiliser ce fichier pour retrouver votre
configuration personnelle. Le contenu initial devrait
ressembler à ceci :
# This is a Mercurial configuration file. [ui] username = Firstname Lastname <email.address@domain.net>
La ligne avec [ui]
commence une
section du fichier de configuration, ainsi la ligne
“username = ...
” signifie “
définir la valeur de l'élément username
dans la
section ui
”. Une section continue jusqu'à ce
qu'une nouvelle commence, ou que la fin du fichier soit atteinte.
Mercurial ignore les lignes vides et traite tout texte situé à la suite
d'un “#
” jusqu'à la fin de la ligne
comme un commentaire.
Lorsqu'on effectue une opération de “commit”, Mercurial lance automatiquement un éditeur de texte pour permettre de saisir un message qui décrira les modifications effectuées dans cette révision. Ce message est nommé le message de commit. Ce sera un enregistrement pour tout lecteur expliquant le pourquoi et le comment de vos modifications, et il sera affiché par la commande hg log.
$
hg commit
L'éditeur que la commande hg
commit déclenche ne contiendra qu'une ligne vide suivi
d'un certain nombre de lignes commençant par “HG:
”.
This is where I type my commit comment. HG: Enter commit message. Lines beginning with 'HG:' are removed. HG: -- HG: user: Bryan O'Sullivan <bos@serpentine.com> HG: branch 'default' HG: changed hello.c
Mercurial ignore les lignes qui commencent
avec “HG:
”, il ne les
utilise que pour nous indiquer quels fichiers modifiés il se
prépare à “commiter”. Modifier ou effacer ces lignes n'a
aucune conséquence sur l'opération de “commit”.
Comme hg log n'affiche que la première ligne du message de “commit” par défaut, il est souvent considéré comme une bonne pratique de rédiger des messages de “commit” qui tiennent sur une seule ligne. Voilà un exemple concret de message de “commit” qui ne suit pas cette directive, et qui a donc un résumé peu lisible.
changeset: 73:584af0e231be user: Censored Person <censored.person@example.org> date: Tue Sep 26 21:37:07 2006 -0700 summary: include buildmeister/commondefs. Add an exports and install
À ce sujet, il faut noter qu'il n'existe pas de règle absolue dans ce domaine. Mercurial lui-même n'interprète pas les contenus des messages de “commit”, ainsi votre projet est libre de concevoir différentes politiques de mise en page des messages.
Ma préférence personnelle va au message court, mais informatif, qui offre des précisions supplémentaires par rapport à ce que pourrait m'apprendre une commande hg log --patch.
Si vous exécutez la commande hg commit sans aucun argument, elle enregistre tous les changements qui ont été fait, et qui sont indiqué par les commandes hg status et hg diff.
Si, en rédigeant le message, vous décidez que finalement vous ne voulez pas effectuer ce “commit”, il suffit de quitter simplement l'éditeur sans sauvegarder. Ceci n'aura aucune conséquence sur le dépôt ou les fichiers du répertoire de travail.
Une fois que votre “commit” est terminé, vous pouvez utiliser la commande hg tip pour afficher le “changeset” que vous venez de créer. Cette commande produit une sortie à l'écran qui est identique à celle du hg log, mais qui n'affiche que la dernière révision du dépôt.
$
hg tip -vp
changeset: 5:ec142fc290c8 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:25 2011 +0000 files: hello.c description: Added an extra line of output diff -r 2278160e78d4 -r ec142fc290c8 hello.c --- a/hello.c Sat Aug 16 22:16:53 2008 +0200 +++ b/hello.c Thu Mar 17 05:09:25 2011 +0000 @@ -8,5 +8,6 @@ int main(int argc, char **argv) { printf("hello, world!\"); + printf("hello again!\n"); return 0; }
On fait couramment référence à la dernière révision du dépôt comme étant la révision tip, ou plus simplement le tip.
Au passage, la commande hg
tip accepte la plupart des options qu'accepte
hg log. Ainsi -v
ci-dessus implique “soit
verbeux”, -p
veut dire “affiche le patch”. L'utilisation de l'option
-p
pour afficher un patch est un
autre exemple de la cohérence des commandes évoquée plus tôt.
Nous avons mentionné plus haut que les dépôts
de Mercurial sont autosuffisants. Ce qui signifie que la nouvelle
révision que vous venez de créer existe seulement dans votre
répertoire my-hello
. Étudions
comment propager cette modification dans d'autres dépôts.
Pour commencer, construisons un clone de notre dépôt
hello
qui ne contiendra pas
le changement que nous venons d'effectuer. Nous l'appellerons notre
dépôt temporaire hello-pull
.
$
cd ..
$
hg clone hello hello-pull
updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
Nous allons utiliser la commande hg pull pour envoyer les modifications
depuis my-hello
dans hello-pull
. Néanmoins, récupérer
aveuglement des modifications depuis un dépôt a quelque chose d'un
peu effrayant. Mercurial propose donc une commande hg incoming qui permet de savoir quelles
modifications la commande hg pull
pourrait entraîner dans notre dépôt, et ceci
sans effectuer réellement de modification dessus.
$
cd hello-pull
$
hg incoming ../my-hello
comparing with ../my-hello searching for changes changeset: 5:ec142fc290c8 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:25 2011 +0000 summary: Added an extra line of output
Apporter les modifications rapatriées dans un dépôt se résume donc à exécuter la commande hg pull, et préciser depuis quel dépôt effectuer le hg pull.
$
hg tip
changeset: 4:2278160e78d4 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:16:53 2008 +0200 summary: Trim comments.$
hg pull ../my-hello
pulling from ../my-hello 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)$
hg tip
changeset: 5:ec142fc290c8 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:25 2011 +0000 summary: Added an extra line of output
Comme vous le voyez avec une sortie avant et après de la commande hg tip, nous avons réussi à récupérer aisément les modifications dans notre dépôt. Il reste néanmoins quelque chose à faire avant de retrouver ces modifications dans l'espace de travail.
Nous avons jusqu'à maintenant grossièrement défini la relation entre un dépôt et un espace de travail. La commande hg pull que nous avons exécutée dans la section Section 2.8.1, “Récupérer les modifications d'autres dépôts” a apporté des modifications, que nous avons vérifiées, dans notre dépôt, mais il n'y a aucune trace de ces modifications dans notre espace de travail. En effet, hg pull ne touche pas (par défaut) à l'espace de travail. C'est la commande hg update qui s'en charge.
$
grep printf hello.c
printf("hello, world!\");$
hg update tip
1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
grep printf hello.c
printf("hello, world!\"); printf("hello again!\n");
Il peut sembler un peu étrange que la commande hg pull ne mette pas à jour l'espace de travail automatiquement. Il y a en fait une très bonne raison à cela : vous pouvez utiliser la commande hg update pour mettre à jour votre espace de travail à l'état dans lequel il était à n'importe quelle révision de l'historique du dépôt. Si vous aviez un espace de travail contenant une ancienne révision—pour chercher l'origine d'un bug, par exemple—et que vous effectuiez un hg pull qui mettrait à jour automatiquement votre espace de travail, vous ne seriez probablement pas très satisfait.
Néanmoins, comme les opérations de pull sont très souvent
suivies d'un update, Mercurial vous permet de combiner les
deux aisément en passant l'option -u
à la commande hg pull.
Si vous étudiez de nouveau la sortie de la commande hg pull dans la section Section 2.8.1, “Récupérer les modifications d'autres dépôts” quand nous l'avons exécutée sans l'option
-u
, vous pouvez constater qu'elle a
affiché un rappel assez utile : vous devez encore effectuer une
opération pour mettre à jour votre espace de travail.
Pour découvrir sur quelle révision de l'espace de travail on se trouve, utilisez la commande hg parents.
$
hg parents
changeset: 5:ec142fc290c8 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:25 2011 +0000 summary: Added an extra line of output
Si vous regardez de nouveau le dessin Figure 2.1, “Historique graphique du dépôt hello
”, vous verrez les flèches reliant
entre elles les révisions. Le nœud d'où la flèche
part est dans chaque cas un parent,
et le nœud où la flèche arrive est un
enfant.
Pour mettre à jour l'espace de travail d'une révision particulière, indiquez un numéro de révision ou un “changeset ID” à la commande hg update.
$
hg update 2
2 files updated, 0 files merged, 0 files removed, 0 files unresolved$
hg parents
changeset: 2:fef857204a0c user: Bryan O'Sullivan <bos@serpentine.com> date: Sat Aug 16 22:05:04 2008 +0200 summary: Introduce a typo into hello.c.$
hg update
2 files updated, 0 files merged, 0 files removed, 0 files unresolved$
hg parents
changeset: 5:ec142fc290c8 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:25 2011 +0000 summary: Added an extra line of output
Si vous ne précisez pas de manière explicite de numéro de révision la commande hg update mettra à jour votre espace de travail avec le contenu de la révison “tip”, comme montré dans l'exemple ci-dessus lors du second appel à hg update.
Mercurial vous laisse transférer les modifications vers un autre dépôt, depuis votre dépôt actuel. Comme dans l'exemple du hg pull ci-dessus, nous allons créer un dépôt temporaire vers lequel transférer nos modifications.
$
cd ..
$
hg clone hello hello-push
updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
La commande hg outgoing nous indique quels changements nous allons transférer vers l'autre serveur.
$
cd my-hello
$
hg outgoing ../hello-push
comparing with ../hello-push searching for changes changeset: 5:ec142fc290c8 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:25 2011 +0000 summary: Added an extra line of output
Et la commande hg push effectue réellement le transfert.
$
hg push ../hello-push
pushing to ../hello-push searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files
Comme avec hg pull, la
commande hg push ne met pas à jour
le répertoire de travail du dépôt dans lequel il transfère les
modifications. À l'inverse de hg
pull, hg push ne fournit
pas d'option -u
pour forcer la mise à jour de
l'espace de travail cible. Cette asymétrie est délibéré : le dépot
vers lequel nous transférons peut très bien être un serveur distant
et partagé par plusieurs personnes. Si nous devions mettre à jour son
répertoire de travail alors que quelqu'un d'autre travaille dessus,
nous risquerions de perturber son travail.
Que se passe-t-il lorsque vous essayez de récupérer ou de transférer vos modifications et que le dépôt cible a déjà reçu ces modifications ? Rien de bien excitant.
$
hg push ../hello-push
pushing to ../hello-push searching for changes no changes found
Quand nous faisons un clone d'un dépôt, Mercurial
enregistre l'emplacement du dépôt d'origine dans le fichier
.hg/hgrc
de notre nouveau dépôt. Si nous ne
fournissons pas d'emplacement à la commande hg
pull ou à la commande hg push, ces
commandes utiliseront alors cet emplacement comme valeur par défaut.
Les commandes hg incoming et hg
outgoing feront de même.
Si vous regardez le fichier
.hg/hgrc
, vous constaterez que son contenu
ressemble à ce qui suit.
[paths] default = http://www.selenic.com/repo/hg
Il est possible—et souvent
pratique—d'avoir un emplacement par défaut pour les commandes
hg push et hg outgoing
différent de celui des commandes hg pull et
hg incoming. C'est faisable en ajoutant une entrée
default-push
à la section
[paths]
du .hg/hgrc
, comme
suit.
[paths] default = http://www.selenic.com/repo/hg default-push = http://hg.example.com/hg
Les commandes que nous avons étudiées dans les sections précédentes ne sont pas limitées aux dépôts locaux. Chacune fonctionne de la même manière à travers une connexion réseau, il suffit de lui passer une URL à la place d'un chemin de fichier local.
$
hg outgoing http://hg.serpentine.com/tutorial/hello
comparing with http://hg.serpentine.com/tutorial/hello searching for changes changeset: 5:ec142fc290c8 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:25 2011 +0000 summary: Added an extra line of output
Dans cet exemple, nous allons voir quels changements nous pourrions transférer vers le dépôt distant, mais le dépôt n'est, de manière tout à fait compréhensible, pas configuré pour accepter des modifications d'utilisateurs anonymes.
$
hg push http://hg.serpentine.com/tutorial/hello
pushing to http://hg.serpentine.com/tutorial/hello searching for changes remote: ssl required
Il est tout aussi aisé de commencer un nouveau projet que de travailler sur un qui existe déjà. La commande hg init crée un nouveau dépôt Mercurial vide.
$
hg init myproject
Ceci crée simplement un répertoire nommé
myproject
dans le répertoire courant.
$
ls -l
total 8 -rw-r--r-- 1 oracle dba 47 Mar 17 05:08 goodbye.c -rw-r--r-- 1 oracle dba 45 Mar 17 05:08 hello.c drwxr-xr-x 3 oracle dba 72 Mar 17 05:08 myproject
Nous pouvons dire que myproject
est
un dépôt Mercurial car il contient un répertoire
.hg
.
$
ls -al myproject
total 0 drwxr-xr-x 3 oracle dba 72 Mar 17 05:08 . drwx------ 3 oracle dba 184 Mar 17 05:08 .. drwxr-xr-x 3 oracle dba 128 Mar 17 05:08 .hg
Si vous voulons ajouter quelques fichiers préexistants dans ce dépôt, il suffit de les recopier dans le répertoire de travail, et demander à Mercurial de commencer à les suivre en utilisant la commande hg add.
$
cd myproject
$
cp ../hello.c .
$
cp ../goodbye.c .
$
hg add
adding goodbye.c adding hello.c$
hg status
A goodbye.c A hello.c
Une fois que nous sommes satisfaits de notre projet, nous pouvons commencer à ajouter nos révisions.
$
hg commit -m 'Initial commit'
Il ne prend que quelques instants pour commencer à utiliser Mercurial sur un nouveau projet, ce qui fait aussi de ses points forts. Travailler avec une gestion de révision devient très facile, nous pouvons même l'utiliser pour les plus petits projets où nous aurions probablement jamais pensé utiliser un outil aussi complexe.
[1] L'économie d'espace disque apparait clairement quand les dépôts source et destination sont sur le même système de fichier, où, dans ce cas, Mercurial utilisera des liens physiques pour effectuer des partages copie-lors-des-écritures de ses métadonnées internes. Si cette explication ne signifie rien pour vous, ne vous inquiétez pas : tout ceci se passe de manière transparente et automatique. Vous n'avez pas du tout besoin de comprendre ceci.
Table of Contents
Nous avons maintenant étudié comment cloner un dépôt, effectuer des changements dedans, et récupérer ou transférer depuis un autre dépôt. La prochaine étape est donc de fusionner les modifications de différents dépôts.
La fusion est un aspect fondamental lorsqu'on travaille avec un gestionnaire de révisions distribué.
Alice et Bob ont chacun une copie personnelle du dépôt d'un projet sur lequel ils collaborent. Alice corrige un bug dans son dépôt, et Bob ajoute une nouvelle fonctionnalité dans le sien. Ils veulent un dépôt partagé avec à la fois le correctif du bug et la nouvelle fonctionnalité.
Je travaille régulièrement sur plusieurs tâches différentes sur un seul projet en même temps, chacun isolé dans son propre dépôt. Travailler ainsi signifie que je dois régulièrement fusionner une partie de mon code avec celui des autres.
Parce que nous devons fusionner souvent, Mercurial rend cette opération facile. Étudions ensemble le déroulement des opérations. Nous commencerons encore par faire un clone d'un autre dépôt (vous voyez que l'on fait ça tout le temps ?) puis nous ferons quelques modifications dessus.
$
cd ..
$
hg clone hello my-new-hello
updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd my-new-hello
# Make some simple edits to hello.c.$
my-text-editor hello.c
$
hg commit -m 'A new hello for a new day.'
Nous devrions avoir maintenant deux copies de
hello.c
avec des contenus différents. Les
historiques de ces deux dépôts ont aussi divergés, comme illustré dans
la figure Figure 3.1, “Historique divergeant des dépôts my-hello
et my-new-hello
.”.
$
cat hello.c
/* * Placed in the public domain by Bryan O'Sullivan. This program is * not covered by patents in the United States or other countries. */ #include <stdio.h> int main(int argc, char **argv) { printf("once more, hello.\n"); printf("hello, world!\"); printf("hello again!\n"); return 0; }
Et ici se trouve notre version légèrement différente du dépôt.
$
cat ../my-hello/hello.c
/* * Placed in the public domain by Bryan O'Sullivan. This program is * not covered by patents in the United States or other countries. */ #include <stdio.h> int main(int argc, char **argv) { printf("hello, world!\"); printf("hello again!\n"); return 0; }
Nous savons déjà que récupérer les modifications depuis
notre dépôt my-hello
n'aura
aucun effet sur l'espace de travail.
$
hg pull ../my-hello
pulling from ../my-hello 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)
Néanmoins, la commande hg pull nous indique quelque chose au sujet des “heads”.
Rappelez vous que Mercurial enregistre quelle révision est le parent de chaque révision. Si une révision a un parent, nous l'appelons un enfant “child” ou un descendant de ce parent. Une “head” est une révision qui n'a donc pas d'enfant. La révision “tip” est donc une “head”, car c'est la révision la plus récente du dépôt qui n'a pas d'enfant. Il y a des moments où un dépôt peut contenir plusieurs “heads”.
Figure 3.2. Contenu du dépôt après une récupération (pull) depuis le
dépôt my-hello
vers le dépôt my-new-hello
Dans la figure Figure 3.2, “Contenu du dépôt après une récupération (pull) depuis le
dépôt my-hello
vers le dépôt my-new-hello
”,
vous pouvez constater l'effet d'un “pull” depuis le dépôt
my-hello
dans le dépôt
my-new-hello
. L'historique qui
était déjà présent dans le dépôt my-new-hello
reste intact, mais une
nouvelle révision a été ajoutée. En vous reportant à la figure Figure 3.1, “Historique divergeant des dépôts my-hello
et my-new-hello
.”, vous pouvez voir que l'
ID de révision “changeset ID” reste le même dans
le nouveau dépôt, mais que le numéro de
révision reste le même. (Ceci est un parfait exemple de
pourquoi il n'est fiable d'utiliser les numéros de révision lorsque
l'on discute d'un “changeset”.) Vous pouvez voir les “heads”
présentes dans le dépôt en utilisant la commande hg heads.
$
hg heads
changeset: 6:ec142fc290c8 tag: tip parent: 4:2278160e78d4 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:25 2011 +0000 summary: Added an extra line of output changeset: 5:2491baec1ec8 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:35 2011 +0000 summary: A new hello for a new day.
Que se passe-t-il quand vous essayez d'utiliser la commande hg update pour mettre à jour votre espace de travail au nouveau “tip” ?
$
hg update
abort: crosses branches (use 'hg merge' or use 'hg update -c')
Mercurial nous prévient que la commande hg update n'effectuera pas la fusion, il ne veut pas mettre à jour l'espace de travail quand il estime que nous pourrions avoir besoin d'une fusion, à moins de lui forcer la main. À la place, il faut utiliser la commande hg merge pour fusionner les deux “heads”.
Pour commencer une fusion (merge) entre deux “heads”, nous utilisons la commande hg merge.
$
hg merge
merging hello.c 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit)
Nous résolvons les conflits dans le fichier
hello.c
. Ceci met à jour le répertoire de travail
de sorte qu'il ne contienne les modifications en provenance des
deux “heads”, ce qui est indiqué par la
la sortie de la commande hg
parents et le contenu du fichier
hello.c
.
$
hg parents
changeset: 5:2491baec1ec8 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:35 2011 +0000 summary: A new hello for a new day. changeset: 6:ec142fc290c8 tag: tip parent: 4:2278160e78d4 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:25 2011 +0000 summary: Added an extra line of output$
cat hello.c
/* * Placed in the public domain by Bryan O'Sullivan. This program is * not covered by patents in the United States or other countries. */ #include <stdio.h> int main(int argc, char **argv) { printf("once more, hello.\n"); printf("hello, world!\"); printf("hello again!\n"); return 0; }
Dès l'instant où vous avez effectué une fusion (merge), hg parents vous affichera deux parents, avant que vous n'exécutiez la commande hg commit sur le résultat de la fusion.
$
hg commit -m 'Merged changes'
Nous avons maintenant un nouveau tip, remarquez qu'il contient à la fois nos anciennes “heads” et leurs parents. Ce sont les mêmes révisions que nous avions affichées avec la commande hg parents.
$
hg tip
changeset: 7:3c1554942e80 tag: tip parent: 5:2491baec1ec8 parent: 6:ec142fc290c8 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:36 2011 +0000 summary: Merged changes
Dans la figure Figure 3.3, “Répertoire de travail et dépôt pendant une fusion, et le “commit” qui suit”, vous pouvez voir une représentation de ce qui se passe dans l'espace de travail pendant la fusion, et comment ceci affecte le dépôt lors du “commit”. Pendant la fusion, l'espace de travail, qui a deux révisions (changesets) comme parents, voit ces derniers devenir le parent d'une nouvelle révision (changeset).
La plupart des fusions sont assez simples à réaliser, mais parfois vous vous retrouverez à fusionner des fichiers où la modification touche la même portion de code, au sein d'un même fichier. À moins que ces modification ne soient identiques, ceci aboutira à un conflit, et vous devrez décider comment réconcilier les différentes modifications dans un ensemble cohérent.
La figure Figure 3.4, “Modifications en conflit dans un document” illustre un cas de modifications conflictuelles dans un document. Nous avons commencé avec une version simple de ce fichier, puis nous avons ajouté des modifications, pendant que quelqu'un d'autre modifiait le même texte. Notre tâche dans la résolution du conflit est de décider à quoi le fichier devrait ressembler.
Mercurial n'a pas de mécanisme interne pour gérer les conflits. À la place, il exécute un programme externe appelé hgmerge. Il s'agit d'un script shell qui est compris avec Mercurial, vous pouvez le modifier si vous voulez. Ce qu'il fait par défaut est d'essayer de trouver un des différents outils de fusion qui seront probablement installés sur le système. Il commence par les outils totalement automatiques, et s'ils échouent (parce que la résolution du conflit nécessite une intervention humaine) ou s'ils sont absents, le script tente d'exécuter certains outils graphiques de fusion.
Il est aussi possible de demander à Mercurial d'exécuter
un autre programme ou un autre script en définissant la variable
d'environnement HGMERGE
avec le nom
du programme de votre choix.
Mon outil de fusion préféré est kdiff3, que j'utilise ici pour illustrer les fonctionnalités classiques des outils graphiques de fusion. Vous pouvez voir une capture d'écran de l'utilisation de kdiff3 dans la figure Figure 3.5, “Utiliser kdiff3 pour fusionner les différentes version d'un fichier.”. Cet outil effectue une fusion “three-way”, car il y a trois différentes versions du fichier qui nous intéressent. Le fichier découpe la partie supérieure de la fenêtre en trois panneaux :
À gauche on trouve la version de base du fichier, soit la plus récente version des deux versions qu'on souhaite fusionner.
Au centre, il y a “notre” version du fichier, avec le contenu que nous avons modifié.
Sur la droite, on trouve “leur” version du fichier, celui qui contient la révision que nous souhaitons intégrer.
Dans le panneau en dessous, on trouve le résultat actuel de notre fusion. Notre tâche consiste donc à remplacer tous les textes en rouge, qui indiquent des conflits non résolus, avec une fusion manuelle et pertinente de “notre” version et de la “leur”.
Les quatre panneaux sont accrochés ensemble, si nous déroulons les ascenseurs verticalement ou horizontalement dans chacun d'entre eux, les autres sont mis à jour avec la section correspondante dans leurs fichiers respectifs.
Pour chaque portion de fichier posant problème, nous pouvons choisir de résoudre le conflit en utilisant une combinaison de touches depuis la version de base, la nôtre, ou la leur. Nous pouvons aussi éditer manuellement les fichiers à tout moment, si c'est nécessaire.
Il y a beaucoup d'outils de fusion disponibles, bien trop pour parler de tous ici. Leurs disponibilités varient selon les plateformes ainsi que leurs avantages et inconvénients. La plupart sont optimisés pour la fusion de fichier contenant un texte plat, certains sont spécialisés dans un format de fichier précis (généralement XML).
Dans cet exemple, nous allons reproduire la modification de l'historique du fichier de la figure Figure 3.4, “Modifications en conflit dans un document” ci-dessus. Commençons par créer un dépôt avec une version de base de notre document.
$
cat > letter.txt <<EOF
>
Greetings!
>
I am Mariam Abacha, the wife of former
>
Nigerian dictator Sani Abacha.
>
EOF
$
hg add letter.txt
$
hg commit -m '419 scam, first draft'
Créons un clone de ce dépôt et effectuons une modification dans le fichier.
$
cd ..
$
hg clone scam scam-cousin
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd scam-cousin
$
cat > letter.txt <<EOF
>
Greetings!
>
I am Shehu Musa Abacha, cousin to the former
>
Nigerian dictator Sani Abacha.
>
EOF
$
hg commit -m '419 scam, with cousin'
Et un autre clone, pour simuler que quelqu'un d'autre effectue une modification sur le fichier. (Ceci pour suggérer qu'il n'est pas rare de devoir effectuer des fusions (merges) avec vos propres travaux quand vous isolez les tâches dans des dépôts distincts. En effet, vous aurez alors à trouver et résoudre certains conflits).
$
cd ..
$
hg clone scam scam-son
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd scam-son
$
cat > letter.txt <<EOF
>
Greetings!
>
I am Alhaji Abba Abacha, son of the former
>
Nigerian dictator Sani Abacha.
>
EOF
$
hg commit -m '419 scam, with son'
Maintenant que ces deux versions différentes du même fichier sont créées, nous allons configurer l'environnement de manière appropriée pour exécuter notre fusion (merge).
$
cd ..
$
hg clone scam-cousin scam-merge
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd scam-merge
$
hg pull -u ../scam-son
pulling from ../scam-son searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) not updating, since new heads added (run 'hg heads' to see heads, 'hg merge' to merge)
Dans cette exemple, je n'utiliserais pas la commande Mercurial
habituelle hgmerge pour effectuer la
fusion (merge), car il me faudrait abandonner ce joli petit exemple automatisé
pour utiliser un outil graphique. À la place, je vais définir la
variable d'environnement HGMERGE
pour indiquer à
Mercurial d'utiliser la commande non-interactive merge.
Cette dernière est comprise dans de nombreux systèmes “à la Unix”.
Si vous exécutez cet exemple depuis votre ordinateur, ne vous
occupez pas de définir HGMERGE
.
$
export HGMERGE=merge
$
hg merge
merging letter.txt merge: warning: conflicts during merge merging letter.txt failed! 0 files updated, 0 files merged, 0 files removed, 1 files unresolved use 'hg resolve' to retry unresolved file merges or 'hg update -C' to abandon$
cat letter.txt
Greetings! <<<<<<< /tmp/tour-merge-conflictJzT4po/scam-merge/letter.txt I am Shehu Musa Abacha, cousin to the former ======= I am Alhaji Abba Abacha, son of the former >>>>>>> /tmp/letter.txt~other.LnSKVz Nigerian dictator Sani Abacha.
Parce que merge ne peut pas résoudre les modifications conflictuelles, il laisse des marqueurs de différences à l'intérieur du fichier qui a des conflits, indiquant clairement quelles lignes sont en conflit, et si elles viennent de notre fichier ou du fichier externe.
Mercurial peut distinguer, à la manière dont la commande merge se termine, qu'elle n'a pas été capable d'effectuer la fusion (merge), alors il nous indique que nous devons effectuer de nouveau cette opération. Ceci peut être très utile si, par exemple, nous exécutons un outil graphique de fusion et que nous le quittons sans nous rendre compte qu'il reste des conflits ou simplement par erreur.
Si la fusion (merge) automatique ou manuelle échoue, il n'y a rien pour nous empêcher de “corriger le tir” en modifiant nous-même les fichiers, et enfin effectuer le “commit” du fichier:
$
cat > letter.txt <<EOF
>
Greetings!
>
I am Bryan O'Sullivan, no relation of the former
>
Nigerian dictator Sani Abacha.
>
EOF
$
hg resolve -m letter.txt
$
hg commit -m 'Send me your money'
$
hg tip
changeset: 3:2eb6858f32d9 tag: tip parent: 1:3136513772a5 parent: 2:7d96870fc99d user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:37 2011 +0000 summary: Send me your money
La procédure pour effectuer la fusion indiquée ci-dessus est simple, mais requiert le lancement de trois commandes à la suite.
hg pull -u hg merge hg commit -m 'Merged remote changes'
Lors du “commit” final, vous devez également saisir un message, qui aura vraisemblablement assez peu d'intérêt.
Il serait assez sympathique de pouvoir réduire le
nombre d'opérations nécessaire, si possible. De fait Mercurial est
fournit avec une extension appelée fetch
qui fait justement cela.
Mercurial fournit un mécanisme d'extension flexible qui permet à chacun d'étendre ces fonctionnalités, tout en conservant le cœur de Mercurial léger et facile à utiliser. Certaines extensions ajoutent de nouvelles commandes que vous pouvez utiliser en ligne de commande, alors que d'autres travaillent “en coulisse”, par exemple en ajoutant des possibilités au serveur.
L'extension fetch
ajoute une nouvelle commande nommée, sans surprise, hg fetch. Cette extension consiste en une
combinaison de hg pull, hg update et hg
merge. Elle commence par récupérer les modifications d'un
autre dépôt dans le dépôt courant. Si elle trouve que les
modifications ajoutent une nouvelle “head”, elle effectue un “merge”,
et ensuite “commit” le résultat du “merge” avec un message généré
automatiquement. Si aucune “head” n'a été ajouté, elle met à jour le
répertoire de travail au niveau de la nouvelle révision “tip”.
Activer l'extension fetch
est facile. Modifiez votre fichier .hgrc
, et soit allez à la section extensions
soit créez une section
extensions
. Ensuite ajoutez
une ligne qui consiste simplement en “fetch =”.
[extensions] fetch =
(Normalement, sur la partie droite de
“=
” devrait apparaître le chemin de
l'extension, mais étant donné que l'extension fetch
fait partie de la distribution standard,
Mercurial sait où la trouver.)
En cours de la vie d'un projet, nous allons souvent vouloir changer la disposition de ses fichiers et de ses répertoires. Ceci peut être aussi simple que de changer le nom d'un seul fichier, et aussi compliqué que de restructurer une hiérarchie entière de fichiers au sein du projet.
Mercurial permet de faire ce genre de modification de manière fluide, à condition de l'informer de ce que nous faisons. Si vous voulez renommer un ficher, vous devriez utiliser la commande hg rename[2] pour changer son nom, ainsi Mercurial peut ensuite prendre la bonne décision, plus tard, en cas de fusion (merge).
Nous étudierons, en détail, l'utilisation de ces commandes dans le chapitre Section 5.3, “Copier des fichiers”.
[2] Si vous êtes un utilisateur d'Unix, vous serez content de savoir que la commande hg rename peut être abrégée en hg mv.
Table of Contents
À la différence de beaucoup d'outils de gestion de révisions, les concepts sur lesquels se base Mercurial sont assez simples pour qu'il soit facile de comprendre comment le logiciel fonctionne. Bien que leur connaissance ne soit pas indispensable, je trouve utile d'avoir un “modèle mental” de ce qui se passe.
En effet, cette compréhension m'apporte la confiance que Mercurial a été développé avec soin pour être à la fois sûr et efficace. De surcroît, s'il m'est facile de garder en tête ce que le logiciel fait lorsque j'accomplis des tâches de révision, j'aurai moins de risques d'être surpris par son comportement.
Dans ce chapitre, nous décrirons tout d'abord les concepts essentiels de l'architecture de Mercurial, pour ensuite discuter quelques détails intéressants de son implémentation.
Lorsque Mercurial effectue un suivi des modifications
faites à un fichier, il conserve l'historique pour ce fichier dans un
filelog sous forme de métadonnées. Chaque entrée
dans le “filelog” contient assez d'informations pour reconstituer une
révision du fichier correspondant. Les “filelogs” sont des fichiers
stockés dans le répertoire .hg/store/data
. Un “filelog” contient
des informations de deux types : les données de révision, et un index
pour permettre à Mercurial une recherche efficace d'une révision
donnée.
Lorsqu'un fichier devient trop gros ou a un long
historique, son “filelog” se voit stocké dans un fichier de données
(avec un suffixe “.d
”) et un fichier
index (avec un suffixe“.i
”)
distincts. La relation entre un fichier dans le répertoire de travail
et le “filelog” couvrant le suivi de son historique dans le dépôt est
illustré à la figure Figure 4.1, “Relations entre les fichiers dans le répertoire de travail et
leurs “filelogs” dans le dépôt”.
Figure 4.1. Relations entre les fichiers dans le répertoire de travail et leurs “filelogs” dans le dépôt
Mercurial a recours à une structure nommée manifest pour rassembler les informations sur les fichiers dont il gère le suivi. Chaque entrée dans ce “manifest” contient des informations sur les fichiers présents dans une révision donnée. Une entrée enregistre la liste des fichiers faisant partie de la révision, la version de chaque fichier, et quelques autres métadonnées sur ces fichiers.
Le changelog contient les informations sur chaque “changeset”. Chaque révision enregistre qui a “committé” un changement, le commentaire du “changeset”, d'autres morceaux d'information relatives au “changeset” et la révision du “manifest” à utiliser.
À l'intérieur d'un “changelog”, d'un “manifest”, ou d'un “filelog”, chaque révision enregistre un pointeur vers son parent immédiat (ou à ses deux parents s'il s'agit d'une révision correspondant à une fusion (merge)). Comme mentionné plus haut, il y a aussi des relations entre les révisions à travers ces structures, qui sont de nature hiérarchique.
Pour chaque “changeset” dans un dépôt, il y a exactement une révision stockée dans le “changelog”. Chaque révision du “changelog” contient un pointeur vers une unique révision du “manifest”. Une révision du “manifest” garde un pointeur vers une unique révision pour chaque “filelog” suivi lorsque le “changeset” est créé. Ces relations sont illustrées dans Figure 4.2, “Metadata relationships”.
Comme l'illustration le montre, il n'y a pas de relation “un à un” entre les révisions dans un “changelog”, “manifest” ou “filelog”. Si un fichier que Mercurial suit n'a pas changé entre deux “changesets”, l'entrée pour ce fichier dans les deux révisions du “manifest” pointera vers la même révision de son “filelog” [3].
Les fondements des “changelogs”, des “manifests” et des “filelogs” sont fournis par une unique structure appelée le revlog.
Le “revlog” fournit un stockage efficace des révisions en utilisant un mécanisme delta. Au lieu de stocker une copie complète d'un fichier à chaque révision, il stocke les changements requis pour transformer une révision plus ancienne en une nouvelle révision. Pour plusieurs types de données, ces deltas sont typiquement une fraction de pourcentage de la taille de la copie complète d'un fichier.
Certains systèmes de gestion de révisions obsolètes peuvent seulement travailler avec les deltas de fichiers texte. Il doivent d'ailleurs stocker les fichiers binaires comme des images complètes ou encodées avec une représentation texte, chacune de ces approches étant gaspilleuse. Mercurial peut traiter les deltas de fichiers avec du contenu binaire arbitraire ; il n'a pas besoin de traiter spécialement du texte.
Mercurial empile toujours les données à la fin d'un fichier “revlog”. Il ne modifie jamais la section d'un fichier après qu'il l'ait écrite. C'est à la fois plus robuste et efficace que les schémas qui ont besoin de modifier ou réécrire les données.
De plus, Mercurial traite chaque écriture comme la partie d'une transaction qui peut comprendre plusieurs fichiers. Une transaction est atomique : soit la transaction entière réussit et ses effets sont tous visibles aux lecteurs en une étape, soit la totalité est annulée. Cette garantie de l'atomicité signifie que si vous exécutez deux copies de Mercurial, où une lit les données et l'autre les écrit, le lecteur ne verra jamais un résultat partiellement écrit qui pourrait le perturber.
Le fait que Mercurial ne fasse qu'ajouter aux fichiers fait qu'il est facile de fournir cette garantie de transaction. Plus les choses sont faites simplement comme ça, plus vous pouvez être rassurés qu'elles sont bien faites.
Mercurial évite habillement un piège commun à tous les vieux systèmes de gestion de révisions : le problème de la récupération inefficace. La plupart des systèmes de gestion de révisions stockent le contenu d'une révision comme une série incrémentale de modifications faites à un “snapshot”. (Certains basent le “snapshot” sur la plus vieille révision, d'autres sur la plus récente.) Pour reconstruire une révision spécifique, vous devez d'abord lire le “snapshot”, et ensuite toutes les révisions entre le “snapshot” et votre révision cible. Plus vous avez d'historique accumulé dans un fichier, plus de révisions vous avez à lire, d'où la longueur que cela prend à reconstruire une révision particulière.
L'innovation que Mercurial apporte à ce problème est simple mais efficace. Une fois que la quantité cumulée de deltas d'informations stockées depuis le dernier snapshot excède un seuil fixé, il stocke un nouveau “snapshot” (compressé bien sûr), plutôt qu'un nouveau delta. Ceci rend possible la reconstruction de toute révision d'un fichier rapidement. Cette approche fonctionne si bien que depuis, elle a été copiée par plusieurs autres systèmes de gestion de révisions.
Figure 4.3, ““Snapshot” d'un “revlog”, avec des deltas incrémentaux” illustre l'idée. Dans une entrée d'un fichier d'index de “revlog”, Mercurial stocke l'intervalle des entrées depuis le fichier de données qu'il doit lire pour reconstruire une révision particulière.
Si vous êtes familiés de la compression vidéo ou avez déjà regardé un programme TV par câble ou par un service satellite, vous devez savoir que la plupart des schémas de compression vidéo stockent chaque trame de vidéo comme un delta vis-à-vis de la trame précédente.
Mercurial emprunte cette idée pour rendre possible la reconstruction d'une révision à partir d'un snapshot et d'un petit nombre de deltas.
Avec les deltas ou l'information du snapshot, une entrée d'un revlog contient un hash cryptographique des données qu'il représente. Ceci fait qu'il est difficile de construire les données d'une révision, mais facile de détecter une corruption accidentelle.
Les hash fournissent plus qu'un bon moyen de vérification contre la corruption ; il sont aussi utilisés comme identifiants pour les révisions. Les “hashs” d'identifications d'un “changeset” que vous voyez comme utilisateur final proviennent des révisions du “changelog”. Bien que les “filelogs” et le “manifest” utilisent aussi des “hashs”, Mercurial ne les utilise qu'en arrière-plan.
Mercurial vérifie que les “hashs” sont corrects lorsqu'il récupère les révisions de fichiers et lorsqu'il récupère (pull) les changements d'un autre dépôt. S'il rencontre un problème d'intégrité, il se plaindra et arrêtera tout ce qu'il est en train de faire.
En plus de l'effet qu'il a sur l'efficacité des récupérations, l'utilisation par Mercurial de “snapshots” périodiques fait qu'il est plus robuste contre la corruption partielle de données. Si un “revlog” devient partiellement corrompu à cause d'une erreur matérielle ou d'un bug système, il est souvent possible de reconstruire certaines ou la plupart des révisions à partir des sections non corrompues du “revlog”, avant et après la section corrompue. Ceci ne serait pas possible à partir d'un modèle de stockage de deltas seul.
Chaque entrée dans un “revlog” Mercurial connaît l'identité de l'ancêtre immédiat de la révision, habituellement désignée comme son parent. En fait, une révision contient de la place pour non pas un parent, mais deux. Mercurial utilise un “hash” spécial, appelé le “null ID” pour représenter l'idée qu'“il n'y a pas de parent ici”. Ce “hash” est simplement une chaîne de zéros.
Dans Figure 4.4, “Le concept de la structure d'un “revlog””, vous pouvez voir un exemple de la structure conceptuelle d'un “revlog”. Les “filelogs”, “manifests” et “changelogs” ont tous cette même structure ; ils diffèrent simplement dans le type de donnée stockée dans chaque delta ou “snapshot”.
La première révision d'un “revlog” (au bas de l'image) a le “null ID” dans chacune de ses cases parent. Pour une révision “normale”, sa première case parent contient l'ID de sa révision parent et la seconde contient le “null ID”, indiquant que cette révision n'a qu'un seul vrai parent. Si deux révisions ont le même parent, il s'agit de branches. Une révision qui représente une fusion (merge) entre deux branches a deux identifiants de révision normaux dans ses cases parents.
Dans un répertoire de travail, Mercurial stocke une image des fichiers du dépôt à un “changeset” particulier.
Le répertoire de travail “sait” quel “changeset” il contient. Lorsque vous mettez à jour (update) le répertoire de travail à un certain “changeset”, Mercurial regarde la révision appropriée du “manifest” pour trouver quels fichiers il suivait au moment où le “changeset” a été “committé”, et quelle révision de chaque fichier était alors courante. Il recrée ensuite une copie de chacun de ces fichiers, avec le même contenu qu'ils avaient lorsque le “changeset” a été “committé”.
La structure spéciale dirstate
contient la connaissance de Mercurial sur le répertoire de travail.
Elle est maintenue par un fichier appelé
.hg/dirstate
dans un dépôt. Les détails du
dirstate sont le “changeset” vers lequel le répertoire de travail se met
à jour (update), et tous les fichiers que Mercurial suit dans le
répertoire de travail. Il permet aussi à Mercurial de connaître
rapidement les fichiers modifiés, en enregistrant l'heure de
dernière modification et la taille de chacun.
Puisqu'une révision de “revlog” a des emplacements pour deux parents et peut représenter aussi bien une révision normale (avec un parent) ou une fusion de deux révisions anciennes, le “dirstate” a des emplacements pour deux parents. Lorsque vous utilisez la commande hg update, le “changeset” que vous mettez à jour est stocké dans l'emplacement du “premier parent”, et le “null ID” l'est dans le second. Lorsque vous utilisez la commande hg merge avec un autre changeset, le premier parent reste inchangé, et le second est rempli avec le “changeset” à partir duquel vous êtes en train de fusionner. La commande hg parents vous donne les parents du dirstate.
Le “dirstate” stocke les informations sur les parents pour plus qu'une simple comptabilité. Mercurial utilise les parents du “dirstate” comme les parents d'un nouveau “changeset” lorsque vous “committez”.
Figure 4.5, “Le répertoire de travail peut avoir deux parents” montre l'état normal d'un répertoire de travail, où il n'y a qu'un seul “changeset” comme parent. Ce “changeset” est le tip, le “changeset” le plus récent dans le dépôt n'a pas d'enfant.
On peut se représenter le répertoire de travail comme “le changeset que je vais committer”. Chaque fichier que vous demandez à Mercurial d'ajouter, de supprimer, de renommer ou de copier va être intégré dans ce changeset, tout comme les modifications de n'importe quel fichier que Mercurial est déjà en train de suivre ; le nouveau “changeset” aura les mêmes parents que le répertoire de travail.
Après un commit, Mercurial va mettre à jour les parents du répertoire de travail, ainsi, le premier parent est l'ID du nouveau “changeset”, et le second, le “null ID”. Ceci est illustré dans Figure 4.6, “Le répertoire de travail gagne de nouveaux parents après un “commit””. Mercurial ne touche à aucun des fichiers du répertoire de travail lorsque vous “committez” ; il modifie simplement le dirstate pour noter ses nouveaux parents.
Il est parfaitement normal de faire un “update” du répertoire de travail à un “changeset” autre que le “tip” courant. Par exemple, vous pourriez vouloir savoir à quoi votre projet ressemblait mardi dernier, ou regarder le “changeset” qui a introduit un bug. Dans des cas comme ça, la chose naturelle à faire est de faire un “update” du répertoire de travail au “changeset” qui vous intéresse, et ensuite d'en examiner les fichiers pour regarder leurs contenus comme ils l'étaient lorsque vous avez “committé” ce “changeset”. L'effet de ceci est montré dans Figure 4.7, “Le répertoire de travail, “updaté” pour un “changeset” plus ancien”.
En ayant fait un “update” du répertoire de travail vers un “changeset” plus ancien, que se passe-t-il si vous faites des changements et ensuite “committez” ? Mercurial se comporte comme je l'ai fait remarqué plus haut. Les parents du répertoire de travail deviennent les parents du nouveau “changeset”. Ce nouveau “changeset” n'a pas d'enfant, donc il devient le nouveau “tip”. Le dépôt contient maintenant deux “changesets” qui n'ont pas d'enfant ; on appelle ceci des heads. Vous pouvez voir la structure que cela crée dans Figure 4.8, “Après un “commit” fait pendant la synchronisation avec un ancien “changeset””.
Lorsque vous exécutez la commande hg merge, Mercurial laisse le premier parent du répertoire de travail inchangé et fixe le second au “changeset” avec lequel vous fusionnez (merge), comme montré dans Figure 4.9, “Fusionner (merge) deux “heads””.
Mercurial doit aussi modifier le répertoire de travail pour fusionner les fichiers gérés dans les deux “changesets”. Un peu simplifié, le processus de fusion fonctionne comme ça : pour chaque fichier dans le “manifest” de chaque “changeset”.
Si aucun “changeset” n'a modifié un fichier, ne rien faire avec ce fichier.
Si un “changeset” a modifié un fichier et que l'autre ne l'a pas fait, créer une copie modifiée du fichier dans le répertoire de travail.
Si un “changeset” a modifié un fichier, et que l'autre ne l'a pas fait (ou l'a supprimé), supprimer le fichier du répertoire de travail.
Si un “changeset” a supprimé un fichier, mais que l'autre a modifié le fichier, demander à l'utilisateur quoi faire : garder le fichier modifié ou le supprimer ?
Si chacun des “changesets” a modifié un fichier, invoquer le programme externe de fusion pour choisir les nouveaux contenus pour le fichier fusionné. Ceci peut demander une intervention de l'utilisateur.
Si un “changeset” a modifié un fichier, et que l'autre a renommé ou copié le fichier, être sûr que les changements suivent le nouveau nom du fichier.
Il y a plus de détails—fusionner a beaucoup de cas épineux—mais ceux-ci sont des choix plus communs qui sont liés à une fusion (merge). Comme vous pouvez le voir, la plupart des cas sont entièrement automatiques, et effectivement, la plupart des fusions (merge) se terminent automatiquement, sans nécessiter votre intervention pour résoudre un conflit.
Lorsque vous pensez à ce qu'il se passe lorsque vous “committez” après un “merge”, une fois encore, le répertoire de travail est “le changeset que je suis sur le point de committer”. Après que la commande hg merge soit terminée, le répertoire de travail a deux parents ; ceux ci vont devenir les parents du nouveau “changeset”.
Mercurial vous permet d'exécuter de multiples fusions, mais vous devez “committer” le résultat de chaque fusion individuellement au fur et à mesure. Ceci est nécessaire puisque Mercurial ne stocke que deux parents pour chaque révision et le répertoire de travail. Alors qu'il serait techniquement faisable de fusionner de multiples “changesets” en même temps, Mercurial interdit cela pour être plus simple. Avec des fusions multiples, les risques de confusion pour l'utilisateur, de mauvaie résolution de conflits, et de pagaille dans les fusions augmenteraient de façon intolérable.
Un nombre surprenant de systèmes de gestion de révisions fait peu ou pas attention à un nom de fichier au cours du temps. Par exemple, il était habituel que si un fichier était renommé d'un coté de la fusion, les changements à partir de l'autre coté étaient supprimés silencieusement.
Mercurial enregistre les metadata lorsque vous lui dites d'exécuter un renommage ou une copie. Il utilise ces metadatas durant une fusion pour faire les bonnes choses dans le cas d'un “merge”. Par exemple, si je renomme un fichier et que vous l'éditez sans le renommer, lorsque l'on fusionne, le fichier sera renommé et aura les changements appliqués.
Dans les sections au-dessus, j'ai tenté de mettre l'accent sur certains aspects importants du design de Mercurial pour illustrer l'attention particulière qui a été portée à la fiabilité et à la performance. Cependant, l'attention aux détails ne s'arrête pas ici. Il y a de nombreux aspects sur la construction de Mercurial que je trouve personnellement intéressants. J'en détaillerai quelques-uns ici, séparément des éléments du “big ticket” ci-dessus, ainsi, si vous êtes intéressés, vous pourrez avoir une meilleure idée de la quantité d'ingéniosité qu'il y a derrière un système bien conçu.
Lorsque cela est approprié, Mercurial stocke les “snapshots” et deltas sous une forme compressée. Il le fait en essayant toujours de compresser un “snapshot” ou un delta, mais en ne stockant la version compressée que si celle-ci est plus petite que la version non compressée.
Ceci signifie que Mercurial fait “la bonne
chose” lorsqu'il stocke un fichier dont la forme native est
compressée, comme une archive zip
ou une image
JPEG. Lorsque ces types de fichiers sont compressés une seconde fois,
le fichier obtenu est habituellement plus gros que la forme
compressée une seule fois et Mercurial stockera alors le
zip
ou JPEG.
Les deltas entre les révisions d'un fichier compressé sont habituellement plus gros que les snapshots du fichier, et Mercurial fait à nouveau “la bonne chose” dans ces cas. Il trouve qu'un delta dépasse le seuil auquel il devrait stocker un “snapshot” complet du fichier, alors il stocke le “snapshot”, en gagnant encore de la place en comparaison d'une approche naïve avec un delta seulement.
Lors du stockage des révisions sur le disque,
Mercurial utilise l'algorithme de compression
“deflate” (le même que celui utilisé pour le format populaire
d'archive zip
), qui est un bon
compromis entre la vitesse et le taux de compression. Cependant,
lors de la transmission d'une révision de données par une connexion
réseau, Mercurial décompresse les données de révision
compressées.
Si la connexion passe par HTTP, Mercurial
recompresse le flux entier de données en utilisant un algorithme de
compression qui donne un meilleur taux de compression (l'algorithme
Burrows-Wheeler utilisé principalement par le logiciel de
compression bzip2
). Cette combinaison de
l'algorithme et de la compression du flux entier (plutôt que pour une
révision à la fois) réduit substantiellement le nombre de bits qui
sont transférés, résultant en une performance réseau accrue sur
la plupart des supports.
Si la connexion passe par
ssh, Mercurial ne
recompresse pas le flux puisque
ssh peut déjà le faire par lui-même. Vous pouvez
demander à Mercurial de toujours utiliser la compression
ssh en éditant le fichier
.hgrc
de votre répertoire personnel comme ci-dessous.
[ui] ssh = ssh -C
L'histoire ne se résume pas à ajouter à la fin des fichiers lorsque l'on cherche à garantir que le lecteur ne verra pas qu'une écriture partielle. Si vous relisez Figure 4.2, “Metadata relationships”, les révisions dans le “changelog” pointent vers les révisions dans le “manifest”, et les révisions du “manifest” pointent vers les révisions du “filelog”. Cette hiérarchie est délibérée.
L'écriture commence par une transaction en écrivant dans le “filelog” et dans les données du “manifest”, et n'écrit aucune donnée du “changelog” tant que ce n'est pas terminé. La lecture commence en lisant les données du “changelog”, puis les données du “manifest”, et enfin les données du “filelog”.
Puisque que l'écriture ne finit pas d'écrire les données du “filelog” et du “manifest” avant d'écrire dans le “changelog”, la lecture ne verra jamais un pointeur vers une révision du “manifest” partiellement écrite à partir du “changelog”, et ne lira jamais un pointeur vers une révision du “filelog” partiellement écrite dans le “manifest”.
La garantie de l'ordre de lecture/écriture et de l'atomicité signifie que Mercurial n'a jamais besoin de poser de lock sur un dépôt lorsqu'il lit des données, même si le dépôt est en train d'être écrit au même moment que la lecture a lieu. Ceci a un grand impact sur la fiabilité ; vous pouvez avoir un nombre arbitraire de processus Mercurial qui lisent sans risque les données d'un dépôt en même temps, peu importe s'il est en train d'être lu ou non.
La nature sans “lock” de la lecture signifie que si vous partagez un dépôt sur un système multi-utilisateurs, vous n'avez pas besoin de donner aux autres utilisateurs locaux la permission d'écrire sur votre dépôt pour qu'ils soient capable de faire un clone ou un “pull” des changements à partir de celui-ci ; ils ont seulement besoin de la permission en lecture. (Il ne s'agit pas d'une fonctionnalité commune à travers les systèmes de gestion de révisions, donc ne prenez pas ça pour argent comptant ! La plupart ont besoin que les lecteurs soient capables de mettre un lock sur le dépôt pour y accéder en toute sécurité, et ceci demande des permissions en écriture, sur au moins un répertoire, ce qui provoque bien sûr toutes sortes de problèmes pénibles et agaçants relatifs à la sécurité et à l'administration.)
Mercurial utilise des “locks” pour assurer qu'un seul processus peut écrire dans le dépôt à un moment donné (le mécanisme de “lock” est sûr, même sur des systèmes de fichiers qui sont connus pour être hostiles aux “locks”, comme NFS). Si un dépôt dispose d'un “lock”, un processus qui cherche à écrire va attendre un peu avant de retenter pour voir si le dépôt perd son “lock”, mais si le dépôt garde trop longtemps son “lock”, le processus qui tente d'écrire va expirer (time out) après un moment. Cela veut dire par exemple que vos scripts lancés quotidiennement n'attendront pas toujours et boucleront si un système plantait sans avertissement, par exemple. (Oui, le timeout est configurable, de zéro à l'infini.)
Comme avec les données de révision, Mercurial n'utilise pas
de “lock” pour lire le fichier “dirstate” ; il n'acquiert pas un “lock” pour
y écrire. Pour empêcher la possibilité de lire une copie partiellement
écrite du fichier “dirstate”, Mercurial écrit sur un fichier avec un nom
unique dans le même répertoire que le fichier “dirstate”, ensuite renomme
le fichier temporaire automatiquement en dirstate
.
Le fichier nommé dirstate
est ainsi garanti d'être
écrit totalement, et non partiellement.
L'absence de recherche sur les têtes de disques est critique pour la performance de Mercurial, puisque toute recherche est beaucoup plus coûteuse comparativement à une grosse opération de lecture.
C'est pour ça, par exemple, que le “dirstate” est stocké dans un fichier unique. S'il y avait eu un “dirstate” par répertoire que Mercurial suivrait, le disque aurait recherché une fois par répertoire. Au lieu de ça, Mercurial lit entièrement un fichier unique, en une étape.
Mercurial utilise aussi un schéma “copie à l'écriture” lorsqu'il clone un dépôt sur un stockage local. Au lieu de copier chaque “fichier” revlog depuis l'ancien dépôt vers le nouveau dépôt, il crée un “lien physique”, qui est le plus court chemin pour dire “Ces deux noms pointent vers le même fichier”. Lorsque Mercurial est sur le point d'écrire sur l'un des revlogs de ces fichiers, il vérifie si le nombre de noms pointant sur ce fichier est plus grand que un. Si c'est le cas, plus d'un dépôt utilise le fichier, donc Mercurial crée une nouvelle copie du fichier qui est privée à ce dépôt.
Quelques développeurs de systèmes de gestion de révisions ont montré que cette idée de faire une copie privée complète d'un fichier n'est pas vraiment efficace au niveau du stockage. Bien que ce soit vrai, le stockage est peu onéreux, et cette méthode donne la plus grande performance lorsque l'on reporte la plupart des journalisations au système d'exploitation. Un schéma alternatif réduirait certainement la performance tout en augmentant la complexité du logiciel, mais la vitesse et la simplicité sont les clefs du “confort” de l'utilisation quotidienne.
Puisque Mercurial ne vous force pas à signaler que vous modifiez un fichier, il utilise le “dirstate” pour stocker certaines informations supplémentaires pour déterminer efficacement si vous avez ou non modifié un fichier. Pour chaque fichier du répertoire de travail, il stocke l'heure à laquelle il a été modifié, ainsi que la taille du fichier à cette heure.
Lorsque vous faites explicitement un hg add, hg remove, hg rename ou hg copy sur des fichiers, Mercurial met à jour le “dirstate” afin de savoir que faire lorsque vous effectuez un “commit”.
Le “dirstate” aide Mercurial à vérifier efficacement le statut des fichiers dans un dépôt.
Lorsque Mercurial vérifie l'état d'un fichier du répertoire de travail, il compare d'abord la date de dernière modification du fichier avec celle enregistrée dans le “dirstate” qui correspond à celle que Mercurial a écrit en dernier sur ce fichier. Si la date de dernière modification correspond à la date où Mercurial a écrit le fichier, celui ci n'a pas été modifié, donc Mercurial n'a pas besoin de revérifier.
Si la taille du fichier a changé, celui-ci a été modifié. Si la date de modification a changé mais que la taille est restée inchangée, seulement à ce moment là Mercurial doit vérifier le contenu du fichier pour savoir s'il a été modifié.
Enregistrer la date de modification et la taille réduit grandement le nombre d'opérations de lecture que Mercurial doit effectuer lorsque l'on utilise une commande comme hg status. Le résultat est un grand gain de performance.
[3] Il est possible (bien qu'inhabituel) qu'un “manifest” reste le même entre deux “changesets”, auquel cas l'entrée du “changelog” pour ces “changesets” pointera vers la même révision du “manifest”.
Table of Contents
Mercurial ne suit pas les fichiers de votre dépôt tant
que vous ne lui avez pas dit de les gérer. La commande hg status vous dira quels fichiers sont
inconnus de Mercurial. Il utilise un
“?
” pour montrer ces fichiers.
Pour informer Mercurial de suivre un fichier, utilisez
la commande hg add. Une fois que vous
avez ajouté un fichier, la ligne correspondante à ce fichier dans la
sortie de hg status change de
“?
” à
“A
”.
$
hg init add-example
$
cd add-example
$
echo a > myfile.txt
$
hg status
? myfile.txt$
hg add myfile.txt
$
hg status
A myfile.txt$
hg commit -m 'Added one file'
$
hg status
Après avoir exécuté un hg commit, les fichiers que vous avez ajoutés avant le commit ne seront plus listés dans la sortie de hg status. La raison de ceci est que, par défaut, hg status ne vous montre que les fichiers “intéressants” —ceux que vous avez (par exemple) modifiés, supprimés ou renommés. Si vous avez un dépôt qui contient un millier de fichiers, vous ne voudriez certainement que rarement entendre parler des fichiers que Mercurial suit, mais qui n'ont pas changés. (Vous pouvez quand même avoir cette information, nous y reviendrons plus tard.)
Une fois que vous ajoutez un fichier, Mercurial ne fait rien du tout avec celui-ci immédiatement. Au lieu de ça, il va prendre un "snapshot" de l'état du fichier la prochaine fois que vous exécuterez un commit. Il continuera ensuite à suivre les changements que vous avez fait au fichier chaque fois que vous committerez, et ce, jusqu'à ce que vous supprimiez le fichier.
Un comportement utile que Mercurial possède est que si vous passez le nom d'un répertoire à une commande, toute commande Mercurial la traitera comme : “Je veux opérer sur chaque fichier dans ce répertoire et ses sous-répertoires”.
$
mkdir b
$
echo b > b/somefile.txt
$
echo c > b/source.cpp
$
mkdir b/d
$
echo d > b/d/test.h
$
hg add b
adding b/d/test.h adding b/somefile.txt adding b/source.cpp$
hg commit -m 'Added all files in subdirectory'
Remarquez que dans cet exemple, Mercurial affiche le
nom des fichiers qu'il a ajouté, alors qu'il ne l'a pas fait lorsque
nous avons ajouté le fichier nommé myfile.txt
dans l'exemple précédent.
Ce qu'il se passe est que dans le premier cas, nous avons nommé explicitement le fichier à ajouter sur la ligne de commande. Ce que Mercurial suppose dans ce cas est que nous savons ce que nous faisons, il n'affiche donc rien en sortie.
Cependant, lorsque nous avons implicitement donné les fichiers à l'aide du nom d'un répertoire, Mercurial prend l'initiative d'afficher le nom de chaque fichier avec lequel il fait quelque chose. Ceci clarifie ce qu'il se passe et réduit la probabilité d'une mauvaise surprise restée silencieuse. Ce comportement est commun à la plupart des commandes Mercurial.
Mercurial ne suit pas les informations sur les répertoires. En contrepartie, il suit le chemin vers un fichier. Avant de créer un fichier, il crée au préalable les répertoires manquants dans le chemin. Après avoir supprimé un fichier, il supprime chaque répertoire vide qui apparaît dans le chemin du fichier. Ceci apparaît comme une distinction triviale, cependant, cela a une conséquence pratique mineure : il n'est pas possible de représenter un répertoire totalement vide dans Mercurial.
Les répertoires vides sont rarement utiles. Il existe cependant des solutions alternatives et non intrusives que vous pouvez utiliser pour obtenir l'effet approprié. Les développeurs de Mercurial ont ainsi pensé que la complexité requise pour gérer les répertoires n'était pas aussi importante que le bénéfice que cette fonctionnalité apporterait.
Si vous avez besoin d'un répertoire vide dans votre
dépôt, il existe quelques façons d'y arriver. L'une d'elles est de
créer un répertoire et ensuite, de faire un hg
add sur un fichier “caché” dans ce
répertoire. Sur les systèmes de type Unix, tout fichier dont le nom
commence avec un point (“.
”) est
considéré comme caché par la plupart des commandes et outils
graphiques. Cette approche est illustrée ci-après.
$
hg init hidden-example
$
cd hidden-example
$
mkdir empty
$
touch empty/.hidden
$
hg add empty/.hidden
$
hg commit -m 'Manage an empty-looking directory'
$
ls empty
$
cd ..
$
hg clone hidden-example tmp
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
ls tmp
empty$
ls tmp/empty
Une autre façon de s'attaquer au besoin d'un répertoire vide est de simplement d'en créer un dans vos scripts de construction avant qu'ils n'en aient le besoin.
Une fois que vous décidez qu'un fichier n'appartient
plus à votre dépôt, utilisez la commande hg
remove. Ceci supprime le fichier et informe Mercurial
d'arrêter de le suivre (ce qui prendra effet lors du prochain commit).
Un fichier supprimé est représenté dans la sortie de la commande
hg status par un
“R
”.
$
hg init remove-example
$
cd remove-example
$
echo a > a
$
mkdir b
$
echo b > b/b
$
hg add a b
adding b/b$
hg commit -m 'Small example for file removal'
$
hg remove a
$
hg status
R a$
hg remove b
removing b/b
Après avoir fait un hg remove sur un fichier, Mercurial ne suivra plus aucun changement sur ce fichier, même si vous recréez un fichier avec le même nom dans votre répertoire de travail. Si vous recréez un fichier avec le même nom et que vous désirez que Mercurial suive ce dernier, faite simplement un hg add sur celui-ci. Mercurial saura alors que le nouveau fichier ne fait pas référence à l'ancien fichier qui portait le même nom.
Il est important de comprendre que supprimer un fichier n'a que deux effets.
Supprimer un fichier n'affecte en aucun cas l'historique du fichier.
Si vous mettez à jour le répertoire de travail à un changeset qui a été committé alors que le fichier que vous venez de supprimer était encore suivi, ce fichier réapparaîtra dans le répertoire de travail, avec le contenu qu'il avait lorsque vous aviez committé ce changeset. Si vous mettez à jour (update) le répertoire de travail à un changeset ultérieur dans lequel le fichier a été supprimé, Mercurial supprimera une nouvelle fois le fichier du répertoire de travail.
Mercurial considère qu'un fichier que vous avez
supprimé sans utiliserhg remove
comme étant manquant. Un fichier manquant est
représenté avec un “!
” en sortie de
hg status.
Les commandes Mercurial ne feront rien avec les fichiers
manquants.
$
hg init missing-example
$
cd missing-example
$
echo a > a
$
hg add a
$
hg commit -m 'File about to be missing'
$
rm a
$
hg status
! a
Si votre dépôt contient un fichier que hg status reporte comme manquant, et que
vous voulez que ce fichier reste supprimé, vous pouvez exécuter
hg remove --after
à tout moment
pour dire à Mercurial que vous aviez bien voulu supprimer ce
fichier.
$
hg remove --after a
$
hg status
R a
D'un autre côté, si vous avez supprimé le fichier manquant par accident, donnez à la commande hg revert le nom du fichier à retrouver. Il réapparaitra dans sa forme non modifiée.
$
hg revert a
$
cat a
a$
hg status
Vous pourriez vous demander pourquoi il est nécessaire de dire explicitement à Mercurial que vous souhaitez supprimer un fichier. Au début du développement de Mercurial, celui ci vous laissait pourtant supprimer un fichier sans souci ; Mercurial vous aurait automatiquement informé de l'absence du fichier lorsque vous auriez lancé un hg commit et arrêté de le suivre. En pratique, ceci a montré qu'il était trop facile de supprimer accidentellement un fichier sans le remarquer.
Mercurial offre une commande combinée, hg addremove, qui ajoute les fichiers non suivis et marque les fichiers manquants comme supprimés.
$
hg init addremove-example
$
cd addremove-example
$
echo a > a
$
echo b > b
$
hg addremove
adding a adding b
La commande hg commit
fournit aussi une option -A
qui
exécute le même ajouter-et-supprimer, immédiatement suivi d'un
commit.
$
echo c > c
$
hg commit -A -m 'Commit with addremove'
adding c
Mercurial fournit une commande hg copy qui vous permet de faire une nouvelle copie d'un fichier. Lorsque vous copiez un fichier en utilisant cette commande, Mercurial crée un enregistrement du fait que ce nouveau fichier est une copie du fichier originel. Il traite ces fichiers copiés spécialement lorsque vous fusionnez (merge) votre travail avec quelqu'un d'autre.
Ce qu'il se passe durant une fusion (merge) est que les changements “suivent” une copie. Pour illustrer ce que cela veut dire de la meilleure façon, créons un exemple. Nous allons commencer avec le mini dépôt usuel qui contient un simple fichier.
$
hg init my-copy
$
cd my-copy
$
echo line > file
$
hg add file
$
hg commit -m 'Added a file'
Nous devons faire du travail en parallèle, ainsi, nous aurons quelque chose à fusionner (merge). Donc clonons notre dépôt.
$
cd ..
$
hg clone my-copy your-copy
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
De retour dans notre dépôt initial, utilisons la commande hg copy pour faire une copie du premier fichier que nous avons créé.
$
cd my-copy
$
hg copy file new-file
Si nous regardons ensuite à la sortie de la commande hg status, les fichiers copiés ont l'air de fichiers normalement ajoutés.
$
hg status
A new-file
Mais si nous passons l'option -C
à hg
status, il affiche une autre ligne de sortie : il s'agit
du fichier source pour notre copie.
$
hg status -C
A new-file file$
hg commit -m 'Copied file'
Maintenant, de retour dans le dépôt que nous avons cloné, créons un changement en parallèle. Nous allons ajouter une ligne de contenu au fichier original qui a été créé.
$
cd ../your-copy
$
echo 'new contents' >> file
$
hg commit -m 'Changed file'
Nous avons alors un fichier file
modifié dans ce dépôt. Lorsque nous récupérons (pull) les changements
depuis le premier répertoire et fusionnons (merge) les deux "heads",
Mercurial propagera les changements que nous avons faits localement
au fichier file
dans sa copie
new-file
.
$
hg pull ../my-copy
pulling from ../my-copy 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)$
hg merge
merging file and new-file to new-file 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit)$
cat new-file
line new contents
Ce comportement—des changements d'un fichier qui se propagent aux copies de ce fichier—peut sembler ésotérique, mais, dans la plupart des cas, c'est fortement souhaitable.
Pour commencer, souvenez-vous que cette propagation a lieu seulement lors des fusions (merge). Donc, si vous faites un hg copy sur un fichier, et par la suite modifiez le fichier original durant le cours normal de votre travail, rien n'aura lieu.
La deuxième chose à savoir est que les modifications ne se propageront à travers une copie que si le changeset à partir duquel vous faites une fusion (merge) n'a pas encore vu la copie.
La raison pour laquelle Mercurial fait ainsi est une règle. Imaginons que je corrige un important bug dans un fichier source et que je commit mes changements. Pendant ce temps, vous avez décidé de faire un hg copy du fichier dans votre dépôt, sans rien savoir au sujet du bug ou à propos de la correction. Vous avez alors commencé à "hacker" sur votre copie du fichier.
Si vous aviez récupéré (pull) et fusionné (merge) mes changements, et que Mercurial n'avait pas propagé les changements à travers les copies, votre nouveau fichier source contiendrait maintenant le bug, et à moins que vous ne sachiez qu'il faille propager la correction du bug à la main, le bug aurait subsisté dans votre copie du fichier.
En propageant automatiquement les changements qui fixent les bugs à partir du fichier original vers les copies, Mercurial prévient ce type de problèmes. À ma connaissance, Mercurial est le seul système de gestion de révisions qui propage les changements à travers les copies comme ceci.
Une fois que votre historique des changements a un enregistrement concernant une copie et qu'une fusion postérieure a eu lieu, il n'y a d'habitude pas d'autre besoin de propager les changements du fichier originel vers le fichier copié. C'est pourquoi Mercurial ne propage les changements à travers les copies qu'à la première fusion, et pas après.
Si pour une raison ou une autre, vous décidez que cette fonctionnalité de propager automatiquement les changements à travers les copies n'est pas pour vous, utilisez simplement la commande normale de copie de votre système (sur les systèmes de type Unix, il s'agit de cp) pour faire une copie d'un fichier. Utilisez ensuite hg add pour ajouter les nouveaux fichiers à la main. Cependant, avant d'en faire ainsi, relisez Section 5.3.2, “Pourquoi les changements devraient-ils suivre les copies ?”, et faites un choix en connaissance de cause comme quoi cette fonctionnalité n'est pas appropriée à votre cas spécifique.
Lorsque vous utilisez la commande hg copy, Mercurial crée une copie de chaque fichier source tel qu'il est actuellement dans le répertoire de travail. Cela signifie que si vous effectuez des modifications sur un fichier, puis faites un hg copy sur celui-ci sans avoir au préalable committé ces changements, la nouvelle copie contiendra aussi les modifications que vous avez fait jusqu'à ce point. (Je trouve ce comportement quelque peu contre intuitif, c'est pourquoi j'en fais mention ici.)
La commande hg copy agit comme la commande Unix cp (vous pouvez utilisez l'alias hg cp si vous préférez). Nous devons lui donner deux ou plus arguments où le dernier est considéré comme la destination, et les autres comme les sources.
Si vous passez à hg copy un seul fichier source, et que la destination n'existe pas, ceci créera un nouveau fichier avec ce nom.
$
mkdir k
$
hg copy a k
$
ls k
a
Si la destination est un répertoire, Mercurial copie les sources dans ce répertoire.
$
mkdir d
$
hg copy a b d
$
ls d
a b
La copie de répertoire est récursive et préserve la structure du répertoire source.
$
hg copy z e
copying z/a/c to e/a/c
Si la source et la destination sont tous deux des répertoires, l'arborescence de la source est recréée dans le répertoire destination.
$
hg copy z d
copying z/a/c to d/z/a/c
Comme avec la commande hg
remove, si vous copiez un fichier manuellement et voulez
que Mercurial sache qu'il s'agit d'une copie, utilisez simplement
l'option --after
avec hg copy.
$
cp a n
$
hg copy --after a n
Il est plus commun d'avoir besoin de renommer un fichier que d'en faire une copie. La raison pour laquelle j'ai discuté de la commande hg copy avant de parler de renommage des fichiers est que Mercurial traite les renommages essentiellement comme une copie. Ainsi, savoir comment Mercurial traite les copies de fichiers vous informe sur ce que vous êtes en droit d'attendre lorsque vous renommez un fichier.
Lorsque vous utilisez la commande hg rename, Mercurial crée une copie de tous les fichiers sources, les supprime et marque ces fichiers comme étant supprimés.
$
hg rename a b
La commande hg status montre les nouveaux fichiers comme ajoutés et les fichiers originaux comme supprimés.
$
hg status
A b R a
À cause du hg copy,
nous devons utiliser l'option -C
pour la commande hg status afin
d'observer que le fichier ajouté est bien suivi par Mercurial comme
étant une copie de l'original maintenant supprimé.
$
hg status -C
A b a R a
Comme avec hg remove et
hg copy, vous pouvez informer
Mercurial d'un renommage après coup en utilisant l'option
--after
. Dans la plupart des autres
situations, le comportement de la commande hg
rename, et les options qu'elle accepte sont similaires à la
commande hg copy.
Si vous êtes familier avec la ligne de commande Unix, vous serez heureux d'apprendre que la commande hg rename peut être invoquée par hg mv.
Puisque le “rename” de Mercurial est implanté comme un “copy-and-remove”, la même propagation des changements a lieu après un “rename” qu'après un “copy” lorsque vous fusionnez (merge).
Si je modifie un fichier et que vous le renommez, si ensuite nous fusionnons nos changements respectifs, mes modifications sur le fichier sous son nom originel seront propagés vers le même fichier sous son nouveau nom. (C'est quelque chose que vous pourriez espérer voir “fonctionner simplement”, mais tous les systèmes de gestion de version ne le font pas.)
Tandis qu'avoir des changements qui suivent une copie est une fonctionnalité où vous hocheriez sûrement la tête en disant “oui, cela pourrait être utile”, il est clair que les voir suivre un renommage est sans aucun doute important. Sans cette facilité, il serait vraiment trop facile d'avoir des changements qui deviennent orphelins lorsque des fichiers sont renommés.
Le cas de noms divergeants a lieu lorsque deux
développeurs commencent avec un fichier—appelons le
foo
—dans leurs dépôts respectifs.
$
hg clone orig anne
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
hg clone orig bob
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
Anne renomme le fichier en
bar
.
$
cd anne
$
hg rename foo bar
$
hg ci -m 'Rename foo to bar'
Pendant ce temps, Bob le renomme en
quux
. (Souvenez-vous que hg mv est un alias pour hg rename.)
$
cd ../bob
$
hg mv foo quux
$
hg ci -m 'Rename foo to quux'
J'aime à penser qu'il s'agit d'un conflit puisque chaque développeur a exprimé différentes intentions au sujet de ce que le nom de ce fichier aurait du être.
Que pensez-vous qu'il devrait se produire lorsqu'ils fusionnent (merge) leurs travaux ? Le comportement actuel de Mercurial est qu'il préserve toujours les deux noms lorsqu'il fusionne (merge) des changesets qui contiennent des renommages divergeants.
# See http://www.selenic.com/mercurial/bts/issue455$
cd ../orig
$
hg pull -u ../anne
pulling from ../anne searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files 1 files updated, 0 files merged, 1 files removed, 0 files unresolved$
hg pull ../bob
pulling from ../bob 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)$
hg merge
warning: detected divergent renames of foo to: bar quux 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit)$
ls
bar quux
Remarquez que bien que Mercurial vous avertisse au sujet de la divergeance des renommages, il vous laisse faire quelque chose au sujet de la divergence après la fusion (merge).
Un autre type de conflit de renommage intervient lorsque deux personnes choisissent de renommer différents fichiers source vers la même destination. Dans ce cas, Mercurial exécute la machinerie normale de fusion (merge) et vous guide vers une solution convenable.
Mercurial possède un bug de longue date dans lequel il échoue à traiter une fusion (merge) où un côté a un fichier avec un nom donné, alors que l'autre côté possède un répertoire avec le même nom. Ceci est documenté dans l'issue 29.
$
hg init issue29
$
cd issue29
$
echo a > a
$
hg ci -Ama
adding a$
echo b > b
$
hg ci -Amb
adding b$
hg up 0
0 files updated, 0 files merged, 1 files removed, 0 files unresolved$
mkdir b
$
echo b > b/b
$
hg ci -Amc
adding b/b created new head$
hg merge
abort: Is a directory: /tmp/issue29wS6WTk/issue29/b
Mercurial possède certaines commandes utiles qui vont vous aider à récupérer de certaines erreurs communes.
La commande hg revert vous permet d'annuler les changements que vous avez faits dans votre répertoire de travail. Par exemple, si vous faites un hg add sur un fichier par accident, exécutez juste hg revert avec le nom du fichier que vous avez ajouté et tandis que le fichier ne sera touché d'une quelconque manière, il ne sera plus suivi comme ajouté par Mercurial. Vous pouvez aussi utiliser la commande hg revert pour vous débarrasser de modifications erronées apportées à un fichier.
Il est utile de se souvenir que la commande hg revert est utile pour les modifications qui n'ont pas encore été committées. Une fois que vous avez committé un changement, si vous décidez qu'il s'agissait d'une erreur, vous pouvez toujours faire quelque chose à ce sujet, bien que vos options soient un peu plus limitées.
Pour plus d'informations au sujet de la commande hg revert, et des détails sur comment traiter les modifications que vous avez déjà committées, référez vous à Chapter 9, Finding and fixing mistakes.
Dans des projets compliqués ou conséquents, il n'est pas rare qu'une fusion (merge) de deux changesets finisse par une migraine. Supposez qu'il y ait un gros fichier source qui ait été largement édité de chaque coté de la fusion (merge) : ceci va inévitablement résulter en conflits, dont certains peuvent prendre plusieurs essais pour s'en sortir.
Développons en un cas simple pour voir comment le gérer. Nous allons commencer avec un dépôt contenant un fichier, et le cloner deux fois.
$
hg init conflict
$
cd conflict
$
echo first > myfile.txt
$
hg ci -A -m first
adding myfile.txt$
cd ..
$
hg clone conflict left
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
hg clone conflict right
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
Dans un des clones, nous allons modifier le fichier d'une façon.
$
cd left
$
echo left >> myfile.txt
$
hg ci -m left
Dans un autre, nous allons modifier le fichier différemment.
$
cd ../right
$
echo right >> myfile.txt
$
hg ci -m right
Ensuite, nous allons récupérer (pull) chaque ensemble de changement dans notre dépôt original.
$
cd ../conflict
$
hg pull -u ../left
pulling from ../left searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
hg pull -u ../right
pulling from ../right searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) not updating, since new heads added (run 'hg heads' to see heads, 'hg merge' to merge)
Nous nous attendons à ce que notre dépôt contienne deux "heads".
$
hg heads
changeset: 2:c384aef92f94 tag: tip parent: 0:7b7b4f7a62a5 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:45 2011 +0000 summary: right changeset: 1:5d35cfc5a384 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:45 2011 +0000 summary: left
Normalement, si nous lançons hg
merge à ce point, il nous renverra vers une interface
utilisateur qui nous permettra de résoudre manuellement les éditions
conflictuelles sur le fichier myfile.txt
.
Cependant, pour simplifier ici les choses dans la présentation, nous
aimerions plutôt que la fusion (merge) échoue immédiatement. Voici une
façon de le faire.
$
export HGMERGE=false
Nous avons dit au processus de fusion de Mercurial d'exécuter la commande false (qui échoue immédiatement, à la demande) s'il détecte une fusion (merge) qu'il ne peut pas arranger automatiquement.
Si nous appelons maintenant hg merge, il devrait échouer et reporter une erreur.
$
hg merge
merging myfile.txt merging myfile.txt failed! 0 files updated, 0 files merged, 0 files removed, 1 files unresolved use 'hg resolve' to retry unresolved file merges or 'hg update -C' to abandon
Même si nous ne remarquons pas qu'une fusion (merge) a échoué, Mercurial nous empêchera de committer le résultat d'une fusion ratée.
$
hg commit -m 'Attempt to commit a failed merge'
abort: unresolved merge conflicts (see hg resolve)
Lorsque hg commit échoue dans ce cas, il suggère que nous utilisons la commande peu connue hg resolve. Comme d'habitude, hg help resolve affichera une aide sommaire.
Lorsqu'une fusion intervient, la plupart des fichiers vont, la plupart du temps, rester sans modification. Pour chaque fichier sur lequel Mercurial doit faire quelque chose, il suit l'état de celui-ci.
Si Mercurial voit un fichier quelconque dans un état “unresolved” après une fusion (merge), il considère que la fusion (merge) a échoué. Heureusement, nous n'avons pas à recommencer la procédure à partir du début.
L'option --list
ou -l
passée à hg resolve liste l'état de chaque fichier
fusionné (merge).
$
hg resolve -l
U myfile.txt
En sortie de hg
resolve, un fichier “resolved” est marqué avec un
R
, alors qu'un fichier “unresolved” est marqué
d'un U
. S'il existe un fichier listé avec un
U
, nous savons qu'essayer de committer le résultat
de la fusion (merge) échouera.
Nous avons plusieurs options pour changer l'état d'un
fichier de “unresolved” à “resolved”.
Le plus habituel est de relancer
hg resolve. Si nous passons les noms
des fichiers individuels ou des répertoires, ceci retentera la fusion
de tous les fichiers présents à cet endroit. Nous pouvons aussi
passer l'option --all
ou
-a
qui tentera de fusionner
tous les fichiers “unresolved”.
Mercurial nous laisse aussi modifier la résolution
d'un fichier directement. Nous pouvons marquer un fichier “resolved”
en utilisant l'option --mark
,
ou “unresolved” en utilisant l'option --unmark
. Ceci nous autorise à
nettoyer une fusion particulièrement compliquée à la main, et de
garder un suivi de nos progrès avec chaque fichier pendant que nous
avançons.
La sortie par défaut de la commande hg diff est compatible rétrospectivement avec la commande régulière diff, mais ceci a quelques inconvénients.
Considérez le cas où nous utilisons hg rename pour renommer un fichier.
$
hg rename a b
$
hg diff
diff -r 1cb203eac078 a --- a/a Thu Mar 17 05:08:44 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -a diff -r 1cb203eac078 b --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/b Thu Mar 17 05:08:44 2011 +0000 @@ -0,0 +1,1 @@ +a
La sortie de hg diff
ci-dessus cache le fait que nous avons simplement renommé un fichier.
La commande hg diff accepte l'option
--git
ou -g
pour utiliser un nouveau
format de diff qui montre ces informations sous une forme plus
expressive.
$
hg diff -g
diff --git a/a b/b rename from a rename to b
Cette option peut aussi aider avec le cas qui peut être autrement perturbant : un fichier qui apparaît comme étant modifié en accord avec hg status, mais où hg diff n'affiche rien. Cette situation peut survenir si nous changeons les permissions d'exécution du fichier.
$
chmod +x a
$
hg st
M a$
hg diff
La commande normale diff ne fait pas
attention aux permissions des fichiers, ce qui explique pourquoi
hg diff n'affiche rien du tout par
défaut. Si nous lui passons l'option -g
, ceci nous
informe de ce qui s'est vraiment passé.
$
hg diff -g
diff --git a/a b/a old mode 100644 new mode 100755
Les systèmes de gestion de révisions sont en général meilleurs pour gérer les fichiers textes qui sont écrits par les humains, comme le code source, où les fichiers ne changent pas énormément d'une révision à l'autre. Certains systèmes de gestion de révisions centralisés peuvent aussi traiter très convenablement les fichiers binaires, tels que les images bitmap.
Par exemple, une équipe de développement de jeux va probablement gérer les deux types : ses codes source et tous ses binaires (ex. données géométriques, textures, schémas de cartes) dans un système de contrôle de révisions.
Puisqu'il est d'habitude impossible de fusionner (merge) deux modifications conflictuelles sur un fichier binaire, les systèmes de version centralisés offrent souvent un mécanisme de verrou (lock) qui permet à un utilisateur de dire “Je suis la seule personne qui peut éditer ce fichier”.
En comparaison avec un système centralisé, un système décentralisé de gestion de révision change certains facteurs qui guident les décisions sur quels fichiers gérer et comment.
Par exemple, un système distribué de gestion de révisions ne peut pas, par sa nature, offrir un système de verrou (lock) sur les fichiers. Il n'y a donc pas de mécanisme inclus pour empêcher deux personnes de faire des modifications conflictuelles sur un fichier binaire. Si vous avez une équipe où plusieurs personnes peuvent souvent éditer un fichier binaire, cela ne serait pas une très bonne idée d'utiliser Mercurial —ou tout autre système distribué de gestion de révisions—pour gérer ces fichiers.
Lorsque vous sauvegardez les modifications sur un fichier, Mercurial ne sauvegarde d'habitude que les différences entre la version précédente et la version actuelle d'un fichier. Pour la plupart des fichiers texte, ceci est très efficace. Cependant, certains fichiers (en particulier les fichiers binaires) sont construits d'une façon que même un petit changement sur un contenu logique résulte sur un changement de la plupart des octets du fichier. Par exemple, les fichiers compressés sont particulièrement sujets à ce comportement. Si les différences entre deux versions successives d'un fichier sont toujours très grandes, Mercurial ne sera pas capable de sauvegarder l'historique des révisions sur le fichier très efficacement. Ceci peut affecter aussi bien les besoins pour la sauvegarde locale que le temps nécessaire à cloner le dépôt.
Pour avoir une idée de comment ceci pourrait vous affecter en pratique, supposez que nous voulions que Mercurial gère des documents OpenOffice. OpenOffice sauvegarde les documents sur le disque comme des fichiers compressés zip. Même le fait d'éditer ces fichiers d'une seule lettre, changera les bits de la quasi totalité du fichier lorsque vous le sauvegarderez. Maintenant, supposez que ce fichier fasse une taille de 2 Mo. Puisque la plupart du fichier change à chaque fois que vous sauvegardez, Mercurial aura à sauvegarder tous les 2 Mo du fichier à chaque commit, alors que de votre point de vue, il n'y a que peu de mots qui changent à chaque fois. Un seul fichier souvent édité qui n'est pas bien traité par les hypothèses que Mercurial fait sur les sauvegardes peut facilement avoir un effet colossal sur la taille du dépôt.
Même pire, si vous et quelqu'un d'autre éditez le même document OpenOffice sur lequel vous travaillez, il n'y a pas de façon utile pour fusionner votre travail. En fait, il n'y a pas de moyen utile de montrer que les différences sont faites à partir de votre vision des modifications.
Il y a ainsi quelques recommandations claires sur les types de fichiers spécifiques avec lesquels faire très attention.
Les fichier qui sont très gros et incompressibles, comme les images ISO de CD-ROM, sont, par construction très gros et les cloner à travers un réseau sera très long.
Les fichiers qui changent beaucoup d'une révision à l'autre peuvent être très coûteux à sauvegarder si vous les éditez fréquemment, de même que les conflits entre deux éditions concurrentes peuvent être difficiles à résoudre.
Puisque Mercurial maintient une copie complète de l'historique de chaque clone, toute personne qui utilise Mercurial pour collaborer à un projet peut potentiellement agir comme une source de sauvegarde si une catastrophe survenait. Si un dépôt central devient indisponible, vous pouvez construire un remplaçant en clonant une copie du dépôt à partir d'un des contributeurs en récupérant (pull) tous les changements qui n'auraient pas été vus par les autres.
Il est simple d'utiliser Mercurial pour construire des serveurs hors site de sauvegarde et des miroirs distants. Initiez une tâche périodique (ex. via la commande cron) sur un serveur distant pour récupérer (pull) les changements de votre dépôt distant chaque heure. Ceci sera difficile seulement dans le cas improbable où le nombre des dépôts maîtres que vous maintenez change souvent, auquel cas vous aurez besoin de faire un peu de scripting pour rafraichir la liste des dépôts à sauvegarder.
Si vous exécutez des sauvegardes traditionnelles de
votre dépôt maître sur bande ou disque, et que vous voulez sauvegarder
un dépôt nommé myrepo
, utilisez la commande
hg clone -U myrepo myrepo.bak pour créer un clone de
myrepo
avant de commencer vos backups.
L'option -U
ne crée pas de répertoire de travail après
que le clone soit accompli, puisque ceci serait superflu et ferait que
la sauvegarde prenne plus de temps.
Si vous voulez ensuite sauvegarder
myrepo.bak
au lieu de myrepo
,
vous aurez la garantie d'avoir une image (snapshot) cohérente de
votre dépôt sur lequel un développeur insomniaque n'enverra (push) pas de
changements en milieu de sauvegarde.
Table of Contents
Comme tout outil complètement décentralisé, Mercurial n'impose pas de politique sur la façon dont les personnes devraient travailler ensemble. Cependant, si vous êtes nouveau dans les systèmes de gestion de révisions distribués, cela aide d'avoir des outils et exemples en tête lorsque vous réfléchissez à de possibles modèles de workflow.
Mercurial possède une interface web puissante qui propose plusieurs fonctions utiles.
Pour une utilisation intensive, l'interface web vous permet de naviguer dans un ou une collection de dépôt. Vous pouvez voir l'historique d'un dépôt, examiner chaque modification (commentaires et "diffs"), et voir le contenu de chaque répertoire et fichier. Vous pouvez même accéder à une vue de l'historique qui vous donne une vue graphique de la relation entre les modifications individuelles et les fusions (merge).
De plus, pour l'utilisation humaine, l'interface web fournit des flux Atom et RSS des changements dans un dépôt. Ceci vous permet de “souscrire” à un dépôt en utilisant votre lecteur de flux favori, et être automatiquement avertis de l'activité dans ce dépôt aussi tôt qu'elle change. Je trouve cette fonctionnalité bien plus commode que le modèle qui consiste à souscrire à une mailing list à laquelle les avertissements sont envoyés, puisque cela demande aucune configuration supplémentaire de la part de la personne qui publie un dépôt.
L'interface web permet aussi aux utilisateurs distants de cloner un dépôt, récupérer (pull) les changement à partir de celui ci, et (lorsque le serveur est configuré pour l'autoriser) lui envoyer (push) des changements. Le protocole de tunnel HTTP de Mercurial compresse agressivement les données, ainsi, il fonctionne efficacement, même au-dessus des réseaux avec une faible bande passante.
La plus simple façon de démarrer avec l'interface utilisateur est d'utiliser votre navigateur web pour visiter un dépôt existant, tel que le dépôt principal de Mercurial à l'adresse http://www.selenic.com/repo/hg.
Si vous êtes intéressés pour proposer une interface web de vos propres dépôts, il y a plusieurs façons de le faire.
La façon la plus simple et la plus rapide pour commencer dans un environnement informel est d'utiliser la commande hg serve qui est la plus adaptée à un service à court terme et “léger”. Référez-vous à Section 6.4, “Partage informel avec hg serve” plus bas pour les détails d'utilisation de cette commande.
Pour des dépôts dont la durée de vie est plus longue, où vous voudriez un service accessible en permanence, il existe plusieurs services publics d'hébergement qui sont accessibles. Certains sont libres et gratuits pour les projets Open Source, alors que d'autres offrent un hébergement commercial et payant. Une liste à jour est disponible à l'adresse : http://www.selenic.com/mercurial/wiki/index.cgi/MercurialHosting.
Si vous préférez héberger vos propres dépôts, Mercurial possède un support intégré pour plusieurs technologies populaires d'hébergement, plus particulièrement CGI (Common Gateway Interface) et WSGI (Web Services Gateway Interface). Référez-vous à Section 6.6, “Service sur HTTP grâce à CGI” pour des détails sur la configuration CGI et WSGI.
Avec un outil convenablement flexible, prendre des décisions sur les workflows est plus un problème d'ingénierie sociale qu'un problème technique. Mercurial impose peu de limitations sur la façon dont vous pouvez structurer le flux de travail dans un projet, donc, c'est à vous et votre groupe de fixer et vivre avec un modèle qui convient à vos besoins particuliers.
L'aspect le plus important de tout modèle que vous devez garder en tête est la façon dont il subvient aux besoins et capacités des personnes qui l'utiliseront. Ceci pourrait sembler évident en soi ; pourtant, vous ne pouvez pas vous permettre de l'oublier à un seul moment.
Une fois, j'ai mis en place un modèle de workflow qui m'apparaissait comme parfait, mais il a causé la consternation et des conflits au sein de mon équipe de développement. En dépit de mes tentatives pour expliquer pourquoi nous avions besoin d'un ensemble complexe de branches, et comment les changements devaient couler entre eux, certains membres de l'équipe se révoltèrent. Alors qu'ils étaient pourtant des personnes sympathiques, ils ne voulaient pas prêter attention aux contraintes sur lesquelles nous étions en train d'opérer, ou, face aux conséquences de ces contraintes dans les détails du modèle que je préconisais.
Ne balayez pas les problèmes sociaux ou techniques de la main. Quelque soit le schéma que vous établirez, vous devriez planifier un protocole pour prévenir, ou rapidement vous relever de troubles que vous pouvez anticiper. Par exemple, si vous vous attendez à avoir une branche pour les changements pas-pour-release, vous devriez penser très tôt à la possibilité qu'une personne fusionne (merge) accidentellement ces changements avec une branche de release. Vous pouvez empécher ce problème particulier en écrivant un hook qui prévient les changements d'être fusionnés à partir d'une branche inopportune.
Je ne voudrais pas suggérer qu'une approche “tout peut arriver” comme quelque chose de durable, mais il s'agit d'un modèle qui est simple à saisir et qui fonctionne parfaitement dans quelques situations inhabituelles.
Par exemple, beaucoup de projets ont un groupe distant de collaborateurs qui ne se rencontre physiquement que très rarement. Certains groupes aiment vaincre l'isolation du travail à distance en organisant occasionnellement des “sprints”. Dans un sprint, des personnes viennent ensemble dans un même endroit (la salle de conférence d'une société, la salle de réunion d'un hôtel, ce genre d'endroit) et y passent plusieurs jours, plus ou moins enfermés, et hackant intensément sur une poignée de projets.
Un "sprint" ou une session de "hacking" dans un café sont les endroits parfaits pour utiliser la commande hg serve puisque hg serve n'a pas besoin d'une infrastructure extraordinaire de serveurs. Vous pouvez commencer avec la commande hg serve en quelques instants, en lisant Section 6.4, “Partage informel avec hg serve” plus bas Ensuite, dites simplement à la personne à côté de vous que vous exécutez un serveur, envoyez-lui l'URL par un message instantané, et vous avez immédiatement un moyen simple et rapide de travailler ensemble. Ils peuvent taper votre URL dans leur navigateur web et rapidement revoir vos changements ; ou ils peuvent récupérer chez vous un bugfix et le vérifier ; ou ils peuvent cloner une branche contenant une nouvelle fonctionnalité et la tester.
Le charme et le problème en faisant les choses ainsi, dans un mode ad-hoc est que seules les personnes qui sont au courant de vos changements, et de leur emplacement, peuvent les voir. Une telle approche informelle ne passe simplement pas à l'échelle au delà d'une poignée de personnes, puisque chacun a besoin de connaître n différents dépôts à partir des quels récupérer les changements (pull).
Pour de plus petits projets qui migrent depuis un outil de gestion de révision centralisé, la façon la plus simple de commencer est certainement d'avoir un flux de changement à partir d'un unique dépôt central. Il s'agit aussi du “composant” pour des schémas de workflow plus ambitieux.
Les contributeurs commencent par cloner une copie de ce dépôt. Ils peuvent récupérer les changements à n'importe quel moment où ils en ressentent le besoin, et certains (sûrement tous) développeurs ont les persmissions qui leur permettent d'envoyer leurs modifications (push) en retour lorsqu'elles sont prêtes pour que les autres personnes puissent les voir.
Dans ce modèle, il peut encore être sensé pour les gens de récupérer les changements directement entre eux, sans passer par le dépôt central. Considérez le cas où j'ai un bug fix provisoire, mais je m'inquiète de savoir si, dans le cas où je le publiais, cela ne casserait pas l'arbre des autres contributeurs s'ils la récupèreraient. Pour réduire les dommages potentiels, je peux vous demander de cloner mon dépôt dans un dépôt temporaire qui vous appartient et de le tester. Ceci nous permet de ne pas publier les modification potentiellement dangereuses tant qu'elles n'ont pas encore été un peu testées.
Si une équipe héberge son propre dépôt dans ce type de scénario, les personnes qui utilisent habituellement le protocole ssh pour envoyer (push) en toute sécurité leurs changements au dépôt central, comme docummenté dans Section 6.5, “Utiliser le protocole Secure Shell (ssh)”. Il est aussi usuel de publier une copie en lecture seule du dépôt sur HTTP comme dans Section 6.6, “Service sur HTTP grâce à CGI”. Publier sur HTTP satisfait le besoin des personnes qui n'ont pas d'accès en écriture, et ceux qui veulent utiliser leur navigateur web pour explorer l'historique du dépôt.
Une chose magnifique au sujet des services d'hébergement comme Bitbucket est qu'ils ne gèrent pas uniquement les détails minutieux de la configuration du serveur, tels que les comptes utilisateurs, l'authentification, les protocoles sécurisés, ils fournissent aussi une infrastructure additionnelle pour que ce modèle fonctionne bien.
Par exemple, un service d'hébergement bien conçu laissera les personnes cloner leurs copies d'un dépôt à l'aide d'un simple clic. Ceci laisse les personnes travailler dans des espaces séparés et partager leurs changements lorsqu'ils sont prêts.
De plus, un bon service d'hébergement laissera les personnes communiquer ensemble, par exemple pour dire “Il y a des changements prêts pour toi pour relecture dans cet arbre”.
Les projets d'une taille significative ont tendance à avancer sur plusieurs fronts en même temps. Dans le cas de logiciel, il est commun qu'un projet sorte périodiquement des releases officielles. Une release devrait ensuite aller dans le “mode de maintenance” pour un moment après sa première publication ; les releases de maintenance tendent à contenir seulement des corrections de bugs, et non de nouvelles fonctionnalités. En parallèle de ces releases de maintenance, une ou plusieurs futures releases doivent être en cours de développement. Les gens utilisent en général le mot “branche” pour référer à l'une de ces nombreuses directions légèrement différentes dans lesquelles le développement évolue.
Mercurial est particulièrement bien adapté pour gérer plusieurs branches simultanées mais non identiques. Chaque “direction de développement” peut vivre dans son propre dépôt central, et vous pouvez récupérez les changements de l'un ou l'autre lorsque le besoin s'en fait sentir. Parce que les dépôts sont indépendant les uns des autres, les modifications instables dans une branche de développement n'affecteront jamais une branche stable, sauf si quelqu'un fusionne (merge) explicitement ces changements dans la branche stable.
Voici un exemple sur comment cela peut se passer en pratique. Disons que vous avez une “branche principale” sur un serveur central.
$
hg init main
$
cd main
$
echo 'This is a boring feature.' > myfile
$
hg commit -A -m 'We have reached an important milestone!'
adding myfile
Les contributeurs le clonent, y apportent localement des modifications, les testent et envoient (push) en retour leurs changements.
Une fois que la branche principale atteint une étape assez importante pour une release, vous pouvez utiliser la commande hg tag pour donner un nom permanent à cette étape de révision.
$
hg tag v1.0
$
hg tip
changeset: 1:33e75fb73c66 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:41 2011 +0000 summary: Added tag v1.0 for changeset 73e86f09043d$
hg tags
tip 1:33e75fb73c66 v1.0 0:73e86f09043d
Disons que du developpement continue sur la branche principale.
$
cd ../main
$
echo 'This is exciting and new!' >> myfile
$
hg commit -m 'Add a new feature'
$
cat myfile
This is a boring feature. This is exciting and new!
En utilisant le tag enregistré à l'étape importante, les gens qui clonent ce dépôt peuvent à tout moment dans le futur utiliser la commande hg update pour avoir une copie du répertoire de travail exactement comme il était lorsque cette révision "tag" a été committée.
$
cd ..
$
hg clone -U main main-old
$
cd main-old
$
hg update v1.0
1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cat myfile
This is a boring feature.
De plus, immédiatement après que la branche principale soit taggée, nous pouvons maintenant cloner la branche principale sur le serveur vers une nouvelle branche “stable” sur le même serveur.
$
cd ..
$
hg clone -rv1.0 main stable
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
Si nous avons besoin d'effectuer des modifications à la branche stable, nous pouvons alors cloner ce dépôt, effectuer nos modifications, committer, et envoyer nos changements en retour là bas.
$
hg clone stable stable-fix
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd stable-fix
$
echo 'This is a fix to a boring feature.' > myfile
$
hg commit -m 'Fix a bug'
$
hg push
pushing to /tmp/branchingeS3nOt/stable searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files
Puisque les dépôts Mercurial sont indépendants, et que Mercurial ne déplace pas les changements automatiquement, les branches stable et principale sont isolées l'une de l'autre. Les changements qui sont faits à la branche principale ne “fuient” pas vers la branche stable, et vice versa.
Nous allons souvent avoir envie que toutes nos correction de bugs sur la branche stable soient reportées sur la branche principale. Plutôt que de réécrire une correction de bug pour la branche principale, nous pouvons simplement récupérer (pull) et fusionner (merge) les changements de la branche stable vers la branche principal, et Mercurial se débrouillera pour rapporter ces corrections de bugs pour nous.
$
cd ../main
$
hg pull ../stable
pulling from ../stable 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)$
hg merge
merging myfile 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit)$
hg commit -m 'Bring in bugfix from stable branch'
$
cat myfile
This is a fix to a boring feature. This is exciting and new!
La branche principale contiendra toujours des changements qui ne sont pas dans la branche stable, mais elle contiendra aussi les corrections de bugs de la branche stable. La branche stable restera non affectée par ces changements, tant qu'ils coulent de la branche stable vers la branche principale, et non dans l'autre sens.
Pour de plus gros projets, une façon efficace de gérer les changements est de diviser l'équipe en plus petits groupes. Chaque groupe a une branche partagée qui lui est attitrée, clonée à partir d'une unique branche “principale” utilisée pour le projet entier. Les personnes travaillant sur une branche individuelle sont typiquement isolées des développements sur les autres branches.
Lorsqu'une fonctionnalité particulière est réputée pour être dans une forme adaptée, quelqu'un de l'équipe qui s'en occupe récupère les changements (pull) à partir de la branche principale vers la branche de cette fonctionnalité, fusionne (merge) et renvoie (push) le tout vers la branche principale.
Certains projets sont organisés comme un “train” élémentaire : une release est planifiée tous les quelques mois, et, toutes les fonctionnalités disponibles lorsque le “train” est prêt à s'arrêter sont autorisées ici.
Ce modèle ressemble à travailler avec des branches de fonctionnalités. La différence est que lorsqu'une branche de fonctionnalité rate le train, quelqu'un de l'équipe qui travaille sur cette fonctionnalité récupère (pull) et fusionne (merge) ce qui a été ajouté à la release du train dans la branche de la fonctionnalité, puis, l'équipe continue son travail au-dessus de cette release afin que leur fonctionnalité puisse être ajoutée à la prochaine release.
Le développement du noyau Linux est doté d'une structure hiérarchique superficielle, entourée par un nuage de chaos apparent. Parce que la plupart des développeurs Linux utilisent git, un outil distribué de gestion de révisions avec des capacités similaires à celles de Mercurial, il est utile de décrire comment le travail se déroule dans cet environnement ; si vous aimez ces idées, l'approche se traduit correctement à travers les outils.
Au centre de la communauté siège Linus Torvalds, le créateur de Linux. Il publie un dépôt unique de sources qui est considéré comme faisant “autorité” sur l'arborescence par la communauté entière de développeurs. Tout le monde peut cloner l'arbre de Linus, mais il ne récupère (pull) pas les changements de n'importe quelle arborescence.
Linus a plusieurs “lieutenants de confiance”. Comme règle générale, il récupère (pull) tous les changements qu'ils publient, dans la plupart des cas sans même relire ces modifications. Certains de ces lieutenants sont généralement autorisés à être “mainteneurs”, responsables pour un sous-système spécifique du noyau. Si un hacker du noyau veut apporter des modification au sous-système qu'il veut voir intégré à l'arbre de Linus, il doit trouver le mainteneur du sous-système, et lui demander de récupérer ses changements. Si le mainteneur relit ses changements et les accepte, ils seront transmis à Linus le moment venu.
Les lieutenants individuels ont leur propre approche pour relire, accepter et publier les changements ; et pour décider quand les apporter à Linus. De plus, il y a plusieurs branches connues que les personnes utilisent pour différentes choses. Par exemple, quelques personnes maintiennent des dépôts “stables” de leurs versions du noyau, pour lesquels ils apportent des corrections critiques lorsque nécessaire. Certains mainteneurs publient plusieurs arbres : l'un pour les changements expérimentaux, l'un pour les changements qu'ils vont faire remonter, etc. D'autres ne publient qu'un unique arbre.
Ce modèle a deux caractéristiques remarquables. La première est qu'il s'agit de “pull seulement”. Vous devez demander, convaincre, ou mendier auprès d'un autre développeur pour prendre vos modifications, puisqu'il n'y a vraisemblablement pas d'arbre où plus d'une personne peut envoyer des changements (push), et qu'il n'y a pas de possibilité d'envoyer des changements (push) vers un arbre que quelqu'un d'autre contrôle.
La seconde est que c'est basé sur la réputation et l'acclamation. Si vous êtes un inconnu, Linus va probablement ignorer vos changements sans même répondre. Cependant, un mainteneur de sous-système les relira probablement, et les acceptera sûrement s'ils passent ses critères d'acceptation. Plus vous enverrez du “bon” code à un mainteneur, et plus celui-ci aura confiance en votre jugement et acceptera vos changements. Si vous êtes bien connu et maintenez une branche ancienne pour quelque chose que Linus n'a pas encore accepté, les gens avec un intérêt similaire devraient récupérer vos changements régulièrement pour rester à jour vis-à-vis de votre travail.
La réputation et l'acclamation ne nécessite pas de système croisé ou de limites “entre les gens”. Si vous êtes respectés mais que vous êtes un hacker spécialisé dans la sauvegarde, et que vous tentez de corriger un bug réseau, ce changement recevra un examen approfondi de la part du mainteneur responsable du réseau comparable à celui d'un total étranger.
Pour les personnes qui viennent d'un projet dont le milieu est plus ordonné, le processus chaotique de développement du noyau Linux en comparaison apparaît souvent totalement dément. C'est sujet aux caprices d'individus ; des personnes font des changements considérables quand ils les jugent appropriés ; et l'allure du développement est ahurissante. Et pourtant, Linux est un bout de logiciel d'une grande réussite et bien considéré.
Une source perpétuelle de heurts dans la communauté opensource est de savoir si un modèle de développement où les personnes ne peuvent que récupérer (pull) les changements d'autres est “meilleur” que celui dans lequel de multiples personnes peuvent envoyer (push) leurs changements vers un dépôt partagé.
Typiquement, les partisans du modèle push partagé utilisent des outils qui renforcent activement cette approche. Si vous utilisez un outil centralisé de gestion de révision comme Subversion, il n'y a pas la possibilité de choisir quel modèle utiliser : l'outil vous fournit un push partagé, et si vous voulez faire quelque chose d'autre, vous avez à changer votre propre approche à la base (comme appliquer les patchs manuellement).
Un bon outil de gestion distribuée de révisions doit supporter les deux modèles. Vous et vos collaborateurs pouvez ensuite structurer la façon dont vous travaillez ensemble en vous basant sur vos besoins et vos préférences, et non sur les contorsions que vos outils vous forcent à effectuer.
Lorsque vous et votre équipe configurez des dépôts partagés et commencez à propager vos changement dans tous les sens entre les dépôts locaux et partagés, vous commencez à être face à un défi connexe, mais un peu différent : celui de gérer les multiples directions vers lesquelles votre équipe pourrait aller au même moment. Même si ce sujet est intimement lié à la façon dont votre équipe collabore, il est suffisement dense pour mériter un traitement à part dans Chapter 8, Managing releases and branchy development.
Le reste de ce chapitre est consacré à la question du partage des changements avec vos collaborateurs.
La commande hg serve de Mercurial est magnifiquement conçue pour un environnement de petit groupe, soudé et rapide. Elle fournit aussi un très bon moyen d'avoir un sentiment de l'utilisation des commandes Meruciral sur un réseau.
Exécutez hg serve à
l'intérieur d'un dépôt et en moins d'une seconde, cela mettra en place
un serveur HTTP spécialisé ; qui va accepter les connexions de tout
client, et servir les données pour ce dépôt jusqu'à ce que vous
l'arrêtiez. Toute personne qui connaît l'URL du serveur que vous venez
de démarrer, peut ensuite utiliser un navigateur web ou Mercurial pour
lire les données de ce dépôt. Une URL pour une instance exécutée de
hg serve sur un ordinateur portable
ressemblera vraisemblablement à
http://my-laptop.local:8000/
.
La commande hg serve n'est pas un serveur web générique. Il ne peut faire que deux choses :
En particulier, hg serve ne permettra pas aux utilisateurs distants de modifier votre dépôt. C'est destiné à une utilisation en lecture seule.
Si vous commencez avec Mercurial, il n'y a rien qui vous empêche d'utiliser hg serve pour publier un dépôt sur votre ordinateur, utilisez ensuite des commandes telles que hg clone, hg incoming, et ainsi de suite pour parler à ce serveur comme si ce dépôt était hébergé à distance. Ceci peut vous aider à rapidement familiarisé avec les commandes sur les dépôts hébergés sur un réseau.
Puisque il fournit un accès en lecture sans authentification à tous les clients, vous devriez utiliser la commande hg serve dans un environnement où vous ne vous inquiétez pas ou vous avez tout contrôle sur qui peut avoir accès au réseau et récupérer les données de votre dépôt.
La commande hg serve ne sait rien sur un quelconque firewall que vous auriez installé sur votre système ou réseau. Elle ne peut pas détecter ou contrôler votre logiciel de pare-feu. Si d'autre personnes ont la possibilité de dialoguer avec une instance de hg serve la seconde chose que vous devriez faire (après être sûr qu'ils utilisent l'URL correcte) est de vérifier la configuration de votre firewall.
Par défaut, hg serve
écoute pour les connexions entrantes sur le port 8000. Si un autre
processus est déjà en train d'écouter sur le port que vous voulez
écouter, vous pouvez spécifier un port différent sur lequel écouter à
l'aide de l'option -p
.
Normalement, lorsque hg
serve se lance, il n'affiche aucune sortie, ce qui peut
être un peu énervant. Si vous voulez une confirmation que tout s'est
déroulé correctement, et connaître l'URL que vous devriez fournir à
vos collaborateurs, démarrez avec l'option -v
.
Vous pouvez récupérer (pull) ou envoyer (push) des
changements de façon sécurisé au dessus d'une connexion utilisant le
protocole Secure Shell (ssh
). Pour l'utiliser avec
succès, vous pourriez avoir à faire un peu de configuration du côté
client ou serveur.
Si vous n'êtes pas familiers avec ssh, c'est le nom de la commande et d'un protocole réseau qui vous permet d'établir une communication sécurisée avec un autre ordinateur. Pour l'utiliser avec Mercurial, vous allez configurer un ou plusieurs comptes utilisateurs sur un serveur, comme ça, les utilisateurs distants peuvent se connecter et exécuter les commandes.
(Si vous êtes familiers avec ssh, vous allez probablement trouver quelques-unes des informations qui suivent élémentaires par nature.)
Une URL ssh a tendance à ressembler à ceci :
ssh://bos@hg.serpentine.com:22/hg/hgbook
Le préfixe
“ssh://
” dit à Mercurial
d'utiliser le protocole ssh.
Le composant
“bos@
” indique que le nom
d'utilisateur à connecter sur le serveur. Vous pouvez le laisser
vide si le nom d'utilisateur sur le serveur distant est le même
que localement.
La partie
“hg.serpentine.com
” donne le nom
d'hôte du serveur sur lequel se connecter.
Le “:22” identifie le numéro de port où se connecter au serveur. Le port par défaut est 22, donc vous avez besoin de spécifier ceci que si vous n'utilisez pas le port 22.
Le reste de l'URL est le chemin local du dépôt sur le serveur.
Il y a beaucoup de risque de confusion sur le chemin du composant d'une URL ssh puisqu'il n'y a pas de façon standard pour les outils de l'interpréter. Certains programmes se comportent différemment des autres lorsqu'ils traitent ces chemins. Il ne s'agit pas d'une situation idéale, mais ce n'est pas prêt de changer. Lisez les prochains paragraphes avec attention.
Mercurial traite le chemin vers un dépôt sur le
serveur comme relatif au répertoire personnel de l'utilisateur sur le
serveur distant. Par exemple, si un utilisateur
foo
sur le serveur a un répertoire personnel
/home/foo
, alors l'URL ssh qui
contient un composant chemin de bar
réfère en
réalité au répertoire /home/foo/bar
.
Si vous voulez spécifier un chemin relatif au
répertoire personnel d'un autre utilisateur, vous pouvez préciser un
chemin qui commence à l'aide du caractère tilde suivi du nom de
l'utilisateur (appelons le otheruser
),
ainsi.
ssh://server/~otheruser/hg/repo
Et si vous voulez vraiment spécifier un chemin absolu sur le serveur, débutez le composant chemin par deux slashs comme dans cet exemple.
ssh://server//absolute/path
La plupart des systèmes du type Unix arrivent avec
OpenSSH préinstallé. Si vous utilisez un tel système, utilisez
which ssh
pour trouver où la commande
ssh est installée (il s'agit généralement de
/usr/bin
). Dans le cas peu
probable où il ne serait pas présent, regarder la documentation de
votre système pour voir comment l'installer.
Sous Windows, le paquet TortoiseHg est livré avec une version de l'excellente commande plink de Simon Tatham, et ne devrait pas avoir besoin de plus de configuration.
Pour éviter d'avoir à chaque fois taper un mot de passe lorsque vous utilisez votre client ssh, je recommande de créer une paire de clefs.
Sur un système de type Unix, la commande ssh-keygen fera l'affaire.
Sous Windows, si vous utilisez TortoiseHg, vous devriez avoir besoin de télécharger la commande nommée puttygen à partir du site web de PuTTY pour créer une paire de clefs. Référez-vous à la documentation puttygen pour les détails sur l'utilisation de cette commande.
Lorsque vous créez une paire de clefs, il est habituellement hautement recommandé de la protéger avec un mot de passe. (Le seul moment où vous pourriez ne pas devoir le faire est lorsque vous utilisez le protocole ssh pour des tâches automatisées sur un réseau sécurisé.)
Le simple fait de créer une paire de clefs n'est
cependant pas suffisant. Vous aurez besoin d'ajouter la clef publique
à l'ensemble des clefs autorisées pour tout utilisateur que vous
utilisez pour vous connecter à distance. Pour les serveurs utilisant
OpenSSh (la grande majorité), ceci voudra dire d'ajouter la clef
publique à la liste dans un fichier appelé authorized_keys
dans leur répertoire
.ssh
.
Sur un système de type Unix, votre clef publique aura
l'extension .pub
. Si vous utilisez la commande
puttygen sous Windows, vous pouvez sauvegarder la
clef publique dans un fichier que vous choisissez ou la copier à
partir de la fenêtre qui apparait directement dans le fichier
authorized_keys
.
Un agent d'authentification est un démon qui enregistre les mots de passe en mémoire (il oublira ainsi les mots de passe si vous vous déconnectez et reconnectez). Un client ssh sera averti si un tel agent est en fonctionnement, et lui demandera un mot de passe. S'il n'y a pas d'agent en fonctionnement, ou si l'agent ne connaît pas le mot de passe nécessaire, vous aurez à taper votre mot de passe chaque fois que Mercurial tente de communiquer avec un serveur en votre nom (ex. lorsque vous faite un pull ou un push de changements).
L'inconvénient de sauvegarder les mots de passe dans un agent est qu'il est possible pour un attaquant bien préparé de retrouver le mot de passe clair, dans certains cas, même si votre système a été redémarré. Vous devriez vous faire votre propre jugement pour savoir si ce risque est acceptable. Ceci vous exempte certainement d'un tas de répétitions.
Sur les systèmes de type Unix, l'agent est appelé ssh-agent, et est souvent lancé automatiquement pour vous lorsque vous vous connectez. Vous aurez besoin d'utiliser la commande ssh-add pour ajouter des mots de passe à l'entrepôt de l'agent.
Sous Windows, si vous utilisez TortoiseHg, la commande pageant agit comme un agent. Comme avec puttygen, vous aurez besoin de télécharger pageant à partir du site web de PuTTY et lire sa documentation. La commande pageant ajoute une icône à votre zone de notification (à droite de la barre de tâches) qui vous permettra de gérer les mots de passe stockés.
Parce que ssh peut être délicat à installer si c'est nouveau pour vous, différentes choses peuvent mal se passer. Ajouter Mercurial en plus, et il y a beaucoup plus de possibilités pour s'arracher les cheveux. La plupart de ces problèmes potentiels apparaissent du côté serveur, non du côté client. La bonne nouvelle est qu'une fois que vous avez une configuration qui marche, elle continue habituellement de fonctionner indéfiniment.
Avant d'essayer de faire communiquer Mercurial avec un serveur ssh, il est mieux de s'assurer que vous pouvez d'abord utiliser le ssh normal ou la commande putty pour communiquer avec le serveur. Si vous vous heurtez a des problèmes en utilisant ces commandes directement, Mercurial ne fonctionnera sûrement pas. Pire, il cachera le problème sous-jacent. Chaque fois que vous voulez déboguer des problèmes de Mercurial liés à ssh, vous devriez d'abord vous assurer que les commandes du client ssh fonctionne d'abord, avant de vous inquiéter à propos de savoir si il y a un problème avec Mercurial.
La première chose à s'assurer du côté serveur est que vous puissiez réellement vous connecter depuis une autre machine. Si vous ne pouvez pas utiliser ssh ou putty pour vous connecter, le message d'erreur que vous aurez peut vous donner quelques indices sur ce qui ne fonctionne pas. Les problèmes les plus courants sont les suivants.
Si vous avez une erreur “connexion refusée”, soit il n'y a pas de démon SSH tournant sur le serveur, soit il est inaccessible à cause de la configuration du pare-feu.
Si vous avez une erreur “no route to host”, soit vous avez une adresse incorrecte pour le serveur soit un pare-feu sérieusement verrouillé qui ne veut pas du tout reconnaître son existence.
Si vous avez une erreur “permission denied”, vous pouvez avoir mal tapé le nom d'utilisateur sur le serveur, ou vous pouvez avoir mal tapé votre passphrase de clé ou le mot de passe d'utilisateur distant.
En résumé, si vous avez des difficultés pour communiquer avec le démon ssh du serveur, d'abord assurez-vous qu'il y en a un en fonctionnement. Sur beaucoup de systèmes, il est installé, mais désactivé, par défaut. Une fois que c'est fait, vous devriez alors vérifier que le pare-feu du serveur est configuré pour permettre les connexions entrantes sur le port que le démon ssh écoute (habituellement 22). Ne vous inquiétez pas à propos de possibilités plus exotiques de mauvaise configuration tant que vous n'avez pas vérifier ces deux premières.
Si vous utilisez un agent d'authentification du côté client pour stocker les passphrases de vos clés, vous devriez être capable de vous connecter au serveur sans avoir à entrer une passphrase ou un mot de passe. Si vous devez entrer une passphrase, il y a quelques coupables possibles.
Si vous devez entrer le mot de passe d'utilisateur distant, il y a quelques autres problèmes possibles à vérifier.
Soit le répertoire utilisateur ou le
répertoire .ssh
peut avoir des droits trop tolérants. Comme résultat,
le démon ssh n'aura pas confiance ni ne lira le fichier
authorized_keys
.
Par exemple, un “group-writable home” ou un répertoire .ssh
sera souvent la cause de ce symptôme.
Le fichier authorized_keys
de l'utilisateur peut avoir
un problème. Si quelqu'un d'autre que l'utilisateur possède ou peut écrire
dans ce fichier, le démon ssh n'aura pas confiance ni ne le lira.
Dans le monde idéal, vous devriez être capable d'exécuter la commande suivante avec succès, et elle devrait imprimer exactement une ligne en sortie, la date et l'heure actuelle.
ssh myserver date
Si, sur votre serveur, vous avec des scripts de connexion qui
impriment des bannières ou d'autre "junk" même en fonctionnement non-interactif
comme ici, vous devriez corriger cela avant de continuer, comme cela elles
imprimeront des sorties que si elles fonctionnent en mode interactif.
Autrement, au minimum, ces bannières encombreront les sorties de Mercurial.
Pire, elles pourront potentiellement causer des problèmes avec des commandes
Mercurial exécutées à distance. Mercurial essaye de détecter et d'ignorer des
bannières dans des sessions ssh non-interactives, mais il
n'est pas infaillible. (Si
vous éditez vos scripts de connexion sur votre serveur, la manière habituelle
de voir si un script de connexion fonctionne dans un shell interactif
est de vérifier le code de retour de la commande
tty -s
.)
Lorsque vous avez vérifié que le bon vieux ssh fonctionne avec votre serveur, l'étape suivante est de s'assurer que Mercurial fonctionne sur le serveur. La commande suivant devrait fonctionnement correctement :
ssh myserver hg version
Si vous voyez un message d'erreur au lieu de la sortie normale de
hg version, c'est habituellement
parce que vous n'avez pas installé Mercurial dans /usr/bin
. Ne vous inquiétez pas si
c'est le cas ; vous n'avez pas besoin de le faire. Mais vous devriez
vérifiez quelques problèmes possibles.
Mercurial est-il vraiment installé sur le serveur ? Je sais que cela semble évident, mais cela vaut la peine de vérifier !
Peut-être que le chemin de recherche de votre shell
(habituellement défini via la variable d'environnementPATH
)
est simplement mal configuré.
Peut-être que votre variable d'environnement
PATH
pointe vers l'endroit de l'exécutable
hg uniquement si la session est
interactive. Ceci peut arriver si vous mettez le chemin dans
le mauvais script de connextion. Regardez la documentation de
votre shell pour les détails.
La variable d'environnement PYTHONPATH
peut devoir contenir le chemin vers les modules Python de Mercurial.
Elle peut ne pas être définie ; elle peut être incorrecte ; ou elle peut être
définie que si la connexion est interactive.
Si vous pouvez exécuter hg version
sur une connexion ssh, bravo ! Vous avez réglé les problèmes du serveur
et du client. Vous devriez maintenant être capable d'utiliser Mercurial pour
accéder à des dépôts hébergés par cet utilisateur sur ce serveur.
Si vous avez des problèmes avec Mercurial et ssh à ce moment,
essayer d'utiliser l'option --debug
pour obtenir une image plus claire de ce qui se passe.
Mercurial ne comprime pas les données quand il utilise le protocole ssh, car le protocole ssh peut comprimer de manière transparente les données. De plus, le comportement par défaut des clients ssh est de ne pas demander de la compression.
Sur n'importe quel réseau autre qu'un réseau local rapide (même un réseau sans fil), utiliser la compression améliore significativement la vitesse des opérations réseau de Mercurial. Par exemple, sur un WAN, quelqu'un a mesuré que la compression a réduit la durée pour cloner un dépôt particulièrement grand de 51 minutes à 17 minutes.
Les deux commandes ssh et plink
acceptent une option -C
qui active
la compression. Vous pouvez facilement éditer votre ~/.hgrc
pour activer la compression pour
tous les usages de Mercurial du protocole ssh. Voici comment faire
pour la commande habituelle ssh sur des systèmes Unix-like,
par exemple.
[ui] ssh = ssh -C
Si vous utilisez ssh sur un
système Unix-like, vous pouvez le configurer pour toujours utiliser
la compression lors de la connexion avec votre serveur. Pour cela, éditer
votre fichier .ssh/config
(qui peut ne pas encore exister), comme suit.
Host hg Compression yes HostName hg.example.com
Ceci définit un alias pour le nom d'hôte,
hg
. Quand vous utilisez ce nom d'hôte sur la
ligne de commande ssh ou dans une URL Mercurial en
protocole ssh
,
ssh se connectera à
hg.example.com
et utilisera la compression. Ceci
vous offre à la fois un nom plus court à taper et la compression,
chacun est une bonne chose en lui-même.
La manière la plus simple d'héberger un ou plusieurs dépôts de manière permanente est d'utiliser un serveur web et le support CGI de Mercurial.
En fonction de votre ambition, configurer l'interface CGI de Mercurial peut vous prendre de quelques instants à plusieurs heures.
Nous commencerons par le plus simple des exemples, et travailler vers une configuration plus complexe. Même pour le cas le plus élémentaire, vous devrez certainement lire et modifier la configuration de votre serveur web.
Avant de continuer, prenez quelques instants pour contrôler quelques aspects de l'installation de votre système.
Avez-vous un serveur web installé ? Mac OS X et quelques distributions Linux sont fournis avec Apache, mais beaucoup d'autres systèmes peuvent ne pas avoir de serveur web installé.
Si vous avez un serveur web installé, fonctionne-t-il réellement ? Sur la plupart des systèmes, même si il y a en un, il sera désactivité par défaut.
Votre serveur est-il configuré pour vous permettre de faire fonctionner des programmes CGI dans le répertoire où vous prévoyer de le faire ? La plupart des serveurs désactivent explicitement l'aptitude à faire fonctionner des programmes CGI.
Si vous n'avez pas de serveur web installé, et n'avez pas
d'expérience solide dans la configuration Apache, vous devriez envisager
d'utiliser le serveur web lighttpd
au lieu
d'Apache. Apache a une réputation bien méritée de configuration baroque et
déroutante. Bien que lighttpd
ait moins de fonctions
qu'Apache, la plupart de celles-ci ne sont pas utiles pour servir
des dépôts Mercurial. Et lighttpd
est incontestablement
beaucoup plus facile pour débuter
qu'Apache.
Sur les systèmes Unix-like, il est habituel pour les utilisateurs d'avoir un
sous-répertoire nommé quelque chose comme public_html
dans leur "home
directory", à partir duquel ils peuvent servir des pages web. Un fichier
appelé foo
dans ce répertoire sera
accessible à une URL de la forme
http://www.example.com/username/foo
.
Pour commencer, trouver le script hgweb.cgi
qui devrait être
présent dans votre installation de Mercurial. Si vous ne pouvez pas trouver
rapidement une copie locale sur votre système, téléchargez-le simplement
du dépôt maître de Mercurial à http://www.selenic.com/repo/hg/raw-file/tip/hgweb.cgi.
Vous devrez copier ce script dans votre répertoire public_html
, et vous assurez qu'il est
exécutable.
cp .../hgweb.cgi ~/public_html chmod 755 ~/public_html/hgweb.cgi
L'argument 755
de
chmod est un peu plus général que de rendre
le script exécutable: il garantit que le script est
exécutable par n'importe qui, et que les permissions d'écriture pour le “group” et les
“other” ne sont
pas définies. Si vous laissez ces
permissions d'écriture activées, le sous-système suexec
d'Apache refuserait probablement d'exécuter le script. En fait,
suexec
insiste aussi pour que le
répertoire dans lequel le script réside
ne soit pas inscriptible par d'autres.
chmod 755 ~/public_html
Une fois que vous avez copié le script CGI à sa place,
allez dans un navigateur et essayer d'ouvrir l'URL
http://myhostname/~myuser/hgweb.cgi
,
mais attendez-vous à un échec immédiat.
Il y a une forte probabilité que d'essayer de visiter cette URL
échouera, et il y a beaucoup de raisons possibles pour cela. En
fait, vous devrez probablement trébucher sour pratiquement chacun des
erreurs possibles ci-dessous, donc lisez attentivement. Les
problèmes suivants sont tous ceux que j'ai rencontré sur un système
tournant Fedora 7, avec une nouvelle installation d'Apache, et un
compte utilisateur que j'avais créé spécialement pour réaliser cet
exercice.
Votre serveur web peut avoir des répertoires "per-user" désactivés.
Si vous utilisez Apache, cherchez une directive UserDir
dans votre fichier de configuration. Si il n'y en a pas,
les répertoires "per-user" seront désactivés. Si il en existe un,
mais que sa valeur est disabled
, alors
les répertoires "per-user" seront désactivés. Autrement, la
chaîne de caractères après UserDir
donne le nom du
sous-répertoire qu'Apache regardera dans votre répertoire "home",
par exemplepublic_html
.
Vos permissions d'accès de fichier peuvent être trop restrictives.
Le serveur web doit être capable de traverser votre répertoire home
et les répertoires sous votre répertoire public_html
, et
de lire les fichiers sous le dernier aussi. Voici une recette rapide
pour vous aider à rendre vos permissions plus adéquates.
chmod 755 ~ find ~/public_html -type d -print0 | xargs -0r chmod 755 find ~/public_html -type f -print0 | xargs -0r chmod 644
L'autre possibilité avec les permissions est que vous
pourriez obtenir une fenêtre complètement vide lorsque vous essayez de
charger le script. Dans ce cas, c'est probablement que vos permissions
d'accès sont trop permissives. Le sous-système
suexec
d'Apache n'exécute pas un script
qui est inscriptible par le groupe ou le monde, par exemple.
Votre serveur web peut être configuration pour refuser l'exécution de programmes CGI dans votre répertoire web "per-user". Voici la configuration "per-user" par défaut d'Apache pour mon système Fedora.
<Directory /home/*/public_html> AllowOverride FileInfo AuthConfig Limit Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec <Limit GET POST OPTIONS> Order allow,deny Allow from all </Limit> <LimitExcept GET POST OPTIONS> Order deny,allow Deny from all </LimitExcept> </Directory>
Si vous trouvez un groupe qui ressemble à
Directory
dans votre configuration Apache,
la directive à regarder est
Options
. Ajoutez ExecCGI
à la fin de cette liste si cela manque, et redémarrer le serveur
web.
Si vous trouvez qu'Apache vous donne le texte du script CGI au lie de l'exécuter, vous pouvez avoir besoin soit de décommenter (si déjà présent) ou d'ajouter une directive telle que celle-ci.
AddHandler cgi-script .cgi
La possibilité suivante est que vous pourriez
obtenir une trace coloré Python prétendant qu'il ne peut pas
importer un module relatif à mercurial
. Ceci est
réellement un progrès ! Le serveur est maintenant capable d'exécuter
votre script CGI. Cette erreur arrive probablement si vous exécutez
une installation privée de Mercurial, au lieu d'une version
"system-wide". Souvenez-vous que le serveur web fait fonctionner
le programme CGI sans aucune des variables d'environnement que vous
considérez comme certaines dans une session interactive. Si
cette erreur vous arrive, éditez une copie de hgweb.cgi
et suivez les indications
à l'intérieur de celui-ci pour corriger votre variable
d'environnement PYTHONPATH
.
Pour finir, vous êtes certain
d'avoir une autre trace colorée Python : celle-ci
se plaindra de ne pas pouvoir trouver /path/to/repository
. Éditez
votre script hgweb.cgi
et remplacez la chaîne de caractères /path/to/repository
par le
chemin complet du dépôt que vous voulez servir.
À ce moment-là, quand vous essayez de recharger la page, vous devriez avoir une jolie vue HTML de l'historique de votre dépôt. Ouf !
Pour être exhaustif dans mes expériences, j'ai essayé
de configurer le serveur web lighttpd
devenu
populaire pour servir le même dépôt que celui décrit plus haut
avec Apache. J'ai déjà surmonter tous les problèmes décrits avec
Apache, beaucoup ne sont pas spécifique à un serveur.
Comme résultat, j'étais pratiquement sûr que mes permissions de
fichiers et de répertoires étaient bonnes, et que mon script
hgweb.cgi
était correctement
édité.
Une fois qu'Apache tournait, avoir
lighttpd
pour servir le dépôt était immédiat
(en d'autres mots, même si vous essayez d'utiliser
lighttpd
, vous devriez lire la section
Apache). J'ai d'abord dû éditer la section
mod_access
de son fichier de configuration
pour activer mod_cgi
et
mod_userdir
, les deux étaient désactivés
par défaut sur mon système. J'ai ensuite ajouté quelques lignes à
la fin du fichier de configuration, pour ajouter ces modules.
userdir.path = "public_html" cgi.assign = (".cgi" => "" )
Un fois ceci fait, lighttpd
a
fonctionné immédiatemment pour moi. Si j'avais configuré
lighttpd
avant Apache, je serais sans doute
tombé dans beaucoup des mêmes problèmes de configuration
"system-level" comme j'ai eu avec Apache. De plus, j'ai
trouvé lighttpd
être notablemen plus facile à
configurer qu'Apache, même si j'ai utilisé Apache depuis plus
d'une décennie, et ceci était ma première expérience avec
lighttpd
.
Le script hgweb.cgi
vous laisse seulement publier un seul dépôt, ce qui est une limitation
ennuyeuse. Si vous voulez publier plus d'un dépôt
sans "wracking yourself" avec de multiples copies du même
script, chacune avec un nom différent, un meilleur choix est d'utiliser
le script hgwebdir.cgi
.
La procédure pour configurer hgwebdir.cgi
est seulement un peu plus
compliquée que pour hgweb.cgi
. D'abord, vous devez obtenir
une copie du script. Si vous n'en avez pas une sous la main, vous pouvez
télécharger une copie depuis le dépôt maître de Mercurial à http://www.selenic.com/repo/hg/raw-file/tip/hgwebdir.cgi.
Vous aurez besoin de copier ce script dans votre
répertoire public_html
, et
vous assurer qu'il est exécutable.
cp .../hgwebdir.cgi ~/public_html chmod 755 ~/public_html ~/public_html/hgwebdir.cgi
Avec une configuration élémentaire "out of the way", essayez de
visiter http://myhostname/~myuser/hgwebdir.cgi
dans votre navigateur. Il devrait afficher une liste vide
de dépôts. Si vous obtenez une fenêtre vide ou un message
d'erreur, essayer de parcourir la liste de problèmes
potentiels dans Section 6.6.2.1, “Que pourrait-il éventuellement mal
se passer ?”.
Le script hgwebdir.cgi
se base sur un fichier de configuration externe. Par défaut,
il cherche un fichier nommé hgweb.config
dans le même répertoire
que lui. Vous devrez créer ce fichier, et le rendre
lisible par tout le monde. Le format de ce fichier est semblable à
un fichier Windows “ini”, comme compris par un module
Python ConfigParser
[web:configparser].
La façon la plus facile de configurer hgwebdir.cgi
est avec une section
appellée collections
. Ceci publiera automatiquement
chaque dépôt sous les répertoires
que vous citez. La section devrait ressembler à ceci :
[collections] /my/root = /my/root
Mercurial interprète ceci en regardant le nom du répertoire
sur la partie droite du signe
“=
” ; en trouvant les répertoires
dans cette hiérarchie de répertoires ; et en utilisant le texte sur la
gauche pour retirer le texte correspondant des
noms qu'il listera réellement dans l'interface web.
Le restant du chemin après que ce "stripping" a été réalisé est
appelé un “chemin virtuel (virtual path)”.
Étant donné l'exemple ci-dessus, si vous avez un
dépôt dont le chemin local est /my/root/this/repo
, le script CGI
retirera le début /my/root
du nom, et
publiera le dépôt avec un chemin virtuel this/repo
. Si l'URL de base de
notre script CGI est
http://myhostname/~myuser/hgwebdir.cgi
,
l'URL complète URL pour ce dépôt sera
http://myhostname/~myuser/hgwebdir.cgi/this/repo
.
Si nous remplaçons/my/root
sur la gauche de
cet exemple par /my
, alors hgwebdir.cgi
retirera seulement
/my
du nom du dépôt,
et nous donnera comme chemin virtuel root/this/repo
au lieu de
this/repo
.
Le script hgwebdir.cgi
cherchera récursivement dans chaque répertoire listé dans la
section collections
de son fichier de configuration,
mais, il ne
cherchera pas
recursivement
dans les répertoires qu'il trouvera.
Le mécanisme de collections
rend facile
la publication de plusieurs dépôts de façon “fire and
forget”. Vous devez seulement installer le script CGI
et le fichier de configuration une seule fois. Après, vous pouvez
publier ou "dépublier" un dépôt n'importe quand en le déplaçant simplement
dans, ou hors de la hiérarchie de répertoire dans laquelle vous avez
indiqué à hgwebdir.cgi
de
regarder.
En plus du mécanisme de collections
,
le script hgwebdir.cgi
vous permet
de publier une liste spécifique de dépôts. Pour cela,
créez une section paths
, avec un contenu de
la forme suivante.
[paths] repo1 = /my/path/to/some/repo repo2 = /some/path/to/another
Dans ce cas, le chemin virtuel (le composant qui apparaîtra dans une URL) est sur la gauche de chaque définition, tandis que le chemin vers le dépôt est sur la droite. Notez qu'il n'y a pas besoin de relation entre le chemin virtuel que vous choisissez et l'emplacement d'un dépôt dans votre système de fichiers.
Si vous le souhaitez, vous pouvez utiliser
les mécanismes de
collections
et de paths
simultanément dans un seul fichier de configuration.
L'interface web de Mercurial laisse les utilisateurs télécharger une archive de n'importe quelle révision. Cette archive contiendra une image du répertoire de travail comme pour cette révision, mais il ne contiendra pas une copie des données du dépôt.
Par défaut, cette fonctionnalité n'est pas activée. Pour l'activer,
vous devrez ajouter un élément allow_archive
à la section
web
de votre ~/.hgrc
; voyez plus loin pour des détails.
Les interfaces web de Mercurial (la commande hg
serve, et les scripts hgweb.cgi
et hgwebdir.cgi
) ont un nombre
d'options de configuration que vous pouvez mettre. Celles-ci
appartiennent à une section appelée web
.
allow_archive
: détermine
quels mécanismes de téléchargement d'archive (si il y en a) Mercurial
accepte. Si vous activez cette fonctionnalité, les utilisateurs de
l'interface web seront capable de télécharger une archive de
n'importe quelle révision d'un dépôt qu'ils regardent. Pour activer
la fonction archive, cet élément doit prendre la forme d'une
série de mots pris dans la liste ci-dessous.
bz2
: une
archive tar, comprimée en utilisant la
compression bzip2
. Ceci a le meilleur
taux de compression, mais utilise le plus de temps processeur sur
le serveur.
gz
: une
archive tar, comprimée en utilisant la
compression gzip
.
zip
: une
archive zip, comprimée en utilisant la
compression LZW. Ce format a le pire taux de compression,
mais il est largement utilisé dans le monde Windows.
Si vous fournissez une liste vide, ou n'avez pas
d'entrée allow_archive
du tout,
cette fonctionnalité sera désactivée. Voici un exemple de
comment activer les trois formats supportés.
[web] allow_archive = bz2 gz zip
allowpull
:
Boolean. Détermine si l'interface web permet aux
utilisateurs distants de hg pull
et hg clone ce
dépôt sur HTTP. Si mis à no
ou
false
, seule la partie
“orienté humain” de l'interface web
est disponible.
contact
:
String. Une chaîne de caractères "free-form" (mais de préférence brève)
identifiant la personne ou le groupe responsable du
dépôt. Ceci contient souvent le nom et l'adresse électronique
d'une personne ou d'une liste de distribution. Il est judicieux de
placer cette entrée dans le fichier .hg/hgrc
du dépôt, mais il peut être
judicieux d'utiliser un ~/.hgrc
global si chaque dépôt
a un seul "mainteneur".
maxchanges
:
Integer. La valeur maximum par défaut du nombre de changesets à
afficher dans une seule page de sortie.
maxfiles
:
Integer. Le nombre maximum par défaut de fichier modifiés à
à afficher dans une seule page de sortie.
stripes
:
Integer. Si l'interface web affiche alternativement desIf the web interface displays alternating
“rayures” pour rendre plus facile l'alignement visuel
des lignes lorsque vous regarder une table, ce nombre contrôle
le nombre de lignes dans chaque rayure.
style
: Contrôle le "modèle"
que Mercurial utilise pour afficher l'interface web. Mercurial
est livré avec plusieurs "modèles" web.
Vous pouvez aussi spécifiez un "modèle" personnalisé à vous;
voyez Chapter 11, Customizing the output of Mercurial pour des détails. Ici, vous pouvez
voir comment activer le style gitweb
.
[web] style = gitweb
templates
:
Path. Le répertoire dans lequel chercher les fichiers "modèles".
Par défaut, Mercurial cherche dans le répertoire dans lequel
il a été installé.
Si vous utilisez hgwebdir.cgi
, vous pouvez mettre quelques
éléments de configuration dans une section web
du fichier hgweb.config
au lieu d'un fichier
~/.hgrc
, par
commodité. Ces éléments sont motd
et style
.
Quelques éléments de configuration web
devraient être mis dans .hg/hgrc
local à un dépôt, plutôt que dans un
~/.hgrc
d'utilisateur ou global.
Certaines entrées dans la section web
d'un fichier ~/.hgrc
sont là seulement pour être utilisée
avec la commande hg serve.
accesslog
:
Path. Le nom d'un fichier dans lequel écrire le "log" d'accès.
Par défaut, la commande hg
serve écrit cette information sur la sortie
standard, pas dans un fichier. Les entrées de "log" sont écrites
dans le format de fichier standard “combined” utilisé
par pratiquement tous les serveurs web.
address
:
String. L'adresse locale sur laquelle le serveur devrait
écouter les connexions entrantes. Par défaut, le serveur
écoute sur toutes les adresses.
errorlog
:
Path. Le nom d'un fichier dans lequel écrire le "log" d'erreur.
Par défaut, la commande hg
serve écrit cette information sur la sortie erreur,
pas dans un fichier.
ipv6
:
Boolean. Si il faut utiliser le protocole IPv6. Par défaut,
IPv6 n'est pas utilisé.
port
:
Integer. Le numéro de port TCP sur lequel le serveur devrait
écouter. Le numéro de port par défaut utilisé est 8000.
Il est important de se souvenir qu'un serveur web comme
Apache ou lighttpd
fonctionnera avec un ID utilisateur
qui est différent du vôtre. Les scripts CGI fonctionnant sur votre serveur,
tels que hgweb.cgi
, s'exécuteront habituellement aussi
sous un autre ID utilisateur.
Si vous ajoutez des éléments web
à
votre propre fichier ~/.hgrc
personnel,
les scripts CGI ne pourront pas lire ce fichier
~/.hgrc
. Ces réglages
toucheront donc seulement le comportement de la commande hg serve quand vous l'exécuterez.
Pour faire voir vos réglages aux scripts CGI, soit créez un
fichier ~/.hgrc
dans le répertoire
"home" de l'utilisateur qui exécute votre serveur web, soit
ajouter ces réglages dans un fichier hgrc
"system-wide".
Sur des systèmes "Unix-like" partagé par plusieurs utilisateurs (tel qu'un serveur où les gens publient des modifications), il est souvent judicieux de définir certains comportement globaux par défaut, tels que le thème à utiliser dans les interfaces web.
Si un fichier appelé /etc/mercurial/hgrc
existe, Mercurial le lira au démarrage et appliquera tous les réglages
de configuration qu'il trouve dans ce fichier. Il regardera aussi
les fichiers finissant par une extension .rc
dans un
répertoire appelé /etc/mercurial/hgrc.d
, et
appliquera tous les réglages de configuration qu'il trouve dans chacun
de ces fichiers.
Une situation dans laquelle un hgrc
global peut être utile est si des utilisateurs "pullent" des modifications
faites par d'autres utilisateurs. Par défaut, Mercurial can be useful is if users are pulling changes owned by other
users. By default, Mercurial ne se fiera pas à la plupart des
entrées de configuration dans un fichier .hg/hgrc
à l'intérieur d'un dépôt qui appartient à un utilisateur différent. Si nous
clonons ou "pullons" des modifications d'un tel dépôt, Mercurial
affichera un avertissement indiquant qu'il ne fait pas confiance aux
.hg/hgrc
.
Si quelqu'un dans un groupe Unix particulier est dans la même équipe
et devrait avoir confiance dans chaque réglage
de configuration des autres, ou si nous voulons avoir confiance dans des utilisateurs
particuliers, nous pouvons passer outre les défauts sceptiques de Mercurial
en créant un fichier hgrc
"system-wide" tel que celui qui
suit ::
# Save this as e.g. /etc/mercurial/hgrc.d/trust.rc [trusted] # Trust all entries in any hgrc file owned by the "editors" or # "www-data" groups. groups = editors, www-data # Trust entries in hgrc files owned by the following users. users = apache, bobo
Table of Contents
Mercurial provides mechanisms that let you work with file names in a consistent and expressive way.
Mercurial uses a unified piece of machinery “under the hood” to handle file names. Every command behaves uniformly with respect to file names. The way in which commands work with file names is as follows.
If you explicitly name real files on the command line, Mercurial works with exactly those files, as you would expect.
$
hg add COPYING README examples/simple.py
When you provide a directory name, Mercurial will interpret this as “operate on every file in this directory and its subdirectories”. Mercurial traverses the files and subdirectories in a directory in alphabetical order. When it encounters a subdirectory, it will traverse that subdirectory before continuing with the current directory.
$
hg status src
? src/main.py ? src/watcher/_watcher.c ? src/watcher/watcher.py ? src/xyzzy.txt
Mercurial's commands that work with file names have useful default behaviors when you invoke them without providing any file names or patterns. What kind of behavior you should expect depends on what the command does. Here are a few rules of thumb you can use to predict what a command is likely to do if you don't give it any names to work with.
Most commands will operate on the entire working directory. This is what the hg add command does, for example.
If the command has effects that are difficult or impossible to reverse, it will force you to explicitly provide at least one name or pattern (see below). This protects you from accidentally deleting files by running hg remove with no arguments, for example.
It's easy to work around these default behaviors if they
don't suit you. If a command normally operates on the whole
working directory, you can invoke it on just the current
directory and its subdirectories by giving it the name
“.
”.
$
cd src
$
hg add -n
adding ../MANIFEST.in adding ../examples/performant.py adding ../setup.py adding main.py adding watcher/_watcher.c adding watcher/watcher.py adding xyzzy.txt$
hg add -n .
adding main.py adding watcher/_watcher.c adding watcher/watcher.py adding xyzzy.txt
Along the same lines, some commands normally print file names relative to the root of the repository, even if you're invoking them from a subdirectory. Such a command will print file names relative to your subdirectory if you give it explicit names. Here, we're going to run hg status from a subdirectory, and get it to operate on the entire working directory while printing file names relative to our subdirectory, by passing it the output of the hg root command.
$
hg status
A COPYING A README A examples/simple.py ? MANIFEST.in ? examples/performant.py ? setup.py ? src/main.py ? src/watcher/_watcher.c ? src/watcher/watcher.py ? src/xyzzy.txt$
hg status `hg root`
A ../COPYING A ../README A ../examples/simple.py ? ../MANIFEST.in ? ../examples/performant.py ? ../setup.py ? main.py ? watcher/_watcher.c ? watcher/watcher.py ? xyzzy.txt
The hg add example in the preceding section illustrates something else that's helpful about Mercurial commands. If a command operates on a file that you didn't name explicitly on the command line, it will usually print the name of the file, so that you will not be surprised what's going on.
The principle here is of least surprise. If you've exactly named a file on the command line, there's no point in repeating it back at you. If Mercurial is acting on a file implicitly, e.g. because you provided no names, or a directory, or a pattern (see below), it is safest to tell you what files it's operating on.
For commands that behave this way, you can silence them
using the -q
option. You
can also get them to print the name of every file, even those
you've named explicitly, using the -v
option.
In addition to working with file and directory names, Mercurial lets you use patterns to identify files. Mercurial's pattern handling is expressive.
On Unix-like systems (Linux, MacOS, etc.), the job of matching file names to patterns normally falls to the shell. On these systems, you must explicitly tell Mercurial that a name is a pattern. On Windows, the shell does not expand patterns, so Mercurial will automatically identify names that are patterns, and expand them for you.
To provide a pattern in place of a regular name on the command line, the mechanism is simple:
syntax:patternbody
That is, a pattern is identified by a short text string that says what kind of pattern this is, followed by a colon, followed by the actual pattern.
Mercurial supports two kinds of pattern syntax. The most
frequently used is called glob
; this is the
same kind of pattern matching used by the Unix shell, and should
be familiar to Windows command prompt users, too.
When Mercurial does automatic pattern matching on Windows,
it uses glob
syntax. You can thus omit the
“glob:
” prefix on Windows, but
it's safe to use it, too.
The re
syntax is more powerful; it lets
you specify patterns using regular expressions, also known as
regexps.
By the way, in the examples that follow, notice that I'm careful to wrap all of my patterns in quote characters, so that they won't get expanded by the shell before Mercurial sees them.
This is an overview of the kinds of patterns you can use when you're matching on glob patterns.
The “*
” character matches
any string, within a single directory.
$
hg add 'glob:*.py'
adding main.py
The “**
” pattern matches
any string, and crosses directory boundaries. It's not a
standard Unix glob token, but it's accepted by several popular
Unix shells, and is very useful.
$
cd ..
$
hg status 'glob:**.py'
A examples/simple.py A src/main.py ? examples/performant.py ? setup.py ? src/watcher/watcher.py
The “?
” pattern matches
any single character.
$
hg status 'glob:**.?'
? src/watcher/_watcher.c
The “[
” character begins a
character class. This matches any single
character within the class. The class ends with a
“]
” character. A class may
contain multiple ranges of the form
“a-f
”, which is shorthand for
“abcdef
”.
$
hg status 'glob:**[nr-t]'
? MANIFEST.in ? src/xyzzy.txt
If the first character after the
“[
” in a character class is a
“!
”, it
negates the class, making it match any
single character not in the class.
A “{
” begins a group of
subpatterns, where the whole group matches if any subpattern
in the group matches. The “,
”
character separates subpatterns, and
“}
” ends the group.
$
hg status 'glob:*.{in,py}'
? MANIFEST.in ? setup.py
Don't forget that if you want to match a pattern in any
directory, you should not be using the
“*
” match-any token, as this
will only match within one directory. Instead, use the
“**
” token. This small
example illustrates the difference between the two.
$
hg status 'glob:*.py'
? setup.py$
hg status 'glob:**.py'
A examples/simple.py A src/main.py ? examples/performant.py ? setup.py ? src/watcher/watcher.py
Mercurial accepts the same regular expression syntax as the Python programming language (it uses Python's regexp engine internally). This is based on the Perl language's regexp syntax, which is the most popular dialect in use (it's also used in Java, for example).
I won't discuss Mercurial's regexp dialect in any detail here, as regexps are not often used. Perl-style regexps are in any case already exhaustively documented on a multitude of web sites, and in many books. Instead, I will focus here on a few things you should know if you find yourself needing to use regexps with Mercurial.
A regexp is matched against an entire file name, relative
to the root of the repository. In other words, even if you're
already in subbdirectory foo
, if you want to match files
under this directory, your pattern must start with
“foo/
”.
One thing to note, if you're familiar with Perl-style
regexps, is that Mercurial's are rooted.
That is, a regexp starts matching against the beginning of a
string; it doesn't look for a match anywhere within the
string. To match anywhere in a string, start your pattern
with “.*
”.
Not only does Mercurial give you a variety of ways to specify files; it lets you further winnow those files using filters. Commands that work with file names accept two filtering options.
You can provide multiple -I
and -X
options on the command line,
and intermix them as you please. Mercurial interprets the
patterns you provide using glob syntax by default (but you can
use regexps if you need to).
You can read a -I
filter as “process only the files that match this
filter”.
$
hg status -I '*.in'
? MANIFEST.in
The -X
filter is best
read as “process only the files that don't match this
pattern”.
$
hg status -X '**.py' src
? src/watcher/_watcher.c ? src/xyzzy.txt
When you create a new repository, the chances are that over time it will grow to contain files that ought to not be managed by Mercurial, but which you don't want to see listed every time you run hg status. For instance, “build products” are files that are created as part of a build but which should not be managed by a revision control system. The most common build products are output files produced by software tools such as compilers. As another example, many text editors litter a directory with lock files, temporary working files, and backup files, which it also makes no sense to manage.
To have Mercurial permanently ignore such files, create a
file named .hgignore
in the root of your
repository. You should hg
add this file so that it gets tracked with the rest of
your repository contents, since your collaborators will probably
find it useful too.
By default, the .hgignore
file should
contain a list of regular expressions, one per line. Empty
lines are skipped. Most people prefer to describe the files they
want to ignore using the “glob” syntax that we
described above, so a typical .hgignore
file will start with this directive:
syntax: glob
This tells Mercurial to interpret the lines that follow as glob patterns, not regular expressions.
Here is a typical-looking .hgignore
file.
syntax: glob # This line is a comment, and will be skipped. # Empty lines are skipped too. # Backup files left behind by the Emacs editor. *~ # Lock files used by the Emacs editor. # Notice that the "#" character is quoted with a backslash. # This prevents it from being interpreted as starting a comment. .\#* # Temporary files used by the vim editor. .*.swp # A hidden file created by the Mac OS X Finder. .DS_Store
If you're working in a mixed development environment that contains both Linux (or other Unix) systems and Macs or Windows systems, you should keep in the back of your mind the knowledge that they treat the case (“N” versus “n”) of file names in incompatible ways. This is not very likely to affect you, and it's easy to deal with if it does, but it could surprise you if you don't know about it.
Operating systems and filesystems differ in the way they handle the case of characters in file and directory names. There are three common ways to handle case in names.
Completely case insensitive. Uppercase and lowercase versions of a letter are treated as identical, both when creating a file and during subsequent accesses. This is common on older DOS-based systems.
Case preserving, but insensitive. When a file
or directory is created, the case of its name is stored, and
can be retrieved and displayed by the operating system.
When an existing file is being looked up, its case is
ignored. This is the standard arrangement on Windows and
MacOS. The names foo
and
FoO
identify the same file. This
treatment of uppercase and lowercase letters as
interchangeable is also referred to as case
folding.
Case sensitive. The case of a name
is significant at all times. The names
foo
and FoO
identify different files. This is the way Linux and Unix
systems normally work.
On Unix-like systems, it is possible to have any or all of the above ways of handling case in action at once. For example, if you use a USB thumb drive formatted with a FAT32 filesystem on a Linux system, Linux will handle names on that filesystem in a case preserving, but insensitive, way.
Mercurial's repository storage mechanism is case safe. It translates file names so that they can be safely stored on both case sensitive and case insensitive filesystems. This means that you can use normal file copying tools to transfer a Mercurial repository onto, for example, a USB thumb drive, and safely move that drive and repository back and forth between a Mac, a PC running Windows, and a Linux box.
When operating in the working directory, Mercurial honours the naming policy of the filesystem where the working directory is located. If the filesystem is case preserving, but insensitive, Mercurial will treat names that differ only in case as the same.
An important aspect of this approach is that it is
possible to commit a changeset on a case sensitive (typically
Linux or Unix) filesystem that will cause trouble for users on
case insensitive (usually Windows and MacOS) users. If a
Linux user commits changes to two files, one named
myfile.c
and the other named
MyFile.C
, they will be stored correctly
in the repository. And in the working directories of other
Linux users, they will be correctly represented as separate
files.
If a Windows or Mac user pulls this change, they will not initially have a problem, because Mercurial's repository storage mechanism is case safe. However, once they try to hg update the working directory to that changeset, or hg merge with that changeset, Mercurial will spot the conflict between the two file names that the filesystem would treat as the same, and forbid the update or merge from occurring.
If you are using Windows or a Mac in a mixed environment where some of your collaborators are using Linux or Unix, and Mercurial reports a case folding conflict when you try to hg update or hg merge, the procedure to fix the problem is simple.
Just find a nearby Linux or Unix box, clone the problem repository onto it, and use Mercurial's hg rename command to change the names of any offending files or directories so that they will no longer cause case folding conflicts. Commit this change, hg pull or hg push it across to your Windows or MacOS system, and hg update to the revision with the non-conflicting names.
The changeset with case-conflicting names will remain in your project's history, and you still won't be able to hg update your working directory to that changeset on a Windows or MacOS system, but you can continue development unimpeded.
Table of Contents
Mercurial provides several mechanisms for you to manage a project that is making progress on multiple fronts at once. To understand these mechanisms, let's first take a brief look at a fairly normal software project structure.
Many software projects issue periodic “major” releases that contain substantial new features. In parallel, they may issue “minor” releases. These are usually identical to the major releases off which they're based, but with a few bugs fixed.
In this chapter, we'll start by talking about how to keep records of project milestones such as releases. We'll then continue on to talk about the flow of work between different phases of a project, and how Mercurial can help you to isolate and manage this work.
Once you decide that you'd like to call a particular revision a “release”, it's a good idea to record the identity of that revision. This will let you reproduce that release at a later date, for whatever purpose you might need at the time (reproducing a bug, porting to a new platform, etc).
$
hg init mytag
$
cd mytag
$
echo hello > myfile
$
hg commit -A -m 'Initial commit'
adding myfile
Mercurial lets you give a permanent name to any revision using the hg tag command. Not surprisingly, these names are called “tags”.
$
hg tag v1.0
A tag is nothing more than a “symbolic name” for a revision. Tags exist purely for your convenience, so that you have a handy permanent way to refer to a revision; Mercurial doesn't interpret the tag names you use in any way. Neither does Mercurial place any restrictions on the name of a tag, beyond a few that are necessary to ensure that a tag can be parsed unambiguously. A tag name cannot contain any of the following characters:
You can use the hg tags command to display the tags present in your repository. In the output, each tagged revision is identified first by its name, then by revision number, and finally by the unique hash of the revision.
$
hg tags
tip 1:bcc984c38ff0 v1.0 0:209501c094ec
Notice that tip
is listed in the output
of hg tags. The
tip
tag is a special “floating”
tag, which always identifies the newest revision in the
repository.
In the output of the hg
tags command, tags are listed in reverse order, by
revision number. This usually means that recent tags are listed
before older tags. It also means that tip
is
always going to be the first tag listed in the output of
hg tags.
When you run hg log, if it displays a revision that has tags associated with it, it will print those tags.
$
hg log
changeset: 1:bcc984c38ff0 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:14 2011 +0000 summary: Added tag v1.0 for changeset 209501c094ec changeset: 0:209501c094ec tag: v1.0 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:14 2011 +0000 summary: Initial commit
Any time you need to provide a revision ID to a Mercurial command, the command will accept a tag name in its place. Internally, Mercurial will translate your tag name into the corresponding revision ID, then use that.
$
echo goodbye > myfile2
$
hg commit -A -m 'Second commit'
adding myfile2$
hg log -r v1.0
changeset: 0:209501c094ec tag: v1.0 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:14 2011 +0000 summary: Initial commit
There's no limit on the number of tags you can have in a repository, or on the number of tags that a single revision can have. As a practical matter, it's not a great idea to have “too many” (a number which will vary from project to project), simply because tags are supposed to help you to find revisions. If you have lots of tags, the ease of using them to identify revisions diminishes rapidly.
For example, if your project has milestones as frequent as every few days, it's perfectly reasonable to tag each one of those. But if you have a continuous build system that makes sure every revision can be built cleanly, you'd be introducing a lot of noise if you were to tag every clean build. Instead, you could tag failed builds (on the assumption that they're rare!), or simply not use tags to track buildability.
If you want to remove a tag that you no longer want, use hg tag --remove.
$
hg tag --remove v1.0
$
hg tags
tip 3:0adc01cc3a11
You can also modify a tag at any time, so that it identifies
a different revision, by simply issuing a new hg tag command. You'll have to use the
-f
option to tell Mercurial
that you really want to update the
tag.
$
hg tag -r 1 v1.1
$
hg tags
tip 4:408459d1deb8 v1.1 1:bcc984c38ff0$
hg tag -r 2 v1.1
abort: tag 'v1.1' already exists (use -f to force)$
hg tag -f -r 2 v1.1
$
hg tags
tip 5:47a097da785d v1.1 2:7ea6b557544c
There will still be a permanent record of the previous identity of the tag, but Mercurial will no longer use it. There's thus no penalty to tagging the wrong revision; all you have to do is turn around and tag the correct revision once you discover your error.
Mercurial stores tags in a normal revision-controlled file
in your repository. If you've created any tags, you'll find
them in a file in the root of your repository named .hgtags
. When you run the hg tag command, Mercurial modifies
this file, then automatically commits the change to it. This
means that every time you run hg
tag, you'll see a corresponding changeset in the
output of hg log.
$
hg tip
changeset: 5:47a097da785d tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:15 2011 +0000 summary: Added tag v1.1 for changeset 7ea6b557544c
You won't often need to care about the .hgtags
file, but it sometimes
makes its presence known during a merge. The format of the
file is simple: it consists of a series of lines. Each line
starts with a changeset hash, followed by a space, followed by
the name of a tag.
If you're resolving a conflict in the .hgtags
file during a merge,
there's one twist to modifying the .hgtags
file: when Mercurial is
parsing the tags in a repository, it
never reads the working copy of the
.hgtags
file. Instead, it
reads the most recently committed
revision of the file.
An unfortunate consequence of this design is that you
can't actually verify that your merged .hgtags
file is correct until
after you've committed a change. So if
you find yourself resolving a conflict on .hgtags
during a merge, be sure to
run hg tags after you commit.
If it finds an error in the .hgtags
file, it will report the
location of the error, which you can then fix and commit. You
should then run hg tags
again, just to be sure that your fix is correct.
You may have noticed that the hg
clone command has a -r
option that lets you clone
an exact copy of the repository as of a particular changeset.
The new clone will not contain any project history that comes
after the revision you specified. This has an interaction
with tags that can surprise the unwary.
Recall that a tag is stored as a revision to
the .hgtags
file. When you
create a tag, the changeset in which its recorded refers to an
older changeset. When you run hg clone
-r foo to clone a repository as of tag
foo
, the new clone will not
contain any revision newer than the one the tag refers to,
including the revision where the tag was created.
The result is that you'll get exactly the right subset of the
project's history in the new repository, but
not the tag you might have
expected.
Since Mercurial's tags are revision controlled and carried
around with a project's history, everyone you work with will
see the tags you create. But giving names to revisions has
uses beyond simply noting that revision
4237e45506ee
is really
v2.0.2
. If you're trying to track down a
subtle bug, you might want a tag to remind you of something
like “Anne saw the symptoms with this
revision”.
For cases like this, what you might want to use are
local tags. You can create a local tag
with the -l
option to the
hg tag command. This will
store the tag in a file called .hg/localtags
. Unlike .hgtags
, .hg/localtags
is not revision
controlled. Any tags you create using -l
remain strictly local to the
repository you're currently working in.
To return to the outline I sketched at the beginning of the chapter, let's think about a project that has multiple concurrent pieces of work under development at once.
There might be a push for a new “main” release; a new minor bugfix release to the last main release; and an unexpected “hot fix” to an old release that is now in maintenance mode.
The usual way people refer to these different concurrent directions of development is as “branches”. However, we've already seen numerous times that Mercurial treats all of history as a series of branches and merges. Really, what we have here is two ideas that are peripherally related, but which happen to share a name.
The easiest way to isolate a “big picture”
branch in Mercurial is in a dedicated repository. If you have
an existing shared repository—let's call it
myproject
—that reaches a
“1.0” milestone, you can start to prepare for
future maintenance releases on top of version 1.0 by tagging the
revision from which you prepared the 1.0 release.
$
cd myproject
$
hg tag v1.0
You can then clone a new shared
myproject-1.0.1
repository as of that
tag.
$
cd ..
$
hg clone myproject myproject-1.0.1
updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
Afterwards, if someone needs to work on a bug fix that ought
to go into an upcoming 1.0.1 minor release, they clone the
myproject-1.0.1
repository, make their
changes, and push them back.
$
hg clone myproject-1.0.1 my-1.0.1-bugfix
updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd my-1.0.1-bugfix
$
echo 'I fixed a bug using only echo!' >> myfile
$
hg commit -m 'Important fix for 1.0.1'
$
hg push
pushing to /tmp/branch-repoCrM5SG/myproject-1.0.1 searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files
Meanwhile, development for
the next major release can continue, isolated and unabated, in
the myproject
repository.
$
cd ..
$
hg clone myproject my-feature
updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd my-feature
$
echo 'This sure is an exciting new feature!' > mynewfile
$
hg commit -A -m 'New feature'
adding mynewfile$
hg push
pushing to /tmp/branch-repoCrM5SG/myproject searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files
In many cases, if you have a bug to fix on a maintenance branch, the chances are good that the bug exists on your project's main branch (and possibly other maintenance branches, too). It's a rare developer who wants to fix the same bug multiple times, so let's look at a few ways that Mercurial can help you to manage these bugfixes without duplicating your work.
In the simplest instance, all you need to do is pull changes from your maintenance branch into your local clone of the target branch.
$
cd ..
$
hg clone myproject myproject-merge
updating to branch default 3 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd myproject-merge
$
hg pull ../myproject-1.0.1
pulling from ../myproject-1.0.1 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)
You'll then need to merge the heads of the two branches, and push back to the main branch.
$
hg merge
1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit)$
hg commit -m 'Merge bugfix from 1.0.1 branch'
$
hg push
pushing to /tmp/branch-repoCrM5SG/myproject searching for changes adding changesets adding manifests adding file changes added 2 changesets with 1 changes to 1 files
In most instances, isolating branches in repositories is the right approach. Its simplicity makes it easy to understand; and so it's hard to make mistakes. There's a one-to-one relationship between branches you're working in and directories on your system. This lets you use normal (non-Mercurial-aware) tools to work on files within a branch/repository.
If you're more in the “power user” category (and your collaborators are too), there is an alternative way of handling branches that you can consider. I've already mentioned the human-level distinction between “small picture” and “big picture” branches. While Mercurial works with multiple “small picture” branches in a repository all the time (for example after you pull changes in, but before you merge them), it can also work with multiple “big picture” branches.
The key to working this way is that Mercurial lets you
assign a persistent name to a branch.
There always exists a branch named default
.
Even before you start naming branches yourself, you can find
traces of the default
branch if you look for
them.
As an example, when you run the hg
commit command, and it pops up your editor so that
you can enter a commit message, look for a line that contains
the text “HG: branch default
” at
the bottom. This is telling you that your commit will occur on
the branch named default
.
To start working with named branches, use the hg branches command. This command lists the named branches already present in your repository, telling you which changeset is the tip of each.
$
hg tip
changeset: 0:41d08ce36803 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:37 2011 +0000 summary: Initial commit$
hg branches
default 0:41d08ce36803
Since you haven't created any named branches yet, the only
one that exists is default
.
To find out what the “current” branch is, run the hg branch command, giving it no arguments. This tells you what branch the parent of the current changeset is on.
$
hg branch
default
To create a new branch, run the hg branch command again. This time, give it one argument: the name of the branch you want to create.
$
hg branch foo
marked working directory as branch foo$
hg branch
foo
After you've created a branch, you might wonder what effect the hg branch command has had. What do the hg status and hg tip commands report?
$
hg status
$
hg tip
changeset: 0:41d08ce36803 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:37 2011 +0000 summary: Initial commit
Nothing has changed in the working directory, and there's been no new history created. As this suggests, running the hg branch command has no permanent effect; it only tells Mercurial what branch name to use the next time you commit a changeset.
When you commit a change, Mercurial records the name of the
branch on which you committed. Once you've switched from the
default
branch to another and committed,
you'll see the name of the new branch show up in the output of
hg log, hg tip, and other commands that
display the same kind of output.
$
echo 'hello again' >> myfile
$
hg commit -m 'Second commit'
$
hg tip
changeset: 1:f9bb0667aefe branch: foo tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:37 2011 +0000 summary: Second commit
The hg log-like commands
will print the branch name of every changeset that's not on the
default
branch. As a result, if you never
use named branches, you'll never see this information.
Once you've named a branch and committed a change with that name, every subsequent commit that descends from that change will inherit the same branch name. You can change the name of a branch at any time, using the hg branch command.
$
hg branch
foo$
hg branch bar
marked working directory as branch bar$
echo new file > newfile
$
hg commit -A -m 'Third commit'
adding newfile$
hg tip
changeset: 2:c3cd5b63a4a9 branch: bar tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:38 2011 +0000 summary: Third commit
In practice, this is something you won't do very often, as branch names tend to have fairly long lifetimes. (This isn't a rule, just an observation.)
If you have more than one named branch in a repository,
Mercurial will remember the branch that your working directory
is on when you start a command like hg
update or hg pull
-u. It will update the working directory to the tip
of this branch, no matter what the “repo-wide” tip
is. To update to a revision that's on a different named branch,
you may need to use the -C
option to hg update.
This behavior is a little subtle, so let's see it in action. First, let's remind ourselves what branch we're currently on, and what branches are in our repository.
$
hg parents
changeset: 2:c3cd5b63a4a9 branch: bar tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:38 2011 +0000 summary: Third commit$
hg branches
bar 2:c3cd5b63a4a9 foo 1:f9bb0667aefe (inactive) default 0:41d08ce36803 (inactive)
We're on the bar
branch, but there also
exists an older hg foo
branch.
We can hg update back and
forth between the tips of the foo
and
bar
branches without needing to use the
-C
option, because this
only involves going backwards and forwards linearly through our
change history.
$
hg update foo
0 files updated, 0 files merged, 1 files removed, 0 files unresolved$
hg parents
changeset: 1:f9bb0667aefe branch: foo user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:37 2011 +0000 summary: Second commit$
hg update bar
1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
hg parents
changeset: 2:c3cd5b63a4a9 branch: bar tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:38 2011 +0000 summary: Third commit
If we go back to the foo
branch and then
run hg update, it will keep us
on foo
, not move us to the tip of
bar
.
$
hg update foo
0 files updated, 0 files merged, 1 files removed, 0 files unresolved$
hg update
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
Committing a new change on the foo
branch
introduces a new head.
$
echo something > somefile
$
hg commit -A -m 'New file'
adding somefile$
hg heads
changeset: 3:0192c7c70894 branch: foo tag: tip parent: 1:f9bb0667aefe user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:39 2011 +0000 summary: New file changeset: 2:c3cd5b63a4a9 branch: bar user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:38 2011 +0000 summary: Third commit changeset: 0:41d08ce36803 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:37 2011 +0000 summary: Initial commit
As you've probably noticed, merges in Mercurial are not symmetrical. Let's say our repository has two heads, 17 and 23. If I hg update to 17 and then hg merge with 23, Mercurial records 17 as the first parent of the merge, and 23 as the second. Whereas if I hg update to 23 and then hg merge with 17, it records 23 as the first parent, and 17 as the second.
This affects Mercurial's choice of branch name when you
merge. After a merge, Mercurial will retain the branch name of
the first parent when you commit the result of the merge. If
your first parent's branch name is foo
, and
you merge with bar
, the branch name will
still be foo
after you merge.
It's not unusual for a repository to contain multiple heads,
each with the same branch name. Let's say I'm working on the
foo
branch, and so are you. We commit
different changes; I pull your changes; I now have two heads,
each claiming to be on the foo
branch. The
result of a merge will be a single head on the
foo
branch, as you might hope.
But if I'm working on the bar
branch, and
I merge work from the foo
branch, the result
will remain on the bar
branch.
$
hg branch
bar$
hg merge foo
1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit)$
hg commit -m 'Merge'
$
hg tip
changeset: 4:9288626880c0 branch: bar tag: tip parent: 2:c3cd5b63a4a9 parent: 3:0192c7c70894 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:39 2011 +0000 summary: Merge
To give a more concrete example, if I'm working on the
bleeding-edge
branch, and I want to bring in
the latest fixes from the stable
branch,
Mercurial will choose the “right”
(bleeding-edge
) branch name when I pull and
merge from stable
.
You shouldn't think of named branches as applicable only to situations where you have multiple long-lived branches cohabiting in a single repository. They're very useful even in the one-branch-per-repository case.
In the simplest case, giving a name to each branch gives you a permanent record of which branch a changeset originated on. This gives you more context when you're trying to follow the history of a long-lived branchy project.
If you're working with shared repositories, you can set up a
pretxnchangegroup
hook on each
that will block incoming changes that have the
“wrong” branch name. This provides a simple, but
effective, defence against people accidentally pushing changes
from a “bleeding edge” branch to a
“stable” branch. Such a hook might look like this
inside the shared repo's
/.hgrc
.
[hooks] pretxnchangegroup.branch = hg heads --template '{branches} ' | grep mybranch
Table of Contents
To err might be human, but to really handle the consequences well takes a top-notch revision control system. In this chapter, we'll discuss some of the techniques you can use when you find that a problem has crept into your project. Mercurial has some highly capable features that will help you to isolate the sources of problems, and to handle them appropriately.
I have the occasional but persistent problem of typing rather more quickly than I can think, which sometimes results in me committing a changeset that is either incomplete or plain wrong. In my case, the usual kind of incomplete changeset is one in which I've created a new source file, but forgotten to hg add it. A “plain wrong” changeset is not as common, but no less annoying.
In Section 4.2.2, “Opérations sûres”, I mentioned that Mercurial treats each modification of a repository as a transaction. Every time you commit a changeset or pull changes from another repository, Mercurial remembers what you did. You can undo, or roll back, exactly one of these actions using the hg rollback command. (See Section 9.1.4, “Rolling back is useless once you've pushed” for an important caveat about the use of this command.)
Here's a mistake that I often find myself making: committing a change in which I've created a new file, but forgotten to hg add it.
$
hg status
M a$
echo b > b
$
hg commit -m 'Add file b'
Looking at the output of hg status after the commit immediately confirms the error.
$
hg status
? b$
hg tip
changeset: 1:11ea08511960 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:13 2011 +0000 summary: Add file b
The commit captured the changes to the file
a
, but not the new file
b
. If I were to push this changeset to a
repository that I shared with a colleague, the chances are
high that something in a
would refer to
b
, which would not be present in their
repository when they pulled my changes. I would thus become
the object of some indignation.
However, luck is with me—I've caught my error before I pushed the changeset. I use the hg rollback command, and Mercurial makes that last changeset vanish.
$
hg rollback
rolling back to revision 0 (undo commit)$
hg tip
changeset: 0:2d984fe05d5d tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:13 2011 +0000 summary: First commit$
hg status
M a ? b
Notice that the changeset is no longer present in the
repository's history, and the working directory once again
thinks that the file a
is modified. The
commit and rollback have left the working directory exactly as
it was prior to the commit; the changeset has been completely
erased. I can now safely hg
add the file b
, and rerun my
commit.
$
hg add b
$
hg commit -m 'Add file b, this time for real'
It's common practice with Mercurial to maintain separate development branches of a project in different repositories. Your development team might have one shared repository for your project's “0.9” release, and another, containing different changes, for the “1.0” release.
Given this, you can imagine that the consequences could be messy if you had a local “0.9” repository, and accidentally pulled changes from the shared “1.0” repository into it. At worst, you could be paying insufficient attention, and push those changes into the shared “0.9” tree, confusing your entire team (but don't worry, we'll return to this horror scenario later). However, it's more likely that you'll notice immediately, because Mercurial will display the URL it's pulling from, or you will see it pull a suspiciously large number of changes into the repository.
The hg rollback command will work nicely to expunge all of the changesets that you just pulled. Mercurial groups all changes from one hg pull into a single transaction, so one hg rollback is all you need to undo this mistake.
The value of the hg rollback command drops to zero once you've pushed your changes to another repository. Rolling back a change makes it disappear entirely, but only in the repository in which you perform the hg rollback. Because a rollback eliminates history, there's no way for the disappearance of a change to propagate between repositories.
If you've pushed a change to another repository—particularly if it's a shared repository—it has essentially “escaped into the wild,” and you'll have to recover from your mistake in a different way. If you push a changeset somewhere, then roll it back, then pull from the repository you pushed to, the changeset you thought you'd gotten rid of will simply reappear in your repository.
(If you absolutely know for sure that the change you want to roll back is the most recent change in the repository that you pushed to, and you know that nobody else could have pulled it from that repository, you can roll back the changeset there, too, but you really should not expect this to work reliably. Sooner or later a change really will make it into a repository that you don't directly control (or have forgotten about), and come back to bite you.)
Mercurial stores exactly one transaction in its transaction log; that transaction is the most recent one that occurred in the repository. This means that you can only roll back one transaction. If you expect to be able to roll back one transaction, then its predecessor, this is not the behavior you will get.
$
hg rollback
rolling back to revision 0 (undo commit)$
hg rollback
no rollback information available
Once you've rolled back one transaction in a repository, you can't roll back again in that repository until you perform another commit or pull.
If you make a modification to a file, and decide that you really didn't want to change the file at all, and you haven't yet committed your changes, the hg revert command is the one you'll need. It looks at the changeset that's the parent of the working directory, and restores the contents of the file to their state as of that changeset. (That's a long-winded way of saying that, in the normal case, it undoes your modifications.)
Let's illustrate how the hg revert command works with yet another small example. We'll begin by modifying a file that Mercurial is already tracking.
$
cat file
original content$
echo unwanted change >> file
$
hg diff file
diff -r cc12840d436b file --- a/file Thu Mar 17 05:08:55 2011 +0000 +++ b/file Thu Mar 17 05:08:55 2011 +0000 @@ -1,1 +1,2 @@ original content +unwanted change
If we don't want that change, we can simply hg revert the file.
$
hg status
M file$
hg revert file
$
cat file
original content
The hg revert command
provides us with an extra degree of safety by saving our
modified file with a .orig
extension.
$
hg status
? file.orig$
cat file.orig
original content unwanted change
Here is a summary of the cases that the hg revert command can deal with. We will describe each of these in more detail in the section that follows.
If you modify a file, it will restore the file to its unmodified state.
If you hg add a file, it will undo the “added” state of the file, but leave the file itself untouched.
If you delete a file without telling Mercurial, it will restore the file to its unmodified contents.
If you use the hg remove command to remove a file, it will undo the “removed” state of the file, and restore the file to its unmodified contents.
The hg revert command is useful for more than just modified files. It lets you reverse the results of all of Mercurial's file management commands—hg add, hg remove, and so on.
If you hg add a file, then decide that in fact you don't want Mercurial to track it, use hg revert to undo the add. Don't worry; Mercurial will not modify the file in any way. It will just “unmark” the file.
$
echo oops > oops
$
hg add oops
$
hg status oops
A oops$
hg revert oops
$
hg status
? oops
Similarly, if you ask Mercurial to hg remove a file, you can use hg revert to restore it to the contents it had as of the parent of the working directory.
$
hg remove file
$
hg status
R file$
hg revert file
$
hg status
$
ls file
file
This works just as well for a file that you deleted by hand, without telling Mercurial (recall that in Mercurial terminology, this kind of file is called “missing”).
$
rm file
$
hg status
! file$
hg revert file
$
ls file
file
If you revert a hg copy, the copied-to file remains in your working directory afterwards, untracked. Since a copy doesn't affect the copied-from file in any way, Mercurial doesn't do anything with the copied-from file.
$
hg copy file new-file
$
hg revert new-file
$
hg status
? new-file
Consider a case where you have committed a change a, and another change b on top of it; you then realise that change a was incorrect. Mercurial lets you “back out” an entire changeset automatically, and building blocks that let you reverse part of a changeset by hand.
Before you read this section, here's something to keep in mind: the hg backout command undoes the effect of a change by adding to your repository's history, not by modifying or erasing it. It's the right tool to use if you're fixing bugs, but not if you're trying to undo some change that has catastrophic consequences. To deal with those, see Section 9.4, “Changes that should never have been”.
The hg backout command lets you “undo” the effects of an entire changeset in an automated fashion. Because Mercurial's history is immutable, this command does not get rid of the changeset you want to undo. Instead, it creates a new changeset that reverses the effect of the to-be-undone changeset.
The operation of the hg backout command is a little intricate, so let's illustrate it with some examples. First, we'll create a repository with some simple changes.
$
hg init myrepo
$
cd myrepo
$
echo first change >> myfile
$
hg add myfile
$
hg commit -m 'first change'
$
echo second change >> myfile
$
hg commit -m 'second change'
The hg backout command
takes a single changeset ID as its argument; this is the
changeset to back out. Normally, hg
backout will drop you into a text editor to write
a commit message, so you can record why you're backing the
change out. In this example, we provide a commit message on
the command line using the -m
option.
We're going to start by backing out the last changeset we committed.
$
hg backout -m 'back out second change' tip
reverting myfile changeset 2:7e6f7eb64be6 backs out changeset 1:4cfa2767e880$
cat myfile
first change
You can see that the second line from
myfile
is no longer present. Taking a
look at the output of hg log
gives us an idea of what the hg
backout command has done.
$
hg log --style compact
2[tip] 7e6f7eb64be6 2011-03-17 05:08 +0000 bos back out second change 1 4cfa2767e880 2011-03-17 05:08 +0000 bos second change 0 12ce48809559 2011-03-17 05:08 +0000 bos first change
Notice that the new changeset that hg backout has created is a child of the changeset we backed out. It's easier to see this in Figure 9.1, “Backing out a change using the hg backout command”, which presents a graphical view of the change history. As you can see, the history is nice and linear.
If you want to back out a change other than the last one
you committed, pass the --merge
option to the
hg backout command.
$
cd ..
$
hg clone -r1 myrepo non-tip-repo
requesting all changes adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd non-tip-repo
This makes backing out any changeset a “one-shot” operation that's usually simple and fast.
$
echo third change >> myfile
$
hg commit -m 'third change'
$
hg backout --merge -m 'back out second change' 1
reverting myfile created new head changeset 3:3f3dcea52b6c backs out changeset 1:4cfa2767e880 merging with changeset 3:3f3dcea52b6c merging myfile 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit)
If you take a look at the contents of
myfile
after the backout finishes, you'll
see that the first and third changes are present, but not the
second.
$
cat myfile
first change third change
As the graphical history in Figure 9.2, “Automated backout of a non-tip change using the hg backout command” illustrates, Mercurial still commits one change in this kind of situation (the box-shaped node is the ones that Mercurial commits automatically), but the revision graph now looks different. Before Mercurial begins the backout process, it first remembers what the current parent of the working directory is. It then backs out the target changeset, and commits that as a changeset. Finally, it merges back to the previous parent of the working directory, but notice that it does not commit the result of the merge. The repository now contains two heads, and the working directory is in a merge state.
The result is that you end up “back where you were”, only with some extra history that undoes the effect of the changeset you wanted to back out.
You might wonder why Mercurial does not commit the result of the merge that it performed. The reason lies in Mercurial behaving conservatively: a merge naturally has more scope for error than simply undoing the effect of the tip changeset, so your work will be safest if you first inspect (and test!) the result of the merge, then commit it.
While I've recommended that you always use the --merge
option when backing
out a change, the hg backout
command lets you decide how to merge a backout changeset.
Taking control of the backout process by hand is something you
will rarely need to do, but it can be useful to understand
what the hg backout command
is doing for you automatically. To illustrate this, let's
clone our first repository, but omit the backout change that
it contains.
$
cd ..
$
hg clone -r1 myrepo newrepo
requesting all changes adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd newrepo
As with our earlier example, We'll commit a third changeset, then back out its parent, and see what happens.
$
echo third change >> myfile
$
hg commit -m 'third change'
$
hg backout -m 'back out second change' 1
reverting myfile created new head changeset 3:3f3dcea52b6c backs out changeset 1:4cfa2767e880 the backout changeset is a new head - do not forget to merge (use "backout --merge" if you want to auto-merge)
Our new changeset is again a descendant of the changeset we backout out; it's thus a new head, not a descendant of the changeset that was the tip. The hg backout command was quite explicit in telling us this.
$
hg log --style compact
3[tip]:1 3f3dcea52b6c 2011-03-17 05:08 +0000 bos back out second change 2 ad775e99f896 2011-03-17 05:08 +0000 bos third change 1 4cfa2767e880 2011-03-17 05:08 +0000 bos second change 0 12ce48809559 2011-03-17 05:08 +0000 bos first change
Again, it's easier to see what has happened by looking at a graph of the revision history, in Figure 9.3, “Backing out a change using the hg backout command”. This makes it clear that when we use hg backout to back out a change other than the tip, Mercurial adds a new head to the repository (the change it committed is box-shaped).
After the hg backout command has completed, it leaves the new “backout” changeset as the parent of the working directory.
$
hg parents
changeset: 2:ad775e99f896 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:31 2011 +0000 summary: third change
Now we have two isolated sets of changes.
$
hg heads
changeset: 3:3f3dcea52b6c tag: tip parent: 1:4cfa2767e880 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:31 2011 +0000 summary: back out second change changeset: 2:ad775e99f896 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:31 2011 +0000 summary: third change
Let's think about what we expect to see as the contents of
myfile
now. The first change should be
present, because we've never backed it out. The second change
should be missing, as that's the change we backed out. Since
the history graph shows the third change as a separate head,
we don't expect to see the third change
present in myfile
.
$
cat myfile
first change
To get the third change back into the file, we just do a normal merge of our two heads.
$
hg merge
abort: outstanding uncommitted changes (use 'hg status' to list changes)$
hg commit -m 'merged backout with previous tip'
$
cat myfile
first change
Afterwards, the graphical history of our repository looks like Figure 9.4, “Manually merging a backout change”.
Here's a brief description of how the hg backout command works.
It ensures that the working directory is “clean”, i.e. that the output of hg status would be empty.
It remembers the current parent of the working
directory. Let's call this changeset
orig
.
It does the equivalent of a hg update to sync the working
directory to the changeset you want to back out. Let's
call this changeset backout
.
It finds the parent of that changeset. Let's
call that changeset parent
.
For each file that the
backout
changeset affected, it does the
equivalent of a hg revert -r
parent on that file, to restore it to the
contents it had before that changeset was
committed.
It commits the result as a new changeset.
This changeset has backout
as its
parent.
If you specify --merge
on the command
line, it merges with orig
, and commits
the result of the merge.
An alternative way to implement the hg backout command would be to
hg export the
to-be-backed-out changeset as a diff, then use the --reverse
option to the
patch command to reverse the effect of the
change without fiddling with the working directory. This
sounds much simpler, but it would not work nearly as
well.
The reason that hg backout does an update, a commit, a merge, and another commit is to give the merge machinery the best chance to do a good job when dealing with all the changes between the change you're backing out and the current tip.
If you're backing out a changeset that's 100 revisions back in your project's history, the chances that the patch command will be able to apply a reverse diff cleanly are not good, because intervening changes are likely to have “broken the context” that patch uses to determine whether it can apply a patch (if this sounds like gibberish, see Section 12.4, “Understanding patches” for a discussion of the patch command). Also, Mercurial's merge machinery will handle files and directories being renamed, permission changes, and modifications to binary files, none of which patch can deal with.
Most of the time, the hg backout command is exactly what you need if you want to undo the effects of a change. It leaves a permanent record of exactly what you did, both when committing the original changeset and when you cleaned up after it.
On rare occasions, though, you may find that you've committed a change that really should not be present in the repository at all. For example, it would be very unusual, and usually considered a mistake, to commit a software project's object files as well as its source files. Object files have almost no intrinsic value, and they're big, so they increase the size of the repository and the amount of time it takes to clone or pull changes.
Before I discuss the options that you have if you commit a “brown paper bag” change (the kind that's so bad that you want to pull a brown paper bag over your head), let me first discuss some approaches that probably won't work.
Since Mercurial treats history as accumulative—every change builds on top of all changes that preceded it—you generally can't just make disastrous changes disappear. The one exception is when you've just committed a change, and it hasn't been pushed or pulled into another repository. That's when you can safely use the hg rollback command, as I detailed in Section 9.1.2, “Rolling back a transaction”.
After you've pushed a bad change to another repository, you could still use hg rollback to make your local copy of the change disappear, but it won't have the consequences you want. The change will still be present in the remote repository, so it will reappear in your local repository the next time you pull.
If a situation like this arises, and you know which repositories your bad change has propagated into, you can try to get rid of the change from every one of those repositories. This is, of course, not a satisfactory solution: if you miss even a single repository while you're expunging, the change is still “in the wild”, and could propagate further.
If you've committed one or more changes after the change that you'd like to see disappear, your options are further reduced. Mercurial doesn't provide a way to “punch a hole” in history, leaving changesets intact.
Since merges are often complicated, it is not unheard of for a merge to be mangled badly, but committed erroneously. Mercurial provides an important safeguard against bad merges by refusing to commit unresolved files, but human ingenuity guarantees that it is still possible to mess a merge up and commit it.
Given a bad merge that has been committed, usually the
best way to approach it is to simply try to repair the damage
by hand. A complete disaster that cannot be easily fixed up
by hand ought to be very rare, but the hg backout command may help in
making the cleanup easier. It offers a --parent
option, which lets
you specify which parent to revert to when backing out a
merge.
Suppose we have a revision graph like that in Figure 9.5, “A bad merge”. What we'd like is to redo the merge of revisions 2 and 3.
One way to do so would be as follows.
Call hg backout --rev=4 --parent=2. This tells hg backout to back out revision 4, which is the bad merge, and to when deciding which revision to prefer, to choose parent 2, one of the parents of the merge. The effect can be seen in Figure 9.6, “Backing out the merge, favoring one parent”.
Call hg backout --rev=4 --parent=3. This tells hg backout to back out revision 4 again, but this time to choose parent 3, the other parent of the merge. The result is visible in Figure 9.7, “Backing out the merge, favoring the other parent”, in which the repository now contains three heads.
Redo the bad merge by merging the two backout heads, which reduces the number of heads in the repository to two, as can be seen in Figure 9.8, “Merging the backouts”.
Merge with the commit that was made after the bad merge, as shown in Figure 9.9, “Merging the backouts”.
If you've committed some changes to your local repository and they've been pushed or pulled somewhere else, this isn't necessarily a disaster. You can protect yourself ahead of time against some classes of bad changeset. This is particularly easy if your team usually pulls changes from a central repository.
By configuring some hooks on that repository to validate incoming changesets (see chapter Chapter 10, Handling repository events with hooks), you can automatically prevent some kinds of bad changeset from being pushed to the central repository at all. With such a configuration in place, some kinds of bad changeset will naturally tend to “die out” because they can't propagate into the central repository. Better yet, this happens without any need for explicit intervention.
For instance, an incoming change hook that verifies that a changeset will actually compile can prevent people from inadvertently “breaking the build”.
Even a carefully run project can suffer an unfortunate event such as the committing and uncontrolled propagation of a file that contains important passwords.
If something like this happens to you, and the information that gets accidentally propagated is truly sensitive, your first step should be to mitigate the effect of the leak without trying to control the leak itself. If you are not 100% certain that you know exactly who could have seen the changes, you should immediately change passwords, cancel credit cards, or find some other way to make sure that the information that has leaked is no longer useful. In other words, assume that the change has propagated far and wide, and that there's nothing more you can do.
You might hope that there would be mechanisms you could use to either figure out who has seen a change or to erase the change permanently everywhere, but there are good reasons why these are not possible.
Mercurial does not provide an audit trail of who has pulled changes from a repository, because it is usually either impossible to record such information or trivial to spoof it. In a multi-user or networked environment, you should thus be extremely skeptical of yourself if you think that you have identified every place that a sensitive changeset has propagated to. Don't forget that people can and will send bundles by email, have their backup software save data offsite, carry repositories on USB sticks, and find other completely innocent ways to confound your attempts to track down every copy of a problematic change.
Mercurial also does not provide a way to make a file or changeset completely disappear from history, because there is no way to enforce its disappearance; someone could easily modify their copy of Mercurial to ignore such directives. In addition, even if Mercurial provided such a capability, someone who simply hadn't pulled a “make this file disappear” changeset wouldn't be affected by it, nor would web crawlers visiting at the wrong time, disk backups, or other mechanisms. Indeed, no distributed revision control system can make data reliably vanish. Providing the illusion of such control could easily give a false sense of security, and be worse than not providing it at all.
While it's all very well to be able to back out a changeset that introduced a bug, this requires that you know which changeset to back out. Mercurial provides an invaluable command, called hg bisect, that helps you to automate this process and accomplish it very efficiently.
The idea behind the hg bisect command is that a changeset has introduced some change of behavior that you can identify with a simple pass/fail test. You don't know which piece of code introduced the change, but you know how to test for the presence of the bug. The hg bisect command uses your test to direct its search for the changeset that introduced the code that caused the bug.
Here are a few scenarios to help you understand how you might apply this command.
The most recent version of your software has a bug that you remember wasn't present a few weeks ago, but you don't know when it was introduced. Here, your binary test checks for the presence of that bug.
You fixed a bug in a rush, and now it's time to close the entry in your team's bug database. The bug database requires a changeset ID when you close an entry, but you don't remember which changeset you fixed the bug in. Once again, your binary test checks for the presence of the bug.
Your software works correctly, but runs 15% slower than the last time you measured it. You want to know which changeset introduced the performance regression. In this case, your binary test measures the performance of your software, to see whether it's “fast” or “slow”.
The sizes of the components of your project that you ship exploded recently, and you suspect that something changed in the way you build your project.
From these examples, it should be clear that the hg bisect command is not useful only for finding the sources of bugs. You can use it to find any “emergent property” of a repository (anything that you can't find from a simple text search of the files in the tree) for which you can write a binary test.
We'll introduce a little bit of terminology here, just to make it clear which parts of the search process are your responsibility, and which are Mercurial's. A test is something that you run when hg bisect chooses a changeset. A probe is what hg bisect runs to tell whether a revision is good. Finally, we'll use the word “bisect”, as both a noun and a verb, to stand in for the phrase “search using the hg bisect command”.
One simple way to automate the searching process would be simply to probe every changeset. However, this scales poorly. If it took ten minutes to test a single changeset, and you had 10,000 changesets in your repository, the exhaustive approach would take on average 35 days to find the changeset that introduced a bug. Even if you knew that the bug was introduced by one of the last 500 changesets, and limited your search to those, you'd still be looking at over 40 hours to find the changeset that introduced your bug.
What the hg bisect command does is use its knowledge of the “shape” of your project's revision history to perform a search in time proportional to the logarithm of the number of changesets to check (the kind of search it performs is called a dichotomic search). With this approach, searching through 10,000 changesets will take less than three hours, even at ten minutes per test (the search will require about 14 tests). Limit your search to the last hundred changesets, and it will take only about an hour (roughly seven tests).
The hg bisect command is aware of the “branchy” nature of a Mercurial project's revision history, so it has no problems dealing with branches, merges, or multiple heads in a repository. It can prune entire branches of history with a single probe, which is how it operates so efficiently.
Here's an example of hg bisect in action.
Now let's create a repository, so that we can try out the hg bisect command in isolation.
$
hg init mybug
$
cd mybug
We'll simulate a project that has a bug in it in a simple-minded way: create trivial changes in a loop, and nominate one specific change that will have the “bug”. This loop creates 35 changesets, each adding a single file to the repository. We'll represent our “bug” with a file that contains the text “i have a gub”.
$
buggy_change=22
$
for (( i = 0; i < 35; i++ )); do
>
if [[ $i = $buggy_change ]]; then
>
echo 'i have a gub' > myfile$i
>
hg commit -q -A -m 'buggy changeset'
>
else
>
echo 'nothing to see here, move along' > myfile$i
>
hg commit -q -A -m 'normal changeset'
>
fi
>
done
The next thing that we'd like to do is figure out how to use the hg bisect command. We can use Mercurial's normal built-in help mechanism for this.
$
hg help bisect
hg bisect [-gbsr] [-U] [-c CMD] [REV] subdivision search of changesets This command helps to find changesets which introduce problems. To use, mark the earliest changeset you know exhibits the problem as bad, then mark the latest changeset which is free from the problem as good. Bisect will update your working directory to a revision for testing (unless the -U/--noupdate option is specified). Once you have performed tests, mark the working directory as good or bad, and bisect will either update to another candidate changeset or announce that it has found the bad revision. As a shortcut, you can also use the revision argument to mark a revision as good or bad without checking it out first. If you supply a command, it will be used for automatic bisection. Its exit status will be used to mark revisions as good or bad: status 0 means good, 125 means to skip the revision, 127 (command not found) will abort the bisection, and any other non-zero exit status means the revision is bad. Returns 0 on success. options: -r --reset reset bisect state -g --good mark changeset good -b --bad mark changeset bad -s --skip skip testing changeset -c --command CMD use command to check changeset state -U --noupdate do not update to target use "hg -v help bisect" to show global options
The hg bisect command works in steps. Each step proceeds as follows.
The process ends when hg bisect identifies a unique changeset that marks the point where your test transitioned from “succeeding” to “failing”.
To start the search, we must run the hg bisect --reset command.
$
hg bisect --reset
In our case, the binary test we use is simple: we check to see if any file in the repository contains the string “i have a gub”. If it does, this changeset contains the change that “caused the bug”. By convention, a changeset that has the property we're searching for is “bad”, while one that doesn't is “good”.
Most of the time, the revision to which the working directory is synced (usually the tip) already exhibits the problem introduced by the buggy change, so we'll mark it as “bad”.
$
hg bisect --bad
Our next task is to nominate a changeset that we know doesn't have the bug; the hg bisect command will “bracket” its search between the first pair of good and bad changesets. In our case, we know that revision 10 didn't have the bug. (I'll have more words about choosing the first “good” changeset later.)
$
hg bisect --good 10
Testing changeset 22:1f10bbdac74b (24 changesets remaining, ~4 tests) 0 files updated, 0 files merged, 12 files removed, 0 files unresolved
Notice that this command printed some output.
We now run our test in the working directory. We use the grep command to see if our “bad” file is present in the working directory. If it is, this revision is bad; if not, this revision is good.
$
if grep -q 'i have a gub' *
>
then
>
result=bad
>
else
>
result=good
>
fi
$
echo this revision is $result
this revision is bad$
hg bisect --$result
Testing changeset 16:e244c5b16526 (12 changesets remaining, ~3 tests) 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
This test looks like a perfect candidate for automation, so let's turn it into a shell function.
$
mytest() {
>
if grep -q 'i have a gub' *
>
then
>
result=bad
>
else
>
result=good
>
fi
>
echo this revision is $result
>
hg bisect --$result
>
}
We can now run an entire test step with a single command,
mytest
.
$
mytest
this revision is good Testing changeset 19:20942c6e8002 (6 changesets remaining, ~2 tests) 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
A few more invocations of our canned test step command, and we're done.
$
mytest
this revision is good Testing changeset 20:8e35b931a786 (3 changesets remaining, ~1 tests) 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
mytest
this revision is good Testing changeset 21:38fe4c413fcc (2 changesets remaining, ~1 tests) 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
mytest
this revision is good The first bad revision is: changeset: 22:1f10bbdac74b user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:08:34 2011 +0000 summary: buggy changeset
Even though we had 40 changesets to search through, the hg bisect command let us find the changeset that introduced our “bug” with only five tests. Because the number of tests that the hg bisect command performs grows logarithmically with the number of changesets to search, the advantage that it has over the “brute force” search approach increases with every changeset you add.
When you're finished using the hg bisect command in a repository, you can use the hg bisect --reset command to drop the information it was using to drive your search. The command doesn't use much space, so it doesn't matter if you forget to run this command. However, hg bisect won't let you start a new search in that repository until you do a hg bisect --reset.
$
hg bisect --reset
The hg bisect command requires that you correctly report the result of every test you perform. If you tell it that a test failed when it really succeeded, it might be able to detect the inconsistency. If it can identify an inconsistency in your reports, it will tell you that a particular changeset is both good and bad. However, it can't do this perfectly; it's about as likely to report the wrong changeset as the source of the bug.
When I started using the hg bisect command, I tried a few times to run my tests by hand, on the command line. This is an approach that I, at least, am not suited to. After a few tries, I found that I was making enough mistakes that I was having to restart my searches several times before finally getting correct results.
My initial problems with driving the hg bisect command by hand occurred even with simple searches on small repositories; if the problem you're looking for is more subtle, or the number of tests that hg bisect must perform increases, the likelihood of operator error ruining the search is much higher. Once I started automating my tests, I had much better results.
The key to automated testing is twofold:
In my tutorial example above, the grep
command tests for the symptom, and the if
statement takes the result of this check and ensures that we
always feed the same input to the hg
bisect command. The mytest
function marries these together in a reproducible way, so that
every test is uniform and consistent.
Because the output of a hg bisect search is only as good as the input you give it, don't take the changeset it reports as the absolute truth. A simple way to cross-check its report is to manually run your test at each of the following changesets:
It's possible that your search for one bug could be disrupted by the presence of another. For example, let's say your software crashes at revision 100, and worked correctly at revision 50. Unknown to you, someone else introduced a different crashing bug at revision 60, and fixed it at revision 80. This could distort your results in one of several ways.
It is possible that this other bug completely “masks” yours, which is to say that it occurs before your bug has a chance to manifest itself. If you can't avoid that other bug (for example, it prevents your project from building), and so can't tell whether your bug is present in a particular changeset, the hg bisect command cannot help you directly. Instead, you can mark a changeset as untested by running hg bisect --skip.
A different problem could arise if your test for a bug's presence is not specific enough. If you check for “my program crashes”, then both your crashing bug and an unrelated crashing bug that masks it will look like the same thing, and mislead hg bisect.
Another useful situation in which to use hg bisect --skip is if you can't test a revision because your project was in a broken and hence untestable state at that revision, perhaps because someone checked in a change that prevented the project from building.
Choosing the first “good” and “bad” changesets that will mark the end points of your search is often easy, but it bears a little discussion nevertheless. From the perspective of hg bisect, the “newest” changeset is conventionally “bad”, and the older changeset is “good”.
If you're having trouble remembering when a suitable “good” change was, so that you can tell hg bisect, you could do worse than testing changesets at random. Just remember to eliminate contenders that can't possibly exhibit the bug (perhaps because the feature with the bug isn't present yet) and those where another problem masks the bug (as I discussed above).
Even if you end up “early” by thousands of changesets or months of history, you will only add a handful of tests to the total number that hg bisect must perform, thanks to its logarithmic behavior.
Table of Contents
changegroup
—after
remote changesets addedcommit
—after a new
changeset is createdincoming
—after one
remote changeset is addedoutgoing
—after
changesets are propagatedprechangegroup
—before starting
to add remote changesetsprecommit
—before
starting to commit a changesetpreoutgoing
—before
starting to propagate changesetspretag
—before
tagging a changesetpretxnchangegroup
—before
completing addition of remote changesetspretxncommit
—before
completing commit of new changesetpreupdate
—before
updating or merging working directorytag
—after tagging a
changesetupdate
—after
updating or merging working directoryMercurial offers a powerful mechanism to let you perform automated actions in response to events that occur in a repository. In some cases, you can even control Mercurial's response to those events.
The name Mercurial uses for one of these actions is a hook. Hooks are called “triggers” in some revision control systems, but the two names refer to the same idea.
Here is a brief list of the hooks that Mercurial supports. We will revisit each of these hooks in more detail later, in Section 10.7, “Information for writers of hooks”.
Each of the hooks whose description begins with the word “Controlling” has the ability to determine whether an activity can proceed. If the hook succeeds, the activity may proceed; if it fails, the activity is either not permitted or undone, depending on the hook.
changegroup
: This
is run after a group of changesets has been brought into the
repository from elsewhere.
commit
: This is
run after a new changeset has been created in the local
repository.
incoming
: This is
run once for each new changeset that is brought into the
repository from elsewhere. Notice the difference from
changegroup
, which is run
once per group of changesets brought
in.
outgoing
: This is
run after a group of changesets has been transmitted from
this repository.
prechangegroup
:
This is run before starting to bring a group of changesets
into the repository.
precommit
:
Controlling. This is run before starting a commit.
preoutgoing
:
Controlling. This is run before starting to transmit a group
of changesets from this repository.
pretxnchangegroup
: Controlling. This
is run after a group of changesets has been brought into the
local repository from another, but before the transaction
completes that will make the changes permanent in the
repository.
pretxncommit
:
Controlling. This is run after a new changeset has been
created in the local repository, but before the transaction
completes that will make it permanent.
preupdate
:
Controlling. This is run before starting an update or merge
of the working directory.
update
: This is
run after an update or merge of the working directory has
finished.
When you run a Mercurial command in a repository, and the command causes a hook to run, that hook runs on your system, under your user account, with your privilege level. Since hooks are arbitrary pieces of executable code, you should treat them with an appropriate level of suspicion. Do not install a hook unless you are confident that you know who created it and what it does.
In some cases, you may be exposed to hooks that you did
not install yourself. If you work with Mercurial on an
unfamiliar system, Mercurial will run hooks defined in that
system's global ~/.hgrc
file.
If you are working with a repository owned by another
user, Mercurial can run hooks defined in that user's
repository, but it will still run them as “you”.
For example, if you hg pull
from that repository, and its .hg/hgrc
defines a local outgoing
hook, that hook will run
under your user account, even though you don't own that
repository.
To see what hooks are defined in a repository, use the hg showconfig hooks command. If you are working in one repository, but talking to another that you do not own (e.g. using hg pull or hg incoming), remember that it is the other repository's hooks you should be checking, not your own.
In Mercurial, hooks are not revision controlled, and do not propagate when you clone, or pull from, a repository. The reason for this is simple: a hook is a completely arbitrary piece of executable code. It runs under your user identity, with your privilege level, on your machine.
It would be extremely reckless for any distributed revision control system to implement revision-controlled hooks, as this would offer an easily exploitable way to subvert the accounts of users of the revision control system.
Since Mercurial does not propagate hooks, if you are collaborating with other people on a common project, you should not assume that they are using the same Mercurial hooks as you are, or that theirs are correctly configured. You should document the hooks you expect people to use.
In a corporate intranet, this is somewhat easier to
control, as you can for example provide a
“standard” installation of Mercurial on an NFS
filesystem, and use a site-wide ~/.hgrc
file to define hooks that all users will
see. However, this too has its limits; see below.
Mercurial allows you to override a hook definition by redefining the hook. You can disable it by setting its value to the empty string, or change its behavior as you wish.
If you deploy a system- or site-wide ~/.hgrc
file that defines some
hooks, you should thus understand that your users can disable
or override those hooks.
Sometimes you may want to enforce a policy that you do not
want others to be able to work around. For example, you may
have a requirement that every changeset must pass a rigorous
set of tests. Defining this requirement via a hook in a
site-wide ~/.hgrc
won't
work for remote users on laptops, and of course local users
can subvert it at will by overriding the hook.
Instead, you can set up your policies for use of Mercurial so that people are expected to propagate changes through a well-known “canonical” server that you have locked down and configured appropriately.
One way to do this is via a combination of social engineering and technology. Set up a restricted-access account; users can push changes over the network to repositories managed by this account, but they cannot log into the account and run normal shell commands. In this scenario, a user can commit a changeset that contains any old garbage they want.
When someone pushes a changeset to the server that everyone pulls from, the server will test the changeset before it accepts it as permanent, and reject it if it fails to pass the test suite. If people only pull changes from this filtering server, it will serve to ensure that all changes that people pull have been automatically vetted.
It is easy to write a Mercurial hook. Let's start with a
hook that runs when you finish a hg
commit, and simply prints the hash of the changeset
you just created. The hook is called commit
.
All hooks follow the pattern in this example.
$
hg init hook-test
$
cd hook-test
$
echo '[hooks]' >> .hg/hgrc
$
echo 'commit = echo committed $HG_NODE' >> .hg/hgrc
$
cat .hg/hgrc
[hooks] commit = echo committed $HG_NODE$
echo a > a
$
hg add a
$
hg commit -m 'testing commit hook'
committed 5e13f5636a2f9b68be460ee51f6325f5577050f9
You add an entry to the hooks
section of your ~/.hgrc
. On the left is the name of
the event to trigger on; on the right is the action to take. As
you can see, you can run an arbitrary shell command in a hook.
Mercurial passes extra information to the hook using environment
variables (look for HG_NODE
in the example).
Quite often, you will want to define more than one hook for a particular kind of event, as shown below.
$
echo 'commit.when = echo -n "date of commit: "; date' >> .hg/hgrc
$
echo a >> a
$
hg commit -m 'i have two hooks'
committed bda8a74dd3f18bf3c25fbeb73def663fc74fc003 date of commit: Thu Mar 17 05:09:01 GMT 2011
Mercurial lets you do this by adding an
extension to the end of a hook's name.
You extend a hook's name by giving the name of the hook,
followed by a full stop (the
“.
” character), followed by
some more text of your choosing. For example, Mercurial will
run both commit.foo
and
commit.bar
when the
commit
event occurs.
To give a well-defined order of execution when there are
multiple hooks defined for an event, Mercurial sorts hooks by
extension, and executes the hook commands in this sorted
order. In the above example, it will execute
commit.bar
before
commit.foo
, and commit
before both.
It is a good idea to use a somewhat descriptive extension when you define a new hook. This will help you to remember what the hook was for. If the hook fails, you'll get an error message that contains the hook name and extension, so using a descriptive extension could give you an immediate hint as to why the hook failed (see Section 10.3.2, “Controlling whether an activity can proceed” for an example).
In our earlier examples, we used the commit
hook, which is run after a
commit has completed. This is one of several Mercurial hooks
that run after an activity finishes. Such hooks have no way
of influencing the activity itself.
Mercurial defines a number of events that occur before an activity starts; or after it starts, but before it finishes. Hooks that trigger on these events have the added ability to choose whether the activity can continue, or will abort.
The pretxncommit
hook runs
after a commit has all but completed. In other words, the
metadata representing the changeset has been written out to
disk, but the transaction has not yet been allowed to
complete. The pretxncommit
hook has the ability to decide whether the transaction can
complete, or must be rolled back.
If the pretxncommit
hook
exits with a status code of zero, the transaction is allowed
to complete; the commit finishes; and the commit
hook is run. If the pretxncommit
hook exits with a
non-zero status code, the transaction is rolled back; the
metadata representing the changeset is erased; and the
commit
hook is not run.
$
cat check_bug_id
#!/bin/sh # check that a commit comment mentions a numeric bug id hg log -r $1 --template {desc} | grep -q "\<bug *[0-9]"$
echo 'pretxncommit.bug_id_required = ./check_bug_id $HG_NODE' >> .hg/hgrc
$
echo a >> a
$
hg commit -m 'i am not mentioning a bug id'
transaction abort! rollback completed abort: pretxncommit.bug_id_required hook exited with status 1$
hg commit -m 'i refer you to bug 666'
committed f27131613503698599fefb0c09642901f8eaaff0 date of commit: Thu Mar 17 05:09:02 GMT 2011
The hook in the example above checks that a commit comment contains a bug ID. If it does, the commit can complete. If not, the commit is rolled back.
When you are writing a hook, you might find it useful to run
Mercurial either with the -v
option, or the verbose
config item set to
“true”. When you do so, Mercurial will print a
message before it calls each hook.
You can write a hook either as a normal program—typically a shell script—or as a Python function that is executed within the Mercurial process.
Writing a hook as an external program has the advantage that it requires no knowledge of Mercurial's internals. You can call normal Mercurial commands to get any added information you need. The trade-off is that external hooks are slower than in-process hooks.
An in-process Python hook has complete access to the Mercurial API, and does not “shell out” to another process, so it is inherently faster than an external hook. It is also easier to obtain much of the information that a hook requires by using the Mercurial API than by running Mercurial commands.
If you are comfortable with Python, or require high performance, writing your hooks in Python may be a good choice. However, when you have a straightforward hook to write and you don't need to care about performance (probably the majority of hooks), a shell script is perfectly fine.
Mercurial calls each hook with a set of well-defined parameters. In Python, a parameter is passed as a keyword argument to your hook function. For an external program, a parameter is passed as an environment variable.
Whether your hook is written in Python or as a shell
script, the hook-specific parameter names and values will be
the same. A boolean parameter will be represented as a
boolean value in Python, but as the number 1 (for
“true”) or 0 (for “false”) as an
environment variable for an external hook. If a hook
parameter is named foo
, the keyword
argument for a Python hook will also be named
foo
, while the environment variable for an
external hook will be named HG_FOO
.
A hook that executes successfully must exit with a status of zero if external, or return boolean “false” if in-process. Failure is indicated with a non-zero exit status from an external hook, or an in-process hook returning boolean “true”. If an in-process hook raises an exception, the hook is considered to have failed.
For a hook that controls whether an activity can proceed, zero/false means “allow”, while non-zero/true/exception means “deny”.
When you define an external hook in your ~/.hgrc
and the hook is run, its
value is passed to your shell, which interprets it. This
means that you can use normal shell constructs in the body of
the hook.
An executable hook is always run with its current directory set to a repository's root directory.
Each hook parameter is passed in as an environment
variable; the name is upper-cased, and prefixed with the
string “HG_
”.
With the exception of hook parameters, Mercurial does not set or modify any environment variables when running a hook. This is useful to remember if you are writing a site-wide hook that may be run by a number of different users with differing environment variables set. In multi-user situations, you should not rely on environment variables being set to the values you have in your environment when testing the hook.
The ~/.hgrc
syntax
for defining an in-process hook is slightly different than for
an executable hook. The value of the hook must start with the
text “python:
”, and continue
with the fully-qualified name of a callable object to use as
the hook's value.
The module in which a hook lives is automatically imported
when a hook is run. So long as you have the module name and
PYTHONPATH
right, it should “just
work”.
The following ~/.hgrc
example snippet illustrates the syntax and meaning of the
notions we just described.
[hooks] commit.example = python:mymodule.submodule.myhook
When Mercurial runs the commit.example
hook, it imports mymodule.submodule
, looks
for the callable object named myhook
, and
calls it.
The simplest in-process hook does nothing, but illustrates the basic shape of the hook API:
def myhook(ui, repo, **kwargs): pass
The first argument to a Python hook is always a ui
object. The second
is a repository object; at the moment, it is always an
instance of localrepository
.
Following these two arguments are other keyword arguments.
Which ones are passed in depends on the hook being called, but
a hook can ignore arguments it doesn't care about by dropping
them into a keyword argument dict, as with
**kwargs
above.
It's hard to imagine a useful commit message being very
short. The simple pretxncommit
hook of the example below will prevent you from committing a
changeset with a message that is less than ten bytes long.
$
cat .hg/hgrc
[hooks] pretxncommit.msglen = test `hg tip --template {desc} | wc -c` -ge 10$
echo a > a
$
hg add a
$
hg commit -A -m 'too short'
transaction abort! rollback completed abort: pretxncommit.msglen hook exited with status 1$
hg commit -A -m 'long enough'
An interesting use of a commit-related hook is to help you to write cleaner code. A simple example of “cleaner code” is the dictum that a change should not add any new lines of text that contain “trailing whitespace”. Trailing whitespace is a series of space and tab characters at the end of a line of text. In most cases, trailing whitespace is unnecessary, invisible noise, but it is occasionally problematic, and people often prefer to get rid of it.
You can use either the precommit
or pretxncommit
hook to tell whether you
have a trailing whitespace problem. If you use the precommit
hook, the hook will not know
which files you are committing, so it will have to check every
modified file in the repository for trailing white space. If
you want to commit a change to just the file
foo
, but the file
bar
contains trailing whitespace, doing a
check in the precommit
hook
will prevent you from committing foo
due
to the problem with bar
. This doesn't
seem right.
Should you choose the pretxncommit
hook, the check won't
occur until just before the transaction for the commit
completes. This will allow you to check for problems only the
exact files that are being committed. However, if you entered
the commit message interactively and the hook fails, the
transaction will roll back; you'll have to re-enter the commit
message after you fix the trailing whitespace and run hg commit again.
$
cat .hg/hgrc
[hooks] pretxncommit.whitespace = hg export tip | (! egrep -q '^\+.*[ \t]$')$
echo 'a ' > a
$
hg commit -A -m 'test with trailing whitespace'
adding a transaction abort! rollback completed abort: pretxncommit.whitespace hook exited with status 1$
echo 'a' > a
$
hg commit -A -m 'drop trailing whitespace and try again'
In this example, we introduce a simple pretxncommit
hook that checks for
trailing whitespace. This hook is short, but not very
helpful. It exits with an error status if a change adds a
line with trailing whitespace to any file, but does not print
any information that might help us to identify the offending
file or line. It also has the nice property of not paying
attention to unmodified lines; only lines that introduce new
trailing whitespace cause problems.
#!/usr/bin/env python # # save as .hg/check_whitespace.py and make executable import re def trailing_whitespace(difflines): # linenum, header = 0, False for line in difflines: if header: # remember the name of the file that this diff affects m = re.match(r'(?:---|\+\+\+) ([^\t]+)', line) if m and m.group(1) != '/dev/null': filename = m.group(1).split('/', 1)[-1] if line.startswith('+++ '): header = False continue if line.startswith('diff '): header = True continue # hunk header - save the line number m = re.match(r'@@ -\d+,\d+ \+(\d+),', line) if m: linenum = int(m.group(1)) continue # hunk body - check for an added line with trailing whitespace m = re.match(r'\+.*\s$', line) if m: yield filename, linenum if line and line[0] in ' +': linenum += 1 if __name__ == '__main__': import os, sys added = 0 for filename, linenum in trailing_whitespace(os.popen('hg export tip')): print >> sys.stderr, ('%s, line %d: trailing whitespace added' % (filename, linenum)) added += 1 if added: # save the commit message so we don't need to retype it os.system('hg tip --template "{desc}" > .hg/commit.save') print >> sys.stderr, 'commit message saved to .hg/commit.save' sys.exit(1)
The above version is much more complex, but also more
useful. It parses a unified diff to see if any lines add
trailing whitespace, and prints the name of the file and the
line number of each such occurrence. Even better, if the
change adds trailing whitespace, this hook saves the commit
comment and prints the name of the save file before exiting
and telling Mercurial to roll the transaction back, so you can
use the -l filename
option to hg commit to reuse
the saved commit message once you've corrected the problem.
$
cat .hg/hgrc
[hooks] pretxncommit.whitespace = .hg/check_whitespace.py$
echo 'a ' >> a
$
hg commit -A -m 'add new line with trailing whitespace'
a, line 2: trailing whitespace added commit message saved to .hg/commit.save transaction abort! rollback completed abort: pretxncommit.whitespace hook exited with status 1$
sed -i 's, *$,,' a
$
hg commit -A -m 'trimmed trailing whitespace'
a, line 2: trailing whitespace added commit message saved to .hg/commit.save transaction abort! rollback completed abort: pretxncommit.whitespace hook exited with status 1
As a final aside, note in the example above the use of sed's in-place editing feature to get rid of trailing whitespace from a file. This is concise and useful enough that I will reproduce it here (using perl for good measure).
perl -pi -e 's,\s+$,,' filename
Mercurial ships with several bundled hooks. You can find
them in the hgext
directory of a Mercurial source tree. If you are using a
Mercurial binary package, the hooks will be located in the
hgext
directory of
wherever your package installer put Mercurial.
The acl
extension lets
you control which remote users are allowed to push changesets
to a networked server. You can protect any portion of a
repository (including the entire repo), so that a specific
remote user can push changes that do not affect the protected
portion.
This extension implements access control based on the identity of the user performing a push, not on who committed the changesets they're pushing. It makes sense to use this hook only if you have a locked-down server environment that authenticates remote users, and you want to be sure that only specific users are allowed to push changes to that server.
In order to manage incoming changesets, the acl
hook must be used as a
pretxnchangegroup
hook. This
lets it see which files are modified by each incoming
changeset, and roll back a group of changesets if they
modify “forbidden” files. Example:
[hooks] pretxnchangegroup.acl = python:hgext.acl.hook
The acl
extension is
configured using three sections.
The acl
section has
only one entry, sources
,
which lists the sources of incoming changesets that the hook
should pay attention to. You don't normally need to
configure this section.
serve
:
Control incoming changesets that are arriving from a
remote repository over http or ssh. This is the default
value of sources
, and
usually the only setting you'll need for this
configuration item.
pull
:
Control incoming changesets that are arriving via a pull
from a local repository.
push
:
Control incoming changesets that are arriving via a push
from a local repository.
bundle
:
Control incoming changesets that are arriving from
another repository via a bundle.
The acl.allow
section controls the users that are allowed to add
changesets to the repository. If this section is not
present, all users that are not explicitly denied are
allowed. If this section is present, all users that are not
explicitly allowed are denied (so an empty section means
that all users are denied).
The acl.deny
section determines which users are denied from adding
changesets to the repository. If this section is not
present or is empty, no users are denied.
The syntaxes for the acl.allow
and acl.deny
sections are
identical. On the left of each entry is a glob pattern that
matches files or directories, relative to the root of the
repository; on the right, a user name.
In the following example, the user
docwriter
can only push changes to the
docs
subtree of the
repository, while intern
can push changes
to any file or directory except source/sensitive
.
[acl.allow] docs/** = docwriter [acl.deny] source/sensitive/** = intern
If you want to test the acl
hook, run it with Mercurial's
debugging output enabled. Since you'll probably be running
it on a server where it's not convenient (or sometimes
possible) to pass in the --debug
option, don't forget
that you can enable debugging output in your ~/.hgrc
:
[ui] debug = true
With this enabled, the acl
hook will print enough
information to let you figure out why it is allowing or
forbidding pushes from specific users.
The bugzilla
extension
adds a comment to a Bugzilla bug whenever it finds a reference
to that bug ID in a commit comment. You can install this hook
on a shared server, so that any time a remote user pushes
changes to this server, the hook gets run.
It adds a comment to the bug that looks like this (you can configure the contents of the comment—see below):
Changeset aad8b264143a, made by Joe User <joe.user@domain.com> in the frobnitz repository, refers to this bug. For complete details, see http://hg.domain.com/frobnitz?cmd=changeset;node=aad8b264143a Changeset description: Fix bug 10483 by guarding against some NULL pointers
The value of this hook is that it automates the process of updating a bug any time a changeset refers to it. If you configure the hook properly, it makes it easy for people to browse straight from a Bugzilla bug to a changeset that refers to that bug.
You can use the code in this hook as a starting point for some more exotic Bugzilla integration recipes. Here are a few possibilities:
Require that every changeset pushed to the
server have a valid bug ID in its commit comment. In this
case, you'd want to configure the hook as a pretxncommit
hook. This would
allow the hook to reject changes that didn't contain bug
IDs.
Allow incoming changesets to automatically modify the state of a bug, as well as simply adding a comment. For example, the hook could recognise the string “fixed bug 31337” as indicating that it should update the state of bug 31337 to “requires testing”.
You should configure this hook in your server's
~/.hgrc
as an incoming
hook, for example as
follows:
[hooks] incoming.bugzilla = python:hgext.bugzilla.hook
Because of the specialised nature of this hook, and because Bugzilla was not written with this kind of integration in mind, configuring this hook is a somewhat involved process.
Before you begin, you must install the MySQL bindings for Python on the host(s) where you'll be running the hook. If this is not available as a binary package for your system, you can download it from [web:mysql-python].
Configuration information for this hook lives in the
bugzilla
section of
your ~/.hgrc
.
version
: The version
of Bugzilla installed on the server. The database
schema that Bugzilla uses changes occasionally, so this
hook has to know exactly which schema to use.
host
:
The hostname of the MySQL server that stores your
Bugzilla data. The database must be configured to allow
connections from whatever host you are running the
bugzilla
hook on.
user
:
The username with which to connect to the MySQL server.
The database must be configured to allow this user to
connect from whatever host you are running the bugzilla
hook on. This user
must be able to access and modify Bugzilla tables. The
default value of this item is bugs
,
which is the standard name of the Bugzilla user in a
MySQL database.
password
: The MySQL
password for the user you configured above. This is
stored as plain text, so you should make sure that
unauthorised users cannot read the ~/.hgrc
file where you
store this information.
db
:
The name of the Bugzilla database on the MySQL server.
The default value of this item is
bugs
, which is the standard name of
the MySQL database where Bugzilla stores its data.
notify
: If you want
Bugzilla to send out a notification email to subscribers
after this hook has added a comment to a bug, you will
need this hook to run a command whenever it updates the
database. The command to run depends on where you have
installed Bugzilla, but it will typically look something
like this, if you have Bugzilla installed in /var/www/html/bugzilla
:
cd /var/www/html/bugzilla && ./processmail %s nobody@nowhere.com
The Bugzilla
processmail
program expects to be
given a bug ID (the hook replaces
“%s
” with the bug ID)
and an email address. It also expects to be able to
write to some files in the directory that it runs in.
If Bugzilla and this hook are not installed on the same
machine, you will need to find a way to run
processmail
on the server where
Bugzilla is installed.
By default, the bugzilla
hook tries to use the
email address of a changeset's committer as the Bugzilla
user name with which to update a bug. If this does not suit
your needs, you can map committer email addresses to
Bugzilla user names using a usermap
section.
Each item in the usermap
section contains an
email address on the left, and a Bugzilla user name on the
right.
[usermap] jane.user@example.com = jane
You can either keep the usermap
data in a normal
~/.hgrc
, or tell the
bugzilla
hook to read the
information from an external usermap
file. In the latter case, you can store
usermap
data by itself in (for example)
a user-modifiable repository. This makes it possible to let
your users maintain their own usermap
entries. The main
~/.hgrc
file might look
like this:
# regular hgrc file refers to external usermap file [bugzilla] usermap = /home/hg/repos/userdata/bugzilla-usermap.conf
While the usermap
file that it
refers to might look like this:
# bugzilla-usermap.conf - inside a hg repository [usermap] stephanie@example.com = steph
You can configure the text that this hook adds as a
comment; you specify it in the form of a Mercurial template.
Several ~/.hgrc
entries
(still in the bugzilla
section) control this behavior.
strip
: The number of
leading path elements to strip from a repository's path
name to construct a partial path for a URL. For example,
if the repositories on your server live under /home/hg/repos
, and you
have a repository whose path is /home/hg/repos/app/tests
,
then setting strip
to
4
will give a partial path of
app/tests
. The
hook will make this partial path available when
expanding a template, as webroot
.
template
: The text of the
template to use. In addition to the usual
changeset-related variables, this template can use
hgweb
(the value of the
hgweb
configuration item above) and
webroot
(the path constructed using
strip
above).
In addition, you can add a baseurl
item to the web
section of your ~/.hgrc
. The bugzilla
hook will make this
available when expanding a template, as the base string to
use when constructing a URL that will let users browse from
a Bugzilla comment to view a changeset. Example:
[web] baseurl = http://hg.domain.com/
Here is an example set of bugzilla
hook config information.
[bugzilla] host = bugzilla.example.com password = mypassword version = 2.16 # server-side repos live in /home/hg/repos, so strip 4 leading # separators strip = 4 hgweb = http://hg.example.com/ usermap = /home/hg/repos/notify/bugzilla.conf template = Changeset {node|short}, made by {author} in the {webroot} repo, refers to this bug.\n For complete details, see {hgweb}{webroot}?cmd=changeset;node={node|short}\n Changeset description:\n \t{desc|tabindent}
The most common problems with configuring the bugzilla
hook relate to running
Bugzilla's processmail
script and
mapping committer names to user names.
Recall from Section 10.6.2.1, “Configuring the bugzilla
hook” above that the user
that runs the Mercurial process on the server is also the
one that will run the processmail
script. The processmail
script
sometimes causes Bugzilla to write to files in its
configuration directory, and Bugzilla's configuration files
are usually owned by the user that your web server runs
under.
You can cause processmail
to be run
with the suitable user's identity using the
sudo command. Here is an example entry
for a sudoers
file.
hg_user = (httpd_user) NOPASSWD: /var/www/html/bugzilla/processmail-wrapper %s
This allows the hg_user
user to run a
processmail-wrapper
program under the
identity of httpd_user
.
This indirection through a wrapper script is necessary,
because processmail
expects to be run
with its current directory set to wherever you installed
Bugzilla; you can't specify that kind of constraint in a
sudoers
file. The contents of the
wrapper script are simple:
#!/bin/sh cd `dirname $0` && ./processmail "$1" nobody@example.com
It doesn't seem to matter what email address you pass to
processmail
.
If your usermap
is
not set up correctly, users will see an error message from
the bugzilla
hook when they
push changes to the server. The error message will look
like this:
cannot find bugzilla user id for john.q.public@example.com
What this means is that the committer's address,
john.q.public@example.com
, is not a valid
Bugzilla user name, nor does it have an entry in your
usermap
that maps it to
a valid Bugzilla user name.
Although Mercurial's built-in web server provides RSS
feeds of changes in every repository, many people prefer to
receive change notifications via email. The notify
hook lets you send out
notifications to a set of email addresses whenever changesets
arrive that those subscribers are interested in.
As with the bugzilla
hook, the notify
hook is
template-driven, so you can customise the contents of the
notification messages that it sends.
By default, the notify
hook includes a diff of every changeset that it sends out; you
can limit the size of the diff, or turn this feature off
entirely. It is useful for letting subscribers review changes
immediately, rather than clicking to follow a URL.
You can set up the notify
hook to send one email
message per incoming changeset, or one per incoming group of
changesets (all those that arrived in a single pull or
push).
[hooks] # send one email per group of changes changegroup.notify = python:hgext.notify.hook # send one email per change incoming.notify = python:hgext.notify.hook
Configuration information for this hook lives in the
notify
section of a
~/.hgrc
file.
test
:
By default, this hook does not send out email at all;
instead, it prints the message that it
would send. Set this item to
false
to allow email to be sent. The
reason that sending of email is turned off by default is
that it takes several tries to configure this extension
exactly as you would like, and it would be bad form to
spam subscribers with a number of “broken”
notifications while you debug your configuration.
config
:
The path to a configuration file that contains
subscription information. This is kept separate from
the main ~/.hgrc
so
that you can maintain it in a repository of its own.
People can then clone that repository, update their
subscriptions, and push the changes back to your server.
strip
:
The number of leading path separator characters to strip
from a repository's path, when deciding whether a
repository has subscribers. For example, if the
repositories on your server live in /home/hg/repos
, and
notify
is considering a
repository named /home/hg/repos/shared/test
,
setting strip
to
4
will cause notify
to trim the path it
considers down to shared/test
, and it will
match subscribers against that.
template
: The template
text to use when sending messages. This specifies both
the contents of the message header and its body.
maxdiff
: The maximum
number of lines of diff data to append to the end of a
message. If a diff is longer than this, it is
truncated. By default, this is set to 300. Set this to
0
to omit diffs from notification
emails.
sources
: A list of
sources of changesets to consider. This lets you limit
notify
to only sending
out email about changes that remote users pushed into
this repository via a server, for example. See
Section 10.7.3.1, “Sources of changesets” for the sources you
can specify here.
If you set the baseurl
item in the web
section,
you can use it in a template; it will be available as
webroot
.
Here is an example set of notify
configuration information.
[notify] # really send email test = false # subscriber data lives in the notify repo config = /home/hg/repos/notify/notify.conf # repos live in /home/hg/repos on server, so strip 4 "/" chars strip = 4 template = X-Hg-Repo: {webroot}\n Subject: {webroot}: {desc|firstline|strip}\n From: {author} \n\n changeset {node|short} in {root} \n\ndetails: {baseurl}{webroot}?cmd=changeset;node={node|short} description: {desc|tabindent|strip} [web] baseurl = http://hg.example.com/
This will produce a message that looks like the following:
X-Hg-Repo: tests/slave Subject: tests/slave: Handle error case when slave has no buffers Date: Wed, 2 Aug 2006 15:25:46 -0700 (PDT) changeset 3cba9bfe74b5 in /home/hg/repos/tests/slave details: http://hg.example.com/tests/slave?cmd=changeset;node=3cba9bfe74b5 description: Handle error case when slave has no buffers diffs (54 lines): diff -r 9d95df7cf2ad -r 3cba9bfe74b5 include/tests.h --- a/include/tests.h Wed Aug 02 15:19:52 2006 -0700 +++ b/include/tests.h Wed Aug 02 15:25:26 2006 -0700 @@ -212,6 +212,15 @@ static __inline__ void test_headers(void *h) [...snip...]
An in-process hook is called with arguments of the following form:
def myhook(ui, repo, **kwargs): pass
The ui
parameter is a ui
object. The
repo
parameter is a localrepository
object. The names and values of the
**kwargs
parameters depend on the hook
being invoked, with the following common features:
If a parameter is named
node
or parentN
, it
will contain a hexadecimal changeset ID. The empty string
is used to represent “null changeset ID”
instead of a string of zeroes.
If a parameter is named
url
, it will contain the URL of a
remote repository, if that can be determined.
Boolean-valued parameters are represented as
Python bool
objects.
An in-process hook is called without a change to the process's working directory (unlike external hooks, which are run in the root of the repository). It must not change the process's working directory, or it will cause any calls it makes into the Mercurial API to fail.
If a hook returns a boolean “false” value, it is considered to have succeeded. If it returns a boolean “true” value or raises an exception, it is considered to have failed. A useful way to think of the calling convention is “tell me if you fail”.
Note that changeset IDs are passed into Python hooks as
hexadecimal strings, not the binary hashes that Mercurial's
APIs normally use. To convert a hash from hex to binary, use
the bin
function.
An external hook is passed to the shell of the user running Mercurial. Features of that shell, such as variable substitution and command redirection, are available. The hook is run in the root directory of the repository (unlike in-process hooks, which are run in the same directory that Mercurial was run in).
Hook parameters are passed to the hook as environment
variables. Each environment variable's name is converted in
upper case and prefixed with the string
“HG_
”. For example, if the
name of a parameter is “node
”,
the name of the environment variable representing that
parameter will be “HG_NODE
”.
A boolean parameter is represented as the string
“1
” for “true”,
“0
” for “false”.
If an environment variable is named HG_NODE
,
HG_PARENT1
or HG_PARENT2
, it
contains a changeset ID represented as a hexadecimal string.
The empty string is used to represent “null changeset
ID” instead of a string of zeroes. If an environment
variable is named HG_URL
, it will contain the
URL of a remote repository, if that can be determined.
If a hook exits with a status of zero, it is considered to have succeeded. If it exits with a non-zero status, it is considered to have failed.
A hook that involves the transfer of changesets between a local repository and another may be able to find out information about the “far side”. Mercurial knows how changes are being transferred, and in many cases where they are being transferred to or from.
Mercurial will tell a hook what means are, or were, used
to transfer changesets between repositories. This is
provided by Mercurial in a Python parameter named
source
, or an environment variable named
HG_SOURCE
.
serve
: Changesets are
transferred to or from a remote repository over http or
ssh.
pull
: Changesets are
being transferred via a pull from one repository into
another.
push
: Changesets are
being transferred via a push from one repository into
another.
bundle
: Changesets are
being transferred to or from a bundle.
When possible, Mercurial will tell a hook the location
of the “far side” of an activity that transfers
changeset data between repositories. This is provided by
Mercurial in a Python parameter named
url
, or an environment variable named
HG_URL
.
This information is not always known. If a hook is invoked in a repository that is being served via http or ssh, Mercurial cannot tell where the remote repository is, but it may know where the client is connecting from. In such cases, the URL will take one of the following forms:
This hook is run after a group of pre-existing changesets
has been added to the repository, for example via a hg pull or hg
unbundle. This hook is run once per operation
that added one or more changesets. This is in contrast to the
incoming
hook, which is run
once per changeset, regardless of whether the changesets
arrive in a group.
Some possible uses for this hook include kicking off an automated build or test of the added changesets, updating a bug database, or notifying subscribers that a repository contains new changes.
node
: A changeset ID. The
changeset ID of the first changeset in the group that was
added. All changesets between this and
tip
, inclusive, were added by a single
hg pull, hg push or hg unbundle.
source
: A
string. The source of these changes. See Section 10.7.3.1, “Sources of changesets” for details.
url
: A URL. The
location of the remote repository, if known. See Section 10.7.3.2, “Where changes are going—remote repository
URLs” for more information.
See also: incoming
(Section 10.8.3, “incoming
—after one
remote changeset is added”), prechangegroup
(Section 10.8.5, “prechangegroup
—before starting
to add remote changesets”), pretxnchangegroup
(Section 10.8.9, “pretxnchangegroup
—before
completing addition of remote changesets”)
This hook is run after a new changeset has been created.
See also: precommit
(Section 10.8.6, “precommit
—before
starting to commit a changeset”), pretxncommit
(Section 10.8.10, “pretxncommit
—before
completing commit of new changeset”)
This hook is run after a pre-existing changeset has been added to the repository, for example via a hg push. If a group of changesets was added in a single operation, this hook is called once for each added changeset.
You can use this hook for the same purposes as
the changegroup
hook (Section 10.8.1, “changegroup
—after
remote changesets added”); it's simply more
convenient sometimes to run a hook once per group of
changesets, while other times it's handier once per changeset.
source
: A
string. The source of these changes. See Section 10.7.3.1, “Sources of changesets” for details.
url
: A URL. The
location of the remote repository, if known. See Section 10.7.3.2, “Where changes are going—remote repository
URLs” for more information.
See also: changegroup
(Section 10.8.1, “changegroup
—after
remote changesets added”) prechangegroup
(Section 10.8.5, “prechangegroup
—before starting
to add remote changesets”), pretxnchangegroup
(Section 10.8.9, “pretxnchangegroup
—before
completing addition of remote changesets”)
This hook is run after a group of changesets has been propagated out of this repository, for example by a hg push or hg bundle command.
One possible use for this hook is to notify administrators that changes have been pulled.
node
: A changeset ID. The
changeset ID of the first changeset of the group that was
sent.
source
: A string. The
source of the of the operation (see Section 10.7.3.1, “Sources of changesets”). If a remote
client pulled changes from this repository,
source
will be
serve
. If the client that obtained
changes from this repository was local,
source
will be
bundle
, pull
, or
push
, depending on the operation the
client performed.
url
: A URL. The
location of the remote repository, if known. See Section 10.7.3.2, “Where changes are going—remote repository
URLs” for more information.
See also: preoutgoing
(Section 10.8.7, “preoutgoing
—before
starting to propagate changesets”)
This controlling hook is run before Mercurial begins to add a group of changesets from another repository.
This hook does not have any information about the changesets to be added, because it is run before transmission of those changesets is allowed to begin. If this hook fails, the changesets will not be transmitted.
One use for this hook is to prevent external changes from being added to a repository. For example, you could use this to “freeze” a server-hosted branch temporarily or permanently so that users cannot push to it, while still allowing a local administrator to modify the repository.
source
: A string. The
source of these changes. See Section 10.7.3.1, “Sources of changesets” for details.
url
: A URL. The
location of the remote repository, if known. See Section 10.7.3.2, “Where changes are going—remote repository
URLs” for more information.
See also: changegroup
(Section 10.8.1, “changegroup
—after
remote changesets added”), incoming
(Section 10.8.3, “incoming
—after one
remote changeset is added”), pretxnchangegroup
(Section 10.8.9, “pretxnchangegroup
—before
completing addition of remote changesets”)
This hook is run before Mercurial begins to commit a new changeset. It is run before Mercurial has any of the metadata for the commit, such as the files to be committed, the commit message, or the commit date.
One use for this hook is to disable the ability to commit new changesets, while still allowing incoming changesets. Another is to run a build or test, and only allow the commit to begin if the build or test succeeds.
If the commit proceeds, the parents of the working directory will become the parents of the new changeset.
See also: commit
(Section 10.8.2, “commit
—after a new
changeset is created”), pretxncommit
(Section 10.8.10, “pretxncommit
—before
completing commit of new changeset”)
This hook is invoked before Mercurial knows the identities of the changesets to be transmitted.
One use for this hook is to prevent changes from being transmitted to another repository.
source
: A
string. The source of the operation that is attempting to
obtain changes from this repository (see Section 10.7.3.1, “Sources of changesets”). See the documentation
for the source
parameter to the
outgoing
hook, in
Section 10.8.4, “outgoing
—after
changesets are propagated”, for possible values
of this parameter.
url
: A URL. The
location of the remote repository, if known. See Section 10.7.3.2, “Where changes are going—remote repository
URLs” for more information.
See also: outgoing
(Section 10.8.4, “outgoing
—after
changesets are propagated”)
This controlling hook is run before a tag is created. If the hook succeeds, creation of the tag proceeds. If the hook fails, the tag is not created.
If the tag to be created is
revision-controlled, the precommit
and pretxncommit
hooks (Section 10.8.2, “commit
—after a new
changeset is created” and Section 10.8.10, “pretxncommit
—before
completing commit of new changeset”) will also be run.
See also: tag
(Section 10.8.12, “tag
—after tagging a
changeset”)
This controlling hook is run before a transaction—that manages the addition of a group of new changesets from outside the repository—completes. If the hook succeeds, the transaction completes, and all of the changesets become permanent within this repository. If the hook fails, the transaction is rolled back, and the data for the changesets is erased.
This hook can access the metadata associated with the almost-added changesets, but it should not do anything permanent with this data. It must also not modify the working directory.
While this hook is running, if other Mercurial processes access this repository, they will be able to see the almost-added changesets as if they are permanent. This may lead to race conditions if you do not take steps to avoid them.
This hook can be used to automatically vet a group of changesets. If the hook fails, all of the changesets are “rejected” when the transaction rolls back.
node
: A changeset ID. The
changeset ID of the first changeset in the group that was
added. All changesets between this and
tip
,
inclusive, were added by a single hg pull, hg push or hg unbundle.
source
: A
string. The source of these changes. See Section 10.7.3.1, “Sources of changesets” for details.
url
: A URL. The
location of the remote repository, if known. See Section 10.7.3.2, “Where changes are going—remote repository
URLs” for more information.
See also: changegroup
(Section 10.8.1, “changegroup
—after
remote changesets added”), incoming
(Section 10.8.3, “incoming
—after one
remote changeset is added”), prechangegroup
(Section 10.8.5, “prechangegroup
—before starting
to add remote changesets”)
This controlling hook is run before a transaction—that manages a new commit—completes. If the hook succeeds, the transaction completes and the changeset becomes permanent within this repository. If the hook fails, the transaction is rolled back, and the commit data is erased.
This hook can access the metadata associated with the almost-new changeset, but it should not do anything permanent with this data. It must also not modify the working directory.
While this hook is running, if other Mercurial processes access this repository, they will be able to see the almost-new changeset as if it is permanent. This may lead to race conditions if you do not take steps to avoid them.
See also: precommit
(Section 10.8.6, “precommit
—before
starting to commit a changeset”)
This controlling hook is run before an update or merge of the working directory begins. It is run only if Mercurial's normal pre-update checks determine that the update or merge can proceed. If the hook succeeds, the update or merge may proceed; if it fails, the update or merge does not start.
parent1
: A
changeset ID. The ID of the parent that the working
directory is to be updated to. If the working directory
is being merged, it will not change this parent.
parent2
: A
changeset ID. Only set if the working directory is being
merged. The ID of the revision that the working directory
is being merged with.
See also: update
(Section 10.8.13, “update
—after
updating or merging working directory”)
This hook is run after a tag has been created.
If the created tag is revision-controlled, the commit
hook (section Section 10.8.2, “commit
—after a new
changeset is created”) is run before this hook.
See also: pretag
(Section 10.8.8, “pretag
—before
tagging a changeset”)
This hook is run after an update or merge of the working directory completes. Since a merge can fail (if the external hgmerge command fails to resolve conflicts in a file), this hook communicates whether the update or merge completed cleanly.
error
: A boolean.
Indicates whether the update or merge completed
successfully.
parent1
: A changeset ID.
The ID of the parent that the working directory was
updated to. If the working directory was merged, it will
not have changed this parent.
parent2
: A changeset ID.
Only set if the working directory was merged. The ID of
the revision that the working directory was merged with.
See also: preupdate
(Section 10.8.11, “preupdate
—before
updating or merging working directory”)
Table of Contents
Mercurial provides a powerful mechanism to let you control how it displays information. The mechanism is based on templates. You can use templates to generate specific output for a single command, or to customize the entire appearance of the built-in web interface.
Packaged with Mercurial are some output styles that you can use immediately. A style is simply a precanned template that someone wrote and installed somewhere that Mercurial can find.
Before we take a look at Mercurial's bundled styles, let's review its normal output.
$
hg log -r1
changeset: 1:98ec8953ce6a tag: mytag user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:16 2011 +0000 summary: added line to end of <<hello>> file.
This is somewhat informative, but it takes up a lot of
space—five lines of output per changeset. The
compact
style reduces this to three lines,
presented in a sparse manner.
$
hg log --style compact
3[tip] 950bbee487c2 2011-03-17 05:09 +0000 bos Added tag v0.1 for changeset 2f4851b75c8b 2[v0.1] 2f4851b75c8b 2011-03-17 05:09 +0000 bos Added tag mytag for changeset 98ec8953ce6a 1[mytag] 98ec8953ce6a 2011-03-17 05:09 +0000 bos added line to end of <<hello>> file. 0 948836c2a10e 2011-03-17 05:09 +0000 bos added hello
The changelog
style hints at the
expressive power of Mercurial's templating engine. This style
attempts to follow the GNU Project's changelog
guidelines[web:changelog].
$
hg log --style changelog
2011-03-17 Bryan O'Sullivan <bos@serpentine.com> * .hgtags: Added tag v0.1 for changeset 2f4851b75c8b [950bbee487c2] [tip] * .hgtags: Added tag mytag for changeset 98ec8953ce6a [2f4851b75c8b] [v0.1] * goodbye, hello: added line to end of <<hello>> file. in addition, added a file with the helpful name (at least i hope that some might consider it so) of goodbye. [98ec8953ce6a] [mytag] * hello: added hello [948836c2a10e]
You will not be shocked to learn that Mercurial's default
output style is named default
.
You can modify the output style that Mercurial will use
for every command by editing your ~/.hgrc
file, naming the style
you would prefer to use.
[ui] style = compact
If you write a style of your own, you can use it by either
providing the path to your style file, or copying your style
file into a location where Mercurial can find it (typically
the templates
subdirectory of your
Mercurial install directory).
All of Mercurial's
“log
-like” commands let you use
styles and templates: hg
incoming, hg log,
hg outgoing, and hg tip.
As I write this manual, these are so far the only commands that support styles and templates. Since these are the most important commands that need customizable output, there has been little pressure from the Mercurial user community to add style and template support to other commands.
At its simplest, a Mercurial template is a piece of text. Some of the text never changes, while other parts are expanded, or replaced with new text, when necessary.
Before we continue, let's look again at a simple example of Mercurial's normal output.
$
hg log -r1
changeset: 1:98ec8953ce6a tag: mytag user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:16 2011 +0000 summary: added line to end of <<hello>> file.
Now, let's run the same command, but using a template to change its output.
$
hg log -r1 --template 'i saw a changeset\n'
i saw a changeset
The example above illustrates the simplest possible
template; it's just a piece of static text, printed once for
each changeset. The --template
option to the hg log command tells Mercurial to use
the given text as the template when printing each
changeset.
Notice that the template string above ends with the text
“\n
”. This is an
escape sequence, telling Mercurial to print
a newline at the end of each template item. If you omit this
newline, Mercurial will run each piece of output together. See
Section 11.5, “Escape sequences” for more details
of escape sequences.
A template that prints a fixed string of text all the time isn't very useful; let's try something a bit more complex.
$
hg log --template 'i saw a changeset: {desc}\n'
i saw a changeset: Added tag v0.1 for changeset 2f4851b75c8b i saw a changeset: Added tag mytag for changeset 98ec8953ce6a i saw a changeset: added line to end of <<hello>> file. in addition, added a file with the helpful name (at least i hope that some might consider it so) of goodbye. i saw a changeset: added hello
As you can see, the string
“{desc}
” in the template has
been replaced in the output with the description of each
changeset. Every time Mercurial finds text enclosed in curly
braces (“{
” and
“}
”), it will try to replace the
braces and text with the expansion of whatever is inside. To
print a literal curly brace, you must escape it, as described in
Section 11.5, “Escape sequences”.
You can start writing simple templates immediately using the keywords below.
branches
: String. The
name of the branch on which the changeset was committed.
Will be empty if the branch name was
default
.
date
:
Date information. The date when the changeset was
committed. This is not human-readable;
you must pass it through a filter that will render it
appropriately. See Section 11.6, “Filtering keywords to change their results” for more information
on filters. The date is expressed as a pair of numbers. The
first number is a Unix UTC timestamp (seconds since January
1, 1970); the second is the offset of the committer's
timezone from UTC, in seconds.
files
: List of strings.
All files modified, added, or removed by this
changeset.
file_dels
: List of
strings. Files removed by this changeset.
node
:
String. The changeset identification hash, as a
40-character hexadecimal string.
rev
:
Integer. The repository-local changeset revision
number.
tags
:
List of strings. Any tags associated with the
changeset.
A few simple experiments will show us what to expect when we use these keywords; you can see the results below.
$
hg log -r1 --template 'author: {author}\n'
author: Bryan O'Sullivan <bos@serpentine.com>$
hg log -r1 --template 'desc:\n{desc}\n'
desc: added line to end of <<hello>> file. in addition, added a file with the helpful name (at least i hope that some might consider it so) of goodbye.$
hg log -r1 --template 'files: {files}\n'
files: goodbye hello$
hg log -r1 --template 'file_adds: {file_adds}\n'
file_adds: goodbye$
hg log -r1 --template 'file_dels: {file_dels}\n'
file_dels:$
hg log -r1 --template 'node: {node}\n'
node: 98ec8953ce6a395e0f089d7b0ef91ecb376cf7d5$
hg log -r1 --template 'parents: {parents}\n'
parents:$
hg log -r1 --template 'rev: {rev}\n'
rev: 1$
hg log -r1 --template 'tags: {tags}\n'
tags: mytag
As we noted above, the date keyword does not produce human-readable output, so we must treat it specially. This involves using a filter, about which more in Section 11.6, “Filtering keywords to change their results”.
$
hg log -r1 --template 'date: {date}\n'
date: 1300338556.00$
hg log -r1 --template 'date: {date|isodate}\n'
date: 2011-03-17 05:09 +0000
Mercurial's templating engine recognises the most commonly
used escape sequences in strings. When it sees a backslash
(“\
”) character, it looks at the
following character and substitutes the two characters with a
single replacement, as described below.
As indicated above, if you want the expansion of a template
to contain a literal “\
”,
“{
”, or
“{
” character, you must escape
it.
Some of the results of template expansion are not
immediately easy to use. Mercurial lets you specify an optional
chain of filters to modify the result of
expanding a keyword. You have already seen a common filter,
isodate
, in
action above, to make a date readable.
Below is a list of the most commonly used filters that Mercurial supports. While some filters can be applied to any text, others can only be used in specific circumstances. The name of each filter is followed first by an indication of where it can be used, then a description of its effect.
addbreaks
: Any text. Add
an XHTML “<br/>
” tag
before the end of every line except the last. For example,
“foo\nbar
” becomes
“foo<br/>\nbar
”.
age
: date
keyword. Render
the age of the date, relative to the current time. Yields a
string like “10
minutes
”.
basename
: Any text, but
most useful for the files
keyword and its
relatives. Treat the text as a path, and return the
basename. For example,
“foo/bar/baz
” becomes
“baz
”.
date
: date
keyword. Render a
date in a similar format to the Unix date
command, but with
timezone included. Yields a string like “Mon
Sep 04 15:13:13 2006 -0700
”.
domain
: Any text,
but most useful for the author
keyword. Finds
the first string that looks like an email address, and
extract just the domain component. For example,
“Bryan O'Sullivan
<bos@serpentine.com>
” becomes
“serpentine.com
”.
email
: Any text,
but most useful for the author
keyword. Extract
the first string that looks like an email address. For
example, “Bryan O'Sullivan
<bos@serpentine.com>
” becomes
“bos@serpentine.com
”.
escape
: Any text.
Replace the special XML/XHTML characters
“&
”,
“<
” and
“>
” with XML
entities.
fill68
: Any text. Wrap
the text to fit in 68 columns. This is useful before you
pass text through the tabindent
filter, and
still want it to fit in an 80-column fixed-font
window.
firstline
: Any text.
Yield the first line of text, without any trailing
newlines.
hgdate
: date
keyword. Render
the date as a pair of readable numbers. Yields a string
like “1157407993
25200
”.
isodate
: date
keyword. Render
the date as a text string in ISO 8601 format. Yields a
string like “2006-09-04 15:13:13
-0700
”.
obfuscate
: Any text, but
most useful for the author
keyword. Yield
the input text rendered as a sequence of XML entities. This
helps to defeat some particularly stupid screen-scraping
email harvesting spambots.
person
: Any text,
but most useful for the author
keyword. Yield
the text before an email address. For example,
“Bryan O'Sullivan
<bos@serpentine.com>
” becomes
“Bryan O'Sullivan
”.
rfc822date
:
date
keyword.
Render a date using the same format used in email headers.
Yields a string like “Mon, 04 Sep 2006
15:13:13 -0700
”.
short
: Changeset
hash. Yield the short form of a changeset hash, i.e. a
12-character hexadecimal string.
shortdate
: date
keyword. Render
the year, month, and day of the date. Yields a string like
“2006-09-04
”.
strip
:
Any text. Strip all leading and trailing whitespace from
the string.
tabindent
: Any text.
Yield the text, with every line except the first starting
with a tab character.
urlescape
: Any text.
Escape all characters that are considered
“special” by URL parsers. For example,
foo bar
becomes
foo%20bar
.
user
: Any text,
but most useful for the author
keyword. Return
the “user” portion of an email address. For
example, “Bryan O'Sullivan
<bos@serpentine.com>
” becomes
“bos
”.
$
hg log -r1 --template '{author}\n'
Bryan O'Sullivan <bos@serpentine.com>$
hg log -r1 --template '{author|domain}\n'
serpentine.com$
hg log -r1 --template '{author|email}\n'
bos@serpentine.com$
hg log -r1 --template '{author|obfuscate}\n' | cut -c-76
Bryan O'Sulli$
hg log -r1 --template '{author|person}\n'
Bryan O'Sullivan$
hg log -r1 --template '{author|user}\n'
bos$
hg log -r1 --template 'looks almost right, but actually garbage: {date}\n'
looks almost right, but actually garbage: 1300338556.00$
hg log -r1 --template '{date|age}\n'
2 seconds ago$
hg log -r1 --template '{date|date}\n'
Thu Mar 17 05:09:16 2011 +0000$
hg log -r1 --template '{date|hgdate}\n'
1300338556 0$
hg log -r1 --template '{date|isodate}\n'
2011-03-17 05:09 +0000$
hg log -r1 --template '{date|rfc822date}\n'
Thu, 17 Mar 2011 05:09:16 +0000$
hg log -r1 --template '{date|shortdate}\n'
2011-03-17$
hg log -r1 --template '{desc}\n' | cut -c-76
added line to end of <<hello>> file. in addition, added a file with the helpful name (at least i hope that some m$
hg log -r1 --template '{desc|addbreaks}\n' | cut -c-76
added line to end of <<hello>> file.<br/> <br/> in addition, added a file with the helpful name (at least i hope that some m$
hg log -r1 --template '{desc|escape}\n' | cut -c-76
added line to end of <<hello>> file. in addition, added a file with the helpful name (at least i hope that some m$
hg log -r1 --template '{desc|fill68}\n'
added line to end of <<hello>> file. in addition, added a file with the helpful name (at least i hope that some might consider it so) of goodbye.$
hg log -r1 --template '{desc|fill76}\n'
added line to end of <<hello>> file. in addition, added a file with the helpful name (at least i hope that some might consider it so) of goodbye.$
hg log -r1 --template '{desc|firstline}\n'
added line to end of <<hello>> file.$
hg log -r1 --template '{desc|strip}\n' | cut -c-76
added line to end of <<hello>> file. in addition, added a file with the helpful name (at least i hope that some m$
hg log -r1 --template '{desc|tabindent}\n' | expand | cut -c-76
added line to end of <<hello>> file. in addition, added a file with the helpful name (at least i hope tha$
hg log -r1 --template '{node}\n'
98ec8953ce6a395e0f089d7b0ef91ecb376cf7d5$
hg log -r1 --template '{node|short}\n'
98ec8953ce6a
It is easy to combine filters to yield output in the form you would like. The following chain of filters tidies up a description, then makes sure that it fits cleanly into 68 columns, then indents it by a further 8 characters (at least on Unix-like systems, where a tab is conventionally 8 characters wide).
$
hg log -r1 --template 'description:\n\t{desc|strip|fill68|tabindent}\n'
description: added line to end of <<hello>> file. in addition, added a file with the helpful name (at least i hope that some might consider it so) of goodbye.
Note the use of “\t
” (a
tab character) in the template to force the first line to be
indented; this is necessary since tabindent
indents all
lines except the first.
Keep in mind that the order of filters in a chain is
significant. The first filter is applied to the result of the
keyword; the second to the result of the first filter; and so
on. For example, using fill68|tabindent
gives very different results from
tabindent|fill68
.
A command line template provides a quick and simple way to format some output. Templates can become verbose, though, and it's useful to be able to give a template a name. A style file is a template with a name, stored in a file.
More than that, using a style file unlocks the power of
Mercurial's templating engine in ways that are not possible
using the command line --template
option.
Our simple style file contains just one line:
$
echo 'changeset = "rev: {rev}\n"' > rev
$
hg log -l1 --style ./rev
rev: 3
This tells Mercurial, “if you're printing a changeset, use the text on the right as the template”.
The syntax rules for a style file are simple.
If a line starts with either of the characters
“#
” or
“;
”, the entire line is
treated as a comment, and skipped as if empty.
A line starts with a keyword. This must start
with an alphabetic character or underscore, and can
subsequently contain any alphanumeric character or
underscore. (In regexp notation, a keyword must match
[A-Za-z_][A-Za-z0-9_]*
.)
The next element must be an
“=
” character, which can
be preceded or followed by an arbitrary amount of white
space.
If the rest of the line starts and ends with matching quote characters (either single or double quote), it is treated as a template body.
If the rest of the line does not start with a quote character, it is treated as the name of a file; the contents of this file will be read and used as a template body.
To illustrate how to write a style file, we will construct a few by example. Rather than provide a complete style file and walk through it, we'll mirror the usual process of developing a style file by starting with something very simple, and walking through a series of successively more complete examples.
If Mercurial encounters a problem in a style file you are working on, it prints a terse error message that, once you figure out what it means, is actually quite useful.
$
cat broken.style
changeset =
Notice that broken.style
attempts to
define a changeset
keyword, but forgets to
give any content for it. When instructed to use this style
file, Mercurial promptly complains.
$
hg log -r1 --style broken.style
** unknown exception encountered, details follow ** report bug details to http://mercurial.selenic.com/bts/ ** or mercurial@selenic.com ** Python 2.6.6 (r266:84292, Dec 27 2010, 00:02:40) [GCC 4.4.5] ** Mercurial Distributed SCM (version 1.6.4) ** Extensions loaded: Traceback (most recent call last): File "/usr/bin/hg", line 27, in <module> mercurial.dispatch.run() File "/usr/lib/pymodules/python2.6/mercurial/dispatch.py", line 16, in run sys.exit(dispatch(sys.argv[1:])) File "/usr/lib/pymodules/python2.6/mercurial/dispatch.py", line 34, in dispatch return _runcatch(u, args) File "/usr/lib/pymodules/python2.6/mercurial/dispatch.py", line 54, in _runcatch return _dispatch(ui, args) File "/usr/lib/pymodules/python2.6/mercurial/dispatch.py", line 494, in _dispatch cmdpats, cmdoptions) File "/usr/lib/pymodules/python2.6/mercurial/dispatch.py", line 355, in runcommand ret = _runcommand(ui, options, cmd, d) File "/usr/lib/pymodules/python2.6/mercurial/dispatch.py", line 545, in _runcommand return checkargs() File "/usr/lib/pymodules/python2.6/mercurial/dispatch.py", line 499, in checkargs return cmdfunc() File "/usr/lib/pymodules/python2.6/mercurial/dispatch.py", line 492, in <lambda> d = lambda: util.checksignature(func)(ui, *args, **cmdoptions) File "/usr/lib/pymodules/python2.6/mercurial/util.py", line 420, in check return func(*args, **kwargs) File "/usr/lib/pymodules/python2.6/mercurial/commands.py", line 2501, in log displayer = cmdutil.show_changeset(ui, repo, opts, True) File "/usr/lib/pymodules/python2.6/mercurial/cmdutil.py", line 981, in show_changeset t = changeset_templater(ui, repo, patch, opts, mapfile, buffered) File "/usr/lib/pymodules/python2.6/mercurial/cmdutil.py", line 850, in __init__ cache=defaulttempl) File "/usr/lib/pymodules/python2.6/mercurial/templater.py", line 194, in __init__ if val[0] in "'\"": IndexError: string index out of range
This error message looks intimidating, but it is not too hard to follow.
The first component is simply Mercurial's way of saying “I am giving up”.
___abort___: broken.style:1: parse error
Next comes the name of the style file that contains the error.
abort: ___broken.style___:1: parse error
Following the file name is the line number where the error was encountered.
abort: broken.style:___1___: parse error
Finally, a description of what went wrong.
abort: broken.style:1: ___parse error___
The description of the problem is not always clear (as in this case), but even when it is cryptic, it is almost always trivial to visually inspect the offending line in the style file and see what is wrong.
If you would like to be able to identify a Mercurial repository “fairly uniquely” using a short string as an identifier, you can use the first revision in the repository.
$
hg log -r0 --template '{node}'
435bbfee867467363da332eb8dc9e122943e0817
This is likely to be unique, and so it is useful in many cases. There are a few caveats.
Suppose we want to list the files changed by a changeset, one per line, with a little indentation before each file name.
$
cat > multiline << EOF
>
changeset = "Changed in {node|short}:\n{files}"
>
file = " {file}\n"
>
EOF
$
hg log --style multiline
Changed in 236a36b47120: .bashrc .hgrc test.c
Let's try to emulate the default output format used by another revision control tool, Subversion.
$
svn log -r9653
------------------------------------------------------------------------ r9653 | sean.hefty | 2006-09-27 14:39:55 -0700 (Wed, 27 Sep 2006) | 5 lines On reporting a route error, also include the status for the error, rather than indicating a status of 0 when an error has occurred. Signed-off-by: Sean Hefty <sean.hefty@intel.com> ------------------------------------------------------------------------
Since Subversion's output style is fairly simple, it is easy to copy-and-paste a hunk of its output into a file, and replace the text produced above by Subversion with the template values we'd like to see expanded.
$
cat svn.template
r{rev} | {author|user} | {date|isodate} ({date|rfc822date}) {desc|strip|fill76} ------------------------------------------------------------------------
There are a few small ways in which this template deviates from the output produced by Subversion.
Subversion prints a “readable”
date (the “Wed, 27 Sep 2006
” in the
example output above) in parentheses. Mercurial's
templating engine does not provide a way to display a date
in this format without also printing the time and time
zone.
We emulate Subversion's printing of
“separator” lines full of
“-
” characters by ending
the template with such a line. We use the templating
engine's header
keyword to print a separator line as the first line of
output (see below), thus achieving similar output to
Subversion.
Subversion's output includes a count in the header of the number of lines in the commit message. We cannot replicate this in Mercurial; the templating engine does not currently provide a filter that counts the number of lines the template generates.
It took me no more than a minute or two of work to replace literal text from an example of Subversion's output with some keywords and filters to give the template above. The style file simply refers to the template.
$
cat svn.style
header = '------------------------------------------------------------------------\n\n' changeset = svn.template
We could have included the text of the template file
directly in the style file by enclosing it in quotes and
replacing the newlines with
“\n
” sequences, but it would
have made the style file too difficult to read. Readability
is a good guide when you're trying to decide whether some text
belongs in a style file, or in a template file that the style
file points to. If the style file will look too big or
cluttered if you insert a literal piece of text, drop it into
a template instead.
Table of Contents
Here is a common scenario: you need to install a software package from source, but you find a bug that you must fix in the source before you can start using the package. You make your changes, forget about the package for a while, and a few months later you need to upgrade to a newer version of the package. If the newer version of the package still has the bug, you must extract your fix from the older source tree and apply it against the newer version. This is a tedious task, and it's easy to make mistakes.
This is a simple case of the “patch management” problem. You have an “upstream” source tree that you can't change; you need to make some local changes on top of the upstream tree; and you'd like to be able to keep those changes separate, so that you can apply them to newer versions of the upstream source.
The patch management problem arises in many situations. Probably the most visible is that a user of an open source software project will contribute a bug fix or new feature to the project's maintainers in the form of a patch.
Distributors of operating systems that include open source software often need to make changes to the packages they distribute so that they will build properly in their environments.
When you have few changes to maintain, it is easy to manage a single patch using the standard diff and patch programs (see Section 12.4, “Understanding patches” for a discussion of these tools). Once the number of changes grows, it starts to make sense to maintain patches as discrete “chunks of work,” so that for example a single patch will contain only one bug fix (the patch might modify several files, but it's doing “only one thing”), and you may have a number of such patches for different bugs you need fixed and local changes you require. In this situation, if you submit a bug fix patch to the upstream maintainers of a package and they include your fix in a subsequent release, you can simply drop that single patch when you're updating to the newer release.
Maintaining a single patch against an upstream tree is a little tedious and error-prone, but not difficult. However, the complexity of the problem grows rapidly as the number of patches you have to maintain increases. With more than a tiny number of patches in hand, understanding which ones you have applied and maintaining them moves from messy to overwhelming.
Fortunately, Mercurial includes a powerful extension, Mercurial Queues (or simply “MQ”), that massively simplifies the patch management problem.
During the late 1990s, several Linux kernel developers started to maintain “patch series” that modified the behavior of the Linux kernel. Some of these series were focused on stability, some on feature coverage, and others were more speculative.
The sizes of these patch series grew rapidly. In 2002, Andrew Morton published some shell scripts he had been using to automate the task of managing his patch queues. Andrew was successfully using these scripts to manage hundreds (sometimes thousands) of patches on top of the Linux kernel.
In early 2003, Andreas Gruenbacher and Martin Quinson borrowed the approach of Andrew's scripts and published a tool called “patchwork quilt” [web:quilt], or simply “quilt” (see [gruenbacher:2005] for a paper describing it). Because quilt substantially automated patch management, it rapidly gained a large following among open source software developers.
Quilt manages a stack of patches on top of a directory tree. To begin, you tell quilt to manage a directory tree, and tell it which files you want to manage; it stores away the names and contents of those files. To fix a bug, you create a new patch (using a single command), edit the files you need to fix, then “refresh” the patch.
The refresh step causes quilt to scan the directory tree; it updates the patch with all of the changes you have made. You can create another patch on top of the first, which will track the changes required to modify the tree from “tree with one patch applied” to “tree with two patches applied”.
You can change which patches are applied to the tree. If you “pop” a patch, the changes made by that patch will vanish from the directory tree. Quilt remembers which patches you have popped, though, so you can “push” a popped patch again, and the directory tree will be restored to contain the modifications in the patch. Most importantly, you can run the “refresh” command at any time, and the topmost applied patch will be updated. This means that you can, at any time, change both which patches are applied and what modifications those patches make.
Quilt knows nothing about revision control tools, so it works equally well on top of an unpacked tarball or a Subversion working copy.
In mid-2005, Chris Mason took the features of quilt and wrote an extension that he called Mercurial Queues, which added quilt-like behavior to Mercurial.
The key difference between quilt and MQ is that quilt knows nothing about revision control systems, while MQ is integrated into Mercurial. Each patch that you push is represented as a Mercurial changeset. Pop a patch, and the changeset goes away.
Because quilt does not care about revision control tools, it is still a tremendously useful piece of software to know about for situations where you cannot use Mercurial and MQ.
I cannot overstate the value that MQ offers through the unification of patches and revision control.
A major reason that patches have persisted in the free software and open source world—in spite of the availability of increasingly capable revision control tools over the years—is the agility they offer.
Traditional revision control tools make a permanent, irreversible record of everything that you do. While this has great value, it's also somewhat stifling. If you want to perform a wild-eyed experiment, you have to be careful in how you go about it, or you risk leaving unneeded—or worse, misleading or destabilising—traces of your missteps and errors in the permanent revision record.
By contrast, MQ's marriage of distributed revision control with patches makes it much easier to isolate your work. Your patches live on top of normal revision history, and you can make them disappear or reappear at will. If you don't like a patch, you can drop it. If a patch isn't quite as you want it to be, simply fix it—as many times as you need to, until you have refined it into the form you desire.
As an example, the integration of patches with revision control makes understanding patches and debugging their effects—and their interplay with the code they're based on—enormously easier. Since every applied patch has an associated changeset, you can give hg log a file name to see which changesets and patches affected the file. You can use the hg bisect command to binary-search through all changesets and applied patches to see where a bug got introduced or fixed. You can use the hg annotate command to see which changeset or patch modified a particular line of a source file. And so on.
Because MQ doesn't hide its patch-oriented nature, it is helpful to understand what patches are, and a little about the tools that work with them.
The traditional Unix diff command compares two files, and prints a list of differences between them. The patch command understands these differences as modifications to make to a file. Take a look below for a simple example of these commands in action.
$
echo 'this is my original thought' > oldfile
$
echo 'i have changed my mind' > newfile
$
diff -u oldfile newfile > tiny.patch
$
cat tiny.patch
--- oldfile 2011-03-17 05:09:03.000000000 +0000 +++ newfile 2011-03-17 05:09:03.000000000 +0000 @@ -1 +1 @@ -this is my original thought +i have changed my mind$
patch < tiny.patch
patching file oldfile$
cat oldfile
i have changed my mind
The type of file that diff generates (and patch takes as input) is called a “patch” or a “diff”; there is no difference between a patch and a diff. (We'll use the term “patch”, since it's more commonly used.)
A patch file can start with arbitrary text; the
patch command ignores this text, but MQ uses
it as the commit message when creating changesets. To find the
beginning of the patch content, patch
searches for the first line that starts with the string
“diff -
”.
MQ works with unified diffs (patch can accept several other diff formats, but MQ doesn't). A unified diff contains two kinds of header. The file header describes the file being modified; it contains the name of the file to modify. When patch sees a new file header, it looks for a file with that name to start modifying.
After the file header comes a series of hunks. Each hunk starts with a header; this identifies the range of line numbers within the file that the hunk should modify. Following the header, a hunk starts and ends with a few (usually three) lines of text from the unmodified file; these are called the context for the hunk. If there's only a small amount of context between successive hunks, diff doesn't print a new hunk header; it just runs the hunks together, with a few lines of context between modifications.
Each line of context begins with a space character. Within
the hunk, a line that begins with
“-
” means “remove this
line,” while a line that begins with
“+
” means “insert this
line.” For example, a line that is modified is
represented by one deletion and one insertion.
We will return to some of the more subtle aspects of patches later (in Section 12.6, “More about patches”), but you should have enough information now to use MQ.
Because MQ is implemented as an extension, you must
explicitly enable before you can use it. (You don't need to
download anything; MQ ships with the standard Mercurial
distribution.) To enable MQ, edit your ~/.hgrc
file, and add the lines
below.
[extensions] hgext.mq =
Once the extension is enabled, it will make a number of new commands available. To verify that the extension is working, you can use hg help to see if the qinit command is now available.
$
hg help qinit
hg qinit [-c] init a new queue repository (DEPRECATED) The queue repository is unversioned by default. If -c/--create-repo is specified, qinit will create a separate nested repository for patches (qinit -c may also be run later to convert an unversioned patch repository into a versioned one). You can use qcommit to commit changes to this queue repository. This command is deprecated. Without -c, it's implied by other relevant commands. With -c, use "hg init --mq" instead. options: -c --create-repo create queue repository use "hg -v help qinit" to show global options
You can use MQ with any Mercurial repository, and its commands only operate within that repository. To get started, simply prepare the repository using the qinit command.
$
hg init mq-sandbox
$
cd mq-sandbox
$
echo 'line 1' > file1
$
echo 'another line 1' > file2
$
hg add file1 file2
$
hg commit -m'first change'
$
hg qinit
This command creates an empty directory called .hg/patches
, where
MQ will keep its metadata. As with many Mercurial commands, the
qinit command prints nothing
if it succeeds.
To begin work on a new patch, use the qnew command. This command takes one argument, the name of the patch to create.
MQ will use this as the name of an actual file in the
.hg/patches
directory, as you
can see below.
$
hg tip
changeset: 0:de25809ae9a4 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:09 2011 +0000 summary: first change$
hg qnew first.patch
$
hg tip
changeset: 1:4a443885a8b1 tag: first.patch tag: qbase tag: qtip tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:09 2011 +0000 summary: [mq]: first.patch$
ls .hg/patches
first.patch series status
Also newly present in the .hg/patches
directory are two
other files, series
and
status
. The series
file lists all of the
patches that MQ knows about for this repository, with one
patch per line. Mercurial uses the status
file for internal
book-keeping; it tracks all of the patches that MQ has
applied in this repository.
Once you have created your new patch, you can edit files in the working directory as you usually would. All of the normal Mercurial commands, such as hg diff and hg annotate, work exactly as they did before.
When you reach a point where you want to save your work, use the qrefresh command to update the patch you are working on.
$
echo 'line 2' >> file1
$
hg diff
diff -r 4a443885a8b1 file1 --- a/file1 Thu Mar 17 05:09:09 2011 +0000 +++ b/file1 Thu Mar 17 05:09:09 2011 +0000 @@ -1,1 +1,2 @@ line 1 +line 2$
hg qrefresh
$
hg diff
$
hg tip --style=compact --patch
1[first.patch,qbase,qtip,tip] ba725d25e0f0 2011-03-17 05:09 +0000 bos [mq]: first.patch diff -r de25809ae9a4 -r ba725d25e0f0 file1 --- a/file1 Thu Mar 17 05:09:09 2011 +0000 +++ b/file1 Thu Mar 17 05:09:09 2011 +0000 @@ -1,1 +1,2 @@ line 1 +line 2
This command folds the changes you have made in the working directory into your patch, and updates its corresponding changeset to contain those changes.
You can run qrefresh as often as you like, so it's a good way to “checkpoint” your work. Refresh your patch at an opportune time; try an experiment; and if the experiment doesn't work out, hg revert your modifications back to the last time you refreshed.
$
echo 'line 3' >> file1
$
hg status
M file1$
hg qrefresh
$
hg tip --style=compact --patch
1[first.patch,qbase,qtip,tip] 0043b9b60184 2011-03-17 05:09 +0000 bos [mq]: first.patch diff -r de25809ae9a4 -r 0043b9b60184 file1 --- a/file1 Thu Mar 17 05:09:09 2011 +0000 +++ b/file1 Thu Mar 17 05:09:10 2011 +0000 @@ -1,1 +1,3 @@ line 1 +line 2 +line 3
Once you have finished working on a patch, or need to work on another, you can use the qnew command again to create a new patch. Mercurial will apply this patch on top of your existing patch.
$
hg qnew second.patch
$
hg log --style=compact --limit=2
2[qtip,second.patch,tip] 443ce9afa7e4 2011-03-17 05:09 +0000 bos [mq]: second.patch 1[first.patch,qbase] 0043b9b60184 2011-03-17 05:09 +0000 bos [mq]: first.patch$
echo 'line 4' >> file1
$
hg qrefresh
$
hg tip --style=compact --patch
2[qtip,second.patch,tip] c62e9f1a592f 2011-03-17 05:09 +0000 bos [mq]: second.patch diff -r 0043b9b60184 -r c62e9f1a592f file1 --- a/file1 Thu Mar 17 05:09:10 2011 +0000 +++ b/file1 Thu Mar 17 05:09:10 2011 +0000 @@ -1,3 +1,4 @@ line 1 line 2 line 3 +line 4$
hg annotate file1
0: line 1 1: line 2 1: line 3 2: line 4
Notice that the patch contains the changes in our prior patch as part of its context (you can see this more clearly in the output of hg annotate).
So far, with the exception of qnew and qrefresh, we've been careful to only use regular Mercurial commands. However, MQ provides many commands that are easier to use when you are thinking about patches, as illustrated below.
$
hg qseries
first.patch second.patch$
hg qapplied
first.patch second.patch
The previous discussion implied that there must be a difference between “known” and “applied” patches, and there is. MQ can manage a patch without it being applied in the repository.
An applied patch has a corresponding changeset in the repository, and the effects of the patch and changeset are visible in the working directory. You can undo the application of a patch using the qpop command. MQ still knows about, or manages, a popped patch, but the patch no longer has a corresponding changeset in the repository, and the working directory does not contain the changes made by the patch. Figure 12.1, “Applied and unapplied patches in the MQ patch stack” illustrates the difference between applied and tracked patches.
You can reapply an unapplied, or popped, patch using the qpush command. This creates a new changeset to correspond to the patch, and the patch's changes once again become present in the working directory. See below for examples of qpop and qpush in action.
$
hg qapplied
first.patch second.patch$
hg qpop
popping second.patch now at: first.patch$
hg qseries
first.patch second.patch$
hg qapplied
first.patch$
cat file1
line 1 line 2 line 3
Notice that once we have popped a patch or two patches, the output of qseries remains the same, while that of qapplied has changed.
While qpush and
qpop each operate on a
single patch at a time by default, you can push and pop many
patches in one go. The -a
option to
qpush causes it to push
all unapplied patches, while the -a
option to qpop causes it to pop all applied
patches. (For some more ways to push and pop many patches,
see Section 12.8, “Getting the best performance out of MQ” below.)
$
hg qpush -a
applying second.patch now at: second.patch$
cat file1
line 1 line 2 line 3 line 4
Several MQ commands check the working directory before
they do anything, and fail if they find any modifications.
They do this to ensure that you won't lose any changes that
you have made, but not yet incorporated into a patch. The
example below illustrates this; the qnew command will not create a
new patch if there are outstanding changes, caused in this
case by the hg add of
file3
.
$
echo 'file 3, line 1' >> file3
$
hg qnew add-file3.patch
$
hg qnew -f add-file3.patch
abort: patch "add-file3.patch" already exists
Commands that check the working directory all take an
“I know what I'm doing” option, which is always
named -f
. The exact meaning of
-f
depends on the command. For example,
hg qnew -f
will incorporate any outstanding changes into the new patch it
creates, but hg qpop -f
will revert modifications to any files affected by the patch
that it is popping. Be sure to read the documentation for a
command's -f
option before you use it!
The qrefresh command always refreshes the topmost applied patch. This means that you can suspend work on one patch (by refreshing it), pop or push to make a different patch the top, and work on that patch for a while.
Here's an example that illustrates how you can use this ability. Let's say you're developing a new feature as two patches. The first is a change to the core of your software, and the second—layered on top of the first—changes the user interface to use the code you just added to the core. If you notice a bug in the core while you're working on the UI patch, it's easy to fix the core. Simply qrefresh the UI patch to save your in-progress changes, and qpop down to the core patch. Fix the core bug, qrefresh the core patch, and qpush back to the UI patch to continue where you left off.
MQ uses the GNU patch command to apply patches, so it's helpful to know a few more detailed aspects of how patch works, and about patches themselves.
If you look at the file headers in a patch, you will notice that the pathnames usually have an extra component on the front that isn't present in the actual path name. This is a holdover from the way that people used to generate patches (people still do this, but it's somewhat rare with modern revision control tools).
Alice would unpack a tarball, edit her files, then decide
that she wanted to create a patch. So she'd rename her
working directory, unpack the tarball again (hence the need
for the rename), and use the -r
and -N
options to
diff to recursively generate a patch
between the unmodified directory and the modified one. The
result would be that the name of the unmodified directory
would be at the front of the left-hand path in every file
header, and the name of the modified directory would be at the
front of the right-hand path.
Since someone receiving a patch from the Alices of the net
would be unlikely to have unmodified and modified directories
with exactly the same names, the patch
command has a -p
option
that indicates the number of leading path name components to
strip when trying to apply a patch. This number is called the
strip count.
An option of “-p1
” means
“use a strip count of one”. If
patch sees a file name
foo/bar/baz
in a file header, it will
strip foo
and try to patch a file named
bar/baz
. (Strictly speaking, the strip
count refers to the number of path
separators (and the components that go with them
) to strip. A strip count of one will turn
foo/bar
into bar
,
but /foo/bar
(notice the extra leading
slash) into foo/bar
.)
The “standard” strip count for patches is one; almost all patches contain one leading path name component that needs to be stripped. Mercurial's hg diff command generates path names in this form, and the hg import command and MQ expect patches to have a strip count of one.
If you receive a patch from someone that you want to add
to your patch queue, and the patch needs a strip count other
than one, you cannot just qimport the patch, because
qimport does not yet have
a -p
option (see issue
311). Your best bet is to qnew a patch of your own, then
use patch -pN to apply their patch,
followed by hg addremove to
pick up any files added or removed by the patch, followed by
hg qrefresh. This
complexity may become unnecessary; see issue
311 for details.
When patch applies a hunk, it tries a handful of successively less accurate strategies to try to make the hunk apply. This falling-back technique often makes it possible to take a patch that was generated against an old version of a file, and apply it against a newer version of that file.
First, patch tries an exact match, where the line numbers, the context, and the text to be modified must apply exactly. If it cannot make an exact match, it tries to find an exact match for the context, without honouring the line numbering information. If this succeeds, it prints a line of output saying that the hunk was applied, but at some offset from the original line number.
If a context-only match fails, patch removes the first and last lines of the context, and tries a reduced context-only match. If the hunk with reduced context succeeds, it prints a message saying that it applied the hunk with a fuzz factor (the number after the fuzz factor indicates how many lines of context patch had to trim before the patch applied).
When neither of these techniques works,
patch prints a message saying that the hunk
in question was rejected. It saves rejected hunks (also
simply called “rejects”) to a file with the same
name, and an added .rej
extension. It also saves an unmodified copy of the file with
a .orig
extension; the
copy of the file without any extensions will contain any
changes made by hunks that did apply
cleanly. If you have a patch that modifies
foo
with six hunks, and one of them fails
to apply, you will have: an unmodified
foo.orig
, a foo.rej
containing one hunk, and foo
, containing
the changes made by the five successful hunks.
There are a few useful things to know about how patch works with files.
This should already be obvious, but patch cannot handle binary files.
Neither does it care about the executable bit; it creates new files as readable, but not executable.
patch treats the removal of a file as a diff between the file to be removed and the empty file. So your idea of “I deleted this file” looks like “every line of this file was deleted” in a patch.
It treats the addition of a file as a diff between the empty file and the file to be added. So in a patch, your idea of “I added this file” looks like “every line of this file was added”.
It treats a renamed file as the removal of the old name, and the addition of the new name. This means that renamed files have a big footprint in patches. (Note also that Mercurial does not currently try to infer when files have been renamed or copied in a patch.)
patch cannot represent empty files, so you cannot use a patch to represent the notion “I added this empty file to the tree”.
While applying a hunk at an offset, or with a fuzz factor, will often be completely successful, these inexact techniques naturally leave open the possibility of corrupting the patched file. The most common cases typically involve applying a patch twice, or at an incorrect location in the file. If patch or qpush ever mentions an offset or fuzz factor, you should make sure that the modified files are correct afterwards.
It's often a good idea to refresh a patch that has applied with an offset or fuzz factor; refreshing the patch generates new context information that will make it apply cleanly. I say “often,” not “always,” because sometimes refreshing a patch will make it fail to apply against a different revision of the underlying files. In some cases, such as when you're maintaining a patch that must sit on top of multiple versions of a source tree, it's acceptable to have a patch apply with some fuzz, provided you've verified the results of the patching process in such cases.
If qpush fails to
apply a patch, it will print an error message and exit. If it
has left .rej
files
behind, it is usually best to fix up the rejected hunks before
you push more patches or do any further work.
If your patch used to apply cleanly, and no longer does because you've changed the underlying code that your patches are based on, Mercurial Queues can help; see Section 12.9, “Updating your patches when the underlying code changes” for details.
Unfortunately, there aren't any great techniques for
dealing with rejected hunks. Most often, you'll need to view
the .rej
file and edit the
target file, applying the rejected hunks by hand.
A Linux kernel hacker, Chris Mason (the author of Mercurial Queues), wrote a tool called mpatch (http://oss.oracle.com/~mason/mpatch/), which takes a simple approach to automating the application of hunks rejected by patch. The mpatch command can help with four common reasons that a hunk may be rejected:
If you use mpatch, you should be doubly careful to check your results when you're done. In fact, mpatch enforces this method of double-checking the tool's output, by automatically dropping you into a merge program when it has done its job, so that you can verify its work and finish off any remaining merges.
As you grow familiar with MQ, you will find yourself wanting to perform other kinds of patch management operations.
If you want to get rid of a patch, use the hg qdelete command to delete the patch file and remove its entry from the patch series. If you try to delete a patch that is still applied, hg qdelete will refuse.
$
hg init myrepo
$
cd myrepo
$
hg qinit
$
hg qnew bad.patch
$
echo a > a
$
hg add a
$
hg qrefresh
$
hg qdelete bad.patch
abort: cannot delete applied patch bad.patch$
hg qpop
popping bad.patch patch queue now empty$
hg qdelete bad.patch
Once you're done working on a patch and want to turn it into a permanent changeset, use the hg qfinish command. Pass a revision to the command to identify the patch that you want to turn into a regular changeset; this patch must already be applied.
$
hg qnew good.patch
$
echo a > a
$
hg add a
$
hg qrefresh -m 'Good change'
$
hg qfinish tip
$
hg qapplied
$
hg tip --style=compact
0[tip] 227c2d680a81 2011-03-17 05:08 +0000 bos Good change
The hg qfinish command
accepts an --all
or -a
option, which turns all applied patches into regular
changesets.
It is also possible to turn an existing changeset into a
patch, by passing the -r
option to hg qimport.
$
hg qimport -r tip
$
hg qapplied
0.diff
Note that it only makes sense to convert a changeset into a patch if you have not propagated that changeset into any other repositories. The imported changeset's ID will change every time you refresh the patch, which will make Mercurial treat it as unrelated to the original changeset if you have pushed it somewhere else.
MQ is very efficient at handling a large number of patches. I ran some performance experiments in mid-2006 for a talk that I gave at the 2006 EuroPython conference (on modern hardware, you should expect better performance than you'll see below). I used as my data set the Linux 2.6.17-mm1 patch series, which consists of 1,738 patches. I applied these on top of a Linux kernel repository containing all 27,472 revisions between Linux 2.6.12-rc2 and Linux 2.6.17.
On my old, slow laptop, I was able to hg qpush -a
all
1,738 patches in 3.5 minutes, and hg qpop
-a
them all in 30 seconds. (On a newer laptop, the time to push
all patches dropped to two minutes.) I could qrefresh one of the biggest patches
(which made 22,779 lines of changes to 287 files) in 6.6
seconds.
Clearly, MQ is well suited to working in large trees, but there are a few tricks you can use to get the best performance of it.
First of all, try to “batch” operations together. Every time you run qpush or qpop, these commands scan the working directory once to make sure you haven't made some changes and then forgotten to run qrefresh. On a small tree, the time that this scan takes is unnoticeable. However, on a medium-sized tree (containing tens of thousands of files), it can take a second or more.
The qpush and qpop commands allow you to push and pop multiple patches at a time. You can identify the “destination patch” that you want to end up at. When you qpush with a destination specified, it will push patches until that patch is at the top of the applied stack. When you qpop to a destination, MQ will pop patches until the destination patch is at the top.
You can identify a destination patch using either the name of the patch, or by number. If you use numeric addressing, patches are counted from zero; this means that the first patch is zero, the second is one, and so on.
It's common to have a stack of patches on top of an underlying repository that you don't modify directly. If you're working on changes to third-party code, or on a feature that is taking longer to develop than the rate of change of the code beneath, you will often need to sync up with the underlying code, and fix up any hunks in your patches that no longer apply. This is called rebasing your patch series.
The simplest way to do this is to hg
qpop hg
-a
your patches, then hg pull changes into the underlying
repository, and finally hg qpush -a
your
patches again. MQ will stop pushing any time it runs across a
patch that fails to apply during conflicts, allowing you to fix
your conflicts, qrefresh the
affected patch, and continue pushing until you have fixed your
entire stack.
This approach is easy to use and works well if you don't expect changes to the underlying code to affect how well your patches apply. If your patch stack touches code that is modified frequently or invasively in the underlying repository, however, fixing up rejected hunks by hand quickly becomes tiresome.
It's possible to partially automate the rebasing process. If your patches apply cleanly against some revision of the underlying repo, MQ can use this information to help you to resolve conflicts between your patches and a different revision.
The process is a little involved.
To begin, hg qpush -a all of your patches on top of the revision where you know that they apply cleanly.
Save a backup copy of your patch directory using
hg qsave hg -e
hg -c
.
This prints the name of the directory that it has saved the
patches in. It will save the patches to a directory called
.hg/patches.N
, where
N
is a small integer. It also commits a
“save changeset” on top of your applied
patches; this is for internal book-keeping, and records the
states of the series
and
status
files.
Use hg pull to bring new changes into the underlying repository. (Don't run hg pull -u; see below for why.)
Update to the new tip revision, using hg update -C
to override
the patches you have pushed.
Merge all patches using hg qpush -m
-a. The -m
option to
qpush tells MQ to
perform a three-way merge if the patch fails to
apply.
During the hg qpush hg -m
,
each patch in the series
file is applied normally. If a patch applies with fuzz or
rejects, MQ looks at the queue you qsaved, and performs a three-way
merge with the corresponding changeset. This merge uses
Mercurial's normal merge machinery, so it may pop up a GUI merge
tool to help you to resolve problems.
When you finish resolving the effects of a patch, MQ refreshes your patch based on the result of the merge.
At the end of this process, your repository will have one
extra head from the old patch queue, and a copy of the old patch
queue will be in .hg/patches.N
. You can remove the
extra head using hg qpop -a -n
patches.N or hg
strip. You can delete .hg/patches.N
once you are sure
that you no longer need it as a backup.
MQ commands that work with patches let you refer to a patch
either by using its name or by a number. By name is obvious
enough; pass the name foo.patch
to qpush, for example, and it will
push patches until foo.patch
is
applied.
As a shortcut, you can refer to a patch using both a name
and a numeric offset; foo.patch-2
means
“two patches before foo.patch
”,
while bar.patch+4
means “four patches
after bar.patch
”.
Referring to a patch by index isn't much different. The first patch printed in the output of qseries is patch zero (yes, it's one of those start-at-zero counting systems); the second is patch one; and so on.
MQ also makes it easy to work with patches when you are
using normal Mercurial commands. Every command that accepts a
changeset ID will also accept the name of an applied patch. MQ
augments the tags normally in the repository with an eponymous
one for each applied patch. In addition, the special tags
qbase
and
qtip
identify
the “bottom-most” and topmost applied patches,
respectively.
These additions to Mercurial's normal tagging capabilities make dealing with patches even more of a breeze.
Want to patchbomb a mailing list with your latest series of changes?
hg email qbase:qtip
(Don't know what “patchbombing” is? See
Section 14.4, “Send changes via email with the patchbomb
extension”.)
Need to see all of the patches since
foo.patch
that have touched files in a
subdirectory of your tree?
hg log -r foo.patch:qtip subdir
Because MQ makes the names of patches available to the rest of Mercurial through its normal internal tag machinery, you don't need to type in the entire name of a patch when you want to identify it by name.
Another nice consequence of representing patch names as tags is that when you run the hg log command, it will display a patch's name as a tag, simply as part of its normal output. This makes it easy to visually distinguish applied patches from underlying “normal” revisions. The following example shows a few normal Mercurial commands in use with applied patches.
$
hg qapplied
first.patch second.patch$
hg log -r qbase:qtip
changeset: 1:064c8eee84ff tag: first.patch tag: qbase user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:06 2011 +0000 summary: [mq]: first.patch changeset: 2:68d8a146a80c tag: qtip tag: second.patch tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:06 2011 +0000 summary: [mq]: second.patch$
hg export second.patch
# HG changeset patch # User Bryan O'Sullivan <bos@serpentine.com> # Date 1300338546 0 # Node ID 68d8a146a80c0cf0ae53c4363690aa7ebeac666a # Parent 064c8eee84ffa9a251fbf8069ad3044e561b4e99 [mq]: second.patch diff -r 064c8eee84ff -r 68d8a146a80c other.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/other.c Thu Mar 17 05:09:06 2011 +0000 @@ -0,0 +1,1 @@ +double u;
There are a number of aspects of MQ usage that don't fit tidily into sections of their own, but that are good to know. Here they are, in one place.
Normally, when you qpop a patch and qpush it again, the changeset that represents the patch after the pop/push will have a different identity than the changeset that represented the hash beforehand. See Section B.1.14, “qpush—push patches onto the stack” for information as to why this is.
It's not a good idea to hg merge changes from another branch with a patch changeset, at least if you want to maintain the “patchiness” of that changeset and changesets below it on the patch stack. If you try to do this, it will appear to succeed, but MQ will become confused.
Because MQ's .hg/patches
directory resides
outside a Mercurial repository's working directory, the
“underlying” Mercurial repository knows nothing
about the management or presence of patches.
This presents the interesting possibility of managing the contents of the patch directory as a Mercurial repository in its own right. This can be a useful way to work. For example, you can work on a patch for a while, qrefresh it, then hg commit the current state of the patch. This lets you “roll back” to that version of the patch later on.
You can then share different versions of the same patch stack among multiple underlying repositories. I use this when I am developing a Linux kernel feature. I have a pristine copy of my kernel sources for each of several CPU architectures, and a cloned repository under each that contains the patches I am working on. When I want to test a change on a different architecture, I push my current patches to the patch repository associated with that kernel tree, pop and push all of my patches, and build and test that kernel.
Managing patches in a repository makes it possible for multiple developers to work on the same patch series without colliding with each other, all on top of an underlying source base that they may or may not control.
MQ helps you to work with the .hg/patches
directory as a
repository; when you prepare a repository for working with
patches using qinit, you
can pass the hg
-c
option to create the .hg/patches
directory as a
Mercurial repository.
As a convenience, if MQ notices that the .hg/patches
directory is a
repository, it will automatically hg
add every patch that you create and import.
MQ provides a shortcut command, qcommit, that runs hg commit in the .hg/patches
directory. This saves some bothersome typing.
Finally, as a convenience to manage the patch directory,
you can define the alias mq on Unix
systems. For example, on Linux systems using the
bash shell, you can include the following
snippet in your ~/.bashrc
.
alias mq=`hg -R $(hg root)/.hg/patches'
You can then issue commands of the form mq pull from the main repository.
MQ's support for working with a repository full of patches is limited in a few small respects.
MQ cannot automatically detect changes that you make to
the patch directory. If you hg
pull, manually edit, or hg
update changes to patches or the series
file, you will have to
hg qpop -a
and
then hg qpush -a
in
the underlying repository to see those changes show up there.
If you forget to do this, you can confuse MQ's idea of which
patches are applied.
Once you've been working with patches for a while, you'll find yourself hungry for tools that will help you to understand and manipulate the patches you're dealing with.
The diffstat command
[web:diffstat] generates a histogram of the
modifications made to each file in a patch. It provides a good
way to “get a sense of” a patch—which files
it affects, and how much change it introduces to each file and
as a whole. (I find that it's a good idea to use
diffstat's -p
option as a matter of
course, as otherwise it will try to do clever things with
prefixes of file names that inevitably confuse at least
me.)
$
diffstat -p1 remove-redundant-null-checks.patch
drivers/char/agp/sgi-agp.c | 5 ++--- drivers/char/hvcs.c | 11 +++++------ drivers/message/fusion/mptfc.c | 6 ++---- drivers/message/fusion/mptsas.c | 3 +-- drivers/net/fs_enet/fs_enet-mii.c | 3 +-- drivers/net/wireless/ipw2200.c | 22 ++++++---------------- drivers/scsi/libata-scsi.c | 4 +--- drivers/video/au1100fb.c | 3 +-- 8 files changed, 19 insertions(+), 38 deletions(-)$
filterdiff -i '*/video/*' remove-redundant-null-checks.patch
--- a/drivers/video/au1100fb.c~remove-redundant-null-checks-before-free-in-drivers +++ a/drivers/video/au1100fb.c @@ -743,8 +743,7 @@ void __exit au1100fb_cleanup(void) { driver_unregister(&au1100fb_driver); - if (drv_info.opt_mode) - kfree(drv_info.opt_mode); + kfree(drv_info.opt_mode); } module_init(au1100fb_init);
The patchutils
package
[web:patchutils] is invaluable. It provides a
set of small utilities that follow the “Unix
philosophy;” each does one useful thing with a patch.
The patchutils
command I use
most is filterdiff, which extracts subsets
from a patch file. For example, given a patch that modifies
hundreds of files across dozens of directories, a single
invocation of filterdiff can generate a
smaller patch that only touches files whose names match a
particular glob pattern. See Section 13.9.2, “Viewing the history of a patch” for another
example.
Whether you are working on a patch series to submit to a free software or open source project, or a series that you intend to treat as a sequence of regular changesets when you're done, you can use some simple techniques to keep your work well organized.
Give your patches descriptive names. A good name for a
patch might be rework-device-alloc.patch
,
because it will immediately give you a hint what the purpose of
the patch is. Long names shouldn't be a problem; you won't be
typing the names often, but you will be
running commands like qapplied and qtop over and over. Good naming
becomes especially important when you have a number of patches
to work with, or if you are juggling a number of different tasks
and your patches only get a fraction of your attention.
Be aware of what patch you're working on. Use the qtop command and skim over the text
of your patches frequently—for example, using hg tip -p
)—to be sure
of where you stand. I have several times worked on and qrefreshed a patch other than the
one I intended, and it's often tricky to migrate changes into
the right patch after making them in the wrong one.
For this reason, it is very much worth investing a little time to learn how to use some of the third-party tools I described in Section 12.13, “Third party tools for working with patches”, particularly diffstat and filterdiff. The former will give you a quick idea of what changes your patch is making, while the latter makes it easy to splice hunks selectively out of one patch and into another.
Because the overhead of dropping files into a new Mercurial repository is so low, it makes a lot of sense to manage patches this way even if you simply want to make a few changes to a source tarball that you downloaded.
Begin by downloading and unpacking the source tarball, and turning it into a Mercurial repository.
$
download netplug-1.2.5.tar.bz2
$
tar jxf netplug-1.2.5.tar.bz2
$
cd netplug-1.2.5
$
hg init
$
hg commit -q --addremove --message netplug-1.2.5
$
cd ..
$
hg clone netplug-1.2.5 netplug
updating to branch default 18 files updated, 0 files merged, 0 files removed, 0 files unresolved
Continue by creating a patch stack and making your changes.
$
cd netplug
$
hg qinit
$
hg qnew -m 'fix build problem with gcc 4' build-fix.patch
$
perl -pi -e 's/int addr_len/socklen_t addr_len/' netlink.c
$
hg qrefresh
$
hg tip -p
changeset: 1:dba5bb929e1e tag: build-fix.patch tag: qbase tag: qtip tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 05:09:07 2011 +0000 summary: fix build problem with gcc 4 diff -r 77af328ffc91 -r dba5bb929e1e netlink.c --- a/netlink.c Thu Mar 17 05:09:07 2011 +0000 +++ b/netlink.c Thu Mar 17 05:09:07 2011 +0000 @@ -275,7 +275,7 @@ exit(1); } - int addr_len = sizeof(addr); + socklen_t addr_len = sizeof(addr); if (getsockname(fd, (struct sockaddr *) &addr, &addr_len) == -1) { do_log(LOG_ERR, "Could not get socket details: %m");
Let's say a few weeks or months pass, and your package author releases a new version. First, bring their changes into the repository.
$
hg qpop -a
popping build-fix.patch patch queue now empty$
cd ..
$
download netplug-1.2.8.tar.bz2
$
hg clone netplug-1.2.5 netplug-1.2.8
updating to branch default 18 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd netplug-1.2.8
$
hg locate -0 | xargs -0 rm
$
cd ..
$
tar jxf netplug-1.2.8.tar.bz2
$
cd netplug-1.2.8
$
hg commit --addremove --message netplug-1.2.8
The pipeline starting with hg
locate above deletes all files in the working
directory, so that hg
commit's --addremove
option can
actually tell which files have really been removed in the
newer version of the source.
Finally, you can apply your patches on top of the new tree.
$
cd ../netplug
$
hg pull ../netplug-1.2.8
pulling from ../netplug-1.2.8 searching for changes adding changesets adding manifests adding file changes added 1 changesets with 12 changes to 12 files (run 'hg update' to get a working copy)$
hg qpush -a
(working directory not at a head) applying build-fix.patch now at: build-fix.patch
MQ provides a command, qfold that lets you combine entire patches. This “folds” the patches you name, in the order you name them, into the topmost applied patch, and concatenates their descriptions onto the end of its description. The patches that you fold must be unapplied before you fold them.
The order in which you fold patches matters. If your
topmost applied patch is foo
, and you
qfold
bar
and quux
into it,
you will end up with a patch that has the same effect as if
you applied first foo
, then
bar
, followed by
quux
.
Merging part of one patch into another is more difficult than combining entire patches.
If you want to move changes to entire files, you can use
filterdiff's -i
and -x
options to choose the
modifications to snip out of one patch, concatenating its
output onto the end of the patch you want to merge into. You
usually won't need to modify the patch you've merged the
changes from. Instead, MQ will report some rejected hunks
when you qpush it (from
the hunks you moved into the other patch), and you can simply
qrefresh the patch to drop
the duplicate hunks.
If you have a patch that has multiple hunks modifying a file, and you only want to move a few of those hunks, the job becomes more messy, but you can still partly automate it. Use lsdiff -nvv to print some metadata about the patch.
$
lsdiff -nvv remove-redundant-null-checks.patch
22 File #1 a/drivers/char/agp/sgi-agp.c 24 Hunk #1 static int __devinit agp_sgi_init(void) 37 File #2 a/drivers/char/hvcs.c 39 Hunk #1 static struct tty_operations hvcs_ops = 53 Hunk #2 static int hvcs_alloc_index_list(int n) 69 File #3 a/drivers/message/fusion/mptfc.c 71 Hunk #1 mptfc_GetFcDevPage0(MPT_ADAPTER *ioc, in 85 File #4 a/drivers/message/fusion/mptsas.c 87 Hunk #1 mptsas_probe_hba_phys(MPT_ADAPTER *ioc) 98 File #5 a/drivers/net/fs_enet/fs_enet-mii.c 100 Hunk #1 static struct fs_enet_mii_bus *create_bu 111 File #6 a/drivers/net/wireless/ipw2200.c 113 Hunk #1 static struct ipw_fw_error *ipw_alloc_er 126 Hunk #2 static ssize_t clear_error(struct device 140 Hunk #3 static void ipw_irq_tasklet(struct ipw_p 150 Hunk #4 static void ipw_pci_remove(struct pci_de 164 File #7 a/drivers/scsi/libata-scsi.c 166 Hunk #1 int ata_cmd_ioctl(struct scsi_device *sc 178 File #8 a/drivers/video/au1100fb.c 180 Hunk #1 void __exit au1100fb_cleanup(void)
This command prints three different kinds of number:
You'll have to use some visual inspection, and reading of
the patch, to identify the file and hunk numbers you'll want,
but you can then pass them to to
filterdiff's --files
and --hunks
options, to
select exactly the file and hunk you want to extract.
Once you have this hunk, you can concatenate it onto the end of your destination patch and continue with the remainder of Section 12.15.2, “Combining entire patches”.
If you are already familiar with quilt, MQ provides a similar command set. There are a few differences in the way that it works.
You will already have noticed that most quilt commands have
MQ counterparts that simply begin with a
“q
”. The exceptions are quilt's
add
and remove
commands,
the counterparts for which are the normal Mercurial hg add and hg
remove commands. There is no MQ equivalent of the
quilt edit
command.
Table of Contents
series
fileWhile it's easy to pick up straightforward uses of Mercurial Queues, use of a little discipline and some of MQ's less frequently used capabilities makes it possible to work in complicated development environments.
In this chapter, I will use as an example a technique I have used to manage the development of an Infiniband device driver for the Linux kernel. The driver in question is large (at least as drivers go), with 25,000 lines of code spread across 35 source files. It is maintained by a small team of developers.
While much of the material in this chapter is specific to Linux, the same principles apply to any code base for which you're not the primary owner, and upon which you need to do a lot of development.
The Linux kernel changes rapidly, and has never been internally stable; developers frequently make drastic changes between releases. This means that a version of the driver that works well with a particular released version of the kernel will not even compile correctly against, typically, any other version.
To maintain a driver, we have to keep a number of distinct versions of Linux in mind.
One target is the main Linux kernel development tree. Maintenance of the code is in this case partly shared by other developers in the kernel community, who make “drive-by” modifications to the driver as they develop and refine kernel subsystems.
We also maintain a number of “backports” to older versions of the Linux kernel, to support the needs of customers who are running older Linux distributions that do not incorporate our drivers. (To backport a piece of code is to modify it to work in an older version of its target environment than the version it was developed for.)
Finally, we make software releases on a schedule that is necessarily not aligned with those used by Linux distributors and kernel developers, so that we can deliver new features to customers without forcing them to upgrade their entire kernels or distributions.
There are two “standard” ways to maintain a piece of software that has to target many different environments.
The first is to maintain a number of branches, each intended for a single target. The trouble with this approach is that you must maintain iron discipline in the flow of changes between repositories. A new feature or bug fix must start life in a “pristine” repository, then percolate out to every backport repository. Backport changes are more limited in the branches they should propagate to; a backport change that is applied to a branch where it doesn't belong will probably stop the driver from compiling.
The second is to maintain a single source tree filled with conditional statements that turn chunks of code on or off depending on the intended target. Because these “ifdefs” are not allowed in the Linux kernel tree, a manual or automatic process must be followed to strip them out and yield a clean tree. A code base maintained in this fashion rapidly becomes a rat's nest of conditional blocks that are difficult to understand and maintain.
Neither of these approaches is well suited to a situation where you don't “own” the canonical copy of a source tree. In the case of a Linux driver that is distributed with the standard kernel, Linus's tree contains the copy of the code that will be treated by the world as canonical. The upstream version of “my” driver can be modified by people I don't know, without me even finding out about it until after the changes show up in Linus's tree.
These approaches have the added weakness of making it difficult to generate well-formed patches to submit upstream.
In principle, Mercurial Queues seems like a good candidate to manage a development scenario such as the above. While this is indeed the case, MQ contains a few added features that make the job more pleasant.
Perhaps the best way to maintain sanity with so many targets
is to be able to choose specific patches to apply for a given
situation. MQ provides a feature called “guards”
(which originates with quilt's guards
command) that does just this. To start off, let's create a
simple repository for experimenting in.
$
hg qinit
$
hg qnew hello.patch
$
echo hello > hello
$
hg add hello
$
hg qrefresh
$
hg qnew goodbye.patch
$
echo goodbye > goodbye
$
hg add goodbye
$
hg qrefresh
This gives us a tiny repository that contains two patches that don't have any dependencies on each other, because they touch different files.
The idea behind conditional application is that you can “tag” a patch with a guard, which is simply a text string of your choosing, then tell MQ to select specific guards to use when applying patches. MQ will then either apply, or skip over, a guarded patch, depending on the guards that you have selected.
A patch can have an arbitrary number of guards; each one is positive (“apply this patch if this guard is selected”) or negative (“skip this patch if this guard is selected”). A patch with no guards is always applied.
The qguard command lets you determine which guards should apply to a patch, or display the guards that are already in effect. Without any arguments, it displays the guards on the current topmost patch.
$
hg qguard
goodbye.patch: unguarded
To set a positive guard on a patch, prefix the name of the
guard with a “+
”.
$
hg qguard +foo
$
hg qguard
goodbye.patch: +foo
To set a negative guard
on a patch, prefix the name of the guard with a
“-
”.
$
hg qguard -- hello.patch -quux
$
hg qguard hello.patch
hello.patch: -quux
Notice that we prefixed the arguments to the hg
qguard command with a --
here, so
that Mercurial would not interpret the text
-quux
as an option.
Mercurial stores guards in the series
file; the form in which they
are stored is easy both to understand and to edit by hand. (In
other words, you don't have to use the qguard command if you don't want
to; it's okay to simply edit the series
file.)
$
cat .hg/patches/series
hello.patch #-quux goodbye.patch #+foo
The qselect command determines which guards are active at a given time. The effect of this is to determine which patches MQ will apply the next time you run qpush. It has no other effect; in particular, it doesn't do anything to patches that are already applied.
With no arguments, the qselect command lists the guards currently in effect, one per line of output. Each argument is treated as the name of a guard to apply.
$
hg qpop -a
popping goodbye.patch popping hello.patch patch queue now empty$
hg qselect
no active guards$
hg qselect foo
number of unguarded, unapplied patches has changed from 1 to 2$
hg qselect
foo
In case you're interested, the currently selected guards are
stored in the guards
file.
$
cat .hg/patches/guards
foo
We can see the effect the selected guards have when we run qpush.
$
hg qpush -a
applying hello.patch applying goodbye.patch now at: goodbye.patch
A guard cannot start with a
“+
” or
“-
” character. The name of a
guard must not contain white space, but most other characters
are acceptable. If you try to use a guard with an invalid name,
MQ will complain:
$
hg qselect +foo
abort: guard '+foo' starts with invalid character: '+'
Changing the selected guards changes the patches that are applied.
$
hg qselect quux
number of guarded, applied patches has changed from 0 to 2$
hg qpop -a
popping goodbye.patch popping hello.patch patch queue now empty$
hg qpush -a
skipping goodbye.patch - guarded by ['+foo']
You can see in the example below that negative guards take precedence over positive guards.
$
hg qselect foo bar
number of unguarded, unapplied patches has changed from 0 to 2$
hg qpop -a
no patches applied$
hg qpush -a
applying hello.patch applying goodbye.patch now at: goodbye.patch
The rules that MQ uses when deciding whether to apply a patch are as follows.
If the patch has any negative guard that matches any currently selected guard, the patch is skipped.
If the patch has any positive guard that matches any currently selected guard, the patch is applied.
If the patch has positive or negative guards, but none matches any currently selected guard, the patch is skipped.
In working on the device driver I mentioned earlier, I don't apply the patches to a normal Linux kernel tree. Instead, I use a repository that contains only a snapshot of the source files and headers that are relevant to Infiniband development. This repository is 1% the size of a kernel repository, so it's easier to work with.
I then choose a “base” version on top of which the patches are applied. This is a snapshot of the Linux kernel tree as of a revision of my choosing. When I take the snapshot, I record the changeset ID from the kernel repository in the commit message. Since the snapshot preserves the “shape” and content of the relevant parts of the kernel tree, I can apply my patches on top of either my tiny repository or a normal kernel tree.
Normally, the base tree atop which the patches apply should be a snapshot of a very recent upstream tree. This best facilitates the development of patches that can easily be submitted upstream with few or no modifications.
I categorise the patches in the series
file into a number of logical
groups. Each section of like patches begins with a block of
comments that describes the purpose of the patches that
follow.
The sequence of patch groups that I maintain follows. The ordering of these groups is important; I'll describe why after I introduce the groups.
The “accepted” group. Patches that the development team has submitted to the maintainer of the Infiniband subsystem, and which he has accepted, but which are not present in the snapshot that the tiny repository is based on. These are “read only” patches, present only to transform the tree into a similar state as it is in the upstream maintainer's repository.
The “rework” group. Patches that I have submitted, but that the upstream maintainer has requested modifications to before he will accept them.
The “pending” group. Patches that I have not yet submitted to the upstream maintainer, but which we have finished working on. These will be “read only” for a while. If the upstream maintainer accepts them upon submission, I'll move them to the end of the “accepted” group. If he requests that I modify any, I'll move them to the beginning of the “rework” group.
The “in progress” group. Patches that are actively being developed, and should not be submitted anywhere yet.
The “backport” group. Patches that adapt the source tree to older versions of the kernel tree.
The “do not ship” group. Patches that for some reason should never be submitted upstream. For example, one such patch might change embedded driver identification strings to make it easier to distinguish, in the field, between an out-of-tree version of the driver and a version shipped by a distribution vendor.
Now to return to the reasons for ordering groups of patches
in this way. We would like the lowest patches in the stack to
be as stable as possible, so that we will not need to rework
higher patches due to changes in context. Putting patches that
will never be changed first in the series
file serves this
purpose.
We would also like the patches that we know we'll need to modify to be applied on top of a source tree that resembles the upstream tree as closely as possible. This is why we keep accepted patches around for a while.
The “backport” and “do not ship”
patches float at the end of the series
file. The backport patches
must be applied on top of all other patches, and the “do
not ship” patches might as well stay out of harm's
way.
In my work, I use a number of guards to control which patches are to be applied.
“Accepted” patches are guarded with
accepted
. I enable this guard most of
the time. When I'm applying the patches on top of a tree
where the patches are already present, I can turn this patch
off, and the patches that follow it will apply
cleanly.
Patches that are “finished”, but not yet submitted, have no guards. If I'm applying the patch stack to a copy of the upstream tree, I don't need to enable any guards in order to get a reasonably safe source tree.
Those patches that need reworking before being
resubmitted are guarded with
rework
.
For those patches that are still under
development, I use devel
.
A backport patch may have several guards, one
for each version of the kernel to which it applies. For
example, a patch that backports a piece of code to 2.6.9
will have a 2.6.9
guard.
This variety of guards gives me considerable flexibility in determining what kind of source tree I want to end up with. For most situations, the selection of appropriate guards is automated during the build process, but I can manually tune the guards to use for less common circumstances.
Using MQ, writing a backport patch is a simple process. All such a patch has to do is modify a piece of code that uses a kernel feature not present in the older version of the kernel, so that the driver continues to work correctly under that older version.
A useful goal when writing a good backport patch is to
make your code look as if it was written for the older version
of the kernel you're targeting. The less obtrusive the patch,
the easier it will be to understand and maintain. If you're
writing a collection of backport patches to avoid the
“rat's nest” effect of lots of
#ifdef
s (hunks of source code that are only
used conditionally) in your code, don't introduce
version-dependent #ifdef
s into the patches.
Instead, write several patches, each of which makes
unconditional changes, and control their application using
guards.
There are two reasons to divide backport patches into a
distinct group, away from the “regular” patches
whose effects they modify. The first is that intermingling the
two makes it more difficult to use a tool like the patchbomb
extension to automate the
process of submitting the patches to an upstream maintainer.
The second is that a backport patch could perturb the context
in which a subsequent regular patch is applied, making it
impossible to apply the regular patch cleanly
without the earlier backport patch
already being applied.
If you're working on a substantial project with MQ, it's not difficult to accumulate a large number of patches. For example, I have one patch repository that contains over 250 patches.
If you can group these patches into separate logical categories, you can if you like store them in different directories; MQ has no problems with patch names that contain path separators.
If you're developing a set of patches over a long time, it's a good idea to maintain them in a repository, as discussed in Section 12.12, “Managing patches in a repository”. If you do so, you'll quickly discover that using the hg diff command to look at the history of changes to a patch is unworkable. This is in part because you're looking at the second derivative of the real code (a diff of a diff), but also because MQ adds noise to the process by modifying time stamps and directory names when it updates a patch.
However, you can use the extdiff
extension, which is bundled
with Mercurial, to turn a diff of two versions of a patch into
something readable. To do this, you will need a third-party
package called patchutils
[web:patchutils]. This provides a command
named interdiff, which shows the
differences between two diffs as a diff. Used on two versions
of the same diff, it generates a diff that represents the diff
from the first to the second version.
You can enable the extdiff
extension in the usual way,
by adding a line to the extensions
section of your
~/.hgrc
.
[extensions] extdiff =
The interdiff command expects to be
passed the names of two files, but the extdiff
extension passes the program
it runs a pair of directories, each of which can contain an
arbitrary number of files. We thus need a small program that
will run interdiff on each pair of files in
these two directories. This program is available as hg-interdiff
in the examples
directory of the
source code repository that accompanies this book.
With the hg-interdiff
program in your shell's search path, you can run it as
follows, from inside an MQ patch directory:
hg extdiff -p hg-interdiff -r A:B my-change.patch
Since you'll probably want to use this long-winded command
a lot, you can get hgext
to
make it available as a normal Mercurial command, again by
editing your ~/.hgrc
.
[extdiff] cmd.interdiff = hg-interdiff
This directs hgext
to
make an interdiff
command available, so you
can now shorten the previous invocation of extdiff to something a
little more wieldy.
hg interdiff -r A:B my-change.patch
The extdiff
extension is
useful for more than merely improving the presentation of MQ
patches. To read more about it, go to Section 14.2, “Flexible diff support with the extdiff
extension”.
Table of Contents
While the core of Mercurial is quite complete from a functionality standpoint, it's deliberately shorn of fancy features. This approach of preserving simplicity keeps the software easy to deal with for both maintainers and users.
However, Mercurial doesn't box you in with an inflexible command set: you can add features to it as extensions (sometimes known as plugins). We've already discussed a few of these extensions in earlier chapters.
Section 3.3, “Simplification de la séquence “pull-merge-commit””
covers the fetch
extension;
this combines pulling new changes and merging them with local
changes into a single command, fetch.
In Chapter 10, Handling repository events with hooks, we covered
several extensions that are useful for hook-related
functionality: acl
adds
access control lists; bugzilla
adds integration with the
Bugzilla bug tracking system; and notify
sends notification emails on
new changes.
The Mercurial Queues patch management extension is so invaluable that it merits two chapters and an appendix all to itself. Chapter 12, Managing change with Mercurial Queues covers the basics; Chapter 13, Advanced uses of Mercurial Queues discusses advanced topics; and Appendix B, Mercurial Queues reference goes into detail on each command.
In this chapter, we'll cover some of the other extensions that are available for Mercurial, and briefly touch on some of the machinery you'll need to know about if you want to write an extension of your own.
In Section 14.1, “Improve performance with the inotify
extension”,
we'll discuss the possibility of huge
performance improvements using the inotify
extension.
Are you interested in having some of the most common Mercurial operations run as much as a hundred times faster? Read on!
Mercurial has great performance under normal circumstances. For example, when you run the hg status command, Mercurial has to scan almost every directory and file in your repository so that it can display file status. Many other Mercurial commands need to do the same work behind the scenes; for example, the hg diff command uses the status machinery to avoid doing an expensive comparison operation on files that obviously haven't changed.
Because obtaining file status is crucial to good performance, the authors of Mercurial have optimised this code to within an inch of its life. However, there's no avoiding the fact that when you run hg status, Mercurial is going to have to perform at least one expensive system call for each managed file to determine whether it's changed since the last time Mercurial checked. For a sufficiently large repository, this can take a long time.
To put a number on the magnitude of this effect, I created a repository containing 150,000 managed files. I timed hg status as taking ten seconds to run, even when none of those files had been modified.
Many modern operating systems contain a file notification
facility. If a program signs up to an appropriate service, the
operating system will notify it every time a file of interest is
created, modified, or deleted. On Linux systems, the kernel
component that does this is called
inotify
.
Mercurial's inotify
extension talks to the kernel's inotify
component to optimise hg status
commands. The extension has two components. A daemon sits in
the background and receives notifications from the
inotify
subsystem. It also listens for
connections from a regular Mercurial command. The extension
modifies Mercurial's behavior so that instead of scanning the
filesystem, it queries the daemon. Since the daemon has perfect
information about the state of the repository, it can respond
with a result instantaneously, avoiding the need to scan every
directory and file in the repository.
Recall the ten seconds that I measured plain Mercurial as
taking to run hg status on a
150,000 file repository. With the inotify
extension enabled, the time
dropped to 0.1 seconds, a factor of one
hundred faster.
Before we continue, please pay attention to some caveats.
The inotify
extension is Linux-specific. Because it interfaces directly
to the Linux kernel's inotify
subsystem,
it does not work on other operating systems.
It should work on any Linux distribution that
was released after early 2005. Older distributions are
likely to have a kernel that lacks
inotify
, or a version of
glibc
that does not have the necessary
interfacing support.
Not all filesystems are suitable for use with
the inotify
extension.
Network filesystems such as NFS are a non-starter, for
example, particularly if you're running Mercurial on several
systems, all mounting the same network filesystem. The
kernel's inotify
system has no way of
knowing about changes made on another system. Most local
filesystems (e.g. ext3, XFS, ReiserFS) should work
fine.
The inotify
extension is
not yet shipped with Mercurial as of May 2007, so it's a little
more involved to set up than other extensions. But the
performance improvement is worth it!
The extension currently comes in two parts: a set of patches
to the Mercurial source code, and a library of Python bindings
to the inotify
subsystem.
To get going, it's best to already have a functioning copy of Mercurial installed.
Clone the Python inotify
binding repository. Build and install it.
hg clone http://hg.kublai.com/python/inotify cd inotify python setup.py build --force sudo python setup.py install --skip-build
Clone the crew
Mercurial repository.
Clone the inotify
patch
repository so that Mercurial Queues will be able to apply
patches to your cope of the crew
repository.
hg clone http://hg.intevation.org/mercurial/crew hg clone crew inotify hg clone http://hg.kublai.com/mercurial/patches/inotify inotify/.hg/patches
Make sure that you have the Mercurial Queues
extension, mq
, enabled. If
you've never used MQ, read Section 12.5, “Getting started with Mercurial Queues” to get started
quickly.
Go into the inotify
repo, and apply all
of the inotify
patches
using the hg
-a
option to the qpush command.
cd inotify hg qpush -a
If you get an error message from qpush, you should not continue. Instead, ask for help.
Build and install the patched version of Mercurial.
python setup.py build --force sudo python setup.py install --skip-build
Once you've build a suitably patched version of Mercurial,
all you need to do to enable the inotify
extension is add an entry to
your ~/.hgrc
.
[extensions] inotify =
When the inotify
extension
is enabled, Mercurial will automatically and transparently start
the status daemon the first time you run a command that needs
status in a repository. It runs one status daemon per
repository.
The status daemon is started silently, and runs in the
background. If you look at a list of running processes after
you've enabled the inotify
extension and run a few commands in different repositories,
you'll thus see a few hg
processes sitting
around, waiting for updates from the kernel and queries from
Mercurial.
The first time you run a Mercurial command in a repository
when you have the inotify
extension enabled, it will run with about the same performance
as a normal Mercurial command. This is because the status
daemon needs to perform a normal status scan so that it has a
baseline against which to apply later updates from the kernel.
However, every subsequent command that does
any kind of status check should be noticeably faster on
repositories of even fairly modest size. Better yet, the bigger
your repository is, the greater a performance advantage you'll
see. The inotify
daemon makes
status operations almost instantaneous on repositories of all
sizes!
If you like, you can manually start a status daemon using
the inserve command.
This gives you slightly finer control over how the daemon ought
to run. This command will of course only be available when the
inotify
extension is
enabled.
When you're using the inotify
extension, you should notice
no difference at all in Mercurial's
behavior, with the sole exception of status-related commands
running a whole lot faster than they used to. You should
specifically expect that commands will not print different
output; neither should they give different results. If either of
these situations occurs, please report a bug.
Mercurial's built-in hg diff command outputs plaintext unified diffs.
$
hg diff
diff -r 9d08afba4ca1 myfile --- a/myfile Thu Mar 17 05:08:57 2011 +0000 +++ b/myfile Thu Mar 17 05:08:57 2011 +0000 @@ -1,1 +1,2 @@ The first line. +The second line.
If you would like to use an external tool to display
modifications, you'll want to use the extdiff
extension. This will let you
use, for example, a graphical diff tool.
The extdiff
extension is
bundled with Mercurial, so it's easy to set up. In the extensions
section of your
~/.hgrc
, simply add a
one-line entry to enable the extension.
[extensions] extdiff =
This introduces a command named extdiff, which by default uses your system's diff command to generate a unified diff in the same form as the built-in hg diff command.
$
hg extdiff
--- a.9d08afba4ca1/myfile 2011-03-17 05:08:58.000000000 +0000 +++ /tmp/extdiffbEjuKD/a/myfile 2011-03-17 05:08:57.000000000 +0000 @@ -1 +1,2 @@ The first line. +The second line.
The result won't be exactly the same as with the built-in hg diff variations, because the output of diff varies from one system to another, even when passed the same options.
As the “making snapshot
”
lines of output above imply, the extdiff command works by
creating two snapshots of your source tree. The first snapshot
is of the source revision; the second, of the target revision or
working directory. The extdiff command generates
these snapshots in a temporary directory, passes the name of
each directory to an external diff viewer, then deletes the
temporary directory. For efficiency, it only snapshots the
directories and files that have changed between the two
revisions.
Snapshot directory names have the same base name as your
repository. If your repository path is /quux/bar/foo
, then foo
will be the name of each
snapshot directory. Each snapshot directory name has its
changeset ID appended, if appropriate. If a snapshot is of
revision a631aca1083f
, the directory will be
named foo.a631aca1083f
.
A snapshot of the working directory won't have a changeset ID
appended, so it would just be foo
in this example. To see what
this looks like in practice, look again at the extdiff example above. Notice
that the diff has the snapshot directory names embedded in its
header.
The extdiff command
accepts two important options. The hg -p
option
lets you choose a program to view differences with, instead of
diff. With the hg -o
option,
you can change the options that extdiff passes to the program
(by default, these options are
“-Npru
”, which only make sense
if you're running diff). In other respects,
the extdiff command
acts similarly to the built-in hg
diff command: you use the same option names, syntax,
and arguments to specify the revisions you want, the files you
want, and so on.
As an example, here's how to run the normal system
diff command, getting it to generate context
diffs (using the -c
option)
instead of unified diffs, and five lines of context instead of
the default three (passing 5
as the argument
to the -C
option).
$
hg extdiff -o -NprcC5
*** a.9d08afba4ca1/myfile Thu Mar 17 05:08:58 2011 --- /tmp/extdiffbEjuKD/a/myfile Thu Mar 17 05:08:57 2011 *************** *** 1 **** --- 1,2 ---- The first line. + The second line.
Launching a visual diff tool is just as easy. Here's how to launch the kdiff3 viewer.
hg extdiff -p kdiff3 -o
If your diff viewing command can't deal with directories,
you can easily work around this with a little scripting. For an
example of such scripting in action with the mq
extension and the
interdiff command, see Section 13.9.2, “Viewing the history of a patch”.
It can be cumbersome to remember the options to both the
extdiff command and
the diff viewer you want to use, so the extdiff
extension lets you define
new commands that will invoke your diff
viewer with exactly the right options.
All you need to do is edit your ~/.hgrc
, and add a section named
extdiff
. Inside this
section, you can define multiple commands. Here's how to add
a kdiff3
command. Once you've defined
this, you can type “hg kdiff3
”
and the extdiff
extension
will run kdiff3 for you.
[extdiff] cmd.kdiff3 =
If you leave the right hand side of the definition empty,
as above, the extdiff
extension uses the name of the command you defined as the name
of the external program to run. But these names don't have to
be the same. Here, we define a command named
“hg wibble
”, which runs
kdiff3.
[extdiff] cmd.wibble = kdiff3
You can also specify the default options that you want to
invoke your diff viewing program with. The prefix to use is
“opts.
”, followed by the name
of the command to which the options apply. This example
defines a “hg vimdiff
” command
that runs the vim editor's
DirDiff
extension.
[extdiff] cmd.vimdiff = vim opts.vimdiff = -f '+next' '+execute "DirDiff" argv(0) argv(1)'
Many projects have a culture of “change review”, in which people send their modifications to a mailing list for others to read and comment on before they commit the final version to a shared repository. Some projects have people who act as gatekeepers; they apply changes from other people to a repository to which those others don't have access.
Mercurial makes it easy to send changes over email for
review or application, via its patchbomb
extension. The extension is
so named because changes are formatted as patches, and it's usual
to send one changeset per email message. Sending a long series
of changes by email is thus much like “bombing” the
recipient's inbox, hence “patchbomb”.
As usual, the basic configuration of the patchbomb
extension takes just one or
two lines in your
/.hgrc
.
[extensions] patchbomb =
Once you've enabled the extension, you will have a new command available, named email.
The safest and best way to invoke the email command is to
always run it first with the hg -n
option.
This will show you what the command would
send, without actually sending anything. Once you've had a
quick glance over the changes and verified that you are sending
the right ones, you can rerun the same command, with the hg -n
option
removed.
The email command
accepts the same kind of revision syntax as every other
Mercurial command. For example, this command will send every
revision between 7 and tip
, inclusive.
hg email -n 7:tip
You can also specify a repository to
compare with. If you provide a repository but no revisions, the
email command will
send all revisions in the local repository that are not present
in the remote repository. If you additionally specify revisions
or a branch name (the latter using the hg -b
option),
this will constrain the revisions sent.
It's perfectly safe to run the email command without the
names of the people you want to send to: if you do this, it will
just prompt you for those values interactively. (If you're
using a Linux or Unix-like system, you should have enhanced
readline
-style editing capabilities when
entering those headers, too, which is useful.)
When you are sending just one revision, the email command will by default use the first line of the changeset description as the subject of the single email message it sends.
If you send multiple revisions, the email command will usually send one message per changeset. It will preface the series with an introductory message, in which you should describe the purpose of the series of changes you're sending.
Not every project has exactly the same conventions for
sending changes in email; the patchbomb
extension tries to
accommodate a number of variations through command line
options.
You can write a subject for the introductory
message on the command line using the hg -s
option. This takes one argument, the text of the subject
to use.
To change the email address from which the
messages originate, use the hg -f
option. This takes one argument, the email address to
use.
The default behavior is to send unified diffs
(see Section 12.4, “Understanding patches” for a
description of the
format), one per message. You can send a binary bundle
instead with the hg -b
option.
Unified diffs are normally prefaced with a
metadata header. You can omit this, and send unadorned
diffs, with the hg
--plain
option.
Diffs are normally sent “inline”,
in the same body part as the description of a patch. This
makes it easiest for the largest number of readers to
quote and respond to parts of a diff, as some mail clients
will only quote the first MIME body part in a message. If
you'd prefer to send the description and the diff in
separate body parts, use the hg -a
option.
Instead of sending mail messages, you can
write them to an mbox
-format mail
folder using the hg -m
option. That option takes one argument, the name of the
file to write to.
If you would like to add a
diffstat-format summary to each patch,
and one to the introductory message, use the hg -d
option. The diffstat command displays
a table containing the name of each file patched, the
number of lines affected, and a histogram showing how much
each file is modified. This gives readers a qualitative
glance at how complex a patch is.
Table of Contents
Une manière courante de s'essayer à un nouveau gestionnaire de révisions est d'expérimenter en migrant un projet existant, plutôt que le faire avec un nouveau projet.
Dans cette annexe, nous discuterons comment importer l'historique d'un projet dans Mercurial, et à quoi faire attention si vous êtes habitués à un autre outil de gestion de révisions.
Mercurial est livré avec une extension nommée
convert
, qui permet d'importer un historique
depuis les gestionnaire de révisions les plus courants. Au moment de
l'écriture de ce livre, il pouvait importer l'historique depuis:
(Pour savoir pourquoi Mercurial lui même est supporté comme source, voir Section A.1.3, “Nettoyer l'arboresence”.)
Vous pouvez activer l'extension de la manière
habituelle, en éditant votre fichier ~/.hgrc
[extensions] convert =
Ceci rendra la commande hg convert disponible. La commande est facile à utiliser. Par exemple, la commande suivante va importer l'historique Subversion du framework de test “Nose Unit” dans Mercurial.
$
hg convert http://python-nose.googlecode.com/svn/trunk
L'extension convert
opère de
manière incrémentale. En d'autres mots, après une première exécution de
la commande hg convert, les exécutions ultérieures
importeront les révisions ultérieures à l'exécution précédente.
La conversion incrémentale ne réussira que si
vous exécutez hg convert dans le même dépôt que vous
aviez utilisé à l'origine, ceci parce que l'extension convert
sauvegarde un certain nombre de méta-données privées dans le fichier
.hg/shamap
(non versioné) au sein du dépôt cible.
Lorsque vous voulez faire des modifications en utilisant Mercurial, le mieux est de faire un clone de l'ensemble de l'arborescence que vous souhaitez convertir, et de laisser l'arborescence d'origine pour de futures conversions incrémentales. C'est la manière la plus sûre pour vous laisser récupérer et fusionner les modifications futures depuis l'outil de gestion de révisions dans votre nouveau dépôt Mercurial.
La commande hg convert citée
ci-dessus convertit seulement l'historique de la branche
principale (trunk)
du dépôt Subversion. Si nous utilisons
à la place l'URL http://python-nose.googlecode.com/svn
,
Mercurial va automatiquement détecter la
branche principale (trunk)
, les étiquettes
(tags)
, et les branches
que les dépôts
Subversion utilisent généralement, et les importera chacun dans
une branche Mercurial distincte.
Par défaut, chaque branche Subversion importée
dans Mercurial se voit attribuer un nom de branche. Une fois la
conversion achevée, vous pouvez obtenir la liste des noms des branches
actives dans le dépôt Mercurial en utilisant la commande
hg branches -a. Si vous préférez importer les
branches Subversion sans noms, ajoutez l'option --config
convert.hg.usebranches=false
à la commande
hg convert.
Une fois votre arborescence convertie, si vous souhaitez travailler selon la pratique habituelle sous Mercurial avec une arborescence qui ne contient qu'une seule branche, vous pouvez cloner cette seule branche en utilisant hg clone -r nomdemabranche.
Certains outils de gestion de révisions ne sauvegardent, avec les modifications, que les noms d'utilisateurs raccourcis. Ceux-ci peuvent être difficiles à interpréter. La norme avec Mercurial est de sauvegarder le nom du committeur et son adresse mail, ce qui est beaucoup plus utile pour discuter avec lui par la suite.
Si vous convertissez une arborescence depuis
un gestionnaire de révisions qui utilise seulement les noms
raccourcis, vous pouvez associer ces noms à des équivalents
plus détaillés en passant l'option --authors
à la commande hg convert. Cette option
attend un fichier qui contient des entrées sous la forme suivante:
arist = Aristotle <aristotle@phil.example.gr> soc = Socrates <socrates@phil.example.gr>
Quand convert
trouve une
modification associée au nom arist
dans le
dépôt de source, il va utiliser le nom Aristotle
<aristotle@phil.example.gr>
dans les révisions
Mercurial. Si aucune correspondance n'est trouvé, il utilise
le nom tel quel.
Tous les projets n'ont pas un historique parfait. Il peut y avoir des répertoires qui n'auraient jamais dû être ajoutés, un fichier qui est trop volumineux, ou même une partie de la hiérarchie qui devrait être réorganisée.
L'extension convert
permet
d'utiliser un “fichier d'association” qui peut
réorganiser les fichiers et les répertoires dans un projet lors de
l'importation de son historique. Ceci est utile non seulement quand vous
importez l'historique d'un autre gestionnaire de révisions, mais
aussi pour nettoyer ou réorganiser l'arborescence d'un projet
Mercurial.
Pour indiquer le fichier d'association, on utilise
l'option --filemap
en lui fournissant un nom de
fichier. Le fichier d'association contient des lignes de la forme
suivante :
# Ceci est un commentaire. # Les lignes vides sont ignorées. include path/to/file exclude path/to/file rename from/some/path to/some/other/place
La directive include
inclut un
fichier, ou l'ensemble des fichiers d'un répertoire, dans le dépôt
de destination. La directive exclude
omet les
fichiers ou répertoires du dépôt. Ceci inclut aussi les autres
fichiers et répertoires qui ne sont pas explicitement inclus.
La directive exclude
entraine l'omission
des fichiers ou répertoires, et autres fichiers qui ne sont pas
explicitement inclus.
Pour déplacer un fichier ou un répertoire d'un
emplacement à un autre, utilisez la directive
rename
. Si vous avez besoin de déplacer un
fichier ou un répertoire depuis un sous répertoire dans la racine
du dépôt, utilisez .
comme second argument de
la directive rename
.
Vous aurez souvent besoin de plusieurs essais
avant d'arriver à la parfaite combinaison de fichier d'association de fichiers,
de fichier d'association de noms d'utilisateurs et des autres paramètres. Or,
convertir un dépôt Mercurial via un protocole comme ssh
ou http
peut être des milliers de fois plus long
que ce dont le système d'exploitation est en fait capable de faire,
à cause des latence réseau. Ceci peut rendre la conception de cette
combinaison parfaite très douloureuse.
La commande svnsync peut grandement améliorer la vitesse de conversion d'un dépôt Subversion. Il s'agit d'un programme de miroir de dépôt Subversion en lecture seule. L'idée est de créer un miroir local d'une arborescence Subversion, puis de convertir ce miroir en dépôt Mercurial.
Supposez que nous voulions convertir le dépôt Subversion du populaire projet Memcached en une arborescence Mercurial. Tout d'abord, nous créons un dépôt Subversion local.
$
svnadmin create memcached-mirror
Puis, nous allons mettre en place un “hook” Subversion dont svnsync a besoin.
$
echo '#!/bin/sh' > memcached-mirror/hooks/pre-revprop-change
$
chmod +x memcached-mirror/hooks/pre-revprop-change
Nous initialisons ensuite svnsync dans ce dépôt.
$
svnsync --init file://`pwd`/memcached-mirror \ http://code.sixapart.com/svn/memcached
La prochaine étape est de commencer le processus de mirroring de svnsync.
$
svnsync sync file://`pwd`/memcached-mirror
Enfin, nous importons l'historique de notre dépôt local Subversion dans Mercurial.
$
hg convert memcached-mirror
Nous pouvons utiliser ce processus de manière incrémentale, si le dépôt Subversion est toujours en activité. Il suffit d'exécuter de nouveau svnsync pour récupérer les récentes modifications dans notre miroir, puis hg convert les importe dans notre arborescence Mercurial.
Il y a deux avantages à utiliser un import à deux étages comme avec svnsync. Le premier est qu'il utilise du code de synchronisation réseau de Subversion plus efficace que la commande hg convert, et donc transfère moins de données par le réseau. Le deuxième est que l'import depuis un dépôt Subversion local est si rapide que vous pouvez peaufiner et réitérer les paramètres de conversion de ce dernier sans souffrir de la qualité de la connexion réseau.
Subversion est le système de gestion de versions open source le plus populaire aujourd'hui. Bien qu'il y ait des différences entre Mercurial et Subversion, faire la transition de l'un à l'autre n'est pas très difficile. Les deux disposent en effet de jeux de commandes similaires et d'interfaces similaires.
La différence fondamentale entre Subversion et Mercurial est bien évidement que Subversion est centralisé, alors que Mercurial est distribué. Puisque que Mercurial enregistre tout l'historique d'un projet sur votre disque dur local, il n'a besoin d'effectuer des accès au réseau que lorsque vous voulez explicitement communiquer avec un autre dépôt. Subversion, par contre, ne conserve que peu d'informations localement, et le client doit donc communiquer avec le serveur central pour la plupart des opérations communes.
Subversion s'en tire plus ou moins bien sans notion de branche réellement bien définie : quelle portion de l'espace de nommage du serveur est une branche est une simple question de convention, le logiciel n'imposant rien à ce sujet. Mercurial considère un dépôt comme un élément de la gestion des branches.
Puisque que Subversion ne sait pas réellement quelle partie de son espace de nommage est en fait une branche, il traite la plupart des commandes comme des requêtes à exécuter sur le répertoire où vous vous situez, et ses sous répertoires. Par exemple, si vous exécutez svn log, vous verrez l'historique de la partie de l'arborescence où vous vous situez, et non de la hiérarchie entière.
Les commandes de Mercurial ont un comportement différent : toutes les commandes s'appliquent à l'ensemble de l'arborescence du dépôt. Exécutez la commande hg log et elle vous donnera l'historique de l'ensemble de l'arborescence, quel que soit le sous-répertoire où vous vous situez. Si vous souhaitez obtenir l'historique d'un répertoire ou seulement d'un fichier, ajouter simplement le nom de celui-ci à la commande, par exemple hg log src.
De ma propre expérience, cette différence dans leur comportement par défaut est probablement ce qui risque de vous surprendre le plus si vous passez régulièrement d'un outil à l'autre.
Avec Subversion, il est normal (bien que légèrement désapprouvé) que différentes personnes collaborent sur une seule branche. Si Alice et Bob travaillent ensemble, et Alice ajoute ses modifications à leur branche partagée, Bob doit alors mettre à jour sa vue de la branche avant de pouvoir appliquer un commit. Puisqu'il n'a, à ce moment, pas effectué de commit des modifications qu'il a faites, il se peut qu'il ne corrompe ou ne perde ses modifications pendant ou après la mise à jour.
Mercurial encourage, à l'inverse, un modèle "commit-puis-merge". Avant de récupérer des modifications depuis le serveur, ou avant d'y envoyer les siennes, Bob enregistre ses modifications de manière locale en appliquant un commit. C'est à dire que si Alice avait envoyé ses modifications sur le serveur avant que Bob n'envoie les siennes, ce dernier ne pourra le faire qu'après avoir récupéré et fusionné celles d'Alice avec les siennes. Si Bob fait alors une erreur lors de la fusion, il pourra toujours restaurer sa version, pour laquelle il avait appliqué le commit.
Il est important de souligner qu'il s'agit de la manière habituelle de travailler avec ces outils. Subversion propose une manière plus sûre de "travailler-dans-votre-propre-branche", mais elle est assez complexe pour que, en pratique, elle ne soit que rarement utilisé. Mercurial propose de son côté un mode un peu moins sûr, permettant de récupérer des modifications par dessus des modifications non committées, qui reste toutefois très peu répandu.
Une commande Subversion svn commit publie immédiatement les modifications sur le serveur, où elles peuvent être vu par n'importe qui doté d'un privilège de lecture.
Avec Mercurial, les modifications sont toujours d'abord enregistrées localement, et doivent être par la suite transférés par la commande hg push.
Chaque approche a ses avantages et ses inconvénients. Le modèle Subversion implique que les modifications soient publiées, et donc disponibles immédiatement. D'un autre coté, cela implique aussi que, pour pouvoir utiliser le logiciel normalement, un utilisateur doit avoir les droits d'écriture dans le dépôt, et ce privilège n'est pas concédé facilement par la plupart des projets Open Source.
L'approche de Mercurial permet à quiconque de faire un clone du dépôt et d'y ajouter ses modifications sans jamais avoir besoin de la permission de quiconque, et l'on peut même publier ses modifications et continuer à participer comme on le désire. Toutefois, la distinction entre les commits et le transfert de ces derniers présente le risque que quelqu'un applique ses modifications par un commit local sur son portable et parte se promener pendant quelques jours en ayant oublié de les transférer, ce qui peut, dans certains rares cas, bloquer temporairement ses collaborateurs.
Table A.1. Commandes Subversion et leurs équivalents Mercurial
Subversion | Mercurial | Notes |
---|---|---|
svn add | hg add | |
svn blame | hg annotate | |
svn cat | hg cat | |
svn checkout | hg clone | |
svn cleanup | n/a | Aucun nettoyage nécessaire. |
svn commit | hg commit; hg push | hg push publie les modifications après un commit. |
svn copy | hg clone | Pour créer une nouvelle branche |
svn copy | hg copy | Pour copier des fichiers ou des répertoires |
svn delete (svn remove) | hg remove | |
svn diff | hg diff | |
svn export | hg archive | |
svn help | hg help | |
svn import | hg addremove; hg commit | |
svn info | hg parents | Affiche la version sur la base de laquelle on travaille |
svn info | hg showconfig paths.default | Affiche de quelle URL est extrait ce dépôt |
svn list | hg manifest | |
svn log | hg log | |
svn merge | hg merge | |
svn mkdir | n/a | Mercurial ne versionne pas les répertoires |
svn move (svn rename) | hg rename | |
svn resolved | hg resolve -m | |
svn revert | hg revert | |
svn status | hg status | |
svn update | hg pull -u |
Avec la plupart des gestionnaire de versions, afficher un diff associé à une révision peut être assez douloureux. Par exemple, avec Subversion, pour voir ce qui a été modifiée dans la révision 104654, vous devez saisir svn diff -r104653:104654. Mercurial élimine le besoin de saisir l'identifiant d'une révision deux fois dans ce cas classique. Pour un simple diff, hg export 104654 suffit. Pour obtenir une entrée du journal suivie d'un diff, hg log -r104654 -p.
Quand vous exécutez la commande hg status sans aucun argument, elle affiche l'état de l'ensemble de l'arborescence, avec des chemins relatifs partant de la racine du dépôt. Ceci rend difficile de copier un nom de fichier depuis la sortie de la commande hg status dans une autre ligne de commande. Si vous fournissez un fichier ou un répertoire à la commande hg status, elle va afficher les chemins relatif depuis votre répertoire courant à la place. Ainsi, pour avoir un état sur l'ensemble de l'arborescence à l'aide de hg status, avec des chemins relatifs à votre répertoire courant, et non la racine du dépôt, ajoutez la sortie de hg root à la commande hg status. Vous pouvez le faire aisément sur un système Unix ainsi :
$
hg status `hg root`
Table of Contents
series
fileFor an overview of the commands provided by MQ, use the command hg help mq.
The qapplied command prints the current stack of applied patches. Patches are printed in oldest-to-newest order, so the last patch in the list is the “top” patch.
The qcommit command
commits any outstanding changes in the .hg/patches
repository. This command only works if the .hg/patches
directory is a repository, i.e. you created the directory
using hg qinit -c
or
ran hg init in the directory
after running qinit.
The qdelete command
removes the entry for a patch from the series
file in the .hg/patches
directory. It does not pop the patch if the patch is already
applied. By default, it does not delete the patch file; use
the -f
option
to do that.
The hg qfinish command converts the specified applied patches into permanent changes by moving them out of MQ's control so that they will be treated as normal repository history.
The qfold command merges multiple patches into the topmost applied patch, so that the topmost applied patch makes the union of all of the changes in the patches in question.
The patches to fold must not be applied; qfold will exit with an error if
any is. The order in which patches are folded is significant;
hg qfold a b means
“apply the current topmost patch, followed by
a
, followed by
b
”.
The comments from the folded patches are appended to the
comments of the destination patch, with each block of comments
separated by three asterisk
(“*
”) characters. Use the
-e
option to
edit the commit message for the combined patch/changeset after
the folding has completed.
The qheader command prints the header, or description, of a patch. By default, it prints the header of the topmost applied patch. Given an argument, it prints the header of the named patch.
The qimport command
adds an entry for an external patch to the series
file, and copies the patch
into the .hg/patches
directory. It adds
the entry immediately after the topmost applied patch, but
does not push the patch.
If the .hg/patches
directory is a
repository, qimport
automatically does an hg add
of the imported patch.
The qinit command
prepares a repository to work with MQ. It creates a directory
called .hg/patches
.
When the .hg/patches
directory is a
repository, the qimport
and qnew commands
automatically hg add new
patches.
The qnew command
creates a new patch. It takes one mandatory argument, the
name to use for the patch file. The newly created patch is
created empty by default. It is added to the series
file after the current
topmost applied patch, and is immediately pushed on top of
that patch.
If qnew finds modified
files in the working directory, it will refuse to create a new
patch unless the -f
option is used
(see below). This behavior allows you to qrefresh your topmost applied
patch before you apply a new patch on top of it.
-f
: Create a new
patch if the contents of the working directory are
modified. Any outstanding modifications are added to the
newly created patch, so after this command completes, the
working directory will no longer be modified.
-m
: Use the given
text as the commit message. This text will be stored at
the beginning of the patch file, before the patch
data.
The qnext command
prints the name name of the next patch in the series
file after the topmost
applied patch. This patch will become the topmost applied
patch if you run qpush.
The qpop command removes applied patches from the top of the stack of applied patches. By default, it removes only one patch.
This command removes the changesets that represent the popped patches from the repository, and updates the working directory to undo the effects of the patches.
This command takes an optional argument, which it uses as the name or index of the patch to pop to. If given a name, it will pop patches until the named patch is the topmost applied patch. If given a number, qpop treats the number as an index into the entries in the series file, counting from zero (empty lines and lines containing only comments do not count). It pops patches until the patch identified by the given index is the topmost applied patch.
The qpop command does
not read or write patches or the series
file. It is thus safe to
qpop a patch that you have
removed from the series
file, or a patch that you have renamed or deleted entirely.
In the latter two cases, use the name of the patch as it was
when you applied it.
By default, the qpop
command will not pop any patches if the working directory has
been modified. You can override this behavior using the
-f
option,
which reverts all modifications in the working
directory.
The qpop command
removes one line from the end of the status
file for each patch that it
pops.
The qprev command
prints the name of the patch in the series
file that comes before the
topmost applied patch. This will become the topmost applied
patch if you run qpop.
The qpush command adds patches onto the applied stack. By default, it adds only one patch.
This command creates a new changeset to represent each applied patch, and updates the working directory to apply the effects of the patches.
The default data used when creating a changeset are as follows:
The commit date and time zone are the current date and time zone. Because these data are used to compute the identity of a changeset, this means that if you qpop a patch and qpush it again, the changeset that you push will have a different identity than the changeset you popped.
The author is the same as the default used by the hg commit command.
The commit message is any text from the patch file that comes before the first diff header. If there is no such text, a default commit message is used that identifies the name of the patch.
If a patch contains a Mercurial patch header, the information in the patch header overrides these defaults.
-a
: Push all
unapplied patches from the series
file until there are
none left to push.
-l
: Add the name
of the patch to the end of the commit message.
-m
: If a patch
fails to apply cleanly, use the entry for the patch in
another saved queue to compute the parameters for a
three-way merge, and perform a three-way merge using the
normal Mercurial merge machinery. Use the resolution of
the merge as the new patch content.
The qpush command
reads, but does not modify, the series
file. It appends one line
to the hg status file for
each patch that it pushes.
The qrefresh command updates the topmost applied patch. It modifies the patch, removes the old changeset that represented the patch, and creates a new changeset to represent the modified patch.
The qrefresh command looks for the following modifications:
Changes to the commit message, i.e. the text before the first diff header in the patch file, are reflected in the new changeset that represents the patch.
Modifications to tracked files in the working directory are added to the patch.
Changes to the files tracked using hg add, hg copy, hg remove, or hg rename. Added files and copy and rename destinations are added to the patch, while removed files and rename sources are removed.
Even if qrefresh detects no changes, it still recreates the changeset that represents the patch. This causes the identity of the changeset to differ from the previous changeset that identified the patch.
The qrename command
renames a patch, and changes the entry for the patch in the
series
file.
With a single argument, qrename renames the topmost applied patch. With two arguments, it renames its first argument to its second.
The qseries command
prints the entire patch series from the series
file. It prints only patch
names, not empty lines or comments. It prints in order from
first to be applied to last.
The qunapplied command
prints the names of patches from the series
file that are not yet
applied. It prints them in order from the next patch that
will be pushed to the last.
The hg strip command removes a revision, and all of its descendants, from the repository. It undoes the effects of the removed revisions from the repository, and updates the working directory to the first parent of the removed revision.
The hg strip command saves a backup of the removed changesets in a bundle, so that they can be reapplied if removed in error.
The series
file
contains a list of the names of all patches that MQ can apply.
It is represented as a list of names, with one name saved per
line. Leading and trailing white space in each line are
ignored.
Lines may contain comments. A comment begins with the
“#
” character, and extends to
the end of the line. Empty lines, and lines that contain only
comments, are ignored.
You will often need to edit the series
file by hand, hence the
support for comments and empty lines noted above. For
example, you can comment out a patch temporarily, and qpush will skip over that patch
when applying patches. You can also change the order in which
patches are applied by reordering their entries in the
series
file.
Placing the series
file under revision control is also supported; it is a good
idea to place all of the patches that it refers to under
revision control, as well. If you create a patch directory
using the -c
option to qinit, this will
be done for you automatically.
Table of Contents
Si vous utilisez un système Unix ou similaire, pour lequel une version récente de Python (2.3 ou plus) est disponible, l'installation de Mercurial à partir des sources est simple.
Téléchargez un paquet récent depuis http://www.selenic.com/mercurial/download.
gzip -dc mercurial-MYVERSION.tar.gz | tar xf -
Allez dans le répertoires où les sources ont été extraites et exécutez le script d'installation. Ce dernier compilera Mercurial et l'installera dans votre répertoire utilisateur.
cd mercurial-MYVERSION python setup.py install --force --home=$HOME
Lorsque l'installation est terminée, Mercurial se
trouvera dans le répertoire bin
de votre répertoire
utilisateur.
N'oubliez pas de vérifier que ce répertoire se trouve dans la liste
des répertoires où votre shell recherche les exécutables.
Vous devrez vraisemblablement définir la variable
d'environnement PYTHONPATH
de manière à ce que
l'exécutable de Mercurial puisse trouver le reste des paquets logiciels.
Par exemple, sur mon ordinateur portable, je dois le définir ainsi:
/home/bos/lib/python
. Le chemin exact à utiliser
dépendra de la manière dont Python aura été construit pour votre
système. Il ne devrait pas être difficile de le trouver. En cas de
doute, lisez le texte généré lors de l'installation ci-dessus, et
recherchez l'emplacement où le contenu du répertoire
mercurial
a été installé.
Construire et installer Mercurial sous Windows nécessite des outils logiciels divers, une certaine connaissance technique et une bonne dose de patience. Je vous déconseille fortement de tenter de le faire si vous êtes un “simple utilisateur”. A moins que vous n'ayez l'intention de "hacker" Mercurial, je vous suggère d'avoir recours à un paquet d'installation de la version binaire.
Si vous avez vraiment l'intention de construire Mercurial à partir des sources sous Windows, suivez les indications pour ce “chemin laborieux” sur le wiki de Mercurial : http://www.selenic.com/mercurial/wiki/index.cgi/WindowsInstall, et préparez vous à un travail épineux.
Table of Contents
The Open Publication works may be reproduced and distributed in whole or in part, in any medium physical or electronic, provided that the terms of this license are adhered to, and that this license or an incorporation of it by reference (with any options elected by the author(s) and/or publisher) is displayed in the reproduction.
Proper form for an incorporation by reference is as follows:
Copyright (c) year by author's name or designee. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, vx.y or later (the latest version is presently available at http://www.opencontent.org/openpub/).
The reference must be immediately followed with any options elected by the author(s) and/or publisher of the document (see Section D.6, “License options”).
Commercial redistribution of Open Publication-licensed material is permitted.
Any publication in standard (paper) book form shall require the citation of the original publisher and author. The publisher and author's names shall appear on all outer surfaces of the book. On all outer surfaces of the book the original publisher's name shall be as large as the title of the work and cited as possessive with respect to the title.
The following license terms apply to all Open Publication works, unless otherwise explicitly stated in the document.
Mere aggregation of Open Publication works or a portion of an Open Publication work with other works or programs on the same media shall not cause this license to apply to those other works. The aggregate work shall contain a notice specifying the inclusion of the Open Publication material and appropriate copyright notice.
Severability. If any part of this license is found to be unenforceable in any jurisdiction, the remaining portions of the license remain in force.
No warranty. Open Publication works are licensed and provided “as is” without warranty of any kind, express or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose or a warranty of non-infringement.
All modified versions of documents covered by this license, including translations, anthologies, compilations and partial documents, must meet the following requirements:
The person making the modifications must be identified and the modifications dated.
Acknowledgement of the original author and publisher if applicable must be retained according to normal academic citation practices.
The location of the original unmodified document must be identified.
The original author's (or authors') name(s) may not be used to assert or imply endorsement of the resulting document without the original author's (or authors') permission.
In addition to the requirements of this license, it is requested from and strongly recommended of redistributors that:
If you are distributing Open Publication works on hardcopy or CD-ROM, you provide email notification to the authors of your intent to redistribute at least thirty days before your manuscript or media freeze, to give the authors time to provide updated documents. This notification should describe modifications, if any, made to the document.
All substantive modifications (including deletions) be either clearly marked up in the document or else described in an attachment to the document.
Finally, while it is not mandatory under this license, it is considered good form to offer a free copy of any hardcopy and CD-ROM expression of an Open Publication-licensed work to its author(s).
The author(s) and/or publisher of an Open Publication-licensed document may elect certain options by appending language to the reference to or copy of the license. These options are considered part of the license instance and must be included with the license (or its incorporation by reference) in derived works.
To prohibit distribution of substantively modified versions without the explicit permission of the author(s). “Substantive modification” is defined as a change to the semantic content of the document, and excludes mere changes in format or typographical corrections.
To accomplish this, add the phrase “Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.” to the license reference or copy.
To prohibit any publication of this work or derivative works in whole or in part in standard (paper) book form for commercial purposes is prohibited unless prior permission is obtained from the copyright holder.
To accomplish this, add the phrase “Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.” to the license reference or copy.