Composition des solutions

Les solutions de CiviKey sont des solutions dites « composées » : chaque solution tire parti d’une ou plusieurs solutions pour ne faire qu’ajouter des fonctionnalités, sans devoir recopier des projets essentiels. La solution Core est la solution de base, elle regroupe les projets du noyau permettant par exemple de lire et sauvegarder les données de CiviKey. Ensuite la solution Certified apporte un host d’exécution du Core, ainsi que des plugins essentiels, elle est donc composée du résultat de la solution Core. Enfin Contrib est une solution « bac à sable » où tout le monde peut venir tester son plugin. Elle est composée par la solution Certified et contient donc tous ses plugins, et en y ajoutant les plugins développés par les développeurs de la communauté Open Source ou par les étudiants d’In’Tech INFO.

Création d’un « Runtime »

Pour que ces solutions puissent être composées par d’autres il a fallu réfléchir à un système assez souple pour ne lier trop fortement deux solutions entre elles. Il faut par exemple que le développement de Core puisse continuer en parallèle sans impacter Certified.

Pour cela nous avons ajouté un dossier « Runtime » à chaque solution (sauf Core car c’est la solution élémentaire) contenant les résultats de la (ou des) Solutions qui la compose : Certified/Runtime contient donc les binaires de Core. Les projets de la Solution composée n’ont qu’à faire référence aux binaires du dossier Runtime pour les utiliser et les développeurs de la Solution composée gèrent leur répertoire Runtime : ce sont eux qui choisissent de prendre en compte on non les nouvelles versions des composants sur lesquels ils s’appuient.

Ce dossier Runtime peut être mis sous contrôle de SubVersion (ou autre gestionnaire de code source), c’est d’ailleurs ce que nous faisons à Invenietis. De cette façon, les Solutions sont autonomes et il est aisé de « remonter dans le temps » du projet : les sources sont en phase avec les versions exactes des composants sur lesquels la Solution est fondée.

Les « Saveurs »

Lorsqu’on compile une solution dans Visual Studio il est possible de choisir dans quel mode se déroulera la compilation. En mode Debug, pour que les « #If Debug », « Debug.* », etc. soient pris en compte, cela permet de résoudre les bugs plus facilement (via des outils de diagnostic). En mode Release, pour que les performances soient optimales.

Mais il n’y a pas que Debug et Release : si ces « modes de compilation » sont les plus courants, il peut s’avérer nécessaire de gérer d’autres « saveurs » : processeur, optimisations plus ou moins agressive, versions gratuites vs. payantes, signé ou non, etc. La notion de « Saveur » généralise le « mode de compilation » : un même composant logiciel est disponible pour le développeur en plusieurs « saveurs » et celui-ci doit pouvoir les choisir.

Le Runtime de chaque Solution contient un sous dossier par « saveur », typiquement Debug et Release, qui contiennent les résultats des compilations correspondantes. Il est donc possible de choisir quelle « saveur » utiliser simplement en changeant les chemins des références.

