Git 🌙
Chapters â–Ÿ 2nd Edition

10.2 Les tripes de Git - Les objets de Git

Les objets de Git

Git est un systĂšme de fichier adressables par contenu. Super ! Mais qu’est-ce que ça veut dire ? Ça veut dire que le cƓur de Git est une simple base de paires clĂ©/valeur. Vous pouvez y insĂ©rer n’importe quelle sorte de donnĂ©es et il vous retournera une clĂ© que vous pourrez utiliser Ă  n’importe quel moment pour rĂ©cupĂ©rer ces donnĂ©es.

Pour illustrer cela, vous pouvez utiliser la commande de plomberie hash-object, qui prend des données, les stocke dans votre répertoire .git, puis retourne la clé sous laquelle les données sont stockées.

Tout d’abord, crĂ©ez un nouveau dĂ©pĂŽt Git et vĂ©rifiez que rien ne se trouve dans le rĂ©pertoire objects :

$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f

Git a initialisé le répertoire objects et y a créé les sous-répertoires pack et info, mais ils ne contiennent pas de fichier régulier. Maintenant, stockez du texte dans votre base de données Git :

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

C’est la forme la plus simple, git hash-object prendrait le contenu que vous lui passez et retournerait simplement la clĂ© unique qui serait utilisĂ©e pour le stocker dans la base de donnĂ©es Git. L’option -w spĂ©cifie Ă  hash-object de stocker l’objet, sinon la commande rĂ©pondrait seulement quelle serait la clĂ©. --stdin spĂ©cifie Ă  la commande de lire le contenu depuis l’entrĂ©e standard, sinon hash-object s’attend Ă  trouver un chemin vers un fichier.

La sortie de la commande est une empreinte de 40 caractĂšres. C’est l’empreinte SHA-1 ‒ une somme de contrĂŽle du contenu du fichier que vous stockez plus un en-tĂȘte, que vous apprendrez sous peu. Voyez maintenant comment Git a stockĂ© vos donnĂ©es :

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Vous pouvez voir un fichier dans le rĂ©pertoire objects. C’est comme cela que Git stocke initialement du contenu ‒ un fichier par contenu, nommĂ© d’aprĂšs la somme de contrĂŽle SHA-1 du contenu et de son en-tĂȘte. Le sous-rĂ©pertoire est nommĂ© d’aprĂšs les 2 premiers caractĂšres de l’empreinte et le fichier d’aprĂšs les 38 caractĂšres restants.

Vous pouvez rĂ©cupĂ©rer le contenu avec la commande cat-file. Cette commande est un peu le couteau suisse pour l’inspection des objets Git. Lui passer l’option -p ordonne Ă  la commande cat-file de dĂ©terminer le type de contenu et de vous l’afficher joliment :

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

Vous pouvez maintenant ajouter du contenu Ă  Git et le rĂ©cupĂ©rer Ă  nouveau. Vous pouvez faire de mĂȘme avec le contenu de fichiers. Par exemple, vous pouvez mettre en Ɠuvre une gestion de version simple d’un fichier. D’abord, crĂ©ez un nouveau fichier et enregistrez son contenu dans la base de donnĂ©es :

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

Puis, modifiez le contenu du fichier et enregistrez-le à nouveau :

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

Votre base de données contient les 2 versions du fichier, ainsi que le premier contenu que vous avez stocké ici :

$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Vous pouvez maintenant restaurer le fichier à sa premiÚre version :

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

ou à sa seconde version :

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

Mais se rappeler de la clĂ© SHA-1 de chaque version de votre fichier n’est pas pratique. En plus, vous ne stockez pas le nom du fichier dans votre systĂšme ‒ seulement le contenu. Ce type d’objet est appelĂ© un blob (Binary Large OBject, soit en français : Gros Objet Binaire). Git peut vous donner le type d’objet de n’importe quel objet Git, Ă©tant donnĂ© sa clĂ© SHA-1, avec cat-file -t :

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

Les objets arbres

