git reset vs revert vs restore: cuál usar y cuándo
Diferencias entre git reset, revert y restore: qué área de Git toca cada uno, los tres modos de reset y cuál usar sin romper el historial compartido.
Escribes durante una hora, algo no cuadra, y sueltas un git reset --hard para “empezar limpio”. Un segundo después te das cuenta de que esa hora de trabajo no estaba commiteada. Y Git no tiene papelera de reciclaje para eso. El susto no viene de que hayas usado un comando peligroso: viene de que reset, revert y restore suenan casi igual y cada uno toca una parte distinta de Git. Aquí explico qué área maneja cada uno, cómo funcionan los tres modos de reset, y una regla clara para elegir sin llevarte otro susto.
¿Por qué se confunden reset, revert y restore?
Se confunden porque los tres “deshacen”, pero operan sobre tres zonas diferentes de tu repositorio. Si no tienes claras esas zonas, cualquiera de los tres parece intercambiable, y ahí empiezan los accidentes.
Git guarda tu trabajo en tres sitios. El working tree son los archivos tal como están ahora mismo en tu disco, con tus ediciones sin guardar. El staging area (también llamado index) es la foto de lo que has marcado con git add, lo que entrará en el próximo commit. Y el historial es la cadena de commits ya confirmados, con su puntero de rama apuntando al último.
Cada comando vive principalmente en una de esas zonas. restore trabaja sobre el working tree y el staging, sin tocar commits. reset mueve el puntero de la rama en el historial y, según el modo, arrastra el staging y el working tree con él. revert solo añade al historial: escribe un commit nuevo. Cuando piensas “qué zona quiero cambiar” antes de teclear, la elección deja de ser adivinanza.
¿Qué hace git restore y cuándo usarlo?
git restore recupera archivos desde una fuente conocida hacia el working tree o el staging, y nunca toca el historial. Es el comando menos peligroso de los tres para la mayoría de casos, y el más olvidado.
Si tienes cambios sin commitear en un archivo y quieres tirarlos, restauras ese archivo desde el index:
# Descarta los cambios NO commiteados de archivo.js en el working tree.
# Los reemplaza por la versión que hay en staging (o en HEAD si no hay nada staged).
git restore archivo.js
Ojo con este: lo que descarta no se guarda en ningún lado. No hay red de seguridad. Antes de ejecutarlo, confirma que de verdad no quieres esos cambios.
El otro uso es sacar algo del staging sin perder el trabajo. Hiciste git add de un archivo que todavía no querías incluir y quieres devolverlo a “modificado pero sin stage”:
# Saca archivo.js del staging. Tus cambios siguen intactos en el working tree.
git restore --staged archivo.js
La diferencia entre las dos formas es la que más gente pilla mal: git restore --staged conserva tus cambios (solo los desmarca del index), mientras que git restore a secas los sobrescribe. Ese flag cambia por completo lo que ocurre: con --staged recolocas, sin él sobrescribes. Si el caso concreto que quieres es sacar cosas del git add, lo tienes desarrollado en cómo deshacer un git add.
git restore dejó de estar marcado como experimental hace tiempo y es hoy la vía recomendada para descartar cambios del working tree.[1] Reemplaza los usos antiguos de git checkout -- archivo (descartar) y git reset HEAD archivo (quitar del staging), que hacían lo mismo con una sintaxis mucho más confusa. Si aprendiste Git con checkout --, este es el momento de cambiar el hábito.
git reset y sus tres modos (—soft, —mixed, —hard)
git reset mueve el puntero de tu rama a otro commit, y lo que pasa con el staging y el working tree depende del modo que elijas. Esto reescribe el historial local, así que es potente y peligroso a partes iguales.
Imagina que hiciste tres commits que aún no has empujado y quieres rehacerlos. git reset HEAD~1 mueve la rama un commit hacia atrás. La pregunta es qué haces con los cambios de ese commit deshecho, y para eso están los modos:
# --soft: mueve la rama atrás. Los cambios del commit quedan en STAGING, listos para recommitear.
git reset --soft HEAD~1
# --mixed (el modo por defecto): mueve la rama atrás. Los cambios quedan en el WORKING TREE, sin stage.
git reset --mixed HEAD~1
# --hard: mueve la rama atrás y BORRA los cambios del working tree. No hay vuelta atrás para lo no commiteado.
git reset --hard HEAD~1
--soft deja el working tree y el index intactos: solo retrocede el puntero.[2] Es lo que usas para juntar varios commits en uno o reescribir el mensaje. --mixed es el comportamiento por defecto cuando no pones flag: mueve la rama y actualiza el index para que nada quede staged, pero tus archivos en disco no cambian. --hard sobrescribe el working tree con la versión del commit destino: ahí es donde desaparece trabajo.
La regla mental es sencilla. --soft conserva todo y solo toca el historial. --mixed conserva tu trabajo pero lo desmarca. --hard no conserva nada del working tree. Cuanto más “hard”, más zonas arrasa.
El peligro real de reset no es el modo, es la rama. Si esos commits ya están en el remoto y otros compañeros los tienen, moverlos hacia atrás y forzar el push reescribe una historia que ya no es solo tuya. Para el caso específico de deshacer el último commit hay un post dedicado que compara reset y revert sobre HEAD: deshacer el último commit en Git.
¿Qué hace git revert y por qué es seguro en ramas compartidas?
git revert crea un commit nuevo que invierte los cambios de otro commit, sin borrar nada del historial.[3] En vez de retroceder el puntero como hace reset, avanza: suma un commit que deshace, y así el registro completo queda intacto.
# Crea un commit nuevo que revierte el último commit (HEAD).
git revert HEAD
# Revierte un commit concreto por su hash. El hash siempre es hexadecimal (0-9, a-f).
git revert a1b2c3d
Esta es la opción correcta cuando la rama ya está empujada y otros trabajan sobre ella. Como no reescribes historia, sino que la añades, nadie tiene que resincronizar nada raro: para tus compañeros es un commit más que llega por el flujo normal. Ese commit inverso queda en el log, lo cual sorprende a quien esperaba que el commit malo “desapareciera”. No desaparece, y esa es justo la propiedad que lo hace seguro.
También sirve para varios commits de golpe. Le pasas un rango y Git genera un commit inverso por cada uno:
# Revierte todos los commits desde A (sin incluirlo) hasta B (incluido).
git revert a1b2c3d..f4e5d6c
Si en el equipo la norma es “no reescribir nunca la rama compartida”, revert es tu herramienta por defecto para arreglar cosas ya publicadas.
Tabla comparativa: reset vs revert vs restore
Los tres comandos, uno al lado del otro, según lo que tocan y dónde es seguro usarlos:
| Comando | Qué toca | ¿Reescribe historia? | ¿Seguro en rama compartida? | Caso típico |
|---|---|---|---|---|
restore | Working tree y/o staging | No | Sí | Tirar cambios sin commitear o sacar algo del git add |
reset | Puntero de rama y, según el modo, staging y working tree | Sí (en local) | No | Rehacer commits que aún no has empujado |
revert | Añade un commit inverso al historial | No (suma, no borra) | Sí | Anular un commit que ya está en el remoto |
La columna que más importa es la penúltima. Si la respuesta a “¿seguro en rama compartida?” es No, y esa rama ya está empujada, para y usa otra cosa.
¿Cuál uso? Guía de decisión rápida
Antes de teclear cualquiera de los tres, respóndete estas preguntas en orden:
- ¿El commit que quieres deshacer ya está empujado a una rama que otros usan? Usa
git revert. Es la única opción que no reescribe la historia compartida. - ¿Solo quieres descartar cambios en archivos que ni siquiera has commiteado? Usa
git restore archivo. No necesitas tocar el historial para nada. - ¿Hiciste
git addde algo por error y quieres sacarlo del staging sin perderlo? Usagit restore --staged archivo. - ¿Quieres rehacer commits locales que todavía no has empujado? Usa
git resetcon el modo que corresponda:--softsi quieres conservar los cambios staged,--mixedpara dejarlos sin stage,--hardsolo cuando de verdad quieres tirarlos.
El orden importa. La primera pregunta, la del remoto, manda sobre todas las demás: en cuanto la respuesta sea “sí, está empujado y compartido”, revert gana aunque otro comando parezca más directo. Si quieres el mapa completo de todas las formas de deshacer en Git, con más casos, tienes la guía general para deshacer cambios en Git.
Antes de ver los errores clásicos, prueba tú a decidir qué comando toca en cada situación:
Cargando ejercicio...
Errores comunes al deshacer en Git
reset --hard sobre trabajo no commiteado. Es el susto del principio. Piensas que --hard es un “deshacer” con vuelta atrás, pero lo que borra del working tree nunca llegó a un commit, así que no aparece en git reflog y, si tampoco pasó por git add, casi nunca se recupera. Si dudas, commitea primero (aunque sea un commit temporal) y luego decide. Un commit basura es reversible; un working tree sobrescrito, casi nunca.
reset sobre commits ya empujados y luego push --force. Aquí el trabajo que se pierde es el de tus compañeros: al reescribir la rama y forzarla, cualquier commit que otros hubieran construido encima de tu historia original desaparece del remoto. Si necesitas deshacer algo publicado, revert. Si de verdad tienes que reescribir una rama compartida (algo raro), habla con el equipo antes.
Esperar que revert “borre” el commit. Lo que hace es sumar un commit inverso encima y dejar el original en el log. Es el comportamiento correcto y el que lo hace seguro, pero sorprende a quien buscaba que el commit malo se esfumara. Si querías que desapareciera del historial, buscabas reset, no revert, y eso solo es viable en local.
Confundir git restore con git restore --staged. Un flag separa recolocar de destruir. --staged saca del index y conserva tus cambios; sin --staged los sobrescribe. Lee el comando dos veces antes de darle a enter cuando no llevas el --staged.
Creer que --force-with-lease es “la única forma segura” de forzar. Es más segura que --force porque comprueba que el remoto sigue en el estado que esperabas antes de sobrescribirlo, y así no pisas el trabajo que otro empujó mientras tú rebasabas.[4] Pero sigue reescribiendo historia. “Más segura” no significa “segura sin más”: en una rama compartida, la opción realmente tranquila sigue siendo revert.
Perderles el miedo a estos tres comandos se consigue usándolos hasta que la elección salga sola. En el curso Git de cero a profesional practicas deshacer cambios sobre un repositorio real, con las tres zonas de Git delante y sin arriesgar tu trabajo de verdad.
Un concepto nuevo cada semana
Fuentes
- Documentación oficial de Git: git restore. Comportamiento de
git restoresobre el working tree y de la opción--staged. - Documentación oficial de Git: git reset. Descripción exacta de los modos
--soft,--mixedy--hard. - Documentación oficial de Git: git revert. Confirma que
revertregistra commits nuevos que invierten los cambios y admite rangos conA..B. - Documentación oficial de Git: git push. Diferencia entre
--forcey--force-with-lease.
Preguntas Frecuentes
¿git restore borra mis cambios para siempre?
En el working tree, sí. git restore archivo reemplaza el archivo por la versión del staging (o de HEAD) y lo que había sin commitear no se guarda en ningún lado. Por eso conviene confirmar antes de ejecutarlo. La variante git restore --staged archivo es distinta: esa solo desmarca del staging y tus cambios siguen ahí.
¿Cuál es la diferencia entre reset —mixed y reset —soft?
Las dos mueven el puntero de la rama al commit que indiques. La diferencia está en el staging. Con --soft, los cambios del commit deshecho quedan staged, listos para recommitear. Con --mixed, que es el modo por defecto, esos cambios pasan al working tree sin stage. Ninguno de los dos borra archivos de tu disco.
¿Puedo recuperar algo tras git reset —hard?
Depende de si estaba commiteado. Si el --hard te movió por encima de commits ya hechos, git reflog guarda por dónde estuvo HEAD y puedes volver al hash anterior:
# Muestra el historial de posiciones de HEAD; ahí encuentras el commit "perdido".
git reflog
git reset --hard a1b2c3d # vuelves al estado anterior por su hash
Pero los cambios que nunca llegaron a un commit no están en reflog y casi nunca se recuperan.
¿git revert sirve para varios commits a la vez?
Sí. Le pasas un rango con la sintaxis git revert A..B y Git crea un commit inverso por cada commit del rango. Es útil cuando quieres anular una tanda de cambios ya empujados sin reescribir la historia compartida.