samedi 3 novembre 2012

Etats des objets Hibernate (poster)

Ce poster comprend :
  • La définition des états Hibernate : objets volatiles ("transient"), persistants et détachés.
  • L'effet des opérations supportées par la session Hibernate lors des transitions d'état.
  • Des extraits de code montrant où peuvent intervenir les états entre les différentes couches d'une application 3-tiers (couche de présentation, couche de service et couche de persistance).
Poster
États des objets Hibernate
Liens :

samedi 29 septembre 2012

Lister les ports ouverts dans un batch

Quelques commandes utiles

  • Afficher la date : date /t, ou bien : echo %date%
  • Afficher l'heure : time /t, ou bien : echo %time%
  • Afficher toutes les connexions TCP actives, avec le nom du programme impliqué dans la création de chaque connexion et ports ouverts, les adresses et les ports étant affichés au format numérique : netstat -bon
  • Lancer une nouvelle instance de l'interpréteur de commande Windows, avec fermeture en fin d'exécution : cmd /C

Le tout dans un batch :

echo off

set dateTime=%date%T%time%
set output=netstat-report.txt

(
    echo %dateTime%
    cmd /C netstat -bon
) > %output%

Les résultats peuvent ensuite être lus dans le fichier "netstat-report.txt".
Remarque : Si l'utilitaire netstat n'est pas exécuté avec cmd, le fichier de rapport ne contiendra pas la sortie de netstat.

Liens :

dimanche 22 juillet 2012

User Story ou Use Case ?

Entre User Story et Use Case, la différence ou la séparation est parfois confuse, et il me paraît intéressant de bien situer les deux en terme de similitudes, de spécificités et de complémentarité. Pour rappel, voici quelques définitions utiles se rapportant au sujet des user stories et des use cases, avant de les comparer.


Définition d'une Feature :
Une feature est un service fourni par le système, observable de l'extérieur, qui répond à un besoin, et dont la description se situe à un niveau tel que toutes les parties prenantes comprennent facilement ce dont il s'agit.
On emploie également dans le domaine de l'ingénierie des exigences les termes suivants : theme, epic, minimal marketable feature (MMF).
Exemples d'atelier pour identifier les features :
  • Product Box / Boîte du Produit
  • Remember the Future / Souvenir du Futur
Définition d'une User Story (ou histoire utilisateur) :
Une user story est une fonctionnalité élémentaire décrite du point de vue de l'utilisateur. Elle apporte de la valeur à l'utilisateur et peut être réalisée en moins d'une itération de deux semaines, c'est-à-dire quelques jours tout au plus. Les US sont obtenues par la décomposition des features. L'écriture d'une US s'appuie sur la formalisation suivante :
En tant que <rôle>, je peux <action>, afin de <justification>.

User Story : matrice Rôle - Fonctionnalité - Bénéfice
User Story : matrice Rôle - Fonctionnalité - Bénéfice
Avec 3 attributs récurrents :
  • un rôle,
  • une action : le but fonctionnel,
  • une justification (parfois facultative, car elle peut être évidente),
  • la plus-value métier, représentant le bénéfice que va tirer l'utilisateur de la user story,
  • le coût en story points, c'est-à-dire l'estimation de l'effort nécessaire à la réalisation de la user story.
Et éventuellement des attributs complémentaires :
  • une priorité déduite de la plus-value métier et du coût,
  • La fréquence d'utilisation de la US par le rôle : plus grande est la fréquence d'utilisation, plus important sera le soin apporté aux tests (tests unitaires, d'intégration, de charge, etc).
  • Des informations complémentaires : texte, schéma, diagramme.
Et toujours les tests d'acceptation (story tests) associés à la US : en fin d'itération, la US est considérée finie seulement si tous les tests d'acceptation sont validés. Les tests d'acceptation, ou conditions de satisfaction, ont eux-mêmes leur formalisme :
Étant donné... <pré-condition : état du système>
Quand... <événement : acteur + action>
Alors... <résultat observable de l'extérieur>
[Et... <autre résultat observable>]


Définition d'un Use Case (ou cas d'utilisation) :
Un use case est une description des interactions qui vont permettre à l'acteur d'atteindre son objectif en utilisant le système. Les interactions sont regroupées en scénarii. Un acteur est une entité décrite par son rôle, ses besoins et ses capacités.
Rédaction d'un cas d'utilisation :
  • Fiche récapitulative :
    • Titre,
    • Identifiant,
    • Acteurs,
    • Description.
  • Préconditions
  • Post-conditions
  • Scénario principal : séquence d'étapes ordonnées dans le temps (3 à 9 étapes).
  • Scénarii alternatifs éventuels : variations / exceptions, cas d'erreur et leur gestion.
Un cas d'utilisation ne doit pas se réduire à l'IHM, ce qui masquerait les interactions du système avec d'autres acteurs que l'utilisateur final.
Le diagramme des cas d'utilisation comprend :
  • un ou des acteurs, représentés par des pictogrammes humanoïdes,
  • des cas d'utilisation, représentés par des ellipses,
  • des relations, représentées par des traits :
    • relations simples (trait continu), 
    • inclusions (traits pointillés terminés par une flèche et stéréotype "includes"),
    • extensions (traits pointillés terminés par une flèche et stéréotype "extends"),
    • généralisations (ou spécialisations, ou héritages) (trait continu terminé par une flèche triangulaire).


Qu'il s'agisse de User Story ou de Use Case, les deux techniques servent à appréhender les besoins des utilisateurs en terme d'interaction avec un système, mais leurs manières de le faire divergent en de nombreuses occasions.


