Gestion de version pour le travail en équipe

Les outils de gestions de version se sont imposés comme l’outil central d’un l’écosystème des technologies utilisées pour la réalisation de projets de développement en informatique.

Ces outils sont en particulier utiles pour le travail en équipe.

Beaucoup de plateformes Web dédiées au développement de projets en équipe ont la gestion de version au centre de leurs applications. (Github, BitBucket, GitLab, SourceForge).

Différents types d’outils de gestions de version existent. C’est depuis l’apparition des gestionnaires de version distribués que l’écosystème de développement en équipe s’est vraiment développé (Github, BitBucket, GitLab).

Ces plateformes permettent la mise en place de mécanismes d’intégration continue pour l’exécution automatique des testes liés au code, et la diffusion de rapport permettant à toute l’équipe de voire l’avancement du projet.

Introduction aux systèmes de gestion de version

une bonne introduction a GIT se trouve dans le livre Pro Git book par Scott Chacon (CC BY-NC-SA 3.0). Ce cours s’inspire grandement des chapitres 1 à 3 de ce livre.

Quelques définitions

Diff
Diff désigne à la fois le nom de la commande et celui du résultat produit. Le diff permet de visualiser la différence textuelle, ligne par ligne, entre 2 fichiers. Quand on compare deux versions d’un même fichier, le diff donne la liste des modifications pour passer de la première version à la seconde.

Exemple. On a un fichier hello_wold.c :

void main()
{
  printf("Hello World!\n");
}

et le fichier hello_wold2.c:

#include <stdio.h>

int main()
{
  printf("Hello World!\n");
  return 0;
}

Le diff entre hello_wold.c et hello_wold2.c donne :

diff --git a/hello_world.c b/hello_world2.c
index 14049e2..73c3748 100644
--- a/hello_world.c
+++ b/hello_world2.c
@@ -1,6 +1,8 @@
-void main()
+#include <stdio.h>
+
+int main()
 {
     printf("Hello World!\n");
-
+      return 0;
 }

Dépôt
Le dépôt (repository) représente plus ou moins la totalité de la basse de données que représente un système de gestion de version. Le dépôt contient toutes les versions (tout l’historique) d’un projet.
Copie de travail
La copie de travail (working copy, directory) représente le système de fichiers actuelle du projet (l’ensembles des fichiers dans le dossier et les sous-dossier du projet).
Instantané
L’instantané (Snapshot) est l’état ponctuel des fichiers d’un système de fichiers (projet, dossier, sélection de fichiers, etc.). Il représente son contenu, à un moment donnée.
Commit
Un commit (validation), représente un ensemble de changements réalisés sur le dépôt (repository) et qui constitue une nouvelle version atomique du dépôt.
Branche
Un ensemble de validations (commits) ayant divergé de la branche d’origine.
Système de gestion de version
Un système de gestion de version est donc constitué d’au moins un dépôt (repository) et d’une copie de travail (directory). L’ajout de nouvelles versions dans le système se fait grâce à la création de validations (commits) qui sont constitué de diffs entre les instantanés d’un commit précédent et de la copie de travail.

Les systèmes de gestion de version

Historique des systèmes de gestions de version

il y a 3 grandes familles systèmes de gestion de version :

les Systèmes locaux (Version Control Systems)

Copie manuelle de fichiers locaux.

Local Version Control Systems

Systèmes de gestion de version centralisés

Un serveur central accessible sur le web contient le dépôt. Les utilisateurs ne possèdent que des copies de travail.

Systèmes de gestion de version centralisés

Systèmes de gestion de version distribués

Chaque utilisateur possède toute le dépôt (les banches, les commits, etc.) en local.

Il n’y a pas de serveur central. N’importe qui (avec une connexion internet entrante) peut devenir un serveur.

Systèmes de gestion de version distribués

GIT : 3 états / 3 zones / 3 actions

Dans un dépôt GIT un fichier peut avoir 3 états différents :

Ces états correspondent à 3 zones dans un GIT :

Le passage entre ses 3 états se fait par 3 actions:

configuration de GIT

une commande pour configurer GIT : git config.

git config --list

Trois niveaux de configuration :

L’authentification est le paramètre de configuration le plus important qu’il ne faut pas négliger.

$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com

Manipulations de bases avec GIT

Elles sont décrites ici en ligne de commande mais sont également disponible dans les IDE dignes de ce nom.