Notons qu’il est même possible d’intégrer la version dans la « saveur » : rien n’empêche de travailler en même temps sur des composants « ReleaseOld » et « ReleaseNew » (afin de vérifier que le plugin que l’on écrit marche aussi bien avec deux versions différentes du même composant.

Malheureusement, changer toutes les références à chaque changement de configuration est très fastidieux (une solution comme Certified possède, à date, 14 projets, chaque projet possède au moins 3 références au Core, il faut donc changer 42 chemins à chaque fois que l’on veut passer en Debug/Release).

Pour éviter cette opération, le dossier Runtime contient un répertoire Current. Ce dossier est une copie exacte d’une des saveurs disponible dans Runtime. La solution n’a plus qu’à faire référence systématiquement aux binaires de Current : pour changer de configuration, il suffit de remplacer ces binaires par les binaires de la saveur que vous voulez utiliser. Il est ainsi très facile par exemple d’utiliser le Core en Release dans la solution Certified en mode Debug (ou le contraire).

Pour le moment ce dossier Current est à gérer manuellement, il faut le créer, le mettre en « ignore list » dans SubVersion, et faire un export des binaires de Debug / Release dedans afin de pouvoir les utiliser. Nous spécifions actuellement un petit utilitaire pour automatiser et sécuriser cette opération, il devrait permettre aussi de faire un update automatique des « saveurs » disponibles.

Finissons sur une remarque générale : nous proposons ici un moyen simple de gérer un très classique problème du développement logiciel. Dans le bon vieux temps des compilateurs C, des fichiers make permettaient de se lier à des librairies différentes en fonction de valeur de macro (la « saveur » à produire). Visual Studio n’intègre pas cette possibilité (subordonner ses références au « mode de compilation ») car elle a souvent été mal utilisée et source de problème.

Pour notre part, nous estimons avoir besoin de ce petit jeu : compiler facilement ma Solution avec tous les checks intégrés dans la saveur « Debug » du Core pour faciliter la traque des bugs… alors que ma Solution, elle-même, est compilée en « Release ».

Ou le contraire : j’ai confiance dans le Core, je travaille en Debug et utilise le Core en Release. Bref, les différentes combinaisons sont possibles… Mais nous imposons néanmoins UNE contrainte forte : les « saveurs » doivent être interchangeables au sens où elles doivent toutes contenir les mêmes composants.

Exemple pratique : Faire fonctionner Contrib après un checkout

Voici toutes les étapes pour mettre en œuvre la solution Contrib directement après un checkout. Pour information le dépôt de Contrib est à l’adresse suivante : https://civikey.svn.codeplex.com/svn/trunk/Contrib Voici la marche à suivre :

  1. 1 - Aller dans Runtime/, créer le dossier Current/ et l’ajouter en « ignore list » dans votre client SubVersion.
  2. 2 - Exporter les dll d’un des deux dossiers de Runtime/ vers Current/.
  3. 3 - Lancer la solution Contrib.VS2008.sln.
  4. 4 - Compiler une première fois la solution.
  5. 5 - Dans les propriétés du projet Artefact aller dans la section « Déboguer » puis choisir « Output/Debug (ou Release selon votre mode de compilation)/CiviKey.exe » comme programme de démarrage. Attention cette configuration s’enregistre dans un fichier .user, qui ne doit pas être commit car cette configuration est liée à chaque poste.

Une fois cette configuration faite vous n’aurez plus qu’à changer le mode de compilation de Contrib ou de changer les dll de Runtime/Current/ pour compiler avec les saveurs qu’il vous faut.

Liens entre structure et layout

Les travaux sur la version 2.5.0 du Core, et plus précisément le développement de la nouvelle interface graphique de CiviKey nous ont permis de soulever une nouvelle problématique qui a entrainé l’évolution de la structure même de ce que sont un clavier et son affichage (ou layout).

Un petit rappel ne fait pas de mal

Les classes qui définissent la structure des objets d’un contexte CiviKey sont organisées en deux sections parallèles :

  • la structure de « ce qu’est un clavier », au sens des données liées au comportement et à la nature des objets (propriété Enabled et commandes associées à l’appui d’une touche par exemple)
  • et la façon dont ces claviers doivent être affichés : les Layouts (propriété Visible et positionnement à l’écran par exemple).

 

D’abord, de nouveaux noms

Lors de l’évolution décrite ci-après, on s’est rapidement rendu compte que les noms des classes n’étaient pas suffisamment explicites : ils avaient déjà évolué une première fois pour perdre le préfixe CVK (ICVKContext est devenu IContext), et l’ajout des interfaces « Current » (voir ci-dessous) a fait déborder le vase (avec un horrible ICurrentActualKeyLayoutCurrent…). Olivier a donc décidé de renommer certaines interfaces pour plus de clarté : IKeyboardZone devient IZone (parce qu’après tout, lorsqu’on travaille dans CiviKey on sait bien qu’une zone est une zone de clavier) ; IActualKey devient IKeyMode (car « actual » étant un faux-ami en anglais, certains ne comprenait pas le sens « Réel » de ce nom, IKeyMode est plus clair car cet objet représente simplement une touche - ou Key - dans un Mode donné). Du côté des layouts certains noms ont changé aussi en respectant une parfaite cohérence avec la section structurelle. L’intérêt de ce re-nommage est double :

  • les noms des objets sont « alignés » sur leur « structure » et il est beaucoup plus facile de s’y retrouver.
  • cela permet aussi de séparer les objets Layout des objets Structure dans la liste des classes dans Visual Studio : les Layouts, commençant tous par « ILayout » sont regroupés et non plus éparpillés parmi les autre classes/interfaces.

 

Voici donc la nouvelle organisation des interfaces du modèle :

Avant Maintenant
  • IContext
    • IKeyboard
      • IKeyboardZone
        • IKey
          • IActualKey
      • IKeyboardLayout
        • IKeyboardZoneLayout
          • IKeyLayout
            • IActualKeyLayout
  • IContext
    • IKeyboard
      • IZone
        • IKey
          • IKeyMode
      • ILayout
        • ILayoutZone
          • ILayoutKey
            • ILayoutKeyMode

Mais où est le problème ?

Avant tout, si cela n’est pas déjà fait, il vous faut commencer par lire l’article d’Olivier sur les modes et les fallbacks calculés lorsqu’un IActualKey n’existe pas dans le mode courant.

Désormais armés pour comprendre les modes ainsi que les fallbacks, voici le souci lié à l’ancienne conception de ces classes:

Les relations qui lient les IXXX aux ILayoutXXX sont de type 1:1, et cela est très bien … sauf pour les IKeyMode vs. IKeyModeLayout. En effet cela oblige chaque IKeyMode à « posséder » son ILayoutKeyMode, donc pour deux IKeyMode il faut deux ILayoutKeyMode distincts, ce qui impose donc redéfinir les valeurs des propriétés de ces ILayoutKeyMode, or dans la plupart des cas (99% je pense) les propriétés de layout (largeur, hauteur, position) ne changent pas entre deux IKeyMode différents (ce qui change, ce sont le comportement ou le label). Cela entraine une copie des données, et du coup une surcharge, aussi bien pour le système que pour l’utilisateur lors de la modification de l’affichage d’un clavier (pour déplacer une touche, il faudrait modifier la position de chacun de ses IKeyMode via chaque ILayoutKeyMode).

La solution trouvée à ce problème est assez simple, il suffit d’appliquer le même principe de fallback (la façon dont on retrouve le « meilleur » IKeyMode pour un mode donné) aux ILayoutKeyMode que celui applicable aux IKeyMode. L’impact est qu’il n’y a plus de lien direct entre un IKeyMode et un ILayoutKeyMode. Par contre il est toujours possible via tout IKey de retrouver le ILayoutKey courant, et sur cet ILayoutKey de trouver le « meilleur » ILayoutKeyMode calculé en fonction du mode courant (et non en fonction de l’IKeyMode courant).

Afin de faciliter la programmation, deux nouvelles interfaces ont étés introduites dans le modèle : un IKeyModeCurrent étend IKeyMode, et un ILayoutKeyModeCurrent étend ILayoutKeyMode. Ces deux interfaces ne peuvent être obtenues que via les différentes propriétés « Current » et ne font qu’ajouter une propriété « IsFallBack » qui est vrai si l’objet ne correspond pas exactement au mode courant (mais est juste le « meilleur » objet calculé pour le mode courant).

Pour plus de détails vous trouverez ici le diagramme de classes des objets du Contexte (aussi disponible dans le dossier Context/Model du projet CK.Context de la solution CK.Core).

Le fallback est plus simple à comprendre car il s’applique de façon indépendante aux touches et à leurs layouts. L’implémentation bénéficie également de cette simplification : les objets (internes au CK.Context) qui implémentent IKeyMode et ILayoutKeyMode utilisent exactement le même mécanisme de gestion des fallbacks (qui, à l’occasion, a été optimisé).

Le modes d’un clavier

Une des idées que je préfère dans le CVK, ce sont les « Modes » du clavier qui généralisent les touches « modifiantes » telles que Ctrl, Alt, Shift et autres Fn, et Alt Gr. Dans la première version du CVK (celle écrite en C++), les modes étaient gérés par un entier dans lequel chaque bit était assigné à un mode : on ne pouvait pas en définir plus de 32 – ce qui ne représentait pas un inconvénient majeur – et leur sémantique était fixée, figée, gravée dans ces bit flags – ce qui était beaucoup plus gênant.

L’idée

Le principe retenu, depuis la version 2, s’affranchit totalement de ces deux limitations et permet d’introduire quelques notions, design patterns et algorithmes intéressants.

L’idée est simple :

  • Les modes sont identifiés par des chaînes de caractères quelconques : « Ctrl », « Frigo », « Majuscule », etc. On les appelle modes atomiques (Atomic Mode).
  • Ils sont combinés simplement en les appariant séparés avec des ‘+’ : « Ctrl+Majuscule », « Frigo+Ctrl+Majuscule ». Ce sont les modes combinés (Combined Mode).
  • A chaque « touche » (Key) est associée un ensemble de « touches réelles » (Actual Keys) qui, chacune, est dédiée à un mode (combiné ou atomique) précis.

L’exemple

Un exemple très simplifié de contexte est donné ci-dessous, qui respecte la structure des objets d’un Contexte : Context / Keyboard / Zone / Key / ActualKey. Ce contexte contient un clavier avec deux touches :

  • Clavier « Démo »
    • Zone « Alphabet »
      • Touche n°1
        • Par défaut (pas de mode actif) : Label = ‘a’, Command = ‘SenKey « a »’
        • En mode « Maj » : Label = ‘A’, Command = ‘SenKey « A »’
        • En mode « Frigo » : Label = ‘Ouvrir Porte’, Command = ‘Domo.OpenFridgeDoor()’
      • Touche n°2
        • Par défaut (pas de mode actif) : Label = ‘b’, Command = ‘SenKey « b »’
        • En mode « Frigo » : Enabled = false

En résumé ce clavier va pouvoir envoyer les caractères ‘a’, ‘A’ et ‘b’ ainsi qu’ouvrir la porte du frigo. Il n’y a plus qu’à se poser quelques questions :

  • En mode « Maj + Frigo », qui gagne ? ‘A’ ou ‘Ouvrir Porte’ ?
  • Si on décide de supprimer le mode « Maj » des modes possibles, que devient l’ActualKey correspondante ? (Bon, là, on se doute qu’il faut la supprimer.)
  • « Shift + Ctrl » est bien le même mode que « Ctrl + Shift », non ?

Il existe bien d’autres questions à se poser ET à résoudre. Un gros paquet. Vraiment.

Normalisation : « Shift + Ctrl » est bien le même mode que « Ctrl + Shift » !

Il est donc nécessaire d’introduire une forme canonique (une représentation pivot, normée) qui permette de s’affranchir de cette dépendance à l’ordre des modes.

Ce n’est pas compliqué : « Shift + Ctrl » est transformé en « Ctrl + Shift » car… « Ctrl » précède « Shift » dans l’ordre lexicographique.

Grâce à cet ordonnancement, les modes combinés sont aussi facilement manipulables que les modes atomiques. D’autre part, l’ordre lexicographique nous apporte aussi une possibilité de désambiguïsation : si « Ctrl » et plus fort que « Shift » (il est devant), alors « Frigo » est plus fort que « Maj » !

Et je peux alors répondre à la première des questions précédentes : en mode « Frigo+Maj » (remarquer que Frigo est devant), c’est ‘Ouvrir Porte’ que voit l’utilisateur.

Cette normalisation ayant été comprise, on peut aborder l’implémentation. La version 2.5 n’a plus grand-chose à voir avec la version 2.0 en termes de gestion des modes. Avant de détailler la nouvelle, je voudrai revenir rapidement sur la précédente afin de montrer ses limites.

La première version

La première version implémentait les Modes comme une classe qui encapsulait la mise en forme canonique dans son constructeur. Un Mode n’était qu’un objet (immuable) autour d’une chaîne de caractère : utiliser ce type de classe est aisé, notamment en définissant des opérateurs de casting implicite de et vers les chaînes de caractères.

Le constructeur de l’objet KeyboardMode acceptait une chaîne de caractère et la normalisait avant de la conserver et de l’exposer en lecture seule : toute chaîne de caractère, à tout moment, pouvait donc être « transformé » en mode. L’aspect « lecture seule » de la chaîne une fois le mode construit garantissait ensuite l’immuabilité d’un tel mode.

L’inconvénient majeur de cette approche est qu’il n’y a pas de centralisation, de réutilisation des différents modes en mémoire : chaque objet mode est, en fait, une « valeur » (au sens des Types Valeurs) qui existe là où elle est nécessaire (dans chaque définition d’ActualKey par exemple).

Ce principe de programmation (une classe avec une sémantique de valeur) a eu plusieurs impacts négatifs :

  1. 1 - Il n’a pas été facile à mettre en œuvre par les étudiants : on ne sait jamais, finalement, s’il vaut mieux utiliser un objet Mode ou une chaîne de caractère. Cette ambigüité a polluée l’API à plusieurs endroits.
  2. 2 - Il n’était pas spécialement performant ni particulièrement extensible (pas d’opportunités pour cacher des informations lourdes ou coûteuse à produire notamment).

Ajoutons à cela que la manipulation de chaînes de caractères systématiquement requise pour travailler sérieusement avec les Modes (intersection, combinaison, soustraction, etc.) a freiné leur mise en œuvre et les développements des fonctionnalités s’y attachant.

Il était temps (et amusant :-)) de faire quelque chose. L’implémentation nouvelle est disponible dans la 2.5 et fera l’objet de très prochains billets.

