Une protection inégalée : récupération après divers incidents

Nicola Paolucci
Nicola Paolucci
Retour à la liste

Git est un outil avancé. Sa philosophie me tient beaucoup à cœur : considérer les développeurs comme des gens intelligents et responsables. Elle implique que vous avez un grand pouvoir entre vos mains. Le pouvoir de vous tirer une balle dans le pied, par exemple. Et même si vous portez un gilet en titane, vous avez néanmoins le pouvoir de vous tirer dessus.

The topic of this post is to confirm what my colleague Charles O'Farrell said very well in the ever popular "Why Git?" article some time ago:

[...] Git is actually the safest of all the DVCS options. As we saw above, Git never actually lets you change anything, it just creates new objects.

[...] Git actually keeps track of every change you make, storing them in the reflog. Because every commit is unique and immutable, all the reflog has to do is store a reference to them. This means you're safe from harm and your code is always preserved, but there are situations where you might need some conjuring to get it back.

Voici quelques exemples concrets qui expliquent comment résoudre les problèmes, des plus simples aux plus complexes :

Sommaire

  1. Annulation d'un (soft) reset et récupération d'un fichier supprimé
  2. Perte d'un commit durant un rebase interactif
  3. How To Undo reset --hard If You Only Staged Your Changes

Annulation d'un (soft) reset et récupération d'un fichier supprimé

If you delete a file with git rm and immediately after you git reset your working directory (which is called a soft reset), you will find yourself with a missing file and a dirty working directory like the following:

On branch master
Changes not staged for commit:
(use "git add/rm file..." to update what will be committed)
(use "git checkout -- file..." to discard changes in working directory)
deleted: test.txt

There are a couple of simple ways to go about restoring the file. One is to use a git reset --hard which will recreate the missing files but it will delete any local modifications you might have in your working directory.

The second is to just re-check out the file yourself with git checkout test.txt.

In a slightly different scenario, if the file was removed in an earlier commit, you can recover it by noting down the exact commit where the file was deleted and use the reference to pick the commit immediately before: git checkout commit_id~ -- test.txt

Where the ~ (tilde) sign means the one before this one.

Perte d'un commit durant un rebase interactif

Interactive rebase is one of the most useful tools in the git arsenal; It allows to edit, squash and delete commits interactively.

Personnellement, j'aime nettoyer et simplifier des commits avant de les partager. C'est plus sympa de travailler avec des commits compréhensibles et logiques. Dans le vif du développement (ou dans une branche privée locale), je peux enchaîner les commits, puis, quand je suis satisfait, je prends le temps de nettoyer l'historique.

Mais que se passe-t-il si vous supprimez par accident une ligne de commit dans la session de rebase interactif, que vous ne vous en rendez pas compte et que vous enregistrez ?

This situation is slightly annoying cause you have just removed a commit from your history!

If you created a throwaway branch before starting the rebase - which you should always do - you're fine and you can restart the process all over. But what if you didn't?

No worries, git has got you covered. Nothing is ever lost. This is a good opportunity to use the [reflog][6, which is an automatic mechanism recording where the tip of all branches has been for the past 30 days.

Say at the start of the rebase I had 4 commits like the following:

$ git log
cfdf880 [2 seconds ago] (HEAD, master) wrote fifth change
ab446e6 [34 seconds ago] changed one line
6e1a130 [25 minutes ago] third change
6566977 [26 minutes ago] second change
5feeb33 [26 minutes ago] initial commit

I rebase the last 4 commits and accidentally remove one line:

$ git rebase -i HEAD~4

L'éditeur s'ouvre :

pick 6566977 second change
pick 6e1a130 third change
pick ab446e6 changed one line
pick cfdf880 wrote fifth change
Rebase 5feeb33..cfdf880 onto 5feeb33

Commandes :

  • p, pick = utiliser le commit
  • r, reword = utiliser le commit, mais modifier le message de commit
  • e, edit = utiliser le commit, mais s'arrêter pour faire des modifications
  • s, squash = use commit, but meld into previous commit
  • f, fixup = comme « squash », mais supprime le message de commit des logs
  • x, exec = run command (the rest of the line) using shell

Ces lignes peuvent être réorganisées ; elles s'exécutent du haut vers le bas.

Si vous supprimez une ligne ici, CE COMMIT VA DISPARAÎTRE.

Cependant, si vous supprimez tout, le rebase sera annulé.

Notez que les commits vides sont mis en commentaire.

I removed one commit line (pick ab446e6 changed one line), I saved and got this:

Successfully rebased and updated refs/heads/master.

Si je vérifie à présent la liste des commits, je vois celui qui était perdu :

$ git log
3dd6845 [6 minutes ago] (HEAD, master) wrote fifth change
6e1a130 [32 minutes ago] third change
6566977 [32 minutes ago] second change
5feeb33 [32 minutes ago] initial commit

But the reflog has our lost commit:

$ git reflog
3dd6845 HEAD@{0}: rebase -i (finish): returning to refs/heads/master
3dd6845 HEAD@{1}: rebase -i (pick): wrote fifth change
6e1a130 HEAD@{2}: checkout: moving from master to 6e1a130
cfdf880 HEAD@{3}: commit: wrote fifth change
----}} ab446e6 HEAD@{4}: commit: changed one line
[...]

