Skip to content
This page has been auto-translated and may contain errors.View in English

Deshacer cambios y commits en Git ​

docs.scrimba.com

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:

bash
$ 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 clean

git 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:

bash
$ git add styles.css
$ git restore --staged styles.css
$ git restore styles.css

Son 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.

Los tutoriales y scripts más antiguos usan git checkout -- <file> para hacer exactamente lo que hoy hace git restore <file>. checkout solía servir tanto para "cambiar de rama" como para "restaurar un archivo", lo cual era lo bastante confuso como para que Git dividiera las dos funciones en switch y restore. Todavía te vas a encontrar con checkout en material antiguo, reconócelo, pero usa restore en todo lo que escribas de ahora en adelante. También puedes descartar todos los cambios sin staging de la carpeta actual de una sola vez con git restore ., lo cual vale la pena pensarlo dos veces antes de ejecutarlo, ya que borra todos los archivos de golpe.

El deshacer de restore se detiene firmemente en el directorio de trabajo. Un commit es un objeto guardado que permanece hasta que el garbage collection de Git lo elimina, y por eso un commit problemático se puede recuperar a través del reflog incluso después de un reset duro. Una edición sin commit nunca llegó a convertirse en un objeto guardado: restore extrae de nuevo la última versión guardada de un archivo sobre tu edición, y no hay ningún estado anterior guardado en ningún lado para recuperar. Ese es el argumento práctico para hacer commits temprano y con frecuencia. Un commit desordenado casi siempre se puede arreglar; una edición que nunca guardaste generalmente no.

JunoDescartar un cambio sin commitgit 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.
JunoDescartar un cambio sin commitgit restore <file> es el reemplazo moderno de git checkout -- <file>, y git restore . borra todos los cambios sin staging de la carpeta de una sola vez, así que ejecuta primero git status para ver qué afectaría. Todavía vas a ver a checkout haciendo este trabajo en respuestas y scripts antiguos, es la misma idea con un nombre viejo y más sobrecargado.
JunoDescartar un cambio sin commitrestore nunca crea ni toca un commit, solo sobrescribe tu directorio de trabajo con la última versión guardada de un archivo. Eso significa que no deja ningún rastro del cual recuperarte si te equivocas, a diferencia de casi todo lo demás en este capítulo. Haz commits pequeños y frecuentes, y así un "ups" se convierte en un problema que reset o el reflog pueden solucionar, en vez de un cambio que nunca tuvo un punto de guardado al cual volver.

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.

bash
$ 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 clean

La edición a styles.css no desapareció, quedó guardada aparte. Recupérala con git stash pop cuando estés listo:

bash
$ git stash pop
On branch main
Changes not staged for commit:
        modified:   styles.css

git stash sirve para cambios que todavía no estás listo para committear, nunca crea un commit en tu rama.

Un solo stash rara vez cubre una semana real de trabajo, así que conviene conocer el resto de las herramientas. git stash list muestra todo lo que has guardado, con lo más reciente primero. git stash pop reaplica la entrada más reciente y la quita de la lista; git stash apply la reaplica pero la deja en la lista, algo útil si quieres el mismo cambio guardado en dos ramas. Ponle nombre a un stash desde el principio, así list no se convierte en un muro de entradas sin etiquetar más adelante:

bash
$ git stash push -m "wip: forecast card layout"
$ git stash list
stash@{0}: On main: wip: forecast card layout

Un stash está construido con la misma maquinaria de commits que todo lo demás en Git. git stash guarda tu estado en staging y sin staging como un par de objetos commit, y apunta una referencia especial, refs/stash, hacia ellos, y por eso git stash list se lee como una pequeña historia propia. El reflog se apoya en el mismo truco, una referencia que apunta a commits que ninguna rama alcanza.

JunoGuardar trabajo temporalmentegit 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.
JunoGuardar trabajo temporalmentegit stash list muestra cada cambio guardado, pop reaplica y quita el más reciente, apply lo reaplica sin quitarlo. Ponle nombre a los stashes desde el principio con push -m, una lista de stashes llena de entradas sin etiquetar una semana después no le sirve a nadie, ni siquiera a ti.
JunoGuardar trabajo temporalmente Un stash es un par de objetos commit hacia los que apunta refs/stash, situados fuera de cualquier rama, y por eso se comporta como su propia historia pequeñita. Una vez que lo ves así, el stash deja de sentirse como magia, es el mismo modelo de objetos haciendo el mismo truco otra vez.

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.

bash
$ 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.

