Deshacer cambios en Git: reset, revert y restore
1. Descripción general
Cualquiera que use Git necesita retroceder en algún momento: corregir un archivo por error, hacer git add de más, o hacer commit de algo que no debería. Git tiene tres comandos principales para esto: restore, reset y revert. Sus nombres suenan parecido, pero cada comando afecta a una zona diferente.
Este artículo explora cada comando usando un repositorio de ejemplo. Después de leerlo, sabrás qué comando usar en cada situación. También aprenderás cómo rescatar commits cuando cometes un error con reset --hard.
2. Preparar el repositorio de ejemplo
Primero, recuerda que Git tiene tres zonas:
- Working tree: los archivos que estás editando en el disco.
- Staging area (index): lo que entrará en el próximo commit.
- Commit history: los commits guardados, con
HEADapuntando al commit actual.
Creemos un repositorio pequeño con cuatro commits:
mkdir order-service && cd order-service
git init -b main
echo "class Order {}" > Order.java
git add Order.java && git commit -m "Añadir clase Order"
echo "class Customer {}" > Customer.java
git add Customer.java && git commit -m "Añadir clase Customer"
echo "class Order { int total; }" > Order.java
git commit -am "Añadir total a Order"
echo "class PaymentService {}" > PaymentService.java
git add PaymentService.java && git commit -m "Añadir PaymentService"
git log --oneline
El comando git commit -am añade al staging todos los archivos ya monitoreados y hace commit de inmediato. El resultado de git log será similar al siguiente (los hashes en tu máquina serán diferentes):
d4e5f60 (HEAD -> main) Añadir PaymentService
c3d4e5f Añadir total a Order
b2c3d4e Añadir clase Customer
a1b2c3d Añadir clase Order
3. git restore: deshacer cambios no confirmados
git restore está disponible desde Git 2.23. Este comando solo funciona con archivos. Nunca mueve HEAD ni modifica el historial de commits. En versiones antiguas de Git, esto lo hacía git checkout -- <file>.
3.1. Descartar cambios en el working tree
Supongamos que editas Order.java y luego cambias de idea:
echo "class Order { int total; String note; }" > Order.java
git status --short
git restore Order.java
cat Order.java
M Order.java
class Order { int total; }
git status --short muestra M, que significa el archivo fue modificado pero no añadido al staging. git restore Order.java copia el contenido desde el staging area al working tree. Como no hay nada en el staging, este contenido coincide con HEAD.
Nota: Los cambios descartados con
git restorese pierden completamente. Git nunca los guardó, así quereflogtampoco puede rescatarlos. Si tienes dudas, ejecutagit stashen lugar derestore.
3.2. Sacar un archivo del staging area
Accidentalmente hiciste git add de un archivo que aún no quieres hacer commit:
echo "class Order { int total; String note; }" > Order.java
git add Order.java
git restore --staged Order.java
git status --short
M Order.java
La opción --staged copia el contenido desde HEAD al staging area. El resultado es que el archivo se ‘desañade del staging’, pero el contenido que acabas de editar permanece en el working tree. El comando antiguo equivalente es git reset HEAD Order.java.
Si quieres descartar tanto el staging como el working tree de una vez, usa:
git restore --staged --worktree Order.java
3.3. Recuperar un archivo de un commit anterior
La opción --source te permite obtener el contenido de un archivo de cualquier commit:
git restore --source=HEAD~2 Order.java
cat Order.java
git status --short
class Order {}
M Order.java
HEAD~2 es el commit “Añadir clase Customer”. En ese commit, Order.java aún no tenía total. Por defecto, el comando solo escribe en el working tree, por lo que el archivo es ahora M (sin añadir al staging). Verifica el cambio, luego git add y haz commit si quieres conservarlo. Si no, ejecuta git restore Order.java para revertir.
4. git reset: mover el puntero de rama
git reset <commit> mueve la rama actual (y HEAD) al commit especificado. Los tres modos --soft, --mixed y --hard difieren en si el staging area y el working tree se sobrescriben o no.
| Modo | Branch/HEAD | Staging area | Working tree |
|---|---|---|---|
--soft |
se mueve al commit | se mantiene | se mantiene |
--mixed (predeterminado) |
se mueve al commit | se reinicia según el commit | se mantiene |
--hard |
se mueve al commit | se reinicia según el commit | se reinicia según el commit |
Todos los ejemplos a continuación parten de un estado limpio en el commit d4e5f60.
4.1. –soft: quitar commit, preservar cambios añadidos al staging
git reset --soft HEAD~1
git log --oneline -1
git status --short
c3d4e5f (HEAD -> main) Añadir total a Order
A PaymentService.java
El commit “Añadir PaymentService” desaparece de la rama. El archivo PaymentService.java sigue existiendo y está en estado A (añadido al staging). Esto es útil cuando quieres cambiar el mensaje de commit o fusionar varios commits en uno:
git commit -m "Añadir PaymentService para procesar pagos"
4.2. –mixed: quitar commit y desañadir del staging
git reset HEAD~1
git status --short
?? PaymentService.java
Este es el modo predeterminado. La rama retrocede un commit y el staging area se reinicia. PaymentService.java es un archivo nuevo, así que ahora aparece como no monitoreado (??). Usa --mixed cuando quieras dividir un commit de nuevo: añade partes al staging y haz commits separados.
4.3. –hard: descartar todo
git add PaymentService.java && git commit -m "Añadir PaymentService"
git reset --hard HEAD~1
ls
HEAD is now at c3d4e5f Añadir total a Order
Customer.java Order.java
El commit, el contenido del staging y el working tree de los archivos ya monitoreados se revierten a c3d4e5f. PaymentService.java desaparece.
Nota:
reset --hardtambién elimina los cambios no confirmados en archivos ya monitoreados. Estos cambios no se pueden recuperar. Por el contrario, no elimina archivos no monitoreados. Para eliminar archivos no monitoreados, debes usargit clean -fd, que es igual de peligroso.
4.4. Rescatar commits con reflog
Los commits descartados con reset no se pierden de inmediato. Git registra todos los movimientos de HEAD en reflog:
git reflog -3
c3d4e5f HEAD@{0}: reset: moving to HEAD~1
9f8e7d6 HEAD@{1}: commit: Añadir PaymentService
c3d4e5f HEAD@{2}: commit: Añadir PaymentService para procesar pagos
Solo necesitas reset al commit que quieres recuperar:
git reset --hard 9f8e7d6
git log --oneline -1
9f8e7d6 (HEAD -> main) Añadir PaymentService
Reflog solo existe en tu máquina. Por defecto, las entradas sin referencias expiran después de 30 días y el recolector de basura las elimina. Por lo tanto, es mejor rescatar lo antes posible.
4.5. Cuándo NO debes usar reset
No debes hacer reset de commits ya enviados a una rama compartida. Después del reset, el historial local y remoto divergen, obligándote a hacer git push --force. Los compañeros que ya han extraído esos commits tendrán un historial confuso y podrían empujarlos de nuevo accidentalmente.
Si tienes que sobrescribir, por ejemplo en una rama personal, usa un comando más seguro:
git push --force-with-lease
--force-with-lease rechaza el push si el remoto tiene commits nuevos que aún no has traído. De esta forma no sobrescribilás accidentalmente el trabajo de otros.
5. git revert: deshacer con un nuevo commit
git revert no elimina el historial. Este comando crea un nuevo commit con contenido opuesto al commit que quieres deshacer. El historial solo crece sin ser reescrito, por lo que es seguro para ramas ya enviadas.
5.1. Revertir un commit
git revert --no-edit HEAD
git log --oneline -3
ls
1a2b3c4 (HEAD -> main) Revert "Añadir PaymentService"
9f8e7d6 Añadir PaymentService
c3d4e5f Añadir total a Order
Customer.java Order.java
--no-edit usa el mensaje predeterminado sin abrir el editor. El commit original 9f8e7d6 permanece en el historial. El nuevo commit 1a2b3c4 elimina PaymentService.java. El cuerpo del nuevo commit contiene la línea This reverts commit 9f8e7d6..., ayudando a otros a rastrear el origen.
Puedes revertir incluso commits antiguos en medio del historial, no necesariamente HEAD:
git revert --no-edit c3d4e5f
cat Order.java
class Order {}
Si los commits posteriores modifican el mismo código, Git reportará un conflicto. Resuelves el conflicto, ejecutas git add y luego git revert --continue. Para cancelar a mitad del proceso, usa git revert --abort.
5.2. Revertir múltiples commits en uno
Por defecto, cada commit revertido genera un commit separado. Para combinarlos, usa --no-commit:
git revert --no-commit HEAD~3..HEAD~1
git commit -m "Deshacer cambios de Customer y Order"
La sintaxis A..B selecciona los commits después de A, hasta B. Git revierte cada commit en el staging area y luego haces un commit único.
5.3. Revertir un commit de merge
Un commit de merge tiene dos padres. Por lo tanto, Git necesita saber qué rama quieres mantener como ‘línea principal’:
git revert -m 1 <hash-merge-commit>
-m 1 selecciona el primer padre, generalmente la rama en la que hiciste el merge (por ejemplo main). El resultado es que todos los cambios aportados por la otra rama se deshacen.
Nota: Después de revertir un merge, si haces merge de la misma rama de nuevo, Git considera los commits antiguos como “ya merged”. Esos cambios no volverán. Para traerlos de vuelta, debes revertir el commit revert en sí.
6. ¿Qué comando elegir?
| Situación | Comando a usar |
|---|---|
| Descartar cambios sin stage de un archivo | git restore <file> |
| Desañadir un archivo del staging | git restore --staged <file> |
| Obtener una versión anterior de un archivo | git restore --source=<commit> <file> |
| Corregir/fusionar el último commit, no enviado | git reset --soft HEAD~1 |
| Dividir un commit de nuevo, no enviado | git reset HEAD~1 |
| Descartar completamente un commit local | git reset --hard <commit> |
| Deshacer un commit ya enviado | git revert <commit> |
Una regla simple para recordar:
restorefunciona con archivos.resetfunciona con ramas, úsalo localmente.revertfunciona con historial compartido.
7. Errores comunes
- Confundir
resetconreverten una rama compartida. Este es el error más común. Si ya enviaste cambios, usa revert. - Ejecutar
reset --hardcon cambios no confirmados. Antes de ejecutar, revisagit status. Si hay cambios, hazgit stashprimero. - Pensar que
reset --hardlimpia todo el directorio. Los archivos no monitoreados permanecen. Necesitasgit clean -npara ver una vista previa, luegogit clean -fdpara eliminar. - Olvidar que
HEAD~1yHEAD^son diferentes en un commit de merge.HEAD^2es el segundo padre, mientras queHEAD~2es el abuelo, es decir, el padre del padre. Con un historial lineal,HEAD~1yHEAD^son equivalentes. - Usar
git checkout <file>por costumbre. Este comando sigue funcionando, perocheckouttiene demasiados roles y es fácil confundirlo con cambiar de rama. En Git moderno,restoreyswitchson más claros.
8. Conclusión
En este artículo, viste tres formas de deshacer cambios en Git:
git restorerecupera el contenido de archivos en el working tree o staging area sin tocar el historial.git resetmueve una rama a un commit diferente, con tres niveles--soft,--mixedy--hard. Úsalo solo para commits no enviados.git revertcrea un commit inverso, la opción segura para ramas compartidas.
Cuando cometes un error, recuerda git reflog. La mayoría de los commits ‘perdidos’ siguen esperándote para recuperarlos.