Git reset vs git revert: cuál usar y cómo deshacer sin miedo

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 resetgit revert
Qué haceMueve la rama a un commit anteriorCrea un commit nuevo que anula otro
HistorialLo reescribe (los commits “desaparecen”)Lo conserva y le suma un commit
Uso seguroCommits locales, antes del pushRamas compartidas, main, producción
Riesgo--hard puede borrar trabajoCasi 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

ModoHistorial (commits)StagingArchivos de trabajo
--softRetrocedeSe conservaSe conservan
--mixed (por defecto)RetrocedeSe vacíaSe conservan
--hardRetrocedeSe vacíaSe 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~1 y HEAD^ apuntan al mismo sitio: el padre directo del commit actual. La diferencia aparece solo en commits de merge (que tienen dos padres).
  • HEAD~2 retrocede dos commits, HEAD~3 tres, 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 addNoNo hay registro en Git
Archivos nuevos que alcanzaste a pasar por git addCon suerteGit 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…ComandoQué pasa con mi cambio
Sacar un archivo del staginggit restore --staged archivo (o git reset archivo)Se conserva en el archivo
Descartar los cambios de un archivogit restore archivoSe pierde
Descartar staging y cambios a la vezgit restore --staged --worktree archivoSe pierde
Retroceder commitsgit resetDepende 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: revert deja trazabilidad y no incomoda a nadie.
  • Si vas a forzar un push, usa --force-with-lease, nunca --force a ciegas.
  • Explora git reflog antes 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ónComando
Trabajo local, antes de hacer pushgit reset (--soft o --mixed para conservar tu trabajo)
Repositorio o rama compartidagit revert <hash>
Eliminar por completo los cambiosgit reset --hard (con cuidado)
Conservar el historial y deshacer un cambio puntualgit revert <hash>
Deshacer un merge ya compartidogit revert -m 1 <hash-del-merge>
Sacar un archivo del staginggit restore --staged <archivo>
Descartar cambios sin guardar de un archivogit restore <archivo>
Me arrepentí de un reset --hardgit 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

Ideas, sistemas y lecturas de diseño web que merecen ser probadas

EXPLORAR SILO COMPLETO→