git reflog: recupera commits y ramas que creías perdidos

git reflog guarda cada posición de HEAD y te deja recuperar commits, ramas y trabajo tras un reset --hard. Aprende a leerlo y a deshacer el pánico.

git reflog: recupera commits y ramas que creías perdidos

Conoces el momento. Hiciste git reset --hard sobre el commit equivocado, o borraste una rama con git branch -D, y de repente el trabajo de la mañana “ya no está”. El terminal no protesta. No hay confirmación, no hay papelera. Solo el sudor frío de pensar que has perdido dos horas de código. Ese pánico dura hasta que descubres git reflog, el registro que guarda dónde ha estado HEAD y que convierte casi cualquier accidente en algo reversible. Aquí te explico cómo leerlo y cómo usarlo para recuperar commits, ramas y resets que creías irrecuperables.

¿Qué es exactamente el reflog?

El reflog es un registro local de cada posición por la que ha pasado HEAD en tu repositorio. Cada vez que haces un commit, un checkout, un reset, un merge o un rebase, Git anota en qué commit acabó apuntando HEAD y por qué. Ejecutar git reflog sin argumentos es lo mismo que git reflog show HEAD: te muestra ese historial de movimientos, del más reciente al más antiguo.

Una línea del reflog tiene tres partes. El hash corto del commit, el selector HEAD@{n} que indica cuántos movimientos hacia atrás está esa posición, y la acción que provocó el cambio.

$ git reflog
7c2b8f5 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
a4d19e0 HEAD@{1}: commit: validación del formulario de registro
3b6d4c2 HEAD@{2}: commit: endpoint de registro
7c2b8f5 HEAD@{3}: checkout: moving from feature-login to main
e1f0a9d HEAD@{4}: commit: maquetación inicial del login

HEAD@{0} es siempre donde estás ahora. HEAD@{1} es donde estabas justo antes del último movimiento, y así sucesivamente. Lee esas líneas como una máquina del tiempo: cada una es un punto al que puedes volver.

¿Por qué en Git casi nada se pierde de verdad?

Porque un commit no se borra cuando lo dejas de usar: solo deja de tener quien lo señale. Un commit en Git es un objeto guardado en .git/objects, identificado por su hash. Las ramas y HEAD son punteros que apuntan a esos objetos. Cuando haces git reset --hard, Git mueve el puntero de la rama a otro commit, actualiza el índice y sobrescribe tu working tree para que coincida.[1] Lo que no hace es tocar los objetos: el commit del que “te alejaste” sigue intacto en la base de datos.

El problema es que ya nada lo referencia. git log no lo muestra, porque git log solo recorre lo alcanzable desde HEAD. El commit está ahí, huérfano, esperando. Y el reflog es precisamente lo que guarda su hash aunque ninguna rama lo apunte. Esa es la diferencia entre “perdido para siempre” y “descolgado un rato”: el reflog conserva la referencia que tú perdiste.

Diagrama de dos estados mostrando cómo git reset --hard mueve los punteros de rama y HEAD sin borrar los commits, dejando C2 y C3 huérfanos pero intactos en .git/objects, y cómo el reflog conserva una referencia a C3 en la entrada anterior de HEAD.
Un commit no se borra al hacer reset: solo se queda sin puntero. El reflog es lo que recuerda dónde apuntaba HEAD antes.

Con ese modelo en la cabeza, recuperar deja de ser magia y pasa a ser un procedimiento.

Recuperar tras un reset --hard al commit equivocado

El caso más común y el más aterrador. Querías tirar los últimos cambios de tu working tree, pero pusiste el commit de destino mal y reset --hard se llevó por delante commits buenos. La rama ahora apunta a un sitio anterior y tu código reciente parece haberse esfumado.

Abre el reflog y localiza la línea inmediatamente anterior al reset. En el ejemplo de arriba, HEAD@{0} es el propio reset y HEAD@{1} es el estado justo antes, con la validación del formulario todavía presente. Para volver ahí:

# HEAD@{1} = donde apuntaba la rama justo antes del último movimiento
git reset --hard HEAD@{1}

HEAD@{1} no es un hash memorizado: es una referencia relativa que Git resuelve al commit anterior del reflog. Por eso funciona aunque no hayas apuntado el hash. Si prefieres ser explícito, coge el hash de la línea (a4d19e0) y haz git reset --hard a4d19e0. El resultado es idéntico.

Un aviso: reset --hard sobrescribe el working tree. Si tienes cambios sin guardar encima de este lío, haz un git stash antes de experimentar con resets. Recuperar un commit no te devuelve lo que nunca llegó a estar en Git.

Cargando ejercicio...

Rescatar un commit huérfano tras un rebase

Un rebase que sale mal deja commits huérfanos igual que un reset, y el reflog los sigue teniendo. Después de un git rebase que reescribió la historia y dejó fuera un commit que sí querías, el reflog conserva el hash del estado previo a la operación. Búscalo:

