quaternum.net
Menu

2023-07-05

Commits intermédiaires : pour un historique concis et cohérent

Dans mes pratiques d’écriture et d’édition avec Git, je crée parfois des commits qui n’ont d’intérêt que pour effectuer une sauvegarde de mon travail vers un dépôt distant, générant trop de versions et de messages associés, et produisant un historique peu cohérents. L’utilisation de commits intermédiaires permet de pallier à cela, il s’agit de réécrire l’historique avec l’aide de deux fonctions de Git, git rebase et git squash, qui fonctionnent bien à condition de respecter quelques précautions.

Utiliser Git pour sauvegarder son travail : la fausse bonne idée

Git n’est pas pensé pour effectuer des sauvegardes, puisque tout repose sur les commits et leur cohérence, un commit pouvant correspondre à un travail de quelques minutes ou de plusieurs jours. Pourtant, pour l’écriture de ma thèse, j’effectue parfois des commits qui n’ont pas beaucoup de sens, comme la rédaction d’un tiers d’une sous-partie ou la relecture d’un dixième d’une partie. Un commit est nécessaire pour faire ensuite un push vers un dépôt distant, pour ne pas que les modifications ne restent que sur ma machine. J’effectue des sauvegardes sur des supports externes au mieux une fois par semaine, et mes outils de synchronisation vers des serveurs distants (comme Nexcloud) ne concernent pas mes dépôts Git. Ces commits ne sont donc pas réalisés pour leur pertinence (une tâche terminée), mais pour sauvegarder mon travail ailleurs, et je pense que ce n’est pas très cohérent.

Pourquoi vouloir à tout pris disposer d’un bel historique de versions ? Pour deux raisons principales. La première étant que le fait de d’avoir de nombreux commits a deux effets de bord : la taille du dépôt devient d’autant plus conséquente qu’il y a beaucoup de commits, et certains systèmes fonctionnent mieux avec un nombre limité de commits (typiquement stagit avec moins de 2000 commits). La deuxième raison est la lisibilité du projet pour moi mais aussi pour des personnes extérieures, un commit qui ne concerne que la reformulation d’une phrase vient perturber un historique construit sur des jalons éditoriaux comme la rédaction d’une sous-partie, la correction orthotypographique d’une partie, ou l’ajout de références bibliographiques dans une autre.

Des commits intermédiaires ?

Une solution envisagée, relativement délicate, est de continuer d’effectuer des commits nombreux, mais dans la perspective de les rassembler sous un seul commit qui correspond à une tâche cohérente.

Ainsi voici une série de commits temporaires (avec un préfixe tmp pour signaler leur fonction), avec un commit qui lui n’est pas temporaire. Le commit le plus récent apparaît en premier :

f0bd764 tmp: finalisation de la sous-partie 3.4. avant relecture
a9e9d41 tmp: ajout d'un argument non détaillé
13a0696 tmp: rédaction 2/3
50fd63b tmp: rédaction du premier paragraphe
98f7b3a edit: relecture de 3.3.

Chacun de ces messages décrits ce que j’ai effectué, mais en soit ils n’ont que peu d’intérêt dans un historique plus global. L’idée est donc de réécrire cet historique pour disposer d’un seul message au lieu de quatre, pour aboutir à quelque chose de plus cohérent.

Réécrire l’historique

Git dispose d’une fonction squash qui permet de réécrire l’historique, et ainsi de supprimer des commits sans perdre les modifications associées. Pour effectuer une telle opération, une autre fonction est bien utile, git rebase, qui consiste à récupérer des modifications issues de n’importe quel commit vers un autre état du projet. git rebase peut par ailleurs être utilisé en mode interactif, ce qui permet plus d’options. En utilisant l’exemple ci-dessus, voici la commande à appliquer (l’option -i faisant référence au mode interactif de rebase) :

git rebase -i 98f7b3a

