← Volver al blog

Recupera commits perdidos con git reflog (probablemente no has perdido nada)

tutorial git

"Lo he perdido todo" (Spoiler: probablemente no)

Respira hondo. Si el trabajo que echas en falta llegó a estar en un commit alguna vez -- aunque fuera una sola vez, aunque fuera en el branch equivocado, aunque fuera en un commit que luego "borraste" -- es casi seguro que sigue ahí, en tu repositorio, ahora mismo. Git es mucho mejor guardando cosas que perdiéndolas, y los próximos diez minutos de lectura terminarán, muy probablemente, con tu código de vuelta en pantalla.

Esta es la verdad tranquilizadora sobre cómo funciona Git por dentro: Git casi nunca borra nada de inmediato. Cada commit que haces se guarda como un objeto en el directorio .git, identificado por su SHA. Cuando ejecutas git reset --hard, borras un branch o reescribes la historia con un rebase, Git no elimina esos objetos de commit. Solo mueve punteros -- nombres de branch y HEAD -- para que dejen de apuntar a ellos. Los commits pasan a ser "inalcanzables", lo que significa que ningún branch ni tag conduce ya hasta ellos, pero los objetos en sí permanecen en disco durante semanas antes de que la garbage collection se plantee siquiera tocarlos.

Así que el problema no es que tus commits hayan desaparecido. El problema es que ya no tienes un nombre para ellos. Necesitas el SHA. Y Git mantiene un diario que ha estado registrando en silencio cada SHA por el que ha pasado tu repositorio: el reflog.

Qué es realmente el reflog

El reflog (reference log) es un diario local, propio de cada máquina, de los lugares a los que han apuntado HEAD y la punta de cada branch a lo largo del tiempo. Cada vez que HEAD se mueve -- porque haces commit, checkout, merge, rebase, reset o amend -- Git añade una línea a este diario. Es la caja negra de tu repositorio.

Para verlo, ejecuta:

git reflog

La salida tiene esta pinta:

e4f5a6b HEAD@{0}: reset: moving to HEAD~3
a1b2c3d HEAD@{1}: commit: add payment validation
9f8e7d6 HEAD@{2}: commit: refactor checkout flow
3c4d5e6 HEAD@{3}: checkout: moving from main to feature/payments

Léela de arriba abajo como "lo más reciente primero". Cada línea muestra el SHA al que apuntaba HEAD, una referencia posicional como HEAD@{1}, la operación que movió HEAD y una breve descripción. La sintaxis HEAD@{n} significa "dónde estaba HEAD hace n movimientos": HEAD@{0} es donde estás ahora, HEAD@{1} es una operación atrás, y así sucesivamente. Puedes usar estas referencias en cualquier comando que acepte un commit, y eso es exactamente lo que hace que las operaciones de rescate sean tan directas.

En el ejemplo de arriba, la historia está clara: hiciste dos commits (HEAD@{2} y HEAD@{1}), y luego un reset movió HEAD tres commits hacia atrás. Esos dos commits no están perdidos -- están ahí mismo, en a1b2c3d.

HEAD no es la única referencia con un log. Cada branch mantiene el suyo:

git reflog show feature/payments

Esto muestra cada posición que ha ocupado la punta de feature/payments -- útil cuando quieres la historia de un branch concreto sin el ruido de cada checkout que hayas hecho.

Escenarios de rescate, paso a paso

1. Te pasaste con git reset --hard

Querías deshacer un commit y borraste tres, o hiciste reset a un sitio completamente equivocado. El reflog registró dónde estabas justo antes del reset, y esa posición es HEAD@{1}:

git reset --hard HEAD@{1}

Eso es todo el arreglo. El reset en sí fue solo otro movimiento de HEAD, así que devolver HEAD a su posición anterior lo restaura todo. Si has hecho otras cosas desde el reset fallido, ejecuta antes git reflog, busca la línea justo anterior a la entrada del reset y haz reset a ese SHA. Para un recorrido más amplio por las estrategias de deshacer, mira cómo deshacer un commit de Git.

2. Borraste un branch

