Mise à jour : 19 décembre 2024
Lecture : 17 min
Dans la version 2.45.0 de Git, GitLab a introduit le backend « reftable », révolutionnant ainsi le stockage des références. Découvrez en détail le fonctionnement de ce nouveau format.
Jusqu'à récemment, Git ne pouvait stocker des références qu'au format « fichiers ». Avec la version 2.45.0 de Git, Git peut désormais stocker des références au format « reftable ». Ce nouveau format binaire est un peu plus complexe, mais c'est précisément cette complexité qui lui permet de corriger plusieurs lacunes du format « fichiers ». Les objectifs du format « reftable » sont les suivants :
Dans cet article, nous allons examiner le format « reftable » pour comprendre son fonctionnement.
Avant d'entrer dans les détails, récapitulons rapidement la façon dont Git stockait les références jusqu'à présent. Si le sujet vous est familier, vous pouvez passer cette section.
Un dépÎt Git contient deux structures de données importantes :
Les objets, qui contiennent les données réelles de votre dépÎt. Ils incluent les validations (« commits »), la structure d'arborescence des répertoires (« tree ») et les blobs qui contiennent votre code source. Les objets sont organisés de maniÚre à ce qu'ils pointent les uns vers les autres, formant ainsi un graphe des objets. En outre, chaque objet est associé à un identifiant unique, appelé ID d'objet.
Les références, telles que les branches et les étiquettes (« tags »). Ces pointeurs sur des objets du graphe vous permettent de donner des noms à des objets qui sont plus faciles à mémoriser et de suivre les différentes étapes et directions de votre historique de développement. Par exemple, un dépÎt peut contenir une branche main
: il s'agit d'une référence nommée refs/heads/main
qui pointe vers une validation spécifique.
Les références sont stockées dans la base de données des références. Jusqu'à la version 2.45.0 de Git, le format « fichiers » constituait le seul format de base de données disponible. Ce format stocke chaque référence en tant que fichier normal contenant l'un des éléments suivants :
Ces références sont réguliÚrement empaquetées dans un seul fichier packed-refs
afin de faciliter les recherches.
Les exemples suivants illustrent le fonctionnement du format « fichiers » :
$ git init .
$ git commit --allow-empty --message "Initial commit"
[main (root-commit) 6917c17] Initial commit
# HEAD is a symbolic reference pointing to refs/heads/main.
$ cat .git/HEAD
ref: refs/heads/main
# refs/heads/main is a regular reference pointing to a commit.
$ cat .git/refs/heads/main
6917c178cfc3c50215a82cf959204e9934af24c8
# git-pack-refs(1) packs these references into the packed-refs file.
$ git pack-refs --all
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
6917c178cfc3c50215a82cf959204e9934af24c8 refs/heads/main
Si vous avez installé la version 2.45.0 de Git ou une version plus récente, vous pouvez créer un dépÎt au format « reftable » en utilisant l'option --ref-format=reftable
 :
$ git init --ref-format=reftable .
Initialized empty Git repository in /tmp/repo/.git/
$ git rev-parse --show-ref-format
reftable
# Irrelevant files have been removed for ease of understanding.
$ tree .git
.git
âââ config
âââ HEAD
âââ index
âââ objects
âââ refs
â âââ heads
âââ reftable
âââ 0x000000000001-0x000000000002-40a482a9.ref
âââ tables.list
4 directories, 6 files
Regardez la configuration du dépÎt. Remarquez tout d'abord sa clé de configuration extension.refstorage
 :
$ cat .git/config
[core]
repositoryformatversion = 1
filemode = true
bare = false
logallrefupdates = true
[extensions]
refstorage = reftable
Cette configuration indique à Git que le dépÎt a été initialisé avec le format « reftable » et que Git doit utiliser le backend « reftable » pour y accéder.
Curieusement, le dépÎt contient encore des fichiers qui semblent utilisés par le backend « fichiers » :
HEAD
est une référence symbolique dans Git qui indique généralement la branche sur laquelle vous travaillez actuellement. Bien que le format « reftable » n'utilise pas HEAD, la présence de cette référence est nécessaire pour qu'un répertoire soit reconnu comme un dépÎt Git par les clients Git. Par conséquent, lorsque vous utilisez le format « reftable », HEAD
est juste une entité fictive et temporaire dont le contenu est ref: refs/heads/.invalid
.
refs/head
est un fichier dont le contenu est : this repository uses the reftable format
. Les clients Git qui ne connaissent pas le format « reftable » s'attendent généralement à ce que le chemin d'accÚs soit un répertoire. Par conséquent, la création de ce chemin d'accÚs en tant que fichier entraßne intentionnellement l'échec des anciens clients Git s'ils tentent d'accéder au dépÎt par le biais du backend « fichiers ».
Les références réelles sont stockées dans le répertoire reftable/
 :