User StoryUse Case
FinalitéEn association avec les tests d'acceptation, rédiger les exigences fonctionnelles du point de vue de l'utilisateur.
Faciliter l'estimation et la planification : les US décomposées peuvent être planifiées dans des itérations différentes.
Comme éléments du backlog de produit, les US sont orientées vers la gestion et le suivi des projets.
Seulement recueillir l'expression des besoins et rédiger les spécifications détaillées des exigences fonctionnelles sous forme de processus métier.
TraçabilitéUS conservée dans le Backlog de produit.UC conservé dans le document des spécifications fonctionnelles.
GranularitéFine : une fonctionnalité élémentaire. Une US peut être une partie d'un UC : tout ou partie d'un scénario, ou éclatée dans plusieurs scénarii.
Implémentée et testée en moins d'une itération.
Grande : une macro-fonctionnalité qui en réunit de plus petites.
Implémenté et testé en une ou plusieurs itérations.
FormalismePhrase composée de 3 attributs : rôle ("qui ?"), action ("quoi ?"), justification ("pourquoi ?"), et complétée par des tests d'acceptation.Le formalisme est défini par UML.
Scénarii constitués d'une séquence d'actions ou d'événements. Répondent à "qui ?" et "quoi ?".
Les UC sont regroupés dans un diagramme des cas d'utilisation.
ExpressionDescription brève, facile à comprendre, dans le langage de tous les jours ou du métier (mais sans jargon technique).Quantité importante d'informations, dans le langage spécifique de l'utilisateur final ou de l'expert du domaine métier.
(Une demi-page pour un UC court, jusqu'à 2 pages pour un long)
EffortÉmergence rapide des exigences.
Facile à écrire avec ou par des utilisateurs ou des clients.
Les UC nécessitent un long travail d'analyse et de formalisation, afin de décrire un système entier et pensé dans son intégralité.
ComplétudeLes tests d'acceptation fournissent les conditions de satisfaction sous forme de scénarii courts pour valider une US.Le scénario principal décrit le comportement nominal. Pour le reste, les scénarii alternatifs et d'erreur viennent combler les manques.
MaintenabilitéFacile à maintenir, car le format est synthétique.Difficile à maintenir, car le format implique un contenu très conséquent. De plus, les UC mélangent les interactions et les règles métier, ce qui pose problème quand les besoins évoluent.
FocusValeur métier pour l'utilisateur final.Interactions entre l'utilisateur final et le système.
Visibilité du contexte globalDifficile de lier les US les unes aux autres. Une partie du contexte est fournie par les features et la vision du produit.Les scénarii montrent l'enchaînement temporel des actions.
Culture de travailAdaptée au travail collaboratif et à des échanges de proximité. Favorable à l'émergence de débats et à la négociation.Adapté à des échanges distants et contractuels.
TestsLes US contiennent déjà les tests d'acceptation à réaliser pendant les développements, puis lors de la démonstration pour la validation avec le Product Owner. Cf. ATDD.Les UC permettent de préparer les tests de recette, car les scénarii donnent les parcours à réaliser au cours des tests fonctionnels. Mais ils n'indiquent pas les critères d'acceptation pour statuer.
Relation avec d'autres méthodesTechnique issue de l'eXtreme Programming et associée aux méthodes agiles.Technique associée au Processus Unifié.
Auteur de référenceMike CohnAlistair Cockburn

Ainsi, on constate déjà un certain nombre d'impacts selon le choix qui sera fait entre User Story et Use Case :
  • Impact sur la vision du produit :
    • Les US expliquent ce que doit faire un système, mais aussi pourquoi. Les US sont regroupées au travers des features dont elles sont issues.
    • Les UC expliquent bien ce que doit faire un système, mais pas pourquoi il le fait. Les UC sont liés entre eux par le diagramme des cas d'utilisation, et leurs scénarii relient les fonctionnalités entre elles.
  • Impact sur les cycles des livraisons :
    • Cycle court avec les US et livraison au plus tôt de leur plus-value métier pour un retour sur investissement plus tôt et plus rapide ;
    • Livraisons plus espacées avec les UC.
  • Impact sur le processus de développement :
    • Longue phase de spécification avec les UC suivie de phases distinctes définies par le processus (RUP, cycle en V ou cascade) ;
    • Réalisation par petits périmètres fonctionnels, qui autorise un transfert continu et opportuniste de la connaissance métier vers l'équipe de développement, une collaboration entre toutes les parties prenantes à chaque itération, ainsi qu'un retour rapide du ressenti utilisateur permettant d'éventuels changements au plus tôt (et donc à moindre coût).
  • Impact sur la validation ou recette :
    • Les critères sont connus avant les développements avec les US, ce qui permet de limiter les interprétations ;
    • Les critères sont exprimés après les développements avec les UC seuls comme exigences, ce qui nécessite une deuxième interprétation des exigences à la rédaction du cahier de recette, potentiellement différente, et donc une maintenance corrective accrue.

En revanche, il existe une complémentarité entre user story et use case, entre la vision du produit et la visibilité du contexte global. Dans la mesure où il serait possible de construire le scénario principal d'un cas d'utilisation sur une seule histoire utilisateur, on pourrait tirer parti d'un diagramme des cas d'utilisation reprenant différents items du backlog de produit.

Liens :

dimanche 15 avril 2012

[Scrum] Casting du Product Owner


Dans un projet en mode Scrum, un bon PO, hors la définition du rôle et les aptitudes requises, doit tendre à satisfaire quelques critères que voici :
  1. Être un visionnaire : Le PO porte le projet, c'est-à-dire que sa contribution devrait aider le projet à devenir un succès et guider l'équipe vers les objectifs à atteindre. Pour cela, il se sent directement concerné par la réussite ou l'échec du projet. En cas de difficultés rencontrées, il devrait agir et prendre des décisions qui relèvent de son périmètre. C'est donc l'opposition entre engagement et implication qui revient : "Engagé, je me sens concerné et je réagis ; alors qu'impliqué seulement, je me contente d'observer."
  2. Maîtriser les aspects financiers du projet : Le PO connait bien les aspects financiers du projet, et devrait être capable de prioriser entre délai, périmètre et coûts. Par exemple, si le délai et le périmètre sont fortement contraints tandis que le budget est plus souple, l'équipe pourrait avoir besoin de faire intervenir ou de consulter des experts ponctuellement pour asseoir ou parvenir à une dynamique gagnante.
  3. Voir Scrum aussi comme un état d'esprit, pas simplement comme une méthode alternative, en adhérant aux valeurs et aux principes véhiculés par l'Agilité.
Liens :

jeudi 29 mars 2012

Comment se présenter en entretien

Comment se présenter en entretien et structurer son discours pour être clair et convainquant ? Telle est la question fatidique.
Depuis des années, l'entretien se déroule classiquement en trois parties, mais voyons ensemble les enjeux de chacune :
  1. Le recruteur présente la société, puis le projet.
    Le plus intéressant dans la présentation de la société relève des informations qui ne sont pas déjà disponibles sur le site web : organisation, culture, vision, méthodes.
    Au moment d'expliquer le projet, le recruteur oublie parfois d'expliciter la finalité du produit ou du projet, et rentre tout de suite dans les détails techniques. Or on ne peut pas comprendre l'architecture et la cartographie des composants et des flux d'un système sans connaître la nature de ses activités.
    Pendant ce temps, le candidat note tout ce qui lui permettra de faire un choix entre plusieurs opportunités le cas échéant, après la réunion. Il doit poser des questions pour éclaircir certains points, et ses questions auront pour effet de démontrer son intérêt pour le projet, ce qui est non seulement un élément indispensable pour rassurer un recruteur, mais aussi pour mieux préparer la suite de l'entretien et se distinguer.
  2. Le candidat présente son profil et son parcours.
    Le candidat fait une rapide introduction pour se présenter : qui est-il, son niveau de formation, son profil et ses atouts, des atouts et domaines de compétences qu'il devra mettre en relief en exposant son parcours.
    Pour un candidat qui recherche le poste auquel il aspire, au contraire du candidat qui doit gagner une mission pour la SSII dont il est salarié, le déroulement de la présentation est plus libre : il s'agit surtout de mettre en avant l'orientation qu'il souhaite donner à sa carrière, ses réussites professionnelles et ses choix de carrière passés, afin de déterminer si la société est en mesure de lui proposer le poste adéquat. Le candidat en SSII devra, lui, jongler entre l'obtention de la mission et ses intérêts propres, ainsi que le souci pour la SSII de garder ouverte la porte du client pour y placer d'autres consultants après lui, notamment s'il décline la mission.
    Pendant la présentation du projet qui a précédé, le candidat aura aussi noté dans un coin d'une feuille tous les mots-clés qui vont lui permettre d'adapter sa trame et d'orienter son discours, pour faire l'adéquation entre le poste proposé et ses compétences :
    • Réemployer oralement ces mots-clés afin d'y faire écho et déclencher un signal positif dans l'esprit du recruteur.
    • Passer rapidement sur les expériences qui ne sont pas significatives ou qui s'éloignent des préoccupations relatives au projet.
    • Insister sur les expériences révélatrices ou qui s'avèrent pertinentes dans le cadre du projet. Pour celles-ci, la méthode de l’entonnoir consiste à partir des généralités pour rentrer dans les détails :
      • présentation concise de la société,
      • le nom du département et sa taille, éventuellement lié à un produit,
      • l'équipe et sa taille, et le projet à sa charge,
      • Réalisations du candidat.
      • Ne pas oublier de citer les environnements techniques : méthodes de travail, technologies.
    • Tisser un fil directeur entre les expériences citées en ménageant des transitions : le candidat montre qu'il maîtrise son discours, est capable de prendre du recul, et garde l'attention du recruteur. Rien n'est plus fastidieux qu'une énumération, et ces transitions vont insuffler une dynamique au discours. Voici des éléments typiques d'une bonne transition :
      • Compétences développées ?
      • Compétences acquises ?
      • Ce que m'a apporté cette expérience ?
      • Ce que j'en ai retenu ?
      • "Le projet étant arrivé à terme", "Puis j'ai souhaité découvrir autre chose", etc, et on continue.
    • Lors de la conclusion finale, c'est le moment ou jamais de parler des points qui n'ont pas été abordés et qui peuvent valoriser la candidature, et lui donner un point d'orgue :
      • Projets personnels,
      • Blog professionnel,
      • Veille technologique,
      • Auto-formation,
      • Participation à des conférences publiques,
      • Ce qui vous intéresse dans le projet, le cas échéant,
      • Insister sur votre plus-value : ce que vous allez apporter à l'entreprise.
  3. Test technique, éventuellement, à réaliser dans un délai plus ou moins contraint.
  4. Si vous êtes présenté chez un client d'une SSII par un commercial, la phase de debriefing aura lieu sans vous : bilan sur la candidature, quelques questions-réponses pour lever des doutes persistants, TJM (tarif journalier moyen) de la prestation et négociation, date d'un retour pour communiquer la décision définitive, date de démarrage potentielle.
Remarques :
Aujourd'hui, des sociétés ont entrepris de demander aux candidats de se présenter en premier, afin d'éviter qu'ils n'utilisent la corrélation par mots-clés. C'est plus difficile, car le candidat doit se sentir prêt à prendre la parole dès le début de l'entretien et s'expose à un degré élevé d'incertitude. Or il a dû se déplacer pour arriver jusqu'aux locaux de la société, peut-être sort-il du métro (bruits tonitruants, odeurs nauséabondes, promiscuité excessive, chaleur étouffante, fatigue, sentiment d'oppression) et n'est-il pas vraiment dans les meilleures dispositions...
Mon point de vue est que le candidat ayant fait l'effort de venir jusqu'au recruteur, celui-ci devrait normalement témoigner d'une certaine courtoisie en ce sens.

