Fusions : pratiques avancées

Ici finit la magie automatisée. Tôt ou tard, une fois que vous maîtrisez bien la gestion des branches et les fusions, vous allez vous retrouver à demander à Subversion de fusionner des modifications spécifiques d'un endroit à un autre. Pour faire cela, vous allez devoir commencer à passer des paramètres plus compliqués à svn merge. Le paragraphe suivant décrit la syntaxe complète de la commande et aborde un certain nombre de scénarios d'utilisation courants.

Sélection à la main

De la même façon que le terme « ensemble de modifications » est utilisé couramment dans les systèmes de gestion de versions, le terme « sélectionner à la main » pourrait l'être aussi. Il désigne l'action de choisir une liste de modifications particulière au sein d'une branche et de la recopier dans une autre. Sélectionner à la main peut aussi faire référence à l'action de dupliquer un ensemble de modifications (pas nécessairement contiguës !) d'une branche vers une autre. Ceci est en opposition avec des scénarios de fusion plus courants, où l'ensemble de révisions contiguës « suivant » est dupliqué automatiquement.

Pourquoi voudrait-on ne recopier qu'une modification unique ? Cela arrive plus souvent qu'on ne croit. Par exemple, remontons le temps et imaginons que vous n'avez pas encore réintégré votre branche de développement privée dans le tronc. À la machine à café, vous apprenez par hasard que Sally a apporté une modification intéressante à entier.c dans le tronc. Vous reportant à l'historique des propagations du tronc, vous vous apercevez qu'elle a corrigé un bogue crucial en révision 355, qui impacte directement la fonctionnalité sur laquelle vous êtes en train de travailler. Vous n'êtes peut-être pas encore prêt à fusionner toutes les modifications du tronc dans votre branche, mais vous avez certainement besoin de ce correctif pour continuer votre travail.

$ svn diff -c 355 http://svn.exemple.com/depot/calc/trunk

Index: entier.c
===================================================================
--- entier.c	(revision 354)
+++ entier.c	(revision 355)
@@ -147,7 +147,7 @@
     case 6:  sprintf(info->operating_system, "HPFS (OS/2 ou NT)"); break;
     case 7:  sprintf(info->operating_system, "Macintosh"); break;
     case 8:  sprintf(info->operating_system, "Z-System"); break;
-    case 9:  sprintf(info->operating_system, "CP/MM");
+    case 9:  sprintf(info->operating_system, "CP/M"); break;
     case 10:  sprintf(info->operating_system, "TOPS-20"); break;
     case 11:  sprintf(info->operating_system, "NTFS (Windows NT)"); break;
     case 12:  sprintf(info->operating_system, "QDOS"); break;

De la même façon que vous avez utilisé svn diff dans l'exemple précédent pour examiner la révision 355, vous pouvez passer le même paramètre à svn merge :

$ svn merge -c 355 http://svn.exemple.com/depot/calc/trunk
U    entier.c

$ svn status
M      entier.c

Vous pouvez à présent lancer les procédures habituelles de tests, avant de propager cette modification à votre branche. Après la propagation, Subversion marque r355 comme ayant été fusionnée dans la branche, afin qu'une future fusion « magique » synchronisant votre branche avec le tronc sache qu'elle doit sauter r355 (fusionner une même modification dans une même branche aboutit presque toujours à un conflit !).

$ cd ma-branche-calc

$ svn propget svn:mergeinfo .
/trunk:341-349,355

# Remarquez que r355 n'est pas listée comme "éligible" à la fusion,
# parce qu'elle a déjà été fusionnée.
$ svn mergeinfo http://svn.exemple.com/depot/calc/trunk --show-revs eligible
r350
r351
r352
r353
r354
r356
r357
r358
r359
r360

$ svn merge http://svn.example.com/depot/calc/trunk
--- Fusion de r350 à r354 dans '.':
 U   .
U    entier.c
U    Makefile
--- Fusion de r356 à r360 dans '.':
 U   .
U    entier.c
U    bouton.c

Ce type d'utilisation de la copie (ou rétroportage) de correctifs d'une branche à une autre est peut-être la raison la plus répandue pour sélectionner à la main des modifications ; le cas se présente très souvent, par exemple lorsqu'une équipe gère une « branche de production » du logiciel (ce thème est développé dans la section intitulée « Branches de publication »).

