Chapitre 5. Les leçons de l’histoire

L’une des conséquences de la nature distribuée de Git est qu’il est facile de modifier l’historique. Mais si vous réécrivez le passé, faites attention : ne modifiez que la partie de l’historique que vous êtes le seul à posséder. Sinon, comme des nations qui se battent éternellement pour savoir qui a commis telle ou telle atrocité, si quelqu’un d’autre possède un clone dont l’historique diffère du vôtre, vous aurez des difficultés à vous réconcilier lorsque vous interagirez.

Certains développeurs insistent très fortement pour que l’historique soit considéré comme immuable. D’autres pensent au contraire que les historiques doivent être rendus présentables avant d'être présentés publiquement. Git s’accommode des deux points de vue. Comme les clones, les branches et les fusions, la réécriture de l’historique est juste un pouvoir supplémentaire que vous donne Git. C’est à vous de l’utiliser à bon escient.

Je me corrige…

Que faire si vous avez fait un commit mais que vous souhaitez y attacher un message différent ? Pour modifier le dernier message, tapez :

$ git commit --amend

Vous apercevez-vous que vous avez oublié un fichier ? Faites git add pour l’ajouter puis exécutez la commande ci-dessus.

Voulez-vous ajouter quelques modifications supplémentaires au dernier commit ? Faites ces modifications puis exécutez :

$ git commit --amend -a

… et bien plus

Supposons que le problème précédent est dix fois pire. Après une longue séance, vous avez effectué une série de commits. Mais vous n'êtes pas satisfait de la manière dont ils sont organisés et certains des messages associés doivent être revus. Tapez alors :

$ git rebase -i HEAD~10

et les dix derniers commits apparaissent dans votre $EDITOR favori. Voici un petit extrait :

pick 5c6eb73 Added repo.or.cz link
pick a311a64 Reordered analogies in "Work How You Want"
pick 100834f Added push target to Makefile

Ensuite :

  • Supprimez un commit en supprimant sa ligne.
  • Réordonnez des commits en réordonnant leurs lignes.
  • Remplacez pick par :

    • edit pour marquer ce commit pour amendement.
    • reword pour modifier le message associé.
    • squash pour fusionner ce commit avec le précédent.
    • fixup pour fusionner ce commit avec le précédent en supprimant le message associé.

Sauvegardez et quittez. Si vous avez marqué un commit pour amendement alors tapez :

$ git commit --amend

Sinon, tapez :

$ git rebase --continue

Donc faites des commits très tôt et faites-en souvent : vous pourrez tout ranger plus tard grâce à rebase.

Les changements locaux en dernier

Vous travaillez sur un projet actif. Vous faites quelques commits locaux puis vous vous resynchronisez avec le dépôt officiel grâce à une fusion (merge). Ce cycle se répète jusqu’au moment où vous êtes prêt à pousser vos contributions vers le dépôt central.

Mais à cet instant l’historique de votre clone Git local est un fouillis infâme mélangeant les modifications officielles et les vôtres. Vous préféreriez que toutes vos modifications soient contiguës et se situent après toutes les modifications officielles.

C’est un boulot pour git rebase comme décrit ci-dessus. Dans la plupart des cas, vous pouvez utilisez l’option --onto et éviter les interactions.

Lisez git help rebase pour des exemples détaillés sur cette merveilleuse commande. Vous pouvez scinder des commits. Vous pouvez même réarranger des branches de l’arbre.

Réécriture de l’histoire

De temps en temps, vous avez besoin de faire des modifications équivalentes à la suppression d’une personne d’une photo officielle, la gommant ainsi de l’histoire d’une manière quasi Stalinienne. Supposons que vous ayez publié un projet mais en y intégrant un fichier que vous auriez dû conserver secret. Par exemple, vous avez accidentellement ajouté un fichier texte contenant votre numéro de carte de crédit. Supprimer ce fichier n’est pas suffisant puisqu’il pourra encore être retrouvé via d’anciennes versions du projet. Vous devez supprimer ce fichier dans toutes les versions :

$ git filter-branch --tree-filter 'rm top/secret/fichier' HEAD

La documentation git help filter-branch explique cette exemple et donne une méthode plus rapide. De manière générale, filter-branch vous permet de modifier des pans entiers de votre historique grâce à une seule commande.

Après cela, le dossier .git/refs/original contiendra l'état de votre dépôt avant l’opération. Vérifiez que la commande filter-branch a bien fait ce que vous souhaitiez puis effacer ce dossier si vous voulez appliquer d’autres commandes filter-branch.

Finalement, remplacez tous les clones de votre projet par votre version révisée si vous voulez pouvoir interagir avec eux plus tard.

Faire l’histoire

Voulez-vous faire migrer un projet vers Git ? S’il est géré par l’un des systèmes bien connus alors il y a de grandes chances que quelqu’un ait déjà écrit un script afin d’importer l’ensemble de l’historique dans Git.

Sinon, regarder du côté de git fast-import qui lit un fichier texte dans un format spécifique pour créer un historique Git à partir de rien. Typiquement un script utilisant cette commande est un script jetable qui ne servira qu’une seule fois pour migrer le projet d’un seul coup.

À titre d’exemple, collez le texte suivant dans un fichier temporaire (/tmp/historique) :

commit refs/heads/master
committer Alice <alice@example.com> Thu, 01 Jan 1970 00:00:00 +0000
data <<EOT
Commit initial
EOT

M 100644 inline hello.c
data <<EOT
#include <stdio.h>

int main() {
  printf("Hello, world!\n");
  return 0;
}
EOT