$ tree .git/reftable
.git/reftable/
âââ 0x000000000001-0x000000000001-794bd722.ref
âââ tables.list
$ cat .git/reftable/tables.list
0x000000000001-0x000000000001-794bd722.ref
Il y a ici deux fichiers :
0x000000000001-0x000000000001-794bd722.ref
est une table contenant des références et des données de reflog dans un format binaire.
tables.list
est, comme son nom l'indique, une liste de tables. Dans l'état actuel du dépÎt, le fichier contient une seule ligne, qui est le nom de la table. Ce fichier suit l'ensemble actuel des tables actives dans la base de données « reftable ». Il est mis à jour chaque fois que de nouvelles tables sont ajoutées au dépÎt.
La mise à jour d'une référence crée une nouvelle table :
$ git commit --allow-empty --message "Initial commit"
[main (root-commit) 1472a58] Initial commit
$ tree .git/reftable
.git/reftable/
âââ 0x000000000001-0x000000000002-eb87d12b.ref
âââ tables.list
$ cat .git/reftable/tables.list
0x000000000001-0x000000000002-eb87d12b.ref
Comme vous pouvez le constater, la table précédente a été remplacée par une nouvelle. En outre, le fichier tables.list
a été mis à jour pour contenir la nouvelle table.
Comme mentionné précédemment, les données réelles de la base de données des références sont contenues dans des tables. Pour faire simple, une table se divise en plusieurs sections :
Chaque type de section est structuré de maniÚre similaire. Les sections contiennent un ensemble d'enregistrements qui sont classés en fonction de la clé de chaque enregistrement. Par exemple, lorsque vous avez ces deux enregistrements de références : refs/heads/aaaaa
et refs/heads/bbb
, vous avez deux enregistrements de références dont les noms de référence sont les clés respectives, et refs/heads/aaaaa
vient avant refs/heads/bbb
.
Chaque section est par ailleurs divisĂ©e en blocs d'une longueur fixe. La longueur d'un bloc est encodĂ©e dans l'en-tĂȘte et remplit deux fonctions :
Intéressons-nous à une section « ref » : elle ressemble à peu prÚs au graphique suivant. Notez comment ses enregistrements sont ordonnés lexicographiquement à l'intérieur des blocs, mais aussi entre différents blocs.
Maintenant que vous disposez de ces informations, vous pouvez localiser un enregistrement en suivant les étapes suivantes :
Effectuez une recherche par dichotomie sur les blocs en regardant les clés de leurs premiers enregistrements respectifs, ce qui permet d'identifier le bloc qui doit contenir l'enregistrement.
Effectuez une recherche linéaire sur les enregistrements de ce bloc.
Ces deux Ă©tapes sont encore quelque peu inefficaces. S'il y a beaucoup de blocs, vous devrez peut-ĂȘtre effectuer une lecture logarithmique d'un bon nombre d'entre eux au cours de la recherche par dichotomie pour trouver celui que vous recherchez. Et lorsque les blocs contiennent de nombreux enregistrements, une recherche linĂ©aire doit potentiellement tous les lire.
Le format « reftable » intÚgre des mécanismes supplémentaires pour résoudre ces problÚmes de performance. Nous les mentionnerons dans les prochaines sections.
Comme vous l'avez peut-ĂȘtre remarquĂ©, toutes les clĂ©s d'enregistrement partagent le mĂȘme prĂ©fixe refs/
. C'est une norme établie dans Git :
refs/heads/
.refs/tags/
.Par conséquent, il est trÚs probable que les enregistrements suivants partagent une partie importante du préfixe de leur clé. C'est une bonne occasion d'économiser de l'espace de stockage précieux. Comme nous savons que la plupart des clés partagent un préfixe commun, il est logique d'utiliser ce dernier à des fins d'optimisation.
Celle-ci passe par la compression de préfixe. Chaque enregistrement encode une longueur de préfixe qui indique au processus qui lit les tables le nombre d'octets à réutiliser à partir de la clé de l'enregistrement précédent. Si vous avez deux enregistrements, refs/heads/a
et refs/heads/b
, ce dernier peut ĂȘtre encodĂ© en spĂ©cifiant une longueur de prĂ©fixe de 11, puis en stockant uniquement le suffixe b
. Le processus qui lit les tables prendra alors les 11Â premiers octets de refs/heads/a
, c'est-Ă -dire refs/heads/
, et y ajoutera le suffixe b
.
Comme expliquĂ© prĂ©cĂ©demment et d'aprĂšs vos connaissances actuelles du format « reftable », une recherche linĂ©aire constitue la meilleure façon de chercher une rĂ©fĂ©rence dans un bloc, car la longueur des enregistrements n'est pas fixe. Il est donc impossible de savoir oĂč les enregistrements devraient commencer sans parcourir l'intĂ©gralitĂ© du bloc depuis le dĂ©but. En outre, mĂȘme si les enregistrements Ă©taient de longueur fixe, il ne serait pas possible de rechercher au milieu d'un bloc, car la compression de prĂ©fixe nous oblige Ă©galement Ă lire les enregistrements prĂ©cĂ©dents.
Une recherche linĂ©aire serait assez inefficace, dans la mesure oĂč les blocs peuvent contenir des centaines, voire des milliers d'enregistrements. Pour rĂ©soudre ce problĂšme, le format « reftable » encode des « restart points » dans chaque bloc. Les « restart points » sont des enregistrements non compressĂ©s oĂč la compression de prĂ©fixe est rĂ©initialisĂ©e. Par consĂ©quent, les enregistrements des « restart points » contiennent toujours leur clĂ© complĂšte. Il devient donc possible de rechercher et de lire directement l'enregistrement, sans avoir Ă lire les enregistrements prĂ©cĂ©dents. Ces « restart points » sont rĂ©pertoriĂ©s dans le footer de chaque bloc.
GrĂące Ă ces informations, vous pouvez maintenant Ă©viter d'effectuer une recherche linĂ©aire sur le bloc. Effectuez plutĂŽt une recherche par dichotomie sur les « restart points » en cherchant le premier « restart point » dont la clĂ© est lexicographiquement supĂ©rieure Ă la clĂ© recherchĂ©e. Il s'ensuit que l'enregistrement que vous recherchez doit ĂȘtre situĂ© dans la section s'Ă©tendant entre le « restart point » prĂ©cĂ©dent et celui identifiĂ©.
La procédure initiale pour rechercher un enregistrement (recherche par dichotomie du bloc, recherche linéaire de l'enregistrement) est donc la suivante :
Effectuez une recherche par dichotomie sur les blocs, pour identifier celui qui doit contenir l'enregistrement.
Effectuez une recherche par dichotomie sur les « restart points », en identifiant la sous-section du bloc qui doit contenir l'enregistrement.
Effectuez une recherche linéaire sur les enregistrements de cette sous-section.
Bien que la recherche d'enregistrements Ă l'intĂ©rieur d'un bloc soit dĂ©sormais raisonnablement efficace, ce n'est pas le cas pour l'identification du bloc spĂ©cifique. Une recherche par dichotomie peut ĂȘtre relativement performante sur quelques blocs, mais les dĂ©pĂŽts contenant des millions de rĂ©fĂ©rences peuvent comporter des centaines, voire des milliers de blocs. En l'absence d'une structure de donnĂ©es supplĂ©mentaire visant Ă minimiser les accĂšs au disque, chaque recherche de bloc pourrait entraĂźner en moyenne un nombre important d'accĂšs au disque.
Pour Ă©viter cette situation, chaque section peut ĂȘtre suivie d'une section d'indexation qui fournit un moyen efficace de rechercher un bloc. Chaque enregistrement d'index contient les informations suivantes :
Pour trois blocs ou moins, une recherche par dichotomie nĂ©cessite toujours, au plus, deux lectures de disque pour trouver le bloc cible souhaitĂ©. Le nombre de lectures est le mĂȘme pour un index : une pour lire l'index et une pour lire le bloc souhaitĂ©. Par consĂ©quent, les index ne sont Ă©crits que lorsqu'ils Ă©vitent de fait plusieurs lectures, ce qui est le cas lorsque quatre blocs ou plus sont indexĂ©s.
La question se pose alors de savoir ce qu'il se passe lorsque l'index lui-mĂȘme devient si grand qu'il s'Ă©tend sur plusieurs blocs. Vous l'avez peut-ĂȘtre deviné : nous Ă©crivons un nouvel index qui indexe l'index. Ces index Ă plusieurs niveaux ne deviennent vraiment nĂ©cessaires que lorsque vos dĂ©pĂŽts contiennent des centaines de milliers de rĂ©fĂ©rences.
à l'aide de ces index, vous pouvez désormais accélérer la recherche d'enregistrements :
Jusqu'à présent, nous n'avons discuté que de la façon de lire une table unique. Comme l'indique le nom tables.list
, votre base de données « reftable » peut contenir une liste de tables.
Chaque fois que vous mettez à jour une référence dans votre dépÎt, une nouvelle table est créée et ajoutée à tables.list
. Vous obtiendrez donc plusieurs tables, comme suit :
$ tree .git/reftable/
.git/reftable/
âââ 0x000000000001-0x000000000007-8dcd8a77.ref
âââ 0x000000000008-0x000000000008-30e0f6f6.ref
âââ tables.list
$ cat .git/reftable/tables.list
0x000000000001-0x000000000007-8dcd8a77.ref
0x000000000008-0x000000000008-30e0f6f6.ref
La lecture de l'état réel d'un dépÎt nous oblige à fusionner ces multiples tables en une seule table virtuelle.
Vous vous demandez peut-ĂȘtre comment le format « reftable » connaĂźt la valeur la plus rĂ©cente d'une rĂ©fĂ©rence donnĂ©e, si une table est créée pour chaque mise Ă jour de rĂ©fĂ©rence et que la mĂȘme rĂ©fĂ©rence est mise Ă jour plusieurs fois. Intuitivement, on pourrait supposer que la valeur serait celle de la table la plus rĂ©cente contenant la rĂ©fĂ©rence.
En rĂ©alitĂ©, chaque enregistrement a un index de mise Ă jour qui encode la « priorisation » d'un enregistrement. Par exemple, s'il existe deux enregistrements pour le mĂȘme nom de rĂ©fĂ©rence, celui qui a l'index de mise Ă jour le plus Ă©levĂ© remplace celui qui a l'index de mise Ă jour le plus bas.
Ces index de mise Ă jour sont visibles dans la structure de fichiers ci-dessus. Les longues chaĂźnes hexagonales (par exemple 0x000000000001
) sont les index de mise Ă jour. L'index de mise Ă jour minimum contenu dans la table se trouve dans la partie gauche de son nom de fichier et l'index de mise Ă jour maximum dans la partie droite.
Le merge des tables se fait ensuite via une file d'attente de priorisation qui est triĂ©e par clĂ© dâenregistrement ainsi que son index de mise Ă jour. Imaginez que vous voulez parcourir tous les enregistrements de rĂ©fĂ©rences. Vous devez suivre les Ă©tapes suivantes :
Vous pouvez maintenant répéter les étapes ci-dessus pour lire les enregistrements pour d'autres clés.
Les tables peuvent contenir des enregistrements spéciaux « tombstone » qui marquent un enregistrement supprimé. Vous pouvez ainsi supprimer des enregistrements sans avoir à réécrire toutes les tables afin qu'elles ne contiennent plus ces enregistrements.
La file d'attente de priorisation a beau ĂȘtre un concept simple, elle ne parviendrait pas Ă fusionner efficacement des centaines de tables, ni mĂȘme des dizaines. S'il est vrai que chaque mise Ă jour de vos rĂ©fĂ©rences ajoute une nouvelle table Ă votre fichier tables.list
, ce n'est pas tout.
Le compactage automatique entre ici en scĂšne : lorsqu'une nouvelle table a Ă©tĂ© ajoutĂ©e Ă la liste des tables, le backend « reftable » vĂ©rifie si certaines des tables doivent ĂȘtre fusionnĂ©es. Ce processus utilise une mĂ©thode simple : nous vĂ©rifions que les tailles de fichiers de la liste des tables forment une sĂ©quence gĂ©omĂ©trique. Chaque table n
doit ĂȘtre au moins deux fois plus grande que la table suivante la plus rĂ©cente n + 1
. Si cette séquence géométrique n'est pas respectée, le backend compacte les tables afin de restaurer la séquence géométrique.
Ce processus aboutit à des structures de ce type :
$ du --apparent-size .git/reftable/*
429 .git/reftable/0x000000000001-0x00000000bd7c-d9819000.ref
101 .git/reftable/0x00000000bd7d-0x00000000c5ac-c34b88a4.ref
32 .git/reftable/0x00000000c5ad-0x00000000cc6c-60391f53.ref
8 .git/reftable/0x00000000cc6d-0x00000000cdc1-61c30db1.ref
3 .git/reftable/0x00000000cdc2-0x00000000ce67-d9b55a96.ref
1 .git/reftable/0x00000000ce68-0x00000000ce6b-44721696.ref
1 .git/reftable/tables.list
Notez que la propriété size(n) > size(n+1) * 2
est respectée pour chaque table.
L'une des conséquences du compactage automatique est la maintenance automatique du backend « reftable ». Il n'est plus nécessaire d'exécuter git pack-refs
dans un dépÎt.
à présent, le fonctionnement du nouveau format « reftable » ne devrait plus avoir de secret pour vous. Si vous souhaitez approfondir vos connaissances, vous pouvez consulter la documentation technique fournie par le projet Git.
Lisez notre récapitulatif sur Git 2.45.0, Git 2.46.0 et Git 2.47.0 pour découvrir les autres points forts de cette version.