Llevamos seis módulos aplazando esta lección. En la 03-02 dijimos que el reflog explica cómo Git recuerda por dónde ha pasado HEAD. En la 03-06 prometimos que borrar una rama con -D casi nunca es irreversible, y remitimos aquí. En la 05-01 dijimos que un rebase desastroso se puede deshacer, y remitimos aquí. En la 05-04, que un stash drop accidental es recuperable. En la 09-02, que los commits que un reset --hard deja atrás siguen ahí. Y en la 09-03, que origin/main@{1} guarda dónde estaba el servidor antes de la reescritura.

Todas esas promesas se apoyan en la misma pieza, y es hora de desarrollarla entera.

El reflog es probablemente la funcionalidad de Git con mejor relación entre lo que salva y lo poco que se conoce. Mucha gente lleva años usando Git sin haberlo ejecutado nunca, y es exactamente el comando que convierte "he perdido tres días de trabajo" en "he tardado dos minutos en recuperarlo".

Empecemos por lo más importante: si has llegado aquí en pánico, no has perdido nada, siempre que lo que buscas llegara a ser un commit. Sigue leyendo con calma.

Contenido

  1. Qué es el reflog y dónde vive
  2. Local y temporal: los dos límites que hay que conocer
  3. Leer git reflog
  4. HEAD@{n} frente a HEAD@{tiempo}
  5. El patrón universal de recuperación
  6. Recetario: reset --hard, rama borrada, rebase, merge, --amend
  7. Cuando el reflog no basta: git fsck --lost-found
  8. Recuperar un stash eliminado
  9. Los límites reales: lo que no se puede recuperar
  10. Higiene: no destruir tu propia red de seguridad

  1. Qué es el reflog y dónde vive

El reflog es el registro local de todos los movimientos de cada referencia.

Cada vez que una referencia —HEAD, una rama, una rama de seguimiento remota— cambia de valor, Git escribe una línea en un fichero de texto anotando de dónde venía, adónde va, quién lo hizo y por qué.

Míralo directamente:

cat .git/logs/HEAD | tail -3
4f8a2e6c9b1d5e3a b52c9d1e4a7f3c8b Ana Ferrer <[email protected]> 1785312041 +0200	commit: GT-241 base del filtro
b52c9d1e4a7f3c8b 7d3a8f4a2c6e9b1d Ana Ferrer <[email protected]> 1785312388 +0200	commit: GT-241 calcular el contador
7d3a8f4a2c6e9b1d 4f8a2e6c9b1d5e3a Ana Ferrer <[email protected]> 1785312551 +0200	reset: moving to HEAD~2

Cada línea tiene: hash anterior, hash nuevo, identidad, marca de tiempo, y descripción de la operación. Es texto plano, sin magia.

La estructura de directorios:

find .git/logs -type f | head
.git/logs/HEAD
.git/logs/refs/heads/main
.git/logs/refs/heads/GT-241
.git/logs/refs/remotes/origin/main
.git/logs/refs/stash
Fichero Registra los movimientos de
.git/logs/HEAD HEAD: todo lo que has hecho, incluidos los cambios de rama
.git/logs/refs/heads/<rama> Solo esa rama
.git/logs/refs/remotes/origin/<rama> La rama de seguimiento: lo que trajo cada fetch (09-03)
.git/logs/refs/stash La pila de stashes (05-04)

Y aquí está la clave de todo el módulo. Recupera la idea de la lección 09-01:

Un commit sobrevive mientras algo lo alcance. Las entradas del reflog cuentan como referencias.

Por eso git gc no elimina un commit que aparece en el reflog, aunque ninguna rama lo apunte. El reflog no es solo un registro: es lo que mantiene vivos los objetos huérfanos. Esa es la razón técnica de que la recuperación funcione.

flowchart LR
    subgraph refs["Referencias que mantienen vivo un commit"]
        R1["Ramas<br/>refs/heads/*"]
        R2["Etiquetas<br/>refs/tags/*"]
        R3["Ramas remotas<br/>refs/remotes/*"]
        R4["Stash<br/>refs/stash"]
        R5["REFLOG<br/>.git/logs/*"]
    end
    C["commit b52c9d1"]
    R1 -.-> C
    R5 ==>|"aunque las demás<br/>desaparezcan"| C

  1. Local y temporal: los dos límites que hay que conocer

Antes del recetario, las dos limitaciones. No entenderlas es lo que hace que alguien confíe en el reflog cuando no debe.

Es LOCAL

El reflog no viaja. No se publica con push, no se descarga con fetch, y un git clone no lo trae.

git clone git.ejemplo.es:equipo/gestor-tareas.git
cd gestor-tareas
git reflog
2f8c6e1 HEAD@{0}: clone: from git.ejemplo.es:equipo/gestor-tareas.git

Una sola entrada. Todo el historial de movimientos que Ana tenía en su máquina se queda en su máquina.

Tres consecuencias prácticas:

  1. El reflog de Ana no puede rescatar el desastre de Bruno. Cada uno tiene el suyo.
  2. El reflog no es una copia de seguridad. Si el disco muere, muere con él. Las copias de seguridad reales son git bundle y los propios clones del equipo (lección 09-05).
  3. Un clon recién hecho no tiene red de seguridad. El primer reset --hard desafortunado en un clon nuevo sí puede dejarte sin nada que recuperar, porque no hay entradas anteriores.

Y una consecuencia positiva, la del apartado 9 de la lección 09-03: si un compañero fuerza y destruye una rama del servidor, tu reflog de origin/rama conserva dónde estaba, y con él se restaura.

Es TEMPORAL

Las entradas caducan. Dos parámetros lo gobiernan:

git config --get gc.reflogExpire            # por defecto: 90 días
git config --get gc.reflogExpireUnreachable # por defecto: 30 días
Parámetro Se aplica a Valor por defecto
gc.reflogExpire Entradas cuyo commit sigue siendo alcanzable 90 días
gc.reflogExpireUnreachable Entradas cuyo commit ya no es alcanzable desde ninguna rama 30 días

La segunda es la que importa: los commits realmente huérfanos tienen 30 días. Después, un git gc los purga definitivamente.

La caducidad no ocurre sola: la ejecuta git gc (lección 08-06), que Git lanza automáticamente cuando se acumulan objetos sueltos. En la práctica, tienes semanas, no meses, y desde luego no años.

Puedes alargar el plazo:

# Un año para todo, en tus repositorios de trabajo
git config --global gc.reflogExpire "1 year"
git config --global gc.reflogExpireUnreachable "1 year"

# O por rama, con un patrón
git config gc.refs/heads/main.reflogExpire never