[Avertissement] Avertissement

Avez-vous remarqué la façon dont, dans le dernier exemple, le lancement de la fusion a eu pour effet l'application de deux ensembles distincts de fusions ? La commande svn merge a appliqué deux correctifs indépendants à votre copie de travail, afin de sauter l'ensemble de modifications 355, que votre branche contenait déjà. Il n'y a rien de mal en soi là-dedans, sauf que ça risque de rendre plus délicate la résolution des conflits. Si le premier groupe de modifications engendre des conflits, vous devrez les résoudre de façon interactive pour que la procédure de fusion puisse continuer et appliquer le deuxième groupe de modifications. Si vous remettez à plus tard un conflit lié à la première vague de modifications, la commande de fusion renverra au final un message d'erreur [23].

Avertissement : bien que svn diff et svn merge soient conceptuellement très similaires, leur syntaxe est différente dans de nombreux cas. Pour plus de détails, reportez-vous au Chapitre 9, Références complètes de Subversion ou consultez svn help. Par exemple, svn merge demande en entrée, en tant que cible, le chemin d'une copie de travail, c'est-à-dire un emplacement où il va appliquer le correctif généré. Si la cible n'est pas spécifiée, il suppose que vous essayez d'exécuter l'une des opérations suivantes :

  • Vous voulez fusionner les modifications du dossier dans votre dossier de travail en cours.

  • Vous voulez fusionner les modifications d'un fichier donné dans un fichier du même nom existant dans votre dossier de travail en cours.

Si vous fusionnez un dossier et que vous n'avez pas encore spécifié de cible, svn merge suppose qu'il est dans la première situation et essaie d'appliquer les modifications dans votre dossier en cours. Si vous fusionnez un fichier et que ce fichier (ou un fichier du même nom) existe dans votre dossier de travail en cours, svn merge suppose qu'il est dans la seconde situation et essaie d'appliquer les modifications au fichier local du même nom.

Syntaxe de la fusion : pour tout vous dire

Nous venons de voir des exemples d'utilisation de la commande svn merge et nous allons bientôt en voir plusieurs autres. Si vous n'avez pas bien assimilé le le fonctionnement des fusions, rassurez-vous, vous n'êtes pas un cas isolé. De nombreux utilisateurs (en particulier ceux qui découvrent la gestion de versions) commencent par une phase de perplexité au sujet de la syntaxe de la commande, ainsi que quand et comment utiliser cette fonctionnalité. Mais, en fait, cette commande est bien plus simple que vous ne le pensez ! Il y a une technique très simple pour comprendre comment svn merge agit.

La raison principale de la confusion est le nom de la commande. Le terme « merge » indique en quelque sorte que les branches vont être combinées, ou qu'un mystérieux mélange des données va avoir lieu. Ce n'est pas le cas. Un nom plus approprié pour cette commande aurait pu être « comparer-et-appliquer », car c'est là tout ce qui se passe : deux arborescences sont comparées et les différences sont appliquées à une copie de travail.

Si vous utilisez svn merge pour effectuer de simples copies de modifications entre branches, elle fait généralement ce qu'il faut automatiquement. Par exemple, une commande telle que :

$ svn merge http://svn.exemple.com/depot/calc/une-branche

tente de dupliquer toutes les modifications faites dans une-branche vers votre répertoire de travail actuel, qui est sans doute une copie de travail partageant des liens historiques avec la branche. La commande est suffisamment intelligente pour ne copier que les modifications que votre copie de travail ne possède pas encore. Si vous répétez cette commande une fois par semaine, elle ne copie que les modifications « les plus récentes » qui ont eu lieu depuis la dernière fusion.

Si vous choisissez d'utiliser la commande svn merge dans sa version intégrale en lui fournissant les groupes de révisions spécifiques à copier, la commande prend trois paramètres :

  1. une arborescence initiale (souvent appelée côté gauche de la comparaison) ;

  2. une arborescence finale (souvent appelée côté droit de la comparaison) ;

  3. une copie de travail qui reçoit les différences en tant que modifications locales (souvent appelée cible de la fusion).

Une fois ces trois paramètres fournis, les deux arborescences sont comparées et les différences sont appliquées à la copie de travail cible en tant que modifications locales. Une fois que la commande s'est terminée, le résultat est le même que si vous aviez édité les fichiers à la main ou lancé diverses commandes svn add ou svn delete vous-même. Si le résultat vous plaît, vous pouvez le propager. S'il ne vous plaît pas, vous pouvez toujours lancer svn revert pour revenir en arrière sur toutes les modifications.

