Módulo 6: Recuperación segura

Unidad: GIT-08
Público: alumnado de DAW, DAM, ASIR y grados de informática.
Itinerario: tronco, bloque de corrección y entrega; tras ramas y colaboración.
Prerrequisitos: módulos 3 a 5; manejar status, add, commit, log, diff, ramas y remotos.
Objetivo observable: elegir de forma justificada entre restore, stash, revert, reset y reflog según el estado del cambio y el riesgo, sin recurrir a operaciones destructivas sobre trabajo publicado.
Fuente técnica: documentación oficial de git restore, git stash, git revert, git reset y git reflog.
Duración orientativa: 3 horas.

Idea clave

Casi todo lo que llega a un commit se puede recuperar, aunque parezca perdido. Lo que Git no puede devolverte es lo que nunca llegó a confirmarse: un cambio en el directorio de trabajo que descartas no deja rastro en ninguna parte.

Por eso este módulo no va de memorizar cinco comandos, sino de responder dos preguntas antes de tocar nada:

  1. ¿El cambio está confirmado en un commit?
  2. ¿Ese commit está publicado en un remoto?

Las respuestas determinan qué herramienta es segura. Equivocarse de herramienta es lo que convierte un error pequeño en un problema de equipo.

El árbol de decisión

Esta tabla es la columna vertebral del módulo. Consúltala antes de ejecutar nada:

Situación Herramienta ¿Destructivo?
Cambio en el archivo, sin git add git restore archivo : se pierde el cambio
Cambio ya preparado con git add git restore --staged archivo No: solo lo saca del área de preparación
Trabajo a medias que estorba ahora git stash push -m "motivo" No: se guarda aparte
Commit publicado que hay que corregir git revert <commit> No: añade un commit inverso
Commits privados que hay que rehacer git reset (con rama de seguridad) --hard
Referencia perdida (rama, commit, reset) git reflog + recrear No: solo consulta

Regla que resume la tabla: si está publicado, se corrige añadiendo; si es privado, se puede rehacer. Nunca al revés.

Parte 1 — Cambios sin confirmar

Partimos de un repositorio de práctica:

mkdir notas && cd notas && git init
printf "# Notas del curso\n\nApuntes de clase.\n" > README.md
git add README.md && git commit -m "docs: crea README"
printf "Tema 1: variables\n" > tema1.txt
git add tema1.txt && git commit -m "docs: añade tema 1"

Descartar un cambio no preparado

Estropea el archivo a propósito y mira antes de actuar:

printf "esto no debería estar aquí\n" >> tema1.txt
git status --short
git diff
 M tema1.txt
diff --git a/tema1.txt b/tema1.txt
index a824b9a..5e2d8cc 100644
--- a/tema1.txt
+++ b/tema1.txt
@@ -1 +1,2 @@
 Tema 1: variables
+esto no debería estar aquí

Ese git diff no es opcional: es lo último que verás de ese contenido. Ahora descártalo:

git restore tema1.txt
git status --short
cat tema1.txt
Tema 1: variables

git status --short no imprime nada y el archivo ha vuelto a su estado del último commit. Ese cambio ya no existe en ningún sitio. git restore sobre el directorio de trabajo es de los pocos comandos de este módulo que destruyen sin red de seguridad, porque el contenido nunca estuvo en un commit.

Deshacer un git add

Distinto caso: el cambio está preparado y solo quieres sacarlo del área de preparación, no perderlo.

printf "borrador sin terminar\n" >> tema1.txt
git add tema1.txt
git status --short
M  tema1.txt
git restore --staged tema1.txt
git status --short
cat tema1.txt
 M tema1.txt
Tema 1: variables
borrador sin terminar

El cambio sigue en el archivo: solo ha dejado de estar preparado. Es una operación reversible y sin riesgo.

Leer git status --short: las dos columnas

La diferencia entre los dos casos anteriores está en una columna:

M  tema1.txt    ← primera columna: área de preparación
 M tema1.txt    ← segunda columna: directorio de trabajo
Salida Significa
M Modificado y preparado
M Modificado y sin preparar
MM Preparado, y modificado otra vez después
A Archivo nuevo, preparado
?? Sin seguimiento (Git no lo controla)

Acostúmbrate a mirar la posición del carácter, no solo la letra. La mitad de los errores de este módulo salen de confundir esas dos columnas.

Recuperar un archivo de un commit anterior

restore también trae contenido del historial sin tocar el resto del proyecto:

git restore --source=HEAD~1 datos.txt
git status --short
 M datos.txt

