Deshacer cambios y commits en Git


Siempre hay algo que deshacer. Escribiste una edición que no querías, probaste una solución que empeoró las cosas, o hiciste un commit de algo que preferirías que nadie viera. Git guarda un registro de casi todas las versiones de tu proyecto que ha visto alguna vez, así que la mayoría de los errores son más reversibles de lo que parecen en el momento. Qué herramienta te ayuda a volver atrás depende de qué tan lejos haya llegado el error: si todavía está en tu editor, si ya está en staging, si ya se hizo el commit, o si ya se subió a algún lugar donde un compañero de equipo puede verlo. Este capítulo recorre cada etapa en ese orden.
¿Qué son push y pull?
Hacer push envía tus commits a una copia compartida del repositorio de la que también trabajan tus compañeros de equipo, y hacer pull trae sus commits hacia tu copia. Remotos y GitHub cubre ambos en detalle; para este capítulo, "pusheado" significa que el commit ya salió de tu máquina.
Descartar un cambio antes de hacer commit
Digamos que todavía estás cambiando el estilo de styles.css en weather-app desde el ciclo del commit, y el nuevo diseño resulta ser una mala idea. No lo has puesto en staging, no has hecho commit. Quieres que el archivo vuelva a como se veía la última vez, nada más. El comando exacto para eso es git restore:
$ git status
On branch main
Changes not staged for commit:
modified: styles.css
$ git restore styles.css
$ git status
On branch main
nothing to commit, working tree cleangit restore <file> descarta una edición sin commit y devuelve el archivo a su último estado guardado. Si ya habías puesto el cambio en staging con git add antes de cambiar de opinión, un git restore simple no lo va a tocar, porque el archivo ahora está en el área de staging y no solo en tu directorio de trabajo. Sácalo de staging primero, y después restáuralo:
$ git add styles.css
$ git restore --staged styles.css
$ git restore styles.cssSon dos movimientos separados para dos áreas separadas: git restore --staged saca un archivo del área de staging, y git restore por sí solo descarta la edición en tu directorio de trabajo. git restore solo llega hasta el trabajo sin commit, una vez que un cambio ya tiene commit, entra en juego un comando distinto.
git restore <file> descarta una edición que no has committeado y devuelve el archivo a como estaba antes. Si ya habías puesto el cambio en staging con git add, sácalo de staging primero con git restore --staged <file>, y después restaura el archivo si también quieres deshacer la edición. Esto solo funciona con cambios que aún no has committeado, así que no puede rescatar un error que ya guardaste. Guardar trabajo temporalmente con git stash
A veces no hay ningún error que corregir, solo mal momento. Estás a mitad de una edición en styles.css y un compañero de equipo necesita que te pases a otra rama ahora mismo, pero no estás listo para hacer commit de un estilo a medio terminar. git stash guarda tus cambios en un lugar aparte y te deja un directorio de trabajo limpio para que puedas irte y volver después.
$ git status
On branch main
Changes not staged for commit:
modified: styles.css
$ git stash
Saved working directory and index state WIP on main: 8f3c7d1 Cache forecast responses in localStorage
$ git status
On branch main
nothing to commit, working tree cleanLa edición a styles.css no desapareció, quedó guardada aparte. Recupérala con git stash pop cuando estés listo:
$ git stash pop
On branch main
Changes not staged for commit:
modified: styles.cssgit stash sirve para cambios que todavía no estás listo para committear, nunca crea un commit en tu rama.
git stash guarda a un lado los cambios que todavía no estás listo para committear, así que tu directorio de trabajo queda limpio, y git stash pop los recupera más tarde. Nada se committea y nada se pierde, la edición queda guardada por un tiempo. Uso esto constantemente cada vez que un compañero de equipo necesita que me pase a otra rama en ese momento. Deshacer un commit con git revert
Digamos que el commit 8f3c7d1, "Cache forecast responses in localStorage", resulta estar mal, y ya está pusheado, así que un compañero de equipo también lo bajó con un pull. Reescribir ese commit ahora cambiaría algo que otra persona ya tiene, y su copia y la tuya no coincidirían la próxima vez que haga pull. git revert resuelve esto sin cambiar nada de lo que ya existe: crea un commit completamente nuevo que aplica lo opuesto del cambio.
$ git revert 8f3c7d1
[main 2b6f0d1] Revert "Cache forecast responses in localStorage"El historial sigue mostrando ambos commits, el original y el que lo deshace, en orden. Ninguna copia necesita reajustarse, un compañero de equipo baja el nuevo commit de revert como cualquier otro.
git revert siempre es seguro en un commit que otros ya tienen, porque agrega un commit nuevo en vez de cambiar uno antiguo.
git revert <commit> deshace un commit agregando un commit completamente nuevo que lo invierte, en lugar de borrar el original. Eso lo convierte en la opción segura una vez que un commit ya se comparte con alguien más, porque nada de lo ya guardado se modifica. El historial mantiene un registro tanto del error como de la corrección, cosa que me gusta más cuanto más tiempo uso Git. Retroceder en el tiempo con git reset: soft, mixed y hard
Hay una tercera manera de deshacer un commit, git reset, y funciona diferente a revert: en vez de agregar un commit nuevo que deshace el anterior, reset mueve tu rama actual para que apunte a otro commit por completo. Eso lo hace rápido y limpio para commits que nadie más ha visto todavía, y riesgoso para commits que otros ya tienen.
Reset es el pariente más amplio y más fácil de usar mal de restore y revert. No lo vas a necesitar para la mayoría de las correcciones del día a día, restore, revert y stash cubren la gran mayoría de los momentos de "auxilio, deshaz esto". Sin embargo, lo vas a ver en las instrucciones de otras personas, así que aquí va lo esencial: reset mueve tu rama hacia atrás en el historial, y uno de sus modos, --hard, descarta el trabajo sin commit sin pedir confirmación. Si un comando que estás copiando de algún lado incluye git reset --hard, lee lo que hace antes de ejecutarlo.
git reset en la mayoría de las correcciones del día a día, restore, revert y stash ya cubren casi todo. Aun así, conoce cómo funciona: reset mueve tu rama hacia atrás a un commit anterior, y su modo --hard borra tu directorio de trabajo para que coincida, sin pedir confirmación. Trata con verdadera precaución cualquier comando que copies que incluya reset --hard. Elegir entre revert o reset
La regla que decide qué herramienta usar: ¿alguien más ya bajó este commit con pull? Si sí, revert. Si el commit solo existe en tu máquina, en una rama que no has pusheado, reset funciona bien y muchas veces es la solución más prolija. Cuando no estés seguro, revert siempre es la opción segura, porque funciona en ambos casos.
Recuperar un commit con git reflog
Un reset --hard, un rebase desordenado, o borrar una rama por error puede parecer que hizo que un commit desapareciera por completo. Por lo general no es así. Git mantiene un registro local de todos los lugares donde han apuntado tu HEAD y tus ramas, llamado el reflog, y ese registro es casi siempre tu camino de vuelta a un commit que parece perdido.
Es poco probable que necesites esto con frecuencia, pero aquí va lo esencial: Git rara vez descarta algo de verdad en el momento en que parece haberlo hecho. Si en algún momento ejecutas reset --hard y te das cuenta después de que necesitabas ese commit, muy probablemente haya una manera de recuperarlo, y eso se cubre aquí para cuando estés listo para profundizar más.
reset --hard y te das cuenta demasiado tarde de que necesitabas ese commit, probablemente todavía se puede recuperar. Git mantiene un registro local llamado reflog de todos los lugares donde ha estado tu trabajo, y eso suele bastar para recuperar un commit que parecía perdido. Este es territorio de Deep Dive, pero es tranquilizador saber que está ahí. 