Cette commande consiste à récupérer toutes les modifications depuis le commit le plus récent, f0bd764, pour les rebasées sur le commit souhaité, 98f7b3a. Le mode interactif permet d’accéder à un fichier texte comme celui-ci :

pick 50fd63b tmp: rédaction du premier paragraphe
pick 13a0696 tmp: rédaction 2/3
pick a9e9d41 tmp: ajout d'un argument non détaillé
pick f0bd764 tmp: finalisation de la sous-partie 3.4. avant relecture

Il suffit d’indiquer comment chaque commit doit être géré, en l’occurrence ici il ne s’agit pas de les prendre (pick) mais de les écraser (squash), donc de les supprimer tout en conservant les modifications. La version française de Git indique “utiliser le commit, mais le fusionner avec le précédent”, pour cela il faut remplacer “pick” par “squash”, ou “s” en version abrégée. Sauf pour le commit le plus récent, ce qui est logique car autrement il ne reste plus de commit qui rassemble toutes les modifications précédentes :

pick 50fd63b tmp: rédaction du premier paragraphe
s 13a0696 tmp: rédaction 2/3
s a9e9d41 tmp: ajout d'un argument non détaillé
s f0bd764 tmp: finalisation de la sous-partie 3.4. avant relecture

Un dernier écran s’affiche alors, permettant de préciser les différents messages, voici ce que propose Git par défaut (en version française) :

# Ceci est la combinaison de 4 commits.
# Ceci est le premier message de validation :

tmp: rédaction du premier paragraphe

# Ceci est le message de validation numéro 2 :

tmp: rédaction 2/3

# Ceci est le message de validation numéro 3 :

tmp: ajout d'un argument non détaillé

# Ceci est le message de validation numéro 4 :

tmp: finalisation de la sous-partie 3.4. avant relecture

C’est à ce moment qu’il est possible de modifier ces messages et de n’en conserver qu’un, censé résumer les quatre commits ainsi rassemblés :

# Ceci est la combinaison de 4 commits.
# Ceci est le premier message de validation :

# Ceci est le message de validation numéro 2 :

# Ceci est le message de validation numéro 3 :

# Ceci est le message de validation numéro 4 :

edit: rédaction de 3.4. avec l'ajout d'un argument

Une fois ce fichier enregistré et fermé, Git a terminé l’opération. Pour vérifier que tout est en ordre, un git log permet d’afficher l’historique remanié :

69ecbc2 edit: rédaction de 3.4. avec l'ajout d'un argument
98f7b3a edit: relecture de 3.3.

Toutes les modifications sont conservées, un git diff entre 98f7b3a et 69ecbc2 affiche tous les ajouts et suppressions des quatre commits initiaux, mais en un seul commit. Opération réussie.

Précautions

Réécrire l’historique d’un projet n’est pas rien, c’est pourquoi cela est fortement déconseillé lorsque l’on travaille à plusieurs. En effet si vous décidez sciemment de supprimer des informations (ici des commits), cela peut engendrer des problèmes pour d’autres personnes. D’ailleurs, même sur un dépôt où l’on travaille seul·e, un git push après les opérations présentées ci-dessus ne va pas fonctionner. Il faut forcer l’envoi vers le dépôt distant, Git ne laissant pas modifier un historique potentiellement partagé. Pour cela il faut utiliser la commande git push -f, en prenant conscience que ce n’est pas un acte (éditorial) anodin.

Enfin, pour un travail collectif, la stratégie présentée ici est envisageable à condition d’éditer des fichiers sur une branche sur laquelle personne d’autre n’intervient — ce qui ne peut être garanti qu’avec un protocole éditorial clair.

Pour conclure, adopter ce fonctionnement me semble un compromis acceptable à condition de connaître ces précautions. La réécriture d’un historique des versions permet de mieux identifier les tâches effectuées, en respectant une granularité cohérente avec le projet, et avec des messages de commits lisibles et explicites.