Project

General

Profile

L'ancienne page de recueil de commandes Git est ici http://dev-eole.ac-dijon.fr/projects/eole/wiki/RecueilGit

Les bonnes pratiques c’est quoi ?

Ce document n’est pas une référence git, nous n’allons pas réécrire le
Pro Git book, ni les autres références disponibles sur Internet.

Il existe même des tutoriels pas à pas bien fait.

Nous allons partir du principe qu’un minimum de connaissance de l’outil git est acquis, notamment
le démarrage rapide et les bases de git du livre Pro Git book.

Nous allons essayer de décrire ici une méthodologie de travail basée sur le
système de gestion de version décentralisé git avec les objectifs suivants :

  • Communiquer entre développeur
  • S’y retrouver dans ses développements, ou comment gérer des ajouts fonctionnels et des corrections de bug en parallèle
  • Publier des modifications clairement identifiables et analysables, par des humains et des outils de tests automatiques

Ce document ne représente en rien LA bonne méthode de travail, il s’agit juste d’une façon de faire,
appuyée par l’expérience d’autres personnes que l’on trouve un peu partout sur Internet.

Il est tout à fait possible de trouver autant, voir plus, de références prônant un point de vue opposé ou orthogonal au nôtre.

De la méthode agile

Comme il est défini dans le manifeste agile, la première valeur est

Les individus et leurs interactions.

Afin de travailler correctement en équipe, il faut communiquer, et l’outil de gestion de version git, utilisé à EOLE, peut-être un excellent outil de communication.

De l’écriture des ChangeLog

Afin de communiquer avec les autres membres d’une équipe sur le travail que chacun effectue, l’écriture d’un ChangeLog, ou plus précisément d’un « commit message » dans la littérature anglaise (qui pourrait être traduit par « message de consignation ») se doit d’être fait correctement(sic).

Que penser d’un message comme « typo » ou encore « correction d'une coquille » ?

Un message de commit doit expliquer le pourquoi de la modification afin de comprendre ce qui a motivé le changement.

Le changement en lui même est expliqué par le diff.

Nous pouvons établir une règle simple et souple, par exemple :

Un message de commit comprend :

  • Une première ligne résumant le pourquoi du patch ;
  • Une description longue optionnelle permettant d’expliciter le
    contexte du résumé donné en première ligne ;
  • Une liste de fichier et leurs modifications.

Une ligne vide sépare les différentes parties : la première est obligatoire et la troisième est optionnelle pour les changements triviaux

Par exemple :

Le répertoire /usr/share/eole/noyau/ est nécessaire

* debian/dirs: Création du répertoire.

Ref: #2405
Simplification de la gestion des noyaux

* creole/fonctionseole.py: Suppression du code effectué par
  eole-kernel-version depuis sa version 2.3-eole37~2.
  Renommage de la variable 'boot_ok' en 'need_boot'.

Ref: #2406

Cela impose les règles suivantes :

  1. « Ne jamais utiliser l’option "-m" de la commande "git commit" »
  2. « Ne pas décrire ce que fait le commit mais pourquoi »

La description de se que fait un commit est le rôle du diff.

Ainsi, le second exemple donné plus haut n’est pas correct : « la première ligne ne décrit pas pourquoi on simplifie la gestion des noyaux ».

De la cohérence de l’histoire du monde

Un système de gestion de version permet avant tout de conserver un livre d’histoire d’un projet.

Dans les premiers systèmes de gestion de version, cette histoire était centralisée. Il fallait avoir le droit d’écrire dans le « livre des comptes et légendes » afin d’y ajouter un morceau

Impossible donc de réécrire l’histoire sans être tout puissant sur le registre.

Grâce aux systèmes décentralisés comme git, tout le monde peut avoir une copie privée de l’histoire publique d’un projet, pour cela il suffit de « cloner » l’histoire originale :

moi@work:~/src$ git clone git://far-far.away.example.net/bidule.git

À partir de là il est possible de vouloir y ajouter ses propres morceaux, et cela sans rien demander à personne.

On modifie le projet et on consigne les nouveaux événements dans notre copie locale de l’histoire du projet :

moi@work:~/src/bidule(master)$ $EDITOR src/bidule.c # On modifie un fichier du projet
moi@work:~/src/bidule(master)$ git add src/bidule.c # On ajoute un événement à enregistrer dans le livre d’histoire
moi@work:~/src/bidule(master)$ git commit # On consigne ce nouvel événement