Des SSII ont même institutionnalisé ce procédé, certaines sont sans doute conscientes d'avoir une image peu reluisante. Peut-être ne préfèrent-elles pas s'étendre sur la présentation de la société, qu'elles parviennent à éluder pour certaines d'entre elles. Quand je fais ce constat, pour ma part c'est rédhibitoire. Peut-être préfèrent-elles juger de la réaction du candidat, et voir quelles inflexions il va donner à sa présentation : affirme-t-il son projet professionnel, ou bien va-t-il le diluer dans des choix qu'il a dû subir, auquel cas on peut supposer qu'il est malléable, qu'il aura peu de revendications, voire qu'il sera facile de l'orienter, de l'influencer. Faute d'un objectif à atteindre, un candidat peut même en arriver à commettre des faux pas en se présentant. Faute d'un objectif à viser, il peut être tentant de se montrer le plus synthétique possible, mais en manquant d'évoquer certains traits qui vont révéler toute la qualité de son profil, un candidat risque indirectement de se dévaloriser et de jouer le jeu du recruteur au moment de la négociation des prétentions salariales.


Enfin, je dirais qu'il y a des jours avec et des jours sans, en dépit de votre préparation, sans compter qu'il faut savoir se garder une part d'improvisation, afin d'y gagner en spontanéité et vivacité : se sentir en danger est stimulant et pousse à se dépasser. Aussi je vous souhaite bonne chance et bon courage. Car, comme disait Louis Pasteur, "la chance ne sourit qu'aux esprits bien préparés".

Comment compter les lignes de code

En une seule ligne de commande dans un terminal Shell :
  • Pour compter le nombre de lignes de code en Java :
    find . -iname "*.java" -exec grep -E '^[.]*' {} \; | wc -l 
    find . -iname "*.java" -exec grep -vE '^[ ]*$' {} \; | wc -l sans ligne vide
    find . -iname "*.java" -exec grep -vE '^[ ]*($|//|/\*|\*|\*/)' {} \; | wc -l sans ligne vide ni commentaire
  • Pour compter le nombre de classes Java :
    find . -iname "*.java" | wc -l
Si le langage n'est pas Java, il suffit de changer l'extension.


Dans Eclipse IDE, c'est aussi possible, en sélectionnant une ressource dans la vue "Project Explorer", puis "Search" (Ctrl+H) :


La vue "Search" indique le nombre de lignes de code.

dimanche 25 mars 2012

[JUnit] Personnalisez vos tests avec @Rule

Les règles JUnit proposent un mécanisme d'extension du framework. Cette fonctionnalité permet de personnaliser le déroulement des tests :
  • vérifications supplémentaires des résultats,
  • préparation avant et nettoyage après,
  • rapports complémentaires.
Pour ce faire, il suffit d'ajouter un membre publique du type org.junit.rules.TestRule à la classe de test, et de l'annoter avec @org.junit.Rule :
public class MyTestCase {
    @Rule public TestRule myRule;
    ...
}


Règles proposées par JUnit

