Oups je suis en retard sur mes articles. Revenons un peu cette fois-ci sur les fondamentaux du worklflow de commit avec git. On attaque ici mon dada, mon cheval de bataille, mon idée fixe, mon obsession… Les bonnes pratiques du commit ! Elles sonnent souvent comme le truc pénible, qu’on aime asséner juste pour se faire plaisir ou pour répondre à une manie personnelle… Et pourtant elles sont fondamentales. Elles vont conditionner l’efficacité de l’utilisation de git en général et votre gestion de version en particulier. 

 

Mais au fait, ça sert à quoi un gestionnaire de versions ?

C’est bien beau de vouloir maîtriser git mais encore faut-il savoir pourquoi j’en suis arrivé là. Pourquoi vouloir faire de la gestion de version d’un code donné ? Une des premières raisons évoquées souvent, c’est la sauvegarde du code. Que nenni ! Ca n’est nullement le rôle d’un gestionnaire de version, tout juste un effet de bord.

Au centre de ses objectifs, l’historique ! Eh oui un gestionnaire de version a pour objectif principal de générer et gérer un historique de code.  Et il y a plein de bonnes raisons à cela : consulter cet historique quand on arrive sur un projet pour en comprendre la logique et le fonctionnement, pouvoir travailler à plusieurs sans se marcher sur les pieds, revenir en arrière ou annuler une modification quand on se rend compte qu’on a explosé le travail de ses petits camarades… et souvent l’historique, au sens messages de commit, est la seule documentation disponible et lisible. D’où son importance.

Bref, que des bonnes raisons qui s’imposent à vous pour bichonner cet historique. Mais ça veut dire quoi au juste au quotidien ?

Source : xkcd

Tes messages de commit tu soigneras

Atomique le contenu de tes commits sera

Au contenu précis de tes commits tu réfléchiras

C’est beau non ? Oui mais encore ?… En terme de contenu de commits, on parle souvent de commits atomiques. Le contenu d’un commit doit être maîtrisé et ciblé. Or lorsque vous travaillez sur votre code, vous êtes amené à faire des modifications de natures complètement différentes, qui ne devraient pas faire partie d’un seul et même commit. D’où la construction d’un historique de commits successifs et atomiques.

Malheureusement il n’existe pas de formule magique pour définir l’atomicité des commits. On peut éventuellement se poser la question suivante : “mon commit X vient d’exploser mon code. Le plus simple est donc de l’annuler. Vais-je obtenir quelque chose de satisfaisant ?” Or ce fameux commit contenait ma modification erronée mais aussi une correction de bug, un peu de doc… Bref la cata, tout va disparaître…

Pour éviter ce genre de déconvenues, un élément indispensable à connaître : la zone de cache. Elle a pour objectif de vous aider à construire ces commits en mettant de côté le contenu du prochain commit. Après quelques années de formation, je vous rassure, plus 80% des gens ne connaissaient absolument pas cette zone de cache ou tout au moins ne l’utilisent pas.

Concrètement, la zone de cache c’est quoi ?

La zone de cache est une zone tampon entre vos modifications réalisées dans ce qu’on appelle la zone de travail (en gros les fichiers et répertoires que vous manipulez au quotidien dans votre projet) et le commit, c’est à dire l’enregistrement dans le dépôt git de votre modification. Je me permet de reprendre une analogie proposée par jub0bs à laquelle je n’avais pas pensé mais qui illustre très bien le pourquoi de l’existence de cette zone.

 

Sempé

Vous êtes dans une cour d’école et vous devez faire une photo de classe avec une trentaine de mômes qui courent partout en hurlant. Tant bien que mal vous y mettez de l’ordre et tous les gamins sont prêts. Vous prenez la photo mais là vous vous rendez compte qu’il en manque 2. Vite on va les chercher et finalement on prend la photo finale, photo qui sera ensuite ajoutée à l’album de l’école. Imaginez maintenant la chose suivante. Vous rassemblez tout le monde pour la photo, vous la prenez et vous la mettez dans l’album. Et là vous vous rendez compte que 2 enfants manquent sur la photo. Il va falloir rassembler à nouveau la classe, les mettre en rang et prier pour qu’entre temps une épidémie de gastro ne soit pas passée par là… Ah et j’oubliais, la photo erronée a été collée à la super glue dans l’album. Bref, une vraie galère :).

