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.
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
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.
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.
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.
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.
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.
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.
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.