git reset y git revert: cómo deshacer un commit sin romper el historial del equipo
git reset mueve el puntero de la rama a otro commit y reescribe el historial, así que solo sirve para commits locales que aún no se han subido. git revert crea un commit nuevo con los cambios inversos y no toca el historial, por eso es la forma segura de deshacer un commit que ya está en el servidor.
En resumen: git reset hace retroceder la rama y finge que los commits sobrantes nunca existieron. git revert deja el historial tal cual y añade un commit nuevo que anula los cambios del antiguo. Lo primero sirve para el trabajo local; lo segundo, para todo lo que ya está en el repositorio compartido. Si solo recuerdas esta regla, evitarás la mayoría de los desastres con Git.
A continuación veremos cómo funcionan ambos comandos, en qué se diferencian los modos de reset, cómo deshacer un commit después de un push, cómo recuperar commits «perdidos» y qué pinta aquí git clean.
Las tres áreas que modifican los comandos para deshacer
Para entender la diferencia, recuerda las tres áreas de Git: el directorio de trabajo (los archivos en disco), el índice (staging area, lo que entrará en el próximo commit) y el historial (los commits a los que apuntan la rama y HEAD). Cada comando para deshacer actúa sobre su propio conjunto de áreas.
| Comando | Historial | Índice | Directorio de trabajo | Seguro en una rama compartida |
|---|---|---|---|---|
git restore archivo | no | no | sí | sí |
git restore --staged archivo | no | sí | no | sí |
git reset --soft | sí | no | no | no |
git reset --mixed (por defecto) | sí | sí | no | no |
git reset --hard | sí | sí | sí | no |
git revert | añade un commit | — | — | sí |
git clean | no | no | borra archivos sin seguimiento | sí, pero es irreversible |
Cómo funciona git reset
El comando git reset <commit> mueve el puntero de la rama actual (y HEAD con él) al commit indicado. Los commits «posteriores» no se borran físicamente, pero dejan de verse en la rama. El modo determina qué pasa con el índice y los archivos.
git reset --soft: deshacer el commit y dejar los cambios en el índice
La rama se mueve, pero el índice y los archivos quedan como estaban. Todos los cambios de los commits deshechos quedan preparados para el commit. Es la forma ideal de reescribir un mensaje o juntar los últimos commits en uno solo.
git reset --soft HEAD~3
git commit -m "feat: formulario de registro"
Los tres últimos commits se han convertido en uno y el contenido de los archivos no ha cambiado.
git reset --mixed: deshacer el commit y el índice
Es el modo por defecto: git reset HEAD~1 equivale a git reset --mixed HEAD~1. La rama se mueve, el índice se vacía y los cambios se quedan en el directorio de trabajo sin preparar. Útil cuando has incluido de más en un commit y quieres volver a decidir qué añadir y en qué partes.
git reset HEAD~1
git status
# Changes not staged for commit:
# modified: app.py
# modified: config.yml
git reset --hard: volver atrás por completo
La rama, el índice y el directorio de trabajo quedan en el estado del commit indicado. Todos los cambios sin commit en archivos con seguimiento se pierden sin aviso. Es el único modo de reset que realmente puede destruir tu trabajo.
git reset --hard HEAD~1 # descartar el último commit y todos los cambios
git reset --hard origin/main # dejar la rama igual que en el servidor
Antes de --hard conviene ejecutar git status y git stash si hay algo valioso. Lo que está en un commit se puede recuperar (lo vemos más abajo); lo que no, no.
git reset sobre un solo archivo
Con una ruta, reset no mueve la rama, solo modifica el índice: git reset app.py saca el archivo de los preparados. El equivalente moderno es git restore --staged app.py, que es más claro. Las opciones --soft y --hard no funcionan con rutas.
Cómo funciona git revert
git revert <commit> calcula los cambios del commit indicado, los aplica a la inversa y crea un commit nuevo. Las líneas añadidas se eliminan y las eliminadas vuelven. El historial avanza, no se reescribe nada, así que tus compañeros no tienen que arreglar nada en sus copias.
git log --oneline
# a3f9c21 (HEAD -> main) docs: README
# 7be41d0 feat: caché de respuestas
# 1c02e8f fix: timeout de las peticiones
git revert 7be41d0
# [main 5d8e2aa] Revert "feat: caché de respuestas"
# 2 files changed, 3 insertions(+), 41 deletions(-)
Git abrirá el editor con un mensaje del tipo Revert "..." y la línea This reverts commit 7be41d0.... Es buena práctica añadir por qué se ha deshecho el cambio.
Opciones útiles de revert
--no-edit: aceptar el mensaje estándar sin abrir el editor.-n(--no-commit): aplicar los cambios inversos al índice sin crear el commit. Así puedes deshacer varios commits con uno solo.git revert A..B: deshacer un rango de commits (cada uno con su propio commit de revert, de los más nuevos a los más antiguos).-m 1: obligatorio para un commit de merge; indica qué padre se considera la línea principal.
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert: deshacer tres commits del experimento"
Revert de un commit de merge
Una fusión tiene dos padres y Git no sabe respecto a cuál calcular los cambios «inversos», así que sin -m da el error is a merge but no -m option was given. Normalmente la rama principal es el primer padre:
git revert -m 1 9f1e2ab
La trampa: si más adelante quieres volver a fusionar esa misma rama, Git considerará que sus commits ya están fusionados y no traerá los cambios. La solución es hacer primero un revert del propio commit de revert y después añadir los cambios nuevos.
git reset vs git revert: cuál elegir
| Criterio | git reset | git revert |
|---|---|---|
| Qué hace con el historial | lo reescribe (los commits desaparecen de la rama) | añade un commit nuevo |
| El commit ya está subido | requiere push --force y rompe el trabajo de los compañeros | un git push normal |
| Deshacer un commit en mitad del historial | solo junto con todos los posteriores | se puede de forma puntual |
| Rastro en el historial | no queda | queda: se ve quién lo deshizo y por qué |
| Escenario típico | limpiar commits locales antes del push | deshacer en main un commit que rompió producción |
Regla sencilla: si alguien aparte de ti ha visto el commit, revert; si no, reset. La excepción es tu rama de feature personal que nadie ha descargado: ahí reescribir el historial y hacer git push --force-with-lease es aceptable.
Por qué reset --hard en una rama compartida es mala idea
Imagina que haces git reset --hard HEAD~2 en main y git push --force. Tus compañeros siguen teniendo esos dos commits. En el siguiente git pull, Git intentará fusionar su rama con la tuya y los commits «borrados» volverán, ahora con un commit de merge sobrante. Peor aún si alguien ya había subido sus commits encima: el force push simplemente los sobrescribe.
Por eso en las ramas protegidas (main, release) el force push suele estar prohibido en la configuración del hosting. Si no hay más remedio que reescribir, usa git push --force-with-lease: se negará si en el servidor hay commits que tú no has visto.
Escenarios típicos para deshacer cambios
Cómo deshacer el último commit que aún no se ha subido
git reset --soft HEAD~1 # cambios en el índice, se puede volver a hacer commit
git reset HEAD~1 # cambios en los archivos, índice vacío
git reset --hard HEAD~1 # cambios descartados
Si solo quieres corregir el mensaje o añadir un archivo olvidado, no hace falta reset: git add olvidado.txt y git commit --amend.
Cómo deshacer un commit después del push
git revert <hash>
git push
Esta es la respuesta a la búsqueda frecuente «git reverse commit»: en Git no existe un comando git reverse; el commit «inverso» lo crea precisamente git revert.
Cómo dejar la rama igual que en el servidor
git fetch origin
git reset --hard origin/main
Útil cuando la rama local se ha liado tras un rebase fallido o experimentos. Todos los commits locales que no estén en el servidor desaparecerán de la rama.
Cómo recuperar commits tras un reset fallido
Git registra cada movimiento de HEAD en el reflog. Tras un reset, los commits siguen en la base de objetos al menos varias semanas (por defecto, las entradas inalcanzables del reflog se guardan 30 días).
git reflog
# 1c02e8f HEAD@{0}: reset: moving to HEAD~2
# a3f9c21 HEAD@{1}: commit: docs: README
# 7be41d0 HEAD@{2}: commit: feat: caché de respuestas
git reset --hard a3f9c21 # o HEAD@{1}
El reflog es local: solo existe en tu clon y no se envía al servidor.
git clean: borrar archivos sin seguimiento
git reset --hard no toca los archivos que Git no sigue: archivos nuevos que nunca pasaron por el índice, artefactos de compilación, logs. Para ellos existe git clean. El comando es irreversible: los archivos que borra no van ni al reflog ni a la papelera.
git clean -n # dry run: mostrar qué se va a borrar
git clean -f # borrar archivos sin seguimiento
git clean -fd # lo mismo más las carpetas sin seguimiento
git clean -fdx # más los archivos ignorados (.gitignore): node_modules, dist
git clean -i # selección interactiva
Sin -f, Git se niega a ejecutarlo por defecto (opción clean.requireForce). Empieza siempre con -n, sobre todo con -x: también borrará tu .env local si está ignorado.
Combinación para volver por completo al último commit: git reset --hard && git clean -fd.
Errores habituales
- reset --hard en lugar de --soft. Lo que estaba en commits lo salva el reflog; lo que no, se ha perdido. La costumbre de hacer
git stashantes de experimentar resuelve el problema. - revert sin entender las dependencias. Si commits posteriores modificaron las mismas líneas, revert producirá un conflicto. Resuélvelo como cualquier otro: edita los archivos,
git add,git revert --continue; si cambias de idea,git revert --abort. - Confundir restore y reset. Para deshacer los cambios de un archivo no hace falta un reset de toda la rama:
git restore archivo. - force push a main. Mejor revert y un push normal, aunque el historial quede algo más largo.
Conclusión
git reset es la herramienta para tu propio historial aún no publicado: --soft conserva los cambios en el índice, --mixed en los archivos y --hard lo descarta todo. git revert es la forma segura de deshacer en ramas compartidas y deja un rastro honesto en el historial. git clean termina de limpiar los archivos sin seguimiento, pero no admite errores. Los escenarios para deshacer cambios con restore, reset y revert se practican en el curso de Git.
Preguntas frecuentes
¿git reset borra los commits para siempre?
No, los commits siguen en la base de objetos y se pueden encontrar con git reflog hasta que el recolector de basura los elimine; por defecto, a los 30 días para las entradas inalcanzables.
¿Se puede deshacer un git revert?
Sí, el commit de revert es un commit normal: puedes deshacerlo con otro git revert o, si no lo has subido, quitarlo con git reset.
¿En qué se diferencia git reset de git checkout?
git reset mueve el puntero de la rama actual, mientras que git checkout (o git switch) cambia HEAD a otra rama o commit sin modificar la rama en sí.
¿Qué significan HEAD~1 y HEAD^?
Ambos indican el padre del commit actual; HEAD~3 es el tercer antecesor siguiendo los primeros padres y HEAD^2 es el segundo padre de un commit de merge.
¿Cómo deshacer git clean?
Con Git no se puede: los archivos sin seguimiento borrados nunca estuvieron en el repositorio, así que ejecuta siempre antes git clean -n.
Este tema se explica a fondo y con práctica en el curso «Git desde cero hasta desarrollador seguro».
Ir al curso