Chapter 3. Un tour rapide de Mercurial : fusionner les travaux

Table of Contents

3.1. Fusionner différents travaux
3.1.1. Les révisions heads
3.1.2. Effectuer la fusion
3.1.3. Effectuer l'ajout (commit) du résultat de la fusion
3.2. Fusionner les modifications en conflit
3.2.1. Utiliser un outil graphique de fusion
3.2.2. Un exemple concret
3.3. Simplification de la séquence pull-merge-commit
3.4. Renommer, copier, et fusionner (merge)

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.

3.1. Fusionner différents travaux

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;
}

Figure 3.1. Historique divergeant des dépôts my-hello et my-new-hello.

XXX ajoute un test

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.

3.1.1. Les révisions 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

XXX ajoute un texte

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.

3.1.2. Effectuer la fusion

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;
}

3.1.3. Effectuer l'ajout (commit) du résultat de la fusion

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

Figure 3.3. Répertoire de travail et dépôt pendant une fusion, et le commit qui suit

XXX ajoute texte

3.2. Fusionner les modifications en conflit

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.

Figure 3.4. Modifications en conflit dans un document

XXX ajoute texte

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.

3.2.1. Utiliser un outil graphique de fusion

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.

Figure 3.5. Utiliser kdiff3 pour fusionner les différentes version d'un fichier.

XXX ajoute texte

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

3.2.2. Un exemple concret

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

[Note] Où est la commande hg resolve ?

La commande hg resolve a été introduite dans la version 1.1 de Mercurial, qui a été publiée en décembre 2008. Si vous utilisez une version plus anciennne de Mercurial (exécutez la command hg version pour en avoir le cœur net), cette commande ne sera pas disponible. Si votre version de Mercurial est plus ancienne que la 1.1, vous devriez très fortement considérer une mise à jour vers une version plus récente avant d'essayer de régler des fusions complexes.

3.3. Simplification de la séquence pull-merge-commit

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

3.4. Renommer, copier, et fusionner (merge)

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.