Cuesta unos megabytes y compra mucha tranquilidad.

Y lo contrario, que nunca debes ejecutar salvo que sepas exactamente qué haces (lección 08-06):

# DESTRUYE la red de seguridad de todo este módulo
git reflog expire --expire=now --all
git gc --prune=now

  1. Leer git reflog

git reflog
7d3a8f4 HEAD@{0}: reset: moving to HEAD~2
9c4e7b2 HEAD@{1}: commit: GT-241 estilos del indicador
3e7f1a8 HEAD@{2}: commit: GT-241 calcular el contador
8a1f6c3 HEAD@{3}: rebase (finish): returning to refs/heads/GT-241
8a1f6c3 HEAD@{4}: rebase (pick): GT-241 base del filtro
2f8c6e1 HEAD@{5}: rebase (start): checkout origin/main
b52c9d1 HEAD@{6}: checkout: moving from main to GT-241
7d3a8f4 HEAD@{7}: pull: Fast-forward
4f8a2e6 HEAD@{8}: commit: chore: configuración de eslint

Cómo se lee:

  • HEAD@{0} es siempre lo último que pasó. El orden es del presente hacia el pasado.
  • El hash de cada línea es dónde quedó HEAD DESPUÉS de esa operación. Este detalle es crucial y produce el error más frecuente: si quieres el estado anterior a la operación de HEAD@{0}, necesitas el hash de HEAD@{1}.
  • La descripción dice qué comando lo provocó.

El vocabulario de las descripciones:

Descripción Qué la produjo
commit: git commit
commit (amend): git commit --amend
commit (initial): El primer commit del repositorio
checkout: moving from X to Y git switch o git checkout
reset: moving to X git reset
pull: / merge X: Una integración
rebase (start/pick/finish): Las fases de un rebase
cherry-pick: git cherry-pick
revert: git revert
branch: Created from X git branch o git switch -c
clone: from X El clon inicial
fetch: forced-update Alguien reescribió la rama remota (09-03)

El reflog de una rama concreta

git reflog show GT-241
9c4e7b2 GT-241@{0}: commit: GT-241 estilos del indicador
3e7f1a8 GT-241@{1}: commit: GT-241 calcular el contador
8a1f6c3 GT-241@{2}: rebase (finish): refs/heads/GT-241 onto 2f8c6e1
b52c9d1 GT-241@{3}: branch: Created from HEAD

Mucho más limpio que el de HEAD cuando sabes qué rama te interesa, porque no incluye los cambios de rama.

Formatos útiles

# Con fechas relativas: imprescindible para orientarse
git reflog --date=relative -10

# Con fecha y hora exactas
git reflog --date=iso -10

# Con el mensaje del commit además de la operación
git reflog --format='%h  %gd  %gs  |  %s' -10

# Solo lo de una rama, con gráfico
git log --walk-reflogs --oneline GT-241
7d3a8f4 HEAD@{hace 3 minutos}: reset: moving to HEAD~2
9c4e7b2 HEAD@{hace 41 minutos}: commit: GT-241 estilos del indicador
3e7f1a8 HEAD@{hace 2 horas}: commit: GT-241 calcular el contador

--date=relative es el que más ayuda en una recuperación: "lo tenía justo antes de comer" se traduce directamente a una entrada.

Y git log -g (o --walk-reflogs), que recorre el reflog mostrando cada entrada como un commit completo:

git log -g --oneline --stat -5

Útil cuando no reconoces un commit por su mensaje y necesitas ver qué ficheros tocaba.

  1. HEAD@{n} frente a HEAD@{tiempo}

Dos sintaxis que se parecen y significan cosas distintas.

Sintaxis Qué es Ejemplo
HEAD@{2} La entrada número 2 del reflog (contando desde 0) Dos operaciones atrás
HEAD@{2.hours.ago} Dónde estaba HEAD hace dos horas Según el reloj
HEAD~2 Dos commits atrás en el grafo Recorre padres, no reflog
HEAD^2 El segundo padre (solo en fusiones) Otra rama de la fusión
main@{5} Quinta entrada del reflog de main
main@{yesterday} Dónde estaba main ayer
origin/main@{1} Dónde estaba la rama remota antes del último fetch 09-03
@{-1} La rama anterior en la que estuviste Lo que usa git switch -

La distinción crítica, y merece un ejemplo:

git log --oneline HEAD~3 -1        # tres commits atrás en la historia
git log --oneline HEAD@{3} -1      # donde estaba HEAD hace tres operaciones

HEAD~3 recorre el grafo: sigue la cadena de padres. HEAD@{3} recorre tu historia personal: puede estar en otra rama, en un commit huérfano, en cualquier sitio donde hayas estado.

Cuando has hecho un reset, HEAD~1 te lleva al padre del commit actual (que ya no es lo que buscabas) y HEAD@{1} te lleva a donde estabas antes del reset (que sí lo es).

Las formas temporales aceptan expresiones muy naturales:

git log --oneline main@{yesterday} -3
git log --oneline main@{"1 week ago"} -3
git log --oneline main@{2026-07-28.09:00:00} -3
git diff main@{1.day.ago} main

Y el aviso que Git da cuando te pasas de plazo:

warning: log for 'main' only goes back to Tue, 14 Jul 2026 10:12:04 +0200

No es un error: te está diciendo que ha usado la entrada más antigua disponible. Si la fecha que pediste es anterior, el resultado no es el que crees.

  1. El patrón universal de recuperación

Todas las recetas del apartado siguiente son el mismo procedimiento. Apréndelo una vez.

flowchart TD
    A["1. git reflog (o git reflog show rama)"] --> B["2. Localizar la entrada:<br/>usar la descripción y --date=relative"]
    B --> C["3. VERIFICAR el candidato:<br/>git show hash --stat<br/>git log --oneline hash -5"]
    C --> D{"¿Es lo que buscaba?"}
    D -->|No| B
    D -->|Sí| E["4. git branch rescate hash"]
    E --> F["5. Comprobar en la rama nueva"]
    F --> G["6. Integrar: merge, cherry-pick,<br/>o reset --hard si estás seguro"]

El paso 4 es el que hay que interiorizar, y es la diferencia entre una recuperación tranquila y un segundo susto:

# BIEN: crea un puntero nuevo. No mueve nada de lo que ya tienes
git branch rescate 9c4e7b2

# PEOR: mueve tu rama actual, y si te equivocas de hash pierdes de vista lo de ahora
git reset --hard 9c4e7b2

git branch es puramente aditivo. Si el hash era el equivocado, borras la rama y pruebas otro. Nada se ha movido. Con reset --hard cada intento fallido añade una capa más de confusión.

