Git
Chapters ▾ 2nd Edition

10.7 Les tripes de Git - Maintenance et récupération de données

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 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 utilisez 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 supprimez 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