Les tripes de Git
- Plomberie et porcelaine
- Les objets de Git
- Références Git
- Fichiers groupés
- La refspec
- Les protocoles de transfert
- Maintenance et récupération de données
- Les variables d’environnement
- Résumé
Vous êtes peut-être arrivé à ce chapitre en en sautant certains autres ou après avoir parcouru tout le reste du livre. Dans tous les cas, c’est ici que le fonctionnement interne et la mise en œuvre de Git sont abordés. Pour nous, leur apprentissage a été fondamental pour comprendre à quel point Git est utile et puissant, mais d’autres soutiennent que cela peut être source de confusion et peut être trop complexe pour les débutants. Nous en avons donc fait le dernier chapitre de ce livre pour que vous puissiez le lire tôt ou tard lors de votre apprentissage. Nous vous laissons le choix.
Maintenant que vous êtes ici, commençons. Tout d’abord, si ce n’est pas encore clair, Git est fondamentalement un système de fichiers adressable par contenu avec l’interface utilisateur d’un VCS au-dessus. Vous en apprendrez plus sur ce que cela signifie dans quelques instants.
Aux premiers jours de Git (surtout avant la version 1.5), l’interface utilisateur était beaucoup plus complexe, car elle était centrée sur le système de fichiers plutôt que sur l’aspect VCS. Ces dernières années, l’interface utilisateur a été peaufinée jusqu’à devenir aussi cohérente et facile à utiliser que n’importe quel autre système. Pour beaucoup, l’image du Git des débuts avec son interface utilisateur complexe et difficile à apprendre est toujours présente.
La couche système de fichiers adressable par contenu est vraiment géniale et nous l’aborderons dans ce chapitre. Ensuite, vous apprendrez les mécanismes de transfert ainsi que les tâches que vous serez amené à accomplir pour maintenir un dépôt.
Plomberie et porcelaine
Ce livre couvre l’utilisation de Git avec une trentaine de verbes comme
checkout
, branch
, remote
… Mais, puisque Git était initialement
une boîte à outils (toolkit) pour VCS, plutôt qu’un VCS complet et
convivial, il dispose de tout un ensemble d’actions pour les tâches bas
niveau qui étaient conçues pour être liées dans le style UNIX ou
appelées depuis des scripts. Ces commandes sont dites commandes de
« plomberie » (plumbing) et les autres, plus conviviales sont appelées
« la porcelaine » (porcelain).
Les neuf premiers chapitres du livre concernent presque exclusivement les commandes de porcelaine. Par contre, dans ce chapitre, vous serez principalement confronté aux commandes de plomberie bas niveau, car elles vous donnent accès au fonctionnement interne de Git et aident à montrer comment et pourquoi Git fonctionne comme il le fait. Beaucoup de ces commandes ne sont pas faites pour être utilisées à la main sur une ligne de commande, mais sont plutôt utilisées comme briques de base pour écrire de nouveaux outils et scripts personnalisés.
Quand vous exécutez git init
dans un nouveau répertoire ou un
répertoire existant, Git crée un répertoire .git
qui contient presque
tout ce que Git stocke et manipule. Si vous voulez sauvegarder ou cloner
votre dépôt, copier ce seul répertoire suffirait presque. Ce chapitre
traite principalement de ce que contient ce répertoire. Voici à quoi il
ressemble :
$ ls -F1
HEAD
config*
description
hooks/
info/
objects/
refs/
Vous y verrez sans doute d’autres fichiers, mais ceci est un dépôt qui
vient d’être créé avec git init
et c’est ce que vous verrez par
défaut. Le fichier description
est utilisé uniquement par le programme
GitWeb, il ne faut donc pas s’en soucier. Le fichier config
contient
les options de configuration spécifiques à votre projet et le répertoire
info
contient un fichier d’exclusions listant les motifs que vous
souhaitez ignorer et que vous ne voulez pas mettre dans un fichier
.gitignore
. Le répertoire hooks
contient les scripts de procédures
automatiques côté client ou serveur, ils sont décrits en détail dans
Crochets Git.
Il reste quatre éléments importants : les fichiers HEAD
et (pas encore
créé) index
, ainsi que les répertoires objects
et refs
. Ce sont
les composants principaux d’un dépôt Git. Le répertoire objects
stocke
le contenu de votre base de données, le répertoire refs
stocke les
pointeurs vers les objets commit de ces données (branches), le fichier
HEAD
pointe sur la branche qui est en cours dans votre répertoire de
travail et le fichier index
est l’endroit où Git stocke les
informations sur la zone d’attente. Vous allez maintenant plonger en
détail dans chacune de ces sections et voir comment Git fonctionne.
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
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
Conceptuellement, les données que Git stocke ressemblent ceci :
Figure 149. 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 :
Figure 150. 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
Vous obtiendrez une valeur de hashage différente à cause d’un moment de
création et d’une information d’auteur différents. Remplacez les valeurs
de hashage de commit et d’étiquette par vos propres valeurs de somme
de contrôle plus loin dans ce chapitre. Vous pouvez voir votre nouvel
objet commit avec 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 :
Figure 151. Tous les objets 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"
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
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.
Références Git
On peut exécuter quelque chose comme git log 1a410e
pour visualiser
tout l’historique, mais il faut se souvenir que 1a410e
est le dernier
commit afin de parcourir l’historique et trouver tous ces objets. Vous
avez besoin d’un fichier dans lequel vous pouvez stocker l’empreinte
SHA-1 sous un nom simple afin d’utiliser ce pointeur plutôt que
l’empreinte SHA-1 elle-même.
Git appelle ces pointeurs des « références », ou « refs ». On trouve les
fichiers contenant des empreintes SHA-1 dans le répertoire git/refs
.
Dans le projet actuel, ce répertoire ne contient aucun fichier, mais
possède une structure simple :
$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
$ find .git/refs -type f
Pour créer une nouvelle référence servant à se souvenir du dernier commit, vous pouvez simplement faire ceci :
$ echo "1a410efbd13591db07496601ebc7a059dd55cfe9" > .git/refs/heads/master
Vous pouvez maintenant utiliser la référence principale que vous venez de créer à la place de l’empreinte SHA-1 dans vos commandes Git :
$ git log --pretty=oneline master
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
Il n’est pas conseillé d’éditer directement les fichiers des références.
Git propose une manière sûre de mettre à jour une référence, c’est la
commande update-ref
:
$ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9
C’est simplement ce qu’est une branche dans Git : un simple pointeur ou référence sur le dernier état d’une suite de travaux. Pour créer une branche à partir du deuxième commit, vous pouvez faire ceci :
$ git update-ref refs/heads/test cac0ca
Cette branche contiendra seulement le travail effectué jusqu’à ce commit :
$ git log --pretty=oneline test
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
La base de donnée Git ressemble maintenant à quelque chose comme ceci :
Figure 152. Le répertoire d’objets de Git avec les références de branches incluses.
Quand vous exécutez une commande comme git branch (nomdebranche)
, Git
exécute simplement la commande update-ref
pour ajouter l’empreinte
SHA-1 du dernier commit de la branche sur laquelle vous êtes quelle
que soit la nouvelle référence que vous voulez créer.
La branche HEAD
On peut se poser la question : « Comment Git peut avoir connaissance de
l’empreinte SHA-1 du dernier commit quand on exécute
git branch (branchname)
? » La réponse est dans le fichier HEAD (qui
veut dire tête en français, soit, ici, l’état courant).
Le fichier HEAD est une référence symbolique à la branche courante. Par référence symbolique, j’entends que contrairement à une référence normale, elle ne contient pas une empreinte SHA-1, mais plutôt un pointeur vers une autre référence. Si vous regardez ce fichier, vous devriez voir quelque chose comme ceci :
$ cat .git/HEAD
ref: refs/heads/master
Si vous exécutez git checkout test
, Git met à jour ce fichier, qui
ressemblera à ceci :
$ cat .git/HEAD
ref: refs/heads/test
Quand vous exécutez git commit
, il crée l’objet commit en spécifiant
le parent de cet objet commit quelle que soit l’empreinte SHA-1
pointée par la référence de HEAD.
On peut éditer manuellement ce fichier, mais encore une fois, il existe
une commande plus sûre pour le faire : symbolic-ref
. Vous pouvez lire
le contenu de votre fichier HEAD avec cette commande :
$ git symbolic-ref HEAD
refs/heads/master
Vous pouvez aussi définir la valeur de HEAD :
$ git symbolic-ref HEAD refs/heads/test
$ cat .git/HEAD
ref: refs/heads/test
Vous ne pouvez pas définir une référence symbolique à une valeur non contenu dans refs :
$ git symbolic-ref HEAD test
fatal: Refusing to point HEAD outside of refs/
Étiquettes
Nous venons de parcourir les trois types d’objets utilisés par Git, mais il en existe un quatrième. L’objet étiquette (tag en anglais) ressemble beaucoup à un objet commit. Il contient un étiqueteur, une date, un message et un pointeur. La principale différence est que l’étiquette pointe en général vers un commit plutôt qu’un arbre. C’est comme une référence à une branche, mais elle ne bouge jamais : elle pointe toujours vers le même commit, lui donnant un nom plus sympathique.
Comme présenté au Les bases de Git, il existe deux types d’étiquettes : annotée et légère. Vous pouvez créer une étiquette légère comme ceci :
$ git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d
C’est tout ce qu’est une étiquette légère : une référence qui n’est
jamais modifiée. Une étiquette annotée est plus complexe. Quand on crée
une étiquette annotée, Git crée un objet étiquette, puis enregistre une
référence qui pointe vers lui plutôt que directement vers le commit.
Vous pouvez voir ceci en créant une étiquette annotée (-a
spécifie que
c’est une étiquette annotée) :
$ git tag -a v1.1 1a410efbd13591db07496601ebc7a059dd55cfe9 -m 'test tag'
Voici l’empreinte SHA-1 de l’objet créé :
$ cat .git/refs/tags/v1.1
9585191f37f7b0fb9444f35a9bf50de191beadc2
Maintenant, exécutez la commande cat-file
sur cette empreinte SHA-1 :
$ git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2
object 1a410efbd13591db07496601ebc7a059dd55cfe9
type commit
tag v1.1
tagger Scott Chacon <schacon@gmail.com> Sat May 23 16:48:58 2009 -0700
test tag
Remarquez que le contenu de l’objet pointe vers l’empreinte SHA-1 du commit que vous avez étiqueté. Remarquez qu’il n’est pas nécessaire qu’il pointe vers un commit. On peut étiqueter n’importe quel objet. Par exemple, dans le code source de Git, le mainteneur a ajouté sa clé publique GPG dans un blob et a étiqueté ce blob. Vous pouvez voir la clé publique en exécutant ceci sur un clone du dépôt Git :
$ git cat-file blob junio-gpg-pub
Le noyau Linux contient aussi une étiquette ne pointant pas vers un commit : la première étiquette créée pointe vers l’arbre initial lors de l’importation du code source.
Références distantes
Le troisième type de références que l’on étudiera sont les références
distantes (remotes). Si l’on ajoute une référence distante et que l’on
pousse des objets vers elle, Git stocke la valeur que vous avez poussée
en dernier vers cette référence pour chaque branche dans le répertoire
refs/remotes
. Vous pouvez par exemple ajouter une référence distante
nommée origin
et y pousser votre branche master
:
$ git remote add origin git@github.com:schacon/simplegit-progit.git
$ git push origin master
Counting objects: 11, done.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 716 bytes, done.
Total 7 (delta 2), reused 4 (delta 1)
To git@github.com:schacon/simplegit-progit.git
a11bef0..ca82a6d master -> master
Ensuite, vous pouvez voir l’état de la branche master
dans la
référence distante origin
la dernière fois que vous avez communiqué
avec le serveur en regardant le fichier refs/remotes/origin/master
:
$ cat .git/refs/remotes/origin/master
ca82a6dff817ec66f44342007202690a93763949
Les références distantes diffèrent des branches (références
refs/heads
) principalement parce qu’on ne peut y accéder qu’en lecture
seule. Vous pouvez éxécuter git checkout
sur l’une d’entre elles, mais
Git ne fera jamais pointer HEAD sur l’une d’elles, donc vous ne pourrez
jamais en mettre une à jour en utilisant une commande commit
. Git les
gère comme des marque-pages du dernier état connu de vers quoi ces
branches pointent sur le serveur.
Fichiers groupés
Revenons à la base de donnée d’objets de notre dépôt Git de test. Pour l’instant, elle contient 11 objets : 4 blobs, 3 arbres, 3 commits et 1 étiquette :
$ 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/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag
.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
Git compresse le contenu de ces fichiers avec zlib et on ne stocke pas
grand chose ; au final, tous ces fichiers occupent seulement 925 octets.
Ajoutons de plus gros contenu au dépôt pour montrer une fonctionnalité
intéressante de Git. Pour la démonstration, nous allons ajouter le
fichier repo.rb
de la bibliothèque Grit. Il représente environ 22 ko
de code source :
$ curl https://raw.githubusercontent.com/mojombo/grit/master/lib/grit/repo.rb > repo.rb
$ git checkout master
$ git add repo.rb
$ git commit -m 'added repo.rb'
[master 484a592] added repo.rb
3 files changed, 709 insertions(+), 2 deletions(-)
delete mode 100644 bak/test.txt
create mode 100644 repo.rb
rewrite test.txt (100%)
Si vous observez l’arbre qui en résulte, vous verrez l’empreinte SHA-1
du blob contenant le fichier repo.rb
:
$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt
Vous pouvez vérifier la taille de l’objet sur disque à l’aide de
git cat-file
:
$ git cat-file -s 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5
22044
Maintenant, modifiez légèrement le fichier et voyez ce qui arrive :
$ echo '# testing' >> repo.rb
$ git commit -am 'modified repo.rb a bit'
[master 2431da6] modified repo.rb a bit
1 file changed, 1 insertion(+)
Regardez l’arbre créé par ce commit et vous verrez quelque chose d’intéressant :
$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob b042a60ef7dff760008df33cee372b945b6e884e repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt
Ce blob est un blob différent. Bien que l’on ait ajouté une seule ligne à la fin d’un fichier en faisant 400, Git enregistre ce nouveau contenu dans un objet totalement différent :
$ git cat-file -s b042a60ef7dff760008df33cee372b945b6e884e
22054
Il y a donc deux objets de 22 ko quasiment identiques sur le disque. Ne serait-ce pas charmant si Git pouvait n’enregistrer qu’un objet en entier, le deuxième n’étant qu’un delta (une différence) avec le premier ?
Il se trouve que c’est possible. Le format initial dans lequel Git
enregistre les objets sur le disque est appelé le format brut (loose
object). De temps en temps, Git compacte plusieurs de ces objets en un
seul fichier binaire appelé packfile (fichier groupé), afin
d’économiser de l’espace et d’être plus efficace. Git effectue cette
opération quand il y a trop d’objets au format brut, ou si l’on exécute
manuellement la commande git gc
, ou encore quand on pousse vers un
serveur distant. Pour voir cela en action, vous pouvez demander
manuellement à Git de compacter les objets en exécutant la commande
git gc
:
$ git gc
Counting objects: 18, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (18/18), done.
Total 18 (delta 3), reused 0 (delta 0)
Si l’on jette un œil dans le répertoire des objets, on constatera que la plupart des objets ne sont plus là et qu’un couple de fichiers est apparu :
$ find .git/objects -type f
.git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/info/packs
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack
Les objets restant sont des blobs qui ne sont pointés par aucun commit. Dans notre cas, il s’agit des blobs « what is up, doc? » et « test content » créés plus tôt comme exemple. Puisqu’ils n’ont été ajoutés à aucun commit, ils sont considérés en suspend et ne sont pas compactés dans le nouveau fichier groupé.
Les autres fichiers sont le nouveau fichier groupé et un index. Le
fichier groupé est un fichier unique rassemblant le contenu de tous les
objets venant d’être supprimés du système de fichier. L’index est un
fichier contenant les emplacements dans le fichier groupé, pour que l’on
puisse accéder rapidement à un objet particulier. Ce qui est vraiment
bien, c’est que les objets occupaient environ 15 ko d’espace disque
avant gc
et que le nouveau fichier groupé en occupe seulement 7. On a
réduit l’occupation du disque de ½ en regroupant les objets.
Comment Git réalise-t-il cela ? Quand Git compacte des objets, il
recherche les fichiers qui ont des noms et des tailles similaires, puis
enregistre seulement les deltas entre une version du fichier et la
suivante. On peut regarder à l’intérieur du fichier groupé et voir
l’espace économisé par Git. La commande de plomberie git verify-pack
vous permet de voir ce qui a été compacté :
$ git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
2431da676938450a4d72e260db3bf7b0f587bbc1 commit 223 155 12
69bcdaff5328278ab1c0812ce0e07fa7d26a96d7 commit 214 152 167
80d02664cb23ed55b226516648c7ad5d0a3deb90 commit 214 145 319
43168a18b7613d1281e5560855a83eb8fde3d687 commit 213 146 464
092917823486a802e94d727c820a9024e14a1fc2 commit 214 146 610
702470739ce72005e2edff522fde85d52a65df9b commit 165 118 756
d368d0ac0678cbe6cce505be58126d3526706e54 tag 130 122 874
fe879577cb8cffcdf25441725141e310dd7d239b tree 136 136 996
d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree 36 46 1132
deef2e1b793907545e50a2ea2ddb5ba6c58c4506 tree 136 136 1178
d982c7cb2c2a972ee391a85da481fc1f9127a01d tree 6 17 1314 1 \
deef2e1b793907545e50a2ea2ddb5ba6c58c4506
3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree 8 19 1331 1 \
deef2e1b793907545e50a2ea2ddb5ba6c58c4506
0155eb4229851634a0f03eb265b69f5a2d56f341 tree 71 76 1350
83baae61804e65cc73a7201a7252750c76066a30 blob 10 19 1426
fa49b077972391ad58037050f2a75f74e3671e92 blob 9 18 1445
b042a60ef7dff760008df33cee372b945b6e884e blob 22054 5799 1463
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 9 20 7262 1 \
b042a60ef7dff760008df33cee372b945b6e884e
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob 10 19 7282
non delta: 15 objects
chain length = 1: 3 objects
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack: ok
Ici, le blob 033b4
, qui, si on se souvient bien, était la première
version du fichier repo.rb
, référence le blob b042a
, qui est la
seconde version du fichier. La troisième colonne de l’affichage est la
taille de l’objet dans le fichier compact et on peut voir que b042a
occupe 22 ko dans le fichier, mais que 033b4
occupe seulement 9
octets. Ce qui est aussi intéressant est que la seconde version du
fichier est celle qui est enregistrée telle quelle, tandis que la
version originale est enregistrée sous forme d’un delta. La raison en
est que vous aurez sans doute besoin d’accéder rapidement aux versions
les plus récentes du fichier.
Une chose intéressante à propos de ceci est que l’on peut recompacter à
tout moment. Git recompacte votre base de donnée occasionnellement, en
essayant d’économiser de la place. Vous pouvez aussi recompacter à la
main, en exécutant la commande git gc
vous-même.
La refspec
Tout au long de ce livre, nous avons utilisé des associations simples entre les branches distantes et les références locales. Elles peuvent être plus complexes. Supposons que vous ajoutiez un dépôt distant comme ceci :
$ git remote add origin https://github.com/schacon/simplegit-progit
Cela ajoute une section au fichier .git/config
, contenant le nom du
dépôt distant (origin
), l’URL de ce dépôt et la refspec pour la
récupération :
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/*:refs/remotes/origin/*
Le format de la refspec est un +
facultatif, suivi de <src>:<dst>
,
où <src>
est le motif des références du côté distant et <dst>
est
l’emplacement local où les références seront enregistrées. Le +
précise à Git de mettre à jour la référence même si ce n’est pas une
avance rapide.
Dans le cas par défaut, qui est celui d’un enregistrement automatique
par la commande git remote add origin
, Git récupère toutes les
références de refs/heads/
sur le serveur et les enregistre localement
dans refs/remotes/origin/
. Ainsi, s’il y a une branche master
sur le
serveur, vous pouvez accéder localement à l’historique de cette branche
via :
$ git log origin/master
$ git log remotes/origin/master
$ git log refs/remotes/origin/master
Ces syntaxes sont toutes équivalentes, car Git les développe en
refs/remotes/origin/master
.
Si vous préférez que Git récupère seulement la branche master
et non
chacune des branches du serveur distant, vous pouvez remplacer la ligne
fetch par :
fetch = +refs/heads/master:refs/remotes/origin/master
C’est la refspec par défaut de git fetch
pour ce dépôt distant. Si
l’on veut effectuer une action particulière une seule fois, la refspec
peut aussi être précisée en ligne de commande. Pour tirer la branche
master
du dépôt distant vers la branche locale origin/mymaster
, vous
pouvez exécuter :
$ git fetch origin master:refs/remotes/origin/mymaster
Vous pouvez indiquer plusieurs refspecs. En ligne de commande, vous pouvez tirer plusieurs branches de cette façon :
$ git fetch origin master:refs/remotes/origin/mymaster \
topic:refs/remotes/origin/topic
From git@github.com:schacon/simplegit
! [rejected] master -> origin/mymaster (non fast forward)
* [new branch] topic -> origin/topic
Dans ce cas, la récupération (pull) de la branche master
a été
refusée car ce n’était pas une avance rapide. On peut surcharger ce
comportement en précisant un +
devant la refspec.
On peut aussi indiquer plusieurs refspecs pour la récupération, dans
le fichier de configuration. Si vous voulez toujours récupérer les
branches master
et experiment
, ajoutez ces deux lignes :
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/master:refs/remotes/origin/master
fetch = +refs/heads/experiment:refs/remotes/origin/experiment
Vous ne pouvez pas utiliser des jokers partiels, ce qui suit est donc invalide :
fetch = +refs/heads/qa*:refs/remotes/origin/qa*
On peut toutefois utiliser des espaces de noms (namespaces) ou des
répertoires pour accomplir cela. S’il existe une équipe qualité (QA) qui
publie une série de branches et que l’on veut la branche master
, les
branches de l’équipe qualité et rien d’autre, on peut utiliser la
configuration suivante :
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/master:refs/remotes/origin/master
fetch = +refs/heads/qa/*:refs/remotes/origin/qa/*
Si vous utilisez des processus complexes impliquant une équipe qualité, des développeurs et des intégrateurs qui publient des branches et qui collaborent sur des branches distantes, vous pouvez facilement utiliser des espaces de noms de cette façon.
Pousser des refspecs
Il est pratique de pouvoir récupérer des références issues d’espace de
nom de cette façon, mais comment l’équipe qualité insère-t-elle ces
branches dans l’espace de nom qa/
en premier lieu ? On peut accomplir
cela en utilisant les spécifications de références pour la publication.
Si l’équipe qualité veut publier sa branche master
vers qa/master
sur le serveur distant, elle peut exécuter :
$ git push origin master:refs/heads/qa/master
Si elle veut que Git le fasse automatiquement à chaque exécution de
git push origin
, elle peut ajouter une entrée push
au fichier de
configuration :
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/*:refs/remotes/origin/*
push = refs/heads/master:refs/heads/qa/master
De même, cela fera que, par défaut, git push origin
publiera la
branche locale master
sur la branche distante qa/master
.
Supprimer des références
Vous pouvez aussi utiliser les refspecs pour supprimer des références sur le serveur distant en exécutant une commande comme :
$ git push origin :topic
La refspec ressemble à <src>:<dst>
, mais en laissant vide la partie
<src>
, cela signifie une création de la branche à partir de rien et
donc sa suppression.
Les protocoles de transfert
Git peut transférer des données entre deux dépôts de deux façons principales : le protocole « stupide » et le protocole « intelligent ».
Cette section fait un tour d’horizon du fonctionnement de ces deux protocoles.
Le protocole stupide
Si vous mettez en place un dépôt à accéder en lecture seule sur HTTP, c’est vraisemblablement le protocole stupide qui sera utilisé.
Ce protocole est dit « stupide », car il ne nécessite aucun code spécifique à Git côté serveur durant le transfert ; le processus de récupération est une série de requêtes GET, où le client devine la structure du dépôt Git présent sur le serveur.
Le protocole stupide est rarement utilisé ces derniers temps. Il est difficile de le rendre sécurisé ou privé, et donc la plupart des hébergeurs Git (sur le cloud ou sur serveur dédié) refusent de l’utiliser. On conseille généralement d’utiliser le protocole intelligent, qui est décrit plus loin. |
Suivons le processus http-fetch
pour la bibliothèque simplegit :
$ git clone http://server/simplegit-progit.git
La première chose que fait cette commande est de récupérer le fichier
info/refs
. Ce fichier est écrit par la commande update-server-info
et c’est pour cela qu’il faut activer le crochet post-receive
, sinon
le transfert HTTP ne fonctionnera pas correctement :
> GET info/refs
ca82a6dff817ec66f44342007202690a93763949 refs/heads/master
On possède maintenant une liste des références distantes et empreintes SHA-1. Ensuite, on regarde vers quoi pointe HEAD, pour savoir sur quelle branche se placer quand on aura fini :
> GET HEAD
ref: refs/heads/master
On aura besoin de se placer sur la branche master
, quand le processus
sera terminé. On est maintenant prêt à démarrer le processus de
parcours. Puisque votre point de départ est l’objet commit ca82a6
que vous avez vu dans le fichier info/refs
, vous commencez par le
récupérer :
> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)
Vous obtenez un objet, cet objet est dans le format brut sur le serveur et vous l’avez récupéré à travers une requête HTTP GET statique. Vous pouvez le décompresser avec zlib, ignorer l’en-tête et regarder le contenu du commit :
$ git cat-file -p ca82a6dff817ec66f44342007202690a93763949
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700
changed the version number
Puis, vous avez deux autres objets supplémentaires à récupérer :
cfda3b
qui est l’arbre du contenu sur lequel pointe le commit que
nous venons de récupérer et 085bb3
qui est le commit parent :
> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)
Cela vous donne le prochain objet commit. Récupérez l’objet arbre :
> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)
Oups, on dirait que l’objet arbre n’est pas au format brut sur le serveur, vous obtenez donc une réponse 404. On peut en déduire certaines raisons : l’objet peut être dans un dépôt suppléant ou il peut être dans un fichier groupé de ce dépôt. Git vérifie la liste des dépôts suppléants d’abord :
> GET objects/info/http-alternates
(empty file)
Si la réponse contenait une liste d’URL suppléantes, Git aurait cherché
les fichiers bruts et les fichiers groupés à ces emplacements, c’est un
mécanisme sympathique pour les projets qui ont dérivé d’un autre pour
partager les objets sur le disque. Cependant, puisqu’il n’y a pas de
suppléants listés dans ce cas, votre objet doit se trouver dans un
fichier groupé. Pour voir quels fichiers groupés sont disponibles sur le
serveur, vous avez besoin de récupérer le fichier objects/info/packs
,
qui en contient la liste (générée également par update-server-info
) :
> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
Il n’existe qu’un seul fichier groupé sur le serveur, votre objet se trouve évidemment dedans, mais vous allez tout de même vérifier l’index pour être sûr. C’est également utile lorsque vous avez plusieurs fichiers groupés sur le serveur, vous pouvez donc voir quel fichier groupé contient l’objet dont vous avez besoin :
> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)
Maintenant que vous avez l’index du fichier groupé, vous pouvez vérifier si votre objet est bien dedans car l’index liste les empreintes SHA-1 des objets contenus dans ce fichier groupé et des emplacements de ces objets. Votre objet est là, allez donc récupérer le fichier groupé complet :
> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data)
Vous avez votre objet arbre, vous continuez donc le chemin des
commits. Ils sont également tous contenus dans votre fichier groupé
que vous venez de télécharger, vous n’avez donc pas d’autres requêtes à
faire au serveur. Git récupère une copie de travail de votre branche
master
qui été référencée par HEAD que vous avez téléchargé au début.
Le protocole intelligent
Le protocole stupide est simple mais un peu inefficace, et il ne permet pas l’écriture de données du client au serveur. Le protocole intelligent est une méthode plus habituelle pour transférer des données, mais elle nécessite l’exécution sur le serveur d’un processus qui connaît Git : il peut lire les données locales et déterminer ce que le client a ou ce dont il a besoin pour générer un fichier groupé personnalisé pour lui. Il y a deux ensembles d’exécutables pour transférer les données : une paire pour téléverser des données et une paire pour en télécharger.
Téléverser des données
Pour téléverser des données vers un exécutable distant, Git utilise les
exécutables send-pack
et receive-pack
. L’exécutable send-pack
tourne sur le client et se connecte à l’exécutable receive-pack
du
côté serveur.
SSH
Par exemple, disons que vous exécutez git push origin master
dans
votre projet et origin
est défini comme une URL qui utilise le
protocole SSH. Git appelle l’exécutable send-pack
, qui initialise une
connexion à travers SSH vers votre serveur. Il essaye d’exécuter une
commande sur le serveur distant via un appel SSH qui ressemble à :
$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"
00a5ca82a6dff817ec66f4437202690a93763949 refs/heads/master report-status \
delete-refs side-band-64k quiet ofs-delta \
agent=git/2:2.1.1+github-607-gfba4028 delete-refs
0000
La commande git-receive-pack
répond immédiatement avec une ligne pour
chaque référence qu’elle connaît actuellement, dans ce cas, uniquement
la branche master
et son empreinte SHA-1. La première ligne contient
également une liste des compétences du serveur (ici : report-status
,
delete-refs
et quelques autres, dont l’identifiant du client).
Chaque ligne commence avec une valeur hexadécimale sur 4 caractères,
spécifiant le reste de la longueur de la ligne. La première ligne, ici,
commence avec 00a5
, soit 165 en hexadécimal, ce qui signifie qu’il y a
165 octets restants sur cette ligne. La ligne d’après est 0000
,
signifiant que le serveur a fini de lister ses références.
Maintenant qu’il connait l’état du serveur, votre exécutable send-pack
détermine quels commits il a de plus que le serveur. L’exécutable
send-pack
envoie alors à l’exécutable receive-pack
les informations
concernant chaque référence que cette commande push
va mettre à jour.
Par exemple, si vous mettez à jour la branche master
et ajoutez la
branche experiment
, la réponse de send-pack
ressemblera à quelque
chose comme :
0076ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 \
refs/heads/master report-status
006c0000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d \
refs/heads/experiment
0000
Git envoie une ligne pour chaque référence que l’on met à jour avec
l’ancien SHA-1, le nouveau SHA-1 et la référence en train d’être mise à
jour. La première ligne contient également les compétences du client. La
valeur SHA-1 remplie de 0 signifie qu’il n’y avait rien à cet endroit
avant, car vous êtes en train d’ajouter la référence experiment
. Si
vous étiez en train de supprimer une référence, vous verriez l’opposé :
que des 0 du côté droit.
Puis, le client téléverse un fichier groupé de tous les objets que le serveur n’a pas encore.
Finalement, le serveur répond avec une indication de succès (ou d’échec) :
000eunpack ok
HTTP(S)
Le processus est quasiment le même avec HTTP, à une différence près lors de l’établissement de la liaison (handshaking). La connection est amorcée avec cette requête :
> GET http://server/simplegit-progit.git/info/refs?service=git-receive-pack
001f# service=git-receive-pack
00ab6c5f0e45abd7832bf23074a333f739977c9e8188 refs/heads/master \
report-status delete-refs side-band-64k quiet ofs-delta \
agent=git/2:2.1.1~vmg-bitmaps-bugaloo-608-g116744e
0000
Ceci est la fin du premier échange client-serveur. Le client fait alors
une nouvelle requête, qui est cette fois un POST
, avec les données
fournies par git-upload-pack
.
> POST http://server/simplegit-progit.git/git-receive/pack
La requête POST
contient la sortie de send-pack
et le fichier
groupé. Enfin, le serveur indique le succès ou l’échec dans sa réponse
HTTP.
Téléchargement des données
Lorsque vous téléchargez des données, les exécutables fetch-pack
et
upload-pack
entrent en jeu. Le client démarre un processus
fetch-pack
qui se connecte à un processus upload-pack
du côté
serveur pour négocier les données qui seront téléchargées.
SSH
Si vous téléchargez par SSH, fetch-pack
fait quelque chose comme ceci
:
$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"
Une fois fetch-pack
connecté, upload-pack
lui répond quelque chose
du style :
00dfca82a6dff817ec66f44342007202690a93763949 HEAD multi_ack thin-pack \
side-band side-band-64k ofs-delta shallow no-progress include-tag \
multi_ack_detailed symref=HEAD:refs/heads/master \
agent=git/2:2.1.1+github-607-gfba4028
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
0000
Ceci est très proche de la réponse de receive-pack
mais les
compétences sont différentes. En plus, il envoie ce qui est pointé par
HEAD (symref=HEAD:refs/heads/master
), afin que le client sache ce
qu’il doit récupérer dans le cas d’un clone.
À ce moment, fetch-pack
regarde les objets qu’il a et répond avec la
liste des objets dont il a besoin en envoyant « want » (vouloir) suivi
du SHA-1 qu’il veut. Il envoie tous les objets qu’il a déjà avec
« have » suivi du SHA-1. À la fin de la liste, il écrit « done » (fait)
pour inciter l’exécutable upload-pack
à commencer à envoyer le fichier
groupé des données demandées :
003cwant ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0009done
0000
HTTP(S)
L’établissement de la liaison pour une opération de téléchargement
nécessite deux requêtes HTTP. La première est un GET
vers le même
point que dans le protocole stupide :
> GET $GIT_URL/info/refs?service=git-upload-pack
001e# service=git-upload-pack
00e7ca82a6dff817ec66f44342007202690a93763949 HEAD multi_ack thin-pack \
side-band side-band-64k ofs-delta shallow no-progress include-tag \
multi_ack_detailed no-done symref=HEAD:refs/heads/master \
agent=git/2:2.1.1+github-607-gfba4028
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
0000
Ceci ressemble beaucoup à un appel à git-upload-pack
par une
connection SSH, mais le deuxième échange est fait dans une requête
séparée :
> POST $GIT_URL/git-upload-pack HTTP/1.0
0032want 0a53e9ddeaddad63ad106860237bbf53411d11a7
0032have 441b40d833fdfa93eb2908e52742248faf0ee993
0000
Une fois de plus, ce format est le même que plus haut. La réponse à cette requête indique le succès ou l’échec, et contient le fichier groupé.
Résumé sur les protocoles
Cette section contient un survol basique des protocoles de transfert.
Les protocoles contiennent de nombreuses autres fonctionalités, comme
les compétences multi_ack
ou side-band
, mais leur étude est hors du
sujet de ce livre. Nous avons essayé de vous donner une idée générale
des échanges entre client et serveur. Si vous souhaitez en connaître
davantage, vous devrez probablement jeter un œil sur le code source de
Git.
Maintenance et récupération de données
Parfois, vous aurez besoin de faire un peu de ménage : rendre un dépôt plus compact, nettoyer les dépôts importés, ou récupérer du travail perdu. Cette section couvrira certains de ces scénarios.
Maintenance
De temps en temps, Git exécute automatiquement une commande appelée
« auto gc ». La plupart du temps, cette commande ne fait rien.
Cependant, s’il y a trop d’objets bruts (des objets qui ne sont pas dans
des fichiers groupés), ou trop de fichiers groupés, Git lance une
commande git gc
à part entière. « gc » est l’abréviation de « garbage
collect » (ramasse-miettes) et la commande fait plusieurs choses : elle
rassemble plusieurs objets bruts et les place dans des fichiers groupés,
elle rassemble des fichiers groupés en un gros fichier groupé et elle
supprime des objets qui ne sont plus accessibles depuis aucun commit
et qui sont vieux de plusieurs mois.
Vous pouvez exécuter auto gc
manuellement :
$ git gc --auto
Encore une fois, cela ne fait généralement rien. Vous devez avoir
environ 7 000 objets bruts ou plus de 50 fichiers groupés pour que Git
appelle une vraie commande gc
. Vous pouvez modifier ces limites avec
les propriétés de configuration gc.auto
et gc.autoPackLimit
,
respectivement.
gc
regroupera aussi vos références dans un seul fichier. Supposons que
votre dépôt contienne les branches et étiquettes suivantes :
$ find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1
Si vous exécutez git gc
, vous n’aurez plus ces fichiers dans votre
répertoire refs
. Git les déplacera pour plus d’efficacité dans un
fichier nommé .git/packed-refs
qui ressemble à ceci :
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9
Si vous mettez à jour une référence, Git ne modifiera pas ce fichier,
mais enregistrera plutôt un nouveau fichier dans refs/heads
. Pour
obtenir l’empreinte SHA-1 appropriée pour une référence donnée, Git
cherche d’abord cette référence dans le répertoire refs
, puis dans le
fichier packed-refs
si non trouvée. Si vous ne pouvez pas trouver une
référence dans votre répertoire refs
, elle est probablement dans votre
fichier packed-refs
.
Remarquez la dernière ligne du fichier, celle commençant par ^
. Cela
signifie que l’étiquette directement au-dessus est une étiquette annotée
et que cette ligne est le commit que l’étiquette annotée référence.
Récupération de données
À un moment quelconque de votre vie avec Git, vous pouvez accidentellement perdre un commit. Généralement, cela arrive parce que vous avez forcé la suppression d’une branche contenant du travail et il se trouve que vous vouliez cette branche finalement ; ou vous avez réinitialisé une branche avec suppression, en abandonnant des commits dont vous vouliez des informations. Supposons que cela arrive, comment pouvez-vous récupérer vos commits ?
Voici un exemple qui réinitialise la branche master
avec suppression
dans votre dépôt de test vers un ancien commit et qui récupère les
commits perdus. Premièrement, vérifions dans quel état est votre dépôt
en ce moment :
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
Maintenant, déplaçons la branche master
vers le commit du milieu :
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
Vous avez effectivement perdu les deux commits du haut, vous n’avez pas de branche depuis laquelle ces commits seraient accessibles. Vous avez besoin de trouver le SHA du dernier commit et d’ajouter une branche s’y référant. Le problème est de trouver ce SHA, ce n’est pas comme si vous l’aviez mémorisé, hein ?
Souvent, la manière la plus rapide est d’utiliser l’outil git reflog
.
Pendant que vous travaillez, Git enregistre l’emplacement de votre HEAD
chaque fois que vous le changez. À chaque commit ou commutation de
branche, le journal des références (reflog) est mis à jour. Le journal
des références est aussi mis à jour par la commande git update-ref
, ce
qui est une autre raison de l’utiliser plutôt que de simplement écrire
votre valeur SHA dans vos fichiers de références, comme mentionné dans
la section Références Git plus haut dans ce chapitre.
Vous pouvez voir où vous étiez à n’importe quel moment en exécutant
git reflog
:
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rb
Ici, nous pouvons voir deux commits que nous avons récupérés,
cependant, il n’y a pas plus d’information ici. Pour voir, les mêmes
informations d’une manière plus utile, nous pouvons exécuter
git log -g
, qui nous donnera une sortie normalisée pour votre journal
de références :
$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:22:37 2009 -0700
third commit
commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:15:24 2009 -0700
modified repo.rb a bit
On dirait que le commit du bas est celui que vous avez perdu, vous
pouvez donc le récupérer en créant une nouvelle branche sur ce commit.
Par exemple, vous créez une branche nommée recover-branch
sur ce
commit (ab1afef):
$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
Cool. Maintenant vous avez une nouvelle branche appelée recover-branch
à l’emplacement où votre branche master
se trouvait, rendant les deux
premiers commits à nouveau accessibles. Pour poursuivre, nous
supposerons que vos pertes ne sont pas dans le journal des références
pour une raison quelconque. On peut simuler cela en supprimant
recover-branch
et le journal des références. Maintenant, les deux
premiers commits ne sont plus accessibles :
$ git branch -D recover-branch
$ rm -Rf .git/logs/
Comme les données du journal de référence sont sauvegardées dans le
répertoire .git/logs/
, vous n’avez effectivement plus de journal de
références. Comment pouvez-vous récupérer ces commits maintenant ? Une
manière de faire est d’utiliser l’outil git fsck
, qui vérifie
l’intégrité de votre base de données. Si vous l’exécutez avec l’option
--full
, il vous montre tous les objets qui ne sont pas référencés par
d’autres objets :
$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
Dans ce cas, vous pouvez voir votre commit manquant après « dangling commit ». Vous pouvez le restaurer de la même manière que précédemment, en créant une branche qui référence cette empreinte SHA-1.
Suppression d’objets
Il y a beaucoup de choses dans Git qui sont géniales, mais une
fonctionnalité qui peut poser problème est le fait que git clone
télécharge l’historique entier du projet, incluant chaque version de
chaque fichier. C’est très bien lorsque le tout est du code source,
parce que Git est hautement optimisé pour compresser les données
efficacement. Cependant, si quelqu’un à un moment donné de l’historique
de votre projet a ajouté un énorme fichier, chaque clone sera forcé de
télécharger cet énorme fichier, même s’il a été supprimé du projet dans
le commit suivant. Puisqu’il est accessible depuis l’historique, il
sera toujours là.
Cela peut être un énorme problème, lorsque vous convertissez un dépôt Subversion ou Perforce en un dépôt Git. Comme vous ne téléchargez pas l’historique entier dans ces systèmes, ce genre d’ajout n’a que peu de conséquences. Si vous avez importé depuis un autre système ou que votre dépôt est beaucoup plus gros que ce qu’il devrait être, voici comment vous pouvez trouver et supprimer des gros objets.
Soyez prévenu : cette technique détruit votre historique de commit. Elle réécrit chaque objet commit depuis le premier objet arbre que vous modifiez pour supprimer une référence d’un gros fichier. Si vous faites cela immédiatement après un import, avant que quiconque n’ait eu le temps de commencer à travailler sur ce commit, tout va bien. Sinon, vous devez alerter tous les contributeurs qu’ils doivent rebaser leur travail sur vos nouveaux commits.
Pour la démonstration, nous allons ajouter un gros fichier dans votre dépôt de test, le supprimer dans le commit suivant, le trouver et le supprimer de manière permanente du dépôt. Premièrement, ajoutons un gros objet à votre historique :
$ curl https://www.kernel.org/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'add git tarball'
[master 7b30847] add git tarball
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 git.tgz
Oups, vous ne vouliez pas ajouter une énorme archive à votre projet. Il vaut mieux s’en débarrasser :
$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'oops - removed large tarball'
[master dadf725] oops - removed large tarball
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 git.tgz
Maintenant, faites un gc
sur votre base de données, pour voir combien
d’espace disque vous utilisez :
$ git gc
Counting objects: 17, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0)
Vous pouvez exécuter la commande count-objects
pour voir rapidement
combien d’espace disque vous utilisez :
$ git count-objects -v
count: 7
size: 32
in-pack: 17
packs: 1
size-pack: 4868
prune-packable: 0
garbage: 0
size-garbage: 0
L’entrée size-pack
est la taille de vos fichiers groupés en
kilo-octet, vous utilisez donc presque 5 Mo. Avant votre dernier
commit, vous utilisiez environ 2 ko ; clairement, supprimer le fichier
avec le commit précédent ne l’a pas enlevé de votre historique. À
chaque fois que quelqu’un clonera votre dépôt, il aura à cloner les 5 Mo
pour récupérer votre tout petit projet, parce que vous avez
accidentellement rajouté un gros fichier. Débarrassons-nous en.
Premièrement, vous devez le trouver. Dans ce cas, vous savez déjà de
quel fichier il s’agit. Mais supposons que vous ne le sachiez pas,
comment identifieriez-vous quel(s) fichier(s) prennent trop de place ?
Si vous exécutez git gc
, tous les objets sont dans des fichiers
groupés ; vous pouvez identifier les gros objets en utilisant une autre
commande de plomberie appelée git verify-pack
et en triant sur le
troisième champ de la sortie qui est la taille des fichiers. Vous pouvez
également le faire suivre à la commande tail
car vous ne vous
intéressez qu’aux fichiers les plus gros :
$ git verify-pack -v .git/objects/pack/pack-29…69.idx \
| sort -k 3 -n \
| tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob 4975916 4976258 1438
Le gros objet est à la fin : 5 Mio. Pour trouver quel fichier c’est,
vous allez utiliser la commande rev-list
, que vous avez utilisée
brièvement dans Application d’une politique de format du message de
validation. Si vous mettez l’option
--objects
à rev-list
, elle listera tous les SHA des commits et des
blobs avec le chemin du fichier associé. Vous pouvez utiliser cette
commande pour trouver le nom de votre blob :
$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
Maintenant, vous voulez supprimer ce fichier de toutes les arborescences passées. Vous pouvez facilement voir quels commits ont modifié ce fichier :
$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball
Vous devez réécrire tous les commits en descendant depuis 7b30847
pour supprimer totalement ce fichier de votre historique Git. Pour cela,
utilisez filter-branch
, que vous avez utilisée dans le chapitre
Réécrire l’historique :
$ git filter-branch --index-filter \
'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz'
Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2)
Ref 'refs/heads/master' was rewritten
L’option --index-filter
est similaire à l’option --tree-filter
utilisée dans le chapitre Réécrire l’historique,
sauf qu’au lieu de modifier les fichiers sur le disque, vous modifiez
votre index.
Plutôt que de supprimer un fichier spécifique avec une commande comme
rm file
, vous devez le supprimer avec git rm --cached
; vous devez
le supprimer de l’index, pas du disque. La raison de faire cela de cette
manière est la rapidité, car Git n’ayant pas besoin de récupérer chaque
révision sur disque avant votre filtre, la procédure peut être beaucoup,
beaucoup plus rapide. Vous pouvez faire la même chose avec
--tree-filter
si vous voulez. L’option --ignore-unmatch
de git rm
lui dit que ce n’est pas une erreur si le motif que vous voulez
supprimer n’existe pas. Finalement, vous demandez à filter-branch
de
réécrire votre historique seulement depuis le parent du commit
7b30847
, car vous savez que c’est de là que le problème a commencé.
Sinon, il aurait démarré du début et serait plus long inutilement.
Votre historique ne contient plus de référence à ce fichier. Cependant,
votre journal de révision et un nouvel ensemble de références que Git a
ajouté lors de votre filter-branch
dans .git/refs/original
en
contiennent encore, vous devez donc les supprimer puis regrouper votre
base de données. Vous devez vous débarrasser de tout ce qui fait
référence à ces vieux commits avant de regrouper :
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 12 (delta 0)
Voyons combien d’espace vous avez récupéré :
$ git count-objects -v
count: 11
size: 4904
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0
La taille du dépôt regroupé est retombée à 8 ko, ce qui est beaucoup
mieux que 5 Mo. Vous pouvez voir dans la valeur « size » que votre gros
objet est toujours dans vos objets bruts, il n’est donc pas parti ; mais
il ne sera plus transféré lors d’une poussée vers un serveur ou un
clone, ce qui est l’important dans l’histoire. Si vous le voulez
réellement, vous pouvez supprimer complètement l’objet en exécutant
git prune
avec l’option --expire
:
$ git prune --expire now
$ git count-objects -v
count: 0
size: 0
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0
Les variables d’environnement
Git s’exécute toujours dans un shell bash
, et utilise un certain
nombre de variables d’environnement pour savoir comment se comporter. Il
est parfois pratique de savoir lesquelles, et la façon de les utiliser
pour que Git se comporte comme vous le souhaitez. Ceci n’est pas une
liste exhaustive de toutes les variables d’environnement que Git
utilise, mais nous allons voir les plus utiles.
Comportement général
Certains aspects du comportement général de Git en tant que programme dépend de variables d’environnement.
GIT_EXEC_PATH
détermine l’endroit où Git va chercher ses
sous-programmes (comme git-commit
, git-diff
, et d’autres). Vous
pouvez vérifier le réglage actuel en lançant git --exec-path
.
HOME
n’est pas en général considérée comme modifiable (trop
d’autres choses en dépendent), mais c’est l’endroit où Git va chercher
le fichier de configuration général (global). Si vous voulez une
installation de Git vraiment portable, complète du point de vue de la
configuration générale, vous pouvez surcharger HOME
dans le profil
(profile).
PREFIX
est l’équivalent pour la configuration au niveau du
système. Git va chercher le fichier $PREFIX/etc/gitconfig
.
GIT_CONFIG_NOSYSTEM
, si elle est définie, invalide l’utilisation
du fichier de configuration au niveau du système. Cette variable est
utile si la configuration système interfère avec vos commandes et que
vous n’avez pas les privilèges pour la changer ou la supprimer.
GIT_PAGER
contrôle le programme que vous utilisez pour afficher
les résultats sur plusieurs pages à la ligne de commande. Si elle n’est
pas définie, Git utilisera PAGER
à la place.
GIT_EDITOR
est l’éditeur lancé par Git quand l’utilisateur doit
taper du texte (un message de commit par exemple). Si elle n’est pas
définie, Git utilisera EDITOR
.
Les emplacements du dépôt
Git utilise plusieurs variables d’environnement pour déterminer comment interagir avec le dépôt courant.
GIT_DIR
est l’emplacement du répertoire .git
. S’il n’est pas
spécifié, Git remonte l’arbre des répertoires jusqu’à ce qu’il arrive à
~
ou bien /
, en cherchant un répertoire .git
à chaque étape.
GIT_CEILING_DIRECTORIES
contrôle le comportement de Git pendant la
recherche d’un répertoire .git
. Si vous êtes sur des répertoires qui
se chargent lentement (par exemple sur une bande magnétique ou à travers
une connexion réseau lente), vous pouvez souhaiter que Git s’arrête plus
tôt qu’il ne le ferait habituellemnt, surtout si Git est appelé à la
construction de votre appel shell (prompt).
GIT_WORK_TREE
est l’emplacement de la racine du répertoire de
travail pour un dépôt non nu. Si cette variable n’est pas spécifiée,
c’est le répertoire parent de $GIT_DIR
qui est utilisé.
GIT_INDEX_FILE
est le chemin du fichier d’index (uniquement pour
les dépôts non nus).
GIT_OBJECT_DIRECTORY
peut être utilisé pour spécifier
l’emplacement du répertoire qui se trouve habituellement à
.git/objects
.
GIT_ALTERNATE_OBJECT_DIRECTORIES
est une liste séparée par des
« : » (formattée comme ceci : /rep/un:/rep/deux:…
) qui dit à Git où
trouver les objets s’ils ne sont pas dans GIT_OBJECT_DIRECTORY
. S’il
vous arrive d’avoir beaucoup de projets avec des gros fichiers ayant
exactement le même contenu, cette variable peut vous éviter d’en garder
trop de copies.
Pathspecs
Une "pathspec" fait référence à la façon dont on spécifie les chemins
dans Git, y compris l’utilisation des jokers. Ils sont utilisés dans le
fichier .gitignore
, mais également à la ligne de commande
(git add *.c
).
GIT_GLOB_PATHSPECS
et GIT_NOGLOB_PATHSPECS
contrôlent le
comportement par défaut des jokers dans les pathspecs. Si
GIT_GLOB_PATHSPECS
vaut 1, les caractères jokers agissent comme des
jokers (ce qui est le comportement par défaut) ; si
GIT_NOGLOB_PATHSPECS
vaut 1, les caractères jokers ne correspondent
qu’à eux-même, ce qui veut dire que quelque chose comme *.c
ne
correspondrait qu’à un fichier nommé « *.c », et non pas tout fichier
dont le nom se termine par .c
. Vous pouvez surcharger ce comportement
pour certains cas en faisant commencer la pathspec par :(glob)
pour
utiliser le joker, ou bien :(literal)
pour une correspondance stricte,
comme dans :(glob)*.c
.
GIT_LITERAL_PATHSPECS
empêche ces deux comportements ; aucun joker
ne fonctionnera, et les préfixes de surcharge seront également
inopérants.
GIT_ICASE_PATHSPECS
rend toutes les pathspecs insensibles à la
casse.
Création de commits
La création finale d’un objet Git commit est habituellement faite par
git-commit-tree
, qui utilise les variables d’environnement suivantes
comme première source d’information, se repliant sur les valeurs de
configuration seulement si celles-ci ne sont pas présentes :
GIT_AUTHOR_NAME
est le nom lisible par un humain dans le champ
« Auteur » (author).
GIT_AUTHOR_EMAIL
est l’adresse de courriel pour le champ
« Auteur ».
GIT_AUTHOR_DATE
est l’horodatage utilisé pourle champ « Auteur ».
GIT_COMMITTER_NAME
définit le nom humain pour le champ
« Validateur » (commiter).
GIT_COMMITTER_EMAIL
est l’adresse de courriel pour le champ
« Validateur ».
GIT_COMMITTER_DATE
est utilisé pour l’horodatage dans le champ
« Validateur ».
EMAIL
est l’adresse de courriel de repli pour le cas où la valeur
de configuration user.email
n’est pas définie. Si celle-ci n’est pas
définie, Git se replie sur les noms d’utilisateur système et d’hôte.
Travail sur le réseau
Git utilise la bibliothèque curl
pour effectuer des opérations sur
HTTP, ainsi GIT_CURL_VERBOSE
demande à Git d’émettre tous les
messages générés par cette bibliothèque. C’est similaire à curl -v
en
ligne de commande.
GIT_SSL_NO_VERIFY
demande à Git de ne pas vérifier les certificats
SSL. Cela peut être parfois nécessaire si vous utilisez des certificats
auto-signés pour servir des dépôts Git sur HTTPS, ou si vous êtes au
milieu de l’installation d’un serveur Git mais n’avez pas encore
installé un certificat complet.
Si le taux de données d’une opération HTTP est plus basse que
GIT_HTTP_LOW_SPEED_LIMIT
octets par seconde pendant plus longtemps
que GIT_HTTP_LOW_SPEED_TIME
secondes, Git annulera cette
opération. Ces valeurs surchargent les valeurs de configuration
http.lowSpeedLimit
et http.lowSpeedTime
.
GIT_HTTP_USER_AGENT
définit la chaîne d’agent utilisateur utilisée
par Git quand il communique sur HTTP. La valeur par défaut est quelque
chose comme git/2.0.0
.
Visualisation des différences et Fusion
GIT_DIFF_OPTS
est un terme un peu inapproprié. Les seules valeurs
valides sont -u<n>
ou --unified=<n>
, qui contrôlent le nombre de
lignes de contexte affichées dans une commande git diff
.
GIT_EXTERNAL_DIFF
est utilisée comme une surcharge de la valeur de
configuration diff.external
. Si elle est définie, Git invoquera ce
programme quand git diff
sera invoquée.
GIT_DIFF_PATH_COUNTER
et GIT_DIFF_PATH_TOTAL
sont utiles à
l’intérieur du programme spécifié par GIT_EXTERNAL_DIFF
ou
diff.external
. Le premier represente le fichier de la série dont on
est en train de visualiser les différences (en commençant par 1), et le
dernier est le nombre total de fichiers dans le lot.
GIT_MERGE_VERBOSITY
contrôle la sortie pour la stratégie de fusion
récursive. Les valeurs admises sont les suivantes :
0 ne sort rien, sauf éventuellement un seul message d’erreur.
1 ne montre que les conflits.
2 montre aussi les modifications de fichier.
3 montre quand les fichiers sont sautés parce qu’ils n’ont pas changé.
4 montre tous les chemins qui sont en train d’être traités.
5 et au-delà montrent des informations détaillées de débogage.
La valeur par défaut est 2.
Débogage
Vous voulez vraiment savoir de quoi Git est capable ? Git comprend un ensemble de traces assez complet, et tout ce que vous avez à faire est de les activer. Les valeurs possibles de ces variables sont les suivantes :
« true », « 1 » ou « 2 » – la catégorie de trace est écrite sur la sortie d’erreur standard (stderr).
Un chemin absolu commençant par
/
– la sortie de trace sera écrite dans ce fichier.
GIT_TRACE
contrôle les traces générales, qui ne rentrent dans
aucune catégorie spécifique. Cela inclut le développement des alias et
la délégation aux autres sous-programmes.
$ GIT_TRACE=true git lga
20:12:49.877982 git.c:554 trace: exec: 'git-lga'
20:12:49.878369 run-command.c:341 trace: run_command: 'git-lga'
20:12:49.879529 git.c:282 trace: alias expansion: lga => 'log' '--graph' '--pretty=oneline' '--abbrev-commit' '--decorate' '--all'
20:12:49.879885 git.c:349 trace: built-in: git 'log' '--graph' '--pretty=oneline' '--abbrev-commit' '--decorate' '--all'
20:12:49.899217 run-command.c:341 trace: run_command: 'less'
20:12:49.899675 run-command.c:192 trace: exec: 'less'
GIT_TRACE_PACK_ACCESS
contrôle le traçage d’accès aux fichiers
groupés. Le premier champ est le fichier groupé auquel on est en train
d’accéder, le second est le décalage dans ce fichier :
$ GIT_TRACE_PACK_ACCESS=true git status
20:10:12.081397 sha1_file.c:2088 .git/objects/pack/pack-c3fa...291e.pack 12
20:10:12.081886 sha1_file.c:2088 .git/objects/pack/pack-c3fa...291e.pack 34662
20:10:12.082115 sha1_file.c:2088 .git/objects/pack/pack-c3fa...291e.pack 35175
# […]
20:10:12.087398 sha1_file.c:2088 .git/objects/pack/pack-e80e...e3d2.pack 56914983
20:10:12.087419 sha1_file.c:2088 .git/objects/pack/pack-e80e...e3d2.pack 14303666
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
GIT_TRACE_PACKET
permet le traçage au niveau paquet pour les
opérations sur le réseau.
$ GIT_TRACE_PACKET=true git ls-remote origin
20:15:14.867043 pkt-line.c:46 packet: git< # service=git-upload-pack
20:15:14.867071 pkt-line.c:46 packet: git< 0000
20:15:14.867079 pkt-line.c:46 packet: git< 97b8860c071898d9e162678ea1035a8ced2f8b1f HEAD\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress include-tag multi_ack_detailed no-done symref=HEAD:refs/heads/master agent=git/2.0.4
20:15:14.867088 pkt-line.c:46 packet: git< 0f20ae29889d61f2e93ae00fd34f1cdb53285702 refs/heads/ab/add-interactive-show-diff-func-name
20:15:14.867094 pkt-line.c:46 packet: git< 36dc827bc9d17f80ed4f326de21247a5d1341fbc refs/heads/ah/doc-gitk-config
# […]
GIT_TRACE_PERFORMANCE
contrôle la journalisation d’information de
performance. La sortie montre combien de temps prend chaque invocation
particulère de Git.
$ GIT_TRACE_PERFORMANCE=true git gc
20:18:19.499676 trace.c:414 performance: 0.374835000 s: git command: 'git' 'pack-refs' '--all' '--prune'
20:18:19.845585 trace.c:414 performance: 0.343020000 s: git command: 'git' 'reflog' 'expire' '--all'
Counting objects: 170994, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (43413/43413), done.
Writing objects: 100% (170994/170994), done.
Total 170994 (delta 126176), reused 170524 (delta 125706)
20:18:23.567927 trace.c:414 performance: 3.715349000 s: git command: 'git' 'pack-objects' '--keep-true-parents' '--honor-pack-keep' '--non-empty' '--all' '--reflog' '--unpack-unreachable=2.weeks.ago' '--local' '--delta-base-offset' '.git/objects/pack/.tmp-49190-pack'
20:18:23.584728 trace.c:414 performance: 0.000910000 s: git command: 'git' 'prune-packed'
20:18:23.605218 trace.c:414 performance: 0.017972000 s: git command: 'git' 'update-server-info'
20:18:23.606342 trace.c:414 performance: 3.756312000 s: git command: 'git' 'repack' '-d' '-l' '-A' '--unpack-unreachable=2.weeks.ago'
Checking connectivity: 170994, done.
20:18:25.225424 trace.c:414 performance: 1.616423000 s: git command: 'git' 'prune' '--expire' '2.weeks.ago'
20:18:25.232403 trace.c:414 performance: 0.001051000 s: git command: 'git' 'rerere' 'gc'
20:18:25.233159 trace.c:414 performance: 6.112217000 s: git command: 'git' 'gc'
GIT_TRACE_SETUP
montre des informations sur ce que Git découvre
sur le dépôt et l’environnement avec lequel il interagit.
$ GIT_TRACE_SETUP=true git status
20:19:47.086765 trace.c:315 setup: git_dir: .git
20:19:47.087184 trace.c:316 setup: worktree: /Users/ben/src/git
20:19:47.087191 trace.c:317 setup: cwd: /Users/ben/src/git
20:19:47.087194 trace.c:318 setup: prefix: (null)
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
Divers
GIT_SSH
, si spécifié, est un programme qui est invoqué à la place
de ssh
quand Git essaie de se connecter à un hôte SSH. Il est invoqué
comme $GIT_SSH [username@]host [-p <port>] <command>
. Notez que ce
n’est pas le moyen le plus facile de personnaliser la façon dont ssh est
invoqué ; il ne prendra pas en compte des paramètres supplémentaires en
ligne de commande, donc vous devriez écrire un script l’enveloppant et
faire pointer GIT_SSH
dessus. Il est sans doute plus facile d’utiliser
le fichier ~/.ssh/config
pour cela.
GIT_ASKPASS
est une surcharge pour la valeur de configuration
core.askpass
. C’est le programme invoqué lorsque Git à besoin de
demander ses identifiants à l’utilisateur, qui peut s’attendre à un
texte comme argument en ligne de commande, et qui devrait retourner la
réponse sur la sortie standard (stdout
). (Consultez Stockage des
identifiants pour plus d’information sur ce
sous-système.)
GIT_NAMESPACE
contrôle l’accès des références cloisonnées dans des
espaces de nom, et est équivalent à l’option --namespace
. C’est
surtout utile côté serveur, où vous pourriez vouloir stocker plusieurs
bifurcations (forks) d’un seul dépôt dans un seul dépôt, en gardant
seulement les références séparées.
GIT_FLUSH
peut être utilisée pour forcer Git à utiliser des
entrées/sorties non mises en mémoire tampon (buffer) quand il écrit
progressivement dans la sortie standard. Une valeur de 1 fait que Git
évacue (flush) plus souvent, une valeur de 0 fait que la sortie est
mise en mémoire tampon. La valeur par défaut (si la variable n’est pas
définie) est à choisir selon un plan approprié de mise en mémoire tampon
en fonction de l’activité et du mode de sortie.
GIT_REFLOG_ACTION
vous permet de spécifier le texte descriptif
écrit dans le reflog
. Voici un exemple :
$ GIT_REFLOG_ACTION="my action" git commit --allow-empty -m 'my message'
[master 9e3d55a] my message
$ git reflog -1
9e3d55a HEAD@{0}: my action: my message
Résumé
Vous devriez avoir une assez bonne compréhension de ce que Git fait en arrière-plan et, jusqu’à un certain niveau, comment il est implémenté. Ce chapitre a parcouru plusieurs commandes de plomberie, qui sont à un niveau plus bas et plus simple que les commandes de porcelaine que vous avez vues dans le reste du livre. Comprendre comment Git travaille à bas niveau devrait vous aider à comprendre pourquoi il fait ce qu’il fait et à créer vos propres outils et scripts pour vous permettre de travailler comme vous l’entendez.
Git, en tant que système de fichiers adressable par contenu, est un outil puissant que vous pouvez utiliser pour des fonctionnalités au-delà d’un système de contrôle de version. Nous espérons que vous pourrez utiliser votre connaissance nouvellement acquise des tripes de Git pour implémenter votre propre super application avec cette technologie et que vous vous sentirez plus à l’aise pour utiliser Git de manière plus poussée.