Sexe aléatoire, nullité et Sql

Avec un titre comme ça, je suis sûr d’avoir 2 ou 3 lecteurs. Je lis actuellement « Sql for smarties » de Joe Celko. Tout n’est pas parfait dans ce livre, mais il y a certaines choses amusantes que je découvre avec plaisir : par exemple que le genre est normalisé par ISO 5218 « Information technology — Codes for the representation of human sexes ». Il s’agit d’un chiffre unique désigné dans le standard par « SEX » dont les quatre valeurs possibles sont :

  • 0 = inconnu (unknown),
  • 1 = homme (male),
  • 2 = femelle (female),
  • 9 = non applicable (not applicable).

Note 1 : Les rédacteurs du standard ont pris la peine de préciser que le fait que Homme était 1 et la Femme 2 n’avait aucune signification particulière... que cela était dû aux pays qui ont « poussé » ce standard. Et vous reconnaissez le premier chiffre du numéro de sécurité sociale français : c’est une preuve irréfutable du rayonnement culturel de la France dans le monde.

Note 2 : Sexe « non applicable » ne recouvre pas d’accident biologique particulier, il s’agit d’une valeur à utiliser pour des personnes morales (une entreprise n’est donc ni homme ni femme – bien au contraire aurait ajouté Pierre Dac).