Borrar un branch borra el puntero, no los commits. Si el branch tuvo checkout o recibió commits recientemente en esta máquina, su punta está en el reflog de HEAD. Encuéntrala:

git reflog | grep "feature/payments"

Busca el último commit hecho en ese branch, o la última entrada checkout: moving from feature/payments -- el SHA de esa línea es la punta del branch. Después, recrea el branch apuntando a ella:

git branch feature/payments a1b2c3d

El branch está de vuelta, idéntico al momento en que se borró. Como extra, cuando borras un branch Git imprime el SHA de su punta (Deleted branch feature/payments (was a1b2c3d)) -- si ese mensaje sigue en el historial de tu terminal, puedes saltarte la búsqueda en el reflog por completo.

3. Un rebase salió mal

Un rebase reescribe commits, y a mitad de uno plagado de conflictos puede dar la sensación de que tu branch ha quedado destrozado sin remedio. No es así: el estado previo al rebase está en el reflog. Si el rebase sigue en marcha, la salida más limpia es git rebase --abort. Si ya terminó y odias el resultado, busca la entrada anterior a su inicio:

git reflog
# busca: "rebase (start)" -- la entrada justo DEBAJO
# es donde estaba tu branch antes del rebase
git reset --hard HEAD@{5}

(Sustituye {5} por la posición que ocupe la entrada previa al rebase en tu reflog.) Tu branch queda exactamente como estaba. Si los rebases te ponen en esta situación con frecuencia, nuestra guía de rebase interactivo sin miedo explica cómo convertirlos en rutina en lugar de riesgo.

4. Un commit desapareció tras --amend

git commit --amend no edita un commit -- crea uno nuevo y mueve el branch hacia él. El original queda inalcanzable pero intacto, y está en HEAD@{1} justo después del amend. Para inspeccionarlo:

git show HEAD@{1}

Si hiciste amend al commit equivocado o necesitas recuperar el original, haz reset a él o haz cherry-pick para llevarlo a donde corresponda.

5. Hiciste commits en un HEAD detached y luego cambiaste de sitio

Hiciste checkout a un commit o tag concreto, hiciste commits ahí en estado de HEAD detached y luego hiciste checkout a un branch -- y tus commits parecieron evaporarse. Git incluso te avisa de esto al cambiar, imprimiendo el SHA huérfano. El reflog lo tiene de todas formas:

git reflog
# busca el último commit que hiciste antes de la entrada
# "checkout: moving from <sha> to <branch>"
git branch rescued-work e4f5a6b

Tus commits huérfanos viven ahora en un branch de verdad, y puedes hacer merge o rebase para llevarlos a donde tengan que ir.

Inspecciona antes de restaurar

Antes de apuntar nada a un SHA recuperado, míralo. Confirma que es el commit que crees que es:

git show a1b2c3d

Esto imprime el mensaje del commit, el autor, la fecha y el diff completo. Si quieres navegar el reflog con los detalles completos de cada commit en lugar de entradas de una línea, usa el modo reflog del log:

git log -g

Y aquí va el hábito más seguro de todos: en lugar de hacer reset de tu branch actual a un commit recuperado, crea primero un branch temporal sobre él:

git branch rescue a1b2c3d

Ahora el trabajo recuperado tiene un nombre permanente y alcanzable. Tu branch actual no ha cambiado en nada, la garbage collection ya no podrá tocar jamás los commits rescatados, y puedes inspeccionar, comparar diffs y hacer merge con toda la calma. Un branch es gratis; crea uno siempre que no estés seguro al 100%.

Los límites, con honestidad

