Commits perdidos

Mario González - Blog sobre desarrollo web

Soluciones a los problemas más habituales con Git: cómo recuperar commits perdidos

Este post forma parte de una serie orientada a proporcionar soluciones a los atascos más habituales que nos encontramos con Git:

  1. He creado commits en la rama incorrecta
  2. Cómo recuperar commits perdidos

A veces hemos estado avanzando commits y de repente ya no los vemos en el log. Esto suele suceder cuando hemos creado commits estando en detached HEAD. Si viajamos a un commit concreto en vez de a una rama, por ejemplo, para ver cómo estaba nuestro código en un momento determinado de su evolución, entramos en el estado detached HEAD, que básicamente significa que no estamos apuntando a ninguna rama.

Si creamos commits estando en detached HEAD, estos commits crearán un nuevo itinerario sin nombre de rama. Mientras estemos en el último commit de este itinerario podremos verlos en el log, pero en el momento que nos movamos a una rama, desaparecerán. ¿Significa eso que hemos perdido esos cambios?

En Git, todos los cambios que incluyamos en un commit se van a quedar ahí para siempre (bueno, para siempre no, en algún momento los eliminará el recolector de basura, pero pasará mucho tiempo antes). Los commits no se borran, simplemente desaparecen de nuestra vista cuando no se puede llegar a ellos a partir de ninguna referencia (rama o tag).

Git nos ofrece una herramienta valiosísima para recuperar commits desaparecidos: el comando reflog.


git reflog
Salida del comando git reflog

El comando reflog nos lista todos los movimientos de nuestras referencias, incluyendo el puntero HEAD. Aquí podremos ver todos los saltos que hemos hecho entre commits o entre ramas, los reset, los merge, los rebase… y cada movimiento va acompañado del hash del commit donde estábamos en ese momento. Si nos han desaparecido commits, los veremos en este listado.

Veamos un ejemplo. Imaginemos que estamos en la rama develop y cambiamos a un commit antiguo para ver cómo estaba el código en ese punto del desarrollo.

Repositorio en detached HEAD
Al saltar a un commit anterior entramos en detached HEAD.

Si en este momento se nos olvida volver a la rama en la que estábamos y empezamos a crear commits, éstos no se estarán creando en ninguna rama. Estaremos dibujando un itinerario de commits que sólo son accesibles -por ahora- a partir de la referencia HEAD.

Commits sin rama
Estamos creando una secuencia de commits sin nombre de rama.

El log nos muestra los commits que son accesibles a partir de alguna referencia. Si aún podemos ver estos nuevos commits en el log es porque la referencia HEAD apunta al último. En cuanto nos movamos a una rama, estos commits quedarán sin referencia y dejarán de aparecer en el log. Pero eso no quiere decir que los commits no estén.

Commits no alcanzables
Los commits no accesibles no aparecen en el log.

Como decíamos más arriba, Git no borra los commits hasta que no pasa el recolector de basura. Esto quiere decir que podemos volver a recuperar esos commits que han desaparecido de nuestra vista. Si supiéramos el hash del último commit que creamos, podríamos ir directamente a él con git checkout.

El problema es que seguramente no sepamos cuál es el hash de ese commit. Y aquí es donde entra el comando git reflog. Este comando nos muestra un listado con todos los movimientos que han tenido las referencias en nuestro repositorio local. Cada vez que hemos cambiado de rama, hemos hecho creado un commit, hemos hecho un merge, un reset o un rebase, las referencias se han movido, y cada uno de estos movimientos queda registrado en el reflog.

git reflog nos muestra los últimos movimientos de nuestras referencias

Este listado, que a veces será un poco difícil de interpretar, nos muestra los hashes de los commits desde donde se ha originado cada movimiento. Fijándonos en los mensajes de los commits o deduciendo a partir de qué movimientos se han hecho, podemos encontrar los hashes de los commits que habíamos perdido.

Una vez encontrados, sólo tenemos que ir al último commit de la secuencia perdida, y volveremos a verlos en el log, porque estarán accesibles a partir de la referencia HEAD.

Es el momento de hacer algo para que no vuelvan a desaparecer: crear una referencia. Si creamos una rama en el commit en el que estamos, conseguiremos que esos commits ya estén referenciados y entonces se mostrarán en el log.


git checkout -b C8      # Crea una rama y salta a ella
Asignando un nombre de rama, los commits ya son accesibles y siempre aparecerán en el log.

Ya hemos conseguido recuperar nuestros commits de manera fácil. La única dificultad puede estar en buscar hashes de commits en el listado del reflog, pero sólo es cuestión de práctica.

Puede ocurriros que la intención inicial fuera aplicar esos cambios al código que teníais en el commit C6, y no en el C3. Si es vuestro caso, lo siguiente que tendréis que hacer es volver a la rama develop y lanzar un cherry-pick de cada commit. Una vez hecho esto, podéis borrar la rama temporal que habíais creado.


git checkout develop
git cherry-pick C7
git cherry-pick C8
git branch -D feature

Con esto se os aplicarán los cambios que teníais en C7 y C8 al código que tenéis en C6.

Resultado después de los cherry-pick

Resumiendo

Cuando nos desaparezcan commits tenemos que recordar que no se han borrado, sino que por alguna razón se han convertido en commits inaccesibles: no se puede llegar a ellos a partir de ninguna referencia (rama, tag o HEAD). Para recuperarlos sólo tenemos que conseguir el hash del último commit de la secuencia perdida.

El comando git reflog nos muestra un listado de todos los movimientos que han hecho nuestras referencias, y cada movimiento va acompañado del hash del commit desde donde se produjo. De este listado cogeremos el hash del commit que nos interesa, y le pondremos un nombre de rama para que esos commits desaparecidos ya no sean inaccesibles.

¿Qué opinas?

Tu dirección de correo electrónico no será publicada. * Campos obligatorios