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.
| |
Cómo descartar cambios fuera del index con git restore
Editaste config.py, lo rompiste, y quieres recuperar la copia de Git:
| |
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:
| |
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:
| |
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:
| Flag | Working tree | Index | Historial (HEAD) |
|---|---|---|---|
--soft | sin cambios | sin cambios | se mueve |
--mixed (por defecto) | sin cambios | igual al commit indicado | se mueve |
--hard | igual al commit indicado | igual al commit indicado | se 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:
| |
--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:
| |
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:
| |
Agrega más cambios al index primero y se pliegan en el mismo commit, sobre el mismo padre:
| |
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.
| |
Git abre un editor para el mensaje del commit de revert, y luego lo registra:
| |
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:
| |
--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:
| |
| |
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 descartado | git 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 commit | git commit --amend |
| Confirmado, sin subir, y quieres rebobinar más atrás | git reset --soft / --mixed / --hard HEAD~n |
| Confirmado y ya subido o compartido | git revert <commit> |
Perdido tras un reset --hard que lamentas | git 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.