Mais l’histoire, c’est un sujet sensible. On ne badine pas avec l’histoire et on ne la réécrit sous aucun prétexte.

Peu importe si la vérité est ailleurs, l’important c’est que tout le monde soit d’accord sur la même version des faits.

Si un plaisantin modifie l’histoire et la publie à tout vent, il y aura conflit.

Nous allons donc édicter la règle suivante :

« On ne réécrit pas l’histoire connue »

Cela sous entend donc que nous pouvons réécrire comme il nous plaît toute l’histoire qui nous est privée, c’est à dire inconnue de tous les autres.

Mais dès qu’un événement est rendu public, ce n'est plus possible.

Trop de pull, c’est trop chaud !

Que se passe-t-il si un jour nous souhaitons apprendre les nouvelles
histoires de par le monde ?

Que ce passe-t-il si un jour nous souhaitons partager nos petites
histoires avec le reste du monde ?

La plupart du temps, la réponse donnée à ces questions commence par
quelque chose comme « fais-toi donc un pull ».

Cela se traduit, en terme git, par :

moi@work:~/src/bidule(master)$ git pull

la commande est strictement équivalent aux deux commandes suivantes :

moi@work:~/src/bidule(master)$ git fetch origin
moi@work:~/src/bidule(master)$ git merge origin/master

Mais il existe quelques soucis avec cette méthode, la première étant
« qu’une histoire entre en conflit avec l’une des vôtres ».

Un autre problème est que cette méthode « mélange » littéralement
l’histoire publique avec vos histoires privées, même s’il n’y a pas de
conflit, il peut-être difficile de savoir si le résultat aura un sens
ou non.

Dans git, chaque commit a une généalogie, le parent du commit que je
m’apprête à faire est le dernier commit enregistré dans le dépôt.

Lors d’un pull sur un dépôt contenant des modifications, git créé un
commit ayant deux parents dont le seul but est de mélanger la généalogie.

Il est donc préférable d’utiliser la méthode décomposée en deux commandes.

Cela permet de vérifier au préalable si des changements
sont à intégrer, et lesquels, avec les commandes suivantes :

  • ChangeLog des différences
    moi@work:~/src/bidule(master)$ git log master..origin/master
    
  • Liste des fichiers modifiés
    moi@work:~/src/bidule(master)$ git diff --stat master..origin/master
    
  • Modification du code
    moi@work:~/src/bidule(master)$ git diff master..origin/master
    

Mais cela n’empêche nullement :

  • D’être potentiellement désastreux : chaque « fusion » (merge) et
    « consignation » de « fusion » (merge commit) sont à la charge de
    chaque développeur ;
  • Votre histoire devient illisible. Elle intègre tout un tas de
    « fusion » inexplicables ;
  • Retrouver à quel moment précis et quel modification a entraîné un
    changement de comportement devient impossible du fait des
    « fusions »

Tout mélanger dans la même branche, qui devient alors un tronc, pose
des soucis quant à la lecture de l’histoire du projet.

Pour s’y retrouver, il faut s’accrocher aux branches

Une branche est une ligne d’histoire divergente d’un projet, leur
utilisation dans git est très simple et légère.

La création d’une branche peut se faire de deux façons :

  1. Créer une branche que j’utiliserais plus tard :
    moi@work:~/src/projet(master)$ git branch mabranche
    moi@work:~/src/projet(master)$ # on commit dans la branche « master »
    
  2. Créer une branche et commencer à travailler dedans :
    moi@work:~/src/projet(master)$ git checkout -b mabranche
    moi@work:~/src/projet(mabranche)$ # on commit dans la branche « mabranche »
    

Chacune de ces commandes permet de définir, en dernier argument, le point de départ de la nouvelle branche.

Par défaut ce point de départ est le dernier commit de la branche courante.

Tout les commit effectués sur cette branche sont localisés à cette branche.

Cela permet de se focaliser sur une activité :

  • Sans risquer d’impacter la vision que les autres ont de l’histoire ;
  • Sans se soucier de ce qui peut arriver sur les autres branches ;
  • Sans fusionner les modifications de la branche principale dans la
    branche courante.

Il faut travailler ensuite à intégrer cette nouvelle histoire à la ligne principale.

Les voies du temps sont interpénétrables