Créer un nouveau dépôt

Souvent on dispose déjà d’un projet existant avec du code déjà écrit avant de décider d’en faire le suivit de version.

git init initializes a new repository.

cd myProject
git init
git add "*.java"
git commit -m "Initial commit for myProject"

Récupérer un dépôt existant

La commande git clone fait plusieurs choses :

git clone https://github.com/pigne/CountDownWebApp.git

plusieurs protocoles de transfère :

Vérifier l’état des fichiers

la commande la plus utile : git status.

$ git status
# On branch master
nothing to commit (working directory clean)

Commencer le suivit d’un fichier

On utilise la commande git add.

$ git status
On branch master
Initial commit
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	AUTHORS
nothing added to commit but untracked files present (use "git add" to track)
$ git add AUTHORS
$ git status
On branch master
Initial commit
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
  new file:   AUTHORS

Ignorer des fichiers

Certains fichiers n’ont pas leur place dans le dépôt. Pour tant ils existent dans la copie de travail.

Le fichier .gitignore contient des expressions rationnels qui vont filtrer les fichiers a ignorer :

target/
.projets
.settings/
.classpath
*.class
*~

Valider les modifications

La commande git commit va faire un instantané (snapshot) des modification sélectionnées (staged) pour créer une validation (un commit) et l’insérer das le dépôt.

La validation a toujours besoin d’un message.

Supprimer un fichier

suppression du dépôt et du système de fichier

git rm readme.txt

Suppression du dépôt mais le fichier reste dans la copie de travail.

git rm --cached readme.txt

Il faudra une validation (commit) pour que la suppression soit prise en compte.

Déplacer des fichiers

git mv file_from file_to

Historique des validations

git log
git help log

Better use graphical tools to see git history:

Se déplacer dans les différents états du fichier

Les états d'un fichier

Annuler des validations

la commande git reset permet de modifier les validations du dépôt.

Annuler le dernier commit mais garder les modifications en local.

git reset HEAD~1

Commandes GIT de validation

Remotes

Pour pouvoir collaborer on a besoin d’un site distant avec lequel échanger les modifications.

On va échanger les commits entre le dépôt local et le/les dépôts distants.

La commande git remote permet de manipuler de dépôts distants (remotes)

$ git clone https://github.com/pigne/CountDownWebApp.git
$ cd CountDownWebApp
$ git remote -v
origin	https://github.com/pigne/CountDownWebApp.git (fetch)
origin	https://github.com/pigne/CountDownWebApp.git (push)

Ajouter: git remote add [shortname] [url]

Récupérer les modifications d’un remote

git fetch [remote-name] va télécharger les nouveaux commits du dépôt distant dans le dépôt local.

git fetch ne modifie que le dépôt local, pas la copie de travail.

Pour réellement mixer les validation des 2 dépôts on utilise git merge

la commande git pull va utiliser fetch et merge

Envoyer les modifications vers un remote

git push [remote-name] [branch-name]

Souvent le remote par défaut est “origin” branche par défaut est “master

git push origin master

Les Commandes d'envoie et de reception

Les branches

Le dépôt peut se représenter comme un réseau de validations (commits)

Les validations sont identifiées par :

Commits Linked with their parents

Les branches sont des pointeurs

Une branche c’est un pointeur sur une validation.

La branche par défaut s’appelle souvent master.

La commande git branchpermet la manipulation des branches.

HEAD est un pointeur particulier qui indique la branche actuellement utilisée dans la copie de travail.

Branches and HEAD Point to commits

Travailler dans les branches

git checkout <branch-name> déplace le pointeur HEAD vers la branche indéquée et récupère in instantané (snapshot) du dernier commit de la branche dans la copie de travail.

$ git checkout cli_branch  # moves HEAD
  # modify some_file
$ git add some_file
$ git commit # moves cli_branch & HEAD

Commits in branches

Branches divergentes

Faire des validations dans plusieurs branches les fait diverger.

$ git checkout master  # moves HEAD
  # modify some_file
$ git add some_file
$ git commit # moves master & HEAD

Diverging History

Fusionner Branches

Quand les modifications et ajouts sont terminés dans la branche thématique, on va souhaiter réintégrer la branche principale.

git checkout master       ## be sure we are in destination branch
git merge cli_branch
git branch -d cli_branch  ## remove cli_branch as it is useless