Y el paso 3, verificar, evita el error de recuperar el commit equivocado:

git show 9c4e7b2 --stat          # ¿qué ficheros tocaba?
git log --oneline 9c4e7b2 -5     # ¿qué historia tiene detrás?
git diff HEAD 9c4e7b2 --stat     # ¿en qué se diferencia de lo que tengo ahora?

Y para inspeccionar sin comprometerse a nada:

git switch --detach 9c4e7b2      # mirar el proyecto en ese estado
# ...abrir ficheros, ejecutar la aplicación...
git switch -                     # volver

  1. Recetario: reset --hard, rama borrada, rebase, merge, --amend

6.1. Deshacer un git reset --hard

La situación. Ana quería quitar un commit y escribió HEAD~3.

No has perdido nada (salvo lo que estuviera sin confirmar, lección 09-02).

git reflog -5
4f8a2e6 HEAD@{0}: reset: moving to HEAD~3
9c4e7b2 HEAD@{1}: commit: GT-241 estilos del indicador
3e7f1a8 HEAD@{2}: commit: GT-241 calcular el contador
8a1f6c3 HEAD@{3}: commit: GT-241 base del filtro
2f8c6e1 HEAD@{4}: checkout: moving from main to GT-241

HEAD@{1} (9c4e7b2) es la punta que había justo antes del reset.

git show 9c4e7b2 --stat          # verificar
git branch rescate-GT-241 9c4e7b2
git log --oneline rescate-GT-241 -4
9c4e7b2 (rescate-GT-241) GT-241 estilos del indicador
3e7f1a8 GT-241 calcular el contador
8a1f6c3 GT-241 base del filtro
2f8c6e1 chore: configuración de eslint

Recuperado. Ahora, si quieres que GT-241 vuelva a ser eso:

git switch GT-241
git reset --hard rescate-GT-241
git branch -d rescate-GT-241

El atajo, si acabas de hacerlo y no has hecho nada más (lección 09-02):

git reset --hard ORIG_HEAD

O directamente con la sintaxis del reflog:

git reset --hard HEAD@{1}

6.2. Recuperar una rama borrada con -D

Esto cierra la promesa de la lección 03-06.

git branch -D GT-247
Deleted branch GT-247 (was b52c9d1).

Primer regalo: Git imprime el hash al borrar. Si lo tienes en el buffer de la terminal, la recuperación es inmediata:

git branch GT-247 b52c9d1

Si has cerrado la terminal, hay dos caminos.

A. El reflog de HEAD (funciona si estuviste en esa rama):

git reflog --date=relative | grep -i "GT-247"
b52c9d1 HEAD@{hace 3 horas}: checkout: moving from GT-247 to main
b52c9d1 HEAD@{hace 3 horas}: commit: GT-247 validar el formulario

B. Buscar por mensaje entre todos los commits huérfanos (funciona siempre):

git log -g --oneline --all | grep -i "GT-247"

O directamente sobre la base de objetos:

git fsck --lost-found --no-progress | grep commit

Y el detalle importante que se olvida: el reflog de la propia rama se borra con ella.

ls .git/logs/refs/heads/
main

.git/logs/refs/heads/GT-247 ya no existe. Por eso hay que buscar en el reflog de HEAD, que sí sobrevive.

# La recuperación
git branch GT-247 b52c9d1
git log --oneline GT-247 -3

-D casi nunca es irreversible. Los commits siguen en .git/objects, alcanzables desde el reflog de HEAD, durante al menos 30 días. La promesa de la lección 03-06 queda cumplida.

Y la excepción, para ser honestos: si la rama se creó, se le hicieron commits y se borró sin que HEAD pasara nunca por ella (por ejemplo, con git branch X <hash> y luego git branch -D X), el reflog de HEAD no la registra. En ese caso, git fsck del apartado 7.

6.3. Rescatar un rebase que salió mal

Esto cierra la promesa de la lección 05-01.

Bruno hizo un rebase -i de diez commits, aplastó los que no debía y terminó con --continue.

git reflog -12
4b8e2c9 HEAD@{0}: rebase (finish): returning to refs/heads/GT-241
4b8e2c9 HEAD@{1}: rebase (squash): GT-241 el filtro completo
8f2d6a1 HEAD@{2}: rebase (squash): GT-241 base del filtro
6e2b9c7 HEAD@{3}: rebase (pick): GT-241 base del filtro
2f8c6e1 HEAD@{4}: rebase (start): checkout origin/main
9c4e7b2 HEAD@{5}: checkout: moving from main to GT-241

La entrada clave es rebase (start): la anterior a ella es el estado exacto previo al rebase. Aquí, HEAD@{5}9c4e7b2.

git branch antes-del-rebase 9c4e7b2
git log --oneline antes-del-rebase -10

Los diez commits originales, intactos.

Un atajo que casi nadie conoce: durante un rebase, Git guarda la posición inicial en ORIG_HEAD, y también en una referencia especial:

git rev-parse ORIG_HEAD
cat .git/rebase-merge/orig-head 2>/dev/null    # solo mientras el rebase está en marcha

Y si el rebase aún no ha terminado, la salida es mucho más simple:

git rebase --abort

Devuelve todo exactamente al estado previo. Úsalo siempre que puedas: es más limpio que recuperar después.

Para comparar el resultado con el original y decidir qué hacer, git range-diff (lección 07-02):

git range-diff antes-del-rebase...GT-241

6.4. Deshacer un merge que no debiste hacer

git merge funcionalidad/experimental
Merge made by the 'ort' strategy.
 14 files changed, 892 insertions(+), 31 deletions(-)

Si acabas de hacerlo y no hay nada encima:

git reset --hard ORIG_HEAD

merge siempre escribe ORIG_HEAD antes de fusionar. Es la salida canónica.

Si ya has hecho más cosas encima:

git reflog -6
c7e2a91 HEAD@{0}: commit: GT-241 ajustar el contador
2b9e6c1 HEAD@{1}: merge funcionalidad/experimental: Merge made by the 'ort' strategy.
7d3a8f4 HEAD@{2}: commit: GT-241 estilos del indicador

HEAD@{2} es el estado previo a la fusión. Pero cuidado: si vuelves ahí, pierdes también el commit c7e2a91 que hiciste después. Lo correcto es rescatarlo aparte:

git branch antes-del-merge 7d3a8f4
git switch antes-del-merge
git cherry-pick c7e2a91          # traer solo el commit bueno (lección 05-03)

Y si el merge ya está publicado, nada de reflog: la respuesta es git revert -m 1 (lección 05-06).