$ git reflog
9d0e1a7 (HEAD -> main) HEAD@{0}: rebase (finish): returning to refs/heads/main
9d0e1a7 HEAD@{1}: rebase (pick): reescribe el servicio de pagos
a4d19e0 HEAD@{2}: rebase (start): checkout main~3
a4d19e0 HEAD@{3}: commit: fix del cálculo de impuestos

Ese a4d19e0 de HEAD@{3} es el commit con el fix que el rebase se dejó atrás. Tienes dos formas de rescatarlo. Si quieres el commit entero en una rama nueva y segura:

# Crea una rama que empieza en el commit huérfano y salta a ella
git switch -c rescate a4d19e0

git switch es la orden moderna para moverte entre ramas y crearlas. Dejó de estar marcada como experimental en Git 2.46 y hoy es la recomendada frente a checkout para este tipo de tareas, porque separa “cambiar de rama” de “restaurar archivos” en dos comandos distintos.[2] El -c crea la rama rescate apuntando al hash y te lleva a ella en una sola operación.

Si lo que quieres es solo reaplicar ese commit sobre tu rama actual sin traerte toda su historia, usa git cherry-pick a4d19e0. Elige según necesites el árbol completo o un cambio suelto.

Resucitar una rama que borraste

Al borrar una rama con git branch -D, Git solo elimina el puntero que nombraba esos commits; los commits en sí no se tocan. Como siguen en la base de datos y el reflog recuerda dónde estaba la punta, resucitarla es cuestión de encontrar ese hash.

Cuando borras una rama, Git te imprime el hash de su último commit en el mismo mensaje:

$ git branch -D feature-checkout
Deleted branch feature-checkout (was 5e8c3b0).

Si lo cazaste, ya está: git branch feature-checkout 5e8c3b0 recrea la rama exactamente donde estaba. ¿Y si cerraste el terminal y no lo apuntaste? El reflog de HEAD guarda el último punto en que estuviste sobre esa rama, así que busca ahí la línea del checkout: moving from feature-checkout o el último commit que hiciste en ella, y usa ese hash:

git branch feature-checkout 5e8c3b0

La rama vuelve con todos sus commits. Nadie tiene por qué enterarse de que la borraste.

¿Se puede recuperar? Tabla rápida

No todo entra en la red del reflog. La línea divisoria es simple: si algo llegó a ser un commit, casi siempre vuelve; si nunca pasó por ahí, el reflog no puede ayudarte.

Árbol de decisión horizontal: si el cambio llegó a ser commit, el reflog permite recuperarlo con reset --hard sobre la entrada correspondiente o recreando la rama con git branch; si solo se hizo git add, git fsck --lost-found puede rescatar el blob; si nunca hubo git add, no es recuperable.
La pregunta que decide si algo se recupera no es cuánto tiempo ha pasado, sino si llegó a ser un objeto de Git.
Situación¿Recuperable?Cómo
Cambios en el working tree sin git addNoNo hay objeto que rescatar
Cambios staged (git add) sin commitA vecesgit fsck --lost-found puede encontrar el blob
Commit perdido tras reset --hardReflog: reset --hard HEAD@{1}
Rama borrada con git branch -DReflog + git branch <nombre> <hash>
Commit huérfano tras un gc pasados 30 díasNoLa recolección de basura ya lo expiró
El reflog de otro clon del repoNoEl reflog es estrictamente local

Si tu caso es un archivo concreto que borraste, más que un commit entero, el procedimiento cambia un poco y lo cubro en recuperar un archivo borrado.

Cuándo el reflog no te salva

El reflog tiene límites reales, y conviene conocerlos antes de confiar la vida en él. El primero es el más duro de aceptar: si nunca hiciste git add ni git commit, no existe ningún objeto en la base de datos, y no hay reflog que valga. El trabajo que solo vivía en tu editor y que un reset --hard sobrescribió no está en ninguna parte de Git.

El segundo límite es que el reflog es solo tuyo, en esta máquina. No se clona, no se envía con push, no viaja con el repositorio. Si esperabas encontrar en el reflog de un compañero el commit que perdiste, no está: cada clon tiene su propio historial de movimientos de HEAD.

El tercero es el tiempo. La recolección de basura (git gc) acaba eliminando los objetos que llevan demasiado tiempo sin referencia. Por defecto, las entradas alcanzables del reflog expiran a los 90 días y las inalcanzables a los 30.[1] Es margen de sobra para un accidente que descubres el mismo día, pero no cuentes con recuperar un commit huérfano que abandonaste hace tres meses.

Higiene del reflog y el último recurso: git fsck

Antes de darte por vencido, hay una red debajo de la red. Si el commit que buscas ya no aparece en git reflog, git fsck recorre la base de datos entera buscando objetos colgados que ninguna referencia menciona:

# Escribe los objetos colgados en .git/lost-found/ para inspeccionarlos
git fsck --lost-found