Diverging History

Resolving conflicts

En cas de conflit, la fusion est mise en pause. La validation est annulée. Les fichiers concernés par les conflits sont marqués comme non fusionnés (unmerged).

$ git status
index.html: needs merge
# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   unmerged:   index.html
#
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
  please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

Branches distantes

Les branches distantes du dépôt local représentent les branches du dépôt distant. Elles ne sont pas modifiables.

git push origin cli_branch
git fetch origin
git checkout -b bogus123 origin/fix_bug_123

les plateformes de gestion de projet

Il en existe plusieurs. Les plus connues :

GitHub
La plateforme la plus connue. Héberge gratuitement les projets publics. Souscriptions pour projets privés. Possibilité d’obtenir des projets privés gratuitement (programme éducation). Le code est hébergé chez GitHub.
BitBucket
Similaire à GitHub. Programme étudiant (avec l’email universitaire). Le code est hébergé par Atlasian.
GitLab
Similaire aux précédents dans sa version commerciale (Entreprise Edition). Une version open source (Community Edition) permet l’installation privée d’un serveur.

La forge de l’université

L’université du Havre utilise la version Community Edition de GitLab. Cette instance est accessible à l’adresse : https://www-apps.univ-lehavre.fr/forge

Ce serveur est accessible depuis l’université comme à l’extérieur.

Une fois votre compte créé, il restera accessible même si vous n’êtes plus étudiant à l’université.

Cette instance GitLab hébergée à l’université est nommée la forge.

Création d’un compte sur la forge

Il faut avoir un compte à l’université (personnel de l’université ou étudiants inscrits) pour pouvoir créer un compte sur la forge.

Etape 1 : première connexion

La première connection se fait obligatoirement via le Service Central d’Authentification de l’université (le CAS), en cliquant sur le bouton “CAS Université Le Havre Normandie” depuis la page d’accueil.

Page d'accueil de la Forge

Etape 2 : Email valide et vérifié

Une fois connecté il faut donner une adresse mail valide (pas forcement celle de l’université). Un mail de confirmation sera envoyé à l’adresse donnée. Aller sur le lien envoyé par mail pour valider la création du compte.

Etape 3 : Définir un mot de passe

Une fois l’adresse email validée, aller dans l’onglet password afin de définir un mot de passe (différent de celui de l’université).

Etape 4 : Configuration de la machine de travail

Dans un terminal on configure le client GIT local avec les commandes de configuration suivantes (remplacer les mots LOGIN, PRENOM, NOM et EMAIL par ce qui convient) :

git config --global credential.https://www-apps.univ-lehavre.fr.username LOGIN
git config --global user.name "PRENOM NOM"
git config --global user.email "EMAIL"

Pour ne pas retaper à chaque fois le mot de passe. Sous Linux taper :

git config --global credential.helper 'cache --timeout 3600'

Etape 5 : connexion normale

GitLab est maintenant configuré. Plus besoin d’utiliser le “CAS de L’université” pour se connecter à l’avenir. On peut directement entrer le login (ou l’adresse email) et le mot de passe définit précédemment, sur la page de connexion.

Le projets

Le projet est la notion principale dans la forge.

Un projet n’est pas qu’un simple dépôt GIT, c’est aussi :

Tout le monde peut créer des projets.

Il y a trois niveaux de visibilité d’un projet :

Les droits d’accès en écriture eux sont régis par les droits données aux membres du projets. C’est le propriétaire du projet qui définit les droits qu’ont les membres.

Les groupes

Tous les utilisateurs ont la possibilité de créer des groupes. Les groupes permettent de gérer plus facilement les droits sur les projets.

Fork de projets

Un projet peut être créé à partir d’un autre projet. C’est une sorte de copie du projet de départ. Lors d’un fork Le nouveau projet hérite de l’historique du dépôt du projet de départ. Les membres, les issues et autres éléments du projet sont a définir.

Pour forker un projet on clique le l’icône “fork” du projet que l’on veut copier

Page de projet sur la Forge

Fusions et demandes de fusions Merge Requests

La fusion peut être faite entre deux branches du même projets. les demandes de fusion (merge requests ou pull requests) ont deux cas d’utilisations possibles :

Les tickets (issues)

Ils permettent premièrement de gérer et corriger les bugs dans une projet. Mais on peut les utiliser pour faire du développement en méthodes agiles. En particulier :