Le prochain type que nous allons Ă©tudier est l’arbre (tree) qui rĂ©sout le problĂšme de stockage du nom du fichier et vous permet d’enregistrer un groupe de fichiers ensemble. Git stocke du contenu de la mĂȘme maniĂšre, mais plus simplement, qu’un systĂšme de fichier UNIX. Tout le contenu est stockĂ© comme des objets de type arbre ou blob : un arbre correspondant Ă  un rĂ©pertoire UNIX et un blob correspond Ă  peu prĂšs aux inodes ou au contenu d’un fichier. Un unique arbre contient une ou plusieurs entrĂ©es, chacune Ă©tant l’empreinte SHA-1 d’un blob ou d’un sous-arbre (sub-tree) avec ses droits d’accĂšs (mode), son type et son nom de fichier associĂ©s. L’arbre le plus rĂ©cent d’un projet pourrait ressembler, par exemple, Ă  ceci :

$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859      README
100644 blob 8f94139338f9404f26296befa88755fc2598c289      Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

La syntaxe master^{tree} signifie l’objet arbre qui est pointĂ© par le dernier commit de la branche master. Remarquez que le sous-rĂ©pertoire lib n’est pas un blob, mais un pointeur vers un autre arbre :

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b      simplegit.rb
Note

En fonction du shell que vous utilisez, vous pouvez rencontrer des erreurs lors de l’utilisation de la syntaxe master^{tree}.

Dans CMD sous Windows, le caractĂšre ^ est utilisĂ© pour l’échappement, donc vous devez le doubler pour qu’il soit pris en compte : git cat-file -p master^^{tree}. Sous Powershell, les paramĂštres utilisant les caractĂšres {} doivent ĂȘtre citĂ©s pour Ă©viter que ces paramĂštres ne soit incorrectement analysĂ©s : git cat-file -p 'master^{tree}'.

Si vous utilisez ZSH, le caractĂšre ^ est utilisĂ© pour les motifs de fichiers, donc vous devez encadrer l’expression complĂšte entre guillemets : git cat-file -p "master^{tree}".

Conceptuellement, les données que Git stocke ressemblent ceci :

Une version simple du modÚle de données Git.
Figure 147. Une version simple du modÚle de données Git.

Vous pouvez facilement crĂ©er votre propre arbre. Git crĂ©e habituellement un arbre Ă  partir de l’état de la zone d’attente ou index et Ă©crit une sĂ©rie d’objets arbre Ă  partir de lĂ . Donc, pour crĂ©er un objet arbre, vous devez d’abord mettre en place un index en mettant quelques fichiers en attente. Pour crĂ©er un index contenant une entrĂ©e, la premiĂšre version de votre fichier test.txt par exemple, utilisons la commande de plomberie update-index. Vous pouvez utiliser cette commande pour ajouter artificiellement une version plus ancienne Ă  une nouvelle zone d’attente. Vous devez utiliser les options --add car le fichier n’existe pas encore dans votre zone d’attente (vous n’avez mĂȘme pas encore mis en place une zone d’attente) et --cacheinfo car le fichier que vous ajoutez n’est pas dans votre rĂ©pertoire, mais dans la base de donnĂ©es. Vous pouvez ensuite prĂ©ciser le mode, SHA-1 et le nom de fichier :

$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

Dans ce cas, vous prĂ©cisez le mode 100644, qui signifie que c’est un fichier normal. Les alternatives sont 100755, qui signifie que c’est un exĂ©cutable, et 120000, qui prĂ©cise que c’est un lien symbolique. Le concept de « mode » a Ă©tĂ© repris des mode UNIX, mais est beaucoup moins flexible : ces trois modes sont les seuls valides pour Git, pour les fichiers (blobs) dans Git (bien que d’autres modes soient utilisĂ©s pour les rĂ©pertoires et sous-modules).

Vous pouvez maintenant utiliser la commande write-tree pour Ă©crire la zone d’attente dans un objet arbre. L’option -w est inutile (appeler write-tree crĂ©e automatiquement un objet arbre Ă  partir de l’état de l’index si cet arbre n’existe pas) :

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt

Vous pouvez Ă©galement vĂ©rifier que c’est un objet arbre :

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

Vous allez créer maintenant un nouvel arbre avec la seconde version de test.txt et aussi un nouveau fichier :

$ echo 'new file' > new.txt
$ git update-index test.txt
$ git update-index --add new.txt

Votre zone d’attente contient maintenant la nouvelle version de test.txt ainsi que le nouveau fichier new.txt. Enregistrez cet arbre (c’est-Ă -dire enregistrez l’état de la zone d’attente ou index dans un objet arbre) et voyez Ă  quoi il ressemble :

