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:
- ¿El cambio está confirmado en un commit?
- ¿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 |
Sí: 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 sí |
| 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.txtTema 1: variablesgit 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 --shortM tema1.txtgit restore --staged tema1.txt
git status --short
cat tema1.txt M tema1.txt
Tema 1: variables
borrador sin terminarEl 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.txtEl 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.txtGuárdalo con un mensaje que te sirva mañana:
git stash push -m "tema 2 a medias"
git status --short
lsSaved working directory and index state On main: tema 2 a medias
README.md
tema1.txtEl á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.txtFí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 mainEl 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.txtgit log --oneline -3
ls config.txtfdaaed2 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 directoryLos 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 mainLo 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.txtfdaaed2 Revert "feat: añade configuración"
56fbca5 feat: añade configuración
clave_api = 12345-SECRETOEl 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-experimentoEsa 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 --short7ead905 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.txtAparecen 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
lsHEAD is now at 7ead905 wip: prueba 1
README.md
p1.txt
tema1.txt
tema2.txtp2.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 -1e8b9a0e 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/informeerror: 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.txtDeleted branch feat/informe (was 5c36745).
ls: informe.txt: No such file or directoryAparentemente el trabajo se ha perdido. Consulta el reflog:
git reflogb2c00f9 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 inicial5c36745 era la punta de la rama. Recréala:
git switch -c feat/informe-recuperada 5c36745
git log --oneline -3
cat informe.txt5c36745 feat: añade conclusiones
3b7824f feat: redacta informe
b2c00f9 chore: commit inicial
Informe trimestral
Datos de ventas
ConclusionesRecuperada í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.txtHEAD is now at b2c00f9 chore: commit inicial
ls: datos.txt: No such file or directorygit reflog -3b2c00f9 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 mainHEAD@{1} es donde estabas antes del reset. Vuelve:
git reset --hard HEAD@{1}
git log --oneline -1
cat datos.txtHEAD is now at dbc605f feat: añade datos importantes
importanteLos 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 restoreno 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 --hardpara deshacer algo que ya había publicado».
Tu rama retrocede, pero el remoto no. El siguientepushserá rechazado y la tentación será forzarlo, borrando commits de otras personas. Para lo publicado,revert.«Quería quitar el
addy he escritogit restore archivosin--staged».
Has sobrescrito el archivo con la versión del último commit y perdido el contenido. Leegit statusantes:MyMpiden comandos distintos.«He hecho
revertdel 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
stashy luego cambié de rama; al hacerpopha salido un conflicto».
El stash no está atado a una rama. Comprueba congit branch --show-currentantes depop, y recuerda quegit stash listsigue ahí mientras no lo apliques.«Iba a hacer un
resetgrande y no he creado rama de seguridad».git branch respaldo-<algo>cuesta un segundo. Y si ya es tarde,git reflog.«He hecho
--amendde 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:
- Modifica un archivo sin preparar, inspecciona el cambio con
git diffy descártalo conrestore. Explica por qué este paso no es recuperable. - Prepara un cambio y sácalo del área de preparación sin perderlo. Muestra la diferencia en las dos columnas de
git status --short. - Empieza una tarea, aparcala con
git stash push -m, haz otro commit y recupérala. - 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 congit log -Sque el dato sigue en el historial y explica qué habría que hacer si fuera una credencial. - 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. - Borra esa rama con
-Dy recupérala mediantereflog.
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 --forceen 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 restoresabiendo que esa operación no tiene vuelta atrás. - Distinguir
restorederestore --stagedleyendo las dos columnas degit 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 resetsolo sobre historial privado, distinguiendo--soft,--mixedy--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 restoredevuelve el archivo al estado del último commit ygit restore --stageddeja el cambio en el directorio de trabajo;git status --shortdistingue ambos casos en la primera y la segunda columna (Mfrente aM).git stash push -mdeja el árbol limpio y elimina el archivo nuevo del directorio;git stash poprestituye cada cambio en el estado en que estaba, preparado o sin preparar.- Tras
git revert HEADsobre un commit publicado, el historial conserva el commit original y el de reversión,git pushno necesita fuerza, y el contenido revertido sigue siendo accesible:git log -Slo localiza ygit show <hash>:config.txtlo imprime. De ahí la advertencia sobre credenciales, que enlaza con el módulo 11. - Los tres modos de
resetse comportan como indica la tabla:--softdeja los cambios preparados,--mixedlos deja sin preparar (como??al tratarse de archivos nuevos) y--hardelimina los archivos del directorio. git branch -dse niega a borrar una rama sin integrar y sugiere-D; trasgit branch -D, el mensaje incluye el hash(was …)ygit reflogpermite recrear la rama íntegra congit switch -c <nombre> <hash>.git reset --hard HEAD@{1}recupera el estado previo a unreset --hardaccidental, con el archivo restaurado.git commit --amendsustituye 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.