El archivo vuelve a como estaba un commit atrás, y el cambio queda sin preparar para que puedas revisarlo. HEAD~1 significa «un commit antes del actual»; también sirve un hash concreto.

Parte 2 — Aparcar trabajo con stash

Situación típica: estás a medias de una tarea y necesitas el árbol limpio ya, para atender otra cosa. No quieres confirmar trabajo a medias ni quieres perderlo.

printf "Tema 2: bucles (a medias)\n" > tema2.txt
git add tema2.txt
printf "\nNota pendiente de revisar\n" >> README.md
git status --short
 M README.md
A  tema2.txt

Guárdalo con un mensaje que te sirva mañana:

git stash push -m "tema 2 a medias"
git status --short
ls
Saved working directory and index state On main: tema 2 a medias
README.md
tema1.txt

El árbol está limpio y tema2.txt ha desaparecido del directorio. No se ha perdido: está guardado aparte.

git stash list
git stash show --stat stash@{0}
stash@{0}: On main: tema 2 a medias
 README.md | 2 ++
 tema2.txt | 1 +
 2 files changed, 3 insertions(+)

Recupéralo cuando vuelvas:

git stash pop
git status --short
 M README.md
A  tema2.txt

Fíjate en que tema2.txt vuelve preparado y README.md sin preparar: stash conserva en qué estado estaba cada cosa.

pop recupera y borra la entrada; git stash apply recupera y la conserva, por si quieres aplicarla en dos sitios. Con varias entradas acumuladas, git stash list y un mensaje descriptivo son la diferencia entre recuperar tu trabajo y adivinar.

Parte 3 — Corregir lo ya confirmado

Historial publicado: git revert

Este es el caso importante. Has publicado un commit que no debía salir:

git push -u origin main
printf "clave_api = 12345-SECRETO\n" > config.txt
git add config.txt && git commit -m "feat: añade configuración"
git push origin main

El commit ya está en el remoto, así que otras personas pueden tenerlo. La corrección segura no toca lo publicado: añade encima.

git revert HEAD --no-edit
[main fdaaed2] Revert "feat: añade configuración"
 1 file changed, 1 deletion(-)
 delete mode 100644 config.txt
git log --oneline -3
ls config.txt
fdaaed2 Revert "feat: añade configuración"
56fbca5 feat: añade configuración
c8ad862 docs: añade tema 2 y nota
ls: config.txt: No such file or directory

Los dos commits siguen ahí: el error y su corrección. Eso es exactamente lo que se busca — el historial cuenta la verdad y nadie que tuviera el commit anterior se encuentra con que ha desaparecido. Publicar la corrección es un push normal, sin fuerza:

git push origin main

Lo que revert no hace

Aquí hay una trampa que hay que ver una vez para no olvidarla. El archivo ya no está en el proyecto, pero:

git log -S "12345-SECRETO" --oneline
git show 56fbca5:config.txt
fdaaed2 Revert "feat: añade configuración"
56fbca5 feat: añade configuración
clave_api = 12345-SECRETO

El secreto sigue siendo accesible en el historial. revert corrige el estado del proyecto, no borra el pasado. Y como el commit ya se publicó, tampoco serviría reescribir tu copia local.

Por eso, ante una credencial publicada, lo primero no es un comando de Git: es invalidar esa credencial y generar una nueva. Cualquier limpieza del historial viene después y no sustituye a ese paso. Se trata en el módulo 11.

Historial privado: git reset

Cuando los commits no se han publicado, sí puedes rehacerlos. git reset mueve la rama a otro commit, y sus tres modos se diferencian en qué hace con tu trabajo.

Prepara tres commits de prueba y, antes de tocar nada, una rama de seguridad:

git switch -c experimento
printf "prueba 1\n" > p1.txt && git add p1.txt && git commit -m "wip: prueba 1"
printf "prueba 2\n" > p2.txt && git add p2.txt && git commit -m "wip: prueba 2"
printf "prueba 3\n" > p3.txt && git add p3.txt && git commit -m "wip: prueba 3"
git branch respaldo-experimento

Esa rama es un nombre que apunta al commit actual. Cuesta un segundo y convierte cualquier reset en reversible.

--soft: deshace los commits, conserva el trabajo preparado.

git reset --soft HEAD~2
git log --oneline -1
git status --short
7ead905 wip: prueba 1
A  p2.txt
A  p3.txt

--mixed (el modo por defecto): deshace los commits y también la preparación.

git reset --mixed HEAD~2
git status --short
?? p2.txt
?? p3.txt

Aparecen como ?? porque eran archivos nuevos y Git ha dejado de seguirlos; si hubieran sido modificaciones de archivos existentes, saldrían como M.