El reflog es una red de seguridad extraordinaria, pero tiene fronteras reales, y conviene conocerlas antes de necesitarlas:

  • Es estrictamente local. El reflog vive en tu máquina y nunca se hace push, pull ni clone de él. Un clone recién hecho empieza con un reflog vacío. Si perdiste commits en tu portátil, el reflog que importa es el de tu portátil -- el clone de un compañero no puede ayudarte, y la copia del servidor tampoco.
  • Las entradas caducan. Por defecto, las entradas del reflog para commits aún alcanzables desde un branch caducan a los 90 días, y las de commits inalcanzables a los 30 días. Pasado ese plazo, la garbage collection puede borrar de verdad los objetos inalcanzables. En la práctica es tiempo de sobra -- pero significa que el reflog rescata el error del mes pasado, no el del año pasado.
  • No puede recuperar lo que nunca tuvo commit. Los cambios sin commit destruidos por git reset --hard o git checkout -- <file> nunca fueron objetos de Git, así que ningún diario los registró. La única excepción parcial: los archivos que al menos pasaron por staging con git add existen como objetos blob, y git fsck --lost-found puede desenterrar esos blobs sueltos en .git/lost-found/ -- contenido sin nombres de archivo ni historia, pero mejor que nada. Es un último recurso, no un workflow.
  • Los stashes descartados tienen su propia salida de emergencia. Un stash que descartaste o limpiaste no está en el reflog de HEAD, pero los commits del stash suelen sobrevivir como objetos sueltos. git fsck --unreachable | grep commit más git show sobre los candidatos puede encontrarlos. Si usas mucho el stash, nuestra guía de git stash cubre los workflows más seguros.

Prevención: que los próximos pánicos sean aburridos

Todos los escenarios anteriores tenían un requisito: el trabajo tenía commit. De ahí sale el único consejo de prevención que de verdad importa: haz commit pronto y haz commit a menudo. Los commits pequeños, imperfectos, de trabajo en curso, no cuestan nada -- luego puedes hacer squash, reescribirlos o reordenarlos. En el momento en que algo tiene commit, está en la base de objetos y el reflog le cubre las espaldas. En el momento en que no, dependes de la suerte.

Para cambios rápidos de contexto en los que un commit se siente demasiado pesado, usa stash en lugar de hacer malabares con cambios sin commit entre checkouts -- un stash también es un objeto real que Git puede volver a encontrar.

Dónde encaja GitSquid (y dónde gana la CLI)

Respuesta honesta: el reflog es uno de esos sitios donde la línea de comandos es la herramienta adecuada. GitSquid no tiene un navegador de reflog, y cuando necesites git reflog, deberías abrir una terminal y usarlo -- los comandos de este artículo son la vía de recuperación.

Lo que GitSquid hace, en cambio, es volver más seguro el workflow de alrededor, para que recurras al reflog menos a menudo. La línea de tiempo de archivos (clic derecho en cualquier archivo y elige Show history) te da una historia visual por archivo, que a menudo responde a "¿dónde fue a parar ese código?" sin necesidad de rescate alguno. Las operaciones destructivas como reset o el borrado de branches pasan por menús contextuales explícitos en el grafo de commits, así que ves exactamente lo que estás a punto de hacer antes de hacerlo. Y cuando estás reconstruyendo "qué acaba de pasar", el registro de comandos (Cmd/Ctrl+Shift+L) muestra cada comando de Git que GitSquid ejecutó sobre tu repositorio, con todos los argumentos y códigos de salida -- que es exactamente la evidencia que quieres junto a un reflog cuando reconstruyes un incidente.

Referencia rápida

Situación Comando
Ver por dónde ha pasado HEAD git reflog
Ver por dónde ha pasado la punta de un branch git reflog show <branch>
Deshacer un reset --hard fallido git reset --hard HEAD@{1}
Restaurar un branch borrado git branch <name> <sha>
Deshacer un rebase terminado git reset --hard HEAD@{n} (entrada previa al rebase)
Inspeccionar un commit recuperado git show <sha>
Navegar el reflog con todos los detalles git log -g
Aparcar un rescate de forma segura git branch rescue <sha>
Último recurso para archivos en staging que nunca tuvieron commit git fsck --lost-found

El reflog convierte "lo he perdido todo" en "tuve un puntero extraviado durante diez minutos". Haz commit a menudo, consulta el reflog antes de entrar en pánico y aparca las recuperaciones en un branch temporal antes de tocar nada más. Y si quieres un cliente de Git que haga explícitas las operaciones destructivas y guarde un registro completo de cada comando que ejecuta, descarga GitSquid gratis -- no sustituirá a tu reflog, pero te ayudará a necesitarlo menos.