commit refs/heads/master
committer Bob <bob@example.com> Tue, 14 Mar 2000 01:59:26 -0800
data <<EOT
Remplacement de printf() par write().
EOT

M 100644 inline hello.c
data <<EOT
#include <unistd.h>

int main() {
  write(1, "Hello, world!\n", 14);
  return 0;
}
EOT

Puis créez un dépôt Git à partir de ce fichier temporaire en tapant :

$ mkdir projet; cd projet; git init
$ git fast-import --date-format=rfc2822 < /tmp/historique

Vous pouvez extraire la dernière version de ce projet avec :

$ git checkout master .

La commande git fast-export peut convertir n’importe quel dépôt Git en un fichier au format git fast-import ce qui vous permet de l'étudier pour écrire des scripts d’exportation mais vous permet aussi de transporter un dépôt dans un format lisible. Ces commandes permettent aussi d’envoyer un dépôt via un canal qui n’accepte que du texte pur.

Qu’est-ce qui a tout cassé ?

Vous venez tout juste de découvrir un bug dans une fonctionnalité de votre programme et pourtant vous êtes sûr qu’elle fonctionnait encore parfaitement il y a quelques mois. Zut ! D’où provient ce bug ? Si seulement vous aviez testé cette fonctionnalité pendant vos développements.

Mais il est trop tard. En revanche, en supposant que vous avez fait des commits suffisamment souvent, Git peut cerner le problème.

$ git bisect start
$ git bisect bad HEAD
$ git bisect good 1b6d

Git extrait un état à mi-chemin entre ces deux versions (HEAD et 1b6d). Testez la fonctionnalité et si le bug se manifeste :

$ git bisect bad

Si elle ne se manifeste pas, remplacer "bad" (mauvais) par "good" (bon). Git vous transporte à nouveau dans un état à mi-chemin entre la bonne et la mauvaise version, en réduisant ainsi les possibilités. Après quelques itérations, cette recherche dichotomique vous amènera au commit où le bug est survenu. Une fois vos investigations terminées, retourner à votre état original en tapant :

$ git bisect reset

Au lieu de tester chaque état à la main, automatisez la recherche en tapant :

$ git bisect run mon_script

Git utilise la valeur de retour du script fourni pour décider si un état est bon ou mauvais : mon_script doit retourner 0 si l'état courant est ok, 125 si cet état doit être sauté et n’importe quelle valeur entre 1 et 127 si l'état est mauvais. Une valeur négative abandonne la commande bisect.

Vous pouvez faire bien plus : la page d’aide explique comment visualiser les bisects, comment examiner ou rejouer le log d’un bisect et comment éliminer des changements que vous savez sans conséquence afin d’accélérer la recherche.

Qui a tout cassé ?

Comme de nombreux systèmes de gestion de versions, Git a sa commande blame :

$ git blame bug.c

Cette commande annote chaque ligne du fichier afin de montrer par qui et quand elle a été modifiée la dernière fois. À l’inverse de la plupart des autres systèmes, cette commande marche hors-ligne et ne lit que le disque local.

Expérience personnelle

Avec un système de gestion de versions centralisé, la modification de l’historique est une opération difficile et faisable uniquement par les administrateurs. Créer un clone, créer une branche ou en fusionner plusieurs sont des opérations impossibles à réaliser sans communication réseau. Il en est de même pour certains opérations basiques telles que parcourir l’historique ou intégrer une modification. Avec certains systèmes, des communications réseaux sont même nécessaires juste pour voir ses propres modifications ou pour ouvrir un fichier avec le droit de modification.

Ces systèmes centralisés empêchent le travail hors-ligne et nécessitent une infrastructure réseau d’autant plus lourde que le nombre de développeurs augmentent. Plus important encore, certaines opérations deviennent si lentes que les utilisateurs les évitent à moins qu’elles soient absolument indispensables. Dans les cas extrêmes cela devient vrai même pour les commandes les plus basiques. Lorsque les utilisateurs doivent effectuer des opérations lentes, la productivité souffre des interruptions répétées.

J’ai moi-même vécu ce phénomène. Git a été le premier système de gestion de versions que j’ai utilisé. Je me suis vite accoutumé à lui, tenant la plupart de ses fonctionnalités pour acquises. Je pensais que les autres systèmes étaient similaires : le choix d’un système de gestion de versions ne devait pas être bien différent du choix d’un éditeur de texte ou d’un navigateur web.

J’ai été très surpris lorsque, plus tard, il m’a fallu utilisé un système centralisé. Une liaison internet épisodique importe peu avec Git mais rend le développement quasi impossible lorsque le système exige qu’elle soit aussi fiable que les accès au disque local. De plus, je me restreignais afin d'éviter certaines commandes trop longues, ce qui m’empêchait de suivre ma méthode de travail habituelle.

Lorsqu’il me fallait utiliser ces commandes lentes, cela interrompait mes réflexions et avait des effets pervers. En attendant la fin des communications avec le serveur, je me lançais dans autre chose pour passer le temps comme lire mes mails ou écrire de la documentation. Lorsque je revenais à mon travail initial, la commande s'était terminée depuis longtemps et je perdais du temps à retrouver le fil de mes pensées. Les être humains ne sont pas bons pour changer de contexte.

Il y a aussi un effet intéressant du type « tragédie des biens communs » : afin d’anticiper la congestion du réseau, certains vont consommer plus de bandes passantes que nécessaire pour effectuer des opérations visant à réduire leurs attentes futures. Ces efforts combinés vont encore augmenter la congestion, incitant ces personnes à consommer encore plus de bande passante pour éviter ces délais toujours plus longs.