Git reset vs git revert: cuál usar y cómo deshacer sin miedo
Dos comandos que me han salvado el pellejo más veces de las que me gustaría admitir: git reset y git revert.
Cuando empecé a trabajar con Git, cometer errores era parte del menú diario. Crear archivos innecesarios, cometer antes de tiempo, romper el código y… bueno, tú sabes. En ese proceso, dos comandos se convirtieron en mis salvavidas. Este artículo es la guía que quiero que tengas a mano cuando llegue tu turno: qué hace cada uno, cuándo usarlo y cómo salir del apuro si te equivocas.
Todos los comandos y salidas que ves aquí los ejecuté de verdad en un repositorio de prueba con Git 2.53.0 (los hashes aparecen recortados a 7 caracteres y los mensajes de Git, en inglés). Si apenas estás empezando, antes pasa por la primera parte de la guía de Git y, para ramas y conflictos, por Ramas, reset y revert.
Respuesta rápida: ¿reset o revert?
La regla cabe en una línea: si el commit solo existe en tu máquina, reset; si ya lo compartiste, revert.
Tabla 1: Reset y revert en una mirada
git reset | git revert | |
|---|---|---|
| Qué hace | Mueve la rama a un commit anterior | Crea un commit nuevo que anula otro |
| Historial | Lo reescribe (los commits “desaparecen”) | Lo conserva y le suma un commit |
| Uso seguro | Commits locales, antes del push | Ramas compartidas, main, producción |
| Riesgo | --hard puede borrar trabajo | Casi ninguno (solo conflictos) |
Piénsalo así: reset es una máquina del tiempo con poder para borrar el pasado; revert respeta el pasado y escribe encima una corrección.
Qué hace git reset (soft, mixed y hard)
git reset mueve la rama en la que estás a otro commit. Lo que cambia entre sus tres modos es qué pasa con el staging (los cambios preparados con git add) y con tus archivos. Según la documentación oficial, el modo por defecto es --mixed.
Tabla 2: Qué toca cada modo de git reset
| Modo | Historial (commits) | Staging | Archivos de trabajo |
|---|---|---|---|
--soft | Retrocede | Se conserva | Se conservan |
--mixed (por defecto) | Retrocede | Se vacía | Se conservan |
--hard | Retrocede | Se vacía | Se sobrescriben: cambios perdidos |
Partimos de un repositorio con tres commits:
git log --oneline
d8350b2 Commit tres
28d4530 Commit dos
5315c54 Commit uno
git reset —soft HEAD~1
Deshace el commit, pero deja todo preparado, como si estuvieras a punto de hacer git commit:
git reset --soft HEAD~1
git status --short
A tres.txt
La A en la primera columna significa que el archivo sigue en el staging. Es ideal para rehacer el último commit o para unir varios en uno (git reset --soft HEAD~3 y un solo git commit).
git reset HEAD~1 (—mixed)
Sin ningún modo, Git usa --mixed: el commit se deshace y el staging se vacía, pero tus archivos siguen intactos.
git reset HEAD~1
git status --short
?? tres.txt
Ahora tres.txt aparece como archivo sin seguimiento (??): sigue en tu carpeta, solo que Git ya no lo tiene preparado. Sirve para volver a elegir qué entra en el commit. Esto responde a una duda frecuente: git reset HEAD~1 y git reset --mixed HEAD~1 son exactamente lo mismo.
git reset —hard HEAD~1: el botón de autodestrucción
git reset --hard HEAD~1
ls
HEAD is now at 28d4530 Commit dos
dos.txt
uno.txt
El commit se fue, el staging quedó vacío y tres.txt ya no está en la carpeta. La documentación lo dice así: --hard sobrescribe archivos y directorios con la versión del commit y elimina los archivos rastreados que no están en él. Úsalo cuando de verdad quieras descartar todo; el resto del tiempo, --soft o --mixed son más amables.
HEAD~1, HEAD^ y hashes: a dónde apuntas
HEAD es el commit en el que estás parado. Con el mismo repositorio de tres commits:
for r in HEAD^ HEAD~1 HEAD~2; do echo -n "$r -> "; git rev-parse --short $r; done
HEAD^ -> 28d4530
HEAD~1 -> 28d4530
HEAD~2 -> 5315c54
HEAD~1yHEAD^apuntan al mismo sitio: el padre directo del commit actual. La diferencia aparece solo en commits de merge (que tienen dos padres).HEAD~2retrocede dos commits,HEAD~3tres, y así sucesivamente.- También puedes usar un hash que saques de
git log --oneline:
git reset --hard 5315c54
HEAD is now at 5315c54 Commit uno
Por eso, cuando alguien busca “git reset head -1” o “git reset soft -1”, casi siempre quiere decir HEAD~1: “un commit antes del actual”.
Cómo deshacer un git reset —hard (y qué no se puede recuperar)
La pregunta del millón: ¿se puede deshacer un git reset --hard? Depende de qué perdiste.
Si lo que borraste eran commits, sí. Git guarda en el reflog un registro de los movimientos de tus ramas, incluso de los que ya no aparecen en git log. Después del reset --hard HEAD~1:
git reflog
28d4530 HEAD@{0}: reset: moving to HEAD~1
d8350b2 HEAD@{1}: commit: Commit tres
28d4530 HEAD@{2}: commit: Commit dos
5315c54 HEAD@{3}: commit (initial): Commit uno
El commit d8350b2 sigue ahí. Para traerlo de vuelta, usa el hash o la posición en el reflog:
git reset --hard HEAD@{1}
HEAD is now at d8350b2 Commit tres
Verifiqué que tres.txt regresó a la carpeta. Según la documentación de reflog, por defecto las entradas se conservan 90 días (gc.reflogExpire), y las que ya no son alcanzables desde la rama, 30 días (gc.reflogExpireUnreachable).
Ahora la parte incómoda. El reflog recuerda commits, no cambios sueltos. Probé con un archivo modificado sin commit y un archivo nuevo en el staging:
git status --short
A nuevo.txt
M uno.txt
git reset --hard HEAD
cat uno.txt
HEAD is now at d8350b2 Commit tres
uno
La línea que agregué a uno.txt desapareció, nuevo.txt también, y el reflog solo muestra movimientos de commits. Esto es lo que se puede y no se puede recuperar:
Tabla 3: Qué puedes recuperar después de un reset —hard
| Lo que perdiste | ¿Se recupera? | Cómo |
|---|---|---|
| Commits que “desaparecieron” | Sí | git reflog y git reset --hard <hash> |
Cambios modificados que nunca guardaste con commit ni git add | No | No hay registro en Git |
Archivos nuevos que alcanzaste a pasar por git add | Con suerte | Git guarda su contenido como objeto huérfano; git fsck --lost-found lo lista |
En mi prueba, git fsck --lost-found mostró dangling blob 36080d9, y git show 36080d9 imprimió el contenido de nuevo.txt. Es un rescate de último recurso: sin nombre de archivo y sujeto a que Git limpie objetos huérfanos. La conclusión práctica es una sola: haz commit antes de arriesgar. Un commit “feo” que luego deshaces con reset --soft es mejor que un trabajo que no existe en ningún lado.
Qué hace git revert (HEAD, hash, rangos y merges)
Si reset borra el pasado, revert lo respeta. Según la documentación, revierte los cambios que introdujo un commit y registra un commit nuevo con ese resultado. El error queda en el historial, pero anulado. Exige que tu árbol de trabajo esté limpio.
git revert HEAD
git revert --no-edit HEAD
git log --oneline
[main 2397bd1] Revert "Commit tres"
1 file changed, 1 deletion(-)
delete mode 100644 tres.txt
2397bd1 Revert "Commit tres"
d8350b2 Commit tres
28d4530 Commit dos
5315c54 Commit uno
Fíjate: Commit tres sigue en el historial y encima aparece Revert "Commit tres". Sin --no-edit, Git abre tu editor para que escribas el mensaje. Y ahí va un consejo: sé humano. No dejes un mensaje genérico; escribe algo como “Este commit causaba un bug en producción. Revertido hasta nuevo aviso.” El yo del futuro (y tus compañeros) te lo agradecerán.
git revert con un hash
HEAD es solo el último commit. Con un hash puedes anular uno que quedó más atrás:
git revert --no-edit 28d4530
ls
[main 38b8afe] Revert "Commit dos"
1 file changed, 1 deletion(-)
delete mode 100644 dos.txt
uno.txt
Solo se deshizo Commit dos; todo lo que vino después se mantiene (si ese commit tocó las mismas líneas que uno posterior, tendrás un conflicto que resolver).
Revertir un rango de commits
Con el historial Commit uno … Commit cinco, para anular los dos últimos como un único commit usa --no-commit (-n), que aplica los cambios al árbol de trabajo y al staging sin crear commits:
git revert --no-commit HEAD~2..HEAD
git status --short
git commit -m "Revert de cuatro y cinco"
D cinco.txt
D cuatro.txt
HEAD~2..HEAD significa “los commits después de HEAD~2 hasta HEAD”, es decir, cuatro y cinco. Si omites --no-commit, Git crea un revert por cada commit, del más nuevo al más viejo (primero Revert "Commit cinco" y luego Revert "Commit cuatro"), lo cual a veces es justo lo que quieres para dejar el rastro más detallado.
Revertir un merge: el flag -m
Un commit de merge tiene dos padres, así que Git no sabe cuál lado quieres conservar. Sin ayuda, se niega:
git revert --no-edit cf3c3e7
error: commit cf3c3e7... is a merge but no -m option was given.
fatal: revert failed
Con -m 1 le dices que el padre 1 (la rama donde hiciste el merge, normalmente main) es la línea principal:
git revert -m 1 --no-edit cf3c3e7
ls
[main 83770b7] Revert "Merge feature"
1 file changed, 1 deletion(-)
delete mode 100644 f.txt
dos.txt
m.txt
tres.txt
uno.txt
Se fue f.txt (el aporte de feature) y se quedó m.txt (lo que ya estaba en main). Una advertencia de la documentación: revertir un merge declara que no quieres esos cambios, así que un merge posterior de la misma rama solo traerá los commits nuevos. Si más adelante quieres volver a unirla, primero tendrás que revertir el revert.
“git revert —hard” no existe
Es una búsqueda frecuente, y la respuesta es corta: revert no tiene modo --hard. Esos modos son de reset. Si lo escribes, Git no entiende y te muestra la ayuda:
git revert --hard HEAD
usage: git revert [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] <commit>...
or: git revert (--continue | --skip | --abort | --quit)
...
Lo que probablemente quieres, según el caso:
- Anular un commit ya compartido:
git revert <hash>. - Borrar un commit local por completo:
git reset --hard HEAD~1. - Descartar cambios sin guardar:
git restore <archivo>(siguiente sección).
git restore: cuando solo quieres descartar o sacar del staging
No todo error es un commit. A veces solo cambiaste un archivo o hiciste git add de más. Para eso existe git restore (disponible desde Git 2.23), con una ventaja: trabaja sobre archivos concretos, sin tocar el historial.
Partimos de uno.txt modificado y agregado al staging (M uno.txt):
git restore --staged uno.txt
git status --short
M uno.txt
El cambio sigue en el archivo (nota la M en la segunda columna), pero ya no está preparado. Es lo mismo que hace git reset uno.txt: la documentación de restore lo equipara. Para descartar el cambio del archivo:
git restore uno.txt
git status --short
(sin salida: el árbol quedó limpio, y uno.txt volvió a su contenido del último commit). Ojo: este paso no se puede deshacer.
Tabla 4: Reset a un archivo frente a restore
| Quiero… | Comando | Qué pasa con mi cambio |
|---|---|---|
| Sacar un archivo del staging | git restore --staged archivo (o git reset archivo) | Se conserva en el archivo |
| Descartar los cambios de un archivo | git restore archivo | Se pierde |
| Descartar staging y cambios a la vez | git restore --staged --worktree archivo | Se pierde |
| Retroceder commits | git reset | Depende del modo |
Y para el caso de la búsqueda “git restore to HEAD”: git restore --source=HEAD --staged --worktree archivo deja el archivo exactamente como en el último commit.
Reset vs revert en una rama compartida
Esta es la razón por la que la regla de oro existe. Probé el escenario con un repositorio remoto local (un --bare) y un clon “de un compañero”.
Después de hacer git push de tres commits, ejecuto git reset --hard HEAD~1 en mi copia. Mi historial tiene ahora un commit menos que el remoto, así que Git rechaza el push normal:
git push origin main
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to '...remote.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
Para que pase, tendría que forzarlo. La forma menos peligrosa es --force-with-lease:
git push --force-with-lease origin main
+ 57c57e1...406add2 main -> main (forced update)
Según la documentación de push, --force desactiva las comprobaciones de seguridad y puede hacer que el remoto pierda commits. --force-with-lease solo sobrescribe si el remoto sigue en el valor que esperabas; si alguien más empujó mientras tanto, falla en vez de aplastar su trabajo.
Aun así, el problema real es del otro lado. El clon de mi compañero todavía tiene el commit que yo borré. Después de hacer git fetch:
## main...origin/main [ahead 1]
Su rama está “adelantada” con un commit que ya no existe en el remoto. Si hace push lo reintroduce, y si hace pull puede acabar con merges extraños. Con revert esto no ocurre: se agrega un commit y el push es normal.
git revert --no-edit HEAD
git push origin main
[main e4554c4] Revert "Commit nuevo"
cec69b2..e4554c4 main -> main
Sin --force, sin avisarle a nadie, sin romper clones ajenos. Por eso: en una rama compartida, revert.
¿Y rebase?
git rebase también reescribe el historial, igual que reset: toma commits y los vuelve a aplicar sobre otra base, generando commits nuevos con hashes distintos. Por eso comparte la misma regla de oro: no reescribas commits que otros ya descargaron. Se usa para ordenar el historial de una rama propia antes de integrarla, no para “deshacer” un commit. Si quieres profundizar, la referencia es la documentación oficial de git rebase.
Práctica guiada: rompe cosas a propósito
La mejor forma de perderle el miedo es romper algo en un proyecto de juguete. Abre una terminal:
mkdir practica-git && cd practica-git
git init
echo "versión buena" > app.txt
git add . && git commit -m "feat: versión inicial"
Ejercicio 1: revert. Crea un archivo con error, commitéalo y anúlalo:
touch error.txt
git add . && git commit -m "Archivo con error"
git revert HEAD --no-edit
git log --oneline
Verás el commit del error y, encima, su revert. Nada se perdió, todo quedó registrado.
Ejercicio 2: reset y rescate. Crea otro archivo, commitéalo y bórralo con --hard:
touch reset.txt
git add . && git commit -m "Archivo de prueba para reset"
git reset --hard HEAD~1 # reset.txt desaparece
git reflog # ...pero el commit sigue en el registro
git reset --hard HEAD@{1} # y lo recuperas
Ejercicio 3: lo que el reflog no salva. Modifica app.txt sin hacer commit y ejecuta git reset --hard HEAD. Comprueba que el cambio no vuelve. Ese es el motivo para hacer commits pequeños y frecuentes.
Buenas prácticas
- Escribe mensajes descriptivos, sobre todo en un revert: explica qué se deshizo y por qué.
- Piensa dos veces antes de un
reset --hard, y más si la rama es compartida. Haz un commit antes de arriesgar. - Revierte con responsabilidad:
revertdeja trazabilidad y no incomoda a nadie. - Si vas a forzar un push, usa
--force-with-lease, nunca--forcea ciegas. - Explora
git reflogantes de entrar en pánico: casi siempre hay una salida.
Tabla final de decisión
Tabla 5: ¿Qué comando uso según mi situación?
| Situación | Comando |
|---|---|
| Trabajo local, antes de hacer push | git reset (--soft o --mixed para conservar tu trabajo) |
| Repositorio o rama compartida | git revert <hash> |
| Eliminar por completo los cambios | git reset --hard (con cuidado) |
| Conservar el historial y deshacer un cambio puntual | git revert <hash> |
| Deshacer un merge ya compartido | git revert -m 1 <hash-del-merge> |
| Sacar un archivo del staging | git restore --staged <archivo> |
| Descartar cambios sin guardar de un archivo | git restore <archivo> |
Me arrepentí de un reset --hard | git reflog y git reset --hard <hash> |
Conclusión
Git también es una herramienta para pensar. Deshacer no es solo un asunto técnico, es una cuestión de responsabilidad: cuando el error es solo tuyo y vive en tu máquina, reset te deja volver atrás sin dejar rastro; cuando otros ya lo vieron, revert te obliga a dejar constancia, que es justo lo que un equipo necesita.
Aprende a equivocarte y a revertir con clase.
Fuentes
- git reset: modos
--soft,--mixed(por defecto) y--hard. - git revert:
--no-commit,-my advertencia al revertir merges. - git reflog: registro de movimientos y expiración por defecto (90 y 30 días).
- git restore:
--staged,--worktreey--source. - git push:
--forcey--force-with-lease. - git rebase.
- En este blog: ¿Qué es Git y por qué lo necesitas? y Ramas, reset y revert para experimentar con IA.
Ideas, sistemas y lecturas de diseño web que merecen ser probadas
Seguridad en WordPress con datos reales: analicé 1.000 ataques bloqueados. Casi nadie va por la contraseña; más del 40 % busca archivos sensibles (.env, .git).
¿Hostinger es bueno? Mi experiencia de 5 años con dominios, WordPress y VPS, más ventajas, desventajas, precios de renovación, soporte y si sirve para ecommerce.
Segunda parte de la guía de Git para vibe coders: qué son las ramas y cómo usarlas para cada experimento con IA, cómo resolver un conflicto de merge y cuándo usar git restore, reset, revert o reflog para deshacer errores sin perder trabajo.