La syntaxe de svn merge est assez flexible quant à la façon de spécifier les trois paramètres. Voici quelques exemples :

$ svn merge http://svn.exemple.com/depot/branche1@150 \
			 http://svn.exemple.com/depot/branche2@212 \
			 ma-copie-de-travail

$ svn merge -r 100:200 http://svn.exemple.com/depot/trunk ma-copie-de-travail

$ svn merge -r 100:200 http://svn.exemple.com/depot/trunk

La première syntaxe liste les trois arguments de façon explicite, spécifiant chaque arborescence sous la forme URL@REV et incluant la copie de travail cible. La deuxième syntaxe peut être utilisée comme raccourci pour les cas où vous comparez des révisions différentes de la même URL. La dernière syntaxe indique que le paramètre copie de travail est optionnel ; s'il est omis, elle utilise par défaut le répertoire en cours.

Si le premier exemple donne la syntaxe « complète » de svn merge, celle-ci doit être utilisée avec grande prudence ; elle peut en effet aboutir à des fusions qui n'enregistrent pas la moindre méta-donnée svn:mergeinfo. Le paragraphe suivant évoque ceci plus en détail.

Fusions sans mergeinfo

Subversion essaye de générer des métadonnées de fusion dès qu'il le peut, afin de rendre plus intelligentes les invocations suivantes de svn merge. Néanmoins, il reste des situations où les données svn:mergeinfo ne sont ni créées ni modifiées. Pensez à être prudent avec les scénarios suivants :

Fusionner des sources sans lien de parenté

Si vous demandez à svn merge de comparer deux URLs qui n'ont pas de lien entre elles, un correctif est quand même généré et appliqué à votre copie de travail, mais aucune métadonnée de fusion n'est créée. Il n'y a pas d'historique commun aux deux sources et les futures fusions « intelligentes » dépendent de cet historique commun.

Fusionner avec des dépôts extérieurs

Bien qu'il soit possible de lancer une commande telle que svn merge -r 100:200 http://svn.projetexterieur.com/depot/trunk, le correctif résultant ne comporte aucune métadonnée historique de fusion. À la date d'aujourd'hui, Subversion n'est pas capable de représenter des URL de dépôts différents au sein de la propriété svn:mergeinfo.

Utiliser --ignore-ancestry

Si ce paramètre est passé à svn merge, il force la logique de fusion à générer les différences sans réfléchir, de la même façon que svn diff les génère, en ignorant toute considération historique. Nous traitons ce point plus loin dans ce chapitre dans la section intitulée « Prise en compte ou non de l'ascendance ».

Appliquer des fusions inversées à l'historique naturel de la cible

Précédemment dans ce chapitre (dans la section intitulée « Retour en arrière sur des modifications »), nous avons vu comment utiliser svn merge pour appliquer un « correctif inversé », comme moyen de revenir en arrière sur des modifications. Si cette technique est utilisée pour revenir sur une modification faite à l'historique propre d'un objet (par exemple, propager r5 au tronc, puis revenir immédiatement en arrière sur r5 en utilisant svn merge . -c -5), ce type de fusion ne touche pas aux informations de fusion (mergeinfo) enregistrées [24].

Plus de détails sur les conflits liés aux fusions

Tout comme la commande svn update, svn merge applique les modifications à votre copie de travail. Elle est donc aussi susceptible de créer des conflits. Cependant, les conflits engendrés par svn merge sont parfois différents et ce paragraphe va expliquer ces différences.

Pour commencer, supposons que votre copie de travail n'a pas de modification locale en cours. Quand vous lancez svn update pour la mettre à jour à une révision particulière, les modifications envoyées par le serveur s'appliquent toujours « proprement » à votre copie de travail. Le serveur génère le delta en comparant deux arborescences : d'une part un instantané virtuel de votre copie de travail, d'autre part l'arborescence de la révision qui vous intéresse. Parce que la partie gauche de la comparaison est parfaitement égale à ce que vous avez déjà, il est garanti que le delta va convertir correctement votre copie de travail en l'arborescence de droite.