Pour le développeur, il est donc possible soit d'écrire ses propres règles à partir de l'interface TestRule, soit plus simplement de recourir aux règles proposées nativement par JUnit. Voici ce que propose le package org.junit.rules :
  • ExpectedException : cette règle permet de spécifier, à l'intérieur d'un test, le type et le message de l'exception attendue. Elle permet aussi de généraliser l'utilisation de @Test(expected=...) à une classe de test.
  • ExternalResource : classe de base pour toute règle qui doit préparer une ressource externe avant un test (fichier, socket, serveur, connexion à une base de données, etc), et garantir sa libération par la suite.
    • TemporaryFolder (dérivée de ExternalResource): cette règle permet d'inclure au test la création de fichiers et de répertoires temporaires, qui devront être supprimés à la fin de la méthode de test, que le test passe ou échoue.
  • RuleChain : cette règle permet de construire et d'ordonner une liste de règles.
  • TestWatcher : classe de base pour toute règle qui doit observer l'exécution d'un test sans en modifier les résultats.
    • TestName (dérivée de TestWatcher) : cette règle donne accès au nom du test à l'intérieur de la méthode de test.
  • Timeout : cette règle permet de spécifier le même délai d'exécution pour toutes les méthodes de test de la classe de test. Elle permet aussi de généraliser l'utilisation de @Test(timeout=...) à une classe de test.
  • Verifier : classe de base pour toute règle qui doit vérifier l'état d'un objet de test. Ce type de règle est susceptible de modifier le résultat du test.
    • ErrorCollector : cette règle permet de recueillir les échecs quand ils surviennent, sans interrompre le test. Le cas échéant, les échecs feront l'objet d'un rapport tout à la fin du test. Ça peut être utile lorsque les assertions d'un test sont indépendantes.

Règles personnalisées

Les classes de base ExternalResource, TestWatcher, et Verifier ont été ajoutées afin de faciliter et guider le développement de règles personnalisées :
  • ExternalResource propose d'implémenter des méthodes before() pour préparer des ressources et after() pour les libérer.
  • TestWatcher propose d'implémenter des méthodes :
    • starting() et finished() pour journaliser des informations avant et après le déroulement du test, quelle que soit son issue,
    • succeeded() et failed() pour journaliser des informations si le test réussit ou échoue respectivement.
  • Verifier propose d'implémenter une méthode verify() pour ajouter des vérifications au test.
Et pour qui veut écrire une nouvelle règle relative à un nouveau cas d'usage, l'interface TestRule comporte une unique méthode : Statement apply(Statement base, Description description);
base représente ici la méthode de test, et plus généralement encapsule une étape dans une suite de test : base.evaluate() pour l'exécuter,
description décrit le contenu du test et doit éventuellement servir à construire un message de log,
et où la valeur de retour du type Statement encapsule le traitement spécifique de la règle.

Remarque :
  • Avec les règles, il est possible de faire ce qu'il était déjà faisable dans les méthodes annotées par @Before[Class] et @After[Class], mais de manière plus efficace, et plus facile à partager entre projets : les TestRunners de JUnit reconnaissent intrinsèquement les TestRules, ce qui en fait un mécanisme d'extension naturel et privilégié de JUnit.
  • L'interface org.junit.rules.TestRule remplace l'interface org.junit.rules.MethodRule apparue avec JUnit 4.7 et rendue obsolète depuis la version 4.9 : la méthode apply a été simplifiée.

Code Source

Des exemples d'utilisation des règles JUnit sont proposés sur mon dépôt GitHub UnitTesting4Java, dans le module junit4-features (package xapn.testing.junit.rule).

vendredi 23 mars 2012

[Git] Visualiser l'historique

Il est facile de visualiser, grâce à git-log, l'historique des modifications en parcourant le graphe de Git, et de bien des manières, pour la branche courante, ou la branche indiquée le cas échéant.
  • git log équivalent à git log HEAD : affiche, pour chaque noeud, son SHA1, l'auteur, la date et le message.
  • git log -- <path> : affiche la même chose que précédemment pour chaque noeud ayant un impact sur le contenu de l'un des fichiers du répertoire indiqué.
  • git log --stat : affiche, en plus de git-log, la liste des fichiers modifiés pour chaque noeud, et en face de chacun d'entre eux, un aperçu quantitatif et qualitatif des modifications enregistrées.
  • git log -p : affiche, en plus de git-log, toutes les modifications de contenu pour chaque noeud.
  • git log --oneline : affiche, sur une ligne pour chaque noeud, le SHA1 abrégé et le message.
  • git log <since>..<until> : affiche tous les noeuds entre le noeud since exclus et le noeud until inclus. <since>..<until> peut être traduit comme le graphe incluant le noeud until et ses ancêtres, privé du graphe incluant le noeud since et ses ancêtres.
  • git log --graph : affiche tous les noeuds sous la forme d'un graphe. Pour un graphe (--graph) facile à lire (--oneline) incluant toutes ses références (--all --decorate) :
    git log --graph --oneline --all --decorate
  • git log --reverse : inverse l'ordre d'affichage de l'historique, du noeud parent vers le noeud enfant.
Par ailleurs, le plugin de Git pour Eclipse, EGit, propose de visualiser le graphe complet d'un dépôt grâce à la vue "History".

mercredi 21 mars 2012

Générer un matcher Hamcrest unifié

Hamcrest propose une alternative avantageuse aux assertions de JUnit, en fournissant une bibliothèque de matchers ou prédicats pour l'écriture de règles de correspondance de façon déclarative : en cas d'échec du test, le message est beaucoup plus explicite. Une autre qualité, qui a conditionné la viabilité du projet Hamcrest, est son extensibilité : il est facile de développer ses propres prédicats et Hamcrest les prendra en charge. Pour un usage plus aisé, il est même possible de générer un matcher unique à partir d'une multitude de matchers spécialisés, ce qui évitera les imports statiques à répétition.

Rompu à l'utilisation de JUnit, je ne me suis pas laissé facilement convaincre par le recours aux assertions fondées sur Hamcrest plutôt qu'aux traditionnelles assertions natives de JUnit, même si HamcrestCore est désormais embarqué dans JUnit. Peut-être à cause de l'obligation de choisir un matcher adéquat en fonction du type de donnée à vérifier, et d'écrire l'import statique correspondant. En ce sens, j'aurais tendance à préférer dorénavant FEST-Assert, qui se présente comme un DSL (Domain-Specific Language), plus intuitif.

Mais pour en revenir au sujet de l'article, j'ai récemment mis en oeuvre le générateur de code d'Hamcrest, et comme j'ai constaté quelques imperfections, j'ai dû trouver différents remèdes.

1) Prérequis : définition d'un prédicat personnalisé

Un prédicat personnalisé est déclaré sous l'une de ces formes :
  • public class MyCustomMatcher extends org.hamcrest.BaseMatcher {
        ...
    }
  • public class MyCustomMatcher extends org.hamcrest.TypeSafeMatcher {
        ...
    }
  • public class MyCustomMatcher {
        public static org.hamcrest.Matcher factoryMethodName(final ObjectToBeMatchedClass expected) {
            return new org.hamcrest.BaseMatcher() {
                private ObjectToBeMatchedClass theExpected = expected;
                ...
            }
    }
2) Génération d'un prédicat unique