Revertir un cambio que se superpone con commits posteriores puede producir un conflicto, del mismo tipo que cubre Merging y conflictos: Git marca el archivo con <<<<<<<, ======= y >>>>>>> y espera a que lo resuelvas. Corrige los marcadores, haz git add del archivo, y termina con git revert --continue.

bash
$ git revert 8f3c7d1
Auto-merging src/index.js
CONFLICT (content): Merge conflict in src/index.js

$ git add src/index.js
$ git revert --continue

La seguridad de revert en historial compartido se basa en la misma idea que hay detrás de nunca rebasear una rama pública: reescribir un commit que otros ya bajaron con pull hace que su copia y la tuya queden desincronizadas, y alguien después tiene que forzar su camino más allá del desajuste. Revert deja cada commit existente exactamente como estaba. Solo agrega uno nuevo, así que ninguna copia necesita reconciliarse con la tuya.

JunoDeshacer un commit con revertgit 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.
JunoDeshacer un commit con revert Revert puede generar un conflicto de la misma manera que un merge cuando commits posteriores tocaron las mismas líneas, se resuelve igual: corrige los marcadores, haz add del archivo, y luego git revert --continue. Úsalo siempre que el commit que estás deshaciendo ya esté pusheado o compartido.
JunoDeshacer un commit con revert Revert nunca toca un commit existente, así que es la herramienta de deshacer que convive bien con una rama de la que otras personas ya están tirando con pull. Manténlo como tu opción por defecto en el momento en que un commit problemático haya salido de tu propia máquina.

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 <commit> mueve tu rama para que apunte a ese commit, y --soft, --mixed y --hard deciden cuánto más se mueve con ella. Imagina tres capas: tu historial de commits, donde se ubica el puntero de la rama, el área de staging, lo que está listo para committear, y tu directorio de trabajo, los archivos en el disco. Los hashes cortos de abajo se leen igual que como los cubre Historial con git log --oneline, y el objetivo HEAD~1 significa "un commit antes de HEAD", la forma estándar de decir "deshaz el commit más reciente".

bash
$ git log --oneline
8f3c7d1 Cache forecast responses in localStorage
4f2a1c9 Fix temperature conversion in Celsius-to-Fahrenheit formula
5f3d8b2 Add five-day forecast to the dashboard
1a2b3c4 Set up project structure

$ git reset --soft HEAD~1

--soft mueve el puntero de la rama un commit hacia atrás y se detiene ahí. El área de staging y el directorio de trabajo quedan intactos, así que todo lo del commit deshecho queda en staging, listo para que lo vuelvas a committear como quieras.

bash
$ git reset --mixed HEAD~1

--mixed es lo que se ejecuta si no pones ninguna opción. Mueve el puntero de la rama hacia atrás y además vacía el área de staging, así que los cambios del commit deshecho quedan como ediciones sin staging en tu directorio de trabajo. No se pierde nada, solo vuelve a ejecutar git add antes de committear.

bash
$ git reset --hard HEAD~1

git reset --hard mueve el puntero de la rama hacia atrás y sobrescribe tu directorio de trabajo para que coincida, así que cualquier trabajo sin commit y el commit deshecho desaparecen de la vista en un solo paso, sin ninguna pregunta de confirmación. Este es el modo con el que hay que detenerse a pensar antes de ejecutar.

Esto también explica qué estaba haciendo git commit --amend allá en el ciclo del commit: amend es parecido a un reset --soft HEAD~1 seguido inmediatamente de un commit nuevo, y por eso reemplaza tu último commit en vez de apilarse encima de él.

Reset solo mueve punteros y, en modo --hard, también el directorio de trabajo para que coincida. Nunca elimina un objeto commit directamente. El commit que un reset --hard dejó atrás sigue en la base de datos de objetos de Git, inalcanzable desde cualquier rama, lo cual importa porque hay una manera de volver a alcanzarlo: git reflog, lo que viene a continuación. En una rama compartida, sin embargo, mover tu rama hacia atrás y pushearla significa forzar el push sobre commits que tus compañeros de equipo ya tienen, así que su próximo pull no coincide con el historial reescrito. Esa es la razón práctica por la que reset se mantiene como una herramienta de limpieza local mientras que revert cubre todo lo que ya salió de tu máquina.

