Dans un dépôt Git utilisé pour déployer plusieurs applications, chaque mise en production finit parfois par laisser une trace sous forme de tag. Sur le papier, c'est pratique : le nom du tag décrit l'application, l'environnement, la date et parfois l'identifiant du commit. On se retrouve alors avec un historique assez clair des versions déployées.
Sauf que, avec le temps, ça se dégrade. Les milliers de références s'empilent, les commandes deviennent plus bruyantes, et certaines opérations peuvent se ralentir selon la configuration et la version de Git. Et surtout, répondre à des questions du quotidien (« quelle est la dernière version de telle app en prod ? » ou « sur quel tag faut-il revenir ? ») revient souvent à filtrer, puis trier, sur une longue liste.
Une autre approche consiste à représenter les déploiements comme une suite de commits reliés entre eux. Dans ce modèle, une seule référence pointe vers le déploiement le plus récent pour chaque couple application-environnement. Les déploiements précédents se récupèrent en remontant l'historique. L'idée est simple : s'appuyer sur une structure que Git sait déjà parcourir et interroger.
Voici comment ça marche, ce que ça simplifie, et ce qu'il faut garder en tête avant de l'adopter. L'objectif n'est pas de supprimer tous les tags, mais de choisir une représentation quand l'historique des déploiements devient un vrai flux opérationnel.
Pourquoi des milliers de références deviennent un problème
Le coût d'un tag par déploiement
Un tag de déploiement peut contenir pas mal d'infos dans son nom : environnement, application, classe de serveurs, date, identifiant du commit ou de l'arbre déployé. C'est lisible, facile à créer, et pratique pour des scripts. Pour une petite application déployée ponctuellement, garder un tag par version ne pose généralement pas de souci.
À grande échelle, le nombre de références augmente directement avec le nombre de déploiements. Dans un cas décrit par Mark Dominus (monorepo avec plusieurs applications, déployées régulièrement en préproduction et en production), les tags finissent par s'accumuler par milliers. Il explique qu'à son époque, certaines opérations liées à la résolution des noms de références devenaient nettement plus lentes, côté développeur comme côté serveur.
Retenez surtout ceci : les performances dépendent de la version de Git, de la façon dont les références sont stockées, de la configuration serveur et de l'usage réel du dépôt. On ne peut pas conclure « quelques milliers de tags = lenteur partout et tout le temps ». La décision doit venir de mesures concrètes, pas d'un seuil théorique.
Et ce n'est pas que la vitesse. Une longue liste de tags encombre aussi les commandes du quotidien (y compris git fetch) et rend moins lisibles les références qu'on utilise vraiment. Une opération une fois, c'est supportable. La répéter à chaque déploiement, sur plusieurs applis, pendant des années, change l'équation.
Des métadonnées codées dans le nom
Un nom du style stg.www.notre-application-... contient des informations compréhensibles par les humains, mais pas par Git comme des champs structurés. Git sait vers quel objet pointe le tag, mais il ne vérifie pas que « ce que le nom raconte » correspond à l'arbre réellement déployé.
C'est un point important pour l'audit. Si l'automatisation construit mal le nom, ou si la référence est déplacée, ce que vous voyez dans l'étiquette peut ne plus coller à la réalité de ce qui est pointé. Le problème n'est pas que les tags soient mauvais : ils sont utiles pour nommer des versions. Le problème, c'est qu'une convention dans le nom ne fournit pas, à elle seule, une garantie de cohérence pour des métadonnées externes.
Autre détail souvent oublié : les tags ne sont pas « intangibles ». On peut les supprimer ou les recréer. Si l'objectif est une trace d'audit fiable, il faut donc définir des règles de conservation, des droits, et des contrôles. La représentation choisie ne remplace pas cette gouvernance.
Une question métier difficile à poser
Prenons une question simple : « Quand l'application web a-t-elle été déployée pour la dernière fois en production ? » Avec des références nommées séparément, l'approche habituelle consiste à lister, filtrer sur l'environnement et l'application, puis trier pour trouver la date la plus récente.
Revenir à la version précédente implique de refaire la même mécanique (filtrer, retrouver, sélectionner le bon tag). Une procédure automatisée peut le faire, mais elle dépend alors de la forme exacte des noms et de la gestion des cas particuliers. Au fond, le besoin réel n'est pas « créer plus d'objets dans Git », mais répondre vite et sans ambiguïté aux questions d'exploitation.
Le modèle : un historique de commits par application
Une référence pour chaque couple environnement-application
Dans ce modèle, on remplace « un tag par déploiement » par « un historique de commits pour chaque couple environnement-application ». Concrètement, une référence du type refs/release/stg.www.notre-application pointe vers le commit de métadonnées du déploiement le plus récent. Les déploiements plus anciens sont accessibles en remontant l'historique.
À chaque nouveau déploiement, on crée un commit de métadonnées avec deux parents :
- le commit de métadonnées du déploiement précédent (premier parent) ;
- le commit réellement déployé (deuxième parent).
Le premier parent sert à relier les événements dans l'ordre chronologique. Le second parent, lui, pointe sans ambiguïté vers le code associé à l'événement. On ne touche donc pas à l'historique « dev » de l'application : on crée une chaîne distincte, dédiée aux déploiements.
Résultat : la référence reste unique pour le couple donné, peu importe le nombre de déploiements. Après mille mises en production, elle pointe toujours vers le dernier commit de métadonnées, et les événements antérieurs restent récupérables depuis là.
Le commit de métadonnées comme « enveloppe » d'audit
Un commit Git contient déjà des métadonnées : auteur, date, message, et arbre. La date du commit peut servir de date d'enregistrement de l'événement, à condition que l'horloge et la création des commits soient suffisamment maîtrisées. Le message peut aussi porter des champs structurés.
Par exemple, une équipe peut écrire des lignes type application, environnement, identifiant-de-pipeline, version-de-configuration. Des outils comme git-interpret-trailers aident à structurer ces infos dans le message sans inventer un format exotique. Si les données deviennent plus complexes, le commit peut pointer vers un arbre contenant un fichier de métadonnées dédié.
Dans la variante décrite à l'origine, l'arbre du commit de métadonnées peut être identique à celui de la version déployée. C'est pratique pour certaines inspections. Mais ce n'est pas une obligation : l'arbre peut aussi contenir des infos propres au déploiement. Le point clé, c'est d'écrire une convention claire pour le format et de s'assurer que les outils qui publient les déploiements la produisent de façon cohérente.
Pourquoi l'ordre des parents compte
Dans Git, l'ordre des parents n'est pas décoratif : c'est ce qui permet aux outils et aux scripts de comprendre « qui est qui ». Ici, le premier parent correspond à l'événement précédent dans la chaîne, et le deuxième parent correspond au code déployé. Ça permet des expressions simples, mais ça impose une règle stricte à tous les outils qui créent ou lisent ces commits.
Si un outil inverse les parents, les commandes d'inspection et de retour arrière ne viseront plus les objets attendus. Oui, Git acceptera toujours l'historique en tant que structure valide. Mais vous perdrez le contrat « métier » défini par l'équipe. Mieux vaut donc traiter l'ordre des parents comme une partie du schéma de données : le tester, l'automatiser, et le documenter près du code qui publie les déploiements.
Créer, consulter et exploiter cet historique
Créer le commit de métadonnées
Supposons que H soit le commit à déployer et que refs/release/stg.www.notre-application pointe déjà sur le commit de métadonnées précédent. L'idée est de créer un nouveau commit avec les deux parents, dans l'ordre attendu :
git commit-tree \
-p refs/release/stg.www.notre-application \
-p H \
'H^{tree}' < /tmp/message-deploiement
La commande renvoie l'identifiant du nouveau commit, noté N. Ensuite, il faut mettre à jour la référence distante pour qu'elle pointe vers N :
git push origin N:refs/release/stg.www.notre-application
Ces commandes montrent le mécanisme, mais ce ne sont pas un script de déploiement complet. En pratique, il faut gérer l'initialisation au premier déploiement, la validation du commit à publier, les erreurs réseau, et les mises à jour concurrentes.
Et surtout : si un push est refusé parce qu'un autre déploiement a avancé la référence, il ne faut pas « contourner » avec un push --force aveugle.
Lire l'historique avec les commandes Git habituelles
Sur un poste de travail, on peut récupérer l'historique dans une référence locale, par exemple HISTORY :
git fetch origin refs/release/stg.www.notre-application:HISTORY
git log HISTORY
git log parcourt les commits de métadonnées du plus récent au plus ancien (ordre chronologique inverse). Ses options permettent de limiter ou de formater l'affichage. Pour rechercher le dernier déploiement antérieur à une date :
git log -1 --until=2026-10-01 HISTORY
Pour afficher l'historique dans l'ordre chronologique, avec les parents et les dates :
git log --reverse --format='%P %cd' HISTORY
Une fois la convention comprise, les expressions de révision vous donnent accès aux deux types de commits. Si HISTORY pointe vers le déploiement le plus récent :
HISTORY^2désigne le commit réellement déployé ;HISTORY~1désigne le commit de métadonnées précédent ;HISTORY~1^2désigne le commit déployé lors de l'événement précédent.
Ça devient utile pour comparer et auditer rapidement. Par exemple, pour comparer les deux dernières versions déployées :
git diff HISTORY~1^2 HISTORY^2
Comme toujours, la référence doit respecter le contrat de parents. Avant d'intégrer ces commandes dans un outil de retour arrière, testez-les sur un dépôt de test et ajoutez des tests automatisés couvrant plusieurs déploiements.
Préparer un retour arrière contrôlé
Pour revenir à une version précédente, le système retrouve d'abord le commit réellement déployé par l'événement antérieur. Ensuite, il crée un nouvel événement de déploiement qui pointe vers ce commit.
L'historique ne disparaît pas : il enregistre aussi le retour en arrière. C'est une différence importante par rapport à une simple réécriture de référence. Ici, le « rollback » devient un événement auditable : on peut savoir quelle version a été déployée, quand, et quelle version l'a précédée.
Le mécanisme permet aussi de redéployer le même objet Git exact, plutôt que de reconstruire une « équivalence » supposée.
Cela dit, une bonne procédure ne s'arrête pas au code. Il faut vérifier les migrations de base de données, la compatibilité de configuration, les secrets, et les changements qui ne se réversent pas proprement juste en « revenant au code ». Git garde l'historique du code déployé ; il ne garantit pas que le système complet est réversible.
Pour aller plus loin, consultez la documentation officielle sur les références Git et le guide de git-rev-parse.
Limites, performances et conditions d'adoption
Un historique linéaire reste un historique à parcourir
Une objection possible : Git devra toujours parcourir les événements passés pour répondre à certaines requêtes. Oui. La nuance, c'est que le parcours se fait sur une chaîne de commits (structure native de l'historique Git), au lieu de rechercher un événement dans une collection de références distinctes.
Dans l'exemple rapporté par l'auteur, Git pouvait parcourir l'historique de développement d'un monorepo (des centaines de milliers de commits) en quelques secondes. Dans ce contexte, quelques centaines ou quelques milliers de déploiements pour une application ne semblaient pas inquiétants.
Mais ne prenez pas ça comme une règle universelle : un serveur différent, une autre configuration, ou un autre type de requête peuvent donner un résultat différent. Là encore, le bon réflexe est de benchmarker sur votre infrastructure.
Il faut aussi distinguer : taille du dépôt, coût de récupération des objets, et coût de résolution des références. Réduire le nombre de références aide à la lisibilité et peut améliorer certaines opérations, mais ça ne garantit pas d'accélérer toutes les commandes. Mesurez ce qui ralentit vraiment, puis comparez avant/après.
Les contraintes opérationnelles à gérer
Le modèle impose ses propres règles. Il marche particulièrement bien si les déploiements sont séquentiels pour chaque couple application-environnement. Si plusieurs pipelines publient en même temps vers la même référence, ils risquent de partir du même parent précédent.
Dans ce cas, il faut gérer explicitement le refus de mise à jour (un push non fast-forward). L'outil peut recharger l'historique, reconstruire le commit de métadonnées depuis la tête courante, ou refuser l'opération pour intervention humaine.
Ensuite, il faut décider clairement :
- qui a le droit de mettre à jour les références ;
- si on interdit la suppression ou la réécriture côté serveur ;
- quelles informations doivent être obligatoires dans le message ou dans l'arbre ;
- combien de temps les objets doivent rester accessibles ;
- comment migrer les tags existants et valider qu'il manque rien.
Les commits structurent l'historique, mais n'empêchent pas l'effacement. La rétention dépend de ce qui rend les objets accessibles, des sauvegardes, et des règles côté serveur. Si vous avez un besoin d'audit réglementaire ou de sécurité, il faudra peut-être compléter Git avec des contrôles d'accès, des journaux externes, ou un stockage dédié à l'archivage.
Quand garder les tags, quand tester cette approche
Les tags restent un bon choix pour nommer des versions « importantes » : une version logicielle publiée, un jalon durable, un repère clair. Ils sont simples à comprendre et largement pris en charge. Donc, non : il n'y a pas de raison de jeter ce qui fonctionne juste parce qu'une autre représentation paraît plus « élégante ».
L'historique de commits devient intéressant quand trois conditions se cumulent : volume de références de déploiement élevé, historique souvent consulté pour comparer ou revenir en arrière, et pipeline capable de respecter une convention stricte de création des commits et de mise à jour des références.
Avant de généraliser, faites un essai sur une application représentative. Mesurez les commandes qui posent problème, vérifiez les fetch depuis un clone neuf, testez un déploiement concurrent et simulez un rollback. Ensuite seulement, comparez la complexité opérationnelle obtenue à celle des tags actuels. Techniquement cohérent ne veut pas dire utile si personne ne veut (ou ne peut) l'exploiter et le maintenir.
Conclusion
Garder un tag par déploiement est simple au début, mais ça rend vite le dépôt bruyant et complique les questions d'exploitation à mesure que les versions s'accumulent. L'idée d'une référence unique par couple application-environnement, pointant vers une chaîne de commits de métadonnées, rend l'historique plus « navigable » avec les outils Git habituels.
Ça a aussi un coût : convention stricte sur les parents, gestion des conflits, format des métadonnées et droits de mise à jour. Et surtout : ça ne remplace ni les sauvegardes ni les contrôles d'audit. La décision « raisonnable » consiste à mesurer vos difficultés, prototyper sur un cas représentatif, puis garder les tags quand ils restent le meilleur outil.
Commencez par lister les commandes de déploiement qui posent problème et les questions auxquelles votre équipe doit répondre. Si retrouver une version à déployer ou faire un retour arrière est devenu pénible, testez le modèle sur un service non critique avant de l'étendre à tout le monorepo.
