Exemple de workflow git chez Sogilis

Et si on en revenait à la question : “un workflow c’est quoi, et ça sert à quoi ? “

Un workflow, dans notre cas pour Git, c’est surtout la définition de comment on travaille en collaboration avec notre outil de gestion de sources et avec les autres personnes.

Quelles sont les règles, mais surtout dans quel but.

Il ne s’agit surtout pas de contraindre inutilement les possibilités.

Mais pour ce faire, la première chose à se demander c’est justement quelles sont nos contraintes.

Un workflow doit répondre à nos besoins

Alors, s’il existe déjà des workflow, pourquoi ne pas en utiliser un déjà décrit ?

Déjà avant de savoir si on peut utiliser un workflow existant, il convient de savoir quels sont les objectifs visés et quelles sont nos contraintes. Ensuite, par l’étude de ceux-ci il devient possible de déterminer un workflow, soit un nouveau soit un existant.

Mais dans tous les cas un workflow se doit de nous aider, jamais de nous limiter ni nous empêcher de travailler.

Les objectifs

Nous voulons pouvoir :

  • tester facilement chaque fonctionnalité “unitairement” (vous verrez un peu plus tard que c’est un point beaucoup plus complexe qu’il n’y parait…)

  • avoir un historique très lisible pour pouvoir naviguer facilement dedans lors de la découverte de mauvais comportements

  • pouvoir désactiver une fonctionnalité très facilement

  • avoir le détail (les étapes) de chaque fonctionnalité

Les contraintes

Certains points à prendre en compte :

  • pour le moment il n’y a pas de branches de production, maintenance, etc., mais ça pourra arriver un jour

  • le corollaire c’est que pour le moment la branche principale doit toujours être stable

  • il y a 7 (pour le moment) développeurs

  • sprints de 2 semaines

  • développement de logiciel mobile et de logiciel embarqué sur un drone (et là ça change tout…)

Le résultat

Je vais reprendre les objectifs et essayer de placer en face de chacun une “règle” Git, en prenant en compte si besoin nos contraintes.

Tester unitairement chaque fonctionnalité

Bon là c’est simple, tout le monde me dira « chaque fonctionnalité dans une branche dédiée ». Oui. Mais je vous répondrai « mais le code est dédié à un drone ».

La branche pour une fonctionnalité (feature branch ou topic branch) c’est bien, à une condition importante, c’est qu’il existe un moyen de valider si cette branche est ok ou non avant de pouvoir l’intégrer dans le tronc commun.

En général là on va parler de tests unitaires, d’intégration continue, etc. On a tout ça. Mais ça ne suffit pas. En effet, dans notre cas les tests unitaires, les tests de plus haut niveau, les simulations, l’intégration continue, tout ceci ne remplace pas, en tout cas pour le moment, des essais en vol, en extérieur.

Le succès ou l’échec de notre code dépend aussi de différents matériels, de conditions extérieures — que se passe-t-il lorsque le GPS ou la communication est dégradé en plein vol parce qu’il y a des nuages ? et pire, de ressentis visuels.

Et plus que tout ceci, nous ne pouvons pas réaliser les tests en continu. Il faut se déplacer à l’extérieur pour réaliser les tests et dépendre alors de la météo.

Donc nous souhaitons pouvoir tester plusieurs fonctionnalités d’un coup si c’est possible.Le résultat pour nous c’est tout de même une branche par fonctionnalité — what else? — plus une branche d’intégration spécifique à chaque itération. Lorsque nous partons en essais, la branche d’intégration comporte l’ensemble des fonctionnalités à jour ce qui permet, si tout se passe bien, de valider l’ensemble.

Les fonctionnalités qui sont ok sont ensuite intégrées dans la branche principale master. Si jamais cela se passe mal, il est possible de générer très facilement des versions pour chaque fonctionnalité et tester, valider ou invalider chacune.

Avoir un historique lisible

L’objectif est vraiment de pouvoir naviguer facilement dans l’historique, essentiellement pour y rechercher la cause d’un mauvais comportement qui n’aurait pas été mis en évidence par les tests automatisés.

La première solution à mettre en place c’est de limiter au maximum les commits “sans valeur”, par exemple les commits de synchronisation avec l’upstream, et garantir le meilleur rapport signal/bruit possible.

Pour ça c’est assez facile, il suffit d’interdir les pull/merge de synchronisation. Si on souhaite tout de même bénéficier d’améliorations qui sont dans le tronc commun, il faut utiliser rebase ce qui linéarise l’historique.

La deuxième chose c’est d’éviter au maximum les croisements de branches. La solution passe également par l’utilisation systématique de rebase avant d’intégrer les changements. Ceci doit permettre de ne pas avoir de commits inutiles et donc de pouvoir lire facilement l’historique car plus linéaire, moins plat de spaghettis.

Pouvoir désactiver une fonctionnalité

Le scénario est le suivant : on détecte après coup une fonctionnalité qui pose problème (ou simplement on veut supprimer une fonctionnalité).

