Faciliter le merge dans Subversion

A l’heure des DVCS, la création des branches et le merge paraissent quelque chose de bien facile. Surtout lorsque l’on se rappelle des problèmes que cela représentait au temps de Subversion(SVN) ou pire de CVS.

La motivation générale pour essayer de mettre en place Git ou Mercurial et la facilité de mise en place d’un Git over SVN par exemple donnent de l’espoir pour les prochaines années. La migration risque d’être rapide si l’ensemble de la communauté est conquise.

Cependant, dans certains projets, il va falloir vivre encore quelques temps avec Subversion. Cet article présente des outils pour faciliter la gestion des branches dans Subversion et rappeler un ensemble de bonnes pratiques.

Rappels des bonnes pratiques

Tofu-scale

Les concepts utilisés ci-dessous sont issus de Pratical Perforce de  Laura Wingerd. La “Tofu-scale” est une échelle décrite par Laura Wingerd qui note les branches de son repository en niveau de fermeté. Ceci est decrit dans le chapitre 7 que vous pouvez lire ici. Le schema 7-13 presente le degré de fermeté en fonction de l’emplacement de la branche.

Pour chaque niveau de fermeté, on associe une politique de qualité et chaque commit doit s’assurer de respecter cette politique. Un exemple de politique de branches pourrait être la suivante :

  • les branches de maintenance situées au dessus du tronc et donc très fermes (“firmer”) doivent compiler, passer les tests unitaires et les tests d’intégration.
  • le tronc doit compiler et passer les tests unitaires.
  • les branches de développement situées en dessous et donc plus molles (“softer”) ne doivent que compiler. Un commit peut casser les tests unitaires car ce ne sont que des branches de travail.

Ces politiques doivent être vérifiées automatiquement par une intégration continue.

La politique de merge des branches fonctionne selon la regle du “merge down, copy up” :

  • le merge de modifications d’une branche plus ferme a une branche plus molle (ex: branche de maintenance vers le tronc) se fait par merge. Tous les commits effectués sur la branche seront reportés sur le tronc
  • le merge de modifications d’une branche plus molle vers une branche plus ferme (ex: de la branche de développement vers le tronc) se fait par copie. L’ensemble de la branche de développement passe en tronc. Ceci ne peut se faire que si la branche de développement a atteint le niveau de qualité requis par la politique du tronc.

Branche pour fonctionnalité majeure

En mode de développement agile, il peut arriver qu’une fonctionnalité ne soit pas totalement terminée à la fin d’un sprint. Il est intéressant qu’aucun code source concernant cette fonctionnalité ne soit inclus si la fonctionnalité n’est pas terminée. Dans le cas contraire, il faut s’assurer que les parties de la fonctionnalité commités avant la fin du sprint n’entraînent pas de régressions ou ne présentent pas de fonctionnalité incomplète au client.

Dans le schéma ci-dessous, la fonctionnalité A (constituée des commits A-1, A-2, A-3, A-4) n’est pas complète pour la fin du sprint 1.2 et le tag de la version 1.2.0. Par conséquent les commits A-1 et A-2 sont inclus dans la version 1.2.0 alors que la fonctionnalité ne sera disponible complètement que pour le prochain sprint.

fonctionnalite sans branche de developpement

Pour cela, chaque grosse fonctionnalité doit se faire sur une branche de développement plus molle (“softer“) que le tronc. Ce n’est seulement qu’a la fin de la fonctionnalité (développements et tests) que l’ensemble de la fonctionnalité est incluse dans le code source du sprint (en général le tronc).

Dans le schéma ci-dessous, les commits concernant la fonctionnalité A sont appliqués sur une branche. La flèche verte représente le merge de la fonctionnalité A complète vers le tronc après la release 1.2.0.

fonctionnalite avec branche de developpement

Présentation de Jigomerge

Jigomerge est un script goovy open-source qui doit permettre de faciliter les procédures de merge entre les branches. Il se base sur svnmerge.

Svnmerge est une sur-couche a Subversion qui stocke des informations dans les propriétés svn (svnmerge-blocked, svnmerge-integrated) pour faciliter la marquage des révisions déjà intégrées.

L’algorithme de jigomerge est présenté ici

Jigomerge va :

  • initialiser le merge automatiquement si cela n’est pas encore fait (commande “svnmerge init”)
  • verifier pour chaque revision à merger que le commentaire de commit ne contient pas des patterns indiquant qu’il faut ignorer cette révision. Par exemple, les commits d’initialisation d’un merge ne doivent pas être mergé. Ils sont reperés et marqués comme bloqués (commande “svnmerge block”)
  • verifier pour chaque révision à merger que celle ci n’entraîne pas de conflit. Si c’est le cas, jigomerge s’arrête et merge seulement les révisions précédentes sans conflit.

La gestion de conflit reste une opération manuelle. Cependant le pari de jigomerge est que plus le merge est fréquent et automatisé, plus les développeurs travaillent sur des branches le plus a jour les unes par rapport aux autres. Le risque de conflit y est donc moins fréquent.

Jigomerge est pensé pour s’intégrer a un outil d’intégration continue. Dans le cas ou jigomerge tombe sur un conflit, le code retour du script sera une erreur. Nous pouvons ainsi récupérer cette erreur dans hudson par exemple pour qu’il notifie la personne en charge du merge par messagerie ou email.