Un caso frecuente y desconcertante: git merge --abort no funciona porque la fusión terminó bien; solo era mala idea. --abort sirve mientras hay conflictos sin resolver, no después.

6.5. Recuperar un commit sobrescrito por --amend

Carla hizo git commit --amend y se dio cuenta de que ha perdido parte del mensaje original —o peor, de que el commit anterior tenía cambios que el nuevo no.

No has perdido nada. --amend no modifica el commit: crea uno nuevo y mueve la rama (lección 09-01). El original sigue en la base de datos.

git reflog -3
3f8a1d6 HEAD@{0}: commit (amend): GT-241 calcular el contador sobre la lista completa
8f4c2a9 HEAD@{1}: commit: GT-241 calcular el contador
b52c9d1 HEAD@{2}: commit: GT-241 base del filtro

HEAD@{1} (8f4c2a9) es el commit original, antes del --amend.

# Ver el mensaje original completo
git show 8f4c2a9 --stat
git log -1 --format=%B 8f4c2a9

# Comparar los dos
git diff 8f4c2a9 3f8a1d6

Y las formas de recuperar, según lo que necesites:

# A. Volver del todo al original
git reset --hard 8f4c2a9

# B. Solo recuperar el mensaje
git commit --amend -m "$(git log -1 --format=%B 8f4c2a9)"

# C. Recuperar un fichero concreto que se perdió en el amend
git restore --source=8f4c2a9 --staged --worktree app.js

# D. Ver qué se perdió exactamente
git diff 3f8a1d6 8f4c2a9

6.6. Recuperar tras un checkout a otra rama con commits en detached HEAD

Retomando el apartado 5.7 de la lección 09-01: hiciste commits en detached HEAD y te fuiste.

git reflog -5
7d3a8f4 HEAD@{0}: checkout: moving from 9c4e7b2 to main
9c4e7b2 HEAD@{1}: commit: prueba del algoritmo alternativo
3e7f1a8 HEAD@{2}: commit: esbozo del filtro por fecha
4f8a2e6 HEAD@{3}: checkout: moving from main to 4f8a2e6

La línea checkout: moving from 9c4e7b2 to main te da el hash de donde saliste.

git branch experimento-fechas 9c4e7b2

La tabla de referencia rápida

Desastre Dónde mirar Comando de rescate
reset --hard de más git reflog -5, entrada anterior al reset: git branch rescate HEAD@{1}
Rama borrada con -D El hash que imprimió -D, o git reflog | grep <rama> git branch <rama> <hash>
Rebase desastroso Entrada anterior a rebase (start) git branch antes 9c4e7b2 / git rebase --abort si sigue en marcha
Merge indeseado ORIG_HEAD, o entrada anterior a merge git reset --hard ORIG_HEAD
--amend que se llevó algo Entrada commit: anterior a commit (amend): git restore --source=<hash> <fichero>
Commits en detached HEAD checkout: moving from <hash> to <rama> git branch <nombre> <hash>
Rama remota reescrita por otro git reflog show origin/<rama> git branch antigua origin/<rama>@{1}
Stash eliminado git fsck --unreachable | grep commit Apartado 8
Nada aparece en el reflog git fsck --lost-found Apartado 7

  1. Cuando el reflog no basta: git fsck --lost-found

El reflog registra los movimientos de referencias. Hay objetos que llegan a la base de datos sin que ninguna referencia se mueva, y para esos hace falta la otra herramienta.

git fsck --lost-found --no-progress
Checking object directories: 100% (256/256), done.
dangling commit 9c4e7b2e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b
dangling commit 3e7f1a8b6d2e9a5f1c7b3d8e4a6f2c9b7d3a8f4a
dangling blob 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b
dangling tree 8a1f6c3d5e2b9c7f4a6d1e8b3c5f7a9d2e4b6c8a

Y además crea un directorio con enlaces:

ls .git/lost-found/commit/
ls .git/lost-found/other/

Qué es cada cosa

Tipo Qué es Por qué aparece
dangling commit Un commit que ninguna referencia alcanza reset, rama borrada, rebase, --amend, stash eliminado
dangling blob Contenido de un fichero sin árbol que lo referencie Un git add cuyo commit nunca llegó a hacerse
dangling tree Un directorio sin commit que lo use Un commit a medias, o el árbol de un stash
unreachable X Igual, pero contando el reflog Con --unreachable se ven también los del reflog

dangling es normal e inofensivo. No es un síntoma de corrupción. Es lo que se espera en cualquier repositorio activo. La distinción entre eso y los errores de verdad (missing, broken link) es la lección 09-05.

Cómo inspeccionar los candidatos

Un fsck puede devolver decenas de líneas. El trabajo es identificar cuál es el tuyo.

# Los commits colgantes, con fecha, autor y mensaje, ordenados por fecha
git fsck --lost-found --no-progress 2>/dev/null \
  | awk '/dangling commit/ {print $3}' \
  | xargs -I{} git log -1 --format='%ci  %h  %an  %s' {} \
  | sort -r
2026-07-30 18:22:41 +0200  9c4e7b2  Ana Ferrer  GT-241 estilos del indicador
2026-07-30 17:51:03 +0200  3e7f1a8  Ana Ferrer  GT-241 calcular el contador
2026-07-28 11:04:17 +0200  8a1f6c3  Bruno Salas  WIP en GT-238

Eso convierte una lista de hashes en información utilizable. Guárdalo como alias:

git config --global alias.perdidos '!git fsck --lost-found --no-progress 2>/dev/null | awk "/dangling commit/ {print \$3}" | xargs -I{} git log -1 --format="%ci  %h  %an  %s" {} | sort -r'

Inspección individual, con las herramientas de fontanería de la lección 01-04:

# El contenido del objeto commit tal cual
git cat-file -p 9c4e7b2
tree 8a1f6c3d5e2b9c7f4a6d1e8b3c5f7a9d2e4b6c8a
parent 2f8c6e1a4b7d9c3e5f2a8b6d1c9e4f7a3b5d2c8e
author Ana Ferrer <[email protected]> 1785312041 +0200
committer Ana Ferrer <[email protected]> 1785312041 +0200

GT-241 estilos del indicador de pendientes
# El diff completo
git show 9c4e7b2

# Solo los ficheros
git show --stat 9c4e7b2

# Qué contenía un blob colgante
git cat-file -p 6f2b9d4 | head -20

# De qué tipo es un objeto cualquiera
git cat-file -t 8a1f6c3

Los blobs colgantes: el add que nunca se confirmó

Este es el caso que salva más trabajo del que la gente espera. Si preparaste un fichero con git add y luego lo perdiste, el contenido está en la base de datos (lección 09-01, tabla del apartado 1).