Le générateur s'appuie sur un fichier de configuration XML listant les différents prédicats personnalisés à regrouper. A l'exécution, le générateur identifie par introspection la fabrique (factory method) grâce à l'annotation @org.hamcrest.Factory, et génère une classe unique de façade pour l'utilisation de tous ces prédicats. La signature de la méthode annotée par @Factory doit impérativement être de la forme :
public static Matcher factoryMethodName(...)

NB : Sans cela, si la signature devait avoir un autre type de retour, comme celui du matcher personnalisé (par exemple : "public static MyCustomConcreteMatcher factoryMethodName(...)"), alors le type en argument ObjectToBeMatchedClass ne serait pas reporté dans le code généré et n'apparaîtrait plus dans le prédicat de façade. De cela découleraient des warnings à la compilation, notamment dans les classes qui utiliseraient le prédicat de façade.

Résultat en console :

Generating xapn.testing.hamcrest.generated.MyMatchers
                       [T] is(Matcher p0)
                       [T] is(T param1)
               [Object] is(Class p0)
[OrdinarySystem] ready()
[OrdinarySystem] powerful()
[OrdinarySystem] lessOrdinary(OrdinarySystem thanAnother)
[OrdinarySystem] moreOrdinary(OrdinarySystem thanAnother)

Remarque : Le type en argument apparaît dans les logs entre crochets. Si la signature de la fabrique n'est pas correctement formatée, cela apparaîtra dans les logs de cette manière :
                         [] factoryMethodName()
Pour remédier à cet avertissement minimaliste dans le cadre d'une chaîne de génération avec Maven, il pourrait être de bon ton de développer un Mojo qui renverrait une exception du type org.apache.maven.plugin.MojoExecutionException, voire une MojoFailureException (échec du build dans le cas d'une release par exemple).

3) Invocation du générateur dans une chaîne de génération industrialisée

Le générateur peut être très simplement appelé en ligne de commande :
java -cp hamcrest-all-1.1.jar org.hamcrest.generator.config.XmlConfigurator $config-file $source-dir $generated-class $output-dir

Voici les arguments attendus, tels qu'ils sont explicités en ligne de commande  :
Args: config-file source-dir generated-class output-dir
config-file : Path to config file listing matchers to generate sugar for.
                  e.g. path/to/matchers.xml
source-dir  : Path to Java source containing matchers to generate sugar for.
                  May contain multiple paths, separated by commas.
                  e.g. src/java,src/more-java
generated-class : Full name of class to generate.
                  e.g. org.myproject.MyMatchers
output-dir : Where to output generated code (package subdirs will be
                  automatically created).
                  e.g. build/generated-code

Il faudra toutefois avoir déjà compilé les différents matchers listés dans $config-file, puisque le générateur est en fait alimenté par leurs binaires.

Hamcrest est aujourd'hui encore construit par Ant, aussi ce ne sera pas un problème d'ajouter une tâche Ant dans le processus de build d'un projet, afin de générer le code source du prédicat de façade et de le compiler ensuite. Cela donnerait une tâche Ant comme suit pour invoquer le générateur Hamcrest, suivi d'une tâche Ant pour compiler le code source généré :
<java classname="org.hamcrest.generator.config.XmlConfigurator"
    fork="true"
    failonerror="true"
    maxmemory="128m"
    >
  <arg value="${config-file} ${source-dir} ${generated-class} ${output-dir}"/>
  <classpath>
    <pathelement location="lib/hamcrest-all-1.1.jar"/>
    <pathelement path="${java.class.path}"/>
  </classpath>
</java>

Avec Maven, c'est encore plus simple, grâce au plugin org.codehaus.mojo:exec-maven-plugin. Si l’artéfact org.hamcrest:hamcrest-all fait partie des dépendances, toujours en ligne de commande cela donne :
mvn exec:java -Dexec.mainClass="org.hamcrest.generator.config.XmlConfigurator" -Dexec.args="$config-file $source-dir $generated-class $output-dir"

En revanche, dans le cadre d'un projet multimodule Maven, la démarche est un peu plus subtile. Typiquement, vous avez écrit vos matchers personnalisés dans un module dédié, et vous souhaitez pouvoir les utiliser dans vos tests dans des modules qui constituent votre projet. Pour obtenir une bibliothèque de matchers personnalisés, vous créez un module de type jar, et tirez une dépendance vers hamcrest-all, de scope compile. Dans les autres modules de votre projet, vous pourrez dorénavant dépendre de votre bibliothèque pour vos test, avec le scope test, de même qu'avec JUnit, TestNG ou Hamcrest. Rien de compliqué jusqu'à présent, seulement du classique.
Pour générer votre bibliothèque de matchers, vous allez en revanche devoir reproduire la démarche présentée avec Ant :
  1. compilation des matchers personnalisés,
  2. invocation du générateur Hamcrest,
  3. compilation du matcher de façade généré.
C'est là que ça devient problématique, étant donné que le cycle de vie Maven spécifique du type de package jar suit les phases suivantes dans cet ordre, sans qu'il soit possible de les répéter dans le même processus de build :
  • process-resources
  • compile : compilation des matchers personnalisés, à laquelle on ajoute l'invocation du générateur.
  • process-test-resources
  • test-compile
  • test
  • package
  • install
  • deploy
En l'état, le matcher de façade ne sera pas embarqué dans le JAR de l'artéfact final, faute d'avoir été compilé. Il faut donc choisir une solution parmi ces alternatives :
  • soit on développe un nouveau cycle de vie personnalisé : ça revient un peu à tuer une mouche avec un canon ;
  • soit on génère le matcher de façade dans un autre module : simple, expéditif, mais pas très élégant, car j'aurais voulu que tous les matchers soient empaquetés dans le même JAR pour faciliter la distribution de l'artéfact ;
  • soit on s'arrange pour tout faire rentrer dans la phase compile du processus de build de l'artéfact.
Sur internet, j'ai pu lire des posts où il était question de développer un Mojo, mais le problème lié au cycle de vie reste le même. Pour ma part, j'ai réussi à mettre en oeuvre la troisième option, en créant un profil Maven "hamcrest". Voici la définition de ce profil :
  • Profil désactivé par défaut.
  • Activation du profil si le code source à générer est absent.
  • Propriétés : arguments à fournir au générateur de code Hamcrest.
  • Plugin maven-clean-plugin / goal clean, attaché à la phase clean, pour supprimer le code source généré (dans le cycle de vie Clean de Maven).
  • Plugin maven-antrun-plugin / goal run, attaché à la phase compile, pour afficher un message echo en console contenant la valeur des arguments à destination du générateur de code Hamcrest.
  • Plugin exec-maven-plugin / goal java, attaché à la phase compile, pour invoquer le générateur de code Hamcrest.
  • Plugin maven-compiler-plugin / goal compile, attaché à la phase compile, pour forcer la compilation du code source généré.
Dès lors :
  • mvn clean install, pour laisser Maven activer automatiquement le profil "hamcrest".
  • mvn clean install -Phamcrest, pour forcer l'activation du profil "hamcrest".
