Git intermedio II: deshacer con cabeza
Segunda entrega de la serie Git intermedio. La anterior fue sobre ramas: branch, switch y merge. Tiempo de lectura estimado: 5 minutos.
Deshacer es donde la gente más se pone nerviosa con git, y con razón: algunos comandos de deshacer pueden borrarte trabajo de verdad. La buena noticia es que git tiene tres verbos distintos para deshacer, cada uno diseñado para un escenario concreto: git restore, git reset y git revert. Usarlos bien es cuestión de saber qué toca cada uno: ficheros, puntero de la rama, o historia publicada.
git restore: deshacer cambios en ficheros
git restore es el "deshacer" moderno para ficheros. Llegó en git 2.23 junto con switch, para separar responsabilidades que antes mezclaba checkout. Se encarga exclusivamente de restaurar ficheros desde otro estado.
git restore fichero.go # descarta cambios en el working tree
git restore . # descarta todos los cambios no staged
git restore --staged fichero.go # saca un fichero del staging (unstage)
git restore --source=main f.go # trae el fichero como está en main
Casos que resuelve:
"He tocado un fichero y me he arrepentido":
git restore src/handler.go
El fichero vuelve a como estaba en el último commit. Este comando es destructivo: los cambios no staged se pierden para siempre, no están en ningún reflog. Si no estás seguro, primero git diff para ver qué vas a tirar, o git stash para guardarlos por si acaso.
"He añadido algo con git add y quiero sacarlo del staging":
git restore --staged src/handler.go
El fichero sale del staging area pero mantiene los cambios en el working tree. Es lo opuesto de git add. Git te sugiere este comando literalmente en el output de git status cuando tienes cosas stageadas. Léete lo que te dice git.
"Quiero traer un fichero tal como está en otra rama":
git restore --source=main README.md
Copia README.md desde la rama main al working tree actual. No cambia de rama, solo trae el fichero. Útil para "recuperar" un fichero que borraste sin querer en tu rama pero sigue bien en main.
El gran mensaje: restore solo toca ficheros del working tree o del staging. Nunca toca commits ni mueve ramas. Es tan seguro como destructivo puede ser un rm.
git reset: mover el puntero de la rama
git reset mueve el puntero de la rama actual a otro commit. Eso puede sonar abstracto, pero el efecto práctico es "deshacer commits". Hay tres modos:
git reset --soft HEAD~1 # deshace el commit, conserva cambios en staging
git reset --mixed HEAD~1 # deshace el commit, saca cambios del staging
git reset --hard HEAD~1 # deshace el commit y TIRA los cambios
HEAD~1 significa "el commit anterior al actual". Puedes usar cualquier referencia: HEAD~3, abc123, nombre de rama, etc.
--soft es el más conservador: mueve la rama hacia atrás pero los cambios de los commits "borrados" quedan en staging, listos para recommitear. Útil cuando quieres fusionar dos commits en uno, o cuando te diste cuenta de que el mensaje está mal.
--mixed (el valor por defecto si no especificas modo) mueve la rama y deja los cambios en el working tree sin staged. Lo uso cuando quiero deshacer un commit y reorganizar qué entra en el siguiente.
--hard es el peligroso: mueve la rama y borra todo lo que no sea exactamente ese commit. Working tree y staging, al agua. Si ejecutas --hard con cambios sin guardar, se van. Sin reflog, sin nada.
El truco de seguridad: casi cualquier cosa que hayas perdido con reset --hard sigue en git reflog durante un tiempo. Puedes volver con git reset --hard ORIG_HEAD o git reset --hard HEAD@{1}. Pero mejor no probar, mejor no meter --hard sin estar seguro.
Norma esencial: reset reescribe historia local. Si los commits que vas a deshacer ya los has pusheado a una rama compartida, usar reset significa dejar la rama desincronizada y, si haces force push, borrar trabajo en el remoto. Para deshacer algo ya pusheado y compartido, usa revert, no reset.
git revert: deshacer sin reescribir historia
git revert hace lo contrario: en vez de borrar un commit, crea un nuevo commit que deshace los cambios del anterior. La historia queda intacta: el commit original sigue ahí, con un "hermano gemelo" al final que lo neutraliza.
git revert abc123 # crea un commit que revierte abc123
git revert HEAD # revierte el último commit
git revert --no-commit HEAD # prepara la reversión pero no la commitea
git revert abc123..def456 # revierte un rango de commits
Cuándo usar revert:
- El commit malo ya está en una rama compartida o en producción.
- No puedes/no quieres reescribir la historia.
- Quieres que quede constancia de "esto se revirtió, a propósito, tal día".
Durante revert puede haber conflictos si los cambios a revertir chocan con trabajo posterior. Se resuelven igual que en un merge: editas los ficheros con marcadores <<<<<<<, haces git add, git revert --continue. O git revert --abort si te arrepientes.
El mensaje del commit por defecto es del tipo Revert "Mensaje original del commit". Edítalo para explicar por qué reviertes. "Revert X porque rompió el pipeline de checkout en producción" le dice más a alguien que lea la historia dentro de seis meses.
Tabla mental: qué usar cuándo
| Situación | Comando |
|---|---|
| Tocaste un fichero y quieres tirarlo | git restore fichero |
Hiciste git add por error |
git restore --staged fichero |
| El último commit está mal, quieres rehacerlo | git reset --soft HEAD~1 |
| Quieres deshacer los dos últimos commits y sus cambios, trabajo local | git reset --hard HEAD~2 |
| Un commit viejo (ya compartido) está mal y hay que anularlo | git revert abc123 |
Mi regla de oro: si el commit está solo en tu rama y no lo has pusheado, puedes usar reset con tranquilidad. Si ya lo ha visto alguien más, usa revert.
El comando que te salva
Uno que no es "deshacer" pero es la red de seguridad detrás de todos los anteriores:
git reflog
reflog te muestra el historial de dónde ha estado HEAD: cada commit, cada cambio de rama, cada reset. Incluso commits "borrados" por reset --hard siguen ahí durante unos 30 días (por defecto).
Si has hecho un reset destructivo y quieres volver atrás:
git reflog # busca el hash donde estabas antes
git reset --hard HEAD@{3} # vuelves a ese estado
Lo veremos en detalle en la serie avanzada, pero vale la pena saber que existe desde ahora: git rara vez pierde datos de verdad, incluso cuando parece que sí.
Errores típicos al deshacer
Usar reset --hard sin leer qué estás deshaciendo. Dedica dos segundos a git status y git log --oneline -5 antes.
Revertir un commit viejo sin entender las dependencias. Si has reverted algo y hay commits posteriores que construían sobre eso, puede hacer falta revertir más o resolver conflictos. No es automático.
Confundir revert con "volver atrás" en el sentido de checkout. git revert abc123 no lleva tu rama a abc123: crea un commit nuevo. Para "volver" a un estado antiguo, lo que necesitas es reset o checkout de un commit específico.
Lo que viene
Con restore, reset y revert cubres todas las formas cuerdas de deshacer en git. En la siguiente entrega toca el kit de bolsillo intermedio: git stash para guardar trabajo a medias, git tag para marcar versiones y git fetch para mirar qué hay en el remoto sin mezclarlo con tu trabajo. Y después viene el salto a la serie avanzada, donde usaremos reflog a fondo.