Certaines options sont disponible au lancement de jigomerge :

  • eager mode : tous les commits qui peuvent se merger sans conflit sont mergés par jigomerge. Même si le commit précèdent est en conflit, le commit suivant sans conflit est mergé.
  • commit one by one : commit après le merge de chaque révision plutôt que de commiter l’ensemble des merges en une seule fois.

Cas Pratique

Le cas Subversion que nous allons étudier présente une configuration de branche assez classique qui se retrouve dans un grand nombre de projets.

La mise en oeuvre de l’outil “jigomerge” dans Hudson va permettre d’automatiser le processus de merge décrit ci-dessus. Les étapes de copie restent quant a elles manuelles. En effet, il convient avant toute copie de s’assurer de la qualité de la branche et du respect de la politique définit pour la branche de destination. Cependant nous verrons comment lancer cette “copie” a travers jigomerge.

Notre repository comprend un tronc a l’emplacement suivant : http://myrepo.mycompany.fr/svn/trunk.

C’est à partir du tronc que nous releasons une version finale.

Branche de développement

Pour des fonctionnalités entraînant des impacts majeurs, nous utilisons une branche de développement. Cette branche est plus molle (softer) que le tronc. Quelques tests unitaires peuvent échouer sur cette branche alors qu’aucun commit ne doit casser les tests unitaires sur le tronc.

Considérons une branche de développement “D” qui sert au développement de la fonctionnalité A et que cette branche se trouve a l’url suivante : http://myrepo.mycompany.fr/svn/branches/branch-d.

Des que cette branche est créée, il va nous falloir maintenir une cohérence entre le tronc et la branche D. En effet, toutes les corrections commitées sur le tronc doivent être visible le plus vite possible sur la branche D. Nous voulons absolument éviter un effet tunnel ou lorsque l’équipe aura fini la fonctionnalité A sur la branche D le merge vers le tronc consistera également à vérifier que les bug-fixes du tronc ne sont pas écrases par le merge de la fonctionnalité A.

Ainsi tous les commits effectués sur le tronc vont etre mergés automatiquement et le plus rapidement possible vers la branche D. De cette façon, les seules différences visibles entre la branche D et le tronc au moment du merge de la fonctionnalité A seront les commits de la fonctionnalité A elle-même.

Nous allons configurer jigomerge dans un job Hudson. jigomerge se lance a partir d’une copie de travail de la branche D a jour (http://myrepo.mycompany.fr/svn/branches/branch-d).

Nous exécutons la commande suivante

groovy jigomerge.groovy -u http://myrepo.mycompany.fr/svn/trunk

branche de developpement

Le schéma ci-dessus présente le merge des commits du tronc vers la branche D. Les commits D-1 et D-2 sont ceux de la fonctionnalité A sur la branche D. Les actions du job Jigomerge sont representées par les flêches vertes.

Branche de maintenance

Lorsque nous releasons une version a partir du tronc, nous taggons le tronc. Il arrive que cette version, après une phase de recette poussée, présente certaines anomalies. Il nous faut les corriger mais comme le tronc a commencé une nouvelle itération, nous tirons une branche a partir du tag precedemment créé.

Par exemple dans notre cas, l’équipe travaille sur le tronc pour une version 1.2.0. Lorsque la version 1.2.0 est terminée, nous taggons le tronc en 1.2.0 et le tronc commence a travailler sur la version 1.3.0. Si une anomalie est découverte sur la 1.2.0, une branche 1.2 est créée a partir du tag pour permettre de produire une version 1.2.1

La branche http://myrepo.mycompany.fr/svn/branches/branch-1.2 est plus ferme (“firmer“) que le tronc. Les versions issues de cette branche sont celles qui seront livrées en production, par conséquent l’ensemble des tests (unitaires, integrations et même manuels) doivent s’executer correctement.

Dans le schema suivant, M-1 and M-2 sont les corrections effectuées sur la branche 1.2.

branche de maintenance sans merge

Dans ce cas, jigomerge se lance a partir d’une copie de travail du tronc à jour (http://myrepo.mycompany.fr/svn/trunk).

Nous executons la commande suivante :

groovy jigomerge.groovy -u http://myrepo.mycompany.fr/svn/branches/branch-1.2

Les actions de jigomerge sont presentées ci-dessous par des fleches oranges.

branche de maintenance avec merge

Vue globale

Voici un schéma presentant les actions des 2 jobs Jigomerge. Les commits M-1 et M-2 seront automatiquement mergé vers le tronc par le job “branch 1.2 vers tronc” puis le job “tronc vers branch D” mergera ces nouveaux commits (en plus de ceux appliqués directement sur le tronc) du tronc vers la branche de developpement.

vue globale du merge avec branche de maintenance et branche de developpement

Copie de la fonctionnalité A vers le tronc

Lorsque que la fonctionnalité A est terminée sur la branche D que tous les tests sont passés, nous allons l’integrer dans le tronc.

Pour cela, sur une copie de travail du tronc a jour, nous exécutons :

groovy jigomerge.groovy -b -u http://myrepo.mycompany.fr/svn/branches/branch-d

L’option “-b” (bidirectionnal merge) permet de s’assurer que les révisions mergées précédemment du tronc vers la branche D ne seront pas remergées a nouveau de la branche D vers le tronc.

References

http://oreilly.com/catalog/practicalperforce/chapter/ch07.pdf

http://subversion.apache.org/

http://www.orcaware.com/svn/wiki/Svnmerge.py

http://code.google.com/p/jigomerge/

http://code.google.com/p/jigomerge/wiki/Algorithm

http://code.google.com/p/jigomerge/wiki/PracticalCase