Mais svn merge ne dispose pas de telles garanties et peut être bien plus chaotique : l'utilisateur avancé peut demander au serveur de comparer n'importe quelle paire d'arborescences, même des arborescences n'ayant aucun rapport avec la copie de travail ! Cela laisse potentiellement beaucoup de place à l'erreur humaine. Les utilisateurs vont parfois comparer deux arborescences qui ne sont pas les bonnes, créant ainsi un delta qui ne s'appliquera pas proprement. svn merge fera de son mieux pour appliquer la plus grande partie possible du delta, mais ça risque d'être impossible pour certains morceaux. De la même façon que la commande Unix patch se plaint parfois de « morceaux ratés » (failed hunks), svn merge se plaint de « cibles manquantes omises » (skipped targets) :

$ svn merge -r 1288:1351 http://svn.exemple.com/depot/branche
U    truc.c
U    bidule.c
Cible manquante omise : 'baz.c'
U    blob.c
U    machin.h
Conflit découvert dans 'glorb.h'.
Sélectionner : (p) report, (df) diff complet, (e) édite,
        (h) aide pour plus d'options :

Dans l'exemple précédent, il est possible que baz.c existe dans les deux instantanés de la branche en question et que le delta résultant tente de modifier le contenu du fichier, mais que le fichier n'existe pas dans la copie de travail. Quoi qu'il en soit, le message « Cible manquante omise » signifie que l'utilisateur compare probablement les mauvaises arborescences ; c'est le signe classique d'une erreur de l'utilisateur. Quand ça arrive, il est facile de revenir en arrière de manière récursive sur toutes les modifications créées par la fusion (svn revert . --recursive), d'effacer tout fichier ou dossier non suivi en versions restant après le retour en arrière et de relancer svn merge avec des paramètres différents.

Remarquez également que l'exemple précédent indique un conflit sur glorb.h. Nous avons déjà mentionné que la copie de travail n'a pas de modifications locales en cours : comment peut-il donc y avoir conflit ? Encore une fois, parce que l'utilisateur peut utiliser svn merge pour définir et appliquer n'importe quel vieux delta à la copie de travail, ce delta risque de contenir des modifications textuelles qui ne s'appliquent pas proprement à un fichier de la copie de travail, même si ce fichier n'a pas de modifications locales en cours.

Une autre petite différence entre svn update et svn merge concerne les noms des fichiers textes créés quand un conflit a lieu. Dans la section intitulée « Résoudre les conflits (fusionner des modifications) », nous avons vu qu'une mise à jour génère des fichiers appelés nomdufichier.mine, nomdufichier.rOLDREV et nomdufichier.rNEWREV. Cependant, quand svn merge cause un conflit, il crée trois fichiers appelés nomdufichier.working, nomdufichier.left et nomdufichier.right. Dans ce cas, les termes « left » et « right » décrivent de quel côté de la double comparaison d'arborescences le fichier venait. Dans tous les cas, ces noms de fichiers différents vous aideront à distinguer les conflits résultant d'une mise à jour de ceux résultant d'une fusion.

Blocage de modifications

Il peut parfois y avoir un ensemble de modifications particulier dont vous ne voulez pas qu'il soit fusionné automatiquement. Par exemple, peut–être que l'habitude dans votre équipe est d'effectuer tout nouveau travail de développement dans /trunk, mais d'être plus conservateur en ce qui concerne le rétroportage des modifications vers une branche stable que vous utilisez pour la publication. À l'extrême, vous pouvez sélectionner à la main des ensembles de modifications individuels du tronc à porter vers la branche : juste les changements qui sont suffisamment stables pour être acceptables. Peut-être que les choses ne sont pas aussi strictes après tout ; peut-être que la plupart du temps vous aimeriez juste laisser svn merge fusionner automatiquement la plupart des modifications du tronc vers la branche. Dans ce cas, il vous faudrait une façon de masquer quelques modifications particulières, c'est-à-dire d'empêcher qu'elles ne soient fusionnées automatiquement.

Dans Subversion 1.5, la seule manière de bloquer une liste de modifications est de faire croire au système que cette modification a déjà été fusionnée. Pour cela, il est possible de lancer la commande de fusion avec l'option --record-only :

$ cd ma-branche-calc

$ svn propget svn:mergeinfo .
/trunk:1680-3305