Note 3 : Je viens, l’air de rien, de vous faire économiser 92 francs suisses, soit environ 65 euros. C’est en effet le prix à payer pour le PDF officiel de la norme ISO 5218. C’est scandaleux mais c’est comme ça : les standard ISO sont payants.

Revenons au SEX (avec ce billet, ce blog va perdre des points de ranking) et à SQL pour insister sur « l’inconnu » et le « non applicable ». Ces deux notions se cachent habituellement derrière le NULL, ici elles ont été explicitées et c’est une bonne chose d’un point de vue lisibilité/expressivité ainsi qu’en terme de mise en œuvre (moins on utilise de colonne NULLABLE, mieux on se porte et je ne vais pas faire ici le topo classique de la logique ternaire et des problèmes subtils introduits par le NULL car on les trouve partout).

Une autre découverte pour moi de la lecture du livre de Joe Celko est un aspect de l’instruction CASE que j’ignorais. Supposons que l’on veuille initialiser une valeur aléatoire pour un SEX. Nous avons 4 valeurs à tirer au sort (de façon équiprobable), le code suivant semble correct :

select case CAST( (4*rand()) as int )
          when 0 then '0' -- Unknown
          when 1 then '1' -- Male
          when 2 then '2' -- Female
          when 3 then '9' -- Unapplicable
       end