# Buscar entre los blobs colgantes el que contenga una cadena que recuerdes
for b in $(git fsck --lost-found --no-progress 2>/dev/null | awk '/dangling blob/ {print $3}'); do
  if git cat-file -p "$b" 2>/dev/null | grep -q "calcularPendientes"; then
    echo "=== $b ==="
    git cat-file -p "$b" | head -5
  fi
done
=== 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b ===
function calcularPendientes(tareas) {
  return tareas.filter(t => !t.completada).length;
}
# Recuperarlo
git cat-file -p 6f2b9d4 > app.js

Buscar por una cadena que recuerdes es mucho más eficaz que revisar los blobs uno a uno.

  1. Recuperar un stash eliminado

Esto cierra la promesa de la lección 05-04.

git stash drop
Dropped refs/stash@{0} (b52c9d1e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b)

Primer regalo, otra vez: drop imprime el hash. Con él:

git stash apply b52c9d1

Y si fue git stash clear, que borra toda la pila sin imprimir nada, hay dos caminos.

A. El reflog del stash, si aún existe:

git reflog show stash
cat .git/logs/refs/stash

stash clear borra la referencia, pero el fichero de log a veces sobrevive hasta el siguiente gc. Merece la pena mirarlo primero.

B. Buscar los commits del stash entre los inalcanzables:

git fsck --unreachable --no-progress | grep commit | awk '{print $3}' \
  | xargs -I{} git log -1 --format='%h %ci %s' {} \
  | grep -i "WIP on\|On .*:"
b52c9d1 2026-07-30 16:12:44 +0200 WIP on GT-241: 7d3a8f4 GT-241 base del filtro
8f4c2a9 2026-07-29 09:31:20 +0200 On main: pruebas del contador

Los stashes se reconocen por su mensaje: WIP on <rama>: (los automáticos) o On <rama>: <mensaje> (los que creaste con -m).

Un stash es internamente un commit de fusión con dos o tres padres, y merece verlo porque explica cómo funciona:

git show --format='%h  padres: %p  %s' -s b52c9d1
b52c9d1  padres: 7d3a8f4 4f8a2e6 9c4e7b2  WIP on GT-241: 7d3a8f4 GT-241 base del filtro
Padre Qué contiene
1º (7d3a8f4) El HEAD en el momento del stash
2º (4f8a2e6) El índice (lo que estaba preparado)
3º (9c4e7b2) Los ficheros sin seguimiento, si usaste -u

Y la recuperación:

# Aplicarlo directamente
git stash apply b52c9d1

# O devolverlo a la pila para tratarlo con normalidad
git stash store -m "recuperado: GT-241 a medias" b52c9d1
git stash list
stash@{0}: recuperado: GT-241 a medias
# Ver qué contenía sin aplicarlo
git show b52c9d1                  # los cambios del directorio de trabajo
git show b52c9d1^2                # lo que estaba preparado
git show b52c9d1^3                # los ficheros sin seguimiento (si los hay)

  1. Los límites reales: lo que no se puede recuperar

Toca ser honestos. Hay dos fronteras que ninguna técnica de esta lección cruza.

Frontera 1: lo que nunca entró en la base de datos

Ya lo vimos en la lección 09-01, y es la limitación fundamental:

Situación ¿Recuperable?
Un commit, aunque la rama se borrara
Un git add sin confirmar : hay blob
Un stash, incluso eliminado : son commits
Un fichero modificado y destruido con git restore No
Un fichero destruido por reset --hard sin add previo No
Un fichero sin seguimiento borrado por git clean No

Cuando estás en uno de los tres últimos casos, Git ya no puede ayudarte. Lo que queda es fuera de Git:

1. El historial local de tu editor. Es la vía que más veces funciona y la que menos se prueba.

  • VS Code: paleta de comandos → "Local History: Find Entry to Restore". Guarda versiones de cada fichero que has editado, independientemente de Git.
  • IntelliJ / WebStorm / Eclipse: menú contextual del fichero → Local History → Show History. Guarda semanas.
  • Vim: si tienes set undofile, el historial de deshacer persiste entre sesiones (:earlier 1h).

2. Copias de seguridad del sistema: Time Machine en macOS, instantáneas de Btrfs/ZFS, versiones anteriores en Windows, copias en la nube.

3. Buscar en /tmp y en los ficheros de intercambio del editor (.swp de Vim, ~ de otros).

Frontera 2: lo que ya pasó por gc

Si se ejecutó git gc --prune=now después de que el commit quedara huérfano, el objeto ha desaparecido físicamente. Si además se ejecutó git reflog expire --expire=now --all, no hay ni registro.

Comprueba si aún está antes de rendirte:

git cat-file -t 9c4e7b2
fatal: Not a valid object name 9c4e7b2

Ese mensaje significa que el objeto ya no existe. Pero antes de darlo por perdido, mira fuera de tu máquina:

# ¿Lo tiene el servidor?
git fetch origin '+refs/*:refs/remotes/copia/*'
git log --all --oneline | grep -i "GT-241"

# ¿Lo tiene un compañero? (su clon es una copia casi completa)
git remote add bruno /ruta/o/url/al/clon/de/bruno
git fetch bruno
git log --oneline bruno/GT-241

Esa es la red de seguridad del modelo distribuido, y es el tema central de la lección 09-05.

Y la prevención, que es lo que de verdad resuelve esto

# 1. Confirma pronto y a menudo. Un "wip" cada media hora lo cambia todo.
git commit -am "wip"

# 2. Antes de cualquier operación arriesgada, un marcador. Cuesta dos segundos.
git branch respaldo-$(date +%H%M)

# 3. Antes de limpiar, un stash con ficheros sin seguimiento incluidos
git stash push -u -m "por si acaso"

# 4. Alarga el reflog en tus repositorios de trabajo
git config --global gc.reflogExpire "1 year"
git config --global gc.reflogExpireUnreachable "1 year"

# 5. Publica tus ramas, aunque estén a medias
git push -u origin GT-241

La quinta es la más infravalorada. Una rama publicada está en dos máquinas. Ningún accidente local puede con eso.

  1. Higiene: no destruir tu propia red de seguridad

Cierra la lección lo que no hay que hacer.

1. git reflog expire --expire=now --all. Vacía el registro. Todo lo huérfano queda desprotegido.

2. git gc --prune=now. Elimina inmediatamente los objetos inalcanzables. Tiene su uso legítimo (tras un filter-repo de la lección 08-05, donde borrar es justo el objetivo), y ninguno en un día normal.

3. Confiar en el reflog como copia de seguridad. Es local, temporal y muere con el disco. No es una copia de seguridad.