# Marquons l'ensemble de modifications r3328 comme déjà fusionné.
$ svn merge -c 3328 --record-only http://svn.exemple.com/depot/calc/tronc

$ svn status
M     .

$ svn propget svn:mergeinfo .
/trunk:1680-3305,3328

$ svn commit -m "Empêche r3328 d'être fusionnée vers la branche."
…

Cette technique fonctionne, mais elle est un petit peu dangereuse. Le problème principal est que nous ne faisons pas clairement la différence entre « J'ai déjà cette modification » et « Je n'ai pas cette modification ». En fait, nous mentons au système, en lui faisant croire que la modification a déjà été fusionnée. Ce qui transfère vers vous, l'utilisateur, la responsabilité de vous rappeler que la modification n'a en fait pas été fusionnée, qu'elle n'était tout simplement pas voulue. Il n'y a pas moyen de demander à Subversion la liste des « listes de modifications bloquées ». Si vous voulez en conserver la trace (afin de pouvoir les débloquer un jour), vous devrez les consigner dans un fichier texte quelque part, ou peut-être dans une propriété inventée de toutes pièces pour l'occasion. Dans Subversion 1.5, malheureusement, c'est la seule façon de gérer les révisions bloquées ; dans les versions futures il est prévu d'en améliorer l'interface.

Historiques et annotations tenant compte des fusions passées

Une des fonctionnalités principales de tout système de gestion de versions est de conserver la trace de qui a modifié quoi et quand ils l'ont fait. Les commandes svn log et svn blame sont les outils adaptés pour cela : quand on les applique à des fichiers individuels, ils renvoient non seulement l'historique des ensembles de modifications qui ont touché le fichier, mais aussi exactement quel utilisateur a écrit quelle ligne de code et quand il l'a fait.

Cependant, quand des modifications commencent à être copiées entre des branches, les choses commencent à se compliquer. Par exemple, si vous interrogiez svn log sur l'historique de votre branche fonctionnelle, il renverrait exactement toutes les révisions qui ont touché cette branche :

$ cd ma-branche-calc
$ svn log -q
------------------------------------------------------------------------
r390 | utilisateur | 2002-11-22 11:01:57 -0600 (ven. 22 nov. 2002) | 1 ligne
------------------------------------------------------------------------
r388 | utilisateur | 2002-11-21 05:20:00 -0600 (jeu. 21 nov. 2002) | 2 lignes
------------------------------------------------------------------------
r381 | utilisateur | 2002-11-20 15:07:06 -0600 (mer. 20 nov. 2002) | 2 lignes
------------------------------------------------------------------------
r359 | utilisateur | 2002-11-19 19:19:20 -0600 (mar. 19 nov. 2002) | 2 lignes
------------------------------------------------------------------------
r357 | utilisateur | 2002-11-15 14:29:52 -0600 (ven. 15 nov. 2002) | 2 lignes
------------------------------------------------------------------------
r343 | utilisateur | 2002-11-07 13:50:10 -0600 (jeu. 07 nov. 2002) | 2 lignes
------------------------------------------------------------------------
r341 | utilisateur | 2002-11-03 07:17:16 -0600 (dim. 03 nov. 2002) | 2 lignes
------------------------------------------------------------------------
r303 | sally | 2002-10-29 21:14:35 -0600 (mar. 29 oct. 2002) | 2 lignes
------------------------------------------------------------------------
r98 | sally | 2002-02-22 15:35:29 -0600 (ven. 22 fev. 2002) | 2 lignes
------------------------------------------------------------------------

Mais est-ce bien là une description adéquate de tous les changements qui ont eu lieu sur cette branche ? Ce qui manque ici, c'est le fait que les révisions 390, 381 et 357 résultaient en fait de fusions en provenance du tronc. Si vous regardez plus en détail l'historique d'une de ces révisions, vous ne verrez nulle part les multiples ensembles de modifications du tronc qui ont été reportés sur la branche :

$ svn log -v -r 390
------------------------------------------------------------------------
r390 | utilisateur | 2002-11-22 11:01:57 -0600 (ven. 22 nov. 2002) | 1 ligne
Chemins modifiés :
   M /branches/ma-branche-calc/bouton.c
   M /branches/ma-branche-calc/LISEZMOI

Fusion finale des modifications du tronc dans ma-branche-calc.