Il faut alors pouvoir visualiser très rapidement la fonctionnalité et l’ensemble de ses modifications. La pire chose qui existerait c’est de faire du merge en fast forward, c’est-à-dire une linéarisation des commits de la branche.

On les rajoute simplement au-dessus du tronc commun.

Si on fait ça — et ceux qui ont fait du svn (pouah !) connaissent très bien — il devient très compliqué d’identifier l’ensemble des modifications liées à une fonctionnalité.

Et donc il devient très compliqué de les annuler.

La solution est donc d’avoir tant que possible un unique commit pour chaque intégration de fonctionnalité dans le tronc commun.

Si cela est fait, on peut annuler facilement par la réalisation d’un commit inversé. Vous pouvez utiliser directement la commande:

git revert

pour le faire. A ce moment de décision, vous avez deux choix :

  • faire des merges systématiquement sans fast forward:

    git merge --no-ff
    
  • faire des merges avec fusion de tous les commits en un seul:

    git merge --squash
    

Avoir le détail de chaque fonctionnalité

Pour pouvoir débugger plus facilement mais aussi simplement relire et comprendre les modifications, il est intéressant de garder présentes les étapes de développement.

Ceci interdit donc l’utilisation de:

merge --squash

au profit de:

merge --no-ff.

En effet, dans ce cas nous avons un commit de merge mais la branche et donc le détail des opérations restent visibles.

Par contre, souvenez-vous, on parlait un peu plus haut d’historique propre. Dans ce cas la bonne pratique, avant de réaliser la fusion, est de nettoyer l’historique de la branche.

Je vous encourage donc vivement l’utilisation de:

rebase --interactive voir même de
rebase -i --autosquash

ça c’est une pratique qu’elle est bien ! Le but est d’améliorer les messages, fusionner certains commits entre eux voir même les réordonner ou les supprimer.

Le rebase va obliger à réécrire l’historique et donc probablement à forcer les push mais ce n’est pas grave, c’est une bonne chose d’avoir un historique propre.

En résumé

  • une branche par fonctionnalité

  • une branche d’intégration par itération

  • synchronisation uniquement par rebase

  • rebase obligatoire avant intégration

  • fusion sans fast forward obligatoire

  • nettoyage des branches avec du rebase interactif

Et en pratique ?

Voici les quelques commandes / principes que nous utilisons pour mettre en œuvre ce workflow.

1. On suppose qu’on débute une itération, master est propre

Par défaut toutes les branches vont avoir initialement comme origine master

1 git checkout master
2 git checkout -b integ

2. Pour une fonctionnalité donnée, on crée une branche pour bosser dedans

git checkout master
git checkout -b feature/my-super-cool-feature

Nos branches sont préfixées pour améliorer la lisibilité:

  • feature/ pour les fonctionnalités

  • bug/ pour les anomalies

  • refactor/ pour ce qui est lié à du pur refactoring (on en a beaucoup puisqu’on se base sur un existant pas toujours propre…)

3. On développe dans la branche

git checkout feature/my-super-cool-feature
...
hack
...
git add -p
git commit
...
hack
...
git add -p
git commit
...
git push # avec -u pour configurer l'upstream la première fois
...

4. S’il est nécessaire de se synchroniser, on rebase

Attention, j’insiste sur le nécessaire, si ce n’est pas obligatoire on ne le fait pas maintenant.

# je suppose que je suis dans ma branche de fonctionnalité
git checkout integ
# comme on ne développe jamais dans master et qu'on fast forward master
# on peut laisser le pull sans rebase
git pull
git checkout -
git rebase -

A ce moment, pour pousser mes modifications sur le serveur il faut que j’écrase la branche distante. Comme ce n’est qu’une branche de fonctionnalité et qu’il n’y a pas plusieurs personnes, hors binome, qui travaille dessus, c’est permis.

git push -f

5. Fusion dans la branche d’intégration

Pour commencer on se synchronise et rebase avec, cf 4 .

Ensuite on s’occupe de nettoyer la branche, avec::

rebase -i voir rebase -i –autosquash

si vous avez pensé à l’utiliser. Enfin, on fusionne sans faire de fast forward.

1 git checkout integ
2 git pull
3 git checkout -
4 git rebase -
5 git merge --no-ff integ

Concernant le message de commit il y a deux choix:

  • Soit le nom de la branche est déjà explicite et ok,

  • soit on met un beau message bien propre qui indique la fonctionnalité qu’on vient d’intégrer (à préférer).

Evidemment ? on pousse la branche d’intégration sur le serveur.

6. Si les tests automatiques ont montré que la fonctionnalité ainsi que la branche integ sont ok, on peut fusionner integ dans master

git checkout master
git merge --ff
git push

Etant donné qu’on vient de faire une fusion fast forward de integ, il n’est pas nécessaire de faire un rebase ou autre de cette dernière.

7. On nettoie un peu nos branches, c’est-à-dire qu’on ne garde pas sur le serveur de branches fusionnées et terminées, histoire de garder un ensemble lisible.

1 git branch -d feature/my-super-cool-feature
2 git push origin --delete feature/my-super-cool-feature