C’est le principe utilisé par le pull, ce qui est nommé dans la littérature le « merge workflow ».

Ce qui est fait lors d’un git pull entre une branche locale et une branche sur un dépôt distant,
« remote » en jargon git, est applicable entre deux branches d’un dépôt local.

En pratique, on se positionne sur la branche qui doit intégrer les modifications d’une autre,
par exemple une branche publique, et on fusionne la branche privée de développement dans cette branche publique :

moi@work:~/src/projet(master)$ git merge mabranche

Pour ensuite publier ces modifications par git push.

Ceci induit les mêmes inconvénients que l’utilisation des pull.

Lorsque l’on passe du temps à travailler sur une branche privée, il est tentant de « fusionner » régulièrement une branche publique de référence, afin de se mettre à jour.

Mais cette pratique ne fait que rendre illisible l’histoire privée de cette branche.

Cela rend très difficile la relecture de chaque modification apportée par cette branche car son histoire n’est plus simplement une suite logique d’étapes afin d’arriver à un résultat voulu.

Fusionner des branches amont (upstream) dans votre branche de travail en cours sans une raison réellement valable est considéré comme une très mauvaise pratique.

Un telle fusion dans une direction erronée (upstream=>branche privée) ne devrait être fait qu’en cas d’absolue nécessitée, par exemple, lorsque votre travail en cours nécessite des nouveautés effectuées sur la branche upstream.

Autrement, votre branche de travail ne sera plus sur un sujet particulier mais deviendra un amas informe de commit de sources diverses et avariées, venant de votre propre branche mais aussi d’upstream contenant du travail fait par d’autres sur des sujets tout aussi divers et variés.

On ne construit que sur les épaules de géants

Il existe une autre approche à la problématique généalogique des
commits, nommé dans la littérature « rebase workflow ».

Ce principe utilise la réécriture de l’historique, par conséquent,
elle ne peut être utilisée que sur des commits non publiés.

Le principe est le même que le greffage en botanique :

  1. On coupe la branche ;
  2. On la greffe sur une autre.

En pratique, c’est surtout utilisé pour préparer l’intégration de nos
modifications dans une branche publique.

C’est un peu le miroir du « merge workflow ».

Dans le « merge workflow », la fusion se fait sur la branche publique
et les conflits sont gérés dans l’histoire de la branche publique.

Dans le « rebase workflow », c’est l’inverse, tout se fait dans la
branche que l’on souhaite intégrer à la branche publique.

La procédure est décomposée comme suit :

  1. Mise de côté de tous les commits de la branche depuis sa création :
    on revient dans le tronc de développement (branche privée) à la
    version sur laquelle notre branche est basée ;
  2. Intégration de tous les changements de la branche publique survenus
    depuis ;
  3. Application de tous les commits mis de côté, il faut gérer les
    éventuels conflits.

Avant la procédure, la branche privée était basée sur la branche
publique à un certain instant dans le passé.

Après la procédure, la branche privée est basée sur le dernier commit
de la branche publique.

On a donc changé la base de la branche, d’où le nom de cette méthode.

Tout cela se fait simplement par :

moi@work:~/src/projet(mabranche)$ git rebase master

Il est possible de faire fréquemment des rebases des branches privées
par rapport à la branche publique où seront intégrées les
modifications.

On minimise ainsi les conflits ou, plus précisément, on les dilue dans
le temps.

Ce principe de rebase est à utiliser, au moins avant l’intégration à
la branche publique, afin d'avoir une histoire plus propre de la
branche et ce en fusionnant certains commits.

Dans ce cas on ne rebase pas par rapport à une branche publique, mais
par rapport à un commit de la branche privée.

Par exemple, ne pas avoir un commit ajoutant une fonctionnalité et les
30 suivants qui corrigent les inévitables erreurs typographiques.

« Le blabla c’est bien, mais en pratique ? » par moi©®™

Puisque je vous dis que les branches ça ne coûte rien !

Dans git, une branche est simplement un fichier dans l’arborescence du
répertoire .git/refs/heads/.