$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

Remarquez que cet arbre contient des entrĂ©es pour les deux fichiers et que l’empreinte SHA-1 de test.txt est l’empreinte de la « version 2 » de tout Ă  l’heure (1f7a7a). Pour le plaisir, ajoutez le premier arbre Ă  celui-ci, en tant que sous-rĂ©pertoire. Vous pouvez rĂ©cupĂ©rer un arbre de votre zone d’attente en exĂ©cutant read-tree. Dans ce cas, vous pouvez rĂ©cupĂ©rer un arbre existant dans votre zone d’attente comme Ă©tant un sous-arbre en utilisant l’option --prefix de read-tree :

$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

Si vous crĂ©iez un rĂ©pertoire de travail Ă  partir du nouvel arbre que vous venez d’enregistrer, vous auriez deux fichiers Ă  la racine du rĂ©pertoire de travail, ainsi qu’un sous-rĂ©pertoire appelĂ© bak qui contiendrait la premiĂšre version du fichier test.txt. Vous pouvez vous reprĂ©senter les donnĂ©es que Git utilise pour ces structures comme ceci :

Structure du contenu de vos données Git actuelles.
Figure 148. Structure du contenu de vos données Git actuelles.

Les objets commit

Vous avez trois arbres qui dĂ©finissent diffĂ©rents instantanĂ©s du projet que vous suivez, mais le problĂšme prĂ©cĂ©dent persiste : vous devez vous souvenir des valeurs des trois empreintes SHA-1 pour accĂ©der aux instantanĂ©s. Vous n’avez pas non plus d’information sur qui a enregistrĂ© les instantanĂ©s, quand et pourquoi. Ce sont les informations Ă©lĂ©mentaires qu’un objet commit stocke pour vous.

Pour crĂ©er un objet commit, il suffit d’exĂ©cuter commit-tree et de prĂ©ciser l’empreinte SHA-1 d’un seul arbre et quels objets commit, s’il y en a, le prĂ©cĂšdent directement. Commencez avec le premier arbre que vous avez créé :

$ echo 'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Note

Vous obtiendrez une valeur de hashage diffĂ©rente Ă  cause d’un moment de crĂ©ation et d’une information d’auteur diffĂ©rents. De plus, bien qu’en principe tout objet commit peut ĂȘtre reproduit prĂ©cisĂ©ment en lui fournissant la mĂȘme donnĂ©e, les dĂ©tails historiques de la construction de ce livre signifient que les empreintes de commit affichĂ©es peuvent ne pas correspondre aux commits donnĂ©s. Remplacez les valeurs de hashage de commit et d’étiquette par vos propres valeurs de somme de contrĂŽle dans la suite de ce chapitre.

Vous pouvez voir votre nouvel objet commit avec git cat-file :

$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700

first commit

Le format d’un objet commit est simple : il contient l’arbre racine de l’instantanĂ© du projet Ă  ce moment, les informations sur l’auteur et le validateur (qui utilisent vos variables de configuration user.name et user.email et un horodatage); une ligne vide et le message de validation.

Ensuite, vous enregistrez les deux autres objets commit, chacun référençant le commit dont il est issu :

$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'third commit'  | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9

Chacun des trois objets commit pointe sur un des trois arbres d’instantanĂ© que vous avez crĂ©Ă©s. Curieusement, vous disposez maintenant d’un historique Git complet que vous pouvez visualiser avec la commande git log, si vous la lancez sur le SHA-1 du dernier commit :

$ git log --stat 1a410e
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:15:24 2009 -0700

	third commit

 bak/test.txt | 1 +
 1 file changed, 1 insertion(+)

commit cac0cab538b970a37ea1e769cbbde608743bc96d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:14:29 2009 -0700

	second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:09:34 2009 -0700

    first commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

Fantastique. Vous venez d’effectuer les opĂ©rations bas niveau pour construire un historique Git sans avoir utilisĂ© une seule des commandes de haut niveau. C’est l’essence de ce que fait Git quand vous exĂ©cutez les commandes git add et git commit. Il stocke les blobs correspondant aux fichiers modifiĂ©s, met Ă  jour l’index, Ă©crit les arbres et ajoute les objets commit qui rĂ©fĂ©rencent les arbres racines venant juste avant eux. Ces trois objets principaux (le blob, l’arbre et le commit) sont initialement stockĂ©s dans des fichiers sĂ©parĂ©s du rĂ©pertoire .git/objects. Voici tous les objets contenus dans le rĂ©pertoire exemple, commentĂ©s d’aprĂšs leur contenu :

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