Pour ne pas surcharger le contenu de cet article, je n'ai pas inclus le code source des matchers, ni les POM du projet de démonstration. Vous pouvez retrouver ce contenu dans son intégralité sur mon dépôt GitHub UnitTesting4Java : la bibliothèque des matchers personnalisée est "hamcrest-sugar-generation", et le matcher unifié est utilisé dans un test du module "junit4-features", dans le package "xapn.testing.junit.hamcrest.sugarmatcher".

4) Quelques conseils et astuces

Matcher Is :
Pour des raisons de commodité, pensez à toujours ajouter le matcher org.hamcrest.core.Is dans le fichier de configuration XML du générateur de code d'Hamcrest. Comme son utilisation est fréquente, cela vous épargnera d'ajouter un import statique dans tous vos tests pour un seul prédicat.
<matchers>

    <!-- Hamcrest library -->
    <factory class="org.hamcrest.core.Is"/>

</matchers>

Pourquoi un profil ?
Un profil peut comporter des clauses d'activation, ce qui, dans ce cas, évitera de lancer inutilement une génération à chaque processus de build. Par contre, il faudra penser à activer le profil suite à une modification des matchers personnalisés.

Quelle version d'Hamcrest ?
La version d'Hamcrest embarquée dans JUnit n'est pas la dernière, il y a même longtemps qu'elle n'a pas été mise à jour. Pour bénéficier de toute la variété des matchers développés par Hamcrest, il peut être utile d'ajouter la bibliothèque HamcrestAll dans ses dépendances.

Faciliter l'écriture des imports statiques dans une classe :
L'IDE Eclipse permet d'organiser automatiquement les imports statiques par autocomplétion, ou encore grâce au raccourci Ctrl+Shift+M. Pour que cela fonctionne également avec les matchers d'Hamcrest, il convient de modifier les préférences d'Eclipse : dans Java / Editor / Content Assist / Favorites, vous pouvez ajouter soit de nouveaux types ("New Type"), soit de nouveaux membres ("New Member").
Exemples de types intéressants à ajouter aux imports statiques automatiques :
  • org.hamcrest.CoreMatchers.* (hamcrest-core ou junit) : prédicats de base fournis par JUnit et issus d'HamcrestCore.
  • org.hamcrest.Matchers.* (hamcrest-all) : ces prédicats fournis par HamcrestAll réunissent les prédicats de base de HamcrestCore, également embarqués par JUnit, ainsi que des prédicats dédiés aux collections, aux textes, aux nombres et d'autres encore.
  • org.junit.matchers.JUnitMatchers.* (junit) : le prédicat de façade de JUnit agrégeant les prédicats internes de JUnit, ainsi que les prédicats d'Hamcrest embarqués par JUnit.
Hamcrest : org.hamcrest.CoreMatchers vs org.hamcrest.Matchers ?
Qui peut le plus peut le moins, et dans ce cas, Matchers recouvre CoreMatchers.

Contribution :
Hamcrest est aujourd'hui maintenu sous Google Code, et toute contribution devrait passer par la soumission d'un patch, à attacher à une issue existante... La question de faire passer le projet sous GitHub a été soulevée.

Liens :

mercredi 14 mars 2012

Techniques pour amener les équipes à l’excellence

Introduction

J'ai assisté mardi soir à la conférence du même nom animée à Paris par Michel Goldenberg. Le sujet principal était la maîtrise du degré de motivation d'une équipe de développement au cours du cycle de vie d'un produit logiciel. Si Scrum propose un cadre pour gérer le suivi d'un projet, il ne répond pas à la question des moyens dont peuvent disposer un Scrum Master et une équipe pour agir sur la motivation et les performances.

A l'opposé d'une attitude managériale du type "Command & Control", le manifeste agile privilégie l'auto-organisation des équipes et la responsabilisation de leurs membres : l'objectif commun est de fournir un logiciel qui fonctionne et utile pour les utilisateurs en respectant les délais. Cela part d'un bon sentiment, l'objectif est lui-même louable, malheureusement peut-on dire que le concept tiendra en haleine la motivation des développeurs, après des mois, voire des années à suivre le cérémonial de Scrum ? En pratique, que constate-on ? Et si la démotivation doit à terme gagner les esprits comme c'est le cas dans toutes les équipes de par le monde, comment peut-on y remédier ?

Je vais donc essayer de répondre à ces questions en quelques articles, en reprenant les informations échangées lors de cette conférence.

dimanche 19 février 2012

[Maven] Comprendre les fonctionnalités d'un plugin

Il n'existe pas de liste exhaustives des plugins Maven disponibles. En revanche, à partir du nom d'un plugin, il est possible d'obtenir sa documentation, et ce grâce au plugin dédié help.

Le plugin help propose entre autres mojos describe, qui fournit une description de n'importe quel plugin, ainsi que la liste des mojos d'un plugin et l'explication de la tâche qu'ils accomplissent.
mvn help:describle -Dplugin=<nom du plugin>

Appliquons par exemple le mojo help:describe sur lui-même :
mvn help:describe -Dplugin=help

Pour davantage d'informations, notamment sur les paramètres acceptés par chaque mojo :
mvn help:describe -Dplugin=help -Ddetail
ou encore : mvn help:describe -Dplugin=help -Dfull

Un mojo très intéressant du plugin help est help:effective-pom (Cf. la documentation fournie par les commandes précédentes).

[Maven] Plugin & Mojo

Maven est un moteur dont le coeur définit le cycle de vie pour la construction d'un projet logiciel : traitement des ressources, compilation du code source, traitement des binaires, traitement des ressources de test, compilation des tests, passage des tests, packaging.
Toutes les étapes de génération ou de traitement prises en charge par Maven sont en fait réalisées par les plugins de Maven. Le cycle de vie par défaut proposé par Maven consiste en un ensemble ordonné de phases, auxquelles sont attachés un ou plusieurs plugins.

Un plugin Maven est lui-même constitué de tâches programmées (ou goals), et ce sont en fait ces tâches qui sont attachées aux phases du cycle de vie. S'agissant de Maven, on parle plus communément de Mojo : Maven Old Java Object.

Ainsi, lorsqu'on demande à Maven de construire un projet, Maven déroule son cycle de vie, et à chaque phase rencontrée, Maven lance certains Mojos du ou des plugins associés à cette phase.

En définitive, les fonctionnalités de Maven peuvent être étendues à l'infini par le développement et l'utilisation de nouveaux plugins, susceptibles d'enrichir le cycle de vie par défaut, voire de définir des cycles de vie personnalisés.

Liens :

jeudi 2 février 2012

[Git] Mettre du contenu de côté

... ou comment sauvegarder son travail en cours dans la pile temporaire.