Un “dangling commit” es exactamente eso: un commit presente en la base de datos que ningún ref usa directamente.[3] --lost-found los vuelca en .git/lost-found/commit/ para que puedas inspeccionar cada uno con git show <hash> y decidir cuál rescatar. Es el recurso al que acudir cuando el reflog ya no tiene la entrada, y también la única vía razonable para intentar recuperar cambios que llegaste a hacer git add pero nunca a commitear.

Otro truco útil cuando no recuerdas el HEAD@{n} exacto: el reflog acepta sintaxis temporal. git reset --hard HEAD@{1.hour.ago} te lleva a donde apuntaba HEAD hace una hora en este repositorio local. Cómodo cuando sabes “más o menos cuándo” estaba bien todo, pero no el número de movimientos.

Errores comunes

Creer que reset --hard borra el commit

El error de base del que salen todos los demás. reset --hard mueve el puntero de la rama y reescribe tu working tree, pero el commit del que te alejaste sigue en .git/objects intacto. Interiorizar que borras la referencia y no el objeto es lo que te quita el miedo a experimentar.

Buscar en git log en lugar de git reflog

Cuando pierdes un commit, el instinto es abrir git log a ver si aparece. No aparecerá. git log solo recorre lo alcanzable desde HEAD, y un commit huérfano por definición ya no lo es. El reflog es el único que guarda esa referencia caída. Si buscas trabajo perdido, tu primer comando es git reflog, no git log.

Confiar en el reflog de un compañero

El reflog es local. Punto. Si tu commit perdido no está en tu reflog, no lo vas a encontrar clonando de nuevo el repo ni preguntando al reflog de otra persona. Cada máquina tiene el suyo.

Dejar pasar semanas

El reflog no es eterno. Un commit que abandonaste hace un día lo recuperas sin drama; uno que dejaste huérfano hace tres meses puede haber sido expirado por git gc. Si sabes que perdiste algo importante, recupéralo hoy.

Cuando entiendes cuál de estas trampas te ha pillado, el resto del proceso de deshacer cambios en Git encaja solo. Si quieres el mapa completo de resets, reverts y checkouts, la guía completa para deshacer cambios en Git recorre cada herramienta, y para el caso concreto de tirar tu commit más reciente tienes deshacer el último commit.

Todo esto (el modelo de objetos, HEAD, ramas, resets y el reflog como red de seguridad) es lo que trabajamos paso a paso en el curso Git de cero a profesional, con ejercicios interactivos para que pierdas el miedo a romper cosas.

Un concepto nuevo cada semana

Fuentes

  1. Documentación de git-reflog (Git): comportamiento de git reflog show, sintaxis HEAD@{n} y valores por defecto de expiración (gc.reflogExpire 90 días, gc.reflogExpireUnreachable 30 días).
  2. Documentación de git-switch (Git): git switch -c <rama> <inicio> como forma recomendada de crear y cambiar de rama; el aviso de comando experimental se retiró.
  3. Documentación de git-fsck (Git): opción --lost-found, definición de commit colgado (dangling) y volcado en .git/lost-found/.

Preguntas frecuentes

¿El reflog se sube al hacer push?

No. El reflog es estrictamente local: vive en tu carpeta .git y no se clona, ni se envía con git push, ni se comparte de ninguna forma. Cada clon del repositorio tiene su propio reflog con el historial de movimientos de HEAD de esa máquina.

¿Cuánto tiempo permanecen las entradas del reflog?

Por defecto, las entradas que siguen siendo alcanzables se conservan 90 días (gc.reflogExpire) y las inalcanzables 30 días (gc.reflogExpireUnreachable), tras los cuales git gc puede eliminarlas. Son valores configurables, pero para la mayoría de accidentes tienes margen de sobra si actúas el mismo día.

¿git reflog y git log son lo mismo?

No, y confundirlos es la razón por la que mucha gente cree haber perdido trabajo. git log muestra la historia alcanzable desde HEAD siguiendo las relaciones padre-hijo entre commits. git reflog muestra dónde ha estado HEAD movimiento a movimiento, incluidos commits que ya no son alcanzables desde ninguna rama. Cuando buscas algo perdido, el reflog es el que lo tiene.

¿Puedo recuperar cambios que nunca commiteé?

Casi nunca. Si los cambios solo estaban en tu working tree sin pasar por git add, no existe ningún objeto en Git y no hay nada que rescatar. Si llegaste a hacer git add pero no a commitear, hay una posibilidad: git fsck --lost-found puede localizar el blob staged entre los objetos colgados. No es garantía, pero merece el intento.

¿Cómo recupero una rama borrada si no recuerdo el hash?

Cuando ejecutas git branch -D, Git imprime el hash del último commit en el mismo mensaje (“Deleted branch X (was 5e8c3b0)”). Si ya cerraste esa ventana, ejecuta git reflog y busca la última línea en la que estabas sobre esa rama o el último commit que hiciste en ella. Con ese hash, git branch <nombre> <hash> recrea la rama justo donde estaba.