Si vous suivez les pointeurs internes de ces objets, vous obtenez un graphe comme celui-ci :

Tous les objets atteignables de votre répertoire Git.
Figure 149. Tous les objets atteignables de votre répertoire Git.

Stockage des objets

Nous avons parlĂ© plus tĂŽt de l’en-tĂȘte prĂ©sent avec le contenu. Prenons un moment pour Ă©tudier la façon dont Git stocke les objets. On verra comment stocker interactivement un objet blob (ici, la chaĂźne "what is up, doc?") avec le langage Ruby.

Vous pouvez démarrer Ruby en mode interactif avec la commande irb :

$ irb
>> content = "what is up, doc?"
> "what is up, doc?"

Git construit un en-tĂȘte qui commence avec le type de l’objet, ici un blob. Ensuite, il ajoute un espace suivi de taille du contenu et enfin un octet nul :

>> header = "blob #{content.length}\0"
> "blob 16\u0000"

Git concatĂšne l’en-tĂȘte avec le contenu original et calcule l’empreinte SHA-1 du nouveau contenu. En Ruby, vous pouvez calculer l’empreinte SHA-1 d’une chaĂźne en incluant la bibliothĂšque « digest/SHA-1 » via la commande require, puis en appelant Digest::SHA1.hexdigest() sur la chaĂźne :

>> store = header + content
> "blob 16\u0000what is up, doc?"
>> require 'digest/sha1'
> true
>> sha1 = Digest::SHA1.hexdigest(store)
> "bd9dbf5aae1a3862dd1526723246b20206e5fc37"

Comparons cela avec la sortie de git hash-object. Ici nous utilisons echo -n pour Ă©viter d’ajouter un retour Ă  la ligne dans l’entrĂ©e.

$ echo -n "what is up, doc?" | git hash-object --stdin
bd9dbf5aae1a3862dd1526723246b20206e5fc37

Git compresse le nouveau contenu avec zlib, ce que vous pouvez faire avec la bibliothĂšque zlib de Ruby. D’abord, vous devez inclure la bibliothĂšque et ensuite exĂ©cuter Zlib::Deflate.deflate() sur le contenu :

>> require 'zlib'
> true
>> zlib_content = Zlib::Deflate.deflate(store)
> "x\x9CK\xCA\xC9OR04c(\xCFH,Q\xC8,V(-\xD0QH\xC9O\xB6\a\x00_\x1C\a\x9D"

Finalement, vous enregistrerez le contenu compressĂ© dans un objet sur le disque. Vous dĂ©terminerez le chemin de l’objet que vous voulez enregistrer (les deux premiers caractĂšres de l’empreinte SHA-1 formeront le nom du sous-rĂ©pertoire et les 38 derniers formeront le nom du fichier dans ce rĂ©pertoire). En Ruby, on peut utiliser la fonction FileUtils.mkdir_p() pour crĂ©er un sous-rĂ©pertoire s’il n’existe pas. Ensuite, ouvrez le fichier avec File.open() et enregistrez le contenu compressĂ© en appelant la fonction write() sur la rĂ©fĂ©rence du fichier :

>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"
>> require 'fileutils'
> true
>> FileUtils.mkdir_p(File.dirname(path))
> ".git/objects/bd"
>> File.open(path, 'w') { |f| f.write zlib_content }
> 32

Vérifions le contenu du fichier au moyen de git cat-file :

---
$ git cat-file -p bd9dbf5aae1a3862dd1526723246b20206e5fc37
what is up, doc?
---

C’est tout ! Vous venez juste de crĂ©er un objet blob valide.

Tout les objets Git sont stockĂ©s de la mĂȘme façon, mais avec des types diffĂ©rents : l’en-tĂȘte commencera par « commit » ou « tree » au lieu de la chaĂźne « blob ». De plus, alors que le contenu d’un blob peut ĂȘtre Ă  peu prĂšs n’importe quoi, le contenu d’un commit ou d’un arbre est formatĂ© de façon trĂšs prĂ©cise.

scroll-to-top