Et pourtant il ne l’est pas. Ci-dessous un test tout bête : il remplit 200 000 lignes d’une table avec une colonne SEX aléatoire selon le code précédent.

create table dbo.tTest( Sex char );
-- Caution: Declare and initialize in the same statement like this 
-- works only on Sql Server 2008, not 2005. 
declare @i int = 200000; 
while @i > 0
begin
	insert into dbo.tTest( Sex )
		select	case CAST( (4*rand()) as int )
					when 0 then '0' -- Unknown
					when 1 then '1' -- Male
					when 2 then '2' -- Female
					when 3 then '9' -- Unapplicable
				end
	set @i = @i-1;
end

Et on affiche le contenu de la table résultante avec le compte et la probabilité résultante.

select 	Sex, 
        COUNT(*), 
        Prob = cast(COUNT(*) as float)/200000 
    from dbo.tTest
   group by Sex
   order by 1;

Première observation : NULL apparait.

Deuxième constat : les nombres respectifs de valeurs (et donc leurs probabilités) ne sont pas équitablement répartis.

SexCountProb
NULL 63338 0,31669
0 50067 0,250335
1 37320 0,1866
2 27918 0,13959
9 21357 0,106785

Les matheux auront compris. L’instruction CASE est, en fait, réécrite ainsi :

