Chapitre 5 sur 13

Git log : Rechercher, naviguer et modifier l'historique

Laisser un commentaire

Dans un projet Git, l’ensemble des commits forment l’historique. Ce dernier est une mine d’informations concernant le projet : il permet de savoir qui a fait quoi, quand et pourquoi. Le maîtriser, c’est être capable de naviguer, rechercher, restaurer et modifier l’historique. Git nous offre en somme le voyage dans le temps !

La commande de base de gestion de l’historique est sans surprise log. Nous l’avons déjà utilisée dans les chapitres précédents. Nous allons ici l’appréhender de manière plus approfondie.

Afin de pouvoir illustrer nos exemples, nous allons cloner un dépôt ayant un historique un peu plus conséquent que les projets précédents : le dépôt du code source de PHP. PHP est programmé en C, utilise bien entendu Git (what else?) et le dépôt compte plus de cent mille commits. Le dépôt étant conséquent, le clone peut prendre quelques minutes…

Une fois cloné, on se place à l’intérieur et on en affiche l’historique.

# Affichage classique
git log
commit 3e075cadfbb7de49ee0193c332c90a4f71090826 (HEAD -> master, origin/master, origin/HEAD)
Merge: 9b6c0bdcd6 1eb2379aa3
Author: Christoph M. Becker <cmbecker69@gmx.de>
Date:   Tue Jan 7 11:13:24 2020 +0100

    Merge branch 'PHP-7.4'

    * PHP-7.4:
      Bump version

commit 1eb2379aa379303fec1d3fbc72dd0fa4839efbb5 (origin/PHP-7.4)
Merge: 59c3ddab13 38c0a53b60
Author: Christoph M. Becker <cmbecker69@gmx.de>
Date:   Tue Jan 7 11:10:55 2020 +0100

    Merge branch 'PHP-7.3' into PHP-7.4

    * PHP-7.3:
      Bump version

commit 38c0a53b60059e06128f61a28573d6c10b60d1d2 (origin/PHP-7.3)
Author: Christoph M. Becker <cmbecker69@gmx.de>
Date:   Tue Jan 7 11:03:19 2020 +0100

    Bump version

commit 9b6c0bdcd6f29a00d0f3659ecc95898d90cadad1
Merge: b22daa3a06 59c3ddab13
Author: Nikita Popov <nikita.ppv@gmail.com>
Date:   Mon Jan 6 22:42:31 2020 +0100

    Merge branch 'PHP-7.4'

    * PHP-7.4:
      Remove support for preloading on Windows

On constate immédiatement que le projet comporte plusieurs contributeurs et que la branche master ne semble pas être celle où se passe la plus grosse partie du développement. Voyons quelques options utiles de log qui vont nous permettre d’explorer efficacement ce dépôt.

De nombreuses options permettant d’afficher uniquement ce dont on a besoin. Quelques unes des plus courantes sont :

Les options --branches, --tags, --remotes, --author et --comitter prennent un pattern de manière optionnelle, lequel agit comme un filtre. En son absence, tous les éléments du type donné seront retournés.

Lorsque nous avons un dépôt avec plusieurs branches de développement, Git n’affiche par défaut que l’historique de la branche courante. On peut lui demander d’afficher l’historique de toutes les branches confondues avec l’option --all.

git log --oneline
3e075cadfb (HEAD -> master, origin/master, origin/HEAD) Merge branch 'PHP-7.4'
1eb2379aa3 (origin/PHP-7.4) Merge branch 'PHP-7.3' into PHP-7.4
38c0a53b60 (origin/PHP-7.3) Bump version
9b6c0bdcd6 Merge branch 'PHP-7.4'
59c3ddab13 Remove support for preloading on Windows
b22daa3a06 Merge branch 'PHP-7.4'
846b647953 Throw Error when referencing uninit typed prop in __sleep
d9caf3561b [ci skip] Merge branch 'PHP-7.4'
56306cc4af [ci skip] Merge branch 'PHP-7.3' into PHP-7.4
06e78cad83 Revert "Extend CURLFile to support streams"

git log --oneline --all
3e075cadfb (HEAD -> master, origin/master, origin/HEAD) Merge branch 'PHP-7.4'
1eb2379aa3 (origin/PHP-7.4) Merge branch 'PHP-7.3' into PHP-7.4
38c0a53b60 (origin/PHP-7.3) Bump version
793d775b04 (tag: php-7.3.14RC1, origin/PHP-7.3.14) Bump version
452c7d82c6 (origin/PHP-7.4.2) Update NEWS for 7.4.3
470dc9aba3 (tag: php-7.4.2RC1) Update versions for PHP 7.4.2RC1
75a5a08161 Update NEWS for PHP 7.4.2RC1
9b6c0bdcd6 Merge branch 'PHP-7.4'
59c3ddab13 Remove support for preloading on Windows
b22daa3a06 Merge branch 'PHP-7.4'
846b647953 Throw Error when referencing uninit typed prop in __sleep
d9caf3561b [ci skip] Merge branch 'PHP-7.4'
56306cc4af [ci skip] Merge branch 'PHP-7.3' into PHP-7.4
06e78cad83 Revert "Extend CURLFile to support streams"