4. Recuperar con reset --hard en lugar de git branch. Cada intento fallido añade confusión. branch no mueve nada.

5. Trabajar meses sin publicar una rama. Un clon nuevo tiene el reflog vacío; una rama sin publicar existe en un solo disco.

Y una comprobación de salud de treinta segundos, para saber si tu red de seguridad está donde crees:

echo "Entradas en el reflog de HEAD: $(git reflog | wc -l)"
echo "Entrada más antigua: $(git reflog --date=iso | tail -1)"
echo "reflogExpire: $(git config --get gc.reflogExpire || echo '90 días (por defecto)')"
echo "reflogExpireUnreachable: $(git config --get gc.reflogExpireUnreachable || echo '30 días (por defecto)')"
echo "Objetos colgantes: $(git fsck --lost-found --no-progress 2>/dev/null | grep -c dangling)"
echo "Ramas sin publicar: $(git for-each-ref --format='%(refname:short) %(upstream)' refs/heads | awk '$2==""{print $1}' | tr '\n' ' ')"

Errores Comunes y Consejos

Error 1: no probar git reflog. Es el primer comando ante cualquier "he perdido commits", y la mayoría de la gente no lo ejecuta nunca.

Error 2: confundir HEAD@{1} con HEAD~1. El primero es dónde estabas hace una operación; el segundo es el commit padre. Tras un reset, son cosas completamente distintas.

Error 3: coger el hash de la entrada equivocada. El hash de una línea es dónde quedó HEAD después de esa operación. Para el estado previo a HEAD@{0}, necesitas HEAD@{1}.

Error 4: recuperar con reset --hard en lugar de git branch. Si el hash era el equivocado, con branch borras la rama y pruebas otra; con reset cada intento complica más el estado.

Error 5: creer que el reflog viaja en un clone. No viaja. El clon nuevo tiene una sola entrada y ninguna red de seguridad.

Error 6: confiar en el reflog para siempre. 90 días para lo alcanzable, 30 para lo huérfano, y gc los aplica. En la práctica, semanas.

Error 7: asustarse con los dangling de git fsck. Son normales en cualquier repositorio activo. Lo grave es missing y broken link (lección 09-05).

Error 8: dar por perdido un stash. stash drop imprime el hash, stash clear deja los commits huérfanos, y ambos se recuperan con git fsck --unreachable buscando WIP on.

Error 9: rendirse sin mirar fuera de Git. El historial local del editor recupera lo que Git no puede, y casi nadie lo prueba.

Consejo 1: el patrón es siempre el mismo. Reflog → verificar con git showgit branch rescate <hash>.

Consejo 2: git reflog --date=relative. "Lo tenía antes de comer" se traduce directamente a una entrada.

Consejo 3: define el alias git perdidos. El día que lo necesites no vas a recordar la tubería de fsck y awk.

Consejo 4: git rebase --abort mientras puedas. Mucho más limpio que recuperar después.

Consejo 5: alarga el reflog a un año. Dos líneas de configuración global y unos megabytes.

Consejo 6: publica tus ramas. Una rama en dos máquinas es inmune a los accidentes de una.

Ejercicios

Ejercicio 1: el recetario completo

Sobre un repositorio de pruebas con app.js, estilos.css e index.html:

  1. Crea seis commits en main y una rama GT-241 con tres commits propios.
  2. Provoca cuatro desastres seguidos, sin recuperar entre uno y otro: a. git reset --hard HEAD~2 en GT-241. b. git switch main && git branch -D GT-241. c. Un git commit --amend en main que borre parte del mensaje. d. Un git merge de una rama experimental que no querías.
  3. Ejecuta git reflog --date=relative y anota, para cada desastre, qué entrada usarías.
  4. Recupera las cuatro cosas usando siempre git branch, nunca reset.
  5. Verifica cada recuperación con git show --stat y git diff.

Ejercicio 2: los límites de la recuperación

  1. En un repositorio nuevo, crea tres situaciones: un commit, un fichero preparado con git add, y un fichero modificado solo en disco.
  2. Ejecuta git reset --hard HEAD~1.
  3. Recupera el commit con el reflog y el fichero preparado con git fsck --lost-found + git cat-file -p.
  4. Documenta por qué el tercero es irrecuperable, comprobándolo con git fsck y git reflog.
  5. Crea un stash con -u, elimínalo con git stash clear y recupéralo con git fsck --unreachable.
  6. Comprueba con git show --format='%p' -s que el stash tiene tres padres e inspecciona cada uno.

Ejercicio 3: la caducidad y la higiene

  1. Crea un repositorio con diez commits y provoca cinco commits huérfanos con reset.
  2. Comprueba que aparecen en git fsck --lost-found.
  3. Ejecuta git reflog expire --expire=now --all y vuelve a mirar git reflog y git fsck. Explica la diferencia entre las dos salidas.
  4. Ejecuta git gc --prune=now y comprueba con git cat-file -t <hash> que los objetos han desaparecido de verdad.
  5. Repite el experimento configurando antes gc.reflogExpireUnreachable "1 year" y explica qué cambia.
  6. Escribe la comprobación de salud del apartado 10 en un guion e interpreta su salida en un repositorio real tuyo.

Soluciones

Solución 1:

rm -rf /tmp/p9-04 && mkdir /tmp/p9-04 && cd /tmp/p9-04 && git init -q -b main
git config user.name "Ana Ferrer"; git config user.email "[email protected]"
for i in 1 2 3 4 5 6; do echo "linea $i" >> app.js; git add .; git commit -q -m "chore: commit $i"; done

git switch -q -c GT-241
echo "// filtro" > filtro.js && git add . && git commit -q -m "GT-241 base del filtro"
echo "// contador" >> filtro.js && git commit -q -am "GT-241 calcular el contador"
echo ".pendiente{}" > estilos.css && git add . && git commit -q -m "GT-241 estilos del indicador"
git rev-parse --short HEAD
9c4e7b2
# 2a. reset --hard
git reset -q --hard HEAD~2

# 2b. borrar la rama
git switch -q main
git branch -D GT-241
Deleted branch GT-241 (was 8a1f6c3).
# 2c. amend destructivo
git commit -q --amend -m "chore"