Eh bien votre commit, c’est pareil. Vous rassemblez des modifications qui vous semblent cohérentes entre elles, et vous les ajoutez au cache. Et si vous avez oublié un fichier ou que vous souhaitez retirer une des modifications non concluantes, vous pouvez le faire très facilement. Une fois que vous êtes satisfait du résultat, vous committez. Dans le cas où vous n’utilisez pas la zone de cache, le fichier oublié et la modification qui cassait votre code devront être pris en compte en modifiant l’historique de commits existants. Les outils existent mais leur utilisation suppose leur maîtrise et peut avoir des conséquences non négligeables sur cet historique. Et c’est encore plus vrai lorsque vous avez déjà poussé ces commits sur le serveur distant.

La zone de cache est donc essentielle pour être plus efficace et générer un historique utilisable, lisible et interprétable.

Comment fonctionne la zone de cache ?

Je pars du principe que vous avez lu l’article concernant les dessous du fonctionnement de git basé sur les objets. Tout se passe là encore dans votre dépôt local, dans le répertoire .git. La gestion de cette zone repose sur un objet, le blob et un fichier, index. Pour ajouter du contenu à la zone de cache, ou flagger vos modifications, vous utilisez la commande git add. Cette commande réalise 2 choses :

  • elle crée les blobs correspondant à la nouvelle version des fichiers après modification
  • elle alimente le fichier index qui contient alors la liste des fichiers modifiés et des pointeurs vers le ou les blobs.

Parce qu’un exemple vaut mieux qu’un long discours, je modifie le fichier fic1 de mon projet et je crée un nouveau fichier fic2.

[ennael@zarafa lab (dev)]$ ls
Changelog fic1 fic2 README
[ennael@zarafa lab (dev)]$ git status
On branch master
Changes not staged for commit:
(use “git add <file>…” to update what will be committed)
(use “git restore <file>…” to discard changes in working directory)
modified:   fic1

Untracked files:
(use “git add <file>…” to include in what will be committed)
fic2

Je vais ensuite ajouter au cache ces 2 modifications, qui, on le rappelle, peuvent être l’ajout d’un nouveau fichier, la modification ou la suppression d’un fichier existant.

[ennael@zarafa lab (dev)]$ git add fic1 fic2

Que s’est-il vraiment passé ? Consultons le contenu du fichier .git/index avec la commande ci-dessous :

[ennael@zarafa lab (dev)]$ git ls-files --cached
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       README
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       fic1

Sont listés les fichiers dont les modifications ont été indexées avec les informations  suivantes : nom du fichier, permissions, SHA1 du blob contenant la nouvelle version du fichier. Il ne vous reste alors qu’à committer : on reconstruit un nouveau tree à partir de celui proposé par HEAD en le complétant avec les informations de la zone de cache.

J’ai des modifications en zone de cache et je veux en vérifier le contenu

A tout moment vous êtes capable de vérifier le contenu de votre zone de cache. On peut d’abord lister les fichiers contenus dans cette zone, tout simplement avec la commande git status :

[ennael@zarafa lab (dev)]$ git status 
On branch master
Changes to be committed:
 (use "git restore --staged <file>..." to unstage)
       modified:   fic1
       modified:   fic2
Changes not staged for commit:
 (use "git add <file>..." to update what will be committed)
 (use "git restore <file>..." to discard changes in working directory)
       modified:   Makefile
 

Dans ma zone de cache j’ai donc 2 fichiers, fic1 et fic2. Reste à savoir maintenant quel est le contenu des modifications à committer. Rien de plus simple avec la commande git diff :

[ennael@zarafa lab (master)]$ git diff --cached 
diff --git a/fic1 b/fic1 
index 76233cb..ca194e3 100644 
--- a/fic1 
+++ b/fic1 
@@ -1,4 +1,4 @@ 
 #!/bin/bash 
 liste=`ls $1` 
-echo $liste 
+echo "la liste des fichiers de $1 est $liste"

Et si je veux choisir dans un fichier, les modifications à indexer pour le prochain commit ?

Dans son fonctionnement standard la commande git add prend en compte l’intégralité des modifications. Or dans un même fichier, on peut répertorié des modifications de nature bien différente : ajout d’une nouvelle fonction, fix de typos, commentaire de code… Si je veux être le plus efficace possible, il faudrait donc que je puisse diviser ces modifications en plusieurs commits.

Voici un fichier Makefile contenant un certain nombre de modifications :