Intégration Continue

Son but est d’automatiser l’intégration et la vérification du code source d’un projet.

En méthodes Agiles On utilise aussi l’intégration continue (CI) avec du deployment continue.

Concrètement faire de l’intégration continue signifie que les tests d’un projet son exécutés à chaque fois qu’une du code est validé dans le gestionnaire de version et le résultat de cette exécution est visible par les membres du projet.

Tests Unitaires

Pour s’assurer qu’un projet fait ce que l’on veut on doit vérifier qu’il se comporte bien dans tous les cas de figures. C’est une tache difficile, mais en découpant les différentes fonctionnalités d’un projet complexe et en testant chacune d’elles, on réduit les chances d’avoir des bugs.

Les tests les plus simples sont les tests unitaires. chaque fonctionnalité est testée séparément du reste du projet.

Par exemple si je travaille à la réalisation d’une calculatrice, je vais vouloir tester séparément toutes les fonctionnalités. Pour tester la fonctionnalité “Racine Carré d’un nombre réelle”, en imaginant que la fonction Math.sqrt n’existe pas, je vais vérifier que :

Un test unitaire est un ensemble de tests de cas particuliers que j’ai définis et dont je connais le résultat attendu. Le teste me permet de vérifier que la fonction a bien le bon comportement avec ces paramètres la.

Le test doit être le plus pertinent possible. Certes je n’ai pas testé que racineCarréRéelle(25) == 5 mais j’espère que le teste avec un seul nombre positif suffit.

Tests Unitaires en java

On utilise la bibliothèque JUnit.

class CalculatorTests {

  @Test
  @DisplayName("1 + 1 = 2")
  void addsTwoNumbers() {
    Calculator calculator = new Calculator();
    assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2");
  }

  @ParameterizedTest(name = "{0} + {1} = {2}")
  @CsvSource({
    "0,    1,   1",
    "1,    2,   3",
    "49,  51, 100",
    "1,  100, 101"
  })
  void add(int first, int second, int expectedResult) {
    Calculator calculator = new Calculator();
    assertEquals(expectedResult, calculator.add(first, second),
      () -> first + " + " + second + " should equal " + expectedResult);
  }
}

Gestion de production Maven

Maven est un outil qui facilite la compilation et la gestion des dépendances pour les projets en java.

Un projet géré avec Maven possède un fichier de configuration pom.xml à la racine du projet.

On peut ensuite faire toutes les taches de gestion de dépendance, compilation, test et distribution en ligne de commande ou avec un IDE gérant maven (InteliJ IDEA, Eclipse)

Intégration Continue dans GitLab

On peut configurer la forge pour l’Intégration Continue. On configure le projet avec un fichier de configuration (.gitlab-ci.yml) à la racine du projet.

Pour se familiariser avec l’intégration continue dans la forge, on peut se référer au Projet Standard qui contient un petit projet avec des tests unitaires, configuré pour que les tests s’exécutent et que le taux de couverture apparaisse sur les différentes validations.

Demo / Live Coding

Alice et Bob :

User Story

"Donner le temps restant jusqu'à (ou écoulé depuis) une date donnée."

Alice 1

log

  1. Sur la forge :
    • Création d’un groupe (AB-production)
    • Ajout de Bob dans le groupe
    • Création d’un projet (Dates)
    • Ajout d’une issue “User Story” dans le projet Dates
  2. En ligne de commande :
    mkdir Dates && cd Dates
    git init
    git pull https://www-apps.univ-lehavre.fr/forge/pigne/projet-standard.git
    
  3. Avec l’éditeur de texte
    • Modification du fichier README.md
    • Titre du projet
    • urls des badges
    • noms des auteurs
    • Description du projet (User Story) - Modifier le groupId et le artifactId dans le fichier pom.xml
  4. En ligne de commande :
    git add README.md
    git commit -m "Mise a jour du fichier README pour le projet Dates avec Alice et Bob"
    # connection à la forge
    git remote add origin https://alice@www-apps.univ-lehavre.fr/forge/AB-production/Dates.git
    # envoie des modifications
    git push -u origin master
    
  5. Sur la forge:
    • Création d’un ticket “User Story”

Bob 1

log