JunoRetroceder en el tiempo con git reset No vas a usar 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.
JunoRetroceder en el tiempo con git resetgit reset --soft mantiene en staging los cambios del commit deshecho, --mixed (el modo por defecto) los saca de staging pero conserva las ediciones, --hard descarta las ediciones por completo. commit --amend es básicamente un reset soft seguido inmediatamente de un commit nuevo, y por eso reemplaza el último commit en vez de apilarse encima de él.
JunoRetroceder en el tiempo con git reset Reset solo mueve punteros, y con --hard, también tu directorio de trabajo para que coincida, pero nunca elimina el objeto commit en sí. Ese objeto permanece en la base de datos hasta que el reflog y el garbage collection lo alcancen. Pushea una rama reescrita y el próximo pull de tus compañeros de equipo choca con un historial que ya no coincide con el suyo.

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.

"¿Alguien lo bajó con pull?" en realidad significa "¿este commit es alcanzable desde una referencia que otra persona tiene?", lo cual en la práctica significa si está en una rama que ya se pusheó. Un commit en una rama local que inventaste hace cinco minutos y nunca pusheaste es territorio libre para reset. En el momento en que lo pusheas, trátalo como compartido: resetearlo localmente y forzar el push significa coordinarse primero con quien más toque esa rama.

JunoElegir entre revert o reset Hazte una pregunta antes de deshacer un commit: ¿alguien más ya lo bajó con pull? Si sí, usa revert, siempre es seguro. Si el commit solo existe en tu máquina, reset también funciona bien. Cuando no estés seguro de cuál aplica, revert es la opción más segura por defecto.
JunoElegir entre revert o reset Una vez que un commit está pusheado y cualquiera podría tenerlo, usa revert. Mientras siga siendo puramente local para ti, reset funciona bien y muchas veces es la solución más prolija, porque no deja una entrada tipo "revert de un revert" dando vueltas en el log.
JunoElegir entre revert o reset La prueba real es la alcanzabilidad: ¿este commit está en una referencia que alguien más ya bajó con pull? Local y sin pushear, reset es territorio libre. Pusheado y compartido, trata resetearlo como algo que hay que coordinar primero, porque un force-push posterior choca con lo que tus compañeros de equipo ya tienen.

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.

La versión corta: git reflog lista las posiciones recientes de HEAD, cada una con un hash corto, y puedes hacer git reset --hard a cualquiera de esos hashes para volver exactamente a ese estado. Se lee como una segunda historia: cada lugar donde HEAD se ha ubicado, incluyendo commits a los que tus ramas ya no apuntan.

Un commit al que ya no apunta ninguna rama, etiqueta ni HEAD es inalcanzable: flotando en el repositorio, todavía guardado, solo sin ninguna etiqueta que lleve de vuelta a él. El reflog es una lista cronológica de todos los lugares donde HEAD se movió en tu máquina, este cambio de rama, ese reset, ese rebase, cada entrada identificada por una referencia corta como HEAD@{2}. Encuentra la entrada justo antes del error y apunta una rama de vuelta hacia ella:

bash
$ git reflog
2b6f0d1 HEAD@{0}: reset: moving to HEAD~1
8f3c7d1 HEAD@{1}: commit: Cache forecast responses in localStorage
4f2a1c9 HEAD@{2}: commit: Fix temperature conversion in Celsius-to-Fahrenheit formula

$ git reset --hard 8f3c7d1

Ese es el commit que reset --hard parecía haber borrado, ya restaurado. Vale la pena conocer dos límites. Primero, el reflog vive solo en tu propia máquina, pushear y bajar cambios con pull nunca lo tocan, así que no puede recuperar un commit que un compañero de equipo perdió en la suya. Segundo, no dura para siempre: Git eventualmente ejecuta el garbage collection y elimina los commits inalcanzables una vez que superan un límite por defecto de unas pocas semanas, así que esta red de seguridad cubre errores recientes y no toda la vida del proyecto.

JunoRecuperar un commit perdido Si en algún momento ejecutas 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í.
JunoRecuperar un commit perdidogit reflog lista las posiciones recientes de HEAD con hashes cortos a los que puedes volver con reset --hard, así que un reset que se pasó de la raya suele ser recuperable. Sin embargo, vive solo en tu propia máquina, así que no puede rescatar un commit que un compañero de equipo perdió en la suya.
JunoRecuperar un commit perdido El reflog es un registro local y cronológico de todos los lugares donde ha estado HEAD, y así es como un commit que reset --hard parecía haber borrado vuelve a aparecer apuntando una rama a la entrada correcta de HEAD@{n}. Con el tiempo se va con el garbage collection, y solo cubre tus propios errores recientes. Un compañero de equipo que perdió un commit necesita ejecutar la misma recuperación en su propia máquina.