Dans certaines situations où un travail n'est pas terminé ni à jeter et où l'on souhaite le mettre de côté en attendant d'y revenir, le temps de travailler sur autre chose, Git propose d'utiliser une pile dédiée (FiFo) grâce à git stash. Cette commande permet de sauvegarder sans commit, et de revenir au dernier commit de la branche courante (de façon équivalente à git reset --hard HEAD). Au moment de la récupération, Git offre plusieurs possibilités :
  • Voir les éléments de la pile : git stash list
  • Voir les modifications sauvegardées dans un élément de la pile dans la branche courante : git stash show stash@{number}, pour voir les modifications fichier par fichier, et avec l'option -p pour voir les modifications ligne par ligne dans chaque fichier. number=0 désigne le dernier élément, number=1 l'avant-dernier, number=2 l'avant-avant-dernier, etc.
  • Sauvegarder l'index dans la pile, en y joignant un message personnalisé (utile pour se souvenir du contenu sauvegardé) : git stash save "message"
  • Récupérer le dernier élément de la pile dans la branche courante : git stash apply
  • Récupérer un élément précis de la pile dans la branche courante : git stash apply stash@{number}.
  • Créer une branche dans laquelle récupérer le dernier élément de la pile : git stash branch new_branch_name
  • Supprimer un élément de la pile : git stash drop stash@{number}

lundi 30 janvier 2012

[Git] Rejouer l'historique pour le modifier

... ou comment ajouter du contenu sur un commit qui n'est pas le dernier de la branche.

Les opérations suivantes ne suffiront pas pour propager le nouveau contenu jusqu'à la référence de la branche : se déplacer sur un commit antérieur (git checkout), ajouter du contenu (git add) et modifier le commit (git commit --amend). Cela aura pour seul effet de créer une nouvelle branche sans référence, car Git ne modifie pas des objets existants.
Il faut donc rejouer tous les commits depuis le commit à modifier jusqu'au dernier commit de la branche, afin de reconstruire celle-ci :
git rebase -i <commit> pour un rebase interactif depuis le commit indiqué.
Git guide ensuite l'utilisateur.

Dans l'éditeur ouvert par Git, remplacer, pour le commit à modifier, la commande "pick" par la commande "edit" sur la ligne correspondante, puis quitter l'éditeur. Le déroulement du rebase est suspendu en arrivant sur le commit à modifier : c'est le moment d'ajouter le contenu souhaité.
git add <fichier(s)> pour modifier l'index.
git commit --amend pour modifier le dépôt local.
git rebase --continue afin de poursuivre le rebase.
En lisant l'historique des commits de la branche pour le fichier modifié (git log --stat -- <fichier>) ou en ouvrant ce fichier, on voit que les modifications du commit intermédiaire ont été conservées, ce qui ne serait pas le cas sans un rebase.

jeudi 26 janvier 2012

[Git] Bonne pratique : Où (ne pas) effectuer un commit ?

Comme un checkout permet de se déplacer sur n'importe quel noeud du graphe, il est aussi possible d'effectuer un commit au milieu d'une branche et donc d'ajouter un noeud dont le parent est au milieu de cette branche. Or ce n'est pas une bonne idée, car ce noeud ne sera pointé par aucune référence connue, et il faudra se souvenir de la clé sha1 pour être capable de se repositionner sur ce noeud. La bonne pratique qui découle de cela est :

Toujours se trouver sur une référence de type branche avant de faire un commit.

[Git] Préparer un commit de manière intéractive

L'existence de l'index (espace transitoire entre le working directory et le dépôt local) permet d'ajouter du contenu pour le prochain commit en plusieurs fois, grâce à la commande git add. Mais l'utilisation de git add ne s'arrête pas là :
  • L'ajout interactif, avec git add -i (ou --interactive) : Git demande alors, fichier par fichier, la commande à appliquer.
  • L'option patch ou ajout par hunk, avec git add -p (ou --patch) : Un hunk correspondant à un plus ou à un moins lors d'un diff, Git demande donc, pour chaque modification / hunk, si oui ou non il faut l'ajouter dans l'index.
Cela permet de préparer un commit en tout sérénité, notamment lorsque l'on veut que des modifications réalisées sur un même fichier ne soient pas enregistrées dans le même commit. Chaque commit - unitaire - pourra ainsi correspondre vraiment à une seule tâche, pour un historique clair et compréhensible, même après des mois.

samedi 14 janvier 2012

Comprendre Git

Dans le présent billet, je vais présenter les quelques concepts vraiment importants pour comprendre Git, le très populaire SCM (Source Code Management) distribué.
Une chose dont il faut tout de suite prendre conscience lorsqu'on s'attaque à Git est qu'il ne faut surtout pas chercher à tisser un lien entre Git et d'anciens SCM comme Subversion. A vouloir utiliser Git comme on utilisait SVN, non seulement on passerait à côté de toute la puissance de l'outil, mais plus grave encore on s'exposerait à de douloureux retours de fortune.

Pour commencer, les concepts propres à Git sont simples à comprendre. Et c'est la combinaison de ces concepts entre eux qui confèrent à Git sa puissance d'utilisation, ainsi que l'efficacité de son implémentation. Voici ce que nous présenterons de Git, et qui se résume comme suit.
Git est un :
  • gestionnaire de contenu,
  • stocké sous la forme d'un graphe acyclique d'objets,
  • accessible par des références.
Git est structuré en 2 partie :
  • le frontend, qui regroupe les commandes dites porcelain (add, commit, checkout, etc).
  • le backend, développé par Linus Torvalds, qui réunit les commandes dites plumbing (hash-object, cat-file).
On retrouve cette distinction dans la littérature informatique et dans les formations : souvent, les formations pour débutants présentent le frontend, et le backend ne sera traité que par les formations avancées. Les dissocier de cette manière est une erreur, car en maîtrisant le backend, on maîtrisera bien mieux le frondend.


Concept #1 : Git est un gestionnaire de contenu.

Ce concept provient du backend, mais il influence fortement le frontend.
Le backend de Git est un backend de type "Snapshot" (ou Contenu), au contraire de SVN dont le backend est de type "Delta" (ou Fichier). Dans un backend "Delta", on ajoute d'abord un fichier, puis on enregistre pour ce fichier seulement la différence entre 2 versions. Au contraire, dans un backend "Snapshot", on ajoute un contenu, puis à chaque modification de ce contenu, on enregistrera le nouveau contenu dans sa globalité : chaque contenu est enregistré dans un objet rouge qui correspond à un blog, et il existe un objet bleu pour indiquer l'emplacement du fichier du même contenu.

Pour des scénarii usuels dépassant le cas d'une simple modification, comment cela fonctionne-t-il concrètement ?
  • Lors d'un retour-arrière ou revert sur un fichier ? Le backend "Delta" (SVN, CVS, etc) trace les changements successifs : les lignes A ont été ajoutées d'abord, les lignes B ont été ajoutées ensuite, enfin on supprime les lignes B et on ajoute les lignes A.  Le backend "Snapshot" (Git) a créé deux contenus, il met alors à jour l'objet bleu pour indiquer que ce fichier contient désormais le contenu précédent.
  • Lors d'une duplication de fichier vers un autre ? Le backend "Delta" trace deux fichiers ayant chacun leur contenu, même si ces contenus sont identiques. Le backend "Snapshot" voit un seul contenu (un objet rouge) et deux fichiers (deux objets bleus).
  • Lors des renommages successifs d'un fichier ? Le backend "Delta" historise différents fichiers qui seront modifiés au fil du temps. Le backend "Snapshot" enregistre les différentes modifications en autant de contenus (objets rouges), et il dispose d'objets bleus pour indiquer le nom du fichier pour chaque contenu.