[ennael@zarafa lab (master)]$ git status 
On branch master 
Changes not staged for commit: 
  (use "git add <file>..." to update what will be committed) 
  (use "git restore <file>..." to discard changes in working directory) 
        modified:   Makefile

On va réaliser de l’indexation partielle. La seule chose à modifier dans la commande de staging est l’option -p. Allons-y :

[ennael@zarafa lab (master)]$ git add -p ./Makefile
diff --git a/Makefile b/Makefile
index 0ceee0c..2281e4b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,10 @@
# SPDX-License-Identifier: GPL-2.0+

-VERSION = 2018
-PATCHLEVEL = 11
-SUBLEVEL =
+VERSION = 2019
+PATCHLEVEL = 10
+SUBLEVEL = 1
EXTRAVERSION =
-NAME =
+NAME = test

# *DOCUMENTATION*
(1/4) Stage this hunk [y,n,q,a,d,j,J,g,/,s,e,?]?

Tout y est ! Vous avez sous les yeux le premier bloc de modifications rencontré dans votre fichier. Comme dans beaucoup de commandes on utilise ici le format diff. A la suite de ce premier bloc, une question est posée : “faut-il indexer ce bloc de modification ?”. La suite est très simple : “y” pour l’indexer, “n” pour le laisser dans la zone de travail (il ne fera donc pas partie du prochain commit). D’autres possiblités sont disponibles, que je vous laisserai les découvrir dans la page de man.

Les options “s” comme split et “e” comme edit peuvent être intéressantes. Elles vous permettent respectivement de redécouper le bloc s’il ne vous convient pas et d’éditer le contenu du bloc.

Une fois que vous avez répondu à la question, le bloc suivant vous est présenté et ainsi de suite jusqu’à la fin du fichier. Au final on obtient quelque chose comme cela :

[ennael@zarafa lab (master)]$  git status 
On branch master
Changes to be committed:
 (use "git restore --staged <file>..." to unstage)
       modified:   Makefile

Changes not staged for commit:
 (use “git add <file>…” to update what will be committed)
 (use “git restore <file>…” to discard changes in working directory)
       modified:   Makefile

Avec cet outil de staging partiel, plus aucune excuse pour bacler ses commits, même quand on a oublié de committer depuis un moment. C’est aussi un très bon moyen, quand il est utilisé systématiquement, de vérifier le contenu de ses futurs commits avant de les valider.

La zone de cache tout en souplesse !

J’ai ajouté des éléments à ma zone de cache et je me rends compte que j’ai fait une erreur. Qu’à cela ne tienne. git add ajoute des fichiers à la zone de cache, git reset va vous permettre d’en retirer. git reset permet à la base de réécrire votre historique. Mais si vous ne définissez pas de SHA1 de commit en argument de la commande, alors on peut l’utiliser pour modifier le contenu de la zone de cache. Un exemple :

[ennael@zarafa lab (master)]$ git status  
On branch master 
Changes to be committed: 
  (use "git restore --staged <file>..." to unstage) 
        modified:   fic1 
[ennael@zarafa lab (master +)]$ git reset fic1 
Unstaged changes after reset: 
M       fic1 
[ennael@zarafa lab (master *)]$ git status 
On branch master 
Changes not staged for commit: 
  (use "git add <file>..." to update what will be committed) 
  (use "git restore <file>..." to discard changes in working directory) 
        modified:   fic1

Le fichier fic1, qui était auparavant ajouté à la zone de cache, a été désindexé grâce à la commande et ne fera donc pas partie du prochain commit.

Et comme je pouvais faire du staging partiel, eh bien je peux aussi faire du reset partiel pour ne désindexer qu’une partie des modifications du fichier figurant déjà dans la zone de cache :

[ennael@zarafa lab (master +)]$ git reset -p fic1
diff –git a/fic1 b/fic1
index 76233cb..3f42501 100644
— a/fic1
+++ b/fic1
@@ -1,3 +1,5 @@
#!/bin/bash

+echo $1
+
liste=`ls $1`

Attention au sens de la question qui est posée. C’est bien la désindexation du bloc de modifications qui est proposée. 

Voilà vous savez tout de la zone de cache et des bonnes pratiques qui y sont liées. C’est bon, mangez-en ! En n’oubliant pas bien sûr de soigner vos messages de commit ! (une autre idée fixe chez moi)