# 2d. merge indeseado
git switch -q -c experimental HEAD~1
echo "experimento" > experimento.txt && git add . && git commit -q -m "experimento que no vale"
git switch -q main
git merge -q --no-ff experimental -m "Merge experimental"
git log --oneline -3
c7e2a91 (HEAD -> main) Merge experimental
2b9e6c1 (experimental) experimento que no vale
3f8a1d6 chore
# 3. El reflog completo
git reflog --date=relative -12
c7e2a91 HEAD@{hace 4 segundos}: merge experimental: Merge made by the 'ort' strategy.
3f8a1d6 HEAD@{hace 12 segundos}: checkout: moving from experimental to main
2b9e6c1 HEAD@{hace 18 segundos}: commit: experimento que no vale
7d3a8f4 HEAD@{hace 25 segundos}: checkout: moving from main to experimental
3f8a1d6 HEAD@{hace 33 segundos}: commit (amend): chore
8f4c2a9 HEAD@{hace 40 segundos}: checkout: moving from GT-241 to main
6e2b9c7 HEAD@{hace 48 segundos}: reset: moving to HEAD~2
9c4e7b2 HEAD@{hace 55 segundos}: commit: GT-241 estilos del indicador
Desastre Entrada a usar Hash
a. reset --hard La anterior al reset: 9c4e7b2
b. branch -D El hash impreso, o el checkout: moving from GT-241 8a1f6c3 (lo que imprimió -D)
c. --amend La commit: anterior a commit (amend): 8f4c2a9
d. merge La anterior al merge, o ORIG_HEAD 3f8a1d6

Fíjate en el matiz de (b): tras el reset --hard, GT-241 apuntaba a 6e2b9c7, y eso es lo que borró -D (8a1f6c3 en el ejemplo del mensaje). Pero lo que Ana quiere es el estado previo al reset, 9c4e7b2. El reflog las conserva las dos.

# 4. Recuperar, siempre con branch
git branch rescate-GT-241-completa 9c4e7b2
git branch rescate-amend 8f4c2a9
git branch rescate-antes-merge 3f8a1d6

git branch
  experimental
* main
  rescate-GT-241-completa
  rescate-amend
  rescate-antes-merge

Ninguna de esas operaciones ha movido main. Todo lo que había sigue donde estaba, y ahora además hay tres puntos de acceso a lo recuperado.

# 5. Verificar
git log --oneline rescate-GT-241-completa -4
9c4e7b2 (rescate-GT-241-completa) GT-241 estilos del indicador
8a1f6c3 GT-241 calcular el contador
6e2b9c7 GT-241 base del filtro
7d3a8f4 chore: commit 6

Los tres commits, incluidos los dos que el reset --hard había dejado atrás.

git log -1 --format=%B rescate-amend
chore: commit 6

El mensaje original, antes de que el --amend lo dejara en chore.

git show --stat rescate-antes-merge -1
git diff rescate-antes-merge main --stat
 experimento.txt | 1 +
 1 file changed, 1 insertion(+)

Lo único que aportó el merge indeseado. Para deshacerlo:

git reset --hard rescate-antes-merge
git log --oneline -1
3f8a1d6 (HEAD -> main, rescate-antes-merge) chore

Y para restaurar GT-241 de verdad:

git branch -m rescate-GT-241-completa GT-241
git branch -d rescate-amend rescate-antes-merge

Solución 2:

rm -rf /tmp/p9-04b && mkdir /tmp/p9-04b && cd /tmp/p9-04b && git init -q -b main
echo "base" > app.js && git add . && git commit -q -m "c1"

# 1. Las tres situaciones
echo "COMMIT" >> app.js && git commit -q -am "c2: confirmado"
echo "PREPARADO" > estilos.css && git add estilos.css
echo "SOLO EN DISCO" >> app.js

# 2. El desastre
git reset -q --hard HEAD~1
# 3. Recuperar el commit
git reflog -3
3f8a1d6 HEAD@{0}: reset: moving to HEAD~1
8f4c2a9 HEAD@{1}: commit: c2: confirmado
3f8a1d6 HEAD@{2}: commit (initial): c1
git branch rescate-commit 8f4c2a9
git show --stat rescate-commit
 app.js | 1 +
# El fichero preparado
git fsck --lost-found --no-progress
dangling blob 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b
git cat-file -p 6f2b9d4
git cat-file -p 6f2b9d4 > estilos.css
PREPARADO
# 4. El tercero
git fsck --lost-found --no-progress | wc -l
git reflog | grep -c "SOLO EN DISCO"
grep -rl "SOLO EN DISCO" .git/ 2>/dev/null | wc -l
1
0
0

Un solo objeto colgante (el del add), ninguna entrada de reflog, y ni un byte en todo .git. La línea SOLO EN DISCO nunca se convirtió en objeto: no se calculó su SHA, no se comprimió, no se escribió. Git no puede devolver lo que jamás recibió. Las únicas vías serían el historial local del editor o una copia del sistema de ficheros.

# 5. El stash
echo "trabajo a medias" >> app.js
echo "fichero nuevo" > borrador.txt
git stash push -u -q -m "GT-241 a medias"
git stash list
git stash clear
git stash list
stash@{0}: On main: GT-241 a medias
(sin salida)
git fsck --unreachable --no-progress | grep commit | awk '{print $3}' \
  | xargs -I{} git log -1 --format='%h %ci %s' {} | grep -i "on main"
b52c9d1 2026-07-31 12:14:02 +0200 On main: GT-241 a medias
git stash store -m "recuperado" b52c9d1
git stash list
git stash pop
git status --short
stash@{0}: recuperado
 M app.js
?? borrador.txt

Recuperado íntegro, incluido el fichero sin seguimiento.

# 6. La estructura interna del stash
git show --format='%h  padres: %p' -s b52c9d1
b52c9d1  padres: 3f8a1d6 8a1f6c3 9c4e7b2
git cat-file -t b52c9d1
git show --stat b52c9d1^2      # el índice
git show --stat b52c9d1^3      # los sin seguimiento
commit
(vacío: no había nada preparado)
 borrador.txt | 1 +

Tres padres exactamente como decía el apartado 8: HEAD, el índice y los ficheros sin seguimiento. Un stash es un commit, y por eso sobrevive a stash clear.

Solución 3:

rm -rf /tmp/p9-04c && mkdir /tmp/p9-04c && cd /tmp/p9-04c && git init -q -b main
for i in $(seq 1 10); do echo "linea $i" >> app.js; git add .; git commit -q -m "commit $i"; done
git rev-parse --short HEAD
git reset -q --hard HEAD~5
9c4e7b2
# 2. Los huérfanos
git fsck --lost-found --no-progress | grep -c "dangling commit"
git reflog | head -3
5
3f8a1d6 HEAD@{0}: reset: moving to HEAD~5
9c4e7b2 HEAD@{1}: commit: commit 10
8f4c2a9 HEAD@{2}: commit: commit 9
# 3. Vaciar el reflog
git reflog expire --expire=now --all
git reflog | wc -l
git fsck --lost-found --no-progress | grep -c "dangling commit"
0
5