Il se trouve que nous savons que cette fusion vers la branche n'était qu'une fusion de modifications du tronc. Comment pouvons-nous également voir ces modifications du tronc ? La réponse est d'utiliser l'option --use-merge-history (-g). Cette option donne le détail des modifications « filles » qui faisaient partie de la fusion.

$ svn log -v -r 390 -g
------------------------------------------------------------------------
r390 | utilisateur | 2002-11-22 11:01:57 -0600 (ven. 22 nov. 2002) | 1 ligne
Chemins modifiés :
   M /branches/ma-branche-calc/bouton.c
   M /branches/ma-branche-calc/LISEZMOI

Fusion finale des modifications du tronc dans ma-branche-calc.
------------------------------------------------------------------------
r383 | sally | 2002-11-21 03:19:00 -0600 (jeu. 21 nov. 2002) | 2 lignes
Chemins modifiés :
   M /branches/ma-branche-calc/bouton.c
Fusion via: r390

Corrige l'erreur d'inversion graphique sur le bouton.
------------------------------------------------------------------------
r382 | sally | 2002-11-20 16:57:06 -0600 (mer. 20 nov. 2002) | 2 lignes
Chemins modifiés :
   M /branches/ma-branche-calc/LISEZMOI
Fusion via: r390

Documente mon dernier correctif dans LISEZMOI.

En forçant l'opération svn log à utiliser l'historique des fusions, nous obtenons non seulement la révision que nous avions demandé (r390), mais aussi les deux révisions qui l'accompagnaient — deux modifications du tronc faites par Sally. C'est une image bien plus complète de l'historique !

La commande svn blame accepte également l'option --use-merge-history (-g). Si cette option est omise, quelqu'un qui regarderait un relevé annoté ligne par ligne pour bouton.c risquerait d'avoir l'impression erronée que vous êtes responsable des lignes qui ont corrigé une certaine erreur :

$ svn blame bouton.c
…
   390    utilisateur  retval = inverse_func(button, path);
   390    utilisateur  return retval;
   390    utilisateur  }
…

Et bien qu'il soit vrai que vous avez propagé ces trois lignes lors de la révision 390, deux d'entre elles ont en fait été écrites par Sally auparavant, en révision 383 :

$ svn blame bouton.c -g
…
G    383    sally        retval = inverse_func(button, path);
G    383    sally        return retval;
     390    utilisateur  }
…

À présent, nous savons qui doit réellement être tenu responsable pour ces deux lignes de code !

Prise en compte ou non de l'ascendance

Quand vous discutez avec un développeur Subversion, il est très possible qu'il fasse référence au terme d'ascendance. Ce mot est utilisé pour décrire la relation entre deux objets dans un dépôt : s'ils sont liés l'un à l'autre, un des objets est alors qualifié d'ancêtre de l'autre.

Par exemple, supposons que vous propagiez la révision 100 qui contient une modification d'un fichier truc.c. Dès lors, truc.c@99 est un « ancêtre » de truc.c@100. En revanche, supposons que vous propagiez la suppression de truc.c en révision 101 et ensuite l'ajout d'un nouveau fichier du même nom en révision 102. Dans ce cas, truc.c@99 et truc.c@102 pourraient sembler apparentés (ils ont le même chemin), mais en fait ce sont des objets complètement différents au sein du dépôt. Ils ne partagent aucun historique ou « ascendance ».

Nous abordons ce point pour mettre en évidence une différence importante entre svn diff et svn merge. La première commande ignore toute ascendance, tandis que la seconde y est particulièrement sensible. Par exemple, si vous demandez à svn diff de comparer les révisions 99 et 102 de truc.c, vous obtenez des différences basées sur les lignes ; la commande svn diff compare deux chemins à l'aveugle. Mais si vous demandez à svn merge de comparer les deux mêmes objets, il remarque qu'ils ne sont pas liés et essaie d'abord de supprimer l'ancien fichier, puis d'ajouter le nouveau fichier ; le résultat indique une suppression puis un ajout :

D    truc.c
A    truc.c

