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 HEAD apuntando 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 restore se pierden completamente. Git nunca los guardó, así que reflog tampoco puede rescatarlos. Si tienes dudas, ejecuta git stash en lugar de restore.

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 --hard tambié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 usar git 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:

  • restore funciona con archivos.
  • reset funciona con ramas, úsalo localmente.
  • revert funciona con historial compartido.

7. Errores comunes

  • Confundir reset con revert en una rama compartida. Este es el error más común. Si ya enviaste cambios, usa revert.
  • Ejecutar reset --hard con cambios no confirmados. Antes de ejecutar, revisa git status. Si hay cambios, haz git stash primero.
  • Pensar que reset --hard limpia todo el directorio. Los archivos no monitoreados permanecen. Necesitas git clean -n para ver una vista previa, luego git clean -fd para eliminar.
  • Olvidar que HEAD~1 y HEAD^ son diferentes en un commit de merge. HEAD^2 es el segundo padre, mientras que HEAD~2 es el abuelo, es decir, el padre del padre. Con un historial lineal, HEAD~1 y HEAD^ son equivalentes.
  • Usar git checkout <file> por costumbre. Este comando sigue funcionando, pero checkout tiene demasiados roles y es fácil confundirlo con cambiar de rama. En Git moderno, restore y switch son más claros.

8. Conclusión

En este artículo, viste tres formas de deshacer cambios en Git:

  • git restore recupera el contenido de archivos en el working tree o staging area sin tocar el historial.
  • git reset mueve una rama a un commit diferente, con tres niveles --soft, --mixed y --hard. Úsalo solo para commits no enviados.
  • git revert crea 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.

gitversion-controlresetrevertrestore