On pourrait croire que --all n’affiche que peu de choses supplémentaires. Si le git log classique semble indiquer des commits de branches n’étant pas la master, c’est en fait simplement une information supplémentaire. Par exemple, pour le commit 1eb2379aa3 dont le message de commit est Merge branch 'PHP-7.3' into PHP-7.4, Git nous indique que la pointe de la branche origin/PHP-7.4 (autrement dit, HEAD) en est à ce commit.

L’option ajoutant des labels signalant les pointes des branches est --decorate. Il s’agit du comportement par défaut depuis Git 2.13. Cependant, elle peut prendre un paramètre --decorate[=level] car plusieurs niveaux sont possibles :

Afin de mieux visualiser le lien entre les branches, nous aurons souvent recours à l’option --graph, laquelle permet de créer un graphique ASCII de nos commits et des branches.

git log --oneline --graph
*   3e075cadfb (HEAD -> master, origin/master, origin/HEAD) Merge branch 'PHP-7.4'
|\
| *   1eb2379aa3 (origin/PHP-7.4) Merge branch 'PHP-7.3' into PHP-7.4
| |\
| | * 38c0a53b60 (origin/PHP-7.3) Bump version
* | |   9b6c0bdcd6 Merge branch 'PHP-7.4'
|\ \ \
| |/ /
| * | 59c3ddab13 Remove support for preloading on Windows
* | |   b22daa3a06 Merge branch 'PHP-7.4'
|\ \ \
| |/ /
| * | 846b647953 Throw Error when referencing uninit typed prop in __sleep
* | |   d9caf3561b [ci skip] Merge branch 'PHP-7.4'
|\ \ \
| |/ /
| * |   56306cc4af [ci skip] Merge branch 'PHP-7.3' into PHP-7.4
| |\ \
| | |/
| | * 06e78cad83 Revert "Extend CURLFile to support streams"
* | |   d7833fd974 Merge branch 'PHP-7.4'
|\ \ \
| |/ /
| * |   09ebeba1af Merge branch 'PHP-7.3' into PHP-7.4
| |\ \
| | |/
| | * ae2150692a Fix #54298: Using empty additional_headers adding extraneous CRLF
* | | 29df9d13da Rename skeleton stub file
* | | be0b94c220 Fix hypothetical segfault in gdTransformAffineCopy()
* | |   0b4da7e6da Merge branch 'PHP-7.4'
|\ \ \
| |/ /
| * | 68f6ab7113 Don't link against openssl 1.1 in curl
* | |   5c51a482c9 Merge branch 'PHP-7.4'

On visualise ici clairement les différentes branches ainsi que leurs points de fusion. Pour chaque commit, l’astérisque indique la branche sur laquelle le commit a été effectué. Lorsqu’il y a plusieurs branches parallèles, la ou les branches sur lesquelles le commit n’est pas ont alors seulement le “|”.

Par ailleurs, le projet PHP ayant opté pour la stratégie du merge plutôt que du rebase, si on visualise le graphique avec l’option --all, on voit tout de suite le “sapin de noël” caractéristique de ce type de projet.

git log --oneline --all --graph
*   3e075cadfb (HEAD -> master, origin/master, origin/HEAD) Merge branch 'PHP-7.4'
|\
| *   1eb2379aa3 (origin/PHP-7.4) Merge branch 'PHP-7.3' into PHP-7.4
| |\
| | * 38c0a53b60 (origin/PHP-7.3) Bump version
* | |   9b6c0bdcd6 Merge branch 'PHP-7.4'
|\ \ \
| |/ /
* | |   b22daa3a06 Merge branch 'PHP-7.4'
|\ \ \
* \ \ \   d9caf3561b [ci skip] Merge branch 'PHP-7.4'
|\ \ \ \
* \ \ \ \   d7833fd974 Merge branch 'PHP-7.4'
|\ \ \ \ \
* | | | | | 29df9d13da Rename skeleton stub file
* | | | | | be0b94c220 Fix hypothetical segfault in gdTransformAffineCopy()
* | | | | |   0b4da7e6da Merge branch 'PHP-7.4'
|\ \ \ \ \ \
* \ \ \ \ \ \   5c51a482c9 Merge branch 'PHP-7.4'
|\ \ \ \ \ \ \
* \ \ \ \ \ \ \   730f4f25db Merge branch 'PHP-7.4'
|\ \ \ \ \ \ \ \
* \ \ \ \ \ \ \ \   bc04d5e9fc Merge branch 'PHP-7.4'
|\ \ \ \ \ \ \ \ \
* | | | | | | | | | d83f89f076 Remove useless else branch
* | | | | | | | | | 019e8d438c Throw exception for unconstructed intl objects
* | | | | | | | | | ade217d05c Remove duplicate test cases
* | | | | | | | | | fc99e89baf Fix slowest tests
* | | | | | | | | | 0f89d407fc Fix build if SQLITE_RECURSIVE is not supported
* | | | | | | | | |   37a7046d85 Merge branch 'identical-handler' into HEAD
|\ \ \ \ \ \ \ \ \ \

Nous n’avons jusque là observé que les données des commits en eux-mêmes. Cependant, Git nous permet aussi d’en savoir un peu plus sur le contenu des commits.

