Ejecuta git reset --hard sobre un commit que tu equipo ya bajó y acabas de dejar tu historial en desacuerdo con el suyo. Ejecuta git revert sobre un typo que no has subido y añades un commit innecesario al log. Git tiene cuatro comandos para deshacer cambios, restore, reset, commit --amend y revert, y no son intercambiables. Elige el equivocado y el resultado es que no pasa nada, o pasa mucho más de lo que querías.

Lo que decide cuál usar es dónde vive el cambio en este momento: en tu working tree, en la staging area, en un commit que solo tienes tú, o en un commit que ya subiste.

Dónde vive un cambio en Git: working tree, index, historial

Todo comando para deshacer actúa sobre una o más de tres capas:

  • Working tree: los archivos en disco, tal como los estás editando.
  • Index (la staging area): la foto que armaste con git add, esperando a convertirse en el próximo commit.
  • Historial: los commits ya registrados, alcanzables desde HEAD.

Un cambio empieza en el working tree, pasa al index con git add, y pasa al historial con git commit. Deshacerlo significa recorrer el mismo camino en sentido contrario. Cuanto más haya avanzado, más deliberado tiene que ser el comando.

1
2
working tree  --add-->  index  --commit-->  history
   restore              restore --staged     reset / revert / amend

Cómo descartar cambios fuera del index con git restore

Editaste config.py, lo rompiste, y quieres recuperar la copia de Git:

1
git restore config.py

Esto sobrescribe el archivo en tu working tree con la versión del index, que es la misma que la de HEAD salvo que hayas agregado cambios con git add. La edición desaparece. No queda en ningún stash, y ningún comando de Git te la devolverá. Para descartar de una vez todos los cambios fuera del index:

1
git restore .

git restore sustituye al antiguo git checkout -- <archivo>. Los dos siguen funcionando, pero restore existe para que “devuélveme este archivo” ya no comparta comando con “cambiar de rama”.

Cómo sacar un archivo del index sin perder tus cambios

Ejecutaste git add en tres archivos y luego notaste que uno de ellos no pertenece a este commit. La edición en sí está bien:

1
git restore --staged migration.sql

migration.sql sale del index y vuelve a “modificado, fuera del index”. El archivo en disco no se toca. Es lo mismo que el antiguo git reset HEAD -- migration.sql, y es el único punto donde reset y restore se superponen: reset con un pathspec solo toca el index, que es exactamente lo que hace restore --staged.

Cómo deshacer un commit con git reset: soft, mixed y hard

git restore no puede tocar un commit. Para eso necesitas git reset, y el flag controla cuánto deshace:

FlagWorking treeIndexHistorial (HEAD)
--softsin cambiossin cambiosse mueve
--mixed (por defecto)sin cambiosigual al commit indicadose mueve
--hardigual al commit indicadoigual al commit indicadose mueve

Los tres mueven el puntero de la rama. La diferencia está en cuánto de tu trabajo lo sigue hacia atrás.

--soft rebobina los commits pero deja sus cambios en el index. Eso lo convierte en la forma más rápida de aplastar los últimos tres commits en uno:

1
2
git reset --soft HEAD~3
git commit -m "Add payment retry logic"

--mixed es lo que obtienes con un git reset HEAD~1 simple. Rebobina el commit y devuelve sus cambios al working tree como ediciones fuera del index, para que puedas dividirlos en commits más pequeños o descartar parte de ellos.

--hard descarta el diff por completo. El working tree queda como si el commit nunca hubiera existido:

1
git reset --hard HEAD~1

No hay confirmación previa. Los commits que dejaste atrás todavía se pueden encontrar con git reflog (más abajo), pero cualquier edición sin confirmar que tuvieras en el working tree en ese momento se pierde para siempre, así que revisa git status antes de correrlo. Y nunca lo ejecutes sobre commits que ya bajó otra persona: mover el puntero de la rama hacia atrás en una rama compartida deja tu historial y el suyo en desacuerdo sobre lo que pasó, el mismo problema detrás de la regla de oro del rebase.

