Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
306 lines (238 sloc) 15.1 KB
// -*- mode: doc; mode: flyspell; coding: utf-8; fill-column: 79; -*-
== Les secrets révélés ==
Nous allons jeter un œil sous le capot pour comprendre comment Git réalise ses
miracles. Je passerai sous silence la plupart des détails. Pour des
explications plus détaillées, référez-vous au
http://www.kernel.org/pub/software/scm/git/docs/user-manual.html[manuel
utilisateur].
=== L'invisibilité ===
Comment fait Git pour être si discret ? Mis à part lorsque vous faites des
commits et des fusions, vous pouvez travailler comme si la gestion de versions
n'existait pas. Et c'est lorsque vous en avez besoin que vous êtes content de
voir que Git veillait sur vous tout le temps.
D'autres systèmes de gestion de versions vous mettent constamment aux prises
avec de la paperasserie et de la bureaucratie. Les fichiers sont en lecture
seule jusqu'à l'obtention depuis un serveur central du droit d'édition de
tel ou tel fichier. Les commandes les plus basiques voient leurs performances
s'écrouler au fur et à mesure que le nombre d'utilisateurs augmente. Le travail
s'arrête dès lors que le réseau ou le serveur central est en panne.
À l'inverse, Git conserve tout l'historique de votre projet dans le
sous-dossier `.git` de votre dossier de travail. C'est votre propre copie de
l'historique et vous pouvez donc rester déconnecté tant que vous ne voulez pas
communiquer avec les autres. Vous conservez un contrôle total sur le sort de
vos fichiers puisque Git peut aisément les recréer à tout moment à partir de
l'un des états enregistrés dans `.git`.
=== L'intégrité ===
La plupart des gens associent la cryptographie à la conservation du secret des
informations mais l'un de ses buts tout aussi important est de conserver
l'intégrité de ces informations. Un usage approprié des fonctions de hachage
cryptographiques (celles qui calculent l'empreinte d'un document) permet
d'empêcher la corruption accidentelle ou malicieuse des données.
Une empreinte SHA1 peut être vue comme un nombre de 160 bits identifiant de
manière unique n'importe quelle suite d'octets que vous rencontrerez dans votre
vie. On peut même aller plus loin : c'est vrai pour toutes les suites d'octets
que les humains utiliseront sur plusieurs générations.
Comme une empreinte SHA1 est elle-même une suite d'octets, nous pouvons
calculer l'empreinte d'une suite de caractères contenant d'autres
empreintes. Cette simple observation est étonnamment utile (cherchez par
exemple 'hash chain'). Nous verrons plus tard comment Git utilise cela pour
garantir efficacement l'intégrité des données.
En bref, Git conserve vos données dans le sous-dossier `.git/objects` mais à
la place des noms de fichiers normaux, vous n'y trouverez que des ID. En
utilisant ces ID comme noms de fichiers et grâce à quelques astucieux fichiers
de verrouillage et d'horodatage, Git transforme un simple système de fichiers
en une base de données efficace et robuste.
=== L'intelligence ===
Comment fait Git pour savoir que vous avez renommé un fichier même si vous ne
lui avez pas dit explicitement ? Bien sûr, vous pouvez utiliser *git mv* mais
c'est exactement la même chose que de faire *git rm* suivi par *git add*.
Git a des heuristiques pour débusquer les changements de noms et les copies
entre les versions successives. En fait, il peut même détecter les bouts de
code qui ont été déplacés ou copiés d'un fichier à un autre ! Bien que ne
couvrant pas tous les cas, cela marche déjà très bien et cette fonctionnalité
est encore en cours d'amélioration. Si cela échoue pour vous, essayez les
options activant des méthodes de détection de copie plus coûteuses et envisager
de faire une mise à jour.
=== L'indexation ===
Pour chaque fichier suivi, Git mémorise des informations, telles que sa taille
et ses dates de création et de dernières modifications, dans un fichier appelé
'index'. Pour déterminer si un fichier a changé, Git compare son état courant
avec ce qu'il a mémorisé dans l'index. Si cela correspond alors Git n'a pas
besoin de relire le fichier.
Puisque les appels à 'stat' sont considérablement plus rapides que la lecture
des fichiers, si vous n'avez modifié que quelques fichiers, Git peut déterminer
son état en très peu de temps.
Nous avons dit plus tôt que l'index était une aire d'assemblage. Comment se
peut-il qu'un simple fichier contant quelques informations sur les fichiers
soit une aire d'assemblage ? Parce que la commande add ajoute les fichiers à la
base de données de Git et met à jour l'index avec leurs informations alors que
la commande commit, sans option, crée une nouvelle version basée uniquement sur
cet index et les fichiers déjà inclus dans la base de données.
=== Les origines de Git ===
Ce http://lkml.org/lkml/2005/4/6/121[message de la Mailing List du noyau Linux]
décrit l'enchaînement des évènements ayant mené à Git. L'ensemble de l'enfilade
est un site archéologique fascinant pour les historiens de Git.
=== La base d'objets ===
Chacune des versions de vos données est conservée dans la base d'objets
('object database') qui réside dans le sous-dossier `.git/objects` ; le
reste du contenu du dossier `.git` représente moins de données : l'index, le
nom des branches, les tags, les options de configuration, les logs,
l'emplacement actuel de HEAD, et ainsi de suite. La base d'objets est simple
mais élégante et constitue la source de la puissance de Git.
Chaque fichier dans `.git/objects` est un objet. Il y a trois sortes d'objets
qui nous concerne : les `blobs`, les arbres (`trees`) et les `commits`.
=== Les blobs ===
Tout d'abord, faisons un peu de magie. Choisissez un nom de
fichier... n'importe quel nom de fichier ! Puis dans un dossier vide, faites
(en remplaçant `VOTRE_NOM_DE_FICHIER` par le nom que vous avez choisi) :
$ echo joli > VOTRE_NOM_DE_FICHIER
$ git init
$ git add .
$ find .git/objects -type f
Vous verrez +.git/objects/06/80f15d4cb13a09f600a25b84eae36506167970+.
Comment puis-je le savoir sans connaître le nom de fichier que vous avez
choisi ? Tout simplement parce que l'empreinte SHA1 de :
"blob" SP "5" NUL "joli" LF
est 0680f15d4cb13a09f600a25b84eae36506167970. Où SP est un espace, NUL est
l'octet de valeur nulle et LF est un passage à la ligne. Vous pouvez vérifier
cela en tapant :
$ printf "blob 5\000joli\n" | sha1sum
Git utilise un classement par contenu : les fichiers ne sont pas stockés selon
leur nom mais selon l'empreinte des données qu'ils contiennent, dans un fichier
que nous appelons un objet 'blob'. Nous pouvons considérer l'empreinte comme un
ID unique du contenu d'un fichier. Donc nous pouvons retrouver un fichier par
son contenu. La chaîne initiale `blob 5` est simplement un entête indiquant le
type de l'objet et sa longueur en octets ; cela simplifie le classement
interne.
Je peux donc aisément prédire ce que vous voyez. Le nom du fichier ne compte
pas : pour construire l'objet blob, seules comptent les données stockées dans
le fichier.
Peut-être vous demandez-vous ce qui se produit pour des fichiers ayant le même
contenu. Essayez en créant des copies de votre premier fichier, avec des noms
quelconques. Le contenu de +.git/objects+ reste le même quel que soit le nombre
de copies que vous avez ajoutées. Git ne stocke le contenu qu'une seule fois.
À propos, les fichiers dans +.git/objects+ sont compressés par zlib et, par
conséquent, vous ne pouvez pas en consulter le contenu directement. Passez-les
au travers du filtre http://www.zlib.net/zpipe.c[zpipe -d] ou tapez :
$ git cat-file -p 0680f15d4cb13a09f600a25b84eae36506167970
qui affiche proprement l'objet choisi.
=== Les arbres (`trees`) ===
Mais que deviennent les noms des fichiers ? Ils doivent bien être stockés
quelque part à un moment. Git se préoccupe des noms de fichiers lors d'un
commit :
$ git commit # Tapez un message
$ find .git/objects -type f
Vous devriez voir maintenant trois objets. Mais là, je ne peux plus prédire le
nom des deux nouveaux fichiers puisqu'ils dépendent en partie du nom de fichier
que vous avez choisi. Nous continuerons en supposant que vous avez choisi
``rose''. Si ce n'est pas le cas, vous pouvez réécrire l'histoire pour que ce
soit le cas :
$ git filter-branch --tree-filter 'mv VOTRE_NOM_DE_FICHIER rose'
$ find .git/objects -type f
Le fichier +.git/objects/9a/6a950c3b14eb1a3fb540a2749514a1cb81e206+ devrait
maintenant apparaître puisque c'est l'empreinte SHA1 du contenu suivant :
"tree" SP "32" NUL "100644 rose" NUL 0x9a6a950c3b14eb1a3fb540a2749514a1cb81e206
Vérifiez que ce contenu est le bon en tapant :
$ echo 9a6a950c3b14eb1a3fb540a2749514a1cb81e206 | git cat-file --batch
Avec zpipe, il est plus simple de vérifier l'empreinte :
$ zpipe -d < .git/objects/9a/6a950c3b14eb1a3fb540a2749514a1cb81e206 | sha1sum
La vérification de l'empreinte est plus difficile via cat-file puisque cette
commande n'affiche pas que le contenu brut du fichier après décompression.
Cette fichier est un objet arbre ('tree') : une liste de tuples constitués d'un
type, d'un nom de fichier et d'une empreinte. Dans notre exemple, le type est
100644 qui indique que `rose` est un fichier normal et l'empreinte est celle de
l'objet de type blob contenant le contenu de `rose`. Les autres types possibles
pour un fichier sont exécutable, lien symbolique ou dossier. Dans ce dernier
cas, l'empreinte représente un autre objet de type arbre.
Si vous faites appel à la commande filter-branch, vous verrez apparaître de
vieux objets dont vous n'avez pas besoin. Même s'ils disparaîtront
automatiquement une fois expirée la période de rétention, nous allons les
effacer dès maintenant pour rendre notre petit exemple plus facile à suivre :
$ rm -r .git/refs/original
$ git reflog expire --expire=now --all
$ git prune
Sur de vrais projets, vous devriez éviter de telles commandes puisqu'elles
détruisent les sauvegardes. Si vous voulez un dossier propre, il est
conseillé de faire un tout nouveau clone. Faites aussi attention si vous
manipulez directement le contenu de +.git+&#160;: que se passera-t-il si une
commande Git s'effectue au même moment ou si le courant est soudainement
coupé ?
De manière générale, les refs devraient toujours être effacées via *git
update-ref -d* même si on considère comme sans risque la suppression manuelle
de +refs/original+.
=== Les commits ===
Nous avons expliqué 2 des 3 types d'objets. Le troisième est l'objet
'commit'. Son contenu dépend du message de commit ainsi que de la date et
l'heure auxquelles il a été créé. Pour que vous obteniez la même chose qu'ici,
nous devons bidouiller un peu :
$ git commit --amend -m Shakespeare # Changement de message de commit
$ git filter-branch --env-filter 'export
GIT_AUTHOR_DATE="Fri 13 Feb 2009 15:31:30 -0800"
GIT_AUTHOR_NAME="Alice"
GIT_AUTHOR_EMAIL="alice@example.com"
GIT_COMMITTER_DATE="Fri, 13 Feb 2009 15:31:30 -0800"
GIT_COMMITTER_NAME="Bob"
GIT_COMMITTER_EMAIL="bob@example.com"' # Trucage de la date, l'heure et l'auteur.
$ find .git/objects -type f
Le fichier +.git/objects/ae/9d1241b2b6eea90529149a065f6bc444365c2a+ devrait
maintenant exister puisque c'est l'empreinte SHA1 du contenu suivant :
"commit 158" NUL
"tree 9a6a950c3b14eb1a3fb540a2749514a1cb81e206" LF
"author Alice <alice@example.com> 1234567890 -0800" LF
"committer Bob <bob@example.com> 1234567890 -0800" LF
LF
"Shakespeare" LF
Comme précédemment, vous pouvez utiliser zpipe ou cat-file pour vérifier par
vous-même.
C'est le premier commit, ce qui explique pourquoi il n'y a pas de commit
parent. Mais les commits suivants contiendront toujours au moins une ligne
identifiant un commit parent.
=== Indiscernable de la magie ===
Les secrets de Git semblent trop simples. On imagine qu'il suffit de mélanger
quelques scripts shell et d'y ajouter une pincée de code C pour mitonner un tel
système en quelques heures : un assemblage d'opérations basiques sur les
fichiers et de calcul d'empreintes SHA1 garni de quelques fichiers verrou et
d'appels à fsync pour la robustesse. En fait, nous venons précisément de
décrire les premières versions de Git. Malgré tout, mis à part quelques
techniques astucieuses de compression pour gagner de la place et d'indexation
pour gagner du temps, nous savons maintenant comment Git transforme adroitement
un système de fichiers en une base de données parfaitement adaptée à de la
gestion de versions.
Par exemple, si un fichier quelconque de la base d'objets vient à être corrompu
par une erreur disque alors son empreinte ne correspond plus et nous sommes
alertés du problème. En calculant l'empreinte des empreintes d'autres objets,
nous maintenons l'intégrité à tous les niveaux. Les commits sont atomiques
puisque ils ne peuvent jamais mémoriser des modifications partiellement
stockées : nous ne pouvons calculer l'empreinte d'un commit et le stocker dans
la base d'objets qu'après y avoir déjà stocké tous les arbres, blobs et parents
relatifs à ce commit. La base d'objets est immunisée contre les interruptions
inattendues telles que les coupures de courant.
Nous faisons même échouer les tentatives d'attaque les plus sournoises.
Supposez que quelqu'un tente de modifier discrètement le contenu d'un fichier
dans l'une des anciennes versions du projet. Pour rendre cohérent le contenu de
la base d'objets, il lui faut changer l'empreinte de l'objet blob correspondant
puisque elle doit maintenant représenter une chaîne d'octets différente. Cela
signifie qu'il doit aussi changer l'empreinte de tous les arbres référençant ce
blob et donc changer l'empreinte de tous les commits impliquant ces arbres
ainsi que de tous les descendants de ces commits. Cela implique que l'empreinte
du HEAD officiel diffère de celle du HEAD d'un dépôt corrompu. En remontant la
suite d'empreintes erronées nous pouvons localiser avec précision le fichier
corrompu ainsi que le premier commit où il l'a été.
En résumé, tant que nous sommes sûrs des 20 octets représentant le dernier
commit, il est impossible d'altérer un dépôt Git.
Qu'en est-il des fameuses fonctionnalités de Git ? Des branchements ? Des
fusions ? Des tags ? De simples détails. La tête courante est conservée dans le
fichier +.git/HEAD+ qui contient l'empreinte d'un objet commit. Cette empreinte
sera tenue à jour durant un commit ainsi que durant de nombreuses autres
commandes. Les branches fonctionnent de manière similaire : ce sont des
fichiers dans +.git/refs/heads+. Et les tags aussi : ils sont dans
+.git/refs/tags+ mais ils sont mis à jour par un ensemble différent de
commandes.
// LocalWords: doc flyspell coding utf fill-column Git référez-vous commits mv
// LocalWords: git cryptographiques SHA chains d'horodatage rm add
// LocalWords: stat List Linux object database tags logs HEAD blobs trees echo
// LocalWords: init find objects puis-je blob SP LF cb eae printf sha sum zlib
// LocalWords: demandez-vous Passez-les zpipe cat-file filter-branch tree eb
// LocalWords: tree-filter fb batch tuples reflog now all refs update-ref Fri
// LocalWords: amend Shakespeare env-filter export AUTHOR Feb NAME Alice EMAIL
// LocalWords: COMMITTER author committer shell fsync
Jump to Line
Something went wrong with that request. Please try again.