# --stat nous donne quelques infos supplémentaires
git log --oneline --all --stat
3e075cadfb (HEAD -> master, origin/master, origin/HEAD) Merge branch 'PHP-7.4'
1eb2379aa3 (origin/PHP-7.4) Merge branch 'PHP-7.3' into PHP-7.4
38c0a53b60 (origin/PHP-7.3) Bump version
 NEWS               | 4 +++-
 Zend/zend.h        | 2 +-
 configure.ac       | 2 +-
 main/php_version.h | 6 +++---
 4 files changed, 8 insertions(+), 6 deletions(-)
793d775b04 (tag: php-7.3.14RC1, origin/PHP-7.3.14) Bump version
 NEWS               | 2 ++
 Zend/zend.h        | 2 +-
 configure.ac       | 2 +-
 main/php_version.h | 4 ++--
 4 files changed, 6 insertions(+), 4 deletions(-)
452c7d82c6 (origin/PHP-7.4.2) Update NEWS for 7.4.3
 NEWS | 3 +++
 1 file changed, 3 insertions(+)

Cependant, c’est incontestablement -p, --patch, qui nous en dit le plus.

git log --oneline --all --patch
3e075cadfb (HEAD -> master, origin/master, origin/HEAD) Merge branch 'PHP-7.4'
1eb2379aa3 (origin/PHP-7.4) Merge branch 'PHP-7.3' into PHP-7.4
38c0a53b60 (origin/PHP-7.3) Bump version
diff --git a/NEWS b/NEWS
index 1f0544934c..2c2b7481a6 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,8 @@
 PHP                                                                        NEWS
 |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-?? ??? ????, PHP 7.3.14
+?? ??? ????, PHP 7.3.15
+
+23 Jan 2020, PHP 7.3.14

 - Core
   . Fixed bug #78999 (Cycle leak when using function result as temporary).
diff --git a/Zend/zend.h b/Zend/zend.h
index 98c97197d2..73e8bb8f49 100644
--- a/Zend/zend.h
+++ b/Zend/zend.h
@@ -20,7 +20,7 @@
 #ifndef ZEND_H
 #define ZEND_H

-#define ZEND_VERSION "3.3.14-dev"
+#define ZEND_VERSION "3.3.15-dev"

 #define ZEND_ENGINE_3

Une autre option qui peut s’avérer informative tout en restant compacte est --name-only. Elle permet d’afficher la liste des fichiers qui ont été modifiés dans un commit.

Dans tous les exemples précédents, seule une partie du log est affiché. Sans plus d’indication, Git vous permet de paginer dans l’ensemble du log. Vous pouvez facilement limiter la sortie à un nombre d’entrées données en passant -nn est le nombre d’entrées à considérer.

Formatage avancé

Nous avons vu précédemment quelques options courantes de formatage. Il en existe bien d’autres et Git nous permet de faire du 100% sur mesure si nous en avons besoin. On utilise l’option --format=<option> dont les options sont :

Il existe en plus l’option <custom>custom est une chaîne définie par l’utilisateur selon un nombre de placeholders dont voici les plus courants.

Leur nombre est trop important pour tous les lister ici. Aussi, si vous en avez besoin, il faudra se référer directement à la documentation ou à cette cheatsheet.

À titre d’exemple, j’utilise l’alias logall dans mon .gitconfig afin d’avoir un output synthétique mais détaillé.

# Dans le .gitconfig
[alias]
logall = log --graph --name-only --abbrev-commit --decorate --date=relative --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all

Les couleurs ne sont malheureusement pas préservées dans l’exemple suivant, mais vous pouvez tout de même vous rendre compte du résultat. Vous pouvez ajouter cette ligne dans votre .gitconfig, tester et personnaliser la configuration.

git logall
* a978ce3 - (3 months ago) Bump lodash from 4.17.11 to 4.17.15 - dependabot[bot] (HEAD -> master, origin/master, origin/HEAD)|
| package-lock.json

* 69e08b3 - (7 weeks ago) 🚑 add missing return - Buzut|
| models/userModel.js

*   307bd24 - (3 months ago) Merge pull request #4 from Buzut/dependabot/npm_and_yarn/js-yaml-3.13.1 - Buzut
|\
| * d949bc8 - (3 months ago) Bump js-yaml from 3.12.2 to 3.13.1 - dependabot[bot]| |
| | package-lock.json

* |   7d67a96 - (3 months ago) Merge pull request #3 from Buzut/dependabot/npm_and_yarn/eslint-utils-1.4.3 - Buzut
|\ \
| |/
|/|
| * 285fb22 - (3 months ago) Bump eslint-utils from 1.3.1 to 1.4.3 - dependabot[bot]
|/  |
|   package-lock.json

Filtrer et rechercher dans l’historique

Étant donné que certaines commandes s’avèrent vite très verbeuses, on peut vouloir en limiter la portée. Nous avons vu au tout début de ce chapitre que de nombreuses options permettent de filtrer le log. Voyons-en quelques unes plus en détails.

Filtrer par fichiers ou répertoires

Bien souvent, on ne veut afficher les modifications introduites que dans un seul fichier. On peut alors directement le passer en paramètre de log.

# Afficher les modifications du README seulement
git log -p README.md
commit 0292d23bb052e5702ef3a8b000e8ba0635cfac13
Author: Stanislav Malyshev <stas@php.net>
Date:   Thu Nov 21 22:03:01 2019 -0800

    Add fuzzing badge to README