--hard: deshace los commits y borra el trabajo.

git reset --hard HEAD~2
git status --short
ls
HEAD is now at 7ead905 wip: prueba 1
README.md
p1.txt
tema1.txt
tema2.txt

p2.txt y p3.txt han desaparecido del directorio.

Modo Historial Área de preparación Directorio de trabajo
--soft Retrocede Conserva los cambios Intacto
--mixed Retrocede Limpia Intacto
--hard Retrocede Limpia Sobrescribe

Dos normas sobre --hard: nunca con cambios sin guardar que te importen, y nunca sobre commits publicados. Para eso está revert.

Corregir el último commit: git commit --amend

Para el error más común —un mensaje mal escrito o un archivo que faltaba— hay un atajo:

git commit --amend -m "feat: añade datos de ventas del trimestre"
git log --oneline -1
e8b9a0e feat: añade datos de ventas del trimestre

--amend sustituye el último commit por otro nuevo: el hash cambia. Es reescritura de historial, así que vale la misma regla de siempre: solo si no lo has publicado. La reescritura de historial en serio se trata en el módulo 9.

Parte 4 — La red de seguridad: git reflog

git reflog es el registro de por dónde ha pasado HEAD en tu repositorio: cada commit, cambio de rama, merge y reset. Es local y no se publica. Es lo que hace recuperable casi todo lo que parece perdido.

Recuperar una rama borrada

git switch main
git branch -d feat/informe
error: the branch 'feat/informe' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D feat/informe'

Git protege: -d se niega a borrar una rama sin integrar. Fuerza el borrado para simular el accidente:

git branch -D feat/informe
ls informe.txt
Deleted branch feat/informe (was 5c36745).
ls: informe.txt: No such file or directory

Aparentemente el trabajo se ha perdido. Consulta el reflog:

git reflog
b2c00f9 HEAD@{0}: checkout: moving from feat/informe to main
5c36745 HEAD@{1}: commit: feat: añade conclusiones
3b7824f HEAD@{2}: commit: feat: redacta informe
b2c00f9 HEAD@{3}: checkout: moving from main to feat/informe
b2c00f9 HEAD@{4}: commit (initial): chore: commit inicial

5c36745 era la punta de la rama. Recréala:

git switch -c feat/informe-recuperada 5c36745
git log --oneline -3
cat informe.txt
5c36745 feat: añade conclusiones
3b7824f feat: redacta informe
b2c00f9 chore: commit inicial
Informe trimestral
Datos de ventas
Conclusiones

Recuperada íntegra. De hecho, Git te había dado el hash en el propio mensaje de borrado: (was 5c36745).

Deshacer un reset --hard

El mismo mecanismo salva el caso más temido:

git commit -m "feat: añade datos importantes"
git reset --hard HEAD~1
ls datos.txt
HEAD is now at b2c00f9 chore: commit inicial
ls: datos.txt: No such file or directory
git reflog -3
b2c00f9 HEAD@{0}: reset: moving to HEAD~1
dbc605f HEAD@{1}: commit: feat: añade datos importantes
b2c00f9 HEAD@{2}: checkout: moving from feat/informe-recuperada to main

HEAD@{1} es donde estabas antes del reset. Vuelve:

git reset --hard HEAD@{1}
git log --oneline -1
cat datos.txt
HEAD is now at dbc605f feat: añade datos importantes
importante

Los límites del reflog

No es magia infinita, y conviene saber dónde acaba:

  • Es local: no existe en el remoto ni en el clon de otra persona. Si pierdes la carpeta, pierdes el reflog.
  • Caduca: las entradas alcanzables expiran a los 90 días y las inalcanzables a los 30, por defecto.
  • Solo registra lo que pasó por un commit. Un cambio que nunca confirmaste y descartaste con git restore no está en el reflog ni en ningún sitio.

Esa última línea es la que cierra el módulo: la red de seguridad empieza en el commit.

Errores frecuentes

  • «He usado reset --hard para deshacer algo que ya había publicado».
    Tu rama retrocede, pero el remoto no. El siguiente push será rechazado y la tentación será forzarlo, borrando commits de otras personas. Para lo publicado, revert.

  • «Quería quitar el add y he escrito git restore archivo sin --staged».
    Has sobrescrito el archivo con la versión del último commit y perdido el contenido. Lee git status antes: M y M piden comandos distintos.

  • «He hecho revert del commit con la contraseña, ya está arreglado».
    No lo está. El valor sigue accesible en el historial. Invalida la credencial y genera otra; eso es lo urgente.

  • «Hice stash y luego cambié de rama; al hacer pop ha salido un conflicto».
    El stash no está atado a una rama. Comprueba con git branch --show-current antes de pop, y recuerda que git stash list sigue ahí mientras no lo apliques.

  • «Iba a hacer un reset grande y no he creado rama de seguridad».
    git branch respaldo-<algo> cuesta un segundo. Y si ya es tarde, git reflog.

  • «He hecho --amend de un commit que ya estaba publicado».
    Has creado un commit distinto con el mismo contenido; el remoto conserva el original y los historiales divergen. Integra en vez de forzar.