select	case 
		when CAST( (4*rand()) as int ) = 0 then '0' -- Unknown
		when CAST( (4*rand()) as int ) = 1 then '1' -- Male
		when CAST( (4*rand()) as int ) = 2 then '2' -- Female
		when CAST( (4*rand()) as int ) = 3 then '9' -- Unapplicable
	end

Ce qui ne correspond pas du tout à l’idée initiale, et pouf pas marche.

La solution est évidemment de capturer la valeur fournie par la fonction rand() dans une variable:

declare @p int = CAST( (4*rand()) as int );
select	case @p
			when 0 then '0' -- Unknown
			when 1 then '1' -- Male
			when 2 then '2' -- Female
			when 3 then '9' -- Unapplicable
		end

N'incriminez pas SQL Server : ce sont les spécifications de SQL. C’est comme ça et pas autrement que le CASE doit être implémenté, la forme « classique » du switch-case que l’on connait dans de nombreux langages n’est pas celle réellement supportée par SQL, elle n’est qu’un sucre syntaxique autour du « râteau de si ». SQL ne connait vraiment que le râteau !

Pour finir, un exercice : montrer en calculant les probabilités formellement que le comportement observé (NULL et probabilités différentes) est parfaitement logique.

Organisation des Codes Sources du projet CiviKey

Nous avons vu dans un post précédant que le serveur SVN pour CiviKey était accessible via l’url http://svn.invenietis.com/svn/CK et que le serveur d’intégration continue était accessible à l’URL http://ci.civikey.invenietis.com/.