un seul fichier src/main/java/fr/univlehavre/date/Dates.java:

    static String difference(String theDate) {
        String pattern = "dd/MM/yyyy HH:mm:ss";
        Date d2 = null;
        try {
            d2 = new SimpleDateFormat(pattern).parse(theDate);
        } catch (ParseException e) {
            return "Erreur dans le format de date.";
        }
        Date d1 = new Date();
        long diff = d2.getTime() - d1.getTime();
        long diffSeconds = diff / 1000 % 60;
        long diffMinutes = diff / (60 * 1000) % 60;
        long diffHours = diff / (60 * 60 * 1000) % 24;
        long diffDays = diff / (24 * 60 * 60 * 1000);
        return String.format("%d jours, %d heures, %d minutes, %d secondes", diffDays, diffHours, diffMinutes, diffSeconds);
    }
}

Alice 2 (A2)

Bob 2

Que fait la méthode difference ?

  1. conversion du paramètre (chaîne de caractère) en date
  2. Création d’une date de référence
  3. Le véritable calcul de différence de date
  4. construction d’une de la chaîne résultat

Il faut séparer ces 4 actions pour pouvoir les tester.

public class Dates {
    static String difference(String theDate) {
        // 1. conversion string -> date
        Date d2 = null;
        try {
            d2 = stringToDate(theDate);
        } catch (ParseException e) {
            return "Erreur dans le format de date.";
        }
        
        // 2. date de référence
        Date d1 = new Date();

        // 3. Le véritable calcul de diff de date
        DateDiff dd = differenceDates(d2, d1);

        // 4. construction d'une string résultat
        return diffToString(dd);
    }

    static DateDiff differenceDates(Date d2, Date d1) {
        long diff = d2.getTime() - d1.getTime();
        DateDiff dd = new DateDiff();
        dd.seconds = diff / 1000 % 60;
        dd.minutes = diff / (60 * 1000) % 60;
        dd.hours = diff / (60 * 60 * 1000) % 24;
        dd.days = diff / (24 * 60 * 60 * 1000);
        return dd;
    }

    static Date stringToDate(String theDate) throws ParseException {
        String pattern = "dd/MM/yyyy HH:mm:ss";
        Date d2 = new SimpleDateFormat(pattern).parse(theDate);
        return d2;
    }

    static String diffToString(DateDiff dd) {
        return String.format("%d jours, %d heures, %d minutes, %d secondes", dd.days, dd.hours, dd.minutes, dd.seconds);
    }
}
public class DateDiff {
    public long days;
    public long seconds;
    public long minutes;
    public long hours;
    public DateDiff(long d, long h, long m, long s) {
        days = d;
        hours = h;
        minutes = m;
        seconds = s;
    }
    public DateDiff() {
    }
}
class DatesTest {
    @Test
    void differenceDates() {
        Date d1 = new Date(0);
        Date d2 = new Date((3662)*1000);
        DateDiff dd = Dates.differenceDates(d2, d1);
        assertEquals(0, dd.days);
        assertEquals(1, dd.hours);
        assertEquals(1, dd.minutes);
        assertEquals(2, dd.seconds);
    }
    @Test
    @DisplayName("bad format should raise exception")
    void stringToDateError() {
        assertThrows(ParseException.class, () -> Dates.stringToDate("nope"),"bad format");
    }
    @Test
    @DisplayName("proper format should pass")
    void stringToDate() {
        try {
            Dates.stringToDate("12/12/1212 12:12:12");
        } catch (ParseException e) {
            fail("should not generate exception");
        }
        assertTrue(true,"No error on proper format");
    }
    @ParameterizedTest(name = "d: {0} h: {1} m{2} s{3} : {4}")
    @CsvSource({
            "1, 2, 3, 4, '1 jours, 2 heures, 3 minutes, 4 secondes'",
            "-4, -3, -2, -1,'-4 jours, -3 heures, -2 minutes, -1 secondes'"
    })
    void diffToString(long d, long h, long m, long s, String expected) {
        DateDiff dd = new DateDiff(d, h, m, s);
        assertEquals(expected, Dates.diffToString(dd));
    }
    @Test
    void differenceError(){
        assertEquals("Erreur dans le format de date.", Dates.difference("nope"));
    }
    @Test
    void differenceOk(){
        String d = Dates.difference("12/12/1212 12:12:12");
        assertNotEquals("Erreur dans le format de date.",d);
        assertEquals(String.class, d.getClass());
    }
}

Alice

Constate la couverture de tests. Peut merger en ligne.