Dans un projet où l'on manipule un grand nombre de fichiers, on entrevoit les difficultés que va rapidement rencontrer un backend "Delta", tandis qu'un backend "Snapshot" ne s'éloignera jamais de son comportement normal.

Pour gérer du contenu, Git s'appuie sur une base de données clé-valeur, et sur 2 commandes :
  • hash-object, qui reçoit un fichier, crée un objet rouge (blob), et renvoie une clé de hashage.
  • cat-file, qui reçoit une clé, et renvoie le contenu associé.
Git utilise l'algorithme SHA1 comme fonction de hashage cryptographique pour générer des clés, SHA1 étant plus robuste que MD5. Cela permet à Git d'obtenir une clé unique en fonction d'un contenu donné. En effet, un ordinateur qui produirait un péta-objet par seconde pendant une péta-année produirait en tout 3,1536 x 10^37 objets. Comme SHA1 est en mesure de produire 2^160 ou 10^48 clés différentes, il est simplement impossible d'obtenir la même clé pour deux contenus différents, dans l'univers, dans l'histoire de l'Humanité...

En bref, Git dispose de 3 types d'objet dans sa base de données :
  • les objets blog en rouge, qui servent à enregistrer du contenu et à renvoyer un SHA1 ;
  • les objets tree en bleu, qui font la relation entre un contenu et un fichier ;
  • les objets de commit en vert, pour dire qui produit le commit, à quelle date et pourquoi.

Comme les objets tree peuvent atteindre de grandes tailles en comparaison des objets de commit, ils sont séparés dans l'implémentation de Git, afin d'être plus efficace.

En résumé du concept #1, Git est un gestionnaire orienté contenu, et non fichier, qui utilise une base de données clé (sha1) / valeur, comportant 3 types d'objet : commit, tree, blob.


Concept #2 : La relation entre commits est un graphe acyclique (ou DAG).

Si le concept #1 était purement issu du backend, le concept #2 est à cheval entre le frontend et le backend.

Un graphe acyclique est un graphe sans cycle (sans boucle). Dans ce graphe, un noeud peut avoir 0, 1, 2 ou N parents. Pour information, Git est seul à supporter N parents pour un même noeud. Par ailleurs - point très important dans l'implémentation de Git -, l'enfant pointe vers le parent, Git pointe donc vers le passé. Avec SVN, c'est le contraire, SVN pointe vers le futur, ce qui n'a pas d'intérêt pour un SCM et qui est à l'origine de gros problèmes.

Avec Git, ajouter du contenu consiste à se placer sur un noeud X, vouloir le contenu d'un noeud Y et appliquer une opération entre X et Y de son choix : commit, merge, rebase, squash, cherrypick, revert. Chacune de ces opérations permet de transformer le graphe d'une manière différente : on choisit donc l'opération à appliquer en fonction du graphe que l'on veut obtenir après transformation.

Commit : continuer le graphe
Un commit ajoute un nouveau noeud à la suite du dernier noeud de la branche courante.

Merge : modéliser une fusion
Un merge fait la fusion entre deux branches du graphe en un point pour n'en obtenir qu'une.
NB : Avec SVN, on manipule un arbre dégénéré (avec une seule branche), et un merge ne le modifie pas, il ne fait qu'ajouter du contenu à une branche à partir d'une autre.

Rebase : simplifier un graphe
En partant de 2 branches distinctes, un rebase place une branche en tant qu'enfant d'une autre pour n'en obtenir qu'une.

Squash : nettoyer le graphe
Un squash agrège deux ou plusieurs noeuds pour ne garder que le dernier enfant de ces noeuds.

Cherrypick : faire une copie d'un contenu sans son historique
Dans une branche d'expérimentation par exemple, un seul noeud nous intéresse, pas les autres. On veut donc récupérer cet unique noeud en l'insérant dans la branche adéquate.

Revert : faire un retour-arrière sur un noeud
Un revert crée un nouveau noeud qui annule le contenu du noeud visé.

Finalement, la vision chronologique n'a pas de sens, la seule vision correcte est typologique, c'est-à-dire l'agencement des noeuds au sein du graphe, et non la date de création des noeuds. Effectivement, les opérations de transformation du graphe sont susceptibles de bouleverser à volonté l'ordre chronologique.

En résumé du concept #2, les commits sont reliés dans un graphe, manipulable à loisir, où les objets sont immutables : Git ne modifie jamais les objets qu'il a créés, il ne fait qu'en ajouter de nouveaux.


Concept #3 : Le contenu est accessible sous forme de références.

Git manipule deux types de référence :
  • les références déplacées automatiquement par Git,
  • les références déplacées par l'utilisateur.
Avec Git, la notion de référence est indispensable afin de connaître le dernier noeud pour chaque branche du graphe. Comme les noeuds de Git pointent de l'enfant vers le parent, sans référence, il faudrait se souvenir des SHA1. SVN pourrait se passer de référence, puisque ses noeuds pointent du parent vers l'enfant. Git considère d'ailleurs qu'un head inaccessible par référence est à effacer. Au lancement d'une commande Git, le garbage collector de Git passe pour effacer les références inaccessibles, et ce tous les trois mois par défaut.

Références automatiquement déplacées par Git :
Une branche est une référence (déplacée par Git) sur un noeud du graphe. Concrètement, Git crée une branche en créant un fichier de 40 octets qui contiendra sa référence (cheap branching).
Par défaut, à la création d'un dépôt (init), la branche principale est la branche master (équivalent du trunk de SVN), et le dépôt distant partagé s'appelle origin (clone).
Il existe une référence head qui désigne en permanence le noeud actif du graphe.


Références déplacées par l'utilisateur :
Git dispose d'un 4ème type d'objet dans sa base de données : les objets tag en orange, qui permettent de référencer un commit par un nom spécifique. Un tag est une référence définie par l'utilisateur, notamment pour les releases. Git supporte les références avec des namespaces (exemple : toto/titi/tutu). En termes d'implémentation, les namespaces sont en fait des répertoires, et les références des fichiers.

En résumé du concept #3, une référence est un pointeur (post-it), utilisable avec des espaces de nom, utile pour l'utilisateur (frontend).


Conclusion

La conception de Git peut être vue comme un modèle en couche, avec :
  • Un contenu, implémenté par des objets blob,
  • Un système de fichier constitué d'objets tree par-dessus le contenu,
  • Un historique qui encapsule l'ensemble grâce à des objets commit,
  • Facilement mémorisable et accessible par le biais de références.
Si vous avez compris les concepts que nous venons de voir, alors vous avez compris comment fonctionne Git. C'est plutôt une bonne nouvelle, non ?

Liens