Dans ce post nous allons voir la gestion du repository SVN ainsi que de Cruise Control .NET pour le projet CiviKey. Ce qui est présenté ici n'est mis en place que pour la version 2.5 de CiviKey (celle sur laquelle nous concentrons actuellement nos efforts).

Le repository SVN

Le noyau de CiviKey est divisé en deux: le Core, qui contient le noyau proprement dit et Certified qui contient les plugins certifiés. L’architecture globale est la suivante :

Structure des fichiers de CiviKey

/CK Root du repository Svn de CiviKey
/CK/Core Projet CK.Core
/CK/Core/trunk Trunk des développements du projet CK.Core
/CK/Core/releases Dossier des releases de CK.Core (cf. ci-dessous)
/CK/Certified Projet CK.Certified
/CK/Certified/trunk Trunk des développements du projet CK.Certified
/CK/Certified/releases    Dossier des releases de CK.Certified
/CK/Document/trunk Ensemble des documents

Organisation des Versions

Les "releases" sont gérées à de la façon suivante. Le projet DEMO (qui est un projet similaire à Core) détaille l'organisation de l'arborescence des releases :

/trunk Trunk principale du Produit.
/releases Dossier contenant l’ensemble des versions du Produit.
/releases/VER Une des versions du Produit.
/releases/VER/trunk Trunk de développement permettant de corriger cette version.
/releases/VER/tags Dossier contenant l’ensemble des tags pour cette version du Produit.
/releases/VER/tags/TAGS Un tag contenant les sources a un certain stade.

Ce système nous permet de gérer au mieux la montée en version du projet tout en préservant la liberté du développeur. Et il y tient le développeur à sa liberté.

Une petite histoire

Les développeurs travaillent dans /trunk qui est l’espace de développement principal.
Ils font des évolutions, des changements dans l’API pour mettre en place les nouveautés, et, un jour une version est prête à voir le jour, les développeurs décident donc de créer une Version, disons la 2.5.0.

Un nouveau dossier est créé dans /releases : /releases/2.5.0/trunk. Une Version est née, elle n'est pas parfaite (elle va avoir besoin de quelques retouches pour être finalisée) et va donc évoluer et donner naissance à des Tags.

Le premier de ces tags s'appelle CTP (pour Community Technology Preview, il aurait pu s'appeler Alpha ou PreAlpha ou autre nom qui indique que ce n'est pas vraiment sec) : /releases/2.5.0/tags/CTP apparait dans l'arborescence. Noter que les codes sources dans /tags n'évoluent pas: ils représentent des clichés, des instantannés, du /releases/2.5.0/trunk qui leur a donné naissance. Le trunk bouge, les tags figent.

Cet espace permet aux développeurs de continuer à travailler dans /trunk et donc de continuer à faire évoluer le noyau, pendant que d’autres travaillent pour la Version /releases/2.5.0/trunk afin de régler les derniers bugs qui pourraient exister. Une fois les derniers bugs corrigés dans le /trunk les développeurs créent le tag /releases/2.5.0/tags/Final (et espèrent très fort éviter le petit frère /releases/2.5.0/tags/SP1).(Voir http://en.wikipedia.org/wiki/Software_release_life_cycle pour des exemples de noms de révision.)

Pendant ce temps les développeurs du /trunk se sont lancés dans la future version 2.5.1 et ont d'ores et déjà produit une release en Alpha ce qui signifie qu’un dossier /releases/2.5.1/trunk a été créé ainsi qu’un tag /releases/2.5.1/tags/Alpha.

Cette petite histoire nous amène au dépôt suivant :

Avec ce système la version d’un projet à une vraie existence : les corrections sont possibles sans (trop) d'interférences ce qui libère le développeur, qui sait clairement s’il peut imaginer de nouveaux bugs (/trunk) ou s’il doit réfréner sa créativité (/releases).