Migration en largeur

Par migration en largeur, nous entendons l’action de livrer une nouvelle version du code qui impacte une grande partie de la code base pouvant être source de conflit.

Nous pouvons citer par exemples :

  • Changer la signature d’une interface implémentée par de nombreuses classes
  • Migrer vers un nouveau framework ou API
  • Changer les règles d’indentation du code

Dans tous ces cas, plus cette partie du code est utilisée, plus il est probable qu’elle soit source de conflits avec d’autres développements en cours.

La méthode brutale : la migration en une seule fois

C’est la méthode la plus intuitive, qui consiste à publier la migration entièrement en une itération. Elle rend disponible à tous la nouvelle version du code et supprime brutalement l’ancienne.

Par conséquent, une fois cette étape réalisée, tous les développements en cours utilisant l’ancienne version du code se retrouvent bloqués (ne compilent plus). Leurs développements sont obsolètes et ne peuvent plus merger avec master, les développeurs sont obligés de rebase leur branche sur la branche de migration afin d’utiliser la nouvelle version du code.

Avec cette méthode nous impactons directement les autres développements (potentiellement effectués dans d’autres équipes ayant des priorités autres que notre migration). C’est donc une méthode peu élégante et peu recommandée, de plus elle scale très mal avec la taille et le nombre des équipes de développement.

La méthode douce : la migration continue

Nous nous imposons ici comme contrainte de ne pas perturber les autres développements, la migration ne doit pas provoquer de conflits.

Pour ce faire, nous pouvons raisonner comme pour une migration d’API classique :

  • Étape 1 : exposer la nouvelle API, tout en laissant vivre l’ancienne
  • Étape 2 : migrer les anciens usages vers la nouvelle version
  • Étape 3 : supprimer l’ancienne version lorsqu’elle n’est plus utilisée

Au niveau git, nous allons avoir ces étapes :

  • Merge initial dans la branche master ou équivalent, pour exposer la nouvelle version (conflit peu probable)
  • Merge(s) dans master au fil de l’eau de migrations d’usages de l’ancienne vers la nouvelle version (ne pas livrer si conflit)
  • Merge dans master de la suppression de l’ancienne version (pas de conflit une fois que tous les usages sont reportés sur la nouvelle version)

Étape 1 : Exposer la nouvelle version

En fonction de la migration, il existe plusieurs façons d’exposer notre nouvelle version sans retirer l’ancienne (qui est utilisée par des développements en cours).

Un point important ici est de s’assurer que les futurs clients ne consommeront pas l’ancienne version. En java on pourra utiliser l’annotation @Deprecated, en précisant en commentaire javadoc l’usage de remplacement.

Par exemple pour un changement de signature de méthode, il s’agit de laisser l’ancienne méthode et d’exposer la nouvelle, en dépréciant l’ancienne et en indiquant comment utiliser la nouvelle.

Pour un déplacement de package, il faut voir le déplacement comme un ajout et retrait, en laissant la partie qu’on souhaite retirer et l’annoter pour éviter que les nouveaux usages se portent sur l’ancienne.

Pour migrer le style de formattage, cette première étape consistera à faire en sorte que tout le monde utilise le nouveau format.

Étape 2 : Migrer les anciens usages

Pour cette étape, nous avons besoin de savoir qui utilise l’ancienne version afin de ne pas retirer les usages en cours d’utilisation.

Pour détecter les éventuels consommateurs de l’ancienne version, un script vérifiant que chaque développement en cours merge 1 à 1 avec notre migration va permettre de détecter les conflits directs, et donc de voir qui utilise les lignes que nous cherchons à modifier.

Chez LesFurets.com cette vérification est réalisée via l’utilisation de git-octopus, un CLI permettant entre autre de vérifier que tous les développements en cours mergent entre eux.

La plus grande partie des usages consommés par d’autres peuvent être détectés par ce « merge check », qui permet d’obtenir un feedback très rapide.

Nous pouvons aussi obtenir un feedback plus précis mais plus lent cette fois, en vérifiant que le résultat du merge des développements en cours passent les étapes de l’IC (Intégration continue : tests unitaires et tests d’intégrations). En effet certaines branches en cours peuvent ne pas provoquer de conflits, mais ne plus compiler ou casser des tests par exemple.

Nous allons donc procéder aux étapes suivantes :

  1. Migrer tout ou partie des usages
  2. Effectuer un « merge check », c’est à dire vérifier que notre branche de migration merge 1 à 1 avec toutes les branches en cours de développement
  3. S’il y a un ou plusieurs conflit(s), revenir en arrière sur ses modifications pour laisser l’ancienne version (par exemple via git checkout origin/master -- filename)
  4. Effectuer la validation du résultat du merge des branches entre elles
  5. S’il y a des erreurs, ici aussi il faut revenir en arrière sur les endroits conflictuels
  6. Livrer (merge dans master des migrations ne provoquant aucun conflit)

Ainsi nous ne migrons que ce qui n’est pas en cours d’utilisation, et nous ne dérangeons personne.

Une bonne pratique consiste à livrer petit, et rapidement. Ainsi vous minimiserez les chances qu’un conflit ne vienne s’interposer le temps de la migration.

Pour certain cas, comme un rename, il sera plus rapide de migrer tous les usages à l’aide d’un IDE puis de remettre l’ancienne version sur les usages conflictuels.

Mais pour un cas plus complexe, il vaut mieux migrer quelques usages, effectuer les vérifications de conflit puis livrer rapidement, au fil de l’eau. Ainsi nous minimisons les chances qu’un conflit apparaisse.

Étape 3 : Supprimer l’ancienne version

Comme pour l’étape 2, il suffit ici de supprimer l’ancienne version, en s’assurant qu’elle n’est pas en cours d’utilisation par la branche d’un collègue.

Pros & Cons

La méthode brutale permet de faire sa migration sans se soucier des autres. Elle peut s’avérer plus efficace dans le cas où la probabilité de conflit est très faible.

La méthode douce ou « continue » peut sembler plus longue à première vue, mais en pratique la perte de temps liée à la double vie de version est minime dans la mesure où nous livrons en continu. Nous gagnons du temps en n’en faisant pas perdre aux autres.