Fonctionnement interne de Git#
Comprendre les mécanismes internes de Git transforme la manière dont on utilise l’outil. On passe d’un utilisateur qui mémorise des commandes à quelqu’un qui comprend pourquoi chaque commande fonctionne comme elle le fait. Quand on sait qu’une branche n’est qu’un fichier de 41 octets, que git commit enchaine cinq opérations de plomberie, ou que le reflog enregistre chaque mouvement de HEAD, les situations complexes — rebases échoués, commits perdus, conflits inexplicables — deviennent soudain limpides.
Ce chapitre explore le répertoire .git/ en profondeur. Le chapitre 3 a introduit le modèle objet (blobs, trees, commits, tags) et le système de références. Ici, nous allons plus loin : nous examinerons la structure complète du répertoire .git/, nous apprendrons à manipuler les commandes de plomberie pour recréer un commit à la main, nous découvrirons comment Git compresse ses données dans des packfiles, et surtout nous verrons comment le reflog permet de récupérer des commits que l’on croyait perdus à jamais.
Structure du répertoire .git/#
Chaque dépôt Git contient un répertoire .git/ qui héberge l’intégralité de l’historique, de la configuration et des métadonnées du projet. Commençons par créer un dépôt de démonstration et explorer son contenu.
%%bash
cd /tmp && rm -rf demo-internals && mkdir demo-internals && cd demo-internals && git init
git config user.email "demo@example.com" && git config user.name "Demo"
# Créer quelques commits pour peupler le dépôt
echo "# Projet demo" > README.md
echo "print('hello')" > main.py
mkdir src
echo "def add(a, b): return a + b" > src/utils.py
git add .
git commit -m "Commit initial"
echo "## Installation" >> README.md
echo "import utils" > src/app.py
git add .
git commit -m "Ajout documentation et app"
# Creer un tag
git tag -a v1.0 -m "Première version"
echo "--- Dépôt crée avec succès ---"
Dépôt Git vide initialisé dans /tmp/demo-internals/.git/
[main (commit racine) bc64a88] Commit initial
3 files changed, 3 insertions(+)
create mode 100644
README.md
create mode 100644 main.py
create mode 100644 src/utils.py
[main 6749efb] Ajout documentation et app
2 files changed, 2 insertions(+)
create mode 100644 src/
app.py
--- Dépôt crée avec succès ---
Examinons maintenant l’arborescence complète du répertoire .git/.
%%bash
cd /tmp/demo-internals
echo "=== Contenu du repertoire .git/ ==="
find .git -type f | sort | head -60
=== Contenu du repertoire .git/ ===
.git/COMMIT_EDITMSG
.git/config
.git/description
.git/HEAD
.git/hooks/applypatch-msg.sample
.git/hoo
ks/commit-msg.sample
.git/hooks/fsmonitor-watchman.sample
.git/hooks/post-update.sample
.git/hooks/p
re-applypatch.sample
.git/hooks/pre-commit.sample
.git/hooks/pre-merge-commit.sample
.git/hooks/prep
are-commit-msg.sample
.git/hooks/pre-push.sample
.git/hooks/pre-rebase.sample
.git/hooks/pre-receive
.sample
.git/hooks/push-to-checkout.sample
.git/hooks/sendemail-validate.sample
.git/hooks/update.sa
mple
.git/index
.git/info/exclude
.git/logs/HEAD
.git/logs/refs/heads/main
.git/objects/0f/e06ca1039
e6bf6fc0731babc7f2fa43ddad4e1
.git/objects/39/7eaf80f1530eee419ae833d3a21d47436a3dea
.git/objects/67
/49efb8b10a5fd91907baf9ae9379d407dba928
.git/objects/77/9295cf35795fa7ee6867a96d0ea222e7dcd44c
.git/
objects/9b/c8bf7befb9138a8b29c205c921f0a86cfe0ae0
.git/objects/b3/76c9941fda362c8d2c5c8ddb35db3e0b00
3402
.git/objects/b4/572b8b3b063d93dd15c67e4fb0bf4a108af6d1
.git/objects/bc/64a8886698dc78a769c2f3b1
59e3d951528be0
.git/objects/ce/bf945f8d8867ebeef6560999c5e3819bed687e
.git/objects/d7/6de8cb50eae3cc
20306429a013c1152f2a07e2
.git/objects/d9/0027d9849041751d80e4d0d2a366870a3ffc30
.git/objects/dd/4cc9
3d38d0b43f2e1a86b1c3e570dd515f0b48
.git/refs/heads/main
.git/refs/tags/v1.0
Les fichiers et répertoires importants#
Définition 71 (HEAD)
Le fichier .git/HEAD contient une référence symbolique vers la branche courante. En fonctionnement normal, il contient une ligne de la forme ref: refs/heads/main. Lorsque l’on est en état detached HEAD (par exemple après git checkout <hash>), il contient directement le hash SHA-1 d’un commit. C’est en lisant ce fichier que Git sait sur quelle branche on se trouve.
Définition 72 (refs/heads/)
Le répertoire .git/refs/heads/ contient un fichier par branche locale. Chaque fichier porte le nom de la branche et contient le hash SHA-1 du commit au sommet de cette branche. Quand on crée un nouveau commit, Git met à jour le fichier de la branche courante avec le hash du nouveau commit. Quand on crée une branche, Git crée simplement un nouveau fichier dans ce répertoire.
Définition 73 (refs/tags/)
Le répertoire .git/refs/tags/ contient un fichier par tag. Pour un tag léger, le fichier contient directement le hash du commit pointé. Pour un tag annoté, il contient le hash de l’objet tag, qui lui-même pointe vers le commit.
Définition 74 (refs/remotes/)
Le répertoire .git/refs/remotes/ contient les références de suivi distant (remote-tracking branches). Elles sont organisées par remote : .git/refs/remotes/origin/main contient le hash du dernier commit connu de la branche main sur le remote origin. Ces références sont mises à jour automatiquement par git fetch et git pull, mais jamais par des commits locaux.
Définition 75 (objects/)
Le répertoire .git/objects/ est la base de données d’objets de Git. C’est ici que sont stockés tous les blobs, trees, commits et tags annotés. Les objets sont organisés en sous-répertoires nommés par les deux premiers caractères du hash SHA-1. Par exemple, un objet de hash a3f2c1... sera stocké dans .git/objects/a3/f2c1.... Chaque objet est compressé avec zlib.
Définition 76 (index)
Le fichier .git/index est la zone de transit (staging area). C’est un fichier binaire qui contient la liste des fichiers indexés, avec pour chacun son nom, ses permissions, son hash SHA-1 et des métadonnées de cache (timestamps, taille). C’est ce fichier que git add modifie et que git commit lit pour construire le tree du nouveau commit.
Définition 77 (config)
Le fichier .git/config contient la configuration locale du dépôt. C’est le niveau --local de la configuration Git : il écrase les paramètres définis dans ~/.gitconfig (global) et /etc/gitconfig (système). On y trouve typiquement les URLs des remotes, les paramètres de branches de suivi, et toute configuration spécifique au projet.
Définition 78 (hooks/)
Le répertoire .git/hooks/ contient des scripts exécutables que Git invoque automatiquement à certains moments du cycle de vie : avant un commit (pre-commit), avant un push (pre-push), après un merge (post-merge), etc. Par défaut, Git y place des exemples (fichiers .sample) qui ne sont pas actifs. Pour activer un hook, il suffit de retirer l’extension .sample et de rendre le script exécutable.
Définition 79 (logs/)
Le répertoire .git/logs/ contient les données du reflog : un journal local qui enregistre chaque modification de HEAD et des références de branches. Contrairement à git log qui suit les pointeurs de parent dans le DAG des commits, le reflog enregistre toutes les opérations — commits, resets, rebases, checkouts, merges — dans l’ordre chronologique. C’est le filet de sécurité ultime pour récupérer des commits perdus.
Exploration concrète#
%%bash
cd /tmp/demo-internals
echo "=== Contenu de .git/HEAD ==="
cat .git/HEAD
echo ""
echo "=== Branche main : hash du dernier commit ==="
cat .git/refs/heads/main
echo ""
echo "=== Tag v1.0 ==="
cat .git/refs/tags/v1.0
echo ""
echo "=== Configuration locale ==="
cat .git/config
echo ""
echo "=== Hooks disponibles ==="
ls .git/hooks/
=== Contenu de .git/HEAD ===
ref: refs/heads/main
=== Branche main : hash du dernier commit ===
6749efb8b10a5fd91907baf9ae9379d407dba928
=== Tag v1.0 ===
b4572b8b3b063d93dd15c67e4fb0bf4a108af6d1
=== Configuration locale ===
[core]
repositoryformatversion = 0
filemode = true
bare = false
lo
gallrefupdates = true
[user]
email = demo@example.com
name = Demo
=== Hooks disponibles ===
applypatch-msg.sample
commit-msg.sample
fsmonitor-watchman.sample
post-up
date.sample
pre-applypatch.sample
pre-commit.sample
pre-merge-commit.sample
prepare-commit-msg.sampl
e
pre-push.sample
pre-rebase.sample
pre-receive.sample
push-to-checkout.sample
sendemail-validate.sa
mple
update.sample
%%bash
cd /tmp/demo-internals
echo "=== Objets dans la base de données ==="
find .git/objects -type f | grep -v pack | sort
echo ""
echo "=== Nombre d'objets ==="
find .git/objects -type f | grep -v pack | wc -l
=== Objets dans la base de données ===
.git/objects/0f/e06ca1039e6bf6fc0731babc7f2fa43ddad4e1
.git/objects/39/7eaf80f1530eee419ae833d3a21d4
7436a3dea
.git/objects/67/49efb8b10a5fd91907baf9ae9379d407dba928
.git/objects/77/9295cf35795fa7ee686
7a96d0ea222e7dcd44c
.git/objects/9b/c8bf7befb9138a8b29c205c921f0a86cfe0ae0
.git/objects/b3/76c9941fd
a362c8d2c5c8ddb35db3e0b003402
.git/objects/b4/572b8b3b063d93dd15c67e4fb0bf4a108af6d1
.git/objects/bc
/64a8886698dc78a769c2f3b159e3d951528be0
.git/objects/ce/bf945f8d8867ebeef6560999c5e3819bed687e
.git/
objects/d7/6de8cb50eae3cc20306429a013c1152f2a07e2
.git/objects/d9/0027d9849041751d80e4d0d2a366870a3f
fc30
.git/objects/dd/4cc93d38d0b43f2e1a86b1c3e570dd515f0b48
=== Nombre d'objets ===
12
Remarque 68
Le nombre d’objets peut sembler élevé par rapport au nombre de fichiers du projet. C’est normal : chaque commit génère au minimum un objet commit et un objet tree (pour le répertoire racine), plus un tree par sous-répertoire modifié et un blob par fichier dont le contenu a changé. Dans notre exemple avec deux commits, Git a créé des blobs pour chaque version de chaque fichier, des trees pour chaque état de chaque répertoire, et deux objets commits, plus un objet tag annoté.
Plomberie vs porcelaine#
Git distingue deux catégories de commandes, une métaphore empruntée à la plomberie domestique : les tuyaux cachés et la robinetterie visible.
Définition 80 (Commandes de plomberie (plumbing))
Les commandes de plomberie sont les commandes bas niveau qui manipulent directement les objets et les références de Git. Elles constituent l’interface programmatique du système. Les principales sont :
git hash-object: calcule le hash d’un contenu et l’écrit optionnellement dans la base d’objetsgit cat-file: affiche le type, la taille ou le contenu d’un objetgit update-index: manipule directement l’index (zone de transit)git write-tree: crée un objet tree à partir de l’état actuel de l’indexgit commit-tree: crée un objet commit pointant vers un tree donnégit update-ref: modifie une référence (branche, tag) pour qu’elle pointe vers un hash donné
Définition 81 (Commandes de porcelaine (porcelain))
Les commandes de porcelaine sont les commandes haut niveau, conçues pour l’intéraction humaine. Elles combinent plusieurs commandes de plomberie en opérations simples et intuitives :
git add=git hash-object+git update-indexgit commit=git write-tree+git commit-tree+git update-refgit log,git branch,git merge,git checkout, etc.
Ce sont les commandes que l’on utilise au quotidien. Chacune d’elles n’est qu’un raccourci élégant au-dessus des opérations de plomberie.
Recréer un commit à la main#
L’exercice suivant est l’un des plus instructifs pour comprendre Git en profondeur. Nous allons recréer exactement ce que git commit fait en interne, en utilisant uniquement des commandes de plomberie. Chaque étape correspond à une opération réelle que Git exécute en arrière-plan.
Exemple 19 (Créer un commit avec uniquement des commandes de plomberie)
Cet exemple montre les cinq étapes exactes que git commit exécute en arrière-plan :
Créer un blob : hacher le contenu du fichier et le stocker dans la base d’objets.
Mettre à jour l’index : enregistrer le blob dans la zone de transit avec son nom de fichier et ses permissions.
Créer un tree : générer un objet tree à partir de l’état actuel de l’index.
Créer un commit : générer un objet commit qui pointe vers le tree et contient le message, l’auteur et le parent.
Mettre à jour la branche : faire pointer la référence de la branche vers le nouveau commit.
L’ensemble de ces opérations est reproduit dans les cellules qui suivent.
%%bash
# Créer un dépôt vierge pour la démonstration
cd /tmp && rm -rf demo-plumbing && mkdir demo-plumbing && cd demo-plumbing && git init
git config user.email "demo@example.com" && git config user.name "Demo"
echo "=== Dépôt vierge créé ==="
echo "HEAD pointe vers : $(cat .git/HEAD)"
echo "La branche main n'existe pas encore (aucun commit)"
ls .git/refs/heads/
Dépôt Git vide initialisé dans /tmp/demo-plumbing/.git/
=== Dépôt vierge créé ===
HEAD pointe vers : ref: refs/heads/main
La branche main n'existe pas encore (aucun commit)
Etape 1 : Créer un blob. On écrit du contenu dans la base d’objets avec git hash-object -w.
%%bash
cd /tmp/demo-plumbing
# Créer un blob à partir d'un contenu
BLOB_HASH=$(echo "Bonjour, monde !" | git hash-object -w --stdin)
echo "Hash du blob : $BLOB_HASH"
# Vérifier que l'objet existe et est bien un blob
echo "Type : $(git cat-file -t $BLOB_HASH)"
echo "Contenu : $(git cat-file -p $BLOB_HASH)"
# Sauvegarder le hash pour les étapes suivantes
echo $BLOB_HASH > /tmp/demo-plumbing-blob-hash
Hash du blob : 07e152edaaf7b6dbd5eb22c30b3ac8a688394fb7
Type : blob
Contenu : Bonjour, monde !
Etape 2 : Mettre à jour l’index. On enregistre le blob dans la zone de transit avec git update-index.
%%bash
cd /tmp/demo-plumbing
BLOB_HASH=$(cat /tmp/demo-plumbing-blob-hash)
# Ajouter le blob à l'index avec le nom "hello.txt" et les permissions 100644
git update-index --add --cacheinfo 100644,$BLOB_HASH,hello.txt
echo "=== Contenu de l'index ==="
git ls-files --stage
=== Contenu de l'index ===
100644 07e152edaaf7b6dbd5eb22c30b3ac8a688394fb7 0 hello.txt
Etape 3 : Créer un tree. On transforme l’état actuel de l’index en un objet tree avec git write-tree.
%%bash
cd /tmp/demo-plumbing
# Créer un tree à partir de l'index
TREE_HASH=$(git write-tree)
echo "Hash du tree : $TREE_HASH"
echo "Type : $(git cat-file -t $TREE_HASH)"
echo ""
echo "=== Contenu du tree ==="
git cat-file -p $TREE_HASH
# Sauvegarder le hash
echo $TREE_HASH > /tmp/demo-plumbing-tree-hash
Hash du tree : 32d8ca30d539a7f3d936e9473e8a00b1050590cf
Type : tree
=== Contenu du tree ===
100644 blob 07e152edaaf7b6dbd5eb22c30b3ac8a688394fb7 hello.txt
Etape 4 : Créer un commit. On crée un objet commit qui pointe vers le tree avec git commit-tree.
%%bash
cd /tmp/demo-plumbing
TREE_HASH=$(cat /tmp/demo-plumbing-tree-hash)
# Créer un commit pointant vers le tree
# (pas de -p car c'est le premier commit, il n'a pas de parent)
COMMIT_HASH=$(echo "Mon premier commit fait à la main" | git commit-tree $TREE_HASH)
echo "Hash du commit : $COMMIT_HASH"
echo "Type : $(git cat-file -t $COMMIT_HASH)"
echo ""
echo "=== Contenu du commit ==="
git cat-file -p $COMMIT_HASH
# Sauvegarder le hash
echo $COMMIT_HASH > /tmp/demo-plumbing-commit-hash
Hash du commit : d81495090aadc95296b57d6e3441b621524b4d32
Type : commit
=== Contenu du commit ===
tree 32d8ca30d539a7f3d936e9473e8a00b1050590cf
author Demo <demo@example.com> 1773606355 +0100
commit
ter Demo <demo@example.com> 1773606355 +0100
Mon premier commit fait à la main
Etape 5 : Mettre à jour la branche. On fait pointer refs/heads/main vers le nouveau commit avec git update-ref.
%%bash
cd /tmp/demo-plumbing
COMMIT_HASH=$(cat /tmp/demo-plumbing-commit-hash)
# Mettre à jour la référence de la branche main
git update-ref refs/heads/main $COMMIT_HASH
echo "=== La branche main pointe maintenant vers notre commit ==="
cat .git/refs/heads/main
echo ""
echo "=== git log confirme que le commit existe ==="
git log --oneline
echo ""
echo "=== git show affiche le contenu ==="
git show --stat
=== La branche main pointe maintenant vers notre commit ===
d81495090aadc95296b57d6e3441b621524b4d32
=== git log confirme que le commit existe ===
d814950 Mon premier commit fait à la main
=== git show affiche le contenu ===
commit d81495090aadc95296b57d6e3441b621524b4d32
Author: Demo <demo@example.com>
Date: Sun Mar 15 2
1:25:55 2026 +0100
Mon premier commit fait à la main
hello.txt | 1 +
1 file changed, 1 inse
rtion(+)
Nous venons de créer un commit complet sans jamais appeler git add ni git commit. Le fichier hello.txt est enregistré dans l’historique, la branche main pointe vers notre commit, et git log le reconnait parfaitement.
Remarque 69
Chaque git commit que vous exécutez au quotidien effectue exactement ces cinq étapes en arrière-plan. La commande de porcelaine ne fait qu’automatiser et enchainer les opérations de plomberie : hacher les fichiers modifiés, mettre à jour l’index, créer le tree, créer le commit avec le bon parent, et avancer le pointeur de branche. Comprendre cette mécanique permet de démystifier complètement Git : il n’y a pas de magie, juste des fichiers, des hashs et des pointeurs.
Ajoutons un second commit avec un parent pour voir la chaine complète.
%%bash
cd /tmp/demo-plumbing
PARENT_HASH=$(cat /tmp/demo-plumbing-commit-hash)
# Créer un nouveau blob
BLOB2_HASH=$(echo "Au revoir, monde !" | git hash-object -w --stdin)
# Ajouter au même index (hello.txt reste, on ajoute goodbye.txt)
git update-index --add --cacheinfo 100644,$BLOB2_HASH,goodbye.txt
# Créer le tree
TREE2_HASH=$(git write-tree)
# Créer le commit AVEC un parent cette fois
COMMIT2_HASH=$(echo "Deuxième commit manuel" | git commit-tree $TREE2_HASH -p $PARENT_HASH)
# Mettre à jour la branche
git update-ref refs/heads/main $COMMIT2_HASH
echo "=== Historique complet ==="
git log --oneline
echo ""
echo "=== Le second commit a bien un parent ==="
git cat-file -p HEAD
=== Historique complet ===
6f3cb53 Deuxième commit manuel
d814950 Mon premier commit fait à la main
=== Le second commit a bien un parent ===
tree 6a5cef0721285b7ac06eccab560c9f0744006660
parent d81495090aadc95296b57d6e3441b621524b4d32
author
Demo <demo@example.com> 1773606355 +0100
committer Demo <demo@example.com> 1773606355 +0100
Deuxi
ème commit manuel
Packfiles et compression#
Dans les sections précédentes, nous avons vu que chaque objet Git est stocké individuellement dans .git/objects/, compressé avec zlib. Ce format est appelé objets lâches (loose objects). Il est simple et efficace pour de petits dépôts, mais il devient problématique à grande échelle : des milliers de fichiers individuels consomment de l’espace inutilement, surtout lorsque des fichiers similaires sont stockés en tant que blobs séparés.
Définition 82 (Packfile)
Un packfile est un format de stockage optimisé dans lequel Git regroupe de nombreux objets en un seul fichier. Un packfile se compose de deux fichiers :
.git/objects/pack/<hash>.pack: le fichier de données contenant les objets compressés.git/objects/pack/<hash>.idx: l’index qui permet de localiser rapidement un objet dans le pack
Git utilise la compression delta : au lieu de stocker chaque version complète d’un fichier, il stocke une version de base et, pour les versions similaires, uniquement les différences (deltas) par rapport à cette base. C’est ce mécanisme qui rend les dépôts Git remarquablement compacts.
%%bash
cd /tmp/demo-internals
echo "=== Statistiques AVANT git gc ==="
git count-objects -v
echo ""
echo "=== Objets lâches ==="
find .git/objects -type f | grep -v pack | grep -v info | wc -l
echo "fichiers d'objets lâches"
=== Statistiques AVANT git gc ===
count: 12
size: 48
in-pack: 0
packs: 0
size-pack: 0
prune-packable: 0
garbage: 0
size-garbage: 0
=== Objets lâches ===
12
fichiers d'objets lâches
%%bash
cd /tmp/demo-internals
# Déclencher le ramasse-miettes et le packing
git gc
echo "=== Statistiques APRES git gc ==="
git count-objects -v
echo ""
echo "=== Packfiles créés ==="
find .git/objects/pack -type f 2>/dev/null || echo "(aucun packfile)"
=== Statistiques APRES git gc ===
count: 0
size: 0
in-pack: 12
packs: 1
size-pack: 3
prune-packable: 0
garbage: 0
size-garbage: 0
=== Packfiles créés ===
.git/objects/pack/pack-ab665b3ae99c2293f8f8315992e114c7757be97f.idx
.git/objects/pack/pack-ab665b3ae
99c2293f8f8315992e114c7757be97f.rev
.git/objects/pack/pack-ab665b3ae99c2293f8f8315992e114c7757be97f.
pack
%%bash
cd /tmp/demo-internals
# Inspecter le contenu du packfile (s'il existe)
PACK_IDX=$(find .git/objects/pack -name "*.idx" 2>/dev/null | head -1)
if [ -n "$PACK_IDX" ]; then
echo "=== Contenu du packfile ==="
git verify-pack -v "$PACK_IDX" | head -20
else
echo "(pas de packfile à inspecter)"
fi
=== Contenu du packfile ===
6749efb8b10a5fd91907baf9ae9379d407dba928 commit 1076 820 12
b4572b8b3b063d93dd15c67e4fb0bf4a108af6d1
tag 136 134 832
bc64a8886698dc78a769c2f3b159e3d951528be0 commit 1016 773 966
779295cf35795fa7ee6
867a96d0ea222e7dcd44c tree 102 109 1739
d90027d9849041751d80e4d0d2a366870a3ffc30 tree 70 76 1848
cebf945f8d8867ebeef6560999c5e3819bed687e tree 102 109 1924
0fe06ca1039e6bf6fc0731babc7f2fa43ddad4
e1 tree 36 47 2033
397eaf80f1530eee419ae833d3a21d47436a3dea blob 30 40 2080
b376c9941fda362c8d2c
5c8ddb35db3e0b003402 blob 15 24 2120
dd4cc93d38d0b43f2e1a86b1c3e570dd515f0b48 blob 13 22 2144
9b
c8bf7befb9138a8b29c205c921f0a86cfe0ae0 blob 28 38 2166
d76de8cb50eae3cc20306429a013c1152f2a07e2 bl
ob 14 23 2204
pas un delta : 12 objets
.git/objects/pack/pack-ab665b3ae99c2293f8f8315992e114c7757b
e97f.pack: ok
Remarque 70
La compression delta des packfiles est ce qui rend les dépôts Git remarquablement compacts. Imaginons un fichier de 10 000 lignes dont on modifie une seule ligne à chaque commit. Avec des objets lâches, chaque commit stocke une copie complète (compressée) du fichier. Avec un packfile, Git stocke une version de base et, pour chaque autre version, uniquement le delta — quelques octets au lieu de la totalité du fichier. C’est pourquoi un dépôt Git contenant des années d’historique est souvent plus petit qu’une seule copie de travail du projet. La commande git gc (garbage collection) déclenche manuellement ce processus de packing, mais Git l’exécute aussi automatiquement lorsque le nombre d’objets lâches depasse un seuil (typiquement 6700).
Le reflog : filet de sécurité#
Définition 83 (Reflog (Reference Log))
Le reflog est un journal local qui enregistre chaque modification de HEAD ou d’une référence de branche. Contrairement à git log, qui suit les pointeurs parent dans le DAG des commits et ne montre que les commits accessibles, le reflog enregistre toutes les opérations dans l’ordre chronologique :
chaque
git commit(HEAD avance)chaque
git checkout(HEAD change de branche)chaque
git reset(HEAD recule ou avance)chaque
git rebase(HEAD est deplacé pendant le rejeu)chaque
git merge,git pull,git cherry-pick, etc.
Chaque entrée du reflog contient le hash avant, le hash après, l’auteur de l’opération, la date et une description de l’action. Les entrées expirent après 90 jours pour les commits accessibles et 30 jours pour les commits devenus inaccessibles.
%%bash
cd /tmp/demo-internals
echo "=== Reflog de HEAD ==="
git reflog
=== Reflog de HEAD ===
6749efb HEAD@{0}: commit: Ajout documentation et app
bc64a88 HEAD@{1}: commit (initial): Commit initial
%%bash
cd /tmp/demo-internals
# Créer une branche, y travailler, puis revenir
git checkout -b experiment
echo "Expérience en cours" > experiment.txt
git add experiment.txt
git commit -m "Commit expérimental"
git checkout main
echo "=== Reflog après création de branche et checkout ==="
git reflog | head -10
echo ""
echo "=== Reflog de la branche experiment ==="
git reflog show experiment
Basculement sur la nouvelle branche 'experiment'
[experiment f36310c] Commit expérimental
1 file changed, 1 insertion(+)
create mode 100644 experi
ment.txt
Basculement sur la branche 'main'
=== Reflog après création de branche et checkout ===
6749efb HEAD@{0}: checkout: moving from experiment to main
f36310c HEAD@{1}: commit: Commit expérim
ental
6749efb HEAD@{2}: checkout: moving from main to experiment
6749efb HEAD@{3}: commit: Ajout doc
umentation et app
bc64a88 HEAD@{4}: commit (initial): Commit initial
=== Reflog de la branche experiment ===
f36310c experiment@{0}: commit: Commit expérimental
6749efb experiment@{1}: branch: Created from HEAD
Remarque 71
Le reflog est strictement local. Il n’est ni partagé avec les remotes, ni cloné avec le dépôt. Chaque clone possède son propre reflog, qui ne contient que les opérations effectuées localement. C’est pour cette raison que le reflog ne peut pas vous aider à récupérer un commit qui a été perdu par un collègue sur son dépôt — il ne peut récupérer que vos propres opérations.
Récupérer des commits perdus#
Le scenario suivant est l’un des plus stressants pour un utilisateur de Git : on exécute git reset --hard et on réalise trop tard qu’on a perdu des commits importants. Heureusement, le reflog est là pour nous sauver.
Exemple 20 (Scénario de récupération complet)
Voici le scénario pas à pas :
Créer plusieurs commits de travail.
Exécuter
git reset --hard HEAD~3(erreur simulée).Utiliser
git reflogpour retrouver le hash des commits perdus.Récupérer les commits avec
git branch recovery <hash>ougit checkout <hash>.
Ce scénario est reproduit dans les cellules suivantes.
%%bash
cd /tmp && rm -rf demo-recovery && mkdir demo-recovery && cd demo-recovery && git init
git config user.email "demo@example.com" && git config user.name "Demo"
# Créer un historique avec plusieurs commits
echo "Base du projet" > base.txt
git add base.txt
git commit -m "Commit A : base"
echo "Fonctionnalité importante" > feature.txt
git add feature.txt
git commit -m "Commit B : fonctionnalité importante"
echo "Correction critique" >> feature.txt
git add feature.txt
git commit -m "Commit C : correction critique"
echo "Amélioration finale" >> feature.txt
git add feature.txt
git commit -m "Commit D : amélioration finale"
echo "=== Historique avant le désastre ==="
git log --oneline
Dépôt Git vide initialisé dans /tmp/demo-recovery/.git/
[main (commit racine) e5a44fa] Commit A : base
1 file changed, 1 insertion(+)
create mode 100644 b
ase.txt
[main 1b144d2] Commit B : fonctionnalité importante
1 file changed, 1 insertion(+)
create mode 10
0644 feature.txt
[main 287bea4] Commit C : correction critique
1 file changed, 1 insertion(+)
[main 37385ee] Commit D : amélioration finale
1 file changed, 1 insertion(+)
=== Historique avant le désastre ===
37385ee Commit D : amélioration finale
287bea4 Commit C : correction critique
1b144d2 Commit B : fonctionnalité importante
e5a44fa Commit A : base
Simulons maintenant une erreur : on revient trois commits en arrière avec git reset --hard.
%%bash
cd /tmp/demo-recovery
echo "=== Avant le reset ==="
git log --oneline
echo ""
echo "--- Exécution de git reset --hard HEAD~3 ---"
git reset --hard HEAD~3
echo ""
echo "=== Après le reset : les commits B, C, D ont disparu ! ==="
git log --oneline
=== Avant le reset ===
37385ee Commit D : amélioration finale
287bea4 Commit C : correction critique
1b144d2 Commit B : fonctionnalité importante
e5a44fa Commit
A : base
--- Exécution de git reset --hard HEAD~3 ---
HEAD est maintenant à e5a44fa Commit A : base
=== Après le reset : les commits B, C, D ont disparu ! ===
e5a44fa Commit A : base
Les commits B, C et D ne sont plus visibles dans git log. Mais ils existent toujours dans la base d’objets. Le reflog nous permet de les retrouver.
%%bash
cd /tmp/demo-recovery
echo "=== Le reflog a tout enregistré ==="
git reflog
=== Le reflog a tout enregistré ===
e5a44fa HEAD@{0}: reset: moving to HEAD~3
37385ee HEAD@{1}: commit: Commit D : amélioration finale
287bea4 HEAD@{2}: commit: Commit C : corre
ction critique
1b144d2 HEAD@{3}: commit: Commit B : fonctionnalité importante
e5a44fa HEAD@{4}: com
mit (initial): Commit A : base
On voit dans le reflog l’entrée du reset et, juste au-dessus, le hash du commit D (celui où on était avant le reset). On peut maintenant récupérer les commits.
%%bash
cd /tmp/demo-recovery
# Récupérer le hash du commit perdu (avant le reset)
# HEAD@{1} est l'état de HEAD juste avant la dernière opération
LOST_HASH=$(git reflog show --format='%H' | sed -n '2p')
echo "Hash du commit perdu : $LOST_HASH"
echo ""
# Méthode 1 : créer une branche de récupération
git branch recovery $LOST_HASH
echo "=== Branche recovery créée ==="
git log --oneline recovery
echo ""
echo "=== On peut aussi revenir directement ==="
git reset --hard $LOST_HASH
git log --oneline
Hash du commit perdu : 37385ee4e1a30ad42898d6b066d0abbb6f4389fe
=== Branche recovery créée ===
37385ee Commit D : amélioration finale
287bea4 Commit C : correction critique
1b144d2 Commit B : fo
nctionnalité importante
e5a44fa Commit A : base
=== On peut aussi revenir directement ===
HEAD est maintenant à 37385ee Commit D : amélioration finale
37385ee Commit D : amélioration finale
287bea4 Commit C : correction critique
1b144d2 Commit B : fonctionnalité importante
e5a44fa Commit A : base
Trouver des objets inaccessibles avec git fsck#
Une autre méthode, complémentaire au reflog, est git fsck --unreachable, qui inspecte la base d’objets et identifie tous les objets qui ne sont plus accessibles depuis aucune référence.
%%bash
cd /tmp/demo-recovery
# Re-simuler la perte pour la démonstration de fsck
git reset --hard HEAD~3
echo "=== Objets inaccessibles ==="
git fsck --unreachable --no-reflogs 2>&1 | head -15
HEAD est maintenant à e5a44fa Commit A : base
=== Objets inaccessibles ===
Remarque 72
Rien n’est véritablement perdu dans Git tant que deux conditions sont réunies : le ramasse-miettes (git gc) n’a pas supprimé les objets, et le reflog n’a pas expiré. Par défaut, les entrées du reflog expirent après 90 jours pour les commits accessibles et 30 jours pour les commits inaccessibles. Le ramasse-miettes ne supprime que les objets qui ne sont référencés ni par une branche, ni par un tag, ni par le reflog. En pratique, vous disposez donc d’au moins 30 jours pour récupérer un commit perdu — largement assez pour remarquer l’erreur. La commande git gc --prune=now supprime immédiatement les objets inaccessibles : ne l’exécutez jamais sans être certain de ce que vous faites.
Résumé#
Le répertoire .git/ est le coeur de Git. Voici une carte de ses composants principaux :
Chemin |
Rôle |
|---|---|
|
Référence symbolique vers la branche courante |
|
Pointeurs de branches (un fichier par branche) |
|
Pointeurs de tags |
|
Références de suivi distant |
|
Base de données d’objets (blobs, trees, commits, tags) |
|
Packfiles (objets compressés par delta) |
|
Zone de transit (fichier binaire) |
|
Configuration locale du dépôt |
|
Scripts déclenchables automatiquement |
|
Données du reflog |
Commandes essentielles#
Commande |
Description |
|---|---|
|
Afficher le type d’un objet |
|
Afficher le contenu d’un objet |
|
Créer un blob dans la base d’objets |
|
Manipuler directement l’index |
|
Créer un tree à partir de l’index |
|
Créer un commit pointant vers un tree |
|
Modifier une référence |
|
Statistiques de la base d’objets |
|
Déclencher le ramasse-miettes et le packing |
|
Inspecter le contenu d’un packfile |
|
Consulter le journal des opérations sur HEAD |
|
Consulter le reflog d’une branche spécifique |
|
Trouver les objets inaccessibles |
Remarque 73
Les commandes de plomberie sont rarement nécessaires au quotidien. Mais les connaitre change fondamentalement la relation que l’on entretient avec Git. On ne mémorise plus des recettes : on comprend un système. Et quand les choses tournent mal — un rebase qui échoue, un reset malheureux, un historique corrompu — cette compréhension permet de diagnostiquer et de réparer, là où un utilisateur qui ne connait que la porcelaine est réduit à chercher des solutions sur Stack Overflow en espérant que l’une d’elles fonctionne.