Here ab446e6 is the sha-1 for the commit we lost. Armed with this id we can just cherry-pick it back in the code:

$ git cherry-pick ab446e6
[master 032c6a6] changed one line
1 file changed, 1 insertion(+), 1 deletion(-)

Vous pouvez aussi constater que le commit réapparaît dans notre historique :

$ git log
032c6a6 [12 minutes ago] (HEAD, master) changed one line
3dd6845 [12 minutes ago] wrote fifth change
6e1a130 [37 minutes ago] third change
6566977 [38 minutes ago] second change
5feeb33 [38 minutes ago] initial commit

How To Undo reset --hard If You Only Staged Your Changes

Je ne vais pas insister sur le fait qu'il est essentiel de faire des commits fréquemment, que ce soit dans une branche éphémère privée ou dans une branche partagée, pour vous éviter de nombreux problèmes.

But let's say you didn't commit this time and you find yourself in the following scenario: you've done some work, haven't committed it yet but have git added it to the staging area.

Then tragedy strikes: you type git reset --hard and immediately realize that you've zeroed your local changes(!!) and brought back (in it's entirety) the previous commit.

This is a tough situation to be in. How do you recover your work? Is it even possible? The answer is yes! The solution involves some low level searching and can be approached in a couple of ways.

Since git creates new objects in the .git/objects folder as soon as something is added to the staging area we can look there for the most recently created objects:

Dans OSX (et les systèmes BSD), vous pouvez utiliser :

find .git/objects/ -type f | xargs stat -f "%Sm %N" | sort | tail -5

Sous Linux (avec des outils GNU), cette commande devrait fonctionner :

find .git/objects/ -type f -printf '%TY-%Tm-%Td %TT %p\n' | sort | tail -5

With tail -n you can cut to the last n results if your repository is very big:

Le résultat devrait ressembler à ceci :

Apr 2 12:26:33 2013 .git/objects//pack/pack-4cf657357973915f6fb6f90a41f69a88c0b08bb7.pack
Apr 2 12:29:56 2013 .git/objects//3d/d6845a69cd59dac0851e1ae0bd69dde950c46b
Apr 2 12:29:56 2013 .git/objects//68/c983f0cefb4bee2f753516ab6352ee2f2b7d29
Apr 2 12:35:23 2013 .git/objects//03/2c6a653cc00c302020293e020d8d68326b112e
Apr 2 15:34:55 2013 .git/objects//df/485610889e98ff773b4440a032ea2ede1338b9

In my case my sample file was lost exactly around 15:34 so I can inspect and retrieve it with:

git show df485610889e98ff773b4440a032ea2ede1338b9

Remember that the folder is part of the sha-1 reference, so remove the / slash to compute the correct id.

Note that git has specific low level command to explore dangling objects and to verify their connectivity: git fsck. For an alternative solution to this problem using fsck, check out this very cool answer on Stack Overflow.

Conclusions

Il y a beaucoup d'autres exemples et scénarios à présenter sur ce sujet. J'y reviendrai plus tard avec des cas plus intéressants.

One thing to take away from this should be the feeling that it's very very hard to screw up and lose your data when using git. That's all for now!

Follow the awesome @Bitbucket team or me @durdn for more Git rocking.

Prêt à découvrir Git ?

Essayez ce tutoriel interactif.

Démarrez maintenant