Contenido

Git: recuperación de changesets perdidos

Hoy me he enfrentado a un problema algo desagradable en Git: Borré un commit. Básicamente había perdido un artículo de mi blog y algunos cambios más.

En condiciones normales, esto hubiera sido una pérdida terrible. Pero en este caso Git vino al rescate.

Pondré un ejemplo para perder changeset y después recuperarlos. Si tenéis prisa, saltad directamente a la sección “cómo recuperar un commit” :D

Git es una herramienta muy potente

Perdiendo changesets o commits

Pero… ¿Cómo puede darse esta situación?

Pues basta con hacer commit y luego cambiar el HEAD:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ git init
Initialized empty Git repository in /tmp/post/.git/
$ touch foo
$ git add foo
$ git commit -am "commit inicial"
[master (root-commit) 0ac79b1] commit inicial
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 foo
$ echo 'hola' > foo
$ git commit -am "Voy a perder este commit"
[master bea2640] Voy a perder este commit
 1 file changed, 1 insertion(+)
$ git reset --hard HEAD^

En este punto, el último commit no es accesible (directamente):

1
2
$ git log --oneline
0ac79b1 (HEAD, refs/heads/master) Commit inicial

Cómo recuperar un commit

Evidentemente, hay dos casos. En el primero sabemos el identificador del commit a recuperar, en este caso bea2640. Pero también está el caso chungo, en el que no conocemos este identificador. Veamos ambos.

Recuperar un commit dado su SHA-1

Nada más sencillo que hacer un cherry-pick:

1
2
3
4
$ git cherry-pick bea2640
 [master 7aa28d9] Voy a perder este commit
  Date: Fri Mar 6 17:13:36 2015 +0100
  1 file changed, 1 insertion(+)

Otra opción es cambiar la referencia al HEAD:

1
2
$ git reset --hard bea2640
HEAD is now at bea2640 Voy a perder este commit

Con cherry-pick crearemos un commit nuevo, mientras que con reset reutilizaremos el viejo. Cada uno tiene sus ventajas: si teníamos commits posteriores, con cherry-pick los mantenemos, mientras que con reset se eliminarán.

Averiguar el SHA-1 de un commit borrado

La otra opción, la que más nos puede asustar, es qué hacer si no tenemos el SHA-1 para poder recuperarlo. En este caso hay que preguntarle a Git por los changesets huérfanos:

1
2
3
$ git fsck --lost-found
Checking object directories: 100% (256/256), done.
dangling commit bea2640b7b2753d3e48cdd78a6d03f229e11e72f

En este punto podemos preguntar qué tiene ese changeset, para asegurarnos de que es lo que queremos:

1
$ git show bea2640b7b2753d3e48cdd78a6d03f229e11e72f

Y proceder a recuperarlo tal y como hemos visto en el apartado anterior.

Explicación más detallada

En Git, un changeset o commit es un objeto. Los objetos tienen un SHA-1 único que los identifica. Si este objeto se encuentra en local pero no en remoto, puede referirse siempre mediante el SHA-1.

Las ramas, HEAD y otras referencias son… bueno, pues referencias XD. En otras palabras: son punteros a un SHA-1. Tal cual:

1
2
$ cat .git/refs/heads/master
bea2640b7b2753d3e48cdd78a6d03f229e11e72f

Así que podemos jugar con este SHA-1 persistente en lugar de usar las efímeras referencias. Es algo incómodo de recordar (risas), pero muy interesante para arreglar estropicios.

Más información

Podéis encontrar más información en la propia documentación de Git o ir a los posts que escribí yo mismo para sobre aprender a manejar Git(1), aprender a manejar Git(2), y aprender a manejar Git(3).