Ce fichier contient l’identifiant du dernier commit de cette branche,
c’est le SHA1 du contenu de l’objet (les détails sont accessibles dans
Les tripes de Git

L’identifiant de commit référence un fichier dans l’arborescence du
répertoire .git/objects/, avec :

  • les deux premiers caractères (1 octet noté en hexadécimal) sont le
    nom d’un sous répertoire ;
  • le reste des caractères sont le nom du fichier.

Connaître l’identifiant du dernier commit est suffisant pour remonter
l’arbre généalogique.

Faire une nouvelle branche se résume donc à créer un fichier ne
contenant qu’un identifiant.

On créé une branche dédiée pour le développement à faire :

moi@work:~/src/projet(master)$ git checkout -b issue/42 master

Le clonage déontologiquement correcte

Afin de tester nos modifications avant publication, nous pouvons
copier les fichiers sur une machine de test, par quelque moyen que ce
soit.

Un de ces moyens, et le plus simple dans notre cas, est d’utiliser
git.

Étant donné que :

  • Nous souhaitons tester les modifications apportées par le
    développement enregistré dans une branche non publiée ;
  • Nous ne souhaitons pas publier cette branche privée tant que tout ne
    sera pas propre ;

Nous allons créer une copie conforme de notre dépôt, sur la machine de test.

Le fonctionnement est simple, un dépôt git, qu’il soit sur un serveur
http ou dans notre répertoire, peut servir de base à un clone.

Et un clone, ça peut se faire par SSH.

Nous avons besoin :

  • D’une machine de test ;
  • D’un accès SSH sur cette machine ;
  • De l’outil git installé sur cette machine.

Aucun fichier ne sera éditer sur cette machine, cela évite de
s’éparpiller.

Nous mettrons à jour ce dépôt par des pull, ce qui ne posera aucun
soucis comme expliqué plus haut du fait qu’il n’y aura aucune
modification locale à la machine de test.

moi@work:~/src/projet(master)$ ssh user@test-machine
user@test-machine:~$ git clone ssh://moi@work/home/moi/src/projet && cd projet
user@test-machine:~/projet(master)$ git checkout -b issue/42 origin/issue/42
user@test-machine:~/projet(issue/42)$ # On est prêt

On garde un shell sur la machine de test, on y reviendra souvent, à
moins d’être très fort et de tout réussir du premier coup.

Les commits, c’est radioactif ?

Un commit, c’est un instantané du code à un instant « T ».

Si nous souhaitons tester l’impact de chaque commit sur un projet, il
est préférable que chacun d’eux soit fonctionnel.

Par exemple, si on modifie le nom d’une fonction ou d’une variable, on
doit, dans le même commit, modifier toutes les utilisations de l’ancien
nom.

Chaque commit se doit d’être le plus « atomique » possible.

Cycle de développement et tests

  1. On code :
    moi@work:~/src/projet(issue/42)$ ${EDITOR} src/bidule.py
    moi@work:~/src/projet(issue/42)$ git add src/bidule.py
    moi@work:~/src/projet(issue/42)$ git commit
    
  2. On nettoie la machine de test :
    user@test-machine:~/projet(issue/42)$ make uninstall
    
  3. On met à jour le dépôt de la machine de test :
    user@test-machine:~/projet(issue/42)$ git pull origin
    
  4. On met en place la nouvelle version du code à tester :
    user@test-machine:~/projet(issue/42)$ make install
    
  5. On test, si des problèmes persistent, on boucle sur 1 et on peut même utiliser un petit truc pour s’y retrouver dans toutes ces itérations.

Publication du développement

  1. On détermine le nombre de modification de notre branche par rapport à la branche « master »:
    moi@work:~/src/projet(issue/42)$ git log --oneline master..HEAD | wc -l
    
  2. On nettoie notre histoire privée, dans notre exemple, à partir du 5e commit avant la fin:
    moi@work:~/src/projet(issue/42)$ git rebase -i HEAD~5
    
  3. On rebase sur la dernière version de la branche publique :
    moi@work:~/src/projet(issue/42)$ git fetch origin
    moi@work:~/src/projet(issue/42)$ git rebase origin/master
    
  4. On intègre à la branche publique :
    moi@work:~/src/projet(issue/42)$ git checkout master
    moi@work:~/src/projet(master)$ git merge issue/42
    
  5. On tag un numéro de version, dans cet exemple, nous utilisons un timestamp :
    moi@work:~/src/projet(master)$ git tag -s -m "release/$(date +%Y%m%d.%H%M)" release/$(date +%Y%m%d.%H%M)
    
  6. On publie :
    moi@work:~/src/projet(master)$ git push origin
    

Webographie