La diferencia clave: el reflog está vacío, pero los objetos siguen ahí. reflog expire borra el registro, no los objetos. Todavía se pueden recuperar con fsck... pero ya no sabes cuál era la punta ni en qué orden estaban, porque esa información vivía en el reflog. Recuperable, sí; cómodo, no.

git cat-file -t 9c4e7b2
git log --oneline 9c4e7b2 -3        # el commit y su historia siguen accesibles
commit
# 4. Ahora sí, la purga
git gc --prune=now -q
git fsck --lost-found --no-progress | grep -c "dangling commit"
git cat-file -t 9c4e7b2
0
fatal: Not a valid object name 9c4e7b2

Ahora sí ha desaparecido de verdad. El objeto ya no existe en .git/objects. Ninguna técnica de esta lección lo recupera; solo un clon ajeno o una copia de seguridad.

# 5. Con reflog largo
rm -rf /tmp/p9-04d && mkdir /tmp/p9-04d && cd /tmp/p9-04d && git init -q -b main
git config gc.reflogExpire "1 year"
git config gc.reflogExpireUnreachable "1 year"
for i in $(seq 1 10); do echo "l $i" >> app.js; git add .; git commit -q -m "c$i"; done
git rev-parse --short HEAD > /tmp/punta.txt
git reset -q --hard HEAD~5
git gc --prune=now -q
git reflog | wc -l
git cat-file -t "$(cat /tmp/punta.txt)"
6
commit

El gc --prune=now no ha podido purgar nada, porque las entradas del reflog siguen vigentes y el reflog cuenta como referencia. Ese es el mecanismo exacto que mantiene vivos los objetos huérfanos, y por eso alargar la caducidad protege de verdad.

# 6. Comprobación de salud
cat > /tmp/salud-git.sh <<'EOF'
#!/usr/bin/env bash
echo "Entradas en el reflog de HEAD: $(git reflog | wc -l)"
echo "Entrada más antigua:           $(git reflog --date=iso | tail -1)"
echo "reflogExpire:                  $(git config --get gc.reflogExpire || echo '90 días (defecto)')"
echo "reflogExpireUnreachable:       $(git config --get gc.reflogExpireUnreachable || echo '30 días (defecto)')"
echo "Objetos colgantes:             $(git fsck --lost-found --no-progress 2>/dev/null | grep -c dangling)"
echo "Ramas sin publicar:            $(git for-each-ref --format='%(refname:short) %(upstream)' refs/heads | awk '$2==""{print $1}' | tr '\n' ' ')"
EOF
chmod +x /tmp/salud-git.sh && /tmp/salud-git.sh
Entradas en el reflog de HEAD: 11
Entrada más antigua:           3f8a1d6 HEAD@{2026-07-31 11:02:14 +0200}: commit (initial): c1
reflogExpire:                  1 year
reflogExpireUnreachable:       1 year
Objetos colgantes:             5
Ramas sin publicar:            main

La última línea es la que más información da en un repositorio real: cada rama sin upstream existe en un solo disco.

Conclusión

El reflog es la razón por la que casi nada se pierde en Git, y saberlo cambia por completo la relación con los comandos destructivos.

  • El reflog registra cada movimiento de cada referencia, en texto plano bajo .git/logs/. Y no es solo un registro: sus entradas cuentan como referencias, y por eso mantienen vivos los commits que ninguna rama alcanza.
  • Es local: no viaja en un clone, no se publica, y muere con el disco. No es una copia de seguridad. Y es temporal: 90 días para lo alcanzable, 30 para lo huérfano, que gc aplica.
  • HEAD@{n} no es HEAD~n. El primero recorre tu historia personal de movimientos; el segundo recorre el grafo. Tras un reset, son cosas distintas, y confundirlos es el error más común.
  • El patrón de recuperación es siempre el mismo: git reflog → verificar con git show --statgit branch rescate <hash>. Crear una rama es aditivo y no mueve nada; reset --hard complica cada intento fallido.
  • El recetario cubre todo lo que este curso había ido prometiendo: deshacer un reset --hard (entrada anterior al reset:), recuperar una rama borrada con -D (el hash que imprime, o el reflog de HEAD, porque el de la rama se borra con ella), rescatar un rebase (entrada anterior a rebase (start), o --abort si sigue en marcha), deshacer un merge (ORIG_HEAD) y recuperar lo que se llevó un --amend (la entrada commit: anterior).
  • Cuando el reflog no basta, git fsck --lost-found: los objetos dangling son normales, y entre ellos están los commits huérfanos y los blobs de un git add que nunca se confirmó. git cat-file -p y git show los inspeccionan.
  • Un stash es un commit con dos o tres padres (HEAD, índice, sin seguimiento), y por eso sobrevive a drop y a clear: se busca por WIP on entre los inalcanzables y se devuelve con git stash store.
  • Los límites son reales: lo que nunca fue objeto (modificaciones sin add, ficheros borrados por clean) y lo que ya pasó por gc --prune. Ahí solo salvan el historial local del editor, una copia del sistema o el clon de un compañero.
  • Y la prevención que hace innecesaria toda la lección: confirmar a menudo, poner un git branch antes de lo arriesgado, stash push -u antes de limpiar, alargar el reflog a un año y publicar las ramas.

Hasta aquí, todo se apoyaba en una premisa: que la base de datos de objetos estuviera sana. Los objetos existían, los hashes cuadraban y Git podía leerlos.

¿Y cuándo no? Cuando git status responde error: object file .git/objects/4f/8a2e6... is empty, cuando una referencia apunta a un objeto que no existe, cuando el índice está corrupto y Git se niega a hacer absolutamente nada. Eso ya no es un problema de referencias mal puestas: es un problema de integridad, y tiene su propio diagnóstico, sus propias reparaciones y —lo más importante— un criterio claro para saber cuándo hay que dejar de intentar arreglarlo y volver a clonar.

Continúa en la lección 09-05: Tratando con Repositorios Corruptos.

Dominando Git: De Principiante a Avanzado

Módulo 1: Introducción a Git

Módulo 2: Operaciones Básicas de Git

Módulo 3: Ramas y Fusión

Módulo 4: Trabajando con Repositorios Remotos

Módulo 5: Operaciones Avanzadas de Git

Módulo 6: Herramientas y Técnicas de Git

Módulo 7: Estrategias de Colaboración y Flujo de Trabajo

Módulo 8: Mejores Prácticas y Consejos de Git

Módulo 9: Solución de Problemas y Depuración

Módulo 10: Git en el Mundo Real

© Copyright 2026. Todos los derechos reservados