diff --git a/README.md b/README.md
index 120c36ea85..204fe71489 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ blog to the most popular websites in the world. PHP is distributed under the
 [![Build status](https://travis-ci.org/php/php-src.svg?branch=master)](https://travis-ci.org/php/php-src)
 [![Build status](https://ci.appveyor.com/api/projects/status/meyur6fviaxgdwdy/branch/master?svg=true)](https://ci.appveyor.com/project/php/php-src)
 [![Build Status](https://dev.azure.com/phpazuredevops/php/_apis/build/status/php.php-src?branchName=master)](https://dev.azure.com/phpazuredevops/php/_build/latest?definitionId=1&branchName=master)
+[![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/php.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:php)

 ## Documentation

Il est également possible de passer en paramètres plusieurs fichiers ou même un répertoire afin d’obtenir les modifications des fichiers contenus dans ce dernier.

Filtrer par auteur ou comitteur

Il est également possible de n’afficher que les commits d’un auteur ou d’un comitteur. Par exemple, nous voulons dans le projet PHP seulement les commits de Rasmus Lerdorf (le créateur de PHP) lui-même.

git log --author "Rasmus Lerdorf"
commit af57b6330b3cd25f1a4d7dfcebb92181a6f7ff1b
Author: Rasmus Lerdorf <rasmus@lerdorf.com>
Date:   Wed Oct 23 14:34:12 2019 -0700

    Reverting push to wrong repo

commit 5870efbcf5235bb7328fe7cea3b8e2b92fb9fc0d
Author: Rasmus Lerdorf <rasmus@lerdorf.com>
Date:   Wed Oct 23 14:31:27 2019 -0700

    Update alloc patch

Si vous souhaitez filtrer par comitteur, il faudra utiliser l’option --committer. Sachez également que ces options peuvent prendre un pattern en argument, ce n’est pas utile tous les jours, mais c’est tout de même bon à savoir.

Filtrer selon la date de commit

Il est possible de considérer les modifications relatives à une date donnée.

# Afficher les modifs de moins de deux semaines
git log --since "2 weeks"

# Ou antérieure à une date précise
git log --before 2019-12-20

De nombreux formats de dates sont supportés :

Git est assez smart sur les formats de date, aussi, les dates à la française (JJ-MM-1990) ou au format US (MM-JJ-AAAA) sont aussi compris, ainsi que toutes variantes lorsqu’il n’y a pas d’ambiguité. Il est ainsi possible de d’utiliser JJ-MM-AA ou AA-MM-JJ etc.

Cependant, pour se prémunir de toute ambiguité menant à des résultats erronés, le format international (ISO) est recommandé. Il n’est toutefois pas nécessaire qu’il soit complet, on peut en effet omettre l’heure et le fuseau horaire.

Nous l’avons constaté, il est également possible de préciser des dates de manière relative. Les mots clefs sont les suivants :

Le format naturel requièrent des guillemets pour lever l’ambiguité sur le fait qu’il ne s’agit que d’un seul argument, mais il existe un format où l’on remplace les espaces par des points, lequel permet de se passer des guillemets.

git log --since "2 days 3 hours"

# ou
git log --since 2.days.3hours

Les commandes permettant de préciser si l’on veut les commits avant ou après une date sont respectivement --before ou --until et --after ou --since. Cela veut dire la même chose mais selon le contexte, l’un sonnera mieux que l’autre. En outre, il est possible de spécifier des intervalles en utilisant les deux conjointement.

Recherche dans le log

Dès lors que la taille du log est d’importance, une recherche manuelle devient compliquée et fastidieuse. Heureusement, Git nous permet d’effectuer des recherches dans le l’historique.

La forme de recherche la plus simple consite à rechercher dans les messages de commit. On utilise pour cela l’option grep.

# Afficher les commits relatifs aux versions 7-x
git log --oneline --grep "PHP-7-*"
3e075cadfb (HEAD -> master, origin/master, origin/HEAD) Merge branch 'PHP-7.4'
1eb2379aa3 (origin/PHP-7.4) Merge branch 'PHP-7.3' into PHP-7.4
9b6c0bdcd6 Merge branch 'PHP-7.4'
b22daa3a06 Merge branch 'PHP-7.4'
d9caf3561b [ci skip] Merge branch 'PHP-7.4'
56306cc4af [ci skip] Merge branch 'PHP-7.3' into PHP-7.4

On peut aussi rechercher la présence de chaînes de caractères en particulier. L’option -S affiche les commits modifiant le nombre d’occurences d’une chaîne donnée (ajout ou suppression). Pour cet exemple, nous allons travailler avec Jamments – une API de gestion des commentaires (utilisée sur ce site). Étant l’auteur de ce moteur de commentaires, il m’est plus facile de trouver des exemples de recherche dans le code.

# On recherche l'ajout ou la suppression de "hashToMd5"
git log -S "hashToMd5"
commit f41181e706a331bb2361ff5ddd38d03f1dbe6598
Author: Buzut <blog@buzut.fr>
Date:   Thu Mar 21 11:56:14 2019 -0500

    🌟 add delete comment capability

commit 17522f6c6742fdf097d27a36925df2122321c12a
Author: Buzut <blog@buzut.fr>
Date:   Wed Mar 20 17:29:28 2019 -0500

    🌟 generate website cache infos

commit 174c0cc10974dc635e9e8c105e017b427e311af4
Author: Buzut <blog@buzut.fr>
Date:   Sat Mar 16 13:07:31 2019 -0500

    🌟 regenerate cache on comment approve

L’option -G permet de rechercher selon une regex au lieu d’un string. Il y a en plus une petite nuance entre les deux. -S n’affiche que les commits qui modifient le nombre d’occurences du string recherché tandis que -G considère toutes les modifications incluant ce string.

En d’autres termes, un commit contenant la modification suivante sera trouvé par git log -G "hashToMd5" mais pas par git log -S "hashToMd5".

- const { hashToMd5 } = require('../libs/stringProcessors');
+ const { trim, lowerCase, hashToMd5 } = require('../libs/stringProcessors');

Vous pouvez faire un test et observerez que ces deux commandes ne retournent pas exactement le même nombre de ligne sur le repo Jamments. Si vous souhaitez obtenir le comportement de -S tout en utilisant une expression régulière, il faut alors utiliser -S conjointement à l’option --pickaxe-regex.

Recherche binaire avec bisect

Il arrive que l’on ait un bug mais que l’on ne sache pas quel commit l’a introduit. On peut faire une recherche manuelle linéraire – en regardant tous les commits un par un – ou utiliser git bisect.

On indique alors à Git la dernière version connue dans laquelle “ça marche” et la première version connue dans laquelle “ça ne marche pas”. Il se charge alors de trouver quand le bug a été introduit.

# On lance bisect
git bisect start

# On indique la version actuelle est buggée
git bisect bad

# Puis la dernière version dont on est sur du bon fonctionnement
git bisect good 15c50b1

Git va ensuite réduire le nombre des possibilités via une recherche binaire. À chaque étape, il faudra vérifier si le bug est présent ou non et lui indiquer le résultat avec bisect good ou bisect bad. Vous aurez en quelques étapes trouvé le moment où l’erreur a été introduite.

Historique d’une fonction ou d’un bloc de code

L’option -L permet de suivre les modifications affectant une fonction ou un blog de code dans un fichier. Cette commande peut donc prendre deux formes :

start et end sont des numéros de lignes tandis que functionName est un nom de fonction. Il est possible d’omettre un des deux start ou end. Cette forme permet de suivre le bloc de code située entre ces lignes (ou commençant ou terminant à l’une d’elle en l’absence de l’une des deux).

end peut également être un offset, c’est à dire un nombre de ligne à considérer avant ou après la ligne indiquée pour start. C’est indiqué par un signe précédent le nombre +x ou -n.

Enfin, start ou end peut être une regex, auquel cas le match sera fait en fonction du pattern et non du numéro de ligne. Quand à functionName, il s’agit toujours d’une regex.

# Suivi des modifications de saveComment dans commentController.js
git log -L :saveComment:controllers/commentController.js
commit b627ed2575c454f859621fbc8c0c953bf43bbd43
Author: Buzut <blog@buzut.fr>
Date:   Tue Mar 19 17:31:24 2019 -0500

    🌟 provide slug in link to validate comment on article's page

diff --git a/controllers/commentController.js b/controllers/commentController.js
--- a/controllers/commentController.js
+++ b/controllers/commentController.js
@@ -23,26 +23,27 @@
 function saveComment({ slug, parent_id, name, email, ip, comment, notify }) { // eslint-disable-line
     return userModel.save(name, email)
     .then(([userId, userName, userEmail, userMd5Email, userSecret]) => { // eslint-disable-line
         return articleModel.save(slug)
         .then(
             articleId => notificationModel.save(userId, articleId, notify)
             .then(() => commentModel.save(articleId, userId, ip, comment, parent_id))
             .then((commentId) => { // eslint-disable-line
                 return {
                     userName,
                     userEmail,
                     userMd5Email,
                     userSecret,
-                    commentId
+                    commentId,
+                    slug
                 };
             })
         );
     });
 }

…

Inverser les critères

L’option --not permet d’inverser les critères. Tous les critères ne sont pas impactés, il s’agit seulement des critères de types ref. Donc --branches, --tags, --remotes et --glob. Le --not suivant rencontré par la CLI restaure le comportement normal pour les options suivantes… jusqu’au prochain --not s’il y en a un.

Grâce à la négation, on peut par exemple rapidement récupérer tous les commits n’ayant pas été pushés et ce, quelle que soit la branche sur laquelle ils se trouvent.

git log --branches --not --remotes

shortlog, le log résumé

Il est possible d’obtenir un résumé des modifications introduites par auteur. Il faut pour cela utiliser la commande git shortlog. Ci-dessous un exemple dans le repo Git Emojis.

git shortlog
Buzut (20):
      🎉 first commit
      📦 (git) add gitignore
      💄 add space bewteen emoji and scope
      ✏️  add licence
      🚑 add exec rights to hooks (won't work otherwise)
      🚑 avoid composed chars as they are problematic
      📖 add a one-liner install method
      💄 change icon for revert for a more expressive one
      📖 fix typo
      📖 add an exemple on how to automatically integrate to projects
      📖 update docs
      Merge pull request #1 from Exilko/master
      📖 better exemple & explanations
      🚑 add exit 0 if no git dir for npm not to error
      💄 update licence year
      📖 update licence year
      📖 add hooksPath information
      Merge branch 'hooks-distribution'
      💄 capitalize title
      💄 pluralize hook (hooks)

Exilko (1):
      📖 fix typo

Afficher les différences entre snapshots

Il est assez courant de devoir comparer les différences qu’il y a entre plusieurs commits, branches ou tags. L’outil dédié à cet usage possède le nom assez évocateur de diff.

# Dans sa forme la plus simple, cette commande affiche les différences entre
# le working directory et le commit précédent (ou la staging si un git add a déjà été fait)
git diff

# L'option --cached (ou --staged) permet d'afficher la différence entre la staging et le commit précisé (ou HEAD)
git diff --staged

# On peut voir les différences à la fois du working directory et de la staging en précisant HEAD
git diff HEAD

# On peut restreindre le diff à un chemin
git diff <path>

# On peut aussi obtenir la différence avec un commit spécifique
git diff <commit> [<path>]

# Ou deux commits entre eux
git diff <commit> <commit> [<path>]

Il est aussi possible de comparer deux fichiers du répertoire de travail avec --no-index.

git diff --no-index <path> <path>

On peut même l’utiliser pour des fichiers non gérés par Git, auquel cas, il n’est même pas utile de préciser --no-index.

Par ailleurs, il est à noter que dans la lecture du diff, dans le cas de git diff commitA commitB c’est comme si nous demandions à Git : “qu’est-ce que commitB change par rapport à commitA ?”. Ainsi, si commitB ajoute des éléments, ils seront précédés d’un + et inversement, s’il en supprime, ils seront précédés d’un -.

En revanche, si on invere les commits dans la commande, alors on inverse la question et ce qui a été ajouté dans commitB sera précédé du signe moins et inversement.

Prenons le commit 9777310 en exemple – il remplace les slashs par des underscores – et comparons-le au précédent.

git diff 9777310 b075c22
diff --git a/libs/cacheFilesGenerators.js b/libs/cacheFilesGenerators.js
index ab793f0..48e108c 100644
--- a/libs/cacheFilesGenerators.js
+++ b/libs/cacheFilesGenerators.js
@@ -31,7 +31,7 @@ function generateWebsiteInfos() {
  * @return { Promise }
  */
 function writeCacheFile(slug, content) {
-    const path = config.cacheDirs.article + slug.replace(/\//g, '_') + config.cacheDirs.ext;
+    const path = config.cacheDirs.article + cleanSlug(slug) + config.cacheDirs.ext;
     return writeFileP(path, JSON.stringify(content));
 }

La question est alors : qu’est-ce que b075c22 modifie par rapport à 9777310 ? Si on veut plutôt savoir ce que le commit 9777310 ajoute par rapport au commit précédent, c’est dans l’autre sens qu’il faut donner les commits à diff.

git diff b075c22 9777310
diff --git a/libs/cacheFilesGenerators.js b/libs/cacheFilesGenerators.js
index 48e108c..ab793f0 100644
--- a/libs/cacheFilesGenerators.js
+++ b/libs/cacheFilesGenerators.js
@@ -31,7 +31,7 @@ function generateWebsiteInfos() {
  * @return { Promise }
  */
 function writeCacheFile(slug, content) {
-    const path = config.cacheDirs.article + cleanSlug(slug) + config.cacheDirs.ext;
+    const path = config.cacheDirs.article + slug.replace(/\//g, '_') + config.cacheDirs.ext;
     return writeFileP(path, JSON.stringify(content));
 }

Cette fois, la réponse est inversée, c’est logique. Tout cela semble simple, mais lors de diff complexes, si nous ne sommes pas sur de l’ordre de présentation, on peut vite être dérouté.

Tout comme log, cette commande accepte --stat et --name-only si l’on est pas intéressé par l’intégralité du path. En outre, lorsque l’on cherche des modifications inter-lignes, on peut avantageusement utiliser --color-words. Ainsi, les changements seront mis en évidence au sein de la ligne plutôt qu’en deux lignes séparées.

Afficher le responsable de chaque modif d’un fichier

git blame affiche ligne par ligne l’auteur, la date et le commit qui a introduit un changement. On peut lui adjoindre quelques options fort pratiques :

Afficher le détail d’un commit

git show permet de voir dans le détail l’apport d’un ou plusieurs commits.

git show [<commit>] [path]

La commande prend en paramètre un ou plusieurs attributs désignants des révisions (commits, branches, tags…). En l’absence de paramètres, HEAD sera utilisée. Si l’on veut limiter l’affichage aux modifications d’un seul fichier, on peut alors le passer en paramètre.

# Dans le projet Jamments, voir ce qui a été fait lors de l'ajout de Mailjet
git show 7eb13be
commit 7eb13bef1b2b03d4c993f61d3d3cdc61215dec08
Author: Buzut <blog@buzut.fr>
Date:   Fri May 31 12:38:50 2019 +0200

    🌟 add mailjet as an email sender

diff --git a/config.dist.js b/config.dist.js
index a07fe5e..fa668cd 100644
--- a/config.dist.js
+++ b/config.dist.js
@@ -24,6 +24,8 @@ module.exports = {
     },

     email: {
+        // You can use either SMTP or Mailjet, keep only one config object
+
         // check options at https://www.npmjs.com/package/emailjs
         server: {
             user: 'hello@my-blog.net',
@@ -32,15 +34,27 @@ module.exports = {
             ssl: true
             // tls: { ciphers: 'SSLv3' }
         },
+
+        // if you prefer to use mailjet
+        mailjet: {
+            pubkey: 'xxxxx',
+            privkey: 'xxxxx'
+        },
…

Une option parfois utile est de visionner le contenu d’un fichier tel qui l’est dans l’index – lorsqu’il a été modifié dans le répertoire de travail. Pour cela, on préfixe le chemin du fichier par :.

Si pour une raison ou une autre, nous voulons supprimer le diff de la sortie, il suffit d’utiliser l’option -s (--no-patch dans sa version longue). Par ailleurs, cette commande accepte les mêmes options que la commande log pour la présentation des messages de commit (--oneline etc).

Restaurer l’historique

Sujet souvent confus pour les néophytes, revenir à un point de commit antérieur est pourtant d’une grande utilité. On utilisera pour cela la commande switch ou la classique commande checkout.

# Nous voulons revenir à l'état du commit 7eb13be, précédemment utilisé avec "show"
# Comme un commit n'est pas une branche, on passe l'option -d (pour --detach) afin de remonter le temps
git switch -d 7eb13be
HEAD is now at 7eb13be 🌟 add mailjet as an email sender

Comme déjà mentionné dans un précédent chapitre, checkout ayant de trop nombreux usages, elle s’est vu remplacée par plusieurs autres commandes plus spécifiques. Cela permet de réduire la confusion entourant les usages de checkout. Elle reste cependant tout à fait fonctionnelle pour ceux en ayant l’habitude, il faudrait alors faire git checkout 7eb13be.

Detached HEAD

Soyez rassuré, rien à voir avec la décapitation. Comme nous l’avons vu dans le chapitre sur les branches, Git possède un pointeur nommé HEAD, lequel référence en temps normal la pointe d’une branche ou un tag. Lorsque ce n’est pas le cas, HEAD référence alors un commit arbitraire, on dit alors que HEAD est détaché : detached HEAD.

Pour s’en assurer, il suffit de faire un status ou un branch, Git nous l’indiquera alors immédiatement.

git status
HEAD detached at 7eb13be
nothing to commit, working tree clean

# Nous ne sommes sur aucune branche
git branch
* (HEAD detached at 7eb13be)
  master

Dans cette situation, il est possible d’effectuer des changements et de les commiter, cependant, comme ces derniers n’appartiennent à aucune branche, Git n’est pas en mesure d’automatiquement déplacer HEAD afin d’ajouter le nouveau commit à la pointe d’une branche. De ce fait, à moins de pouvoir se souvenir de son emprunte SHA1, il sera assez difficile de s’y référer plus tard.

On pourra alors créer une branche à partir de ce nouveau point dans l’historique du projet. De cette manière, on ré-attache HEAD et il sera facile d’y revenir plus tard.

Que l’on effectue ou non des modifications, afin de revenir à un état antérieur, il suffit de préciser à Git que l’on veut aller sur une branche, qui peut être celle sur laquelle on était avant de détacher HEAD. git switch master par exemple nous ramène donc dans le “présent”.

Cependant, en faisant cela, toutes les modifications que l’on aurait effectuées seront perdues. Si l’on veut les garder, il faut alors créer une branche à partir de la HEAD détachée.

# Création d'une nouvelle branche
git branch <new-branch>

# HEAD est toujours détachée puisqu'on a créé la branche
# mais on ne s'y est pas placé avec switch
# On peut donc continuer nos expérimentations dans mode détaché
# ou utiliser switch pour se rendre sur notre nouvelle branche
git switch <branch>

Enfin, une alternative à toutes ces manipulations existe. Il s’agit de directement créer la branche recevant le snapshot que l’on veut restaurer/explorer. Les branches étant une ressource disponible à peu de frais, on peut sans crainte créer une branche à la volée puis la supprimer lorsqu’elle ne nous est plus utile. Cela nous dispense de l’état de detached HEAD.

git switch <new-branch> <commit>

Restaurer un fichier à un état antérieur

Parfois, nous n’avons pas besoin de revisualiser l’ensemble du projet à un point antérieur, mais de simplement restaurer un fichier spécifique à son état d’un précédent commit. Là encore, c’est tout à fait possible. On utilisera pour cela restore – bien qu’il soit également possible d’utiliser checkout ou reset.

# On ramène tables.sql à son état lors du commit dfa88af
git restore --source dfa88af tables.sql
Unstaged changes after reset:
M    tables.sql

De même que pour switch et checkout, Git a introduit la commande restore à la place de reset pour une plus grande clareté d’usage. Cependant, de même que checkout, reset reste tout à fait fonctionnel et permet la même chose. Nous verrons dans le chapitre sur les usages avancés de Git les différences entre ces commandes.

Réécrire l’histoire

Si vous êtes amateurs de voyages temporels et de l’effet papillon, vous êtes au bon endroit ! Nous avons déjà travaillé avec rebase dans le chapitre sur les branches, mais cette commande a plus d’un tour dans son sac. Le rebase interractif permet en effet de :

Comme le définit la documentation, rebase permet de réécrire l’histoire. On précise à rebase jusqu’à quel commit on souhaite remonter. On peut pour cela le préciser de manière absolue avec un hash de commit ou de manière relative avec la notation HEAD~n – où n représente le nième ancêtre de HEAD. Notez par ailleurs qu’on précise le parent du dernier commit que l’on devra modifier.

Si vous ne précisez pas de point de rebasage, git rebase -i n’incluera aucun commit si votre branche est à jour avec l’upstream. En revanche, si votre dépôt local est en avance sur l’upstream, alors rebase remontera au plus ancien commit qui n’a pas été pushé.

# Remontons jusqu'au dernier commit avant les merge de "depandabot"
git rebase -i fac6595
pick a3f3b13 Bump knex from 0.16.3 to 0.19.5
pick 4ea3fbb Bump mixin-deep from 1.3.1 to 1.3.2
pick 285fb22 Bump eslint-utils from 1.3.1 to 1.4.3
pick d949bc8 Bump js-yaml from 3.12.2 to 3.13.1
pick 69e08b3 🚑 add missing return
pick a978ce3 Bump lodash from 4.17.11 to 4.17.15

# Rebase fac6595..a978ce3 onto d949bc8 (6 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Contrairement au log, les commits sont ici listés par odre chronologique. C’est l’ordre dans lequel Git les modifiera si vous le lui demandez. Git vous présente en fait en quelque sorte son script de rebase et il faut lui indiquer pour chacun des commits listés, l’action à exécuter. Passons nos options en revue :

Il y a également label, reset et merge, que l’on utilise que très rarement voir jamais – en tout cas pour ma part. Je vous laisse donc vous référer à la doc si vous en rencontrez le besoin.

Via le rebase interactif, vous pouvez donc réordonner vos commits simplement en changeant l’ordre des lignes, les fusionner, les modifier ou en changer le message simplement en remplaçant pick par le mot-clef adéquat.

# Nous allons ici fusionner les derniers commits de dependabot
r a3f3b13 Bump knex from 0.16.3 to 0.19.5
s 4ea3fbb Bump mixin-deep from 1.3.1 to 1.3.2
s 285fb22 Bump eslint-utils from 1.3.1 to 1.4.3
s d949bc8 Bump js-yaml from 3.12.2 to 3.13.1
pick 69e08b3 🚑 add missing return
pick a978ce3 Bump lodash from 4.17.11 to 4.17.15

# Git s'arrête d'abord au niveau du commit a3f3b13 pour nous permettre d'éditer le message
# Puis une nouvelle fois pour nous permettre d'éditer le message du nouveau commit incluant a3f3b13 et ses 4 ancêtres
# On contrôle ensuite le résultat
git log --oneline --all
cc80ab4 (HEAD -> master) Bump lodash from 4.17.11 to 4.17.15
d4d40a3 🚑 add missing return
092fddd Update dependencies for security reasons
a978ce3 (origin/master, origin/HEAD) Bump lodash from 4.17.11 to 4.17.15

Vous ne l’avez peut être pas remarqué, mais comme dans tout rebase, tous les commits affectés sont de nouveaux commits, même lorsqu’ils ne sont pas directement modifiés. À partir du moment où un parent est édité, tous les commits ultérieurs à celui-ci seront ré-écrits. Ainsi Bump lodash from 4.17.11 to 4.17.15, qui n’a pas été modifié, n’a plus la même emprunte après le rebase.

C’est pour cette raison que, de la même manière que lors d’un rebase classique, il faut être très vigilant à ce que l’on fait et ne pas modifier l’hitorique d’un projet lorsqu’il a été poussé sur un dépôt partagé.

revert, le civilisé

Il est également possible d’annuler les changements d’un commit dans un nouveau commit. Ceci est particulièrement adapté lorsque les changements ont déjà été poussés sur un repo distant. Dans un tel cas, si d’autres personnes travaillent sur ce même repo, vous risquez fortement d’aboutir à un historique divergeant et ça… c’est le mal.

La commande revert permet donc d’annuler les effets d’un commit en faisant une nouvelle insertion dans l’historique. Ainsi, si d’autres personnes ont ajouté des commits entre temps, elles n’auront qu’à faire un git merge, rien de plus simple !

git revert e581386

filter-branch, le hooligan

filter-branch est à utiliser avec la plus grande prudence. La documentation même de Git met en garde sur ses nombreux écueils : peut laisser un dépôt corrompu, peut être très très lent etc. Il est même recommandé d’utiliser un script Python à la place.

Malgré tout, s’avère parfois bien utile. L’exemple le plus utile est celui permettant d’effacer un fichier de tous les commits. Un petit oubli d’un fichier confidentiel par exemple.

# Effacement de config/prodConfig.js de tous les commits du dépôt
git filter-branch --index-filter \
    'git rm --cached --ignore-unmatch config/prodConfig.js' \
    --tag-name-filter cat -- --all

Pour de plus amples informations et d’autres exemles, je vous laisse entre les mains expertes de la documentation officielle.

Commentaires

Rejoignez la discussion !

Vous pouvez utiliser Markdown pour les liens [ancre de lien](url), la mise en *italique* et en **gras**. Enfin pour le code, vous pouvez utiliser la syntaxe `inline` et la syntaxe bloc

```
ceci est un bloc
de code
```