Cómo corregir el último commit con git commit –amend

Si el error está en el commit más reciente, un reset es excesivo. --amend reemplaza el commit de la punta en vez de rebobinar más allá de él:

1
git commit --amend -m "Fix payment retry logic"

Agrega más cambios al index primero y se pliegan en el mismo commit, sobre el mismo padre:

1
2
git add retry.py
git commit --amend --no-edit

El commit enmendado es un objeto nuevo con un hash nuevo, igual que uno rebaseado. La regla es la misma que para el rebase: haz amend solo de lo que no has subido. Si haces amend de un commit que ya está en una rama compartida, todos los que lo bajaron tienen que reconciliar dos commits que dicen ser el mismo cambio.

Cómo deshacer un commit que ya se subió con git revert

Una vez que un commit está en una rama compartida, reset y --amend quedan descartados. Reescribir historial que otras personas ya bajaron produce ramas divergentes y commits duplicados. git revert toma el otro camino: deja el commit malo donde está y añade uno nuevo que hace lo contrario.

1
git revert a1b2c3d

Git abre un editor para el mensaje del commit de revert, y luego lo registra:

1
2
3
Revert "Add aggressive cache invalidation"

This reverts commit a1b2c3d4e5f6789a1b2c3d4e5f6789a1b2c3d4.

El historial ahora tiene tanto el commit original como su inverso. No se reescribió nada, así que un git pull normal en cualquier otro clon recoge la corrección. Para revertir un rango como un solo commit:

1
2
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert the last three deploy changes"

--no-commit (-n) aplica el inverso de cada commit al working tree y al index sin confirmar, así que tres reverts terminan en un solo commit en vez de tres. Si un revert entra en conflicto con cambios hechos después, Git se detiene igual que a mitad de un merge: resuelve los marcadores de conflicto, haz git add del archivo y luego ejecuta git revert --continue.

“Deshacer un push” significa esto: revertir el commit y luego subir el revert. No hagas force-push de historial reescrito a una rama de la que otras personas bajan cambios.

Cómo recuperarte de un reset erróneo con git reflog

Después de un reset --hard, los commits desaparecen de git log. Siguen existiendo. git reflog lista cada posición que ocupó HEAD en tu máquina, incluida la de justo antes del reset:

1
git reflog
1
2
f3a9c21 HEAD@{0}: reset: moving to HEAD~1
7d4e1b8 HEAD@{1}: commit: Add payment retry logic

git reset --hard HEAD@{1} te devuelve al commit 7d4e1b8. Cuánto tiempo viven las entradas del reflog, y por qué el reflog es solo local, está explicado en la sección de recuperación de Git Rebase vs Merge. Funciona igual sin importar qué comando movió HEAD.

El reflog te devuelve a donde estabas. No te dice cuál fue el último commit bueno. Para eso, git bisect reduce cientos de commits al que introdujo el bug.

Git reset vs revert vs restore: cuál usar en cada caso

El cambio está…Comando
Editado, fuera del index, y lo quieres descartadogit restore <archivo>
En el index, y quieres sacarlo de ahí (la edición se conserva)git restore --staged <archivo>
Confirmado, sin subir, y es el último commitgit commit --amend
Confirmado, sin subir, y quieres rebobinar más atrásgit reset --soft / --mixed / --hard HEAD~n
Confirmado y ya subido o compartidogit revert <commit>
Perdido tras un reset --hard que lamentasgit reflog + git reset --hard HEAD@{n}

Para cualquier cosa ya confirmada, una pregunta resuelve la fila: ¿alguien más vio este commit? Si no, reset o amend y listo. Si sí, revert. Cuesta un commit extra en el log, y ningún compañero tiene que desenredar un historial que ya no coincide con el suyo.

Artículos relacionados