Ejercicio y criterios

Sobre un repositorio gestor-inventario con un remoto de práctica:

  1. Modifica un archivo sin preparar, inspecciona el cambio con git diff y descártalo con restore. Explica por qué este paso no es recuperable.
  2. Prepara un cambio y sácalo del área de preparación sin perderlo. Muestra la diferencia en las dos columnas de git status --short.
  3. Empieza una tarea, aparcala con git stash push -m, haz otro commit y recupérala.
  4. Crea un commit con un dato erróneo, publícalo, y corrígelo con revert. Publica la corrección sin usar --force. Después demuestra con git log -S que el dato sigue en el historial y explica qué habría que hacer si fuera una credencial.
  5. En una rama sin publicar, crea tres commits, haz una rama de seguridad y prueba los tres modos de reset, documentando qué ocurre en cada zona.
  6. Borra esa rama con -D y recupérala mediante reflog.

Criterios de revisión:

  • Cada paso se justifica con las dos preguntas del árbol de decisión: si está confirmado y si está publicado.
  • El remoto nunca se reescribe: no aparece push --force en ninguna parte.
  • La corrección del commit publicado es un revert, y se demuestra que el historial conserva ambos commits.
  • Se distinguen con salidas reales los tres modos de reset.
  • La rama borrada se recupera y su contenido coincide con el original.
  • El estado final es limpio y se explica qué operación del ejercicio fue la única irreversible.

Resumen

Al finalizar, el alumnado debe saber:

  • Preguntarse siempre, antes de recuperar, si el cambio está confirmado y si está publicado.
  • Descartar cambios con git restore sabiendo que esa operación no tiene vuelta atrás.
  • Distinguir restore de restore --staged leyendo las dos columnas de git status --short.
  • Aparcar y recuperar trabajo con git stash.
  • Corregir un commit publicado con git revert, y explicar que no elimina el contenido del historial.
  • Usar git reset solo sobre historial privado, distinguiendo --soft, --mixed y --hard, y creando antes una rama de seguridad.
  • Recuperar ramas y commits perdidos con git reflog, y conocer sus límites.

Siguiente módulo: Módulo 7 · Tags, versiones y entrega.

Estado editorial: listo.

Registro de revisión técnica

Revisado el 4 de agosto de 2026 con Git 2.47.1. Todas las secuencias se han reproducido desde carpetas vacías, con identidad de práctica y configuración aislada. Se ha comprobado que:

  • git restore devuelve el archivo al estado del último commit y git restore --staged deja el cambio en el directorio de trabajo; git status --short distingue ambos casos en la primera y la segunda columna (M frente a M).
  • git stash push -m deja el árbol limpio y elimina el archivo nuevo del directorio; git stash pop restituye cada cambio en el estado en que estaba, preparado o sin preparar.
  • Tras git revert HEAD sobre un commit publicado, el historial conserva el commit original y el de reversión, git push no necesita fuerza, y el contenido revertido sigue siendo accesible: git log -S lo localiza y git show <hash>:config.txt lo imprime. De ahí la advertencia sobre credenciales, que enlaza con el módulo 11.
  • Los tres modos de reset se comportan como indica la tabla: --soft deja los cambios preparados, --mixed los deja sin preparar (como ?? al tratarse de archivos nuevos) y --hard elimina los archivos del directorio.
  • git branch -d se niega a borrar una rama sin integrar y sugiere -D; tras git branch -D, el mensaje incluye el hash (was …) y git reflog permite recrear la rama íntegra con git switch -c <nombre> <hash>.
  • git reset --hard HEAD@{1} recupera el estado previo a un reset --hard accidental, con el archivo restaurado.
  • git commit --amend sustituye el último commit y cambia su hash.

Los hashes de las salidas son los de esta ejecución y serán distintos en cada repositorio.

Pendiente al llevar el módulo a GitHub: repetir el punto 4 del ejercicio contra el repositorio real del curso, para que el revert de un commit publicado se vea también en la interfaz de la forja.