Mise à jour : 19 décembre 2024

Lecture : 17 min

Format reftable de Git : guide pour les débutants

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 :

  • Rendre la recherche d'une rĂ©fĂ©rence unique et l'itĂ©ration Ă  travers des plages de rĂ©fĂ©rences aussi efficaces et rapides que possible.
  • Prendre en charge des lectures cohĂ©rentes des rĂ©fĂ©rences afin que Git ne lise jamais un Ă©tat intermĂ©diaire lorsqu'une mise Ă  jour de plusieurs rĂ©fĂ©rences n'a Ă©tĂ© appliquĂ©e que partiellement.
  • Prendre en charge des Ă©critures atomiques de sorte que la mise Ă  jour de plusieurs rĂ©fĂ©rences peut ĂȘtre effectuĂ©e comme une opĂ©ration tout ou rien.
  • Stocker efficacement des rĂ©fĂ©rences et des journaux de rĂ©fĂ©rence (« reflog »).

Dans cet article, nous allons examiner le format « reftable » pour comprendre son fonctionnement.

Stockage des références dans Git

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 :

  • Une rĂ©fĂ©rence standard qui contient l'ID d'objet de la validation vers laquelle elle pointe.
  • Une rĂ©fĂ©rence symbolique qui contient le nom d'une autre rĂ©fĂ©rence, de la mĂȘme maniĂšre qu'un lien symbolique pointe vers un autre fichier.

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

Vue d'ensemble de la structure « reftable »

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.

Structure d'une 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 :

  • L'en-tĂȘte contient des mĂ©tadonnĂ©es sur la table. En plus d'autres informations, il inclut la version du format, la taille des blocs de donnĂ©es et la fonction de hachage utilisĂ©e par le dĂ©pĂŽt (par exemple, SHA1 ou SHA256).
  • La section « ref » contient vos rĂ©fĂ©rences. Ces enregistrements ont une clĂ© identique au nom de la rĂ©fĂ©rence et pointent soit vers un ID d'objet pour les rĂ©fĂ©rences standards, soit vers une autre rĂ©fĂ©rence pour les rĂ©fĂ©rences symboliques.
  • La section « obj » contient la correspondance inverse entre les ID d'objet et les rĂ©fĂ©rences qui pointent vers eux. Celle-ci permet Ă  Git de rechercher efficacement les rĂ©fĂ©rences qui pointent vers un ID d'objet spĂ©cifique.
  • La section « log » contient vos Ă©lĂ©ments reflog. Ces enregistrements ont une clĂ© identique au nom de la rĂ©fĂ©rence, plus un index qui reprĂ©sente le numĂ©ro de l'Ă©lĂ©ment du journal. Ils contiennent en outre les anciens et nouveaux ID d'objet, ainsi que le message pour cet Ă©lĂ©ment reflog.
  • Le « footer » contient des offsets pour les diffĂ©rentes sections.

table longue avec toutes les sections reftable

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 :

  • En se basant sur le dĂ©but de la section ainsi que la taille des blocs de donnĂ©es, le processus qui lit les tables sait implicitement oĂč chacun des blocs commence. Git peut ainsi facilement effectuer une recherche au milieu d'une section sans lire les blocs prĂ©cĂ©dents, ce qui permet d'effectuer des recherches par dichotomie sur des blocs pour accĂ©lĂ©rer la recherche d'enregistrements.
  • Elle permet au processus qui lit les tables de savoir combien de donnĂ©es lire sur le disque Ă  la fois. Par consĂ©quent, la taille des blocs de donnĂ©es est par dĂ©faut dĂ©finie Ă  4 KiB, ce qui correspond Ă  la taille de secteur la plus courante pour les disques durs. La taille maximale des blocs de donnĂ©es est de 16 Mo.

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.

bloc de référence non compressé

Maintenant que vous disposez de ces informations, vous pouvez localiser un enregistrement en suivant les étapes suivantes :

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

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

Compression de préfixe

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 :

  • Toutes les branches commencent par refs/heads/.
  • Toutes les Ă©tiquettes (« tags ») commencent par 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.

compression de préfixe

« Restart points »

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 :

  1. Effectuez une recherche par dichotomie sur les blocs, pour identifier celui qui doit contenir l'enregistrement.

  2. Effectuez une recherche par dichotomie sur les « restart points », en identifiant la sous-section du bloc qui doit contenir l'enregistrement.

  3. Effectuez une recherche linéaire sur les enregistrements de cette sous-section.

Recherche linéaire pour un enregistrement

Index

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 :

  • L'emplacement du bloc qu'il indexe.
  • La clĂ© du dernier enregistrement du bloc qu'il indexe.

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 :

  1. Déterminez s'il existe un index en consultant le footer de la table.
    • Si c'est le cas, effectuez une recherche par dichotomie dans l'index pour trouver le bloc souhaitĂ©. Ce bloc peut pointer vers un bloc d'index, auquel cas il vous faut rĂ©pĂ©ter l'opĂ©ration jusqu'Ă  atteindre un enregistrement du type souhaitĂ©.
    • Dans le cas contraire, effectuez une recherche par dichotomie dans les blocs comme auparavant.
  2. Effectuez une recherche par dichotomie sur les « restart points » pour identifier la sous-section du bloc qui doit contenir l'enregistrement.
  3. Effectuez une recherche linéaire sur les enregistrements de cette sous-section.

Tables multiples

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 :

  1. Pour chaque table, ajoutez son premier enregistrement Ă  la file d'attente de priorisation.

Ajout du premier enregistrement Ă  la file d'attente de priorisation

  1. RĂ©cupĂ©rez et retirez de la file d'attente l'enregistrement en tĂȘte de la file. Comme la file d'attente est triĂ©e par index de mise Ă  jour, cet enregistrement doit ĂȘtre la version la plus rĂ©cente. Ajoutez l'enregistrement suivant de la table de cet enregistrement Ă  la file d'attente de priorisation.

DĂ©finition de la tĂȘte de la file d'attente de priorisation

  1. Retirez tous les enregistrements de la file d'attente qui ont la mĂȘme clĂ© que l’enregistrement rĂ©cupĂ©rĂ©. Ces enregistrements sont dĂ©passĂ©s et ne seront pas utilisĂ©s. Pour chaque table pour laquelle vous supprimez des enregistrements, ajoutez l'enregistrement suivant Ă  la file d'attente de priorisation.

DĂ©pĂŽt de tous les enregistrements de la file qui ont le mĂȘme nom 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.

Compactage automatique

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.

Vous souhaitez en savoir plus ?

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

Votre avis nous intéresse

Cet article de blog vous a plu ou vous avez des questions ou des commentaires ? Partagez vos réflexions en créant un sujet dans le forum de la communauté GitLab.
Share your feedback

Plus de 50 % des entreprises du classement Fortune 100 font confiance à GitLab

Commencez à livrer des logiciels de meilleurs qualité plus rapidement

Découvrez comment la plateforme DevSecOps intelligente

peut aider votre équipe.