La plupart des fusions impliquent de comparer des arborescences qui ont une relation d'ascendance de l'une à l'autre ; c'est pourquoi svn merge a ce comportement par défaut. Cependant, à l'occasion, vous pourriez vouloir que la commande svn merge compare deux arborescences sans relation d'ascendance entre elles. Par exemple, vous avez peut-être importé deux arborescences de code source représentant des publications différentes de deux fournisseurs d'un projet logiciel (voir la section intitulée « Branches fournisseur »). Si vous demandez à svn merge de comparer les deux arborescences, vous verrez la première arborescence complètement supprimée, puis l'ajout de la seconde arborescence toute entière ! Dans ce genre de situations, vous attendez de svn merge qu'il effectue une comparaison basée sur les chemins uniquement, en ignorant toute relation entre les fichiers et les dossiers. Ajoutez l'option --ignore-ancestry à votre commande svn merge et elle se comportera comme svn diff (et inversement, l'option --notice-ancestry fera se comporter svn diff comme la commande svn merge).

Fusions, copies et renommages

Il arrive souvent qu'on veuille réorganiser le code source, en particulier dans les projets logiciels en Java. Les fichiers et les répertoires sont déplacés et renommés, causant souvent de grandes perturbations pour tous ceux qui travaillent sur le projet. Ceci semble être le cas idéal où utiliser une branche, n'est-ce pas ? Créer une branche, réorganiser les choses, et ensuite fusionner la branche vers le tronc, non ?

Hélas, ce scénario ne fonctionne pas si bien pour le moment et est même considéré comme l'une des faiblesses de Subversion. Le problème est que la commande svn update de Subversion n'est pas aussi robuste qu'elle le devrait, en particulier en ce qui concerne les opérations de copies et de déplacements.

Quand vous utilisez svn copy pour dupliquer un fichier, le dépôt se souvient d'où venait le nouveau fichier, mais il ne transmet pas cette information au client qui lance svn update ou svn merge. Au lieu de dire au client « Copie ce fichier que tu as déjà vers ce nouvel emplacement », il envoie un fichier entièrement nouveau. Ceci peut engendrer des problèmes, en particulier parce que le fonctionnement est le même pour les fichiers renommés. Un fait peu connu à propos de Subversion est qu'il lui manque de « vrais renommages » - la commande svn move n'est rien de plus que l'agrégation de svn copy et svn delete.

Par exemple, supposons que pendant que vous travaillez sur votre branche privée, vous renommez entier.c en tout.c. Ce faisant, vous avez en fait créé un nouveau fichier dans votre branche, qui est une copie du fichier original, et vous avez supprimé le fichier original. Pendant ce temps, dans le tronc, Sally a propagé des améliorations d'entier.c. C'est alors que vous décidez de fusionner votre branche vers le tronc :

$ cd calc/trunk

$ svn merge --reintegrate http://svn.exemple.com/depot/calc/branches/ma-branche-calc
--- Fusion des différences des URLs du dépôt vers '.' :
D   entier.c
A   tout.c
U   .

Ceci n'a pas l'air si mal à première vue, mais ce n'est probablement pas ce à quoi Sally ou vous vous attendiez. L'opération de fusion a supprimé la version la plus récente du fichier entier.c (celle qui contenait les dernières modifications de Sally) et a ajouté machinalement votre nouveau fichier tout.c, qui est une copie de l'ancien fichier entier.c. Le résultat final est que la fusion de votre « renommage » a supprimé de la dernière révision les modifications récentes de Sally !

Il ne s'agit pas d'une vraie perte de données. Les modifications de Sally font toujours partie de l'historique du dépôt, mais le déroulement exact des événements n'est peut-être pas immédiatement évident. La morale de l'histoire est que jusqu'à ce que Subversion ne s'améliore, vous devrez faire très attention aux fusions de copies et de renommages d'une branche à une autre.

Blocage des clients qui ne prennent pas en compte les fusions

Si vous venez de mettre à niveau votre serveur Subversion à la version 1.5 ou plus, il existe un risque significatif que les clients Subversion pré-1.5 sèment la pagaille dans votre suivi automatique des fusions. Pourquoi ? Quand un client Subversion pré-1.5 exécute svn merge, il ne modifie pas du tout la valeur de la propriété svn:mergeinfo. La propagation qui s'ensuit, bien qu'elle soit le résultat d'une fusion, n'envoie donc aucune indication au dépôt au sujet des modifications dupliquées — ces informations sont perdues. Par la suite, lorsque des clients « qui prennent en compte les fusions » tentent d'effectuer une fusion automatique, ils rencontreront probablement toutes sortes de conflits résultant des fusions répétées.

Si votre équipe et vous dépendez des fonctionnalités de suivi des fusions de Subversion, vous voudrez peut-être configurer votre dépôt pour qu'il empêche les anciens clients de propager des modifications. La méthode la plus simple est d'examiner le paramètre « capabilities » dans la procédure automatique de début de propagation. Si le client indique être capable de gérer les mergeinfo, la procédure automatique peut l'autoriser à commencer la propagation. Si le client n'indique pas en être capable, la procédure automatique doit lui refuser la propagation. Nous en apprendrons plus sur les procédures automatiques dans le chapitre suivant ; voir la section intitulée « Mise en place des procédures automatiques » et start-commit pour les détails.

Recommandations finales sur le suivi des fusions

En fin de compte, la fonctionnalité de suivi des fusions de Subversion possède une mécanique interne extrêmement complexe et la propriété svn:mergeinfo est la seule lorgnette dont l'utilisateur dispose pour observer cette mécanique. Parce que cette fonctionnalité est relativement nouvelle, un certain nombre de cas litigieux et de comportements potentiellement inattendus risquent d'être rencontrés.

Par exemple, mergeinfo sera parfois générée lors d'une simple commande svn copy ou svn move. Parfois mergeinfo apparaîtra sur des fichiers dont vous n'auriez pas imaginé qu'ils soient touchés par une opération. Parfois mergeinfo ne sera pas du tout générée, contrairement à ce que vous attendiez. De plus, la gestion de la métadonnée mergeinfo a tout un ensemble de taxonomies et de comportements qui lui sont associés, tels que des mergeinfo « explicites » par opposition à « implicites », des révisions « opérationnelles » par opposition à « non-opérationnelles », des mécanismes spécifiques d'« élision » de mergeinfo et même d'« héritage » de répertoires parents à sous-répertoires.

Nous avons choisi de ne pas couvrir en détail ces sujets dans ce livre pour plusieurs raisons. Premièrement, l'utilisateur moyen serait totalement submergé par le niveau de détail disponible. Deuxièmement, au fur et à mesure que Subversion s'améliore, nous estimons que l'utilisateur moyen ne devrait pas avoir à comprendre ces concepts ; en tant que détails d'implémentation agaçants, ils finiront par disparaître à l'arrière-plan. Malgré tout ce qui vient d'être dit, si vous appréciez ce genre de choses, vous en trouverez une formidable vue d'ensemble dans un article posté sur le site internet de Collabnet : http://www.collab.net/community/subversion/articles/merge-info.html.

Pour le moment, si vous voulez rester à l'écart des bogues et autres comportements inattendus des fusions automatiques, l'article de Collabnet recommande que vous vous en teniez simplement aux bonnes pratiques suivantes :

  • Pour les branches fonctionnelles à courte durée de vie, suivez la procédure simple décrite dans la section intitulée « Fusions : pratiques de base ».

  • Pour les branches de publication à longue durée de vie (comme décrites dans la section intitulée « Modèles courants de gestion des branches »), ne pratiquez de fusions que sur la racine de la branche, pas sur des sous-répertoires.

  • Ne pratiquez jamais de fusion vers des copies de travail contenant un mélange de numéros de révisions de travail, ou ayant des sous-répertoires « déportés » (comme décrit par la suite dans la section intitulée « Parcours des branches »). La cible d'une fusion doit être une copie de travail qui représente un emplacement unique à un moment unique dans le dépôt.

  • Ne modifiez jamais la propriété svn:mergeinfo directement ; utilisez svn merge avec l'option --record-only pour appliquer une modification désirée à cette métadonnée (comme expliqué dans la section intitulée « Blocage de modifications »).

  • Assurez-vous de toujours avoir l'accès complet en lecture à toutes vos sources de fusion, et vérifiez que votre copie de travail cible n'a pas de dossiers clairsemés.



[23] Ceci est au vrai pour Subversion 1.5 au moment où ces lignes sont écrites. Ce fonctionnement sera sans doute amélioré dans les versions futures de Subversion.

[24] À noter qu'après être revenu en arrière sur une révision de cette manière, nous ne serions plus capables de ré-appliquer cette révision avec svn merge . -c 5, puisque les mergeinfo incluraient déjà r5 comme ayant été appliquée. Nous serions alors obligés d'utiliser l'option --ignore-ancestry pour forcer la